Skip to main content

local_registry/
local_registry.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8
9//! Demonstrates registering a validator locally and validating text through
10//! the resulting registry. With the `inventory` feature, it also checks global
11//! registration.
12
13use std::error::Error;
14use std::fmt;
15use std::sync::Arc;
16
17use qubit_validator::ArgumentReader;
18use qubit_validator::BindError;
19use qubit_validator::BindErrorKind;
20use qubit_validator::BoundValidationContext;
21use qubit_validator::DependencySpec;
22use qubit_validator::ExecutionError;
23use qubit_validator::InputType;
24use qubit_validator::NamedValidationArgument;
25use qubit_validator::NamedValidationDependency;
26use qubit_validator::PreparedOutcome;
27use qubit_validator::PreparedValidator;
28use qubit_validator::RegistrationSource;
29use qubit_validator::ValidationArgument;
30use qubit_validator::ValidationOutcome;
31use qubit_validator::ValidationValue;
32use qubit_validator::Validator;
33use qubit_validator::ValidatorDescriptor;
34use qubit_validator::ValidatorId;
35use qubit_validator::ValidatorRegistration;
36use qubit_validator::ValidatorRegistry;
37use qubit_validator::ValidatorSignature;
38use qubit_validator::ViolationCode;
39use qubit_validator::ViolationDraft;
40use qubit_validator::prepare_text_validator;
41#[cfg(feature = "inventory")]
42use qubit_validator::register_validator;
43
44#[derive(Debug)]
45struct BlankText;
46
47impl fmt::Display for BlankText {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        formatter.write_str("text must not be blank")
50    }
51}
52
53impl Error for BlankText {}
54
55struct NonBlank;
56
57impl Validator<str> for NonBlank {
58    type Error = BlankText;
59
60    fn validate(&self, value: &str, _context: &()) -> Result<(), Self::Error> {
61        if value.trim().is_empty() {
62            Err(BlankText)
63        } else {
64            Ok(())
65        }
66    }
67}
68
69fn prepare_non_blank(_reader: &mut ArgumentReader<'_>) -> Result<Arc<dyn PreparedValidator>, BindError> {
70    Ok(prepare_text_validator(NonBlank, |_| {
71        ViolationDraft::new(ViolationCode::new("text.blank"))
72    }))
73}
74
75static SIGNATURES: &[ValidatorSignature] = &[ValidatorSignature::new(InputType::Text, &[], prepare_non_blank)];
76static DESCRIPTOR: ValidatorDescriptor = ValidatorDescriptor::new(SIGNATURES);
77
78#[cfg(feature = "inventory")]
79register_validator!(id = "text.non_blank.global", descriptor = &DESCRIPTOR);
80
81static DEPENDENCIES: &[DependencySpec] = &[
82    DependencySpec::new("minimum", InputType::Text, false),
83    DependencySpec::new("maximum", InputType::Text, false),
84];
85
86struct CheckDependencySlots;
87
88impl PreparedValidator for CheckDependencySlots {
89    fn input_type(&self) -> InputType {
90        InputType::Text
91    }
92    fn dependency_specs(&self) -> &'static [DependencySpec] {
93        DEPENDENCIES
94    }
95
96    fn validate(
97        &self,
98        _: ValidationValue<'_>,
99        context: &BoundValidationContext<'_>,
100    ) -> Result<PreparedOutcome, ExecutionError> {
101        let valid = context.value(0)?.as_text() == Some("minimum") && context.value(1)?.as_text() == Some("maximum");
102        Ok(if valid {
103            PreparedOutcome::Valid
104        } else {
105            PreparedOutcome::Invalid(vec![ViolationDraft::new(ViolationCode::new(
106                "text.dependencies_mismatch",
107            ))])
108        })
109    }
110}
111
112fn prepare_dependency_slots(_reader: &mut ArgumentReader<'_>) -> Result<Arc<dyn PreparedValidator>, BindError> {
113    Ok(Arc::new(CheckDependencySlots))
114}
115
116static DEPENDENCY_SIGNATURES: &[ValidatorSignature] = &[ValidatorSignature::new(
117    InputType::Text,
118    DEPENDENCIES,
119    prepare_dependency_slots,
120)];
121static DEPENDENCY_DESCRIPTOR: ValidatorDescriptor = ValidatorDescriptor::new(DEPENDENCY_SIGNATURES);
122
123fn assert_dependency_specs_come_from_signature() -> Result<(), ExecutionError> {
124    let bound = DEPENDENCY_DESCRIPTOR
125        .bind(ValidatorId::new("text.dependent"), 0, &[])
126        .expect("signature binds");
127    assert_eq!(bound.dependency_specs(), DEPENDENCIES);
128    let dependencies = [
129        NamedValidationDependency::new("maximum", ValidationValue::Text("maximum")),
130        NamedValidationDependency::new("minimum", ValidationValue::Text("minimum")),
131    ];
132    assert_eq!(
133        bound.validate_named(ValidationValue::Text("target"), &dependencies)?,
134        ValidationOutcome::Valid,
135    );
136    Ok(())
137}
138
139fn assert_parameters_are_consumed_once() -> Result<(), BindError> {
140    let args = [NamedValidationArgument::new("limit", ValidationArgument::Unsigned(10))];
141    let mut reader = ArgumentReader::new(&args)?;
142    assert_eq!(reader.required_u32("limit")?, 10);
143    let error = reader
144        .required_u32("limit")
145        .expect_err("a parameter cannot be read twice");
146    assert_eq!(error.kind(), BindErrorKind::ParameterAlreadyConsumed);
147    Ok(())
148}
149
150fn main() -> Result<(), Box<dyn Error>> {
151    let registration = ValidatorRegistration::new(
152        ValidatorId::new("text.non_blank"),
153        &DESCRIPTOR,
154        RegistrationSource::new(env!("CARGO_PKG_NAME"), module_path!(), file!(), line!()),
155    );
156    let registry = ValidatorRegistry::from_registrations([registration])?;
157
158    assert!(registry.get("text.non_blank").is_some());
159    let validator = registry.bind("text.non_blank", InputType::Text, &[])?;
160    let context = BoundValidationContext::new(&[]);
161
162    let valid = validator.validate(ValidationValue::Text("Ada"), &context)?;
163    assert_eq!(valid, ValidationOutcome::Valid);
164
165    let invalid = validator.validate(ValidationValue::Text("   "), &context)?;
166    let ValidationOutcome::Invalid(violations) = invalid else {
167        panic!("blank text must be invalid");
168    };
169    assert_eq!(violations.len(), 1);
170    assert_eq!(violations[0].code(), ViolationCode::new("text.blank"));
171
172    assert_dependency_specs_come_from_signature()?;
173    assert_parameters_are_consumed_once()?;
174
175    #[cfg(feature = "inventory")]
176    {
177        let global = ValidatorRegistry::try_global()?;
178        assert!(global.get("text.non_blank.global").is_some());
179    }
180
181    Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186    use super::main;
187
188    /// Keeps the documented local and optional global registry walkthrough
189    /// executable.
190    #[test]
191    fn test_example_completes_successfully() {
192        main().expect("the documented example should complete");
193    }
194}