1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
// This would be nice once it stabilizes: // https://github.com/rust-lang/rust/issues/44732 // #![feature(external_doc)] // #![doc(include = "../README.md")] //! This is a Rust crate which can take a [json schema (draft //! 4)](http://json-schema.org/) and generate Rust types which are //! serializable with [serde](https://serde.rs/). No checking such as //! `min_value` are done but instead only the structure of the schema //! is followed as closely as possible. //! //! As a schema could be arbitrarily complex this crate makes no //! guarantee that it can generate good types or even any types at all //! for a given schema but the crate does manage to bootstrap itself //! which is kind of cool. //! //! ## Example //! //! Generated types for VS Codes [debug server protocol][]: <https://docs.rs/debugserver-types> //! //! [debug server protocol]:https://code.visualstudio.com/docs/extensions/example-debuggers //! //! ## Usage //! //! Rust types can be generated by passing a path to a JSON schema to the [`schemafy`] //! procedural macro. //! //! ```rust //! extern crate serde; //! extern crate schemafy_core; //! extern crate serde_json; //! //! use serde::{Serialize, Deserialize}; //! //! schemafy::schemafy!( //! "tests/nested.json" //! ); //! //! schemafy::schemafy!( //! root: Schema // Optional name for the root type (if one exists) //! "schemafy_lib/src/schema.json" //! ); //! //! //! fn main() -> Result<(), Box<dyn std::error::Error>> { //! let nested: Defnested = serde_json::from_str(r#"{ "append": "abc" }"#)?; //! assert_eq!(nested.append, Some("abc".to_string())); //! Ok(()) //! } //! ``` use std::path::PathBuf; use schemafy_lib::Expander; /// A configurable builder for generating Rust types from a JSON /// schema. /// /// The default options are usually fine. In that case, you can use /// the [`generate()`](fn.generate.html) convenience method instead. struct GenerateBuilder<'a> { /// The name of the root type defined by the schema. If the schema /// does not define a root type (some schemas are simply a /// collection of definitions) then simply pass `None`. pub root_name: Option<String>, /// The module path to this crate. Some generated code may make /// use of types defined in this crate. Unless you have /// re-exported this crate or imported it under a different name, /// the default should be fine. pub schemafy_path: &'a str, } impl<'a> Default for GenerateBuilder<'a> { fn default() -> Self { GenerateBuilder { root_name: None, schemafy_path: "::schemafy_core::", } } } impl<'a> GenerateBuilder<'a> { fn build_tokens(mut self, tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { struct Def { root: Option<String>, input_file: syn::LitStr, } impl syn::parse::Parse for Def { fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> { let root = if input.peek(syn::Ident) { let root_ident: syn::Ident = input.parse()?; if root_ident != "root" { return Err(syn::Error::new(root_ident.span(), "Expected `root`")); } input.parse::<syn::Token![:]>()?; Some(input.parse::<syn::Ident>()?.to_string()) } else { None }; Ok(Def { root, input_file: input.parse()?, }) } } let def = syn::parse_macro_input!(tokens as Def); self.root_name = def.root; let input_file = PathBuf::from(def.input_file.value()); let crate_root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let input_path = if input_file.is_relative() { crate_root.join(input_file) } else { input_file }; let json = std::fs::read_to_string(&input_path).unwrap_or_else(|err| { panic!("Unable to read `{}`: {}", input_path.to_string_lossy(), err) }); let schema = serde_json::from_str(&json).unwrap_or_else(|err| panic!("{}", err)); let mut expander = Expander::new(self.root_name.as_deref(), self.schemafy_path, &schema); expander.expand(&schema).into() } } /// Generate Rust types from a JSON schema. /// /// If the `root` parameter is supplied, then a type will be /// generated from the root of the schema. /// /// ```rust /// extern crate serde; /// extern crate schemafy_core; /// extern crate serde_json; /// /// use serde::{Serialize, Deserialize}; /// /// schemafy::schemafy!( /// root: MyRoot // Optional name for the root type (if one exists) /// "tests/nested.json" /// ); /// /// fn main() -> Result<(), Box<dyn std::error::Error>> { /// let nested: Defnested = serde_json::from_str(r#"{ "append": "abc" }"#)?; /// assert_eq!(nested.append, Some("abc".to_string())); /// Ok(()) /// } /// ``` #[proc_macro] pub fn schemafy(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { GenerateBuilder { ..GenerateBuilder::default() } .build_tokens(tokens) } #[doc(hidden)] #[proc_macro] pub fn regenerate(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { use std::process::Command; let tokens = GenerateBuilder { ..GenerateBuilder::default() } .build_tokens(tokens); { let out = tokens.to_string(); let schema_path = "schemafy_lib/src/schema.rs"; std::fs::write(schema_path, &out).unwrap(); Command::new("rustfmt").arg(schema_path).output().unwrap(); } tokens }