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