33package cmd
44
55import (
6+ "encoding/json"
67 "errors"
78 "fmt"
89 "io"
910 "os"
11+ "regexp"
12+ "strconv"
13+ "strings"
1014
15+ "github.com/acarl005/stripansi"
1116 "github.com/spf13/cobra"
1217 "kcl-lang.io/cli/pkg/fs"
1318 "kcl-lang.io/kcl-go/pkg/kcl"
@@ -25,12 +30,49 @@ const (
2530 kcl vet data.yaml code.k --format yaml
2631
2732 # Validate the JSON data using the kcl code with the schema name
28- kcl vet data.json code.k -s Schema`
33+ kcl vet data.json code.k -s Schema
34+
35+ # Validate and output results as JSON for CI/CD integration
36+ kcl vet data.json code.k --output json`
2937)
3038
39+ // VetOptions holds the options for the vet command.
40+ type VetOptions struct {
41+ validate.ValidateOptions
42+ // Output specifies the output format: "text" (default) or "json"
43+ Output string
44+ }
45+
46+ // VetResult represents a structured validation result for JSON output.
47+ type VetResult struct {
48+ Success bool `json:"success"`
49+ ErrCount int `json:"errCount,omitempty"`
50+ Errors []VetError `json:"errors,omitempty"`
51+ Message string `json:"message,omitempty"`
52+ }
53+
54+ // VetError represents a single validation error in structured format.
55+ type VetError struct {
56+ ErrorType string `json:"errorType,omitempty"`
57+ File string `json:"file,omitempty"`
58+ Line int `json:"line,omitempty"`
59+ Column int `json:"column,omitempty"`
60+ Message string `json:"message,omitempty"`
61+ CodeSnippet string `json:"codeSnippet,omitempty"`
62+ Schema * SchemaError `json:"schema,omitempty"`
63+ }
64+
65+ // SchemaError represents schema-related error details.
66+ type SchemaError struct {
67+ Filepath string `json:"filepath,omitempty"`
68+ Line int `json:"line,omitempty"`
69+ Column int `json:"column,omitempty"`
70+ Details string `json:"details,omitempty"`
71+ }
72+
3173// NewVetCmd returns the vet command.
3274func NewVetCmd () * cobra.Command {
33- o := validate. ValidateOptions {}
75+ o := VetOptions {}
3476 cmd := & cobra.Command {
3577 Use : "vet" ,
3678 Short : "KCL validation tool" ,
@@ -52,46 +94,49 @@ func NewVetCmd() *cobra.Command {
5294 "Specify the validate config attribute name." )
5395 cmd .Flags ().StringVar (& o .Format , "format" , "" ,
5496 "Specify the validate data format. e.g., yaml, json. Default is json" )
97+ cmd .Flags ().StringVar (& o .Output , "output" , "text" ,
98+ "Specify the output format. e.g., text, json. Default is text" )
5599
56100 return cmd
57101}
58102
59- func doValidate (dataFile , codeFile string , o * validate. ValidateOptions ) error {
103+ func doValidate (dataFile , codeFile string , o * VetOptions ) error {
60104 var ok bool
105+ var errMsg string
61106 if dataFile == "-" {
62107 // Read data from stdin
63108 input , err := io .ReadAll (os .Stdin )
64109 if err != nil {
65- return err
110+ return outputResult ( o . Output , false , "" , err )
66111 }
67112 code , err := os .ReadFile (codeFile )
68113 if err != nil {
69- return err
114+ return outputResult ( o . Output , false , "" , err )
70115 }
71- ok , err = validate .ValidateCode (string (input ), string (code ), o )
116+ ok , err = validate .ValidateCode (string (input ), string (code ), & o . ValidateOptions )
72117 if err != nil {
73- return err
118+ return outputResult ( o . Output , false , err . Error (), nil )
74119 }
75120 } else {
76121 // Read data from files
77122 dataFiles , err := fs .ExpandInputFiles ([]string {dataFile }, false )
78123 if err != nil {
79- return err
124+ return outputResult ( o . Output , false , "" , err )
80125 }
81126 for _ , dataFile := range dataFiles {
82- ok , err = validateFile (dataFile , codeFile , o )
127+ ok , errMsg , err = validateFile (dataFile , codeFile , & o . ValidateOptions )
83128 if err != nil {
84- return err
129+ return outputResult (o .Output , false , "" , err )
130+ }
131+ if ! ok {
132+ return outputResult (o .Output , false , errMsg , nil )
85133 }
86134 }
87135 }
88- if ok {
89- fmt .Println ("Validate success!" )
90- }
91- return nil
136+ return outputResult (o .Output , ok , "" , nil )
92137}
93138
94- func validateFile (dataFile , codeFile string , opts * validate.ValidateOptions ) (ok bool , err error ) {
139+ func validateFile (dataFile , codeFile string , opts * validate.ValidateOptions ) (ok bool , errMsg string , err error ) {
95140 if opts == nil {
96141 opts = & validate.ValidateOptions {}
97142 }
@@ -104,11 +149,147 @@ func validateFile(dataFile, codeFile string, opts *validate.ValidateOptions) (ok
104149 Format : opts .Format ,
105150 })
106151 if err != nil {
107- return false , err
152+ return false , "" , err
153+ }
154+ return resp .Success , resp .ErrMessage , nil
155+ }
156+
157+ // outputResult outputs the validation result in the specified format.
158+ func outputResult (outputFormat string , success bool , errMsg string , err error ) error {
159+ if strings .ToLower (outputFormat ) == "json" {
160+ return outputJSON (success , errMsg , err )
161+ }
162+ // Default text output
163+ return outputText (success , errMsg , err )
164+ }
165+
166+ // outputText outputs the validation result in text format (original behavior).
167+ func outputText (success bool , errMsg string , err error ) error {
168+ if err != nil {
169+ return err
170+ }
171+ if errMsg != "" {
172+ return errors .New (errMsg )
108173 }
109- var e error = nil
110- if resp .ErrMessage != "" {
111- e = errors .New (resp .ErrMessage )
174+ if success {
175+ fmt .Println ("Validate success!" )
112176 }
113- return resp .Success , e
177+ return nil
178+ }
179+
180+ // outputJSON outputs the validation result in JSON format.
181+ func outputJSON (success bool , errMsg string , err error ) error {
182+ result := VetResult {
183+ Success : success ,
184+ }
185+
186+ if err != nil {
187+ result .Errors = []VetError {{
188+ ErrorType : "Error" ,
189+ Message : stripansi .Strip (err .Error ()),
190+ }}
191+ result .ErrCount = 1
192+ } else if errMsg != "" {
193+ // Strip ANSI codes from the error message before parsing
194+ cleanErrMsg := stripansi .Strip (errMsg )
195+ result .Errors = parseErrorMessage (cleanErrMsg )
196+ result .ErrCount = len (result .Errors )
197+ } else if success {
198+ result .Message = "Validate success!"
199+ }
200+
201+ jsonOutput , jsonErr := json .MarshalIndent (result , "" , " " )
202+ if jsonErr != nil {
203+ return jsonErr
204+ }
205+ fmt .Println (string (jsonOutput ))
206+ return nil
207+ }
208+
209+ // parseErrorMessage attempts to parse the error message into structured errors.
210+ // KCL error format example:
211+ //
212+ // EvaluationError
213+ // --> path/to/file.yaml:3:9
214+ // |
215+ // 3 | app_name: "test"
216+ // | ^ Instance check failed
217+ func parseErrorMessage (errMsg string ) []VetError {
218+ var vetErrors []VetError
219+
220+ // Pattern to match error location: --> filepath:line:column
221+ locationPattern := regexp .MustCompile (`-->\s*([^:]+):(\d+):(\d+)` )
222+ // Pattern to match error type at the start
223+ // Pattern to match error type at the start
224+ errorTypePattern := regexp .MustCompile (`^(\w+Error|\w*Exception)` )
225+ // Pattern to match the error message after ^
226+ messagePattern := regexp .MustCompile (`\^\s*(.+)$` )
227+ // Pattern to match code snippet (line number | code)
228+ snippetPattern := regexp .MustCompile (`^\s*\d+\s*\|\s*(.+)$` )
229+
230+ lines := strings .Split (errMsg , "\n " )
231+
232+ var currentError * VetError
233+ for _ , line := range lines {
234+ line = strings .TrimSpace (line )
235+ if line == "" || line == "|" {
236+ continue
237+ }
238+
239+ // Check for error type
240+ if matches := errorTypePattern .FindStringSubmatch (line ); matches != nil {
241+ if currentError != nil {
242+ vetErrors = append (vetErrors , * currentError )
243+ }
244+ currentError = & VetError {
245+ ErrorType : matches [1 ],
246+ }
247+ continue
248+ }
249+
250+ // Check for location
251+ if matches := locationPattern .FindStringSubmatch (line ); matches != nil {
252+ if currentError == nil {
253+ currentError = & VetError {}
254+ }
255+ currentError .File = matches [1 ]
256+ if lineNum , err := strconv .Atoi (matches [2 ]); err == nil {
257+ currentError .Line = lineNum
258+ }
259+ if colNum , err := strconv .Atoi (matches [3 ]); err == nil {
260+ currentError .Column = colNum
261+ }
262+ continue
263+ }
264+
265+ // Check for error message (contains ^)
266+ if matches := messagePattern .FindStringSubmatch (line ); matches != nil {
267+ if currentError != nil {
268+ currentError .Message = strings .TrimSpace (matches [1 ])
269+ }
270+ continue
271+ }
272+
273+ // Check for code snippet
274+ if matches := snippetPattern .FindStringSubmatch (line ); matches != nil {
275+ if currentError != nil && currentError .CodeSnippet == "" {
276+ currentError .CodeSnippet = strings .TrimSpace (matches [1 ])
277+ }
278+ continue
279+ }
280+ }
281+
282+ // Don't forget the last error
283+ if currentError != nil {
284+ vetErrors = append (vetErrors , * currentError )
285+ }
286+
287+ // If parsing failed, return the raw message as a single error
288+ if len (vetErrors ) == 0 {
289+ vetErrors = append (vetErrors , VetError {
290+ Message : errMsg ,
291+ })
292+ }
293+
294+ return vetErrors
114295}
0 commit comments