feat(config): add validated profile core
This commit is contained in:
1 parent
0a75860841
commit
6c5e061359
36 files changed
+3343
No files matched your search
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "kaptan-config-derive"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
kaptan-config = { path = "../kaptan-config" }
|
||||
trybuild = "1"
|
||||
@@ -0,0 +1,187 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::Result;
|
||||
|
||||
use crate::parse::{Check, Model};
|
||||
|
||||
pub(crate) fn expand(model: Model) -> Result<TokenStream> {
|
||||
let Model {
|
||||
ident,
|
||||
generics,
|
||||
crate_path,
|
||||
post_validate,
|
||||
fields,
|
||||
} = model;
|
||||
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
|
||||
let mut checks = Vec::new();
|
||||
let mut descriptors = Vec::new();
|
||||
for field in fields {
|
||||
let FieldMetadata {
|
||||
descriptor,
|
||||
checks: field_checks,
|
||||
} = expand_field(&crate_path, &field)?;
|
||||
if let Some(descriptor) = descriptor {
|
||||
descriptors.push(descriptor);
|
||||
}
|
||||
checks.extend(field_checks);
|
||||
}
|
||||
let post_validate = post_validate.map(|hook| quote! { #hook(self)?; });
|
||||
Ok(quote! {
|
||||
impl #impl_generics #crate_path::ValidateFields for #ident #type_generics #where_clause {
|
||||
fn validate_fields(&self) -> Result<(), #crate_path::ConfigViolation> {
|
||||
#(#checks)*
|
||||
#post_validate
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics #ident #type_generics #where_clause {
|
||||
/// Field metadata generated from the same attributes as validation.
|
||||
pub const DESCRIPTORS: &'static [#crate_path::FieldDescriptor] = &[
|
||||
#(#descriptors),*
|
||||
];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct FieldMetadata {
|
||||
descriptor: Option<TokenStream>,
|
||||
checks: Vec<TokenStream>,
|
||||
}
|
||||
|
||||
fn expand_field(crate_path: &syn::Path, field: &crate::parse::FieldModel) -> Result<FieldMetadata> {
|
||||
let mut checks = Vec::new();
|
||||
let Some(id) = field.id else {
|
||||
return Ok(FieldMetadata {
|
||||
descriptor: None,
|
||||
checks,
|
||||
});
|
||||
};
|
||||
let minimum = field.checks.iter().find_map(|check| match check {
|
||||
Check::Minimum(value) => Some(value),
|
||||
_ => None,
|
||||
});
|
||||
let maximum = field.checks.iter().find_map(|check| match check {
|
||||
Check::Maximum(value) => Some(value),
|
||||
_ => None,
|
||||
});
|
||||
for check in &field.checks {
|
||||
checks.push(expand_check(crate_path, &field.ident, id, check.clone()));
|
||||
}
|
||||
let key = field.display_key.as_ref().expect("id fields have a key");
|
||||
let unit = field.unit.as_ref().expect("id fields have a unit");
|
||||
let default = field.default.as_ref().expect("id fields have a default");
|
||||
let (kind, value) = config_value_tokens(crate_path, &field.ty)?;
|
||||
let minimum = option_value_tokens(value.clone(), minimum);
|
||||
let maximum = option_value_tokens(value.clone(), maximum);
|
||||
Ok(FieldMetadata {
|
||||
descriptor: Some(quote! {
|
||||
#crate_path::FieldDescriptor {
|
||||
id: #crate_path::FieldId::new(#id),
|
||||
display_key: #key,
|
||||
kind: #kind,
|
||||
minimum: #minimum,
|
||||
maximum: #maximum,
|
||||
default: #value(#default),
|
||||
unit: #unit,
|
||||
apply_policy: #crate_path::ApplyPolicy::RestartRequired,
|
||||
}
|
||||
}),
|
||||
checks,
|
||||
})
|
||||
}
|
||||
|
||||
fn option_value_tokens(value: TokenStream, expression: Option<&syn::Expr>) -> TokenStream {
|
||||
match expression {
|
||||
Some(expression) => quote! { Some(#value(#expression)) },
|
||||
None => quote! { None },
|
||||
}
|
||||
}
|
||||
|
||||
fn config_value_tokens(
|
||||
crate_path: &syn::Path,
|
||||
ty: &syn::Type,
|
||||
) -> Result<(TokenStream, TokenStream)> {
|
||||
let syn::Type::Path(path) = ty else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"configuration metadata requires a scalar type",
|
||||
));
|
||||
};
|
||||
let Some(segment) = path.path.segments.last() else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"configuration metadata requires a scalar type",
|
||||
));
|
||||
};
|
||||
let (kind, value) = match segment.ident.to_string().as_str() {
|
||||
"bool" => (
|
||||
quote! { #crate_path::ValueKind::Bool },
|
||||
quote! { #crate_path::ConfigValue::Bool },
|
||||
),
|
||||
"i32" => (
|
||||
quote! { #crate_path::ValueKind::I32 },
|
||||
quote! { #crate_path::ConfigValue::I32 },
|
||||
),
|
||||
"u32" => (
|
||||
quote! { #crate_path::ValueKind::U32 },
|
||||
quote! { #crate_path::ConfigValue::U32 },
|
||||
),
|
||||
"u64" => (
|
||||
quote! { #crate_path::ValueKind::U64 },
|
||||
quote! { #crate_path::ConfigValue::U64 },
|
||||
),
|
||||
"f32" => (
|
||||
quote! { #crate_path::ValueKind::F32 },
|
||||
quote! { #crate_path::ConfigValue::F32 },
|
||||
),
|
||||
"f64" => (
|
||||
quote! { #crate_path::ValueKind::F64 },
|
||||
quote! { #crate_path::ConfigValue::F64 },
|
||||
),
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"configuration metadata supports bool, i32, u32, u64, f32, and f64",
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok((kind, value))
|
||||
}
|
||||
|
||||
fn expand_check(crate_path: &syn::Path, field: &syn::Ident, id: u16, check: Check) -> TokenStream {
|
||||
match check {
|
||||
Check::Finite => quote! {
|
||||
if !self.#field.is_finite() {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::NonFinite,
|
||||
));
|
||||
}
|
||||
},
|
||||
Check::Nonzero => quote! {
|
||||
if self.#field == 0 {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::Zero,
|
||||
));
|
||||
}
|
||||
},
|
||||
Check::Minimum(minimum) => quote! {
|
||||
if self.#field < (#minimum) {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::BelowMinimum,
|
||||
));
|
||||
}
|
||||
},
|
||||
Check::Maximum(maximum) => quote! {
|
||||
if self.#field > (#maximum) {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::AboveMaximum,
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//! Procedural validation derive for Kaptan configuration records.
|
||||
|
||||
mod expand;
|
||||
mod parse;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
/// Implements `kaptan_config::ValidateFields` from `#[config(...)]` helpers.
|
||||
#[proc_macro_derive(ConfigValidate, attributes(config))]
|
||||
pub fn derive_config_validate(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||
match parse::Model::parse(input).and_then(expand::expand) {
|
||||
Ok(tokens) => tokens.into(),
|
||||
Err(error) => error.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
use syn::{
|
||||
Attribute, Data, DeriveInput, Expr, Field, Fields, Ident, Lit, Path, Result, Type, UnOp,
|
||||
spanned::Spanned,
|
||||
};
|
||||
|
||||
pub(crate) struct Model {
|
||||
pub(crate) ident: Ident,
|
||||
pub(crate) generics: syn::Generics,
|
||||
pub(crate) crate_path: Path,
|
||||
pub(crate) post_validate: Option<Path>,
|
||||
pub(crate) fields: Vec<FieldModel>,
|
||||
}
|
||||
|
||||
pub(crate) struct FieldModel {
|
||||
pub(crate) ident: Ident,
|
||||
pub(crate) ty: Type,
|
||||
pub(crate) id: Option<u16>,
|
||||
pub(crate) display_key: Option<syn::LitStr>,
|
||||
pub(crate) unit: Option<syn::LitStr>,
|
||||
pub(crate) default: Option<Expr>,
|
||||
pub(crate) checks: Vec<Check>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Check {
|
||||
Finite,
|
||||
Nonzero,
|
||||
Minimum(Expr),
|
||||
Maximum(Expr),
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn parse(input: DeriveInput) -> Result<Self> {
|
||||
let (crate_path, post_validate) = parse_container_attributes(&input.attrs)?;
|
||||
let Data::Struct(data) = input.data else {
|
||||
return Err(syn::Error::new(
|
||||
input.ident.span(),
|
||||
"ConfigValidate can only be derived for structs",
|
||||
));
|
||||
};
|
||||
let Fields::Named(fields) = data.fields else {
|
||||
return Err(syn::Error::new(
|
||||
input.ident.span(),
|
||||
"ConfigValidate requires named struct fields",
|
||||
));
|
||||
};
|
||||
let mut models = Vec::with_capacity(fields.named.len());
|
||||
let mut ids = Vec::new();
|
||||
for field in fields.named {
|
||||
let model = parse_field(field)?;
|
||||
if let Some(id) = model.id {
|
||||
if ids.contains(&id) {
|
||||
return Err(syn::Error::new(
|
||||
model.ident.span(),
|
||||
"duplicate configuration field id",
|
||||
));
|
||||
}
|
||||
ids.push(id);
|
||||
}
|
||||
models.push(model);
|
||||
}
|
||||
for field in &models {
|
||||
if field.id.is_some() {
|
||||
if field.display_key.is_none() {
|
||||
return Err(syn::Error::new(
|
||||
field.ident.span(),
|
||||
"configuration fields require config(key = \"...\")",
|
||||
));
|
||||
}
|
||||
if field.unit.is_none() {
|
||||
return Err(syn::Error::new(
|
||||
field.ident.span(),
|
||||
"configuration fields require config(unit = \"...\")",
|
||||
));
|
||||
}
|
||||
if field.default.is_none() {
|
||||
return Err(syn::Error::new(
|
||||
field.ident.span(),
|
||||
"configuration fields require config(default = ...)",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
ident: input.ident,
|
||||
generics: input.generics,
|
||||
crate_path,
|
||||
post_validate,
|
||||
fields: models,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_container_attributes(attributes: &[Attribute]) -> Result<(Path, Option<Path>)> {
|
||||
let mut crate_path = syn::parse_quote!(::kaptan_config);
|
||||
let mut has_crate_path = false;
|
||||
let mut post_validate = None;
|
||||
for attribute in attributes
|
||||
.iter()
|
||||
.filter(|attribute| attribute.path().is_ident("config"))
|
||||
{
|
||||
attribute.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("crate") {
|
||||
if has_crate_path {
|
||||
return Err(meta.error("duplicate crate path override"));
|
||||
}
|
||||
crate_path = meta.value()?.parse()?;
|
||||
has_crate_path = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("post") {
|
||||
if post_validate.is_some() {
|
||||
return Err(meta.error("duplicate post validation hook"));
|
||||
}
|
||||
post_validate = Some(meta.value()?.parse()?);
|
||||
return Ok(());
|
||||
}
|
||||
Err(meta.error("struct config options are crate = path and post = path"))
|
||||
})?;
|
||||
}
|
||||
Ok((crate_path, post_validate))
|
||||
}
|
||||
|
||||
fn parse_field(field: Field) -> Result<FieldModel> {
|
||||
let ident = field
|
||||
.ident
|
||||
.clone()
|
||||
.ok_or_else(|| syn::Error::new(field.span(), "configuration fields must be named"))?;
|
||||
let mut checks = Vec::new();
|
||||
let mut id = None;
|
||||
let mut display_key = None;
|
||||
let mut unit = None;
|
||||
let mut default = None;
|
||||
let mut finite = false;
|
||||
let mut nonzero = false;
|
||||
let mut range_minimum = false;
|
||||
let mut range_maximum = false;
|
||||
for attribute in field
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(|attribute| attribute.path().is_ident("config"))
|
||||
{
|
||||
attribute.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("id") {
|
||||
if id.is_some() {
|
||||
return Err(meta.error("duplicate configuration field id"));
|
||||
}
|
||||
let literal: syn::LitInt = meta.value()?.parse()?;
|
||||
let parsed = literal.base10_parse::<u16>()?;
|
||||
if parsed == 0 || parsed == u16::MAX {
|
||||
return Err(meta.error("configuration field id is reserved"));
|
||||
}
|
||||
id = Some(parsed);
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("key") {
|
||||
if display_key.is_some() {
|
||||
return Err(meta.error("duplicate configuration display key"));
|
||||
}
|
||||
display_key = Some(meta.value()?.parse()?);
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("unit") {
|
||||
if unit.is_some() {
|
||||
return Err(meta.error("duplicate configuration unit"));
|
||||
}
|
||||
unit = Some(meta.value()?.parse()?);
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("default") {
|
||||
if default.is_some() {
|
||||
return Err(meta.error("duplicate configuration default"));
|
||||
}
|
||||
let expression = meta.value()?.parse()?;
|
||||
validate_default_literal(&expression, &field.ty)?;
|
||||
default = Some(expression);
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("finite") {
|
||||
if finite {
|
||||
return Err(meta.error("duplicate finite validation rule"));
|
||||
}
|
||||
require_float(&field.ty, &meta.path)?;
|
||||
checks.push(Check::Finite);
|
||||
finite = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("nonzero") {
|
||||
if nonzero {
|
||||
return Err(meta.error("duplicate nonzero validation rule"));
|
||||
}
|
||||
require_numeric(&field.ty, &meta.path)?;
|
||||
checks.push(Check::Nonzero);
|
||||
nonzero = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("range") {
|
||||
require_numeric(&field.ty, &meta.path)?;
|
||||
let (minimum, maximum) = parse_range(meta)?;
|
||||
if minimum.is_none() && maximum.is_none() {
|
||||
return Err(syn::Error::new(
|
||||
field.span(),
|
||||
"range requires min and/or max",
|
||||
));
|
||||
}
|
||||
if let Some(minimum) = minimum {
|
||||
if range_minimum {
|
||||
return Err(syn::Error::new(field.span(), "duplicate range minimum"));
|
||||
}
|
||||
checks.push(Check::Minimum(minimum));
|
||||
range_minimum = true;
|
||||
}
|
||||
if let Some(maximum) = maximum {
|
||||
if range_maximum {
|
||||
return Err(syn::Error::new(field.span(), "duplicate range maximum"));
|
||||
}
|
||||
checks.push(Check::Maximum(maximum));
|
||||
range_maximum = true;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(meta.error(
|
||||
"supported field config rules are id, key, unit, default, finite, nonzero, and range",
|
||||
))
|
||||
})?;
|
||||
}
|
||||
if !checks.is_empty() && id.is_none() {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"validated configuration fields require config(id = N)",
|
||||
));
|
||||
}
|
||||
Ok(FieldModel {
|
||||
ident,
|
||||
ty: field.ty,
|
||||
id,
|
||||
display_key,
|
||||
unit,
|
||||
default,
|
||||
checks,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_range(meta: syn::meta::ParseNestedMeta<'_>) -> Result<(Option<Expr>, Option<Expr>)> {
|
||||
let mut minimum = None;
|
||||
let mut maximum = None;
|
||||
meta.parse_nested_meta(|nested| {
|
||||
if nested.path.is_ident("min") {
|
||||
if minimum.is_some() {
|
||||
return Err(nested.error("duplicate range minimum"));
|
||||
}
|
||||
let expression = nested.value()?.parse()?;
|
||||
validate_numeric_literal(&expression)?;
|
||||
minimum = Some(expression);
|
||||
return Ok(());
|
||||
}
|
||||
if nested.path.is_ident("max") {
|
||||
if maximum.is_some() {
|
||||
return Err(nested.error("duplicate range maximum"));
|
||||
}
|
||||
let expression = nested.value()?.parse()?;
|
||||
validate_numeric_literal(&expression)?;
|
||||
maximum = Some(expression);
|
||||
return Ok(());
|
||||
}
|
||||
Err(nested.error("range accepts only min and max"))
|
||||
})?;
|
||||
Ok((minimum, maximum))
|
||||
}
|
||||
|
||||
fn validate_numeric_literal(expression: &Expr) -> Result<()> {
|
||||
match expression {
|
||||
Expr::Lit(expression) if matches!(expression.lit, Lit::Int(_) | Lit::Float(_)) => Ok(()),
|
||||
Expr::Unary(expression)
|
||||
if matches!(expression.op, UnOp::Neg(_))
|
||||
&& matches!(
|
||||
expression.expr.as_ref(),
|
||||
Expr::Lit(literal) if matches!(literal.lit, Lit::Int(_) | Lit::Float(_))
|
||||
) =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"range bounds must be numeric literals",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_default_literal(expression: &Expr, ty: &Type) -> Result<()> {
|
||||
if scalar_type_name(ty) == Some("bool") {
|
||||
return match expression {
|
||||
Expr::Lit(expression) if matches!(expression.lit, Lit::Bool(_)) => Ok(()),
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"bool configuration defaults must be bool literals",
|
||||
)),
|
||||
};
|
||||
}
|
||||
validate_numeric_literal(expression)
|
||||
}
|
||||
|
||||
fn require_float(ty: &Type, span: &impl Spanned) -> Result<()> {
|
||||
if scalar_type_name(ty).is_some_and(|name| name == "f32" || name == "f64") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
span.span(),
|
||||
"finite requires an f32 or f64 field",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn require_numeric(ty: &Type, span: &impl Spanned) -> Result<()> {
|
||||
if scalar_type_name(ty).is_some_and(|name| {
|
||||
matches!(
|
||||
name,
|
||||
"f32"
|
||||
| "f64"
|
||||
| "i8"
|
||||
| "i16"
|
||||
| "i32"
|
||||
| "i64"
|
||||
| "i128"
|
||||
| "isize"
|
||||
| "u8"
|
||||
| "u16"
|
||||
| "u32"
|
||||
| "u64"
|
||||
| "u128"
|
||||
| "usize"
|
||||
)
|
||||
}) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
span.span(),
|
||||
"nonzero and range require a primitive numeric field",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_type_name(ty: &Type) -> Option<&str> {
|
||||
let Type::Path(path) = ty else {
|
||||
return None;
|
||||
};
|
||||
if path.qself.is_some() || path.path.segments.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let segment = path.path.segments.first()?;
|
||||
if !matches!(segment.arguments, syn::PathArguments::None) {
|
||||
return None;
|
||||
}
|
||||
Some(match &segment.ident {
|
||||
ident if ident == "bool" => "bool",
|
||||
ident if ident == "f32" => "f32",
|
||||
ident if ident == "f64" => "f64",
|
||||
ident if ident == "i8" => "i8",
|
||||
ident if ident == "i16" => "i16",
|
||||
ident if ident == "i32" => "i32",
|
||||
ident if ident == "i64" => "i64",
|
||||
ident if ident == "i128" => "i128",
|
||||
ident if ident == "isize" => "isize",
|
||||
ident if ident == "u8" => "u8",
|
||||
ident if ident == "u16" => "u16",
|
||||
ident if ident == "u32" => "u32",
|
||||
ident if ident == "u64" => "u64",
|
||||
ident if ident == "u128" => "u128",
|
||||
ident if ident == "usize" => "usize",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use kaptan_config_derive::ConfigValidate;
|
||||
|
||||
#[derive(ConfigValidate)]
|
||||
struct RootBooleanConfig {
|
||||
#[config(id = 1, key = "enabled", unit = "bool", default = true)]
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
assert_eq!(RootBooleanConfig::DESCRIPTORS.len(), 1);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#[test]
|
||||
fn rejects_malformed_field_validation_attributes() {
|
||||
let cases = trybuild::TestCases::new();
|
||||
cases.compile_fail("tests/ui/*.rs");
|
||||
cases.pass("tests/pass/*.rs");
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use kaptan_config_derive::ConfigValidate;
|
||||
|
||||
#[derive(ConfigValidate)]
|
||||
struct DuplicateId {
|
||||
#[config(id = 7, nonzero)]
|
||||
first: u32,
|
||||
#[config(id = 7, nonzero)]
|
||||
second: u32,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: duplicate configuration field id
|
||||
--> tests/ui/duplicate_id.rs:8:5
|
||||
|
|
||||
8 | second: u32,
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1,9 @@
|
||||
use kaptan_config_derive::ConfigValidate;
|
||||
|
||||
#[derive(ConfigValidate)]
|
||||
struct DuplicateRange {
|
||||
#[config(id = 7, range(min = 1), range(min = 2))]
|
||||
value: u32,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: duplicate range minimum
|
||||
--> tests/ui/duplicate_range.rs:5:5
|
||||
|
|
||||
5 | #[config(id = 7, range(min = 1), range(min = 2))]
|
||||
| ^
|
||||
@@ -0,0 +1,9 @@
|
||||
use kaptan_config_derive::ConfigValidate;
|
||||
|
||||
#[derive(ConfigValidate)]
|
||||
struct MissingId {
|
||||
#[config(nonzero)]
|
||||
value: u32,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: validated configuration fields require config(id = N)
|
||||
--> tests/ui/missing_id.rs:6:5
|
||||
|
|
||||
6 | value: u32,
|
||||
| ^^^^^
|
||||
@@ -0,0 +1,9 @@
|
||||
use kaptan_config_derive::ConfigValidate;
|
||||
|
||||
#[derive(ConfigValidate)]
|
||||
struct MissingMetadata {
|
||||
#[config(id = 7, unit = "count", default = 1, range(min = 0, max = 10))]
|
||||
value: u32,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: configuration fields require config(key = "...")
|
||||
--> tests/ui/missing_metadata.rs:6:5
|
||||
|
|
||||
6 | value: u32,
|
||||
| ^^^^^
|
||||
Reference in new issue
Block a user