extern crate self as betula_schema;
use std::fmt;
use serde::de::DeserializeOwned;
use serde_json::Value;
pub mod types;
const BUNDLE: &str = include_str!("../schema/betula.bundle.schema.json");
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Strand {
#[serde(rename = "+")]
Forward,
#[serde(rename = "-")]
Reverse,
#[serde(rename = ".")]
Unstranded,
}
pub trait Kind: DeserializeOwned {
const NAME: &'static str;
#[doc(hidden)]
fn validator() -> &'static jsonschema::Validator;
}
#[derive(Debug)]
pub enum Error {
Json(serde_json::Error),
Invalid { kind: &'static str, errors: Vec<String> },
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Json(err) => write!(f, "{err}"),
Error::Invalid { kind, errors } => write!(f, "invalid {kind}: {}", errors.join("; ")),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Json(err) => Some(err),
Error::Invalid { .. } => None,
}
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::Json(err)
}
}
pub fn validate<T: Kind>(value: &Value) -> Result<(), Error> {
let errors: Vec<String> = T::validator()
.iter_errors(value)
.map(|err| format!("{}: {err}", err.instance_path()))
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(Error::Invalid { kind: T::NAME, errors })
}
}
pub fn parse<T: Kind>(value: Value) -> Result<T, Error> {
validate::<T>(&value)?;
Ok(serde_json::from_value(value)?)
}
pub fn parse_str<T: Kind>(text: &str) -> Result<T, Error> {
parse(serde_json::from_str(text)?)
}
#[doc(hidden)]
pub fn build_validator(kind: &str) -> jsonschema::Validator {
let mut schema: Value = serde_json::from_str(BUNDLE).expect("embedded bundle is valid JSON");
schema["$ref"] = Value::String(format!("#/$defs/{kind}"));
jsonschema::draft202012::new(&schema).expect("embedded bundle is a valid schema")
}
macro_rules! impl_kinds {
($($name:ident),* $(,)?) => {
$(
pub use crate::types::$name;
impl Kind for types::$name {
const NAME: &'static str = stringify!($name);
fn validator() -> &'static jsonschema::Validator {
static VALIDATOR: std::sync::OnceLock<jsonschema::Validator> = std::sync::OnceLock::new();
VALIDATOR.get_or_init(|| build_validator(stringify!($name)))
}
}
)*
pub const KINDS: &[&str] = &[$(stringify!($name)),*];
#[doc(hidden)]
pub fn round_trip_by_name(kind: &str, value: Value) -> Option<Result<bool, Error>> {
match kind {
$(stringify!($name) => Some((|| {
let parsed = parse::<types::$name>(value)?;
let reparsed = parse::<types::$name>(serde_json::to_value(&parsed)?)?;
Ok(parsed == reparsed)
})()),)*
_ => None,
}
}
};
}
include!("kinds.rs");