use std::collections::BTreeMap;
use serde_json::Value;
pub type Unknown = BTreeMap<String, Value>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModelShape {
pub schema: &'static str,
pub fields: &'static [&'static str],
}
macro_rules! modules {
($($module:ident),* $(,)?) => {
$(pub mod $module;)*
pub fn shapes() -> impl Iterator<Item = &'static ModelShape> {
[$($module::SHAPES),*].into_iter().flatten()
}
pub fn known_values() -> impl Iterator<Item = &'static KnownValues> {
[$($module::KNOWN_VALUES),*].into_iter().flatten()
}
};
}
modules!(
device, dns, key, logging, policy, service, tailnet, user, webhook
);
pub type KnownValues = (&'static str, &'static [&'static str]);
#[macro_export]
macro_rules! model {
($($declarations:tt)*) => {
$crate::model_entries! { @shapes[] $($declarations)* }
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! model_entries {
(
@shapes[$($shape:expr,)*]
$(#[doc = $doc:literal])*
$name:ident $(as $schema:literal)? {
$(
$(#[doc = $field_doc:literal])*
$field:ident : $json:literal => $ty:ty
),* $(,)?
}
$($rest:tt)*
) => {
$(#[doc = $doc])*
#[derive(Debug, Clone, PartialEq, ::serde::Serialize, ::serde::Deserialize)]
pub struct $name {
$(
$(#[doc = $field_doc])*
#[serde(rename = $json, default, skip_serializing_if = "Option::is_none")]
pub $field: ::std::option::Option<$ty>,
)*
#[serde(flatten)]
pub unknown: $crate::models::Unknown,
}
impl $name {
pub const SCHEMA: &'static str = {
#[allow(unused_mut, unused_assignments)]
let mut schema = ::std::stringify!($name);
$( schema = $schema; )?
schema
};
pub const FIELDS: &'static [&'static str] = &[$($json),*];
}
$crate::model_entries! {
@shapes[
$($shape,)*
$crate::models::ModelShape {
schema: <$name>::SCHEMA,
fields: <$name>::FIELDS,
},
]
$($rest)*
}
};
(
@shapes[$($shape:expr,)*]
$(#[doc = $doc:literal])*
$alias:ident as $schema:literal is $name:ident;
$($rest:tt)*
) => {
$(#[doc = $doc])*
pub type $alias = $name;
$crate::model_entries! {
@shapes[
$($shape,)*
$crate::models::ModelShape { schema: $schema, fields: <$name>::FIELDS },
]
$($rest)*
}
};
(@shapes[$($shape:expr,)*]) => {
pub const SHAPES: &[$crate::models::ModelShape] = &[$($shape,)*];
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_field_the_description_never_mentioned_survives_a_round_trip() {
let body = serde_json::json!({
"id": "example-1",
"hostname": "laptop",
"quantumEntangled": true,
});
let device: device::Device = serde_json::from_value(body.clone()).expect("it parses");
assert_eq!(device.id.as_deref(), Some("example-1"));
assert_eq!(
device.unknown.get("quantumEntangled"),
Some(&Value::Bool(true)),
"the field is retrievable, not merely tolerated"
);
assert_eq!(
serde_json::to_value(&device).expect("it serialises"),
body,
"and it comes back out as it went in"
);
}
#[test]
fn a_field_that_is_absent_is_absent_rather_than_null() {
let empty = device::Device {
id: Some("example-1".to_owned()),
..serde_json::from_value(serde_json::json!({})).expect("an empty body is a device")
};
assert_eq!(
serde_json::to_value(&empty).expect("it serialises"),
serde_json::json!({"id": "example-1"})
);
}
#[test]
fn every_model_names_a_distinct_schema() {
let mut seen = std::collections::BTreeSet::new();
for shape in shapes() {
assert!(
seen.insert(shape.schema),
"{} is declared twice",
shape.schema
);
}
let mut paths = std::collections::BTreeSet::new();
for (path, _) in known_values() {
assert!(paths.insert(*path), "{path} is declared twice");
}
}
#[test]
fn a_secret_field_is_not_printed_by_a_model() {
let minted: key::Key = serde_json::from_value(serde_json::json!({
"id": "kExAmPlE",
"key": "tskey-auth-example1CNTRL-secretpart",
}))
.expect("it parses");
assert!(!format!("{minted:?}").contains("secretpart"), "{minted:?}");
assert_eq!(
minted.key.as_ref().map(crate::Secret::expose),
Some("tskey-auth-example1CNTRL-secretpart"),
"and is still readable by whoever asked for it"
);
}
}