Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions internal/validation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"path/filepath"
"regexp"
"sort"
"strings"

"github.com/speakeasy-api/openapi-generation/v2/pkg/errors"
Expand Down Expand Up @@ -62,7 +63,7 @@ func ValidateConfigAndPrintErrors(ctx context.Context, target string, cfg *sdkGe

err := events.Telemetry(ctx, shared.InteractionTypeTargetGenerate, func(ctx context.Context, event *shared.CliEvent) error {
event.GenerateTargetName = &targetName
errs := ValidateConfig(target, cfg, publishingEnabled)
errs := ValidateConfig(target, cfg, publishingEnabled, logger)
if len(errs) > 0 {
if errors.Is(errs[0], ErrNoConfigFound) {
return ErrNoConfigFound
Expand All @@ -82,17 +83,17 @@ func ValidateConfigAndPrintErrors(ctx context.Context, target string, cfg *sdkGe
}

// ValidateConfig validates the generation config for a target and returns a list of errors
func ValidateConfig(target string, cfg *sdkGenConfig.Config, publishingEnabled bool) []error {
func ValidateConfig(target string, cfg *sdkGenConfig.Config, publishingEnabled bool, logger log.Logger) []error {
if cfg == nil || cfg.Config == nil || len(cfg.Config.Languages) == 0 {
return []error{ErrNoConfigFound}
} else if _, ok := cfg.Config.Languages[target]; !ok {
return []error{fmt.Errorf("target %s not found in configuration", target)}
}

return ValidateTarget(target, cfg.Config.Languages[target].Cfg, publishingEnabled)
return ValidateTarget(target, cfg.Config.Languages[target].Cfg, publishingEnabled, logger)
}

func ValidateTarget(target string, config map[string]any, publishingEnabled bool) []error {
func ValidateTarget(target string, config map[string]any, publishingEnabled bool, logger log.Logger) []error {
t, err := generate.GetTargetFromTargetString(target)
if err != nil {
return []error{err}
Expand Down Expand Up @@ -177,5 +178,30 @@ func ValidateTarget(target string, config map[string]any, publishingEnabled bool
}
}

warnUnknownFields(target, config, fields, logger)

return errs
}

// warnUnknownFields warns on every gen.yaml key that no known config field
// reads. The generator ignores these keys, so they are usually stale or
// misspelled. A forward-only pass never sees them, which hides the problem
// from customers who audit their gen.yaml.
func warnUnknownFields(target string, config map[string]any, fields []sdkGenConfig.SDKGenConfigField, logger log.Logger) {
knownFields := make(map[string]bool, len(fields))
for _, field := range fields {
knownFields[field.Name] = true
}

var unknownKeys []string
for key := range config {
if !knownFields[key] {
unknownKeys = append(unknownKeys, key)
}
}
sort.Strings(unknownKeys)

for _, key := range unknownKeys {
logger.Warnf("field '%s' in gen.yaml matches no known config field for target %s. The generator ignores it. Remove the field or correct its spelling.", key, target)
}
}
61 changes: 61 additions & 0 deletions internal/validation/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package validation

import (
"io"
"strings"
"testing"

"github.com/speakeasy-api/openapi-generation/v2/pkg/generate"
"github.com/speakeasy-api/speakeasy/internal/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestValidateTarget_WarnsOnUnknownFields(t *testing.T) {
t.Parallel()

target, err := generate.GetTargetFromTargetString("go")
require.NoError(t, err)

fields, err := generate.GetLanguageConfigFields(target, false)
require.NoError(t, err)
require.NotEmpty(t, fields)

knownField := fields[0].Name

config := map[string]any{
knownField: "value",
"legacyPyright": true,
"fixFlags": map[string]any{"responseRequiredSep2024": true},
}

var warnings []string
logger := log.New().WithWriter(io.Discard).WithWarnCapture(&warnings)

ValidateTarget("go", config, false, logger)

joined := strings.Join(warnings, "\n")
assert.Contains(t, joined, "legacyPyright")
assert.Contains(t, joined, "fixFlags")
assert.NotContains(t, joined, knownField)
}

func TestValidateTarget_NoWarningsForKnownFields(t *testing.T) {
t.Parallel()

target, err := generate.GetTargetFromTargetString("go")
require.NoError(t, err)

fields, err := generate.GetLanguageConfigFields(target, false)
require.NoError(t, err)
require.NotEmpty(t, fields)

config := map[string]any{fields[0].Name: "value"}

var warnings []string
logger := log.New().WithWriter(io.Discard).WithWarnCapture(&warnings)

ValidateTarget("go", config, false, logger)

assert.Empty(t, warnings)
}