Skip to main content

vld_clap/
lib.rs

1//! # vld-clap — Clap integration for `vld`
2//!
3//! Validate CLI arguments **after** `clap` has parsed them, using
4//! `#[derive(Validate)]` directly on the clap struct. No separate schema
5//! needed.
6//!
7//! # Quick Start
8//!
9//! ```rust,ignore
10//! use clap::Parser;
11//! use vld::Validate;
12//! use vld_clap::prelude::*;
13//!
14//! #[derive(Parser, Debug, serde::Serialize, Validate)]
15//! struct Cli {
16//!     /// Admin email address
17//!     #[arg(long)]
18//!     #[vld(vld::string().email())]
19//!     email: String,
20//!
21//!     /// Server port
22//!     #[arg(long, default_value_t = 8080)]
23//!     #[vld(vld::number().int().min(1).max(65535))]
24//!     port: i64,
25//!
26//!     /// Application name
27//!     #[arg(long)]
28//!     #[vld(vld::string().min(2).max(50))]
29//!     name: String,
30//! }
31//!
32//! fn main() {
33//!     let cli = Cli::parse(); // clap's parse — no conflict
34//!     validate_or_exit(&cli);
35//!     println!("Valid! {:?}", cli);
36//! }
37//! ```
38
39use std::fmt;
40use vld::schema::VldParse;
41
42// ---------------------------------------------------------------------------
43// Error type
44// ---------------------------------------------------------------------------
45
46/// Error returned by `vld-clap` validation functions.
47#[derive(Debug, Clone)]
48pub struct VldClapError {
49    /// The underlying vld validation error (if any).
50    pub source: ErrorSource,
51    /// Human-readable summary of all issues for CLI display.
52    pub message: String,
53}
54
55/// Source of the error.
56#[derive(Debug, Clone)]
57pub enum ErrorSource {
58    /// Validation failed.
59    Validation(vld::error::VldError),
60    /// Serialization to JSON failed.
61    Serialization(String),
62}
63
64impl VldClapError {
65    /// Print the error to stderr and exit with code 2 (standard for usage errors).
66    pub fn exit(&self) -> ! {
67        eprintln!("error: {}", self.message);
68        std::process::exit(2);
69    }
70
71    /// Format each issue on its own line, prefixed with `--field`.
72    pub fn format_issues(&self) -> String {
73        match &self.source {
74            ErrorSource::Validation(e) => e
75                .issues
76                .iter()
77                .map(|i| {
78                    let path = i
79                        .path
80                        .iter()
81                        .map(|p| p.to_string())
82                        .collect::<Vec<_>>()
83                        .join(".");
84                    let field = path.trim_start_matches('.');
85                    if field.is_empty() {
86                        format!("  {}", i.message)
87                    } else {
88                        format!("  --{}: {}", field, i.message)
89                    }
90                })
91                .collect::<Vec<_>>()
92                .join("\n"),
93            ErrorSource::Serialization(msg) => format!("  {}", msg),
94        }
95    }
96}
97
98impl fmt::Display for VldClapError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "{}", self.message)
101    }
102}
103
104impl std::error::Error for VldClapError {
105    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
106        match &self.source {
107            ErrorSource::Validation(e) => Some(e),
108            ErrorSource::Serialization(_) => None,
109        }
110    }
111}
112
113// ---------------------------------------------------------------------------
114// Core validation functions
115// ---------------------------------------------------------------------------
116
117fn make_error(e: vld::error::VldError) -> VldClapError {
118    let summary = e
119        .issues
120        .iter()
121        .map(|i| {
122            let path = i
123                .path
124                .iter()
125                .map(|p| p.to_string())
126                .collect::<Vec<_>>()
127                .join(".");
128            let field = path.trim_start_matches('.');
129            if field.is_empty() {
130                i.message.clone()
131            } else {
132                format!("--{}: {}", field, i.message)
133            }
134        })
135        .collect::<Vec<_>>()
136        .join("\n       ");
137    VldClapError {
138        message: format!("Invalid arguments:\n       {}", summary),
139        source: ErrorSource::Validation(e),
140    }
141}
142
143/// Validate a parsed CLI struct that implements `#[derive(Validate)]` + `Serialize`.
144///
145/// The struct is serialized to JSON, then validated via the vld rules
146/// defined by `#[vld(...)]` attributes on its fields.
147///
148/// # Example
149///
150/// ```rust
151/// use vld::Validate;
152///
153/// #[derive(Debug, serde::Serialize, Validate)]
154/// struct Args {
155///     #[vld(vld::number().int().min(1).max(65535))]
156///     port: i64,
157///     #[vld(vld::string().min(1))]
158///     host: String,
159/// }
160///
161/// let args = Args { port: 8080, host: "localhost".into() };
162/// assert!(vld_clap::validate(&args).is_ok());
163///
164/// let bad = Args { port: 0, host: "".into() };
165/// assert!(vld_clap::validate(&bad).is_err());
166/// ```
167pub fn validate<T>(args: &T) -> Result<(), VldClapError>
168where
169    T: VldParse + serde::Serialize,
170{
171    let json = serde_json::to_value(args).map_err(|e| VldClapError {
172        source: ErrorSource::Serialization(e.to_string()),
173        message: format!("Failed to serialize arguments: {}", e),
174    })?;
175    T::vld_parse_value(&json).map(|_| ()).map_err(make_error)
176}
177
178/// Validate and exit on failure — convenience wrapper.
179///
180/// Calls [`validate`] and, on error, prints the error to stderr
181/// and exits with code 2.
182///
183/// ```rust,ignore
184/// let cli = Cli::parse();
185/// vld_clap::validate_or_exit(&cli);
186/// println!("All good: {:?}", cli);
187/// ```
188pub fn validate_or_exit<T>(args: &T)
189where
190    T: VldParse + serde::Serialize,
191{
192    if let Err(e) = validate(args) {
193        e.exit();
194    }
195}
196
197/// Validate a JSON value against any `VldParse` schema `S`.
198///
199/// Useful when the CLI args are already a JSON object.
200pub fn validate_json<S>(json: &serde_json::Value) -> Result<S, VldClapError>
201where
202    S: VldParse,
203{
204    S::vld_parse_value(json).map_err(make_error)
205}
206
207/// Validate a `Serialize`-able value against any `VldParse` schema `S`.
208///
209/// Use this when you have a separate schema and a data struct (the old
210/// pattern). Prefer [`validate`] with `#[derive(Validate)]` for the
211/// idiomatic approach.
212pub fn validate_with_schema<S, T>(args: &T) -> Result<S, VldClapError>
213where
214    S: VldParse,
215    T: serde::Serialize,
216{
217    let json = serde_json::to_value(args).map_err(|e| VldClapError {
218        source: ErrorSource::Serialization(e.to_string()),
219        message: format!("Failed to serialize arguments: {}", e),
220    })?;
221    S::vld_parse_value(&json).map_err(make_error)
222}
223
224// ---------------------------------------------------------------------------
225// Prelude
226// ---------------------------------------------------------------------------
227
228/// Prelude — import everything you need.
229pub mod prelude {
230    pub use crate::{
231        validate, validate_json, validate_or_exit, validate_with_schema, VldClapError,
232    };
233    pub use vld::prelude::*;
234}