use crate::error::{Error, Result};
use crate::value::Value;
pub use schemars::JsonSchema;
pub fn schema_for<T: JsonSchema>() -> Result<Value> {
let mut generator = schemars::SchemaGenerator::default();
let schema = generator.root_schema_for::<T>();
let json = serde_json::to_string(&schema)
.map_err(|e| Error::Parse(format!("schema_for: schema serialization failed: {e}")))?;
crate::from_str::<Value>(&json)
}
pub fn schema_for_yaml<T: JsonSchema>() -> Result<String> {
let mut generator = schemars::SchemaGenerator::default();
let schema = generator.root_schema_for::<T>();
crate::to_string(&schema)
}
impl JsonSchema for Value {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("YamlValue")
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("noyalib::Value")
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::Schema::try_from(serde_json::json!({
"oneOf": [
{ "type": "null" },
{ "type": "boolean" },
{ "type": "number" },
{ "type": "string" },
{
"type": "array",
"items": { "$ref": "#/$defs/YamlValue" }
},
{
"type": "object",
"additionalProperties": { "$ref": "#/$defs/YamlValue" }
}
],
"title": "YamlValue",
"description": "Any YAML 1.2 scalar, sequence, or mapping value. \
Recursive — sequence items and mapping values are \
themselves `YamlValue`s."
}))
.expect("YamlValue schema must be a valid JSON Schema document")
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, JsonSchema)]
#[allow(dead_code)]
struct Cfg {
port: u16,
name: String,
}
#[test]
fn schema_for_returns_object_schema() {
let v = schema_for::<Cfg>().unwrap();
assert_eq!(v["type"].as_str(), Some("object"));
assert_eq!(v["title"].as_str(), Some("Cfg"));
let required = match &v["required"] {
Value::Sequence(s) => s.clone(),
other => panic!("expected required to be a sequence, got {other:?}"),
};
let names: Vec<&str> = required.iter().filter_map(Value::as_str).collect();
assert!(names.contains(&"port"));
assert!(names.contains(&"name"));
}
#[test]
fn schema_for_yaml_round_trips_to_value() {
let yaml = schema_for_yaml::<Cfg>().unwrap();
let parsed: Value = crate::from_str(&yaml).unwrap();
let direct = schema_for::<Cfg>().unwrap();
assert_eq!(parsed, direct);
}
#[test]
fn schema_records_field_constraints() {
let v = schema_for::<Cfg>().unwrap();
let port = &v["properties"]["port"];
assert_eq!(port["type"].as_str(), Some("integer"));
assert_eq!(port["minimum"].as_i64(), Some(0));
assert_eq!(port["maximum"].as_i64(), Some(65_535));
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[allow(dead_code)]
struct WithDoc {
port: u16,
}
#[test]
fn doc_comments_become_descriptions() {
let v = schema_for::<WithDoc>().unwrap();
let desc = v["properties"]["port"]["description"].as_str();
assert_eq!(desc, Some("Bound TCP port."));
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[allow(dead_code)]
struct WithDefault {
port: u16,
#[serde(default)]
host: String,
}
#[test]
fn serde_default_drops_field_from_required() {
let v = schema_for::<WithDefault>().unwrap();
let required = match &v["required"] {
Value::Sequence(s) => s.clone(),
other => panic!("expected required, got {other:?}"),
};
let names: Vec<&str> = required.iter().filter_map(Value::as_str).collect();
assert!(names.contains(&"port"));
assert!(
!names.contains(&"host"),
"default-bearing field should not be required"
);
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[allow(dead_code)]
struct Renamed {
#[serde(rename = "bind_port")]
port: u16,
}
#[test]
fn serde_rename_renames_schema_property() {
let v = schema_for::<Renamed>().unwrap();
let props = match &v["properties"] {
Value::Mapping(m) => m.clone(),
other => panic!("expected properties to be a Mapping, got {other:?}"),
};
assert!(props.contains_key("bind_port"));
assert!(!props.contains_key("port"));
}
}