diff --git a/config/config.go b/config/config.go index b5476aa..12b6e42 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,9 @@ import ( // to the underlying implementation, making it transparent to the user. type Service = impl.Service +// Options is the public type for the Config service options. +type Options = impl.Options + // New is the public constructor for the Config service. It is a variable // that points to the real constructor in the implementation package. var New = impl.New diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..fd0c648 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,62 @@ +package config_test + +import ( + "testing" + + "github.com/Snider/Core/config" + "github.com/Snider/Core/pkg/core" +) + +func TestInterfaceCompliance(t *testing.T) { + var _ config.Config = (*config.Service)(nil) +} + +func TestRegister(t *testing.T) { + if config.Register == nil { + t.Fatal("config.Register factory is nil") + } +} + +// TestGet_NonExistentKey validates that getting a non-existent key returns an error. +func TestGet_NonExistentKey(t *testing.T) { + coreImpl, err := core.New(core.WithService(config.Register)) + if err != nil { + t.Fatalf("core.New() failed: %v", err) + } + + var value string + err = coreImpl.Config().Get("nonexistent.key", &value) + if err == nil { + t.Fatal("expected an error when getting a nonexistent key, but got nil") + } +} + +// TestSetAndGet verifies that a value can be set and then retrieved correctly. +func TestSetAndGet(t *testing.T) { + coreImpl, err := core.New(core.WithService(config.Register)) + if err != nil { + t.Fatalf("core.New() failed: %v", err) + } + + cfg := coreImpl.Config() + + // 1. Set a value for an existing key + key := "language" + expectedValue := "fr" + err = cfg.Set(key, expectedValue) + if err != nil { + t.Fatalf("Set(%q, %q) failed: %v", key, expectedValue, err) + } + + // 2. Get the value back + var actualValue string + err = cfg.Get(key, &actualValue) + if err != nil { + t.Fatalf("Get(%q) failed: %v", key, err) + } + + // 3. Compare the values + if actualValue != expectedValue { + t.Errorf("Get(%q) returned %q, want %q", key, actualValue, expectedValue) + } +} diff --git a/core_test.go b/core_test.go new file mode 100644 index 0000000..1560c5a --- /dev/null +++ b/core_test.go @@ -0,0 +1,125 @@ +package core_test + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +// TestPublicAPICompleteness dynamically discovers all public services and ensures +// their top-level API packages are in sync with their internal implementations. +func TestPublicAPICompleteness(t *testing.T) { + pkgDir := "pkg" + + // 1. Discover all potential service packages in the pkg/ directory. + internalDirs, err := os.ReadDir(pkgDir) + if err != nil { + t.Fatalf("Failed to read pkg directory: %v", err) + } + + var allMissingSymbols []string + + for _, dir := range internalDirs { + if !dir.IsDir() || dir.Name() == "core" { + continue // Skip files and the core package itself + } + + serviceName := dir.Name() + topLevelDir := serviceName + + // 2. Check if a corresponding top-level public API directory exists. + if _, err := os.Stat(topLevelDir); os.IsNotExist(err) { + continue // Not a public service, so we skip it. + } + + // 3. Define paths for public and internal Go files. + publicFile := filepath.Join(topLevelDir, serviceName+".go") + internalFile := filepath.Join(pkgDir, serviceName, serviceName+".go") + + // Ensure both files exist before trying to parse them. + if _, err := os.Stat(publicFile); os.IsNotExist(err) { + t.Logf("Skipping service '%s': public API file not found at %s", serviceName, publicFile) + continue + } + if _, err := os.Stat(internalFile); os.IsNotExist(err) { + t.Logf("Skipping service '%s': internal implementation file not found at %s", serviceName, internalFile) + continue + } + + // 4. Compare the exported symbols. + missing, err := compareExports(publicFile, internalFile) + if err != nil { + t.Errorf("Error comparing exports for service '%s': %v", serviceName, err) + continue + } + + if len(missing) > 0 { + msg := "- Service: " + serviceName + "\n - Missing: " + strings.Join(missing, ", ") + allMissingSymbols = append(allMissingSymbols, msg) + } + } + + // 5. Report all discrepancies at the end. + if len(allMissingSymbols) > 0 { + t.Errorf("Public APIs are out of sync with internal implementations:\n\n%s", + strings.Join(allMissingSymbols, "\n")) + } +} + +// compareExports takes two file paths, parses them, and returns a list of +// symbols that are exported in the internal file but not the public one. +func compareExports(publicFile, internalFile string) ([]string, error) { + publicAPI, err := getExportedSymbols(publicFile) + if err != nil { + return nil, err + } + + internalImpl, err := getExportedSymbols(internalFile) + if err != nil { + return nil, err + } + + publicSymbols := make(map[string]bool) + for _, sym := range publicAPI { + publicSymbols[sym] = true + } + + var missingSymbols []string + for _, internalSym := range internalImpl { + // The public API re-exports the interface from core, so we don't expect it here. + if internalSym == "Config" { + continue + } + if !publicSymbols[internalSym] { + missingSymbols = append(missingSymbols, internalSym) + } + } + + return missingSymbols, nil +} + +// getExportedSymbols parses a Go file and returns a slice of its exported symbol names. +func getExportedSymbols(path string) ([]string, error) { + absPath, err := filepath.Abs(path) + if err != nil { + return nil, err + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, absPath, nil, parser.AllErrors) + if err != nil { + return nil, err + } + + var symbols []string + for name, obj := range node.Scope.Objects { + if token.IsExported(name) { + symbols = append(symbols, obj.Name) + } + } + + return symbols, nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 09661c2..eba202e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -147,7 +147,7 @@ func (s *Service) Get(key string, out any) error { targetVal := outVal.Elem() srcVal := val.Field(i) - if !targetVal.Type().AssignableTo(srcVal.Type()) { + if !srcVal.Type().AssignableTo(targetVal.Type()) { return fmt.Errorf("cannot assign config value of type %s to output of type %s", srcVal.Type(), targetVal.Type()) } targetVal.Set(srcVal)