Skip to content

Commit 3fd1cd8

Browse files
committed
Add CI report and failure options to sync
Introduce CI-oriented CLI options for the sync command: --report-json, --fail-on-unmatched, --fail-on-ambiguous and a strict mode. Added fields to SyncOptions and validation for the report path. Implemented write_report to emit a pretty JSON summary (source/target/output/lang/match_lang/flags/dry_run and a summary of stats). Sync now writes the report when requested and enforces failure policies (fail on unmatched or ambiguous fallback entries, or when strict is enabled), returning a non-zero error when policies are violated. Also pass strict through to resource reading and update the integration test and README to document the new options.
1 parent 3237f5a commit 3fd1cd8

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

langcodec-cli/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ Matching rules:
5454
- fallback: use `--match-lang` translation (default inferred/en) to match source entries
5555
- never adds new keys to target
5656

57+
CI-oriented options:
58+
- `--report-json <path>` write sync summary as JSON
59+
- `--fail-on-unmatched` return non-zero when unmatched entries exist
60+
- `--fail-on-ambiguous` return non-zero when fallback matching is ambiguous
61+
5762
### view
5863

5964
```sh

langcodec-cli/src/sync.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::convert::read_resources_from_any_input;
22
use crate::validation::{validate_file_path, validate_language_code, validate_output_path};
33
use langcodec::{Codec, Resource, Translation, formats::FormatType};
4+
use serde_json::json;
45
use std::collections::HashMap;
56

67
#[derive(Debug, Clone)]
@@ -10,6 +11,10 @@ pub struct SyncOptions {
1011
pub output: Option<String>,
1112
pub lang: Option<String>,
1213
pub match_lang: Option<String>,
14+
pub report_json: Option<String>,
15+
pub fail_on_unmatched: bool,
16+
pub fail_on_ambiguous: bool,
17+
pub strict: bool,
1318
pub dry_run: bool,
1419
}
1520

@@ -263,6 +268,39 @@ fn write_back(
263268
}
264269
}
265270

271+
fn write_report(
272+
path: &str,
273+
options: &SyncOptions,
274+
match_lang: &str,
275+
stats: &SyncStats,
276+
) -> Result<(), String> {
277+
let report = json!({
278+
"source": options.source,
279+
"target": options.target,
280+
"output": options.output,
281+
"lang": options.lang,
282+
"match_lang": match_lang,
283+
"strict": options.strict,
284+
"fail_on_unmatched": options.fail_on_unmatched,
285+
"fail_on_ambiguous": options.fail_on_ambiguous,
286+
"dry_run": options.dry_run,
287+
"summary": {
288+
"total_entries": stats.total_entries,
289+
"updated": stats.updated,
290+
"unchanged": stats.unchanged,
291+
"fallback_matches": stats.fallback_matches,
292+
"skipped_unmatched": stats.skipped_unmatched,
293+
"skipped_missing_language": stats.skipped_missing_language,
294+
"skipped_ambiguous_fallback": stats.skipped_ambiguous_fallback,
295+
"skipped_type_mismatch": stats.skipped_type_mismatch
296+
}
297+
});
298+
299+
let text = serde_json::to_string_pretty(&report)
300+
.map_err(|e| format!("Failed to serialize report JSON: {}", e))?;
301+
std::fs::write(path, text).map_err(|e| format!("Failed to write report JSON '{}': {}", path, e))
302+
}
303+
266304
pub fn run_sync_command(opts: SyncOptions) -> Result<(), String> {
267305
validate_file_path(&opts.source)?;
268306
validate_file_path(&opts.target)?;
@@ -275,8 +313,11 @@ pub fn run_sync_command(opts: SyncOptions) -> Result<(), String> {
275313
if let Some(match_lang) = &opts.match_lang {
276314
validate_language_code(match_lang)?;
277315
}
316+
if let Some(report_path) = &opts.report_json {
317+
validate_output_path(report_path)?;
318+
}
278319

279-
let source_resources = read_resources_from_any_input(&opts.source, None)?;
320+
let source_resources = read_resources_from_any_input(&opts.source, None, opts.strict)?;
280321
let source_codec = Codec {
281322
resources: source_resources,
282323
};
@@ -392,6 +433,26 @@ pub fn run_sync_command(opts: SyncOptions) -> Result<(), String> {
392433
);
393434
println!("Skipped (type mismatch): {}", stats.skipped_type_mismatch);
394435

436+
if let Some(report_path) = &opts.report_json {
437+
write_report(report_path, &opts, &match_lang, &stats)?;
438+
println!("Report JSON written: {}", report_path);
439+
}
440+
441+
let fail_on_unmatched = opts.fail_on_unmatched || opts.strict;
442+
let fail_on_ambiguous = opts.fail_on_ambiguous || opts.strict;
443+
if (fail_on_unmatched && stats.skipped_unmatched > 0)
444+
|| (fail_on_ambiguous && stats.skipped_ambiguous_fallback > 0)
445+
{
446+
let mut reasons = Vec::new();
447+
if fail_on_unmatched && stats.skipped_unmatched > 0 {
448+
reasons.push(format!("unmatched={}", stats.skipped_unmatched));
449+
}
450+
if fail_on_ambiguous && stats.skipped_ambiguous_fallback > 0 {
451+
reasons.push(format!("ambiguous={}", stats.skipped_ambiguous_fallback));
452+
}
453+
return Err(format!("Sync policy failure ({})", reasons.join(", ")));
454+
}
455+
395456
if opts.dry_run {
396457
println!("Dry-run mode: no files were written");
397458
return Ok(());

langcodec-cli/tests/cli_integration_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ fn test_main_help_command() {
445445
let stdout = String::from_utf8_lossy(&output.stdout);
446446
assert!(stdout.contains("langcodec"));
447447
assert!(stdout.contains("convert"));
448+
assert!(stdout.contains("diff"));
448449
assert!(stdout.contains("merge"));
449450
assert!(stdout.contains("sync"));
450451
assert!(stdout.contains("view"));

0 commit comments

Comments
 (0)