Skip to main content

typify_impl/
structs.rs

1// Copyright 2024 Oxide Computer Company
2
3use heck::ToSnakeCase;
4use proc_macro2::TokenStream;
5use quote::quote;
6use schemars::schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject};
7
8use crate::{
9    output::{OutputSpace, OutputSpaceMod},
10    type_entry::{
11        StructProperty, StructPropertyRename, StructPropertyState, TypeEntry, TypeEntryStruct,
12        WrappedValue,
13    },
14    util::{get_type_name, metadata_description, recase, Case},
15    Name, Result, TypeEntryDetails, TypeId, TypeSpace,
16};
17
18impl TypeSpace {
19    pub(crate) fn struct_members(
20        &mut self,
21        type_name: Option<String>,
22        validation: &ObjectValidation,
23    ) -> Result<(Vec<StructProperty>, bool)> {
24        // These are the fields we don't currently handle
25        //assert!(validation.max_properties.is_none());
26        //assert!(validation.min_properties.is_none());
27        //assert!(validation.pattern_properties.is_empty());
28        //assert!(validation.property_names.is_none());
29
30        // Gather up the properties that are required but for which we have no
31        // schema. In those cases any value will do.
32        let required_unspecified = validation.required.iter().filter_map(|prop_name| {
33            (!validation.properties.contains_key(prop_name))
34                .then_some((prop_name, &Schema::Bool(true)))
35        });
36
37        let mut properties = validation
38            .properties
39            .iter()
40            .chain(required_unspecified)
41            .filter_map(|(prop_name, schema)| {
42                match schema {
43                    // TODO We use the schema `false` to indicate an
44                    // unsatisfiable schema. We take a shortcut here and simply
45                    // ignore these. This is wrong in two subtle and important
46                    // ways that we'll need to address at some point. First,
47                    // there are other schemas in some non-trivial,
48                    // non-canonical form that might indicate the same thing.
49                    // We should handle those in the same way. In addition,
50                    // ignoring them isn't really right. We need to actively
51                    // exclude them. Specifically this would look like a custom
52                    // serde::Deserialize implementation that failed in the
53                    // presence of these values.
54                    Schema::Bool(false) => None,
55                    _ => {
56                        // Generate a name we can use for the type of this
57                        // property should there not be one specified by the
58                        // schema itself (i.e. via the title field).
59                        let sub_type_name = type_name
60                            .as_ref()
61                            .map(|base| format!("{}_{}", base, prop_name.to_snake_case()));
62                        Some(self.struct_property(
63                            sub_type_name,
64                            &validation.required,
65                            prop_name,
66                            schema,
67                        ))
68                    }
69                }
70            })
71            .collect::<Result<Vec<_>>>()?;
72
73        // Sort parameters by name to ensure a deterministic result.
74        properties.sort_by(|a, b| a.name.cmp(&b.name));
75
76        // If there are additional properties tack them on, flattened, at the
77        // end. Note that a `None` value for additional_properties is
78        // equivalent to the permissive schema (Schema::Bool(true)) for reasons
79        // best known to the JSON Schema authors.
80        let deny_unknown_fields = match &validation.additional_properties {
81            // No additional properties allowed; we'll tag the struct with
82            // #[serde(deny_unknown_fields)]
83            Some(a) if a.as_ref() == &Schema::Bool(false) => true,
84
85            // We have a permissive schema so all additional properties are
86            // allowed (None is equivalent to the permissive schema). This is
87            // so common that it would be distracting to represent them with a
88            // flattened struct so instead we just ignore them. One can cause
89            // a flattened struct to be generated by using an equivalently
90            // permissive schema such as {}.
91            Some(a) if a.as_ref() == &Schema::Bool(true) => false,
92            None => false,
93
94            // Only particular additional properties are allowed. Note that
95            // #[serde(deny_unknown_fields)] is incompatible with
96            // #[serde(flatten)] so we allow them even though that doesn't seem
97            // quite right.
98            additional_properties @ Some(_) => {
99                let sub_type_name = type_name.as_ref().map(|base| format!("{}_extra", base));
100                let map_type = self.make_map(
101                    sub_type_name,
102                    &validation.property_names,
103                    additional_properties,
104                )?;
105                let map_type_id = self.assign_type(map_type);
106                let extra_prop = StructProperty {
107                    name: "extra".to_string(),
108                    rename: StructPropertyRename::Flatten,
109                    state: StructPropertyState::Required,
110                    description: None,
111                    type_id: map_type_id,
112                };
113
114                properties.push(extra_prop);
115                false
116            }
117        };
118
119        Ok((properties, deny_unknown_fields))
120    }
121
122    fn struct_property(
123        &mut self,
124        type_name: Option<String>,
125        required: &schemars::Set<String>,
126        prop_name: &str,
127        schema: &schemars::schema::Schema,
128    ) -> Result<StructProperty> {
129        let sub_type_name = match type_name {
130            Some(name) => Name::Suggested(name),
131            None => Name::Unknown,
132        };
133        let (mut type_id, metadata) = self.id_for_schema(sub_type_name, schema)?;
134
135        let state = if required.contains(prop_name) {
136            StructPropertyState::Required
137        } else {
138            // We can use serde's `default` and `skip_serializing_if`
139            // construction for options, arrays, and maps--i.e. properties that
140            // have an "intrinsic" default value. We can also apply `default`
141            // to properties for which there's a default value present. (We
142            // could also skip serializing them when they match the default
143            // value, but that seems both uncommon and more trouble than it's
144            // worth.) Properties with no intrinsic or explicit default value
145            // are converted to an Option<T> type in order to represent the
146            // field as non-required.
147            //
148            // Note that arrays, maps, and even options may have default values
149            // that differ from the intrinsic default values. That is to say,
150            // they may have defaults other than `[]`, `{}`, and `null`
151            // respectively. This affects the eventual generated code, but not
152            // the internal representation produced here.
153            //
154            // We will validate the default values, but not here: the type
155            // space is not yet in a consistent state with regard to references
156            // so we cannot reliably resolve references here.
157            match has_default(
158                self,
159                &type_id,
160                metadata.as_ref().and_then(|m| m.default.as_ref()),
161            ) {
162                StructPropertyState::Required => {
163                    type_id = self.id_to_option(&type_id);
164                    StructPropertyState::Optional
165                }
166                other => other,
167            }
168        };
169
170        let (name, rename) = recase(prop_name, Case::Snake);
171        let rename = match rename {
172            Some(old_name) => StructPropertyRename::Rename(old_name),
173            None => StructPropertyRename::None,
174        };
175
176        Ok(StructProperty {
177            name,
178            rename,
179            state,
180            description: metadata_description(metadata),
181            type_id,
182        })
183    }
184
185    pub(crate) fn make_map(
186        &mut self,
187        type_name: Option<String>,
188        property_names: &Option<Box<Schema>>,
189        additional_properties: &Option<Box<Schema>>,
190    ) -> Result<TypeEntry> {
191        let key_id = match property_names.as_deref() {
192            Some(Schema::Bool(true)) | None => self.assign_type(TypeEntryDetails::String.into()),
193
194            // TODO this would correspond to an empty object: an object with
195            // no legal property values.
196            Some(Schema::Bool(false)) => todo!(),
197
198            Some(Schema::Object(obj)) => {
199                let key_type_name = match &type_name {
200                    Some(name) => Name::Suggested(format!("{}Key", name)),
201                    None => Name::Unknown,
202                };
203                self.id_for_schema_string(key_type_name, obj)?
204            }
205        };
206
207        let (value_id, _) = match additional_properties {
208            Some(value_schema) => {
209                let value_type_name = match &type_name {
210                    Some(name) => Name::Suggested(format!("{}Value", name)),
211                    None => Name::Unknown,
212                };
213                self.id_for_schema(value_type_name, value_schema)?
214            }
215
216            None => self.id_for_schema(Name::Unknown, &Schema::Bool(true))?,
217        };
218
219        Ok(TypeEntryDetails::Map(key_id, value_id).into())
220    }
221
222    /// Perform a schema conversion for a type that must be string-like.
223    pub(crate) fn id_for_schema_string(
224        &mut self,
225        type_name: Name,
226        schema_obj: &SchemaObject,
227    ) -> Result<TypeId> {
228        match schema_obj {
229            // If the schema has no subschemas or references, fill in the
230            // string instance_type if none is present.
231            SchemaObject {
232                instance_type: None,
233                subschemas: None,
234                reference: None,
235                ..
236            } => {
237                let schema = Schema::Object(SchemaObject {
238                    instance_type: Some(InstanceType::String.into()),
239                    ..schema_obj.clone()
240                });
241                Ok(self.id_for_schema(type_name, &schema)?.0)
242            }
243
244            // TODO if and when we perform merging of schemas we could wrap the
245            // schema in an { allOf: [{ type: string }, <schema> ] }
246            _ => {
247                let schema = Schema::Object(schema_obj.clone());
248                Ok(self.id_for_schema(type_name, &schema)?.0)
249            }
250        }
251    }
252
253    /// This is used by both any-of and all-of subschema processing. This
254    /// produces a struct type whose members are the subschemas (flattened).
255    ///
256    /// ```ignore
257    /// struct Name {
258    ///     #[serde(flatten)]
259    ///     schema1: Schema1Type,
260    ///     #[serde(flatten)]
261    ///     schema2: Schema2Type
262    ///     ...
263    /// }
264    /// ```
265    ///
266    /// The only difference between any-of and all-of is that where the latter
267    /// has type T_N for each member of the struct, the former has Option<T_N>.
268    pub(crate) fn flattened_union_struct<'a>(
269        &mut self,
270        type_name: Name,
271        original_schema: &'a Schema,
272        metadata: &'a Option<Box<Metadata>>,
273        subschemas: &[Schema],
274        optional: bool,
275    ) -> Result<(TypeEntry, &'a Option<Box<Metadata>>)> {
276        let properties = subschemas
277            .iter()
278            .enumerate()
279            .map(|(idx, schema)| {
280                let type_name = match get_type_name(&type_name, metadata) {
281                    Some(name) => Name::Suggested(format!("{}Subtype{}", name, idx)),
282                    None => Name::Unknown,
283                };
284
285                let (mut type_id, _) = self.id_for_schema(type_name, schema)?;
286                if optional {
287                    type_id = self.id_to_option(&type_id);
288                }
289
290                // TODO we need a reasonable name that could be derived
291                // from the name of the type
292                let name = format!("subtype_{}", idx);
293
294                Ok(StructProperty {
295                    name,
296                    rename: StructPropertyRename::Flatten,
297                    state: if optional {
298                        StructPropertyState::Optional
299                    } else {
300                        StructPropertyState::Required
301                    },
302                    description: None,
303                    type_id,
304                })
305            })
306            .collect::<Result<Vec<_>>>()?;
307
308        Ok((
309            TypeEntryStruct::from_metadata(
310                self,
311                type_name,
312                metadata,
313                properties,
314                false,
315                original_schema.clone(),
316            ),
317            metadata,
318        ))
319    }
320}
321
322pub(crate) enum DefaultFunction {
323    None,
324    Default,
325    Custom(String),
326}
327
328/// Generate the serde attribute parameters for the given property.
329///
330/// This may include a default value that requires a generated function to
331/// produce it. In such a case, that function will be added to the OutputSpace.
332///
333/// Note that if we have several serde attribute parameters, they could each
334/// appear in their own attribute. We choose to condense them for the sake of
335/// legibility.
336pub(crate) fn generate_serde_attr(
337    type_name: &str,
338    prop_name: &str,
339    naming: &StructPropertyRename,
340    state: &StructPropertyState,
341    prop_type: &TypeEntry,
342    type_space: &TypeSpace,
343    output: &mut OutputSpace,
344) -> (TokenStream, DefaultFunction) {
345    let mut serde_options = Vec::new();
346    match naming {
347        StructPropertyRename::Rename(s) => serde_options.push(quote! { rename = #s }),
348        StructPropertyRename::Flatten => serde_options.push(quote! { flatten }),
349        StructPropertyRename::None => (),
350    }
351
352    let default_fn = match (state, &prop_type.details) {
353        (StructPropertyState::Optional, TypeEntryDetails::Option(_)) => {
354            serde_options.push(quote! { default });
355            serde_options.push(quote! { skip_serializing_if = "::std::option::Option::is_none" });
356            DefaultFunction::Default
357        }
358        (StructPropertyState::Optional, TypeEntryDetails::Vec(_)) => {
359            serde_options.push(quote! { default });
360            serde_options.push(quote! { skip_serializing_if = "::std::vec::Vec::is_empty" });
361            DefaultFunction::Default
362        }
363        (StructPropertyState::Optional, TypeEntryDetails::Map(key_id, value_id)) => {
364            serde_options.push(quote! { default });
365
366            let map_to_use = &type_space.settings.map_type;
367            let key_ty = type_space
368                .id_to_entry
369                .get(key_id)
370                .expect("unresolved key type id for map");
371            let value_ty = type_space
372                .id_to_entry
373                .get(value_id)
374                .expect("unresolved value type id for map");
375
376            if key_ty.details == TypeEntryDetails::String
377                && value_ty.details == TypeEntryDetails::JsonValue
378            {
379                serde_options.push(quote! {
380                    skip_serializing_if = "::serde_json::Map::is_empty"
381                });
382            } else {
383                let is_empty = format!("{}::is_empty", map_to_use);
384                serde_options.push(quote! {
385                    skip_serializing_if = #is_empty
386                });
387            }
388            DefaultFunction::Default
389        }
390        (StructPropertyState::Optional, _) => {
391            serde_options.push(quote! { default });
392            DefaultFunction::Default
393        }
394
395        (StructPropertyState::Default(WrappedValue(value)), _) => {
396            let (fn_name, default_fn) =
397                prop_type.default_fn(value, type_space, type_name, prop_name);
398            serde_options.push(quote! { default = #fn_name });
399
400            if let Some(default_fn) = default_fn {
401                output.add_item(OutputSpaceMod::Defaults, type_name, default_fn);
402            }
403            DefaultFunction::Custom(fn_name)
404        }
405
406        (StructPropertyState::Required, _) => DefaultFunction::None,
407    };
408
409    let serde = if serde_options.is_empty() {
410        quote! {}
411    } else {
412        quote! {
413            #[serde( #(#serde_options),*)]
414        }
415    };
416
417    (serde, default_fn)
418}
419
420/// See if this type is a type that we can omit with a serde directive; note
421/// that the type id lookup will fail only for references (and only during
422/// initial reference processing).
423fn has_default(
424    type_space: &mut TypeSpace,
425    type_id: &TypeId,
426    default: Option<&serde_json::Value>,
427) -> StructPropertyState {
428    // This lookup can fail in the scenario where a struct (or struct
429    // variant) member is optional and the type of that optional member is a
430    // reference to a type that has not yet been converted. This is fine: those
431    // are necessarily named types and not raw options, arrays, maps, or units.
432    match (
433        type_space
434            .id_to_entry
435            .get(type_id)
436            .map(|type_entry| &type_entry.details),
437        default,
438    ) {
439        // No default specified.
440        (Some(TypeEntryDetails::Option(_)), None) => StructPropertyState::Optional,
441        (Some(TypeEntryDetails::Vec(_)), None) => StructPropertyState::Optional,
442        (Some(TypeEntryDetails::Map(..)), None) => StructPropertyState::Optional,
443        (Some(TypeEntryDetails::Unit), None) => StructPropertyState::Optional,
444        (_, None) => StructPropertyState::Required,
445
446        // Default specified is the same as the implicit default: null
447        (Some(TypeEntryDetails::Option(_)), Some(serde_json::Value::Null)) => {
448            StructPropertyState::Optional
449        }
450        // Default specified is the same as the implicit default: []
451        (Some(TypeEntryDetails::Vec(_)), Some(serde_json::Value::Array(a))) if a.is_empty() => {
452            StructPropertyState::Optional
453        }
454        // Default specified is the same as the implicit default: {}
455        (Some(TypeEntryDetails::Map(..)), Some(serde_json::Value::Object(m))) if m.is_empty() => {
456            StructPropertyState::Optional
457        }
458        // Default specified is the same as the implicit default: false
459        (Some(TypeEntryDetails::Boolean), Some(serde_json::Value::Bool(false))) => {
460            StructPropertyState::Optional
461        }
462        // Default specified is the same as the implicit default: 0
463        (Some(TypeEntryDetails::Integer(_)), Some(serde_json::Value::Number(n)))
464            if n.as_u64() == Some(0) =>
465        {
466            StructPropertyState::Optional
467        }
468        // Default specified is the same as the implicit default: 0.0
469        (Some(TypeEntryDetails::Integer(_)), Some(serde_json::Value::Number(n)))
470            if n.as_f64() == Some(0.0) =>
471        {
472            StructPropertyState::Optional
473        }
474        // Default specified is the same as the implicit default: ""
475        (Some(TypeEntryDetails::String), Some(serde_json::Value::String(s))) if s.is_empty() => {
476            StructPropertyState::Optional
477        }
478
479        // This is a reference that will resolve to this type id later.
480        (None, Some(default)) => StructPropertyState::Default(WrappedValue(default.clone())),
481        // All other types as well as types with intrinsic defaults that have
482        // been explicitly overridden.
483        (Some(_), Some(default)) => StructPropertyState::Default(WrappedValue(default.clone())),
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use schema::Schema;
490    use schemars::JsonSchema;
491    use serde::Serialize;
492
493    use crate::{test_util::validate_output, Name, TypeSpace};
494
495    #[allow(dead_code)]
496    #[derive(Serialize, JsonSchema, Schema)]
497    #[serde(deny_unknown_fields)]
498    struct SimpleStruct {
499        alpha: u32,
500        bravo: String,
501        charlie: Vec<(String, u32)>,
502        delta: Option<String>,
503        echo: Option<(u32, String)>,
504    }
505
506    #[test]
507    fn test_simple_struct() {
508        validate_output::<SimpleStruct>();
509    }
510
511    #[allow(dead_code)]
512    #[derive(Serialize, JsonSchema, Schema)]
513    struct LessSimpleStruct {
514        thing: SimpleStruct,
515        things: Vec<SimpleStruct>,
516    }
517
518    #[test]
519    fn test_less_simple_struct() {
520        validate_output::<LessSimpleStruct>();
521    }
522
523    #[allow(dead_code)]
524    #[derive(Serialize, JsonSchema, Schema)]
525    struct SomeMaps {
526        strings: ::std::collections::HashMap<String, String>,
527        things: ::serde_json::Map<String, ::serde_json::Value>,
528    }
529
530    #[test]
531    fn test_some_maps() {
532        validate_output::<SomeMaps>();
533    }
534
535    #[allow(dead_code)]
536    #[derive(Serialize, JsonSchema, Schema)]
537    #[serde(deny_unknown_fields)]
538    struct FlattenStuff {
539        number: i32,
540        #[serde(flatten)]
541        extra: ::std::collections::HashMap<String, String>,
542    }
543
544    #[test]
545    fn test_flatten_stuff() {
546        validate_output::<FlattenStuff>();
547    }
548
549    #[test]
550    fn test_default_field() {
551        #[allow(dead_code)]
552        #[derive(Serialize, JsonSchema, Schema)]
553        #[serde(deny_unknown_fields)]
554        struct DefaultField {
555            #[serde(default)]
556            number: i32,
557        }
558
559        validate_output::<DefaultField>();
560    }
561
562    #[test]
563    fn test_object_no_validation() {
564        let schema = schemars::schema::Schema::Object(schemars::schema::SchemaObject {
565            instance_type: Some(schemars::schema::InstanceType::Object.into()),
566            ..Default::default()
567        });
568
569        let mut type_space = TypeSpace::default();
570        let (ty, _) = type_space.convert_schema(Name::Unknown, &schema).unwrap();
571        let output = ty.type_name(&type_space).replace(" ", "");
572        assert_eq!(
573            output,
574            "::serde_json::Map<::std::string::String,::serde_json::Value>"
575        );
576    }
577}