facet_jsonschema/
lib.rs

1#![warn(missing_docs)]
2#![warn(clippy::std_instead_of_core)]
3#![warn(clippy::std_instead_of_alloc)]
4#![forbid(unsafe_code)]
5#![doc = include_str!("../README.md")]
6
7extern crate facet_core as facet;
8use facet::{PointerType, SmartPointerDef};
9use facet_core::{Def, Facet, ScalarDef, Shape, Type, UserType};
10
11use std::io::Write;
12
13/// Convert a `Facet` type to a JSON schema string.
14pub fn to_string<'a, T: Facet<'a>>() -> String {
15    let mut buffer = Vec::new();
16    write!(buffer, "{{").unwrap();
17    write!(
18        buffer,
19        "\"$schema\": \"https://json-schema.org/draft/2020-12/schema\","
20    )
21    .unwrap();
22
23    // Find the first attribute that starts with "id=", if it exists more than once is an error
24    let mut id = T::SHAPE.attributes.iter().filter_map(|attr| match attr {
25        facet_core::ShapeAttribute::Arbitrary(attr_str) => {
26            if attr_str.starts_with("id") {
27                let id = attr_str
28                    .split('=')
29                    .nth(1)
30                    .unwrap_or_default()
31                    .trim()
32                    .trim_matches('"');
33                Some(id)
34            } else {
35                None
36            }
37        }
38        _ => None,
39    });
40    match (id.next(), id.next()) {
41        (Some(_), Some(_)) => panic!("More than one id attribute found"),
42        (Some(id), None) => {
43            write!(buffer, "\"$id\": \"{id}\",").unwrap();
44        }
45        _ => {
46            // No id attribute found, do nothing
47        }
48    }
49
50    serialize(T::SHAPE, &[], &mut buffer).unwrap();
51    write!(buffer, "}}").unwrap();
52    String::from_utf8(buffer).unwrap()
53}
54
55fn serialize<'shape, W: Write>(
56    shape: &'shape Shape<'shape>,
57    doc: &[&str],
58    writer: &mut W,
59) -> std::io::Result<()> {
60    serialize_doc(&[shape.doc, doc].concat(), writer)?;
61
62    // First check the type system (Type)
63    match &shape.ty {
64        Type::User(UserType::Struct(struct_def)) => {
65            serialize_struct(struct_def, writer)?;
66            return Ok(());
67        }
68        Type::User(UserType::Enum(_enum_def)) => {
69            todo!("Enum");
70        }
71        Type::Sequence(sequence_type) => {
72            use facet_core::SequenceType;
73            match sequence_type {
74                SequenceType::Slice(_slice_type) => {
75                    // For slices, use the Def::Slice if available
76                    if let Def::Slice(slice_def) = shape.def {
77                        serialize_slice(slice_def, writer)?;
78                        return Ok(());
79                    }
80                }
81                SequenceType::Array(_array_type) => {
82                    // For arrays, use the Def::Array if available
83                    if let Def::Array(array_def) = shape.def {
84                        serialize_array(array_def, writer)?;
85                        return Ok(());
86                    }
87                }
88            }
89        }
90        _ => {} // Continue to check the def system
91    }
92
93    // Then check the def system (Def)
94    match shape.def {
95        Def::Scalar(ref scalar_def) => serialize_scalar(scalar_def, writer)?,
96        Def::Map(_map_def) => todo!("Map"),
97        Def::List(list_def) => serialize_list(list_def, writer)?,
98        Def::Slice(slice_def) => serialize_slice(slice_def, writer)?,
99        Def::Array(array_def) => serialize_array(array_def, writer)?,
100        Def::Option(option_def) => serialize_option(option_def, writer)?,
101        Def::SmartPointer(SmartPointerDef {
102            pointee: Some(inner_shape),
103            ..
104        }) => serialize(inner_shape(), &[], writer)?,
105        Def::Undefined => {
106            // Handle the case when not yet migrated to the Type enum
107            // For primitives, we can try to infer the type
108            match &shape.ty {
109                Type::Primitive(primitive) => {
110                    use facet_core::{NumericType, PrimitiveType, TextualType};
111                    match primitive {
112                        PrimitiveType::Numeric(NumericType::Float) => {
113                            write!(writer, "\"type\": \"number\", \"format\": \"double\"")?;
114                        }
115                        PrimitiveType::Boolean => {
116                            write!(writer, "\"type\": \"boolean\"")?;
117                        }
118                        PrimitiveType::Textual(TextualType::Str) => {
119                            write!(writer, "\"type\": \"string\"")?;
120                        }
121                        _ => {
122                            write!(writer, "\"type\": \"unknown\"")?;
123                        }
124                    }
125                }
126                Type::Pointer(PointerType::Reference(pt) | PointerType::Raw(pt)) => {
127                    serialize((pt.target)(), &[], writer)?
128                }
129                _ => {
130                    write!(writer, "\"type\": \"unknown\"")?;
131                }
132            }
133        }
134        _ => {
135            write!(writer, "\"type\": \"unknown\"")?;
136        }
137    }
138
139    Ok(())
140}
141
142fn serialize_doc<W: Write>(doc: &[&str], writer: &mut W) -> Result<(), std::io::Error> {
143    if !doc.is_empty() {
144        let doc = doc.join("\n");
145        write!(writer, "\"description\": \"{}\",", doc.trim())?;
146    }
147    Ok(())
148}
149
150/// Serialize a scalar definition to JSON schema format.
151fn serialize_scalar<W: Write>(scalar_def: &ScalarDef, writer: &mut W) -> std::io::Result<()> {
152    match scalar_def.affinity {
153        facet_core::ScalarAffinity::Number(number_affinity) => {
154            match number_affinity.bits {
155                facet_core::NumberBits::Integer { size, sign } => {
156                    write!(writer, "\"type\": \"integer\"")?;
157                    let bits = match size {
158                        facet_core::IntegerSize::Fixed(bits) => bits,
159                        facet_core::IntegerSize::PointerSized => core::mem::size_of::<usize>() * 8,
160                    };
161                    match sign {
162                        facet_core::Signedness::Unsigned => {
163                            write!(writer, ", \"format\": \"uint{bits}\"")?;
164                            write!(writer, ", \"minimum\": 0")?;
165                        }
166                        facet_core::Signedness::Signed => {
167                            write!(writer, ", \"format\": \"int{bits}\"")?;
168                        }
169                    }
170                }
171                facet_core::NumberBits::Float { .. } => {
172                    write!(writer, "\"type\": \"number\"")?;
173                    write!(writer, ", \"format\": \"double\"")?;
174                }
175                _ => unimplemented!(),
176            }
177            Ok(())
178        }
179        facet_core::ScalarAffinity::String(_) => {
180            write!(writer, "\"type\": \"string\"")?;
181            Ok(())
182        }
183        facet_core::ScalarAffinity::Boolean(_) => {
184            write!(writer, "\"type\": \"boolean\"")?;
185            Ok(())
186        }
187        _ => Err(std::io::Error::other(format!(
188            "facet-jsonschema: nsupported scalar type: {scalar_def:#?}"
189        ))),
190    }
191}
192
193fn serialize_struct<W: Write>(
194    struct_type: &facet_core::StructType,
195    writer: &mut W,
196) -> std::io::Result<()> {
197    write!(writer, "\"type\": \"object\",")?;
198    let required = struct_type
199        .fields
200        .iter()
201        .map(|f| format!("\"{}\"", f.name))
202        .collect::<Vec<_>>()
203        .join(",");
204    write!(writer, "\"required\": [{required}],")?;
205    write!(writer, "\"properties\": {{")?;
206    let mut first = true;
207    for field in struct_type.fields {
208        if !first {
209            write!(writer, ",")?;
210        }
211        first = false;
212        write!(writer, "\"{}\": {{", field.name)?;
213        serialize(field.shape(), field.doc, writer)?;
214        write!(writer, "}}")?;
215    }
216    write!(writer, "}}")?;
217    Ok(())
218}
219
220/// Serialize a list definition to JSON schema format.
221fn serialize_list<W: Write>(list_def: facet_core::ListDef, writer: &mut W) -> std::io::Result<()> {
222    write!(writer, "\"type\": \"array\",")?;
223    write!(writer, "\"items\": {{")?;
224    serialize(list_def.t(), &[], writer)?;
225    write!(writer, "}}")?;
226    Ok(())
227}
228
229/// Serialize a slice definition to JSON schema format.
230fn serialize_slice<W: Write>(
231    slice_def: facet_core::SliceDef,
232    writer: &mut W,
233) -> std::io::Result<()> {
234    write!(writer, "\"type\": \"array\",")?;
235    write!(writer, "\"items\": {{")?;
236    serialize(slice_def.t(), &[], writer)?;
237    write!(writer, "}}")?;
238    Ok(())
239}
240
241/// Serialize an array definition to JSON schema format.
242fn serialize_array<W: Write>(
243    array_def: facet_core::ArrayDef,
244    writer: &mut W,
245) -> std::io::Result<()> {
246    write!(writer, "\"type\": \"array\",")?;
247    write!(writer, "\"minItems\": {},", array_def.n)?;
248    write!(writer, "\"maxItems\": {},", array_def.n)?;
249    write!(writer, "\"items\": {{")?;
250    serialize(array_def.t(), &[], writer)?;
251    write!(writer, "}}")?;
252    Ok(())
253}
254
255/// Serialize an option definition to JSON schema format.
256fn serialize_option<W: Write>(
257    _option_def: facet_core::OptionDef,
258    writer: &mut W,
259) -> std::io::Result<()> {
260    write!(writer, "\"type\": \"[]\",")?;
261    unimplemented!("serialize_option");
262}
263
264#[cfg(test)]
265mod tests {
266    extern crate alloc;
267    use alloc::{rc::Rc, sync::Arc};
268
269    use super::*;
270    use facet_macros::Facet;
271    use insta::assert_snapshot;
272
273    #[test]
274    fn test_basic() {
275        /// Test documentation
276        #[derive(Facet)]
277        #[facet(id = "http://example.com/schema")]
278        struct TestStruct {
279            /// Test doc1
280            string_field: String,
281            /// Test doc2
282            int_field: u32,
283            vec_field: Vec<bool>,
284            slice_field: &'static [f64],
285            array_field: [f64; 3],
286        }
287
288        let schema = to_string::<TestStruct>();
289        assert_snapshot!(schema);
290    }
291
292    #[test]
293    fn test_pointers() {
294        /// Test documentation
295        #[derive(Facet)]
296        #[facet(id = "http://example.com/schema")]
297        struct TestStruct<'a> {
298            normal_pointer: &'a str,
299            box_pointer: Box<u32>,
300            arc: Arc<u32>,
301            rc: Rc<u32>,
302            #[allow(clippy::redundant_allocation)]
303            nested: Rc<&'a Arc<&'a *const u32>>,
304        }
305
306        let schema = to_string::<TestStruct>();
307        assert_snapshot!(schema);
308    }
309}