From 998a6e639ff2773dc13e4160b7e8028ebf68e054 Mon Sep 17 00:00:00 2001 From: Marcin Bilski Date: Sat, 23 Oct 2021 23:05:11 +0200 Subject: [PATCH] Experimental way to expose tasks, adding them to global scope. --- features/expose.feature | 70 +++++++++++++++++++++++++++++++++++++++++ pkg/oyafile/imports.go | 11 ++++++- pkg/oyafile/oyafile.go | 23 ++++++++------ pkg/oyafile/parsing.go | 13 ++++++++ pkg/task/mocks.go | 13 ++++++++ pkg/task/name.go | 11 +++++++ pkg/task/name_test.go | 23 ++++++++++++++ pkg/task/reference.go | 21 +++++++++++++ pkg/task/table.go | 25 ++++++++++++--- pkg/task/table_test.go | 24 ++++++++++++++ todo.org | 5 +++ 11 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 features/expose.feature create mode 100644 pkg/task/mocks.go create mode 100644 pkg/task/reference.go create mode 100644 pkg/task/table_test.go diff --git a/features/expose.feature b/features/expose.feature new file mode 100644 index 00000000..6a2f8f4f --- /dev/null +++ b/features/expose.feature @@ -0,0 +1,70 @@ +Feature: Exposing imported package tasks so they can be invoked without the alias. + +Background: + Given I'm in project dir + + +Scenario: Expose tasks + Given file ./Oyafile containing + """ + Project: main + """ + And file ./project1/Oyafile containing + """ + Values: + foo: project1 + + echo: | + echo "project1" + """ + And file ./project2/Oyafile containing + """ + Import: + p: /project1 + + Expose: p + """ + And I'm in the ./project2 dir + When I run "oya run echo" + Then the command succeeds + And the command outputs + """ + project1 + + """ + +@current +Scenario: Never overwrite existing an task when exposing tasks + Given file ./Oyafile containing + """ + Project: main + """ + And file ./project1/Oyafile containing + """ + Values: + foo: project1 + + echo: | + echo "project1" + """ + And file ./project2/Oyafile containing + """ + Import: + p: /project1 + + Expose: p + + echo: | + echo "project2" + """ + And I'm in the ./project2 dir + When I run "oya run echo" + Then the command succeeds + And the command outputs + """ + project2 + + """ + + +# TODO: Show as aliases when listing tasks. diff --git a/pkg/oyafile/imports.go b/pkg/oyafile/imports.go index a186895e..ad74e200 100644 --- a/pkg/oyafile/imports.go +++ b/pkg/oyafile/imports.go @@ -36,11 +36,13 @@ func (oyafile *Oyafile) resolveImports(loader PackLoader) error { if err != nil { return err } + + // TODO(bilus): Extract function. err = oyafile.Values.UpdateScopeAt(string(alias), func(scope template.Scope) template.Scope { // Values in the main Oyafile overwrite values in the pack Oyafile. merged := packOyafile.Values.Merge(scope) - // Task is keeping a pointed to the scope. + // Task is keeping a pointer to the scope. packOyafile.Values.Replace(merged) return merged }, false) @@ -51,6 +53,13 @@ func (oyafile *Oyafile) resolveImports(loader PackLoader) error { oyafile.Tasks.ImportTasks(alias, packOyafile.Tasks) } + return oyafile.expose() +} + +func (oyafile *Oyafile) expose() error { + for _, alias := range oyafile.ExposedAliases { + oyafile.Tasks.Expose(alias) + } return nil } diff --git a/pkg/oyafile/oyafile.go b/pkg/oyafile/oyafile.go index f8ccbdf4..efc40ac9 100644 --- a/pkg/oyafile/oyafile.go +++ b/pkg/oyafile/oyafile.go @@ -27,16 +27,19 @@ type PackReference struct { type PackReplacements map[types.ImportPath]string type Oyafile struct { - Dir string - Path string - RootDir string - Shell string - Imports map[types.Alias]types.ImportPath - Tasks task.Table - Values template.Scope - Project string // Project is set for root Oyafile. - Ignore []string // Ignore contains directory exclusion rules. - Requires []PackReference + Dir string + Path string + RootDir string + Shell string + Imports map[types.Alias]types.ImportPath + Tasks task.Table + // ExposedAliases contains a list of imported packs' aliases that will + // be exposed to global scope. + ExposedAliases []types.Alias + Values template.Scope + Project string // Project is set for root Oyafile. + Ignore []string // Ignore contains directory exclusion rules. + Requires []PackReference // Replacements map packs to local paths relative to project root directory for development based on the Replace: directive. Replacements PackReplacements IsBuilt bool diff --git a/pkg/oyafile/parsing.go b/pkg/oyafile/parsing.go index dfe29fe5..777ef8e5 100644 --- a/pkg/oyafile/parsing.go +++ b/pkg/oyafile/parsing.go @@ -13,6 +13,7 @@ import ( ) const StmtImport = "Import" +const StmtExpose = "Expose" const StmtValues = "Values" const StmtProject = "Project" const StmtIgnore = "Ignore" @@ -80,6 +81,8 @@ func parseStatement(name string, value interface{}, o *Oyafile) error { return parseRequire(value, o) case StmtReplace: return parseReplace(value, o) + case StmtExpose: + return parseExpose(value, o) default: taskName := task.Name(name) @@ -244,3 +247,13 @@ func parseReplace(value interface{}, o *Oyafile) error { return nil } + +func parseExpose(value interface{}, o *Oyafile) error { + aliasStr, ok := value.(string) + if !ok { + return fmt.Errorf("expected an import alias") + } + alias := types.Alias(aliasStr) + o.ExposedAliases = []types.Alias{alias} + return nil +} diff --git a/pkg/task/mocks.go b/pkg/task/mocks.go new file mode 100644 index 00000000..3a89b6f7 --- /dev/null +++ b/pkg/task/mocks.go @@ -0,0 +1,13 @@ +package task + +import ( + "io" + + "github.com/tooploox/oya/pkg/template" +) + +type MockTask struct{} + +func (MockTask) Exec(workDir string, args []string, scope template.Scope, stdout, stderr io.Writer) error { + return nil +} diff --git a/pkg/task/name.go b/pkg/task/name.go index d783eff5..aa131965 100644 --- a/pkg/task/name.go +++ b/pkg/task/name.go @@ -23,6 +23,17 @@ func (n Name) Split() (types.Alias, string) { } } +func (n Name) IsAliased(alias types.Alias) bool { + // TODO(bilus): Turn Name into a a struct. + actualAlias, _ := n.Split() + return actualAlias == alias +} + +func (n Name) Unaliased() Name { + _, s := n.Split() + return Name(s) +} + func (n Name) IsBuiltIn() bool { firstChar := string(n)[0:1] return firstChar == strings.ToUpper(firstChar) diff --git a/pkg/task/name_test.go b/pkg/task/name_test.go index e7afbe94..a2c4f347 100644 --- a/pkg/task/name_test.go +++ b/pkg/task/name_test.go @@ -28,3 +28,26 @@ func TestName_Split_WithNestedAlias(t *testing.T) { tu.AssertEqual(t, types.Alias("foo.bar"), alias) tu.AssertEqual(t, "zoo", task) } + +func TestName_IsAliased(t *testing.T) { + testCases := []struct { + name task.Name + alias types.Alias + isAliased bool + }{ + {task.Name("name"), types.Alias("alias"), false}, + {task.Name("name").Aliased("alias"), types.Alias("alias"), true}, + {task.Name("name").Aliased("otherAlias"), types.Alias("alias"), false}, + } + + for _, tc := range testCases { + tu.AssertEqual(t, tc.isAliased, tc.name.IsAliased(tc.alias)) + } +} + +func TestName_Unaliased(t *testing.T) { + globalName := task.Name("name") + aliasedName := globalName.Aliased(types.Alias("alias")) + tu.AssertEqual(t, globalName, aliasedName.Unaliased()) + tu.AssertEqual(t, globalName, globalName.Unaliased()) +} diff --git a/pkg/task/reference.go b/pkg/task/reference.go new file mode 100644 index 00000000..320b5bf5 --- /dev/null +++ b/pkg/task/reference.go @@ -0,0 +1,21 @@ +package task + +import ( + "io" + + "github.com/tooploox/oya/pkg/template" +) + +type reference struct { + task Task +} + +func NewReference(task Task) Task { + return reference{ + task: task, + } +} + +func (r reference) Exec(workDir string, args []string, scope template.Scope, stdout, stderr io.Writer) error { + return r.task.Exec(workDir, args, scope, stdout, stderr) +} diff --git a/pkg/task/table.go b/pkg/task/table.go index 0adbd8c0..6b6237dc 100644 --- a/pkg/task/table.go +++ b/pkg/task/table.go @@ -1,8 +1,6 @@ package task import ( - "fmt" - "github.com/tooploox/oya/pkg/types" ) @@ -34,12 +32,31 @@ func (tt Table) AddDoc(taskName Name, s string) { } func (tt Table) ImportTasks(alias types.Alias, other Table) { - for key, t := range other.tasks { + for name, t := range other.tasks { // TODO: Detect if task already set. - tt.AddTask(Name(fmt.Sprintf("%v.%v", alias, key)), t) + tt.AddTask(Name(name).Aliased(alias), t) + } +} + +// Expose copies tasks under an alias to global scope (without the alias) +// never overriding the existing global tasks. +func (tt Table) Expose(alias types.Alias) { + for name, task := range tt.tasks { + if name.IsAliased(alias) { + globalName := name.Unaliased() + _, ok := tt.LookupTask(globalName) + if !ok { + tt.addTaskWithMeta(globalName, task, tt.meta[name]) + } + } } } +func (tt Table) addTaskWithMeta(name Name, task Task, meta Meta) { + tt.tasks[name] = task + tt.meta[name] = meta +} + func (tt Table) ForEach(f func(taskName Name, task Task, meta Meta) error) error { for taskName, task := range tt.tasks { meta := tt.meta[taskName] diff --git a/pkg/task/table_test.go b/pkg/task/table_test.go new file mode 100644 index 00000000..5591386e --- /dev/null +++ b/pkg/task/table_test.go @@ -0,0 +1,24 @@ +package task_test + +import ( + "testing" + + "github.com/tooploox/oya/pkg/task" + "github.com/tooploox/oya/pkg/types" + tu "github.com/tooploox/oya/testutil" +) + +func TestTable_Expose(t *testing.T) { + tt := task.NewTable() + + globalName := task.Name("task") + aliasedName := globalName.Aliased("alias") + tt.AddTask(aliasedName, task.MockTask{}) + + tt.Expose(types.Alias("alias")) + + _, ok := tt.LookupTask(aliasedName) + tu.AssertTrue(t, ok, "Aliased task not found") + _, ok = tt.LookupTask(globalName) + tu.AssertTrue(t, ok, "Global task not found") +} diff --git a/todo.org b/todo.org index 81231d84..ab4a7b20 100644 --- a/todo.org +++ b/todo.org @@ -59,6 +59,11 @@ - State "CANCELLED" from "TODO" [2019-01-25 Fri 10:34] \\ It's enough to have "install" task by convention in packs and then oya tasks will show it. :END: +* TODO Experiment: exposing tasks +** TODO PoC +** TODO Flag for go import +** TODO Multiple packages? +** TODO Transient exposure (package exposing another package) * TODO Vendoring is only partially implemented ** TODO Simplify oya get/vendor (based on Import statements) TBD **** Just use Import