diff --git a/contract.go b/contract.go index 646281a..29c2617 100644 --- a/contract.go +++ b/contract.go @@ -26,6 +26,17 @@ type Contract interface { BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error) } +// ContractReporter is implemented by Commands that know which Contract +// discovered them. Reflect on a discovered Command to learn its contract: +// +// if r, ok := cmd.(ContractReporter); ok { +// fmt.Println(r.Contract()) // e.g. "OpenCLI" +// } +type ContractReporter interface { + // Contract returns the name of the Contract that built the Command. + Contract() string +} + // ErrNotApplicable indicates that a contract does not apply to a given file/directory. // Discovery will try the next contract in the list. var ErrNotApplicable = errors.New("contract does not apply") diff --git a/contract_directory.go b/contract_directory.go index 6e60c7d..45fd752 100644 --- a/contract_directory.go +++ b/contract_directory.go @@ -40,6 +40,7 @@ func (c *DirectoryContract) BuildCommand(path string, info fs.DirEntry, parent C discoveredIn: filepath.Dir(path), executor: d.Executor(), cache: d.Cache(), + contract: "Directory", }, discoverer: d.Next(), }, nil diff --git a/contract_executable.go b/contract_executable.go index 652aebe..ecd2d28 100644 --- a/contract_executable.go +++ b/contract_executable.go @@ -45,6 +45,7 @@ func (c *ExecutableContract) BuildCommand(path string, info fs.DirEntry, parent discoveredIn: filepath.Dir(path), executor: d.Executor(), cache: d.Cache(), + contract: "Executable", }, nil } @@ -56,5 +57,6 @@ func (c *ExecutableContract) BuildCommand(path string, info fs.DirEntry, parent executor: d.Executor(), cache: d.Cache(), discoverer: d.Next(), + contract: "Executable", }, nil } diff --git a/contract_opencli.go b/contract_opencli.go index 6684e87..558eeac 100644 --- a/contract_opencli.go +++ b/contract_opencli.go @@ -39,6 +39,7 @@ func (c *OpenCLIContract) BuildCommand(path string, info fs.DirEntry, parent Com discoveredIn: filepath.Dir(path), executor: d.Executor(), cache: d.Cache(), + contract: "OpenCLI", }, nil } @@ -51,6 +52,7 @@ func (c *OpenCLIContract) BuildCommand(path string, info fs.DirEntry, parent Com cache: d.Cache(), discoverer: d.Next(), describe: describeOpenCLI, + contract: "OpenCLI", }, nil } diff --git a/contract_shell_script.go b/contract_shell_script.go index 677b27a..e206bbb 100644 --- a/contract_shell_script.go +++ b/contract_shell_script.go @@ -48,6 +48,7 @@ func (c *ShellScriptContract) BuildCommand(path string, info fs.DirEntry, parent discoveredIn: filepath.Dir(path), executor: d.Executor(), cache: d.Cache(), + contract: "ShellScript", }, }, nil } diff --git a/contract_standalone_executable.go b/contract_standalone_executable.go index d9afe07..055d203 100644 --- a/contract_standalone_executable.go +++ b/contract_standalone_executable.go @@ -31,6 +31,7 @@ func (c *StandaloneExecutableContract) BuildCommand(path string, info fs.DirEntr discoveredIn: filepath.Dir(path), executor: d.Executor(), cache: d.Cache(), + contract: "StandaloneExecutable", } // Only applies to executables that define a summary diff --git a/contract_test.go b/contract_test.go index 43402f4..4d44cc0 100644 --- a/contract_test.go +++ b/contract_test.go @@ -112,6 +112,37 @@ func TestDefaultCommandFromDescriptor(t *testing.T) { assert.Equal(t, "b", parent.DefaultSubcommand().Name()) } +// TestContractReporter verifies that a Command built by a Contract reports the +// name of that Contract via the ContractReporter interface. +func TestContractReporter(t *testing.T) { + d := &discoverer{maxDepth: -1, executor: defaultExecutor, cache: nullCache{}} + + scenarios := []struct { + contract Contract + executable string + expected string + }{ + {&OpenCLIContract{}, "opencli-tool", "OpenCLI"}, + {&ShellScriptContract{}, "echoargs", "ShellScript"}, + {&ExecutableContract{}, "go.exoskeleton", "Executable"}, + {&StandaloneExecutableContract{}, "hello", "StandaloneExecutable"}, + {&DirectoryContract{MetadataFilename: ".exoskeleton"}, "nested-1", "Directory"}, + } + + for _, s := range scenarios { + path := filepath.Join(fixtures, s.executable) + info, err := os.Lstat(path) + assert.NoErrorf(t, err, "Given executable=%s", s.executable) + + cmd, err := s.contract.BuildCommand(path, fs.FileInfoToDirEntry(info), nil, d) + assert.NoErrorf(t, err, "Given executable=%s", s.executable) + + reporter, ok := cmd.(ContractReporter) + assert.Truef(t, ok, "%s should implement ContractReporter", s.executable) + assert.Equalf(t, s.expected, reporter.Contract(), "Given executable=%s", s.executable) + } +} + // TestWithContractsOption verifies that WithContracts replaces the contracts. func TestWithContractsOption(t *testing.T) { // Create a custom contract that only matches files named "custom" diff --git a/discovery_test.go b/discovery_test.go index f84d5aa..068406e 100644 --- a/discovery_test.go +++ b/discovery_test.go @@ -132,6 +132,7 @@ func TestDiscovererBuildsCommand(t *testing.T) { path: filepath.Join(fixtures, "echoargs"), discoveredIn: fixtures, cache: nullCache{}, + contract: "ShellScript", }, }, }, @@ -144,6 +145,7 @@ func TestDiscovererBuildsCommand(t *testing.T) { path: filepath.Join(fixtures, "nested-1", ".exoskeleton"), discoveredIn: fixtures, cache: nullCache{}, + contract: "Directory", }, discoverer: d.Next(), }, @@ -157,6 +159,7 @@ func TestDiscovererBuildsCommand(t *testing.T) { discoveredIn: fixtures, cache: nullCache{}, discoverer: d.Next(), + contract: "Executable", }, }, } diff --git a/executable_command.go b/executable_command.go index c26ff19..b1e10cd 100644 --- a/executable_command.go +++ b/executable_command.go @@ -28,6 +28,7 @@ type executableCommand struct { discoverer DiscoveryContext cache Cache describe describeFunc + contract string } func (cmd *executableCommand) Parent() Command { return cmd.parent } @@ -35,6 +36,7 @@ func (cmd *executableCommand) Path() string { return cmd.path } func (cmd *executableCommand) Name() string { return cmd.name } func (cmd *executableCommand) Aliases() []string { return cmd.aliases } func (cmd *executableCommand) DiscoveredIn() string { return cmd.discoveredIn } +func (cmd *executableCommand) Contract() string { return cmd.contract } // Command returns an exec.Cmd that will run the executable with the given arguments. func (cmd *executableCommand) Command(args ...string) *exec.Cmd { @@ -188,6 +190,7 @@ func toCommands(parent *executableCommand, descriptors []*commandDescriptor, arg defaultSubcommand: descriptor.DefaultCommand, executor: parent.executor, cache: parent.cache, + contract: parent.contract, } if len(descriptor.Commands) > 0 && d.MaxDepth() != 0 {