facet_jsonschema/
lib.rs

1#![warn(missing_docs)]
2#![warn(clippy::std_instead_of_core)]
3#![warn(clippy::std_instead_of_alloc)]
4#![deny(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                _ => {} // Handle other sequence types
89            }
90        }
91        _ => {} // Continue to check the def system
92    }
93
94    // Then check the def system (Def)
95    match shape.def {
96        Def::Scalar(ref scalar_def) => serialize_scalar(scalar_def, writer)?,
97        Def::Map(_map_def) => todo!("Map"),
98        Def::List(list_def) => serialize_list(list_def, writer)?,
99        Def::Slice(slice_def) => serialize_slice(slice_def, writer)?,
100        Def::Array(array_def) => serialize_array(array_def, writer)?,
101        Def::Option(option_def) => serialize_option(option_def, writer)?,
102        Def::SmartPointer(SmartPointerDef {
103            pointee: Some(inner_shape),
104            ..
105        }) => serialize(inner_shape(), &[], writer)?,
106        Def::Undefined => {
107            // Handle the case when not yet migrated to the Type enum
108            // For primitives, we can try to infer the type
109            match &shape.ty {
110                Type::Primitive(primitive) => {
111                    use facet_core::{NumericType, PrimitiveType, TextualType};
112                    match primitive {
113                        PrimitiveType::Numeric(NumericType::Float) => {
114                            write!(writer, "\"type\": \"number\", \"format\": \"double\"")?;
115                        }
116                        PrimitiveType::Boolean => {
117                            write!(writer, "\"type\": \"boolean\"")?;
118                        }
119                        PrimitiveType::Textual(TextualType::Str) => {
120                            write!(writer, "\"type\": \"string\"")?;
121                        }
122                        _ => {
123                            write!(writer, "\"type\": \"unknown\"")?;
124                        }
125                    }
126                }
127                Type::Pointer(PointerType::Reference(pt) | PointerType::Raw(pt)) => {
128                    serialize((pt.target)(), &[], writer)?
129                }
130                _ => {
131                    write!(writer, "\"type\": \"unknown\"")?;
132                }
133            }
134        }
135        _ => {
136            write!(writer, "\"type\": \"unknown\"")?;
137        }
138    }
139
140    Ok(())
141}
142
143fn serialize_doc<W: Write>(doc: &[&str], writer: &mut W) -> Result<(), std::io::Error> {
144    if !doc.is_empty() {
145        let doc = doc.join("\n");
146        write!(writer, "\"description\": \"{}\",", doc.trim())?;
147    }
148    Ok(())
149}
150
151/// Serialize a scalar definition to JSON schema format.
152fn serialize_scalar<W: Write>(scalar_def: &ScalarDef, writer: &mut W) -> std::io::Result<()> {
153    match scalar_def.affinity {
154        facet_core::ScalarAffinity::Number(number_affinity) => {
155            match number_affinity.bits {
156                facet_core::NumberBits::Integer { bits, sign } => {
157                    write!(writer, "\"type\": \"integer\"")?;
158                    match sign {
159                        facet_core::Signedness::Unsigned => {
160                            write!(writer, ", \"format\": \"uint{bits}\"")?;
161                            write!(writer, ", \"minimum\": 0")?;
162                        }
163                        facet_core::Signedness::Signed => {
164                            write!(writer, ", \"format\": \"int{bits}\"")?;
165                        }
166                    }
167                }
168                facet_core::NumberBits::Float { .. } => {
169                    write!(writer, "\"type\": \"number\"")?;
170                    write!(writer, ", \"format\": \"double\"")?;
171                }
172                _ => unimplemented!(),
173            }
174            Ok(())
175        }
176        facet_core::ScalarAffinity::String(_) => {
177            write!(writer, "\"type\": \"string\"")?;
178            Ok(())
179        }
180        facet_core::ScalarAffinity::Boolean(_) => {
181            write!(writer, "\"type\": \"boolean\"")?;
182            Ok(())
183        }
184        _ => Err(std::io::Error::other(format!(
185            "facet-jsonschema: nsupported scalar type: {scalar_def:#?}"
186        ))),
187    }
188}
189
190fn serialize_struct<W: Write>(
191    struct_type: &facet_core::StructType,
192    writer: &mut W,
193) -> std::io::Result<()> {
194    write!(writer, "\"type\": \"object\",")?;
195    let required = struct_type
196        .fields
197        .iter()
198        .map(|f| format!("\"{}\"", f.name))
199        .collect::<Vec<_>>()
200        .join(",");
201    write!(writer, "\"required\": [{required}],")?;
202    write!(writer, "\"properties\": {{")?;
203    let mut first = true;
204    for field in struct_type.fields {
205        if !first {
206            write!(writer, ",")?;
207        }
208        first = false;
209        write!(writer, "\"{}\": {{", field.name)?;
210        serialize(field.shape(), field.doc, writer)?;
211        write!(writer, "}}")?;
212    }
213    write!(writer, "}}")?;
214    Ok(())
215}
216
217/// Serialize a list definition to JSON schema format.
218fn serialize_list<W: Write>(list_def: facet_core::ListDef, writer: &mut W) -> std::io::Result<()> {
219    write!(writer, "\"type\": \"array\",")?;
220    write!(writer, "\"items\": {{")?;
221    serialize(list_def.t(), &[], writer)?;
222    write!(writer, "}}")?;
223    Ok(())
224}
225
226/// Serialize a slice definition to JSON schema format.
227fn serialize_slice<W: Write>(
228    slice_def: facet_core::SliceDef,
229    writer: &mut W,
230) -> std::io::Result<()> {
231    write!(writer, "\"type\": \"array\",")?;
232    write!(writer, "\"items\": {{")?;
233    serialize(slice_def.t(), &[], writer)?;
234    write!(writer, "}}")?;
235    Ok(())
236}
237
238/// Serialize an array definition to JSON schema format.
239fn serialize_array<W: Write>(
240    array_def: facet_core::ArrayDef,
241    writer: &mut W,
242) -> std::io::Result<()> {
243    write!(writer, "\"type\": \"array\",")?;
244    write!(writer, "\"minItems\": {},", array_def.n)?;
245    write!(writer, "\"maxItems\": {},", array_def.n)?;
246    write!(writer, "\"items\": {{")?;
247    serialize(array_def.t(), &[], writer)?;
248    write!(writer, "}}")?;
249    Ok(())
250}
251
252/// Serialize an option definition to JSON schema format.
253fn serialize_option<W: Write>(
254    _option_def: facet_core::OptionDef,
255    writer: &mut W,
256) -> std::io::Result<()> {
257    write!(writer, "\"type\": \"[]\",")?;
258    unimplemented!("serialize_option");
259}
260
261#[cfg(test)]
262mod tests {
263    extern crate alloc;
264    use alloc::{rc::Rc, sync::Arc};
265
266    use super::*;
267    use facet_derive::Facet;
268    use insta::assert_snapshot;
269
270    #[test]
271    fn test_basic() {
272        /// Test documentation
273        #[derive(Facet)]
274        #[facet(id = "http://example.com/schema")]
275        struct TestStruct {
276            /// Test doc1
277            string_field: String,
278            /// Test doc2
279            int_field: u32,
280            vec_field: Vec<bool>,
281            slice_field: &'static [f64],
282            array_field: [f64; 3],
283        }
284
285        let schema = to_string::<TestStruct>();
286        assert_snapshot!(schema);
287    }
288
289    #[test]
290    fn test_pointers() {
291        /// Test documentation
292        #[derive(Facet)]
293        #[facet(id = "http://example.com/schema")]
294        struct TestStruct<'a> {
295            normal_pointer: &'a str,
296            box_pointer: Box<u32>,
297            arc: Arc<u32>,
298            rc: Rc<u32>,
299            #[allow(clippy::redundant_allocation)]
300            nested: Rc<&'a Arc<&'a *const u32>>,
301        }
302
303        let schema = to_string::<TestStruct>();
304        assert_snapshot!(schema);
305    }
306}