From 680846928fbdf677498738988ae6e6ce15723473 Mon Sep 17 00:00:00 2001 From: John O'Neil Date: Thu, 8 Jan 2026 13:36:05 -0800 Subject: [PATCH 1/4] implementing --spec command line arg --- crates/fspec-core/src/lib.rs | 32 ++++++++++++++++++++------------ crates/fspec/src/args.rs | 2 +- crates/fspec/src/main.rs | 35 ++++++++++------------------------- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/crates/fspec-core/src/lib.rs b/crates/fspec-core/src/lib.rs index 7cf787a..f36658b 100644 --- a/crates/fspec-core/src/lib.rs +++ b/crates/fspec-core/src/lib.rs @@ -17,8 +17,20 @@ pub use spec::{DirType, FSEntry, FSPattern, FileType, MatchSettings, Rule, RuleK pub use walk::{WalkCtx, WalkOutput}; pub fn check_tree(root: &Path, settings: &MatchSettings) -> Result { + check_tree_with_spec(root, None, settings) +} + +/// If `spec_path` is `Some`, that path is used. Otherwise defaults to `{root}/.fspec`. +pub fn check_tree_with_spec( + root: &Path, + spec_path: Option<&Path>, + settings: &MatchSettings, +) -> Result { // --- parse .fspec --- - let fspec_path: PathBuf = root.join(".fspec"); + let fspec_path: PathBuf = match spec_path { + Some(p) => p.to_path_buf(), + None => root.join(".fspec"), + }; if !fspec_path.exists() { return Err(Error::Semantic { @@ -26,24 +38,20 @@ pub fn check_tree(root: &Path, settings: &MatchSettings) -> Result, - /// Explicit spec file path (NOT IMPLEMENTED YET in core) + /// Explicit spec file path #[arg(long)] pub spec: Option, diff --git a/crates/fspec/src/main.rs b/crates/fspec/src/main.rs index 6302f55..807bc6e 100644 --- a/crates/fspec/src/main.rs +++ b/crates/fspec/src/main.rs @@ -3,7 +3,7 @@ mod render; use crate::args::{Cli, LeafMode, SeverityArg}; use clap::Parser; -use fspec_core::{MatchSettings, Severity, check_tree}; +use fspec_core::{MatchSettings, Severity, check_tree, check_tree_with_spec}; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -12,15 +12,6 @@ fn main() -> ExitCode { let root: PathBuf = resolve_root(&cli); - // NOTE: fspec-core currently hardcodes /.fspec inside check_tree(). - // Until core supports a custom spec path, we error out if --spec is used. - if cli.spec.is_some() { - eprintln!( - "error: --spec is not implemented yet (fspec-core currently only reads /.fspec)" - ); - return ExitCode::from(2); - } - let mut settings = MatchSettings::default(); // These fields are described in your CLI design doc. @@ -32,21 +23,15 @@ fn main() -> ExitCode { SeverityArg::Error => Severity::Error, }; - // TODO: Update check_tree to support running an a different directory than - // the .fspec resides in. like: - // let report = if let Some(spec) = &cli.spec { - // check_tree_with_spec(&root, spec, &settings) - // } else { - // check_tree(&root, &settings) - // }; - - let report = match check_tree(&root, &settings) { - Ok(r) => r, - Err(e) => { - eprintln!("{e}"); - return ExitCode::from(2); - } - }; + let report = (if let Some(spec) = cli.spec.as_deref() { + check_tree_with_spec(&root, Some(spec), &settings) + } else { + check_tree(&root, &settings) + }) + .unwrap_or_else(|e| { + eprintln!("{e}"); + std::process::exit(2); + }); let out = render::render_human(&report, &settings, cli.verbosity, cli.quiet); From 89702d03b7cbf69d45ba9617beddd3aca1dcf05d Mon Sep 17 00:00:00 2001 From: John O'Neil Date: Thu, 8 Jan 2026 13:52:21 -0800 Subject: [PATCH 2/4] Added --spec and --format cli arguments for specific spec usage and json output --- Cargo.lock | 57 ++++++++++++++++++++++++++++ crates/fspec/Cargo.toml | 2 + crates/fspec/src/args.rs | 12 +++++- crates/fspec/src/main.rs | 8 ++-- crates/fspec/src/render.rs | 76 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 149 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19c8428..8a57ca1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,8 @@ version = "0.1.0" dependencies = [ "clap", "fspec-core", + "serde", + "serde_json", ] [[package]] @@ -180,6 +182,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "libc" version = "0.2.178" @@ -276,6 +284,49 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "strsim" version = "0.11.1" @@ -347,3 +398,9 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/crates/fspec/Cargo.toml b/crates/fspec/Cargo.toml index 9384128..fb33934 100644 --- a/crates/fspec/Cargo.toml +++ b/crates/fspec/Cargo.toml @@ -6,3 +6,5 @@ edition = "2024" [dependencies] clap = { version = "4", features = ["derive"] } fspec-core = { path = "../fspec-core" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/crates/fspec/src/args.rs b/crates/fspec/src/args.rs index 3a62335..0c854a7 100644 --- a/crates/fspec/src/args.rs +++ b/crates/fspec/src/args.rs @@ -13,10 +13,14 @@ pub struct Cli { #[arg(long)] pub root: Option, - /// Explicit spec file path + /// Explicit spec file path (NOT IMPLEMENTED YET in core) #[arg(long)] pub spec: Option, + /// Output format + #[arg(long, value_enum, default_value_t = OutputFormat::Human)] + pub format: OutputFormat, + /// Leaf matching mode #[arg(long, value_enum, default_value_t = LeafMode::Loose)] pub leaf: LeafMode, @@ -34,6 +38,12 @@ pub struct Cli { pub verbosity: u8, } +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum OutputFormat { + Human, + Json, +} + #[derive(Debug, Clone, Copy, ValueEnum)] pub enum LeafMode { Strict, diff --git a/crates/fspec/src/main.rs b/crates/fspec/src/main.rs index 807bc6e..eb5e922 100644 --- a/crates/fspec/src/main.rs +++ b/crates/fspec/src/main.rs @@ -1,7 +1,7 @@ mod args; mod render; -use crate::args::{Cli, LeafMode, SeverityArg}; +use crate::args::{Cli, LeafMode, OutputFormat, SeverityArg}; use clap::Parser; use fspec_core::{MatchSettings, Severity, check_tree, check_tree_with_spec}; use std::path::{Path, PathBuf}; @@ -33,11 +33,9 @@ fn main() -> ExitCode { std::process::exit(2); }); - let out = render::render_human(&report, &settings, cli.verbosity, cli.quiet); + let out = render::render(&report, &settings, cli.format, cli.verbosity, cli.quiet); - // For now always stdout (per your “ok if incomplete” note). - // --output/--format can land tomorrow. - print!("{out}"); + println!("{}", out); // Current “finding” heuristic: any unaccounted path => fail. // (In the future: incorporate per-item severity + threshold logic.) diff --git a/crates/fspec/src/render.rs b/crates/fspec/src/render.rs index 445b604..3b08707 100644 --- a/crates/fspec/src/render.rs +++ b/crates/fspec/src/render.rs @@ -1,4 +1,67 @@ +use crate::args::OutputFormat; use fspec_core::{MatchSettings, Report, Severity}; +use serde::Serialize; + +// Until the report schema stabilizes, we probably don't want to directly deserialize the +// struct via serde. so we use a helper. +// This +#[derive(Serialize)] +struct JsonOut<'a> { + ok: bool, + unaccounted: Vec<&'a str>, + diagnostics: Vec>, + summary: JsonSummary, +} + +#[derive(Serialize)] +struct JsonDiag<'a> { + code: &'a str, + severity: String, + path: &'a str, + message: &'a str, + rule_lines: &'a [usize], +} + +#[derive(Serialize)] +struct JsonSummary { + unaccounted_count: usize, + // you can add more later without breaking humans +} + +fn severity_to_string(sev: Severity) -> String { + match sev { + Severity::Info => "info", + Severity::Warning => "warning", + Severity::Error => "error", + } + .to_string() +} + +pub fn render_json(report: &Report, settings: &MatchSettings) -> String { + let un = report.unaccounted_paths(); + let diags = report.diagnostics(); + + let out = JsonOut { + ok: un.is_empty(), + unaccounted: un.clone(), + diagnostics: diags + .iter() + .map(|d| JsonDiag { + code: d.code, + severity: severity_to_string(d.severity), + path: d.path.as_str(), + message: d.message.as_str(), + rule_lines: &d.rule_lines, + }) + .collect(), + summary: JsonSummary { + unaccounted_count: un.len(), + }, + }; + + // if this fails, it’s a programmer error; still return something sane + serde_json::to_string_pretty(&out).unwrap_or_else(|_| "{\"ok\":false}".to_string()) +} pub fn render_human( report: &Report, @@ -51,3 +114,16 @@ pub fn render_human( out } + +pub fn render( + report: &Report, + settings: &MatchSettings, + format: OutputFormat, + verbosity: u8, + quiet: bool, +) -> String { + match format { + OutputFormat::Human => render_human(report, settings, verbosity, quiet), + OutputFormat::Json => render_json(report, settings), + } +} From 20b83cd3583376ed270576cbda2d5ebd37be09dc Mon Sep 17 00:00:00 2001 From: John O'Neil Date: Thu, 8 Jan 2026 13:58:22 -0800 Subject: [PATCH 3/4] Add tool and json version to json output for future help --- crates/fspec/src/render.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/fspec/src/render.rs b/crates/fspec/src/render.rs index 3b08707..18ee8d9 100644 --- a/crates/fspec/src/render.rs +++ b/crates/fspec/src/render.rs @@ -2,11 +2,16 @@ use crate::args::OutputFormat; use fspec_core::{MatchSettings, Report, Severity}; use serde::Serialize; +const SCHEMA_VERSION: &str = "fspec.report.v1"; +const TOOL_VERSION: &str = env!("CARGO_PKG_VERSION"); + // Until the report schema stabilizes, we probably don't want to directly deserialize the // struct via serde. so we use a helper. // This #[derive(Serialize)] struct JsonOut<'a> { + schema_version: &'static str, + tool_version: &'static str, ok: bool, unaccounted: Vec<&'a str>, diagnostics: Vec>, @@ -42,6 +47,8 @@ pub fn render_json(report: &Report, settings: &MatchSettings) -> String { let diags = report.diagnostics(); let out = JsonOut { + schema_version: SCHEMA_VERSION, + tool_version: TOOL_VERSION, ok: un.is_empty(), unaccounted: un.clone(), diagnostics: diags From 507c0595da16dc4046e178261636d8fb3da1a208 Mon Sep 17 00:00:00 2001 From: John O'Neil Date: Thu, 8 Jan 2026 14:05:14 -0800 Subject: [PATCH 4/4] update conformance level in readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d67705..995b56b 100644 --- a/README.md +++ b/README.md @@ -189,11 +189,13 @@ fspec is intentionally staged. Not all features need to exist at once. - [x] Improve the parsing grammar design to require/allow the `:` sigil for limiters. - [x] Introduce a command line tool wrapper crate. - [x] Make the basic rule engine usable in real world cases. -- [ ] Command line tool output switches and JSON report output. +- [x] Command line tool output switches and JSON report output. ## Level 2 — Diagnostics and Expansion - [ ] Named path aliases (reduce repetition; improve readability for hierarchical specs) [see proposal.](./docs/proposals/named-path-aliases.md) +- [ ] Improve command line switches +- [ ] Improve logging and verbosity - [ ] explain which rule matched - [ ] ambiguity detection and warnings - [ ] warn on re-allowed ignored paths