Skip to content

Commit 3ba482b

Browse files
committed
Handle non standard config objects
Signed-off-by: Pavol Loffay <[email protected]>
1 parent db85700 commit 3ba482b

File tree

5 files changed

+93
-15
lines changed

5 files changed

+93
-15
lines changed

cmd/mdatagen/internal/command.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,10 +500,41 @@ func generateSchema(md Metadata, ymlDir string) error {
500500
return fmt.Errorf("failed to create schemas directory: %w", err)
501501
}
502502

503+
// Parse config type specification
504+
pkgPath, typeName := parseConfigType(md.Schema.ConfigType)
505+
503506
// Create analyzer and generator
504507
analyzer := schemagen.NewPackageAnalyzer(ymlDir)
505508
generator := schemagen.NewSchemaGenerator(outputDir, analyzer)
506509

507-
// Generate schema (config type is auto-detected)
508-
return generator.GenerateSchema(md.Status.Class, md.Type, "")
510+
// Generate schema
511+
return generator.GenerateSchema(md.Status.Class, md.Type, typeName, pkgPath)
512+
}
513+
514+
// parseConfigType parses a config type specification into package path and type name.
515+
// Examples:
516+
// - "Config" -> ("", "Config")
517+
// - "go.opentelemetry.io/collector/pkg.Config" -> ("go.opentelemetry.io/collector/pkg", "Config")
518+
func parseConfigType(configType string) (pkgPath, typeName string) {
519+
if configType == "" {
520+
return "", ""
521+
}
522+
523+
// Find the last dot that separates package path from type name
524+
lastDot := strings.LastIndex(configType, ".")
525+
if lastDot == -1 {
526+
// No dot means it's just a type name in the local package
527+
return "", configType
528+
}
529+
530+
// Check if this looks like a package path (contains "/" before the last dot)
531+
potentialPkg := configType[:lastDot]
532+
if strings.Contains(potentialPkg, "/") {
533+
// It's a fully qualified type: pkg/path.TypeName
534+
return potentialPkg, configType[lastDot+1:]
535+
}
536+
537+
// No slash means it's just a type name (e.g., "Config" or possibly "pkg.Config" for local)
538+
// Treat as local type name
539+
return "", configType
509540
}

cmd/mdatagen/internal/schema.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,10 @@ package internal // import "go.opentelemetry.io/collector/cmd/mdatagen/internal"
77
type SchemaConfig struct {
88
// Enabled controls whether schema generation is enabled (default: false).
99
Enabled bool `mapstructure:"enabled"`
10+
11+
// ConfigType specifies the config type to generate schema for.
12+
// Can be a simple type name (e.g., "Config") for local package,
13+
// or a fully qualified type (e.g., "go.opentelemetry.io/collector/pkg.Config")
14+
// for external packages. If empty, auto-detection is used.
15+
ConfigType string `mapstructure:"config_type"`
1016
}

cmd/mdatagen/internal/schemagen/analyzer.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ func NewPackageAnalyzer(dir string) *PackageAnalyzer {
2929
}
3030

3131
// analyzeConfig loads the package and finds the Config struct.
32-
// If configTypeName is empty, it auto-detects by looking for component.Config implementation.
33-
func (a *PackageAnalyzer) analyzeConfig(configTypeName string) (*StructInfo, error) {
32+
// configTypeName is the name of the config type (e.g., "Config"). If empty, auto-detection is used.
33+
// configPkgPath is the package path where the config is defined. If empty, the local package is used.
34+
func (a *PackageAnalyzer) analyzeConfig(configTypeName, configPkgPath string) (*StructInfo, error) {
3435
cfg := &packages.Config{
3536
Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo |
3637
packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
@@ -58,16 +59,26 @@ func (a *PackageAnalyzer) analyzeConfig(configTypeName string) (*StructInfo, err
5859

5960
// Auto-detect config type if not specified
6061
if configTypeName == "" {
61-
configTypeName = a.detectConfigType(pkg)
62+
configTypeName = a.detectConfigFromVarDecl(pkg)
6263
if configTypeName == "" {
6364
configTypeName = "Config" // fallback to default
6465
}
6566
}
6667

68+
// Determine which package to analyze
69+
analyzePkg := pkg
70+
if configPkgPath != "" {
71+
// Config is in an external package
72+
analyzePkg, err = a.loadPackage(configPkgPath)
73+
if err != nil {
74+
return nil, fmt.Errorf("failed to load config package %s: %w", configPkgPath, err)
75+
}
76+
}
77+
6778
// Find the Config type
68-
obj := pkg.Types.Scope().Lookup(configTypeName)
79+
obj := analyzePkg.Types.Scope().Lookup(configTypeName)
6980
if obj == nil {
70-
return nil, fmt.Errorf("type %s not found in package %s", configTypeName, pkg.PkgPath)
81+
return nil, fmt.Errorf("type %s not found in package %s", configTypeName, analyzePkg.PkgPath)
7182
}
7283

7384
named, ok := obj.Type().(*types.Named)
@@ -81,11 +92,11 @@ func (a *PackageAnalyzer) analyzeConfig(configTypeName string) (*StructInfo, err
8192
}
8293

8394
// Extract fields
84-
fields := a.extractFields(pkg, structType)
95+
fields := a.extractFields(analyzePkg, structType)
8596

8697
return &StructInfo{
8798
Name: configTypeName,
88-
Package: pkg.PkgPath,
99+
Package: analyzePkg.PkgPath,
89100
Fields: fields,
90101
}, nil
91102
}
@@ -260,9 +271,9 @@ func parseTag(tag string) (name string, squash bool) {
260271
return "", false
261272
}
262273

263-
// detectConfigType looks for the component.Config interface assignment pattern:
274+
// detectConfigFromVarDecl looks for the component.Config interface assignment pattern:
264275
// var _ component.Config = (*TypeName)(nil)
265-
func (a *PackageAnalyzer) detectConfigType(pkg *packages.Package) string {
276+
func (a *PackageAnalyzer) detectConfigFromVarDecl(pkg *packages.Package) string {
266277
for _, file := range pkg.Syntax {
267278
for _, decl := range file.Decls {
268279
genDecl, ok := decl.(*ast.GenDecl)

cmd/mdatagen/internal/schemagen/generator.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ func NewSchemaGenerator(outputDir string, analyzer *PackageAnalyzer) *SchemaGene
3939
}
4040

4141
// GenerateSchema generates a YAML schema for the component's config.
42-
func (g *SchemaGenerator) GenerateSchema(componentKind, componentName, configTypeName string) error {
43-
structInfo, err := g.analyzer.analyzeConfig(configTypeName)
42+
// configTypeName is the name of the config type (e.g., "Config").
43+
// configPkgPath is the package path where the config is defined (empty for local package).
44+
func (g *SchemaGenerator) GenerateSchema(componentKind, componentName, configTypeName, configPkgPath string) error {
45+
structInfo, err := g.analyzer.analyzeConfig(configTypeName, configPkgPath)
4446
if err != nil {
4547
return fmt.Errorf("failed to analyze config: %w", err)
4648
}

cmd/mdatagen/internal/schemagen/generator_test.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ func TestSchemaGenerator_GenerateSchema(t *testing.T) {
2222
analyzer := NewPackageAnalyzer(testDir)
2323
generator := NewSchemaGenerator(outputDir, analyzer)
2424

25-
// Pass empty string to test auto-detection of config type
26-
err := generator.GenerateSchema("receiver", "sample", "")
25+
// Pass empty strings to test auto-detection of config type
26+
err := generator.GenerateSchema("receiver", "sample", "", "")
2727
require.NoError(t, err)
2828

2929
// Check that schema file was created
@@ -53,6 +53,34 @@ func TestSchemaGenerator_GenerateSchema(t *testing.T) {
5353
assert.JSONEq(t, string(expectedJSON), string(generatedJSON))
5454
}
5555

56+
func TestDetectConfigFromFactory(t *testing.T) {
57+
// Test detection from createDefaultConfig() function
58+
// The samplereceiver uses var _ component.Config = (*Config)(nil) pattern,
59+
// but we can test that the detection chain works correctly
60+
61+
testDir := filepath.Join("..", "samplereceiver")
62+
analyzer := NewPackageAnalyzer(testDir)
63+
64+
// Load the package
65+
structInfo, err := analyzer.analyzeConfig("", "")
66+
require.NoError(t, err)
67+
68+
// Verify the config was detected (samplereceiver uses MyConfig)
69+
assert.Equal(t, "MyConfig", structInfo.Name)
70+
assert.Contains(t, structInfo.Package, "samplereceiver")
71+
72+
// Verify fields were extracted
73+
assert.NotEmpty(t, structInfo.Fields)
74+
75+
// Check for known fields
76+
fieldNames := make(map[string]bool)
77+
for _, f := range structInfo.Fields {
78+
fieldNames[f.JSONName] = true
79+
}
80+
assert.True(t, fieldNames["endpoint"], "expected 'endpoint' field")
81+
assert.True(t, fieldNames["timeout"], "expected 'timeout' field")
82+
}
83+
5684
func TestParseTag(t *testing.T) {
5785
tests := []struct {
5886
name string

0 commit comments

Comments
 (0)