diff --git a/cmd/dev/cmd_api_testgen.go b/cmd/dev/cmd_api_testgen.go index 62e35e9..29267f7 100644 --- a/cmd/dev/cmd_api_testgen.go +++ b/cmd/dev/cmd_api_testgen.go @@ -40,15 +40,15 @@ func runTestGen() error { } serviceName := dir.Name() - internalFile := filepath.Join(pkgDir, serviceName, serviceName+".go") + internalDir := filepath.Join(pkgDir, serviceName) publicDir := serviceName publicTestFile := filepath.Join(publicDir, serviceName+"_test.go") - if !coreio.Local.IsFile(internalFile) { + if !coreio.Local.Exists(internalDir) { continue } - symbols, err := getExportedSymbols(internalFile) + symbols, err := getExportedSymbols(internalDir) if err != nil { return cli.Wrap(err, cli.Sprintf("error getting symbols for service '%s'", serviceName)) } diff --git a/cmd/dev/cmd_api_testgen_test.go b/cmd/dev/cmd_api_testgen_test.go index 4ee50c5..536f57e 100644 --- a/cmd/dev/cmd_api_testgen_test.go +++ b/cmd/dev/cmd_api_testgen_test.go @@ -32,6 +32,16 @@ const Answer = 42 var Value = Example{} func Run() {} +`)) + require.NoError(t, io.Local.Write(filepath.Join(serviceDir, "extra.go"), `package demo + +type Another struct{} + +func Extra() {} +`)) + require.NoError(t, io.Local.Write(filepath.Join(serviceDir, "demo_test.go"), `package demo + +func Ignored() {} `)) require.NoError(t, runTestGen()) @@ -44,9 +54,12 @@ func Run() {} require.Contains(t, content, `package demo`) require.Contains(t, content, `impl "forge.lthn.ai/core/cli/demo"`) require.Contains(t, content, `type _ = impl.Example`) + require.Contains(t, content, `type _ = impl.Another`) require.Contains(t, content, `const _ = impl.Answer`) require.Contains(t, content, `var _ = impl.Value`) require.Contains(t, content, `var _ = impl.Run`) + require.Contains(t, content, `var _ = impl.Extra`) + require.NotContains(t, content, `Ignored`) } func TestGeneratePublicAPITestFile_Good(t *testing.T) { @@ -68,3 +81,35 @@ func TestGeneratePublicAPITestFile_Good(t *testing.T) { require.True(t, strings.Contains(content, `type _ = impl.Example`)) require.True(t, strings.Contains(content, `const _ = impl.Answer`)) } + +func TestGetExportedSymbols_Good_MultiFile(t *testing.T) { + tmpDir := t.TempDir() + + serviceDir := filepath.Join(tmpDir, "demo") + require.NoError(t, io.Local.EnsureDir(serviceDir)) + require.NoError(t, io.Local.Write(filepath.Join(serviceDir, "demo.go"), `package demo + +type Example struct{} + +const Answer = 42 +`)) + require.NoError(t, io.Local.Write(filepath.Join(serviceDir, "extra.go"), `package demo + +var Value = Example{} + +func Run() {} +`)) + require.NoError(t, io.Local.Write(filepath.Join(serviceDir, "demo_test.go"), `package demo + +type Ignored struct{} +`)) + + symbols, err := getExportedSymbols(serviceDir) + require.NoError(t, err) + require.Equal(t, []symbolInfo{ + {Name: "Answer", Kind: "const"}, + {Name: "Example", Kind: "type"}, + {Name: "Run", Kind: "func"}, + {Name: "Value", Kind: "var"}, + }, symbols) +} diff --git a/cmd/dev/cmd_sync.go b/cmd/dev/cmd_sync.go index 49433ab..f7b2894 100644 --- a/cmd/dev/cmd_sync.go +++ b/cmd/dev/cmd_sync.go @@ -6,12 +6,15 @@ import ( "go/parser" "go/token" "path/filepath" + "sort" + "strings" "text/template" - "forge.lthn.ai/core/cli/pkg/cli" // Added - "dappco.re/go/core/i18n" // Added + "dappco.re/go/core/i18n" coreio "dappco.re/go/core/io" - // Added + + "forge.lthn.ai/core/cli/pkg/cli" + "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -52,15 +55,15 @@ func runSync() error { } serviceName := dir.Name() - internalFile := filepath.Join(pkgDir, serviceName, serviceName+".go") + internalDir := filepath.Join(pkgDir, serviceName) publicDir := serviceName publicFile := filepath.Join(publicDir, serviceName+".go") - if !coreio.Local.IsFile(internalFile) { + if !coreio.Local.Exists(internalDir) { continue } - symbols, err := getExportedSymbols(internalFile) + symbols, err := getExportedSymbols(internalDir) if err != nil { return cli.Wrap(err, cli.Sprintf("error getting symbols for service '%s'", serviceName)) } @@ -74,23 +77,29 @@ func runSync() error { } func getExportedSymbols(path string) ([]symbolInfo, error) { - // ParseFile expects a filename/path and reads it using os.Open by default if content is nil. - // Since we want to use our Medium abstraction, we should read the file content first. - content, err := coreio.Local.Read(path) + files, err := listGoFiles(path) if err != nil { return nil, err } - fset := token.NewFileSet() - // ParseFile can take content as string (src argument). - node, err := parser.ParseFile(fset, path, content, parser.ParseComments) - if err != nil { - return nil, err - } + symbolsByName := make(map[string]symbolInfo) + for _, file := range files { + content, err := coreio.Local.Read(file) + if err != nil { + return nil, err + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, file, content, parser.ParseComments) + if err != nil { + return nil, err + } + + for name, obj := range node.Scope.Objects { + if !ast.IsExported(name) { + continue + } - var symbols []symbolInfo - for name, obj := range node.Scope.Objects { - if ast.IsExported(name) { kind := "unknown" switch obj.Kind { case ast.Con: @@ -102,14 +111,59 @@ func getExportedSymbols(path string) ([]symbolInfo, error) { case ast.Typ: kind = "type" } - if kind != "unknown" { - symbols = append(symbols, symbolInfo{Name: name, Kind: kind}) + + if kind == "unknown" { + continue + } + + if _, exists := symbolsByName[name]; !exists { + symbolsByName[name] = symbolInfo{Name: name, Kind: kind} } } } + + symbols := make([]symbolInfo, 0, len(symbolsByName)) + for _, symbol := range symbolsByName { + symbols = append(symbols, symbol) + } + + sort.Slice(symbols, func(i, j int) bool { + if symbols[i].Name == symbols[j].Name { + return symbols[i].Kind < symbols[j].Kind + } + return symbols[i].Name < symbols[j].Name + }) + return symbols, nil } +func listGoFiles(path string) ([]string, error) { + entries, err := coreio.Local.List(path) + if err == nil { + files := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := entry.Name() + if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + + files = append(files, filepath.Join(path, name)) + } + sort.Strings(files) + return files, nil + } + + if coreio.Local.IsFile(path) { + return []string{path}, nil + } + + return nil, err +} + const publicAPITemplate = `// package {{.ServiceName}} provides the public API for the {{.ServiceName}} service. package {{.ServiceName}}