#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub enum FieldKind {
#[default]
Any,
Enum(Vec<String>),
EnumArray(Vec<String>),
Object(ObjectSchema),
ObjectArray(ObjectSchema),
TaggedEnum(TaggedEnumSchema),
TaggedEnumArray(TaggedEnumSchema),
Integer,
Number,
Bool,
String,
}
impl FieldKind {
pub fn enum_of<I, S>(values: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::Enum(values.into_iter().map(|s| s.as_ref().to_string()).collect())
}
pub fn enum_array<I, S>(values: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::EnumArray(values.into_iter().map(|s| s.as_ref().to_string()).collect())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldDef {
pub name: String,
pub kind: FieldKind,
}
impl FieldDef {
pub fn new(name: impl AsRef<str>, kind: FieldKind) -> Self {
Self {
name: name.as_ref().to_string(),
kind,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ObjectSchema {
pub fields: Vec<FieldDef>,
}
impl ObjectSchema {
pub fn new<I, S>(valid_fields: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self {
fields: valid_fields
.into_iter()
.map(|name| FieldDef::new(name, FieldKind::Any))
.collect(),
}
}
pub fn empty() -> Self {
Self::default()
}
pub fn with_field(self, name: impl AsRef<str>) -> Self {
self.with_field_kind(name, FieldKind::Any)
}
pub fn with_field_kind(mut self, name: impl AsRef<str>, kind: FieldKind) -> Self {
let name = name.as_ref();
if let Some(def) = self.fields.iter_mut().find(|d| d.name == name) {
def.kind = kind;
} else {
self.fields.push(FieldDef::new(name, kind));
}
self
}
pub fn is_valid_field(&self, field: &str) -> bool {
self.fields.iter().any(|d| d.name == field)
}
pub fn field_names(&self) -> impl Iterator<Item = &str> {
self.fields.iter().map(|d| d.name.as_str())
}
pub fn kind_of(&self, field: &str) -> Option<&FieldKind> {
self.fields.iter().find(|d| d.name == field).map(|d| &d.kind)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TaggedEnumSchema {
pub tag_field: String,
pub variants: Vec<(String, ObjectSchema)>,
pub global_fields: Vec<FieldDef>,
}
impl TaggedEnumSchema {
pub fn new<F>(tag_field: impl AsRef<str>, valid_tags: &[&str], fields_for_tag: F) -> Self
where
F: Fn(&str) -> Option<&'static [&'static str]>,
{
let variants = valid_tags
.iter()
.map(|tag| {
let fields = fields_for_tag(tag)
.map(|fs| ObjectSchema::new(fs.iter().copied()))
.unwrap_or_default();
(tag.to_string(), fields)
})
.collect();
Self {
tag_field: tag_field.as_ref().to_string(),
variants,
global_fields: Vec::new(),
}
}
pub fn with_tag(tag_field: impl AsRef<str>) -> Self {
Self {
tag_field: tag_field.as_ref().to_string(),
variants: Vec::new(),
global_fields: Vec::new(),
}
}
pub fn with_variant(mut self, tag: impl AsRef<str>, schema: ObjectSchema) -> Self {
let tag = tag.as_ref();
if let Some(entry) = self.variants.iter_mut().find(|(t, _)| t == tag) {
entry.1 = schema;
} else {
self.variants.push((tag.to_string(), schema));
}
self
}
pub fn with_enum_array<I, S>(self, field: impl AsRef<str>, valid_values: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.with_field_kind(field, FieldKind::enum_array(valid_values))
}
pub fn with_nested_object<I, S>(self, field: impl AsRef<str>, valid_fields: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.with_field_kind(field, FieldKind::Object(ObjectSchema::new(valid_fields)))
}
pub fn with_field_kind(mut self, field: impl AsRef<str>, kind: FieldKind) -> Self {
let field = field.as_ref();
if let Some(def) = self.global_fields.iter_mut().find(|d| d.name == field) {
def.kind = kind;
} else {
self.global_fields.push(FieldDef::new(field, kind));
}
self
}
pub fn is_valid_tag(&self, tag: &str) -> bool {
self.variants.iter().any(|(t, _)| t == tag)
}
pub fn tag_values(&self) -> impl Iterator<Item = &str> {
self.variants.iter().map(|(t, _)| t.as_str())
}
pub fn variant_schema(&self, tag: &str) -> Option<&ObjectSchema> {
self.variants.iter().find(|(t, _)| t == tag).map(|(_, s)| s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tagged_enum_schema() {
let schema =
TaggedEnumSchema::new("type", &["AddDerive", "RemoveDerive"], |tag| match tag {
"AddDerive" => Some(&["target", "derives"]),
"RemoveDerive" => Some(&["target", "derives"]),
_ => None,
});
assert!(schema.is_valid_tag("AddDerive"));
assert!(!schema.is_valid_tag("InvalidType"));
let fields: Vec<&str> = schema
.variant_schema("AddDerive")
.unwrap()
.field_names()
.collect();
assert_eq!(fields, vec!["target", "derives"]);
}
#[test]
fn test_object_schema() {
let schema = ObjectSchema::new(["name", "value", "is_pub"]);
assert!(schema.is_valid_field("name"));
assert!(!schema.is_valid_field("invalid"));
}
#[test]
fn test_dynamic_schema_from_owned_strings() {
let tag_field = String::from("kind");
let tags = vec![String::from("Create"), String::from("Delete")];
let fields = vec![String::from("name"), String::from("path")];
let mut schema = TaggedEnumSchema::with_tag(&tag_field);
for tag in &tags {
schema = schema.with_variant(tag, ObjectSchema::new(&fields));
}
assert!(schema.is_valid_tag("Create"));
assert!(schema.variant_schema("Delete").unwrap().is_valid_field("path"));
}
#[test]
fn test_with_field_kind_replaces_existing() {
let schema = ObjectSchema::new(["timeout"])
.with_field_kind("timeout", FieldKind::Integer);
assert_eq!(schema.fields.len(), 1);
assert_eq!(schema.kind_of("timeout"), Some(&FieldKind::Integer));
}
#[test]
fn test_recursive_schema_shape() {
let schema = ObjectSchema::empty().with_field_kind(
"outer",
FieldKind::Object(
ObjectSchema::empty()
.with_field_kind("inner", FieldKind::Object(ObjectSchema::new(["leaf"]))),
),
);
let FieldKind::Object(outer) = schema.kind_of("outer").unwrap() else {
panic!("expected object kind");
};
let FieldKind::Object(inner) = outer.kind_of("inner").unwrap() else {
panic!("expected object kind");
};
assert!(inner.is_valid_field("leaf"));
}
}