cedar_policy_validator/
schema_file_format.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use cedar_policy_core::entities::JSONValue;
18use serde::{Deserialize, Serialize};
19use serde_with::serde_as;
20use smol_str::SmolStr;
21use std::collections::{BTreeMap, HashMap};
22
23use crate::Result;
24
25/// A SchemaFragment describe the types for a given instance of Cedar.
26/// SchemaFragments are composed of Entity Types and Action Types. The
27/// schema fragment is split into multiple namespace definitions, eac including
28/// a namespace name which is applied to all entity types (and the implicit
29/// `Action` entity type for all actions) in the schema.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct SchemaFragment(
33    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
34    pub  HashMap<SmolStr, NamespaceDefinition>,
35);
36
37impl SchemaFragment {
38    /// Create a `SchemaFragment` from a JSON value (which should be an object
39    /// of the appropriate shape).
40    pub fn from_json_value(json: serde_json::Value) -> Result<Self> {
41        serde_json::from_value(json).map_err(Into::into)
42    }
43
44    /// Create a `SchemaFragment` directly from a file.
45    pub fn from_file(file: impl std::io::Read) -> Result<Self> {
46        serde_json::from_reader(file).map_err(Into::into)
47    }
48}
49
50/// A single namespace definition from a SchemaFragment.
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde_as]
53#[serde(deny_unknown_fields)]
54#[doc(hidden)]
55pub struct NamespaceDefinition {
56    #[serde(default)]
57    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
58    #[serde(rename = "commonTypes")]
59    pub common_types: HashMap<SmolStr, SchemaType>,
60    #[serde(rename = "entityTypes")]
61    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
62    pub entity_types: HashMap<SmolStr, EntityType>,
63    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
64    pub actions: HashMap<SmolStr, ActionType>,
65}
66
67impl NamespaceDefinition {
68    pub fn new(
69        entity_types: impl IntoIterator<Item = (SmolStr, EntityType)>,
70        actions: impl IntoIterator<Item = (SmolStr, ActionType)>,
71    ) -> Self {
72        Self {
73            common_types: HashMap::new(),
74            entity_types: entity_types.into_iter().collect(),
75            actions: actions.into_iter().collect(),
76        }
77    }
78}
79
80/// Entity types describe the relationships in the entity store, including what
81/// entities can be members of groups of what types, and what attributes
82/// can/should be included on entities of each type.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct EntityType {
86    #[serde(default)]
87    #[serde(rename = "memberOfTypes")]
88    pub member_of_types: Vec<SmolStr>,
89    #[serde(default)]
90    pub shape: AttributesOrContext,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94#[serde(transparent)]
95pub struct AttributesOrContext(
96    // We use the usual `SchemaType` deserialization, but it will ultimately
97    // need to be a `Record` or type def which resolves to a `Record`.
98    pub SchemaType,
99);
100
101impl AttributesOrContext {
102    pub fn into_inner(self) -> SchemaType {
103        self.0
104    }
105}
106
107impl Default for AttributesOrContext {
108    fn default() -> Self {
109        Self(SchemaType::Type(SchemaTypeVariant::Record {
110            attributes: BTreeMap::new(),
111            additional_attributes: false,
112        }))
113    }
114}
115
116/// An action type describes a specific action entity.  It also describes what
117/// kinds of entities it can be used on.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[serde(deny_unknown_fields)]
120pub struct ActionType {
121    /// This maps attribute names to
122    /// `cedar_policy_core::entities::json::jsonvalue::JSONValue` which is the
123    /// canonical representation of a cedar value as JSON.
124    #[serde(default)]
125    pub attributes: Option<HashMap<SmolStr, JSONValue>>,
126    #[serde(default)]
127    #[serde(rename = "appliesTo")]
128    pub applies_to: Option<ApplySpec>,
129    #[serde(default)]
130    #[serde(rename = "memberOf")]
131    pub member_of: Option<Vec<ActionEntityUID>>,
132}
133
134/// The apply spec specifies what principals and resources an action can be used
135/// with.  This specification can either be done through containing to entity
136/// types. The fields of this record are optional so that they can be omitted to
137/// declare that the apply spec for the principal or resource is undefined,
138/// meaning that the action can be applied to any principal or resource. This is
139/// different than providing an empty list because the empty list is interpreted
140/// as specifying that there are no principals or resources that an action
141/// applies to.
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(deny_unknown_fields)]
144pub struct ApplySpec {
145    #[serde(default)]
146    #[serde(rename = "resourceTypes")]
147    pub resource_types: Option<Vec<SmolStr>>,
148    #[serde(default)]
149    #[serde(rename = "principalTypes")]
150    pub principal_types: Option<Vec<SmolStr>>,
151    #[serde(default)]
152    pub context: AttributesOrContext,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156#[serde(deny_unknown_fields)]
157pub struct ActionEntityUID {
158    pub id: SmolStr,
159
160    #[serde(rename = "type")]
161    #[serde(default)]
162    pub ty: Option<SmolStr>,
163}
164
165impl ActionEntityUID {
166    pub fn default_type(id: SmolStr) -> Self {
167        Self { id, ty: None }
168    }
169}
170
171impl std::fmt::Display for ActionEntityUID {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        if let Some(ty) = &self.ty {
174            write!(f, "{}::", ty)?
175        } else {
176            write!(f, "Action::")?
177        }
178        write!(f, "\"{}\"", self.id)
179    }
180}
181
182/// A restricted version of the `Type` enum containing only the types which are
183/// exposed to users.
184#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
185// This enum is `untagged` with these variants as a workaround to a serde
186// limitation. It is not possible to have the known variants on one enum, and
187// then, have catch-all variant for any unrecognized tag in the same enum that
188// captures the name of the unrecognized tag.
189#[serde(untagged)]
190pub enum SchemaType {
191    Type(SchemaTypeVariant),
192    TypeDef {
193        #[serde(rename = "type")]
194        type_name: SmolStr,
195    },
196}
197
198impl From<SchemaTypeVariant> for SchemaType {
199    fn from(variant: SchemaTypeVariant) -> Self {
200        Self::Type(variant)
201    }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
205#[serde(tag = "type")]
206#[serde(deny_unknown_fields)]
207pub enum SchemaTypeVariant {
208    String,
209    Long,
210    Boolean,
211    Set {
212        element: Box<SchemaType>,
213    },
214    Record {
215        #[serde(with = "serde_with::rust::maps_duplicate_key_is_error")]
216        attributes: BTreeMap<SmolStr, TypeOfAttribute>,
217        #[serde(rename = "additionalAttributes")]
218        #[serde(default = "additional_attributes_default")]
219        additional_attributes: bool,
220    },
221    Entity {
222        name: SmolStr,
223    },
224    Extension {
225        name: SmolStr,
226    },
227}
228
229// The possible tags for a SchemaType as written in a schema JSON document. Used
230// to forbid declaring a custom typedef with the same name as a builtin type.
231// This must be kept up to date with the variants for `SchemaTypeVariant` and
232// their actual serialization by serde. There is crate that looks like it could
233// do this automatically, but it returns an empty slice for the variants names
234// of `SchemaTypeVariant`.
235// https://docs.rs/serde-aux/latest/serde_aux/serde_introspection/fn.serde_introspect.html
236pub(crate) static SCHEMA_TYPE_VARIANT_TAGS: &[&str] = &[
237    "String",
238    "Long",
239    "Boolean",
240    "Set",
241    "Record",
242    "Entity",
243    "Extension",
244];
245
246impl SchemaType {
247    /// Is this `SchemaType` an extension type, or does it contain one
248    /// (recursively)? Returns `None` if this is a `TypeDef` because we can't
249    /// easily properly check the type of a typedef, accounting for namespaces,
250    /// without first converting to a `Type`.
251    pub fn is_extension(&self) -> Option<bool> {
252        match self {
253            Self::Type(SchemaTypeVariant::Extension { .. }) => Some(true),
254            Self::Type(SchemaTypeVariant::Set { element }) => element.is_extension(),
255            Self::Type(SchemaTypeVariant::Record { attributes, .. }) => {
256                attributes
257                    .values()
258                    .fold(Some(false), |a, e| match e.ty.is_extension() {
259                        Some(true) => Some(true),
260                        Some(false) => a,
261                        None => None,
262                    })
263            }
264            Self::Type(_) => Some(false),
265            Self::TypeDef { .. } => None,
266        }
267    }
268}
269
270#[cfg(feature = "arbitrary")]
271// PANIC SAFETY property testing code
272#[allow(clippy::panic)]
273impl<'a> arbitrary::Arbitrary<'a> for SchemaType {
274    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<SchemaType> {
275        use cedar_policy_core::ast::Name;
276        use std::collections::BTreeSet;
277
278        Ok(SchemaType::Type(match u.int_in_range::<u8>(1..=8)? {
279            1 => SchemaTypeVariant::String,
280            2 => SchemaTypeVariant::Long,
281            3 => SchemaTypeVariant::Boolean,
282            4 => SchemaTypeVariant::Set {
283                element: Box::new(u.arbitrary()?),
284            },
285            5 => {
286                let attributes = {
287                    let attr_names: BTreeSet<String> = u.arbitrary()?;
288                    attr_names
289                        .into_iter()
290                        .map(|attr_name| Ok((attr_name.into(), u.arbitrary()?)))
291                        .collect::<arbitrary::Result<_>>()?
292                };
293                SchemaTypeVariant::Record {
294                    attributes,
295                    additional_attributes: u.arbitrary()?,
296                }
297            }
298            6 => {
299                let name: Name = u.arbitrary()?;
300                SchemaTypeVariant::Entity {
301                    name: name.to_string().into(),
302                }
303            }
304            7 => SchemaTypeVariant::Extension {
305                name: "ipaddr".into(),
306            },
307            8 => SchemaTypeVariant::Extension {
308                name: "decimal".into(),
309            },
310            n => panic!("bad index: {n}"),
311        }))
312    }
313    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
314        (1, None) // Unfortunately, we probably can't be more precise than this
315    }
316}
317
318/// Used to describe the type of a record or entity attribute. It contains a the
319/// type of the attribute and whether the attribute is required. The type is
320/// flattened for serialization, so, in JSON format, this appears as a regular
321/// type with one extra property `required`.
322///
323/// Note that we can't add #[serde(deny_unknown_fields)] here because we are
324/// using #[serde(tag = "type")] in ty:SchemaType which is flattened here.
325/// The way serde(flatten) is implemented means it may be possible to access
326/// fields incorrectly if a struct contains two structs that are flattened
327/// (`<https://github.com/serde-rs/serde/issues/1547>`). This shouldn't apply to
328/// us as we're using flatten only once
329/// (`<https://github.com/serde-rs/serde/issues/1600>`). This should be ok because
330/// unknown fields for TypeOfAttribute should be passed to SchemaType where
331/// they will be denied (`<https://github.com/serde-rs/serde/issues/1600>`).
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, PartialOrd, Ord)]
333#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
334pub struct TypeOfAttribute {
335    #[serde(flatten)]
336    pub ty: SchemaType,
337    #[serde(default = "record_attribute_required_default")]
338    pub required: bool,
339}
340
341/// Defines the default value for `additionalAttributes` on records and
342/// entities
343fn additional_attributes_default() -> bool {
344    false
345}
346
347/// Defines the default value for `required` on record and entity attributes.
348fn record_attribute_required_default() -> bool {
349    true
350}
351
352#[cfg(test)]
353mod test {
354    use crate::ValidatorSchema;
355
356    use super::*;
357
358    #[test]
359    fn test_entity_type_parser1() {
360        let user = r#"
361        {
362            "memberOfTypes" : ["UserGroup"]
363        }
364        "#;
365        let et = serde_json::from_str::<EntityType>(user).expect("Parse Error");
366        assert_eq!(et.member_of_types, vec!["UserGroup"]);
367        assert_eq!(
368            et.shape.into_inner(),
369            SchemaType::Type(SchemaTypeVariant::Record {
370                attributes: BTreeMap::new(),
371                additional_attributes: false
372            })
373        );
374    }
375
376    #[test]
377    fn test_entity_type_parser2() {
378        let src = r#"
379              { }
380        "#;
381        let et = serde_json::from_str::<EntityType>(src).expect("Parse Error");
382        assert_eq!(et.member_of_types.len(), 0);
383        assert_eq!(
384            et.shape.into_inner(),
385            SchemaType::Type(SchemaTypeVariant::Record {
386                attributes: BTreeMap::new(),
387                additional_attributes: false
388            })
389        );
390    }
391
392    #[test]
393    fn test_action_type_parser1() {
394        let src = r#"
395              {
396                "appliesTo" : {
397                  "resourceTypes": ["Album"],
398                  "principalTypes": ["User"]
399                },
400                "memberOf": [{"id": "readWrite"}]
401              }
402        "#;
403        let at: ActionType = serde_json::from_str(src).expect("Parse Error");
404        let spec = ApplySpec {
405            resource_types: Some(vec!["Album".into()]),
406            principal_types: Some(vec!["User".into()]),
407            context: AttributesOrContext::default(),
408        };
409        assert_eq!(at.applies_to, Some(spec));
410        assert_eq!(
411            at.member_of,
412            Some(vec![ActionEntityUID {
413                ty: None,
414                id: "readWrite".into()
415            }])
416        );
417    }
418
419    #[test]
420    fn test_action_type_parser2() {
421        let src = r#"
422              { }
423        "#;
424        let at: ActionType = serde_json::from_str(src).expect("Parse Error");
425        assert_eq!(at.applies_to, None);
426        assert!(at.member_of.is_none());
427    }
428
429    #[test]
430    fn test_schema_file_parser() {
431        let src = serde_json::json!(
432        {
433            "entityTypes": {
434
435              "User": {
436                "memberOfTypes": ["UserGroup"]
437              },
438              "Photo": {
439                "memberOfTypes": ["Album", "Account"]
440              },
441
442              "Album": {
443                "memberOfTypes": ["Album", "Account"]
444              },
445              "Account": { },
446              "UserGroup": { }
447           },
448
449           "actions": {
450              "readOnly": { },
451              "readWrite": { },
452              "createAlbum": {
453                "appliesTo" : {
454                  "resourceTypes": ["Account", "Album"],
455                  "principalTypes": ["User"]
456                },
457                "memberOf": [{"id": "readWrite"}]
458              },
459              "addPhotoToAlbum": {
460                "appliesTo" : {
461                  "resourceTypes": ["Album"],
462                  "principalTypes": ["User"]
463                },
464                "memberOf": [{"id": "readWrite"}]
465              },
466              "viewPhoto": {
467                "appliesTo" : {
468                  "resourceTypes": ["Photo"],
469                  "principalTypes": ["User"]
470                },
471                "memberOf": [{"id": "readOnly"}, {"id": "readWrite"}]
472              },
473              "viewComments": {
474                "appliesTo" : {
475                  "resourceTypes": ["Photo"],
476                  "principalTypes": ["User"]
477                },
478                "memberOf": [{"id": "readOnly"}, {"id": "readWrite"}]
479              }
480            }
481          });
482        let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
483
484        assert_eq!(schema_file.entity_types.len(), 5);
485        assert_eq!(schema_file.actions.len(), 6);
486    }
487
488    #[test]
489    fn test_parse_namespaces() {
490        let src = r#"
491        {
492            "foo::foo::bar::baz": {
493                "entityTypes": {},
494                "actions": {}
495            }
496        }"#;
497        let schema: SchemaFragment = serde_json::from_str(src).expect("Parse Error");
498        let (namespace, _descriptor) = schema.0.into_iter().next().unwrap();
499        assert_eq!(namespace, "foo::foo::bar::baz".to_string());
500    }
501
502    #[test]
503    fn test_schema_file_with_misspelled_required() {
504        let src = serde_json::json!(
505        {
506            "entityTypes": {
507                "User": {
508                    "shape": {
509                        "type": "Record",
510                        "attributes": {
511                            "favorite": {
512                                "type": "Entity",
513                                "name": "Photo",
514                                "requiredddddd": false
515                            }
516                        }
517                    }
518                }
519            },
520            "actions": {}
521        });
522        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
523        println!("{:#?}", schema);
524    }
525
526    #[test]
527    fn test_schema_file_with_misspelled_field() {
528        let src = serde_json::json!(
529        {
530            "entityTypes": {
531                "User": {
532                    "shape": {
533                        "type": "Record",
534                        "attributes": {
535                            "favorite": {
536                                "type": "Entity",
537                                "nameeeeee": "Photo",
538                            }
539                        }
540                    }
541                }
542            },
543            "actions": {}
544        });
545        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
546        println!("{:#?}", schema);
547    }
548
549    #[test]
550    fn test_schema_file_with_extra_field() {
551        let src = serde_json::json!(
552        {
553            "entityTypes": {
554                "User": {
555                    "shape": {
556                        "type": "Record",
557                        "attributes": {
558                            "favorite": {
559                                "type": "Entity",
560                                "name": "Photo",
561                                "extra": "Should not exist"
562                            }
563                        }
564                    }
565                }
566            },
567            "actions": {}
568        });
569        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
570        println!("{:#?}", schema);
571    }
572
573    #[test]
574    fn test_schema_file_with_misplaced_field() {
575        let src = serde_json::json!(
576        {
577            "entityTypes": {
578                "User": {
579                    "shape": {
580                        "memberOfTypes": [],
581                        "type": "Record",
582                        "attributes": {
583                            "favorite": {
584                                "type": "Entity",
585                                "name": "Photo",
586                            }
587                        }
588                    }
589                }
590            },
591            "actions": {}
592        });
593        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
594        println!("{:#?}", schema);
595    }
596
597    #[test]
598    // This schema was rejected prior to the reverted schema parsing changes.
599    // The error message was improved by the changes, so the current message is
600    // not very helpful.
601    #[should_panic(expected = "UndeclaredCommonTypes({\"Entity\"})")]
602    fn schema_file_with_missing_field() {
603        let src = serde_json::json!(
604        {
605            "entityTypes": {
606                "User": {
607                    "shape": {
608                        "type": "Record",
609                        "attributes": {
610                            "favorite": {
611                                "type": "Entity",
612                            }
613                        }
614                    }
615                }
616            },
617            "actions": {}
618        });
619        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
620        println!("{:#?}", schema);
621        TryInto::<ValidatorSchema>::try_into(schema).unwrap();
622    }
623
624    #[test]
625    // This schema was rejected prior to the reverted schema parsing changes.
626    // The error message was improved by the changes, so the current message is
627    // not very helpful.
628    #[should_panic(expected = "data did not match any variant of untagged enum SchemaType")]
629    fn schema_file_with_missing_type() {
630        let src = serde_json::json!(
631        {
632            "entityTypes": {
633                "User": {
634                    "shape": { }
635                }
636            },
637            "actions": {}
638        });
639        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
640        println!("{:#?}", schema);
641    }
642
643    #[test]
644    fn test_schema_file_with_field_from_other_type() {
645        let src = serde_json::json!(
646        {
647            "entityTypes": {
648                "User": {
649                    "shape": {
650                        "type": "Record",
651                        "attributes": {
652                            "favorite": {
653                                "type": "String",
654                                // These fields shouldn't exist for a String,
655                                // and we could detect this error, but we allow
656                                // it to maintain backwards compatibility.
657                                "name": "Photo",
658                                "attributes": {},
659                                "element": "",
660                            }
661                        }
662                    }
663                }
664            },
665            "actions": {}
666        });
667        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
668        println!("{:#?}", schema);
669    }
670
671    #[test]
672    fn schema_file_unexpected_malformed_attribute() {
673        let src = serde_json::json!(
674        {
675            "entityTypes": {
676                "User": {
677                    "shape": {
678                        "type": "Record",
679                        "attributes": {
680                            "a": {
681                                "type": "Long",
682                                // Similar to above, `attributes` shouldn't
683                                // exist, and `"foo": "bar"` is an invalid
684                                // attribute when it should exist. We allow this
685                                // for backwards compatibility.
686                                "attributes": {
687                                    "b": {"foo": "bar"}
688                                }
689                            }
690                        }
691                    }
692                }
693            },
694            "actions": {}
695        });
696        let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
697        println!("{:#?}", schema);
698    }
699}
700
701/// Tests in this module check the behavior of schema parsing given duplicate
702/// map keys. The `json!` macro silently drops duplicate keys before they reach
703/// our parser, so these tests must be written with `serde_json::from_str`
704/// instead.
705#[cfg(test)]
706mod test_duplicates_error {
707    use super::*;
708
709    #[test]
710    #[should_panic(expected = "invalid entry: found duplicate key")]
711    fn namespace() {
712        let src = r#"{
713            "Foo": {
714              "entityTypes" : {},
715              "actions": {}
716            },
717            "Foo": {
718              "entityTypes" : {},
719              "actions": {}
720            }
721        }"#;
722        serde_json::from_str::<SchemaFragment>(src).unwrap();
723    }
724
725    #[test]
726    #[should_panic(expected = "invalid entry: found duplicate key")]
727    fn entity_type() {
728        let src = r#"{
729            "Foo": {
730              "entityTypes" : {
731                "Bar": {},
732                "Bar": {},
733              },
734              "actions": {}
735            }
736        }"#;
737        serde_json::from_str::<SchemaFragment>(src).unwrap();
738    }
739
740    #[test]
741    #[should_panic(expected = "invalid entry: found duplicate key")]
742    fn action() {
743        let src = r#"{
744            "Foo": {
745              "entityTypes" : {},
746              "actions": {
747                "Bar": {},
748                "Bar": {}
749              }
750            }
751        }"#;
752        serde_json::from_str::<SchemaFragment>(src).unwrap();
753    }
754
755    #[test]
756    #[should_panic(expected = "invalid entry: found duplicate key")]
757    fn common_types() {
758        let src = r#"{
759            "Foo": {
760              "entityTypes" : {},
761              "actions": { },
762              "commonTypes": {
763                "Bar": {"type": "Long"},
764                "Bar": {"type": "String"}
765              }
766            }
767        }"#;
768        serde_json::from_str::<SchemaFragment>(src).unwrap();
769    }
770
771    #[test]
772    fn record_type() {
773        let src = r#"{
774            "Foo": {
775              "entityTypes" : {
776                "Bar": {
777                    "shape": {
778                        "type": "Record",
779                        "attributes": {
780                            "Baz": {"type": "Long"},
781                            "Baz": {"type": "String"}
782                        }
783                    }
784                }
785              },
786              "actions": { }
787            }
788        }"#;
789        serde_json::from_str::<SchemaFragment>(src).unwrap();
790    }
791}