@@ -12,7 +12,7 @@ use std::path::{Path, PathBuf};
1212use std:: process;
1313
1414use config:: load_config;
15- use generator:: generate_specs_for_unspecced_modules;
15+ use generator:: { generate_specs_for_unspecced_modules, generate_specs_for_unspecced_modules_paths } ;
1616use validator:: { compute_coverage, find_spec_files, get_schema_table_names, validate_spec} ;
1717
1818#[ derive( Parser ) ]
@@ -36,6 +36,10 @@ struct Cli {
3636 /// Project root directory (default: cwd)
3737 #[ arg( long, global = true ) ]
3838 root : Option < PathBuf > ,
39+
40+ /// Output results as JSON instead of colored text
41+ #[ arg( long, global = true ) ]
42+ json : bool ,
3943}
4044
4145#[ derive( Subcommand ) ]
@@ -61,9 +65,9 @@ fn main() {
6165
6266 match command {
6367 Command :: Init => cmd_init ( & root) ,
64- Command :: Check => cmd_check ( & root, cli. strict , cli. require_coverage ) ,
65- Command :: Coverage => cmd_coverage ( & root, cli. strict , cli. require_coverage ) ,
66- Command :: Generate => cmd_generate ( & root, cli. strict , cli. require_coverage ) ,
68+ Command :: Check => cmd_check ( & root, cli. strict , cli. require_coverage , cli . json ) ,
69+ Command :: Coverage => cmd_coverage ( & root, cli. strict , cli. require_coverage , cli . json ) ,
70+ Command :: Generate => cmd_generate ( & root, cli. strict , cli. require_coverage , cli . json ) ,
6771 }
6872}
6973
@@ -95,13 +99,31 @@ fn cmd_init(root: &Path) {
9599 println ! ( "{} Created specsync.json" , "✓" . green( ) ) ;
96100}
97101
98- fn cmd_check ( root : & Path , strict : bool , require_coverage : Option < usize > ) {
102+ fn cmd_check ( root : & Path , strict : bool , require_coverage : Option < usize > , json : bool ) {
99103 let ( config, spec_files) = load_and_discover ( root) ;
100104 let schema_tables = get_schema_table_names ( root, & config) ;
101- let ( total_errors, total_warnings, passed, total) =
102- run_validation ( root, & spec_files, & schema_tables, & config) ;
105+ let ( total_errors, total_warnings, passed, total, all_errors , all_warnings ) =
106+ run_validation ( root, & spec_files, & schema_tables, & config, json ) ;
103107 let coverage = compute_coverage ( root, & spec_files, & config) ;
104108
109+ if json {
110+ let exit_code = compute_exit_code (
111+ total_errors,
112+ total_warnings,
113+ strict,
114+ & coverage,
115+ require_coverage,
116+ ) ;
117+ let output = serde_json:: json!( {
118+ "passed" : exit_code == 0 ,
119+ "errors" : all_errors,
120+ "warnings" : all_warnings,
121+ "specs_checked" : total,
122+ } ) ;
123+ println ! ( "{}" , serde_json:: to_string_pretty( & output) . unwrap( ) ) ;
124+ process:: exit ( exit_code) ;
125+ }
126+
105127 print_summary ( total, passed, total_warnings, total_errors) ;
106128 print_coverage_line ( & coverage) ;
107129 exit_with_status (
@@ -113,13 +135,36 @@ fn cmd_check(root: &Path, strict: bool, require_coverage: Option<usize>) {
113135 ) ;
114136}
115137
116- fn cmd_coverage ( root : & Path , strict : bool , require_coverage : Option < usize > ) {
138+ fn cmd_coverage ( root : & Path , strict : bool , require_coverage : Option < usize > , json : bool ) {
117139 let ( config, spec_files) = load_and_discover ( root) ;
118140 let schema_tables = get_schema_table_names ( root, & config) ;
119- let ( total_errors, total_warnings, passed, total) =
120- run_validation ( root, & spec_files, & schema_tables, & config) ;
141+ let ( total_errors, total_warnings, passed, total, _all_errors , _all_warnings ) =
142+ run_validation ( root, & spec_files, & schema_tables, & config, json ) ;
121143 let coverage = compute_coverage ( root, & spec_files, & config) ;
122144
145+ if json {
146+ let file_coverage = if coverage. total_source_files == 0 {
147+ 100.0
148+ } else {
149+ ( coverage. specced_file_count as f64 / coverage. total_source_files as f64 ) * 100.0
150+ } ;
151+
152+ let modules: Vec < serde_json:: Value > = coverage
153+ . unspecced_modules
154+ . iter ( )
155+ . map ( |m| serde_json:: json!( { "name" : m, "has_spec" : false } ) )
156+ . collect ( ) ;
157+
158+ let output = serde_json:: json!( {
159+ "file_coverage" : ( file_coverage * 100.0 ) . round( ) / 100.0 ,
160+ "files_covered" : coverage. specced_file_count,
161+ "files_total" : coverage. total_source_files,
162+ "modules" : modules,
163+ } ) ;
164+ println ! ( "{}" , serde_json:: to_string_pretty( & output) . unwrap( ) ) ;
165+ process:: exit ( 0 ) ;
166+ }
167+
123168 print_coverage_report ( & coverage) ;
124169 print_summary ( total, passed, total_warnings, total_errors) ;
125170 print_coverage_line ( & coverage) ;
@@ -132,13 +177,22 @@ fn cmd_coverage(root: &Path, strict: bool, require_coverage: Option<usize>) {
132177 ) ;
133178}
134179
135- fn cmd_generate ( root : & Path , strict : bool , require_coverage : Option < usize > ) {
180+ fn cmd_generate ( root : & Path , strict : bool , require_coverage : Option < usize > , json : bool ) {
136181 let ( config, spec_files) = load_and_discover ( root) ;
137182 let schema_tables = get_schema_table_names ( root, & config) ;
138- let ( total_errors, total_warnings, passed, total) =
139- run_validation ( root, & spec_files, & schema_tables, & config) ;
183+ let ( total_errors, total_warnings, passed, total, _all_errors , _all_warnings ) =
184+ run_validation ( root, & spec_files, & schema_tables, & config, json ) ;
140185 let coverage = compute_coverage ( root, & spec_files, & config) ;
141186
187+ if json {
188+ let generated_paths = generate_specs_for_unspecced_modules_paths ( root, & coverage, & config) ;
189+ let output = serde_json:: json!( {
190+ "generated" : generated_paths,
191+ } ) ;
192+ println ! ( "{}" , serde_json:: to_string_pretty( & output) . unwrap( ) ) ;
193+ process:: exit ( 0 ) ;
194+ }
195+
142196 print_coverage_report ( & coverage) ;
143197
144198 println ! (
@@ -192,19 +246,34 @@ fn load_and_discover(root: &Path) -> (types::SpecSyncConfig, Vec<PathBuf>) {
192246 ( config, spec_files)
193247}
194248
249+ /// Run validation, returning counts and collected error/warning strings.
195250fn run_validation (
196251 root : & Path ,
197252 spec_files : & [ PathBuf ] ,
198253 schema_tables : & std:: collections:: HashSet < String > ,
199254 config : & types:: SpecSyncConfig ,
200- ) -> ( usize , usize , usize , usize ) {
255+ json : bool ,
256+ ) -> ( usize , usize , usize , usize , Vec < String > , Vec < String > ) {
201257 let mut total_errors = 0 ;
202258 let mut total_warnings = 0 ;
203259 let mut passed = 0 ;
260+ let mut all_errors: Vec < String > = Vec :: new ( ) ;
261+ let mut all_warnings: Vec < String > = Vec :: new ( ) ;
204262
205263 for spec_file in spec_files {
206264 let result = validate_spec ( spec_file, root, schema_tables, config) ;
207265
266+ if json {
267+ all_errors. extend ( result. errors . iter ( ) . cloned ( ) ) ;
268+ all_warnings. extend ( result. warnings . iter ( ) . cloned ( ) ) ;
269+ total_errors += result. errors . len ( ) ;
270+ total_warnings += result. warnings . len ( ) ;
271+ if result. errors . is_empty ( ) {
272+ passed += 1 ;
273+ }
274+ continue ;
275+ }
276+
208277 println ! ( "\n {}" , result. spec_path. bold( ) ) ;
209278
210279 // Frontmatter check
@@ -330,7 +399,36 @@ fn run_validation(
330399 }
331400 }
332401
333- ( total_errors, total_warnings, passed, spec_files. len ( ) )
402+ (
403+ total_errors,
404+ total_warnings,
405+ passed,
406+ spec_files. len ( ) ,
407+ all_errors,
408+ all_warnings,
409+ )
410+ }
411+
412+ /// Compute exit code without printing or exiting.
413+ fn compute_exit_code (
414+ total_errors : usize ,
415+ total_warnings : usize ,
416+ strict : bool ,
417+ coverage : & types:: CoverageReport ,
418+ require_coverage : Option < usize > ,
419+ ) -> i32 {
420+ if total_errors > 0 {
421+ return 1 ;
422+ }
423+ if strict && total_warnings > 0 {
424+ return 1 ;
425+ }
426+ if let Some ( req) = require_coverage
427+ && coverage. coverage_percent < req
428+ {
429+ return 1 ;
430+ }
431+ 0
334432}
335433
336434fn print_summary ( total : usize , passed : usize , warnings : usize , _errors : usize ) {
@@ -418,16 +516,17 @@ fn exit_with_status(
418516 }
419517
420518 if let Some ( req) = require_coverage
421- && coverage. coverage_percent < req {
422- println ! (
423- "\n {} {req}%: actual coverage is {}% ({} file(s) missing specs)" ,
424- "--require-coverage" . red( ) ,
425- coverage. coverage_percent,
426- coverage. unspecced_files. len( )
427- ) ;
428- for f in & coverage. unspecced_files {
429- println ! ( " {} {f}" , "✗" . red( ) ) ;
430- }
431- process:: exit ( 1 ) ;
519+ && coverage. coverage_percent < req
520+ {
521+ println ! (
522+ "\n {} {req}%: actual coverage is {}% ({} file(s) missing specs)" ,
523+ "--require-coverage" . red( ) ,
524+ coverage. coverage_percent,
525+ coverage. unspecced_files. len( )
526+ ) ;
527+ for f in & coverage. unspecced_files {
528+ println ! ( " {} {f}" , "✗" . red( ) ) ;
432529 }
530+ process:: exit ( 1 ) ;
531+ }
433532}
0 commit comments