commit 3a321a901b5ba1bc42fc4d18b88972801452200b Author: Graham Esau Date: Sat Aug 3 17:45:57 2019 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1427b7d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "schemars" +version = "0.1.0" +authors = ["Graham Esau "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3c330b4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +pub mod make_schema; +pub mod schema; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a6c024e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,54 @@ +mod make_schema; +mod schema; + +use make_schema::MakeSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Result; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +enum TodoStatus { + Backlog, + InProgress, + Done, + Archived, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Todo { + id: u64, + title: String, + description: Option, + status: TodoStatus, + assigned_to: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct User { + id: u64, + username: String, +} + +fn main() -> Result<()> { + let schema = <&str>::make_schema(); + let json = serde_json::to_string(&schema)?; + println!("{}", json); + + /*let todo = Todo { + id: 42, + title: "Learn Rust".to_owned(), + description: Option::None, + status: TodoStatus::InProgress, + assigned_to: vec![User { + id: 1248, + username: "testuser".to_owned(), + }], + }; + + let t = serde_json::to_string(&todo)?; + println!("{}", t);*/ + + Ok(()) +} diff --git a/src/make_schema.rs b/src/make_schema.rs new file mode 100644 index 0000000..437918d --- /dev/null +++ b/src/make_schema.rs @@ -0,0 +1,193 @@ +use crate::schema::*; +use serde_json::json; +use std::collections::BTreeMap as Map; + +pub trait MakeSchema { + fn make_schema() -> Schema; +} + +// TODO structs, enums, tuples + +// TODO serde json value, any other serde values? +// https://github.com/serde-rs/serde/blob/ce75418e40a593fc5c0902cbf4a45305a4178dd7/serde/src/ser/impls.rs +// Cell, RefCell, Mutex, RwLock, Result?, Duration, SystemTime, +// IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6, SocketAddrV6, +// Path, PathBuf, OsStr, OsString, Wrapping, Reverse, AtomicBool, AtomixI8 etc., +// NonZeroU8 etc., ArcWeak, RcWeak, BTreeMap, HashMap, unit?, (!)?, Bound?, Range?, RangeInclusive?, +// PhantomData?, CString?, CStr?, fmt::Arguments? +// !map keys must be Into! + +////////// PRIMITIVES (except ints) ////////// + +macro_rules! simple_impl { + ($type:ident => $instance_type:expr) => { + impl MakeSchema for $type { + fn make_schema() -> Schema { + Schema { + instance_type: Some($instance_type.into()), + ..Default::default() + } + } + } + }; +} + +simple_impl!(str => InstanceType::String); +simple_impl!(String => InstanceType::String); +simple_impl!(bool => InstanceType::Boolean); +simple_impl!(f32 => InstanceType::Number); +simple_impl!(f64 => InstanceType::Number); + +impl MakeSchema for char { + fn make_schema() -> Schema { + let mut extra_properties = Map::new(); + extra_properties.insert("minLength".to_owned(), json!(1)); + extra_properties.insert("maxLength".to_owned(), json!(1)); + Schema { + instance_type: Some(InstanceType::String.into()), + extra_properties, + ..Default::default() + } + } +} + +////////// INTS ////////// + +macro_rules! int_impl { + ($type:ident) => { + impl MakeSchema for $type { + fn make_schema() -> Schema { + let mut extra_properties = Map::new(); + // this may be overkill... + extra_properties.insert("minimum".to_owned(), json!($type::min_value())); + extra_properties.insert("maximum".to_owned(), json!($type::max_value())); + Schema { + instance_type: Some(InstanceType::Integer.into()), + extra_properties, + ..Default::default() + } + } + } + }; +} + +int_impl!(i8); +int_impl!(i16); +int_impl!(i32); +int_impl!(i64); +int_impl!(i128); +int_impl!(isize); +int_impl!(u8); +int_impl!(u16); +int_impl!(u32); +int_impl!(u64); +int_impl!(u128); +int_impl!(usize); + +////////// ARRAYS ////////// + +// Does not require T: MakeSchema. +impl MakeSchema for [T; 0] { + fn make_schema() -> Schema { + let mut extra_properties = Map::new(); + extra_properties.insert("maxItems".to_owned(), json!(0)); + Schema { + instance_type: Some(InstanceType::Array.into()), + extra_properties, + ..Default::default() + } + } +} + +macro_rules! array_impls { + ($($len:tt)+) => { + $( + impl MakeSchema for [T; $len] + { + fn make_schema() -> Schema { + let mut extra_properties = Map::new(); + extra_properties.insert("minItems".to_owned(), json!($len)); + extra_properties.insert("maxItems".to_owned(), json!($len)); + Schema { + instance_type: Some(InstanceType::Array.into()), + items: Some(Box::from(T::make_schema())), + extra_properties, + ..Default::default() + } + } + } + )+ + } +} + +array_impls! { + 01 02 03 04 05 06 07 08 09 10 + 11 12 13 14 15 16 17 18 19 20 + 21 22 23 24 25 26 27 28 29 30 + 31 32 +} + +////////// SEQUENCES ///////// + +macro_rules! seq_impl { + ($($desc:tt)+) => { + impl $($desc)+ + where + T: MakeSchema, + { + fn make_schema() -> Schema + { + Schema { + instance_type: Some(InstanceType::Array.into()), + items: Some(Box::from(T::make_schema())), + ..Default::default() + } + } + } + }; +} + +seq_impl!( MakeSchema for std::collections::BinaryHeap); +seq_impl!( MakeSchema for std::collections::BTreeSet); +seq_impl!( MakeSchema for std::collections::HashSet); +seq_impl!( MakeSchema for std::collections::LinkedList); +seq_impl!( MakeSchema for Vec); +seq_impl!( MakeSchema for std::collections::VecDeque); + +////////// OPTION ////////// + +impl MakeSchema for Option { + fn make_schema() -> Schema { + let mut schema = T::make_schema(); + if let Some(instance_type) = schema.instance_type { + let mut vec: Vec<_> = instance_type.into(); + if !vec.contains(&InstanceType::Null) { + vec.push(InstanceType::Null); + } + schema.instance_type = Some(vec.into()); + } + schema + } +} + +////////// DEREF ////////// + +macro_rules! deref_impl { + ($($desc:tt)+) => { + impl $($desc)+ + where + T: ?Sized + MakeSchema, + { + fn make_schema() -> Schema { + T::make_schema() + } + } + }; +} + +deref_impl!(<'a, T> MakeSchema for &'a T); +deref_impl!(<'a, T> MakeSchema for &'a mut T); +deref_impl!( MakeSchema for Box); +deref_impl!( MakeSchema for std::rc::Rc); +deref_impl!( MakeSchema for std::sync::Arc); +deref_impl!(<'a, T: ToOwned> MakeSchema for std::borrow::Cow<'a, T>); diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..f442f3a --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeMap as Map; + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct RootSchema { + #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] + pub schema: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(flatten)] + pub root: Schema, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + #[serde(rename = "$id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub instance_type: Option>, + #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] + pub instance_enum: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub items: Option>, + #[serde(skip_serializing_if = "Map::is_empty")] + pub definitions: Map, + #[serde(flatten)] + pub extra_properties: Map, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum InstanceType { + Null, + Boolean, + Object, + Array, + Number, + String, + Integer, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +pub enum SingleOrVec { + Single(T), + Vec(Vec), +} + +impl From for SingleOrVec { + fn from(single: T) -> Self { + SingleOrVec::Single(single) + } +} + +impl From> for SingleOrVec { + fn from(mut vec: Vec) -> Self { + match vec.len() { + 1 => SingleOrVec::Single(vec.remove(0)), + _ => SingleOrVec::Vec(vec), + } + } +} + +impl Into> for SingleOrVec { + fn into(self) -> Vec { + match self { + SingleOrVec::Single(s) => vec![s], + SingleOrVec::Vec(v) => v, + } + } +} + +/*pub struct Schema { + pub ref_path: Option, + pub description: Option, + pub schema_type: Option, + pub format: Option, + pub enum_values: Option>, + pub required: Option>, + pub items: Option>, + pub properties: Option>, +} +*/