typify_macro/
lib.rs

1// Copyright 2024 Oxide Computer Company
2
3//! typify macro implementation.
4
5#![deny(missing_docs)]
6
7use std::{collections::HashMap, path::Path};
8
9use proc_macro::TokenStream;
10use quote::{quote, ToTokens};
11use serde::Deserialize;
12use serde_tokenstream::ParseWrapper;
13use syn::LitStr;
14use token_utils::TypeAndImpls;
15use typify_impl::{
16    CrateVers, MapType, TypeSpace, TypeSpacePatch, TypeSpaceSettings, UnknownPolicy,
17};
18
19mod token_utils;
20
21/// Import types from a schema file. This may be invoked with simply a pathname
22/// for a JSON Schema file (relative to `$CARGO_MANIFEST_DIR`), or it may be
23/// invoked with a structured form:
24/// ```
25/// use typify_macro::import_types;
26/// import_types!(
27///     schema = "../example.json",
28///     derives = [schemars::JsonSchema],
29/// );
30/// ```
31///
32/// - `schema`: string literal; the JSON schema file
33///
34/// - `derives`: optional array of derive macro paths; the derive macros to be
35///   applied to all generated types
36///
37/// - `struct_builder`: optional boolean; (if true) generates a `::builder()`
38///   method for each generated struct that can be used to specify each
39///   property and construct the struct
40///
41/// - `unknown_crates`: optional policy regarding the handling of schemas that
42///   contain the `x-rust-type` extension whose crates are not explicitly named
43///   in the `crates` section. The options are `generate` to ignore the
44///   extension and generate a *de novo* type, `allow` to use the named type
45///   (which may require the addition of a new dependency to compile, and which
46///   ignores version compatibility checks), or `deny` to produce a
47///   compile-time error (requiring the user to specify the crate's disposition
48///   in the `crates` section).
49///
50/// - `crates`: optional map from crate name to the version of the crate in
51///   use. Types encountered with the Rust type extension (`x-rust-type`) will
52///   use types from the specified crates rather than generating them (within
53///   the constraints of type compatibility).
54///
55/// - `patch`: optional map from type to an object with the optional members
56///   `rename` and `derives`. This may be used to renamed generated types or
57///   to apply additional (non-default) derive macros to them.
58///
59/// - `replace`: optional map from definition name to a replacement type. This
60///   may be used to skip generation of the named type and use a existing Rust
61///   type.
62///   
63/// - `convert`: optional map from a JSON schema type defined in `$defs` to a
64///   replacement type. This may be used to skip generation of the schema and
65///   use an existing Rust type.
66#[proc_macro]
67pub fn import_types(item: TokenStream) -> TokenStream {
68    match do_import_types(item) {
69        Err(err) => err.to_compile_error().into(),
70        Ok(out) => out,
71    }
72}
73
74#[derive(Deserialize)]
75struct MacroSettings {
76    schema: ParseWrapper<LitStr>,
77    #[serde(default)]
78    derives: Vec<ParseWrapper<syn::Path>>,
79    #[serde(default)]
80    struct_builder: bool,
81
82    #[serde(default)]
83    unknown_crates: UnknownPolicy,
84    #[serde(default)]
85    crates: HashMap<CrateName, MacroCrateSpec>,
86    #[serde(default)]
87    map_type: MapType,
88
89    #[serde(default)]
90    patch: HashMap<ParseWrapper<syn::Ident>, MacroPatch>,
91    #[serde(default)]
92    replace: HashMap<ParseWrapper<syn::Ident>, ParseWrapper<TypeAndImpls>>,
93    #[serde(default)]
94    convert:
95        serde_tokenstream::OrderedMap<schemars::schema::SchemaObject, ParseWrapper<TypeAndImpls>>,
96}
97
98struct MacroCrateSpec {
99    original: Option<String>,
100    version: CrateVers,
101}
102
103impl<'de> Deserialize<'de> for MacroCrateSpec {
104    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105    where
106        D: serde::Deserializer<'de>,
107    {
108        let ss = String::deserialize(deserializer)?;
109
110        let (original, vers_str) = if let Some(ii) = ss.find('@') {
111            let original_str = &ss[..ii];
112            let rest = &ss[ii + 1..];
113            if !is_crate(original_str) {
114                return Err(<D::Error as serde::de::Error>::invalid_value(
115                    serde::de::Unexpected::Str(&ss),
116                    &"valid crate name",
117                ));
118            }
119
120            (Some(original_str.to_string()), rest)
121        } else {
122            (None, ss.as_ref())
123        };
124
125        let Some(version) = CrateVers::parse(vers_str) else {
126            return Err(<D::Error as serde::de::Error>::invalid_value(
127                serde::de::Unexpected::Str(&ss),
128                &"valid version",
129            ));
130        };
131
132        Ok(Self { original, version })
133    }
134}
135
136#[derive(Hash, PartialEq, Eq)]
137struct CrateName(String);
138impl<'de> Deserialize<'de> for CrateName {
139    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140    where
141        D: serde::Deserializer<'de>,
142    {
143        let ss = String::deserialize(deserializer)?;
144
145        if is_crate(&ss) {
146            Ok(Self(ss))
147        } else {
148            Err(<D::Error as serde::de::Error>::invalid_value(
149                serde::de::Unexpected::Str(&ss),
150                &"valid crate name",
151            ))
152        }
153    }
154}
155
156fn is_crate(s: &str) -> bool {
157    !s.contains(|cc: char| !cc.is_alphanumeric() && cc != '_' && cc != '-')
158}
159
160#[derive(Deserialize)]
161struct MacroPatch {
162    #[serde(default)]
163    rename: Option<String>,
164    #[serde(default)]
165    derives: Vec<ParseWrapper<syn::Path>>,
166}
167
168impl From<MacroPatch> for TypeSpacePatch {
169    fn from(a: MacroPatch) -> Self {
170        let mut s = Self::default();
171        a.rename.iter().for_each(|rename| {
172            s.with_rename(rename);
173        });
174        a.derives.iter().for_each(|derive| {
175            s.with_derive(derive.to_token_stream());
176        });
177        s
178    }
179}
180
181fn do_import_types(item: TokenStream) -> Result<TokenStream, syn::Error> {
182    // Allow the caller to give us either a simple string or a compound object.
183    let (schema, settings) = if let Ok(ll) = syn::parse::<LitStr>(item.clone()) {
184        (ll, TypeSpaceSettings::default())
185    } else {
186        let MacroSettings {
187            schema,
188            derives,
189            replace,
190            patch,
191            struct_builder,
192            convert,
193            unknown_crates,
194            crates,
195            map_type,
196        } = serde_tokenstream::from_tokenstream(&item.into())?;
197        let mut settings = TypeSpaceSettings::default();
198        derives.into_iter().for_each(|derive| {
199            settings.with_derive(derive.to_token_stream().to_string());
200        });
201        settings.with_struct_builder(struct_builder);
202
203        patch.into_iter().for_each(|(type_name, patch)| {
204            settings.with_patch(type_name.to_token_stream(), &patch.into());
205        });
206        replace.into_iter().for_each(|(type_name, type_and_impls)| {
207            let (replace_type, impls) = type_and_impls.into_inner().into_name_and_impls();
208            settings.with_replacement(type_name.to_token_stream(), replace_type, impls.into_iter());
209        });
210        convert.into_iter().for_each(|(schema, type_and_impls)| {
211            let (type_name, impls) = type_and_impls.into_inner().into_name_and_impls();
212            settings.with_conversion(schema, type_name, impls);
213        });
214
215        crates.into_iter().for_each(
216            |(CrateName(crate_name), MacroCrateSpec { original, version })| {
217                if let Some(original_crate) = original {
218                    settings.with_crate(original_crate, version, Some(&crate_name));
219                } else {
220                    settings.with_crate(crate_name, version, None);
221                }
222            },
223        );
224        settings.with_unknown_crates(unknown_crates);
225
226        settings.with_map_type(map_type);
227
228        (schema.into_inner(), settings)
229    };
230
231    let dir = std::env::var("CARGO_MANIFEST_DIR").map_or_else(
232        |_| std::env::current_dir().unwrap(),
233        |s| Path::new(&s).to_path_buf(),
234    );
235
236    let path = dir.join(schema.value());
237
238    let root_schema: schemars::schema::RootSchema =
239        serde_json::from_reader(std::fs::File::open(&path).map_err(|e| {
240            syn::Error::new(
241                schema.span(),
242                format!("couldn't read file {}: {}", schema.value(), e),
243            )
244        })?)
245        .unwrap();
246
247    let mut type_space = TypeSpace::new(&settings);
248    type_space
249        .add_root_schema(root_schema)
250        .map_err(|e| into_syn_err(e, schema.span()))?;
251
252    let path_str = path.to_string_lossy();
253    let output = quote! {
254        #type_space
255
256        // Force a rebuild when the given file is modified.
257        const _: &str = include_str!(#path_str);
258    };
259
260    Ok(output.into())
261}
262
263fn into_syn_err(e: typify_impl::Error, span: proc_macro2::Span) -> syn::Error {
264    syn::Error::new(span, e.to_string())
265}