Skip to content

Commit 96f839b

Browse files
authored
refactor(config): split validation into focused files with shared helpers (#220)
* refactor(config): extract extension validation into dedicated file * refactor(config): add shared validation helpers to reduce duplication
1 parent da914d5 commit 96f839b

5 files changed

Lines changed: 202 additions & 191 deletions

File tree

internal/config/validator.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,9 @@ func (v *Validator) Validate(ctx context.Context) ([]ValidationResult, error) {
4747
// Reset validations
4848
v.validations = make([]ValidationResult, 0)
4949

50-
// Validate YAML syntax (by trying to load it)
5150
v.validateYAMLSyntax(ctx)
52-
53-
// Validate plugin configurations
5451
v.validatePluginConfigs(ctx)
55-
56-
// Validate workspace configuration
5752
v.validateWorkspaceConfig(ctx)
58-
59-
// Validate extension configurations
6053
v.validateExtensionConfigs(ctx)
6154

6255
return v.validations, nil
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// validateExtensionConfigs validates extension configurations.
11+
func (v *Validator) validateExtensionConfigs(ctx context.Context) {
12+
if v.cfg == nil || len(v.cfg.Extensions) == 0 {
13+
return
14+
}
15+
16+
pathErrorCount := 0
17+
manifestErrorCount := 0
18+
19+
for i, ext := range v.cfg.Extensions {
20+
extPath := v.resolvePath(ext.Path)
21+
22+
if !v.validateExtensionPath(ctx, i, ext, extPath) {
23+
pathErrorCount++
24+
continue
25+
}
26+
27+
if ext.Enabled && !v.validateExtensionManifest(ctx, i, ext, extPath) {
28+
manifestErrorCount++
29+
}
30+
}
31+
32+
if pathErrorCount == 0 && manifestErrorCount == 0 {
33+
enabledCount := v.countEnabledExtensions()
34+
v.addValidation("Extensions", true,
35+
fmt.Sprintf("Configured with %d extension(s) (%d enabled)", len(v.cfg.Extensions), enabledCount), false)
36+
}
37+
}
38+
39+
// validateExtensionPath checks if an extension path exists and is accessible.
40+
func (v *Validator) validateExtensionPath(ctx context.Context, index int, ext ExtensionConfig, extPath string) bool {
41+
_, err := v.fs.Stat(ctx, extPath)
42+
if err == nil {
43+
return true
44+
}
45+
46+
if os.IsNotExist(err) {
47+
v.addValidation("Extensions", false,
48+
fmt.Sprintf("Extension %d ('%s'): path '%s' does not exist", index+1, ext.Name, ext.Path), false)
49+
} else {
50+
v.addValidation("Extensions", false,
51+
fmt.Sprintf("Extension %d ('%s'): cannot access path '%s': %v", index+1, ext.Name, ext.Path, err), false)
52+
}
53+
return false
54+
}
55+
56+
// validateExtensionManifest checks if an enabled extension has a valid manifest file.
57+
func (v *Validator) validateExtensionManifest(ctx context.Context, index int, ext ExtensionConfig, extPath string) bool {
58+
manifestPath := filepath.Join(extPath, "extension.yaml")
59+
_, err := v.fs.Stat(ctx, manifestPath)
60+
if err == nil {
61+
return true
62+
}
63+
64+
if os.IsNotExist(err) {
65+
v.addValidation("Extensions", false,
66+
fmt.Sprintf("Extension %d ('%s'): manifest file 'extension.yaml' not found", index+1, ext.Name), false)
67+
} else {
68+
v.addValidation("Extensions", false,
69+
fmt.Sprintf("Extension %d ('%s'): cannot access manifest: %v", index+1, ext.Name, err), false)
70+
}
71+
return false
72+
}
73+
74+
// countEnabledExtensions returns the number of enabled extensions.
75+
func (v *Validator) countEnabledExtensions() int {
76+
count := 0
77+
for _, ext := range v.cfg.Extensions {
78+
if ext.Enabled {
79+
count++
80+
}
81+
}
82+
return count
83+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"strings"
10+
)
11+
12+
// validateEnum checks that a value is in the allowed set. Returns true if valid.
13+
func (v *Validator) validateEnum(category, field, value string, allowed map[string]bool) bool {
14+
if !allowed[value] {
15+
keys := make([]string, 0, len(allowed))
16+
for k := range allowed {
17+
if k != "" {
18+
keys = append(keys, "'"+k+"'")
19+
}
20+
}
21+
v.addValidation(category, false,
22+
fmt.Sprintf("Invalid %s '%s': must be one of %s", field, value, joinWithOr(keys)), false)
23+
return false
24+
}
25+
return true
26+
}
27+
28+
// validateFileExists checks that a file exists at the given path.
29+
// label is used in the validation message (e.g., "File 1", "Changelog file").
30+
// Returns true if the file exists.
31+
func (v *Validator) validateFileExists(ctx context.Context, category, label, rawPath string) bool {
32+
absPath := v.resolvePath(rawPath)
33+
34+
if _, err := v.fs.Stat(ctx, absPath); err != nil {
35+
if os.IsNotExist(err) {
36+
v.addValidation(category, false,
37+
fmt.Sprintf("%s: '%s' does not exist", label, rawPath), false)
38+
} else {
39+
v.addValidation(category, false,
40+
fmt.Sprintf("%s: cannot access '%s': %v", label, rawPath, err), false)
41+
}
42+
return false
43+
}
44+
return true
45+
}
46+
47+
// validateRegex checks that a pattern is a valid regular expression.
48+
// Returns true if valid.
49+
func (v *Validator) validateRegex(category, label, pattern string) bool {
50+
if _, err := regexp.Compile(pattern); err != nil {
51+
v.addValidation(category, false,
52+
fmt.Sprintf("%s: invalid regex: %v", label, err), false)
53+
return false
54+
}
55+
return true
56+
}
57+
58+
// resolvePath resolves a path relative to the validator's root directory.
59+
// Absolute paths are returned as-is.
60+
func (v *Validator) resolvePath(path string) string {
61+
if filepath.IsAbs(path) {
62+
return path
63+
}
64+
return filepath.Join(v.rootDir, path)
65+
}
66+
67+
// joinWithOr joins strings with commas and "or" before the last element.
68+
func joinWithOr(items []string) string {
69+
switch len(items) {
70+
case 0:
71+
return ""
72+
case 1:
73+
return items[0]
74+
case 2:
75+
return items[0] + " or " + items[1]
76+
default:
77+
var b strings.Builder
78+
for i, item := range items {
79+
if i == len(items)-1 {
80+
b.WriteString("or ")
81+
b.WriteString(item)
82+
} else {
83+
b.WriteString(item)
84+
b.WriteString(", ")
85+
}
86+
}
87+
return b.String()
88+
}
89+
}

0 commit comments

Comments
 (0)