use std::sync::{Arc, OnceLock};
use crate::runtime::{
Field as RuntimeField, Leaf as RuntimeLeaf, LeafType as RuntimeLeafType,
NamedField as RuntimeNamedField, Schema as RuntimeSchema,
};
use toml::Value as TomlValue;
#[derive(Debug)]
pub struct SchemaStatic {
pub name: &'static str,
pub doc: &'static [&'static str],
pub strict: Option<bool>,
pub fields: &'static [NamedFieldStatic],
}
#[derive(Debug)]
pub struct NamedFieldStatic {
pub name: &'static str,
pub field: FieldStatic,
}
#[derive(Debug)]
pub enum FieldStatic {
Leaf(LeafStatic),
Nested(&'static SchemaStatic),
ArrayOf(&'static SchemaStatic),
}
#[derive(Debug)]
pub struct LeafStatic {
pub doc: &'static [&'static str],
pub ty: LeafTypeStatic,
pub default: Option<ValueStatic>,
pub optional: bool,
pub env: Option<&'static str>,
}
#[derive(Debug)]
pub enum LeafTypeStatic {
String,
Integer,
Float,
Bool,
DateTime,
Array(&'static LeafTypeStatic),
Map(&'static LeafTypeStatic),
Enum {
values: &'static [ValueStatic],
},
Value,
}
#[derive(Debug)]
pub enum ValueStatic {
String(&'static str),
Integer(i64),
Float(f64),
Bool(bool),
Datetime(&'static str),
Array(&'static [ValueStatic]),
Table(&'static [(&'static str, ValueStatic)]),
}
impl SchemaStatic {
pub fn to_runtime(&self) -> RuntimeSchema {
RuntimeSchema {
name: self.name.to_string(),
doc: self.doc.iter().map(|s| (*s).to_string()).collect(),
strict: self.strict,
fields: self
.fields
.iter()
.map(NamedFieldStatic::to_runtime)
.collect(),
}
}
}
impl NamedFieldStatic {
fn to_runtime(&self) -> RuntimeNamedField {
RuntimeNamedField {
name: self.name.to_string(),
field: self.field.to_runtime(),
}
}
}
impl FieldStatic {
fn to_runtime(&self) -> RuntimeField {
match self {
FieldStatic::Leaf(leaf) => RuntimeField::Leaf(leaf.to_runtime()),
FieldStatic::Nested(s) => RuntimeField::Nested(s.to_runtime()),
FieldStatic::ArrayOf(s) => RuntimeField::ArrayOf(s.to_runtime()),
}
}
}
impl LeafStatic {
fn to_runtime(&self) -> RuntimeLeaf {
RuntimeLeaf {
doc: self.doc.iter().map(|s| (*s).to_string()).collect(),
ty: self.ty.to_runtime(),
default: self.default.as_ref().map(ValueStatic::to_toml),
optional: self.optional,
env: self.env.map(|s| s.to_string()),
}
}
}
impl LeafTypeStatic {
pub fn to_runtime(&self) -> RuntimeLeafType {
match self {
LeafTypeStatic::String => RuntimeLeafType::String,
LeafTypeStatic::Integer => RuntimeLeafType::Integer,
LeafTypeStatic::Float => RuntimeLeafType::Float,
LeafTypeStatic::Bool => RuntimeLeafType::Bool,
LeafTypeStatic::DateTime => RuntimeLeafType::DateTime,
LeafTypeStatic::Array(elem) => RuntimeLeafType::Array(Box::new(elem.to_runtime())),
LeafTypeStatic::Map(v) => RuntimeLeafType::Map(Box::new(v.to_runtime())),
LeafTypeStatic::Enum { values } => RuntimeLeafType::Enum {
values: values.iter().map(ValueStatic::to_toml).collect(),
},
LeafTypeStatic::Value => RuntimeLeafType::Value,
}
}
}
impl ValueStatic {
pub fn to_toml(&self) -> TomlValue {
match self {
ValueStatic::String(s) => TomlValue::String((*s).to_string()),
ValueStatic::Integer(i) => TomlValue::Integer(*i),
ValueStatic::Float(f) => TomlValue::Float(*f),
ValueStatic::Bool(b) => TomlValue::Boolean(*b),
ValueStatic::Datetime(s) => TomlValue::Datetime(
s.parse()
.expect("clapfig: invalid datetime literal in static schema default"),
),
ValueStatic::Array(items) => {
TomlValue::Array(items.iter().map(ValueStatic::to_toml).collect())
}
ValueStatic::Table(entries) => {
let mut t = toml::map::Map::new();
for (k, v) in entries.iter() {
t.insert((*k).to_string(), v.to_toml());
}
TomlValue::Table(t)
}
}
}
}
pub trait Schema {
const STATIC: &'static SchemaStatic;
fn schema_static() -> &'static SchemaStatic {
Self::STATIC
}
fn schema() -> &'static RuntimeSchema;
fn schema_arc() -> Arc<RuntimeSchema>;
}
pub fn cached_runtime_schema(
cell: &'static OnceLock<Arc<RuntimeSchema>>,
static_schema: &'static SchemaStatic,
) -> &'static RuntimeSchema {
let arc: &'static Arc<RuntimeSchema> =
cell.get_or_init(|| Arc::new(static_schema.to_runtime()));
arc.as_ref()
}
pub fn cached_runtime_schema_arc(
cell: &'static OnceLock<Arc<RuntimeSchema>>,
static_schema: &'static SchemaStatic,
) -> Arc<RuntimeSchema> {
cell.get_or_init(|| Arc::new(static_schema.to_runtime()))
.clone()
}
#[cfg(test)]
mod tests {
use super::*;
static EMPTY_DOC: &[&str] = &[];
static MINIMAL_SCHEMA: SchemaStatic = SchemaStatic {
name: "Minimal",
doc: EMPTY_DOC,
strict: None,
fields: &[NamedFieldStatic {
name: "port",
field: FieldStatic::Leaf(LeafStatic {
doc: EMPTY_DOC,
ty: LeafTypeStatic::Integer,
default: Some(ValueStatic::Integer(8080)),
optional: false,
env: None,
}),
}],
};
#[test]
fn static_to_runtime_roundtrips_minimal_shape() {
let s = MINIMAL_SCHEMA.to_runtime();
assert_eq!(s.name, "Minimal");
assert_eq!(s.fields.len(), 1);
match &s.fields[0].field {
RuntimeField::Leaf(leaf) => {
assert!(matches!(leaf.ty, RuntimeLeafType::Integer));
assert_eq!(leaf.default, Some(TomlValue::Integer(8080)));
assert!(!leaf.optional);
}
other => panic!("expected Leaf, got {other:?}"),
}
}
#[test]
fn value_static_array_to_toml_recurses() {
let v = ValueStatic::Array(&[
ValueStatic::String("a"),
ValueStatic::String("b"),
ValueStatic::Integer(1),
]);
let toml = v.to_toml();
match toml {
TomlValue::Array(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[0], TomlValue::String("a".into()));
assert_eq!(items[2], TomlValue::Integer(1));
}
other => panic!("expected Array, got {other:?}"),
}
}
#[test]
fn value_static_table_to_toml_preserves_keys() {
let v = ValueStatic::Table(&[
("name", ValueStatic::String("x")),
("count", ValueStatic::Integer(3)),
]);
match v.to_toml() {
TomlValue::Table(t) => {
assert_eq!(t.get("name").unwrap().as_str(), Some("x"));
assert_eq!(t.get("count").unwrap().as_integer(), Some(3));
}
other => panic!("expected Table, got {other:?}"),
}
}
#[test]
fn leaf_type_static_enum_to_runtime_carries_values() {
let lt = LeafTypeStatic::Enum {
values: &[
ValueStatic::String("debug"),
ValueStatic::String("info"),
ValueStatic::String("warn"),
ValueStatic::String("error"),
],
};
match lt.to_runtime() {
RuntimeLeafType::Enum { values } => {
assert_eq!(values.len(), 4);
assert_eq!(values[0], TomlValue::String("debug".into()));
}
other => panic!("expected Enum, got {other:?}"),
}
}
static NESTED_INNER: SchemaStatic = SchemaStatic {
name: "Inner",
doc: EMPTY_DOC,
strict: None,
fields: &[NamedFieldStatic {
name: "url",
field: FieldStatic::Leaf(LeafStatic {
doc: EMPTY_DOC,
ty: LeafTypeStatic::String,
default: None,
optional: true,
env: None,
}),
}],
};
static NESTED_OUTER: SchemaStatic = SchemaStatic {
name: "Outer",
doc: EMPTY_DOC,
strict: None,
fields: &[NamedFieldStatic {
name: "db",
field: FieldStatic::Nested(&NESTED_INNER),
}],
};
#[test]
fn nested_static_schemas_compose_via_static_reference() {
let s = NESTED_OUTER.to_runtime();
assert_eq!(s.fields.len(), 1);
match &s.fields[0].field {
RuntimeField::Nested(inner) => {
assert_eq!(inner.name, "Inner");
assert_eq!(inner.fields.len(), 1);
}
other => panic!("expected Nested, got {other:?}"),
}
}
#[test]
fn cached_runtime_schema_returns_same_pointer_across_calls() {
static CELL: OnceLock<Arc<RuntimeSchema>> = OnceLock::new();
let a = cached_runtime_schema(&CELL, &MINIMAL_SCHEMA);
let b = cached_runtime_schema(&CELL, &MINIMAL_SCHEMA);
assert!(std::ptr::eq(a, b));
}
#[test]
fn cached_runtime_schema_arc_shares_underlying_schema_with_ref_accessor() {
static CELL: OnceLock<Arc<RuntimeSchema>> = OnceLock::new();
let r = cached_runtime_schema(&CELL, &MINIMAL_SCHEMA);
let a = cached_runtime_schema_arc(&CELL, &MINIMAL_SCHEMA);
assert!(std::ptr::eq(r, a.as_ref()));
}
}