diff --git a/marketplace/indexer.go b/marketplace/indexer.go new file mode 100644 index 0000000..2af7328 --- /dev/null +++ b/marketplace/indexer.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package marketplace + +import ( + "sort" + + "dappco.re/go/core/io" + "dappco.re/go/core/scm/manifest" +) + +// IndexOptions controls how BuildIndex populates marketplace metadata. +// Usage: IndexOptions{...} +type IndexOptions struct { + // Org is the default organisation used when constructing repo URLs. + Org string + + // ForgeURL is the base URL used when constructing repo URLs. + ForgeURL string + + // CategoryFn assigns a category to a module code. + CategoryFn func(code string) string +} + +// BuildIndex reads .core/manifest.yaml from each repository root and produces +// a marketplace index. Repositories without a manifest are skipped silently. +// Categories are deduplicated and sorted. +// Usage: BuildIndex(...) +func BuildIndex(medium io.Medium, repoPaths []string, opts IndexOptions) (*Index, error) { + idx := &Index{ + Version: IndexVersion, + } + + seen := make(map[string]bool) + categories := make(map[string]bool) + + for _, repoPath := range repoPaths { + m, err := manifest.Load(medium, repoPath) + if err != nil { + continue + } + if m == nil || m.Code == "" { + continue + } + if seen[m.Code] { + continue + } + seen[m.Code] = true + + module := Module{ + Code: m.Code, + Name: m.Name, + } + if opts.ForgeURL != "" && opts.Org != "" { + module.Repo = opts.ForgeURL + "/" + opts.Org + "/" + m.Code + } + if opts.CategoryFn != nil { + module.Category = opts.CategoryFn(m.Code) + } + if module.Category != "" { + categories[module.Category] = true + } + + idx.Modules = append(idx.Modules, module) + } + + sort.Slice(idx.Modules, func(i, j int) bool { + return idx.Modules[i].Code < idx.Modules[j].Code + }) + + for category := range categories { + idx.Categories = append(idx.Categories, category) + } + sort.Strings(idx.Categories) + + return idx, nil +} diff --git a/marketplace/indexer_test.go b/marketplace/indexer_test.go new file mode 100644 index 0000000..830c71b --- /dev/null +++ b/marketplace/indexer_test.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package marketplace + +import ( + "testing" + + "dappco.re/go/core/io" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildIndex_Good_CategoriesAndRepoURLs_Good(t *testing.T) { + medium := io.NewMockMedium() + + require.NoError(t, medium.Write("/repos/a/.core/manifest.yaml", ` +code: a +name: Alpha +version: 1.0.0 +`)) + require.NoError(t, medium.Write("/repos/b/.core/manifest.yaml", ` +code: b +name: Beta +version: 1.0.0 +`)) + require.NoError(t, medium.Write("/repos/c/.core/manifest.yaml", ` +code: c +name: Gamma +version: 1.0.0 +`)) + + idx, err := BuildIndex(medium, []string{"/repos/a", "/repos/b", "/repos/c"}, IndexOptions{ + ForgeURL: "https://forge.example.com", + Org: "core", + CategoryFn: func(code string) string { + switch code { + case "a", "b": + return "tools" + default: + return "products" + } + }, + }) + require.NoError(t, err) + + require.Len(t, idx.Modules, 3) + assert.Equal(t, "a", idx.Modules[0].Code) + assert.Equal(t, "https://forge.example.com/core/a", idx.Modules[0].Repo) + assert.Equal(t, "tools", idx.Modules[0].Category) + assert.Equal(t, []string{"products", "tools"}, idx.Categories) +} + +func TestBuildIndex_Good_SkipsMissingManifest_Good(t *testing.T) { + medium := io.NewMockMedium() + + require.NoError(t, medium.Write("/repos/one/.core/manifest.yaml", ` +code: one +name: One +version: 1.0.0 +`)) + + idx, err := BuildIndex(medium, []string{"/repos/one", "/repos/missing"}, IndexOptions{}) + require.NoError(t, err) + + require.Len(t, idx.Modules, 1) + assert.Equal(t, "one", idx.Modules[0].Code) + assert.Empty(t, idx.Categories) +}