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}