|
1 | 1 | package validation |
2 | 2 |
|
3 | 3 | import ( |
4 | | - _ "embed" |
| 4 | + "embed" |
5 | 5 | "encoding/json" |
6 | 6 | "fmt" |
7 | 7 | "os" |
8 | | - "strings" |
9 | 8 |
|
10 | 9 | "github.com/xeipuuv/gojsonschema" |
11 | | - "gopkg.in/yaml.v3" |
12 | 10 | ) |
13 | 11 |
|
14 | | -// RuleFrontmatterSchema contains the embedded JSON schema for rule frontmatter |
15 | | -// |
16 | | -//go:embed rule_frontmatter_schema.json |
17 | | -var RuleFrontmatterSchema string |
| 12 | +//go:embed schema/rules-schema.json |
| 13 | +var schemaFS embed.FS |
18 | 14 |
|
19 | | -// RuleFrontmatter represents the structure of rule frontmatter |
20 | | -type RuleFrontmatter struct { |
21 | | - AlwaysApply bool `yaml:"alwaysApply,omitempty" json:"alwaysApply,omitempty"` |
22 | | - Description string `yaml:"description,omitempty" json:"description,omitempty"` |
23 | | - Globs string `yaml:"globs,omitempty" json:"globs,omitempty"` |
24 | | - Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` |
25 | | -} |
26 | | - |
27 | | -// ValidationError represents a validation error with context |
28 | | -type ValidationError struct { |
29 | | - Field string `json:"field"` |
30 | | - Message string `json:"message"` |
31 | | - Value interface{} `json:"value,omitempty"` |
32 | | - Type string `json:"type,omitempty"` |
33 | | - SchemaPath string `json:"schemaPath,omitempty"` |
34 | | - Context string `json:"context,omitempty"` |
35 | | -} |
36 | | - |
37 | | -// ValidationResult contains the validation results |
38 | | -type ValidationResult struct { |
39 | | - Valid bool `json:"valid"` |
40 | | - Errors []ValidationError `json:"errors,omitempty"` |
41 | | -} |
42 | | - |
43 | | -var ( |
44 | | - // compiledSchema holds the compiled JSON schema for reuse |
45 | | - compiledSchema *gojsonschema.Schema |
46 | | -) |
47 | | - |
48 | | -// init compiles the schema once at startup |
49 | | -func init() { |
50 | | - schemaLoader := gojsonschema.NewStringLoader(RuleFrontmatterSchema) |
51 | | - schema, err := gojsonschema.NewSchema(schemaLoader) |
52 | | - if err != nil { |
53 | | - panic(fmt.Errorf("failed to compile frontmatter schema: %w", err)) |
54 | | - } |
55 | | - compiledSchema = schema |
56 | | -} |
57 | | - |
58 | | -// ValidateRuleFile validates a markdown rule file's frontmatter against the schema |
59 | | -func ValidateRuleFile(filePath string) (*ValidationResult, error) { |
60 | | - // Read the file |
61 | | - content, err := os.ReadFile(filePath) |
62 | | - if err != nil { |
63 | | - return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) |
64 | | - } |
65 | | - |
66 | | - // Parse frontmatter |
67 | | - frontmatter, err := extractFrontmatter(content) |
| 15 | +// ValidateRulesJSON validates a rules.json file against the JSON schema |
| 16 | +func ValidateRulesJSON(rulesData []byte) error { |
| 17 | + // Load the schema from embedded file |
| 18 | + schemaBytes, err := schemaFS.ReadFile("schema/rules-schema.json") |
68 | 19 | if err != nil { |
69 | | - return &ValidationResult{ |
70 | | - Valid: false, |
71 | | - Errors: []ValidationError{ |
72 | | - { |
73 | | - Field: "frontmatter", |
74 | | - Message: fmt.Sprintf("Failed to parse frontmatter: %v", err), |
75 | | - }, |
76 | | - }, |
77 | | - }, nil |
| 20 | + return fmt.Errorf("failed to load embedded schema: %w", err) |
78 | 21 | } |
79 | 22 |
|
80 | | - // If no frontmatter found, consider it valid (rules without frontmatter are allowed) |
81 | | - if frontmatter == nil { |
82 | | - return &ValidationResult{Valid: true}, nil |
83 | | - } |
| 23 | + // Create schema loader |
| 24 | + schemaLoader := gojsonschema.NewBytesLoader(schemaBytes) |
84 | 25 |
|
85 | | - return ValidateFrontmatter(frontmatter) |
86 | | -} |
| 26 | + // Create document loader with the rules data |
| 27 | + documentLoader := gojsonschema.NewBytesLoader(rulesData) |
87 | 28 |
|
88 | | -// ValidateFrontmatter validates frontmatter data against the JSON schema |
89 | | -func ValidateFrontmatter(frontmatter map[string]interface{}) (*ValidationResult, error) { |
90 | | - // Create a document loader from the frontmatter data |
91 | | - documentLoader := gojsonschema.NewGoLoader(frontmatter) |
92 | | - |
93 | | - // Validate against the compiled schema |
94 | | - result, err := compiledSchema.Validate(documentLoader) |
| 29 | + // Validate |
| 30 | + result, err := gojsonschema.Validate(schemaLoader, documentLoader) |
95 | 31 | if err != nil { |
96 | | - return nil, fmt.Errorf("validation failed: %w", err) |
97 | | - } |
98 | | - |
99 | | - validationResult := &ValidationResult{ |
100 | | - Valid: result.Valid(), |
101 | | - Errors: make([]ValidationError, 0), |
| 32 | + return fmt.Errorf("failed to validate JSON schema: %w", err) |
102 | 33 | } |
103 | 34 |
|
104 | | - // Convert validation errors to our format |
105 | 35 | if !result.Valid() { |
106 | | - for _, err := range result.Errors() { |
107 | | - validationErr := ValidationError{ |
108 | | - Field: extractFieldName(err.Field()), |
109 | | - Message: err.Description(), |
110 | | - Type: err.Type(), |
111 | | - Context: err.Context().String(), |
112 | | - Value: err.Value(), |
113 | | - } |
114 | | - |
115 | | - // Safely extract schema path from details |
116 | | - if details := err.Details(); details != nil { |
117 | | - if property, ok := details["property"]; ok && property != nil { |
118 | | - validationErr.SchemaPath = fmt.Sprintf("%v", property) |
119 | | - } |
| 36 | + var errorMsg string |
| 37 | + for i, validationError := range result.Errors() { |
| 38 | + if i > 0 { |
| 39 | + errorMsg += "\n" |
120 | 40 | } |
121 | | - |
122 | | - validationResult.Errors = append(validationResult.Errors, validationErr) |
| 41 | + errorMsg += fmt.Sprintf(" - %s", validationError.String()) |
123 | 42 | } |
| 43 | + return fmt.Errorf("rules.json validation failed:\n%s", errorMsg) |
124 | 44 | } |
125 | 45 |
|
126 | | - return validationResult, nil |
127 | | -} |
128 | | - |
129 | | -// extractFieldName cleans up the field name for better display |
130 | | -func extractFieldName(field string) string { |
131 | | - // Remove (root). prefix and return clean field name |
132 | | - cleanField := strings.TrimPrefix(field, "(root).") |
133 | | - if cleanField == "(root)" { |
134 | | - return "root" |
135 | | - } |
136 | | - return cleanField |
| 46 | + return nil |
137 | 47 | } |
138 | 48 |
|
139 | | -// extractFrontmatter extracts YAML frontmatter from markdown content |
140 | | -func extractFrontmatter(content []byte) (map[string]interface{}, error) { |
141 | | - lines := string(content) |
142 | | - |
143 | | - // Check if content starts with frontmatter delimiter |
144 | | - if len(lines) < 4 || lines[:4] != "---\n" { |
145 | | - return nil, nil // No frontmatter |
146 | | - } |
147 | | - |
148 | | - // Find the end of frontmatter |
149 | | - endDelimiter := "\n---\n" |
150 | | - endIndex := strings.Index(lines[4:], endDelimiter) |
151 | | - if endIndex == -1 { |
152 | | - return nil, fmt.Errorf("unterminated frontmatter: missing closing ---") |
153 | | - } |
154 | | - |
155 | | - // Extract frontmatter content |
156 | | - frontmatterContent := lines[4 : 4+endIndex] |
157 | | - if strings.TrimSpace(frontmatterContent) == "" { |
158 | | - return nil, nil // Empty frontmatter |
159 | | - } |
160 | | - |
161 | | - // Parse YAML |
162 | | - var frontmatter map[string]interface{} |
163 | | - if err := yaml.Unmarshal([]byte(frontmatterContent), &frontmatter); err != nil { |
164 | | - return nil, fmt.Errorf("invalid YAML frontmatter: %w", err) |
165 | | - } |
166 | | - |
167 | | - return frontmatter, nil |
168 | | -} |
169 | | - |
170 | | -// GetSchema returns the JSON schema as a string for external use |
171 | | -func GetSchema() string { |
172 | | - return RuleFrontmatterSchema |
173 | | -} |
174 | | - |
175 | | -// ValidateJSON validates JSON data directly against the schema |
176 | | -func ValidateJSON(data []byte) (*ValidationResult, error) { |
177 | | - var jsonData interface{} |
178 | | - if err := json.Unmarshal(data, &jsonData); err != nil { |
179 | | - return &ValidationResult{ |
180 | | - Valid: false, |
181 | | - Errors: []ValidationError{ |
182 | | - { |
183 | | - Field: "json", |
184 | | - Message: fmt.Sprintf("Invalid JSON: %v", err), |
185 | | - }, |
186 | | - }, |
187 | | - }, nil |
188 | | - } |
189 | | - |
190 | | - if jsonMap, ok := jsonData.(map[string]interface{}); ok { |
191 | | - return ValidateFrontmatter(jsonMap) |
| 49 | +// ValidateRulesJSONFromFile validates a rules.json file from disk |
| 50 | +func ValidateRulesJSONFromFile(filePath string) error { |
| 51 | + // Read the file content |
| 52 | + rulesData, err := os.ReadFile(filePath) |
| 53 | + if err != nil { |
| 54 | + return fmt.Errorf("failed to read rules file: %w", err) |
192 | 55 | } |
193 | 56 |
|
194 | | - return &ValidationResult{ |
195 | | - Valid: false, |
196 | | - Errors: []ValidationError{ |
197 | | - { |
198 | | - Field: "root", |
199 | | - Message: "JSON data must be an object", |
200 | | - }, |
201 | | - }, |
202 | | - }, nil |
| 57 | + return ValidateRulesJSON(rulesData) |
203 | 58 | } |
204 | 59 |
|
205 | | -// ValidateYAML validates YAML data directly against the schema |
206 | | -func ValidateYAML(data []byte) (*ValidationResult, error) { |
207 | | - var yamlData map[string]interface{} |
208 | | - if err := yaml.Unmarshal(data, &yamlData); err != nil { |
209 | | - return &ValidationResult{ |
210 | | - Valid: false, |
211 | | - Errors: []ValidationError{ |
212 | | - { |
213 | | - Field: "yaml", |
214 | | - Message: fmt.Sprintf("Invalid YAML: %v", err), |
215 | | - }, |
216 | | - }, |
217 | | - }, nil |
| 60 | +// ValidateRulesObject validates a rules object directly |
| 61 | +func ValidateRulesObject(rules interface{}) error { |
| 62 | + // Convert the rules object to JSON bytes |
| 63 | + rulesBytes, err := json.Marshal(rules) |
| 64 | + if err != nil { |
| 65 | + return fmt.Errorf("failed to marshal rules object: %w", err) |
218 | 66 | } |
219 | 67 |
|
220 | | - return ValidateFrontmatter(yamlData) |
| 68 | + return ValidateRulesJSON(rulesBytes) |
221 | 69 | } |
0 commit comments