diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml index 1427b7d..f694cb6 100644 --- a/schemars/Cargo.toml +++ b/schemars/Cargo.toml @@ -9,3 +9,4 @@ edition = "2018" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +schemars_derive = { version = "0.1.0", path = "../schemars_derive" } diff --git a/schemars/src/generator.rs b/schemars/src/generator.rs index 6bde36f..9d20905 100644 --- a/schemars/src/generator.rs +++ b/schemars/src/generator.rs @@ -76,7 +76,6 @@ impl SchemaGenerator { fn make_unique_name(&mut self) -> String { let base_name = T::schema_name(); - // TODO remove namespace, remove special chars if self.names.contains(&base_name) { for i in 2.. { let name = format!("{}{}", base_name, i); diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs index b095a42..13f3091 100644 --- a/schemars/src/lib.rs +++ b/schemars/src/lib.rs @@ -1,11 +1,8 @@ +pub mod generator; pub mod make_schema; pub mod schema; -pub mod generator; -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} +pub use generator::SchemaGenerator; +pub use schema::{Schema, SchemaObject, SchemaRef}; + +pub use schemars_derive::*; diff --git a/schemars/src/main.rs b/schemars/src/main.rs index ec8cfc4..e77c95f 100644 --- a/schemars/src/main.rs +++ b/schemars/src/main.rs @@ -1,9 +1,4 @@ -pub mod generator; -pub mod make_schema; -pub mod schema; - -use make_schema::MakeSchema; -use schema::*; +use schemars::MakeSchema; use serde::{Deserialize, Serialize}; use serde_json::Result; @@ -16,43 +11,26 @@ enum TodoStatus { Archived, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, MakeSchema)] #[serde(rename_all = "camelCase")] struct Todo { id: u64, title: String, description: Option, - status: TodoStatus, + // status: TodoStatus, assigned_to: Vec, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, MakeSchema)] #[serde(rename_all = "camelCase")] struct User { id: u64, username: String, } -impl MakeSchema for User { - fn generates_ref_schema() -> bool { - true - } - - fn make_schema(gen: &mut generator::SchemaGenerator) -> Schema { - let mut o = SchemaObject { - ..Default::default() - }; - o.properties - .insert("id".to_owned(), gen.subschema_for::()); - o.properties - .insert("username".to_owned(), gen.subschema_for::()); - o.into() - } -} - fn main() -> Result<()> { - let gen = generator::SchemaGenerator::new(); - let schema = gen.into_root_schema_for::(); + let gen = schemars::SchemaGenerator::new(); + let schema = gen.into_root_schema_for::(); let json = serde_json::to_string_pretty(&schema)?; println!("{}", json); diff --git a/schemars/src/make_schema.rs b/schemars/src/make_schema.rs index 9b08437..e53c376 100644 --- a/schemars/src/make_schema.rs +++ b/schemars/src/make_schema.rs @@ -17,7 +17,7 @@ pub trait MakeSchema { // It's probably worth removing the default implemenation, // then make every impl in this file set an explicit name // Or maybe hide it under feature flag? - core::any::type_name::().to_owned() + core::any::type_name::().replace(|c: char| !c.is_ascii_alphanumeric(), "_") } fn generates_ref_schema() -> bool { diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index 10fdc99..120ef76 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap as Map; +// TODO use serde_json::Map (or some other wrapper) instead of BTreeMap to ensure preserve_order is possible + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum Schema { diff --git a/schemars_derive/.gitignore b/schemars_derive/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/schemars_derive/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/schemars_derive/Cargo.toml b/schemars_derive/Cargo.toml new file mode 100644 index 0000000..bc3ee46 --- /dev/null +++ b/schemars_derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "schemars_derive" +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 + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "0.4" +quote = "0.6.13" +syn = { version = "0.15.22", features = ["visit"] } # do we need visit? +serde_derive_internals = "0.24.1" diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs new file mode 100644 index 0000000..4de6832 --- /dev/null +++ b/schemars_derive/src/lib.rs @@ -0,0 +1,56 @@ +#[macro_use] +extern crate quote; +#[macro_use] +extern crate syn; + +extern crate proc_macro; +extern crate proc_macro2; + +use proc_macro2::TokenStream; +use syn::spanned::Spanned; +use syn::{Data, DeriveInput, Fields}; + +#[proc_macro_derive(MakeSchema, attributes(schemars, serde))] +pub fn derive_make_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let fn_contents = match input.data { + Data::Struct(data) => struct_implementation(&data.fields), + _ => unimplemented!("Only structs work so far!"), + }; + + let impl_block = quote! { + #[automatically_derived] + impl #impl_generics schemars::make_schema::MakeSchema for #name #ty_generics #where_clause { + fn make_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema { + let mut o = schemars::SchemaObject { + ..Default::default() + }; + #fn_contents + o.into() + } + }; + }; + proc_macro::TokenStream::from(impl_block) +} + +fn struct_implementation(fields: &Fields) -> TokenStream { + match fields { + Fields::Named(ref fields) => { + let recurse = fields.named.iter().map(|f| { + let name = (&f.ident).as_ref().map(ToString::to_string); + let ty = &f.ty; + quote_spanned! {f.span()=> + o.properties.insert(#name.to_owned(), gen.subschema_for::<#ty>()); + } + }); + quote! { + #(#recurse)* + } + } + _ => unimplemented!("Only named fields work so far!"), + } +}