json_shape_build/
lib.rs

1//! Build-time compiler for `json_shape`
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    path::{Path, PathBuf},
6};
7
8use checksum::crc32::Crc32;
9use codegen::{Field, Scope, Variant};
10use convert_case::{Case, Casing};
11use json_shape::JsonShape;
12
13#[cfg(test)]
14mod test;
15
16/// Include generated json shapes as serializable structs.
17///
18/// You must specify the json collections name.
19///
20/// ```rust,ignore
21/// mod shapes {
22///     json_shape_build::include_json_shape!("helloworld");
23/// }
24/// ```
25///
26/// > # Note:
27/// > **This only works if the `json_shape_build` output directory has been unmodified**.
28/// > The default output directory is set to the [`OUT_DIR`] environment variable.
29/// > If the output directory has been modified, the following pattern may be used
30/// > instead of this macro.
31///
32/// ```rust,ignore
33/// mod shapes {
34///     include!("/relative/json_shape/directory/helloworld.rs");
35/// }
36/// ```
37/// You can also use a custom environment variable using the following pattern.
38/// ```rust,ignore
39/// mod shapes {
40///     include!(concat!(env!("JSON_SHAPE_DIR"), "/helloworld.rs"));
41/// }
42/// ```
43///
44/// [`OUT_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts
45#[macro_export]
46macro_rules! include_json_shape {
47    ($package: tt) => {
48        include!(concat!(
49            env!("OUT_DIR"),
50            concat!("/", $package, ".gen.shape.rs")
51        ));
52    };
53}
54
55/// Simple `.json` shape compiling.
56///
57/// The include directory will be the parent folder of the specified path.
58/// The package name will be the filename without the extension.
59///
60/// In your `build.rs`:
61/// ```rust
62/// let dir = env!("CARGO_MANIFEST_DIR");
63/// let extension = "fixture/object.json";
64/// let path = std::path::Path::new(dir).join(extension);
65/// json_shape_build::compile_json("collection_name", &[path]);
66/// ```
67///
68///
69/// # Errors
70/// - failed to write json shape file
71#[allow(clippy::missing_panics_doc)]
72pub fn compile_json(
73    collection_name: &'static str,
74    jsons: &[impl AsRef<Path>],
75) -> std::io::Result<String> {
76    for path in jsons {
77        println!("cargo:rerun-if-changed={}", path.as_ref().display());
78    }
79
80    let sources = jsons
81        .iter()
82        .filter_map(|path| path.as_ref().to_str())
83        .map(std::fs::read_to_string)
84        .collect::<Result<Vec<String>, std::io::Error>>()?;
85
86    let shape = json_shape::JsonShape::from_sources(&sources).map_err(std::io::Error::other)?;
87    let target: PathBuf =
88        std::env::var_os("OUT_DIR").map_or_else(|| std::env::current_dir().unwrap(), PathBuf::from);
89    let target = target.join(collection_name).with_extension("gen.shape.rs");
90
91    let mut scope = Scope::new();
92
93    first_pass(&shape, &mut scope);
94    let content = format!(
95        "//! Generated `JsonShape` file.\nuse serde;\n\n{}",
96        scope.to_string()
97    );
98    std::fs::write(target, content)?;
99    Ok(scope.to_string())
100}
101
102pub(crate) fn first_pass(shape: &JsonShape, scope: &mut Scope) {
103    match &shape {
104        json_shape::JsonShape::Null => {
105            scope.new_type_alias("Void", "()").vis("pub");
106        }
107        json_shape::JsonShape::Bool { optional } => {
108            if *optional {
109                scope
110                    .new_type_alias("NullableBool", "Option<bool>")
111                    .vis("pub");
112            } else {
113                scope.new_type_alias("Bool", "bool").vis("pub");
114            }
115        }
116        json_shape::JsonShape::Number { optional } => {
117            if *optional {
118                scope
119                    .new_type_alias("NullableNumber", "Option<f64>")
120                    .vis("pub");
121            } else {
122                scope.new_type_alias("Number", "f64").vis("pub");
123            }
124        }
125        json_shape::JsonShape::String { optional } => {
126            if *optional {
127                scope
128                    .new_type_alias("NullableStr", "Option<String>")
129                    .vis("pub");
130            } else {
131                scope.new_type_alias("Str", "String").vis("pub");
132            }
133        }
134        json_shape::JsonShape::Array {
135            r#type: inner,
136            optional,
137        } => {
138            let name = shape_name(shape);
139            create_array(scope, &name, *optional, inner);
140            create_subtype(scope, inner);
141        }
142        json_shape::JsonShape::Object { content, .. } => {
143            let name = shape_name(shape);
144            create_object(scope, &name, content);
145            for inner in content.values() {
146                create_subtype(scope, inner);
147            }
148        }
149        json_shape::JsonShape::OneOf { variants, .. } => {
150            let name = shape_name(shape);
151            create_enum(scope, &name, variants);
152            for inner in variants {
153                create_subtype(scope, inner);
154            }
155        }
156        json_shape::JsonShape::Tuple { elements, optional } => {
157            let name = shape_name(shape);
158            create_tuple(scope, &name, *optional, elements);
159            for inner in elements {
160                create_subtype(scope, inner);
161            }
162        }
163    }
164}
165
166fn create_subtype(scope: &mut Scope, shape: &JsonShape) {
167    match shape {
168        json_shape::JsonShape::Array {
169            r#type: inner,
170            optional: _,
171        } => {
172            create_subtype(scope, inner);
173        }
174        json_shape::JsonShape::Object { content, .. } => {
175            let name = shape_name(shape);
176            create_object(scope, &name, content);
177            for inner in content.values() {
178                create_subtype(scope, inner);
179            }
180        }
181        json_shape::JsonShape::OneOf { variants, .. } => {
182            let name = shape_name(shape);
183            create_enum(scope, &name, variants);
184            for inner in variants {
185                create_subtype(scope, inner);
186            }
187        }
188        json_shape::JsonShape::Tuple {
189            elements,
190            optional: _,
191        } => {
192            for inner in elements {
193                create_subtype(scope, inner);
194            }
195        }
196        _ => {}
197    }
198}
199
200fn create_object(scope: &mut Scope, name: &str, content: &BTreeMap<String, json_shape::JsonShape>) {
201    let struct_data = scope
202        .new_struct(name)
203        .vis("pub")
204        .derive("Debug")
205        .derive("Clone")
206        .derive("serde::Serialize")
207        .derive("serde::Deserialize");
208    for (name, r#type) in content {
209        let mut field = Field::new(&name.to_case(Case::Snake), shape_representation(r#type));
210        field.vis("pub");
211        struct_data.push_field(field);
212    }
213}
214
215fn create_enum(scope: &mut Scope, name: &str, variants: &BTreeSet<json_shape::JsonShape>) {
216    let variants = variants
217        .iter()
218        .map(|shape| (shape_name(shape), shape_representation(shape)))
219        .map(|(name, representation)| {
220            let mut var = Variant::new(name);
221            var.tuple(&representation);
222            var
223        });
224    let enum_data = scope
225        .new_enum(name)
226        .vis("pub")
227        .derive("Debug")
228        .derive("Clone")
229        .derive("serde::Serialize")
230        .derive("serde::Deserialize");
231    for var in variants {
232        enum_data.push_variant(var);
233    }
234}
235
236fn create_array(scope: &mut Scope, name: &str, optional: bool, r#type: &JsonShape) {
237    let target = if optional {
238        format!("Option<Vec<{}>>", shape_representation(r#type))
239    } else {
240        format!("Vec<{}>", shape_representation(r#type))
241    };
242    scope.new_type_alias(name, target).vis("pub");
243}
244
245fn create_tuple(scope: &mut Scope, name: &str, optional: bool, elements: &[json_shape::JsonShape]) {
246    let representations = elements
247        .iter()
248        .map(shape_representation)
249        .collect::<Vec<_>>()
250        .join(", ");
251    let target = format!(
252        "{}{}({representations}){}",
253        if optional { "Option" } else { "" },
254        if optional { "<" } else { "" },
255        if optional { ">" } else { "" }
256    );
257    scope.new_type_alias(name, target).vis("pub");
258}
259
260fn shape_representation(shape: &JsonShape) -> String {
261    match shape {
262        JsonShape::Null => "()".to_string(),
263        JsonShape::Bool { optional } => {
264            if *optional {
265                "Option<bool>".to_string()
266            } else {
267                "bool".to_string()
268            }
269        }
270        JsonShape::Number { optional } => {
271            if *optional {
272                "Option<f64>".to_string()
273            } else {
274                "f64".to_string()
275            }
276        }
277        JsonShape::String { optional } => {
278            if *optional {
279                "Option<String>".to_string()
280            } else {
281                "String".to_string()
282            }
283        }
284        JsonShape::Array { r#type, optional } => {
285            let sub_shape = shape_representation(r#type);
286            if *optional {
287                format!("Optional<Vec<{sub_shape}>>")
288            } else {
289                format!("Vec<{sub_shape}>")
290            }
291        }
292        JsonShape::Object {
293            content: _,
294            optional,
295        }
296        | JsonShape::OneOf {
297            variants: _,
298            optional,
299        } => {
300            let name = shape_name(shape);
301            if *optional {
302                format!("Option<{name}>")
303            } else {
304                name
305            }
306        }
307        JsonShape::Tuple { elements, optional } => {
308            let sub_shapes = elements
309                .iter()
310                .map(shape_representation)
311                .collect::<Vec<_>>()
312                .join(", ");
313            if *optional {
314                format!("Option<({sub_shapes})>")
315            } else {
316                format!("({sub_shapes})")
317            }
318        }
319    }
320}
321
322fn shape_name(shape: &JsonShape) -> String {
323    match shape {
324        JsonShape::Null => "Null".to_string(),
325        JsonShape::Bool { optional } => {
326            if *optional {
327                "OptionalBool".to_string()
328            } else {
329                "Bool".to_string()
330            }
331        }
332        JsonShape::Number { optional } => {
333            if *optional {
334                "OptionalNumber".to_string()
335            } else {
336                "Number".to_string()
337            }
338        }
339        JsonShape::String { optional } => {
340            if *optional {
341                "OptionalStr".to_string()
342            } else {
343                "Str".to_string()
344            }
345        }
346        JsonShape::Array { r#type, optional } => {
347            let sub_shape = shape_name(r#type);
348            if *optional {
349                format!("OptionalArrayOf{sub_shape}")
350            } else {
351                format!("ArrayOf{sub_shape}")
352            }
353        }
354        JsonShape::Object { content, optional } => {
355            let len = content.len();
356            let sub_shapes = content
357                .values()
358                .map(shape_name)
359                .collect::<String>()
360                .to_case(convert_case::Case::Pascal);
361            let mut crc = Crc32::new();
362            crc.update(sub_shapes.as_bytes());
363            crc.finalize();
364            let name = format!("{:X}", crc.getsum());
365
366            if *optional {
367                format!("OptionalStruct{len}Crc{name}")
368            } else {
369                format!("Struct{len}Crc{name}")
370            }
371        }
372        JsonShape::OneOf { variants, optional } => {
373            let len = variants.len();
374            let sub_shapes = variants
375                .iter()
376                .map(shape_name)
377                .collect::<String>()
378                .to_case(convert_case::Case::Pascal);
379            let mut crc = Crc32::new();
380            crc.update(sub_shapes.as_bytes());
381            crc.finalize();
382            let name = format!("{:X}", crc.getsum());
383            if *optional {
384                format!("OptionalEnum{len}Crc{name}")
385            } else {
386                format!("Enum{len}Crc{name}")
387            }
388        }
389        JsonShape::Tuple { elements, optional } => {
390            let len = elements.len();
391            let sub_shapes = elements
392                .iter()
393                .map(shape_representation)
394                .collect::<String>()
395                .to_case(convert_case::Case::Pascal);
396            let mut crc = Crc32::new();
397            crc.update(sub_shapes.as_bytes());
398            crc.finalize();
399            let name = format!("{:X}", crc.getsum());
400
401            if *optional {
402                format!("OptionalTuple{len}Crc{name}")
403            } else {
404                format!("Tuple{len}Crc{name}")
405            }
406        }
407    }
408}