use cedar_policy_core::entities::CedarValueJson;
use serde::{
de::{MapAccess, Visitor},
Deserialize, Serialize,
};
use serde_with::serde_as;
use smol_str::SmolStr;
use std::collections::{BTreeMap, HashMap, HashSet};
use crate::{
human_schema::{
self, parser::parse_natural_schema_fragment, SchemaWarning, ToHumanSchemaStrError,
},
HumanSchemaError, Result,
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SchemaFragment(
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
pub HashMap<SmolStr, NamespaceDefinition>,
);
impl SchemaFragment {
pub fn from_json_value(json: serde_json::Value) -> Result<Self> {
serde_json::from_value(json).map_err(Into::into)
}
pub fn from_file(file: impl std::io::Read) -> Result<Self> {
serde_json::from_reader(file).map_err(Into::into)
}
pub fn from_str_natural(
src: &str,
) -> std::result::Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let tup = parse_natural_schema_fragment(src)?;
Ok(tup)
}
pub fn from_file_natural(
mut file: impl std::io::Read,
) -> std::result::Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let mut src = String::new();
file.read_to_string(&mut src)?;
Self::from_str_natural(&src)
}
pub fn as_natural_schema(&self) -> std::result::Result<String, ToHumanSchemaStrError> {
let src = human_schema::json_schema_to_custom_schema_str(self)?;
Ok(src)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde_as]
#[serde(deny_unknown_fields)]
#[doc(hidden)]
pub struct NamespaceDefinition {
#[serde(default)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
#[serde(rename = "commonTypes")]
pub common_types: HashMap<SmolStr, SchemaType>,
#[serde(rename = "entityTypes")]
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
pub entity_types: HashMap<SmolStr, EntityType>,
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
pub actions: HashMap<SmolStr, ActionType>,
}
impl NamespaceDefinition {
pub fn new(
entity_types: impl IntoIterator<Item = (SmolStr, EntityType)>,
actions: impl IntoIterator<Item = (SmolStr, ActionType)>,
) -> Self {
Self {
common_types: HashMap::new(),
entity_types: entity_types.into_iter().collect(),
actions: actions.into_iter().collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EntityType {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(rename = "memberOfTypes")]
pub member_of_types: Vec<SmolStr>,
#[serde(default)]
#[serde(skip_serializing_if = "AttributesOrContext::is_empty_record")]
pub shape: AttributesOrContext,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AttributesOrContext(
pub SchemaType,
);
impl AttributesOrContext {
pub fn into_inner(self) -> SchemaType {
self.0
}
pub fn is_empty_record(&self) -> bool {
self.0.is_empty_record()
}
}
impl Default for AttributesOrContext {
fn default() -> Self {
Self(SchemaType::Type(SchemaTypeVariant::Record {
attributes: BTreeMap::new(),
additional_attributes: partial_schema_default(),
}))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActionType {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<SmolStr, CedarValueJson>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "appliesTo")]
pub applies_to: Option<ApplySpec>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "memberOf")]
pub member_of: Option<Vec<ActionEntityUID>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplySpec {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "resourceTypes")]
pub resource_types: Option<Vec<SmolStr>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "principalTypes")]
pub principal_types: Option<Vec<SmolStr>>,
#[serde(default)]
#[serde(skip_serializing_if = "AttributesOrContext::is_empty_record")]
pub context: AttributesOrContext,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActionEntityUID {
pub id: SmolStr,
#[serde(rename = "type")]
#[serde(default)]
pub ty: Option<SmolStr>,
}
impl ActionEntityUID {
pub fn default_type(id: SmolStr) -> Self {
Self { id, ty: None }
}
}
impl std::fmt::Display for ActionEntityUID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ty) = &self.ty {
write!(f, "{}::", ty)?
} else {
write!(f, "Action::")?
}
write!(f, "\"{}\"", self.id.escape_debug())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(untagged)]
pub enum SchemaType {
Type(SchemaTypeVariant),
TypeDef {
#[serde(rename = "type")]
type_name: SmolStr,
},
}
impl<'de> Deserialize<'de> for SchemaType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(SchemaTypeVisitor)
}
}
#[derive(Hash, Eq, PartialEq, Deserialize)]
#[serde(field_identifier, rename_all = "camelCase")]
enum TypeFields {
Type,
Element,
Attributes,
AdditionalAttributes,
Name,
}
macro_rules! type_field_name {
(Type) => {
"type"
};
(Element) => {
"element"
};
(Attributes) => {
"attributes"
};
(AdditionalAttributes) => {
"additionalAttributes"
};
(Name) => {
"name"
};
}
impl TypeFields {
fn as_str(&self) -> &'static str {
match self {
TypeFields::Type => type_field_name!(Type),
TypeFields::Element => type_field_name!(Element),
TypeFields::Attributes => type_field_name!(Attributes),
TypeFields::AdditionalAttributes => type_field_name!(AdditionalAttributes),
TypeFields::Name => type_field_name!(Name),
}
}
}
#[derive(Deserialize)]
struct AttributesTypeMap(
#[serde(with = "serde_with::rust::maps_duplicate_key_is_error")]
BTreeMap<SmolStr, TypeOfAttribute>,
);
struct SchemaTypeVisitor;
impl<'de> Visitor<'de> for SchemaTypeVisitor {
type Value = SchemaType;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("builtin type or reference to type defined in commonTypes")
}
fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
use TypeFields::*;
let mut type_name: Option<std::result::Result<SmolStr, M::Error>> = None;
let mut element: Option<std::result::Result<SchemaType, M::Error>> = None;
let mut attributes: Option<std::result::Result<AttributesTypeMap, M::Error>> = None;
let mut additional_attributes: Option<std::result::Result<bool, M::Error>> = None;
let mut name: Option<std::result::Result<SmolStr, M::Error>> = None;
while let Some(key) = map.next_key()? {
match key {
Type => {
if type_name.is_some() {
return Err(serde::de::Error::duplicate_field(Type.as_str()));
}
type_name = Some(map.next_value());
}
Element => {
if element.is_some() {
return Err(serde::de::Error::duplicate_field(Element.as_str()));
}
element = Some(map.next_value());
}
Attributes => {
if attributes.is_some() {
return Err(serde::de::Error::duplicate_field(Attributes.as_str()));
}
attributes = Some(map.next_value());
}
AdditionalAttributes => {
if additional_attributes.is_some() {
return Err(serde::de::Error::duplicate_field(
AdditionalAttributes.as_str(),
));
}
additional_attributes = Some(map.next_value());
}
Name => {
if name.is_some() {
return Err(serde::de::Error::duplicate_field(Name.as_str()));
}
name = Some(map.next_value());
}
}
}
Self::build_schema_type::<M>(type_name, element, attributes, additional_attributes, name)
}
}
impl SchemaTypeVisitor {
fn build_schema_type<'de, M>(
type_name: Option<std::result::Result<SmolStr, M::Error>>,
element: Option<std::result::Result<SchemaType, M::Error>>,
attributes: Option<std::result::Result<AttributesTypeMap, M::Error>>,
additional_attributes: Option<std::result::Result<bool, M::Error>>,
name: Option<std::result::Result<SmolStr, M::Error>>,
) -> std::result::Result<SchemaType, M::Error>
where
M: MapAccess<'de>,
{
use TypeFields::*;
let present_fields = [
(Type, type_name.is_some()),
(Element, element.is_some()),
(Attributes, attributes.is_some()),
(AdditionalAttributes, additional_attributes.is_some()),
(Name, name.is_some()),
]
.into_iter()
.filter(|(_, present)| *present)
.map(|(field, _)| field)
.collect::<HashSet<_>>();
let error_if_fields = |fs: &[TypeFields],
expected: &'static [&'static str]|
-> std::result::Result<(), M::Error> {
for f in fs {
if present_fields.contains(f) {
return Err(serde::de::Error::unknown_field(f.as_str(), expected));
}
}
Ok(())
};
let error_if_any_fields = || -> std::result::Result<(), M::Error> {
error_if_fields(&[Element, Attributes, AdditionalAttributes, Name], &[])
};
match type_name.transpose()?.as_ref().map(|s| s.as_str()) {
Some("String") => {
error_if_any_fields()?;
Ok(SchemaType::Type(SchemaTypeVariant::String))
}
Some("Long") => {
error_if_any_fields()?;
Ok(SchemaType::Type(SchemaTypeVariant::Long))
}
Some("Boolean") => {
error_if_any_fields()?;
Ok(SchemaType::Type(SchemaTypeVariant::Boolean))
}
Some("Set") => {
error_if_fields(
&[Attributes, AdditionalAttributes, Name],
&[type_field_name!(Element)],
)?;
if let Some(element) = element {
Ok(SchemaType::Type(SchemaTypeVariant::Set {
element: Box::new(element?),
}))
} else {
Err(serde::de::Error::missing_field(Element.as_str()))
}
}
Some("Record") => {
error_if_fields(
&[Element, Name],
&[
type_field_name!(Attributes),
type_field_name!(AdditionalAttributes),
],
)?;
if let Some(attributes) = attributes {
let additional_attributes =
additional_attributes.unwrap_or(Ok(partial_schema_default()));
Ok(SchemaType::Type(SchemaTypeVariant::Record {
attributes: attributes?.0,
additional_attributes: additional_attributes?,
}))
} else {
Err(serde::de::Error::missing_field(Attributes.as_str()))
}
}
Some("Entity") => {
error_if_fields(
&[Element, Attributes, AdditionalAttributes],
&[type_field_name!(Name)],
)?;
if let Some(name) = name {
Ok(SchemaType::Type(SchemaTypeVariant::Entity { name: name? }))
} else {
Err(serde::de::Error::missing_field(Name.as_str()))
}
}
Some("Extension") => {
error_if_fields(
&[Element, Attributes, AdditionalAttributes],
&[type_field_name!(Name)],
)?;
if let Some(name) = name {
Ok(SchemaType::Type(SchemaTypeVariant::Extension {
name: name?,
}))
} else {
Err(serde::de::Error::missing_field(Name.as_str()))
}
}
Some(type_name) => {
error_if_any_fields()?;
Ok(SchemaType::TypeDef {
type_name: type_name.into(),
})
}
None => Err(serde::de::Error::missing_field(Type.as_str())),
}
}
}
impl From<SchemaTypeVariant> for SchemaType {
fn from(variant: SchemaTypeVariant) -> Self {
Self::Type(variant)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(tag = "type")]
pub enum SchemaTypeVariant {
String,
Long,
Boolean,
Set {
element: Box<SchemaType>,
},
Record {
attributes: BTreeMap<SmolStr, TypeOfAttribute>,
#[serde(rename = "additionalAttributes")]
#[serde(skip_serializing_if = "is_partial_schema_default")]
additional_attributes: bool,
},
Entity {
name: SmolStr,
},
Extension {
name: SmolStr,
},
}
fn is_partial_schema_default(b: &bool) -> bool {
*b == partial_schema_default()
}
pub(crate) static SCHEMA_TYPE_VARIANT_TAGS: &[&str] = &[
"String",
"Long",
"Boolean",
"Set",
"Record",
"Entity",
"Extension",
];
impl SchemaType {
pub fn is_extension(&self) -> Option<bool> {
match self {
Self::Type(SchemaTypeVariant::Extension { .. }) => Some(true),
Self::Type(SchemaTypeVariant::Set { element }) => element.is_extension(),
Self::Type(SchemaTypeVariant::Record { attributes, .. }) => attributes
.values()
.try_fold(false, |a, e| match e.ty.is_extension() {
Some(true) => Some(true),
Some(false) => Some(a),
None => None,
}),
Self::Type(_) => Some(false),
Self::TypeDef { .. } => None,
}
}
pub fn is_empty_record(&self) -> bool {
match self {
Self::Type(SchemaTypeVariant::Record {
attributes,
additional_attributes,
}) => *additional_attributes == partial_schema_default() && attributes.is_empty(),
_ => false,
}
}
}
#[cfg(feature = "arbitrary")]
#[allow(clippy::panic)]
impl<'a> arbitrary::Arbitrary<'a> for SchemaType {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<SchemaType> {
use cedar_policy_core::ast::Name;
use std::collections::BTreeSet;
Ok(SchemaType::Type(match u.int_in_range::<u8>(1..=8)? {
1 => SchemaTypeVariant::String,
2 => SchemaTypeVariant::Long,
3 => SchemaTypeVariant::Boolean,
4 => SchemaTypeVariant::Set {
element: Box::new(u.arbitrary()?),
},
5 => {
let attributes = {
let attr_names: BTreeSet<String> = u.arbitrary()?;
attr_names
.into_iter()
.map(|attr_name| Ok((attr_name.into(), u.arbitrary()?)))
.collect::<arbitrary::Result<_>>()?
};
SchemaTypeVariant::Record {
attributes,
additional_attributes: u.arbitrary()?,
}
}
6 => {
let name: Name = u.arbitrary()?;
SchemaTypeVariant::Entity {
name: name.to_string().into(),
}
}
7 => SchemaTypeVariant::Extension {
name: "ipaddr".into(),
},
8 => SchemaTypeVariant::Extension {
name: "decimal".into(),
},
n => panic!("bad index: {n}"),
}))
}
fn size_hint(_depth: usize) -> (usize, Option<usize>) {
(1, None) }
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct TypeOfAttribute {
#[serde(flatten)]
pub ty: SchemaType,
#[serde(default = "record_attribute_required_default")]
#[serde(skip_serializing_if = "is_record_attribute_required_default")]
pub required: bool,
}
fn is_record_attribute_required_default(b: &bool) -> bool {
*b == record_attribute_required_default()
}
fn partial_schema_default() -> bool {
false
}
fn record_attribute_required_default() -> bool {
true
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_entity_type_parser1() {
let user = r#"
{
"memberOfTypes" : ["UserGroup"]
}
"#;
let et = serde_json::from_str::<EntityType>(user).expect("Parse Error");
assert_eq!(et.member_of_types, vec!["UserGroup"]);
assert_eq!(
et.shape.into_inner(),
SchemaType::Type(SchemaTypeVariant::Record {
attributes: BTreeMap::new(),
additional_attributes: false
})
);
}
#[test]
fn test_entity_type_parser2() {
let src = r#"
{ }
"#;
let et = serde_json::from_str::<EntityType>(src).expect("Parse Error");
assert_eq!(et.member_of_types.len(), 0);
assert_eq!(
et.shape.into_inner(),
SchemaType::Type(SchemaTypeVariant::Record {
attributes: BTreeMap::new(),
additional_attributes: false
})
);
}
#[test]
fn test_action_type_parser1() {
let src = r#"
{
"appliesTo" : {
"resourceTypes": ["Album"],
"principalTypes": ["User"]
},
"memberOf": [{"id": "readWrite"}]
}
"#;
let at: ActionType = serde_json::from_str(src).expect("Parse Error");
let spec = ApplySpec {
resource_types: Some(vec!["Album".into()]),
principal_types: Some(vec!["User".into()]),
context: AttributesOrContext::default(),
};
assert_eq!(at.applies_to, Some(spec));
assert_eq!(
at.member_of,
Some(vec![ActionEntityUID {
ty: None,
id: "readWrite".into()
}])
);
}
#[test]
fn test_action_type_parser2() {
let src = r#"
{ }
"#;
let at: ActionType = serde_json::from_str(src).expect("Parse Error");
assert_eq!(at.applies_to, None);
assert!(at.member_of.is_none());
}
#[test]
fn test_schema_file_parser() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"memberOfTypes": ["UserGroup"]
},
"Photo": {
"memberOfTypes": ["Album", "Account"]
},
"Album": {
"memberOfTypes": ["Album", "Account"]
},
"Account": { },
"UserGroup": { }
},
"actions": {
"readOnly": { },
"readWrite": { },
"createAlbum": {
"appliesTo" : {
"resourceTypes": ["Account", "Album"],
"principalTypes": ["User"]
},
"memberOf": [{"id": "readWrite"}]
},
"addPhotoToAlbum": {
"appliesTo" : {
"resourceTypes": ["Album"],
"principalTypes": ["User"]
},
"memberOf": [{"id": "readWrite"}]
},
"viewPhoto": {
"appliesTo" : {
"resourceTypes": ["Photo"],
"principalTypes": ["User"]
},
"memberOf": [{"id": "readOnly"}, {"id": "readWrite"}]
},
"viewComments": {
"appliesTo" : {
"resourceTypes": ["Photo"],
"principalTypes": ["User"]
},
"memberOf": [{"id": "readOnly"}, {"id": "readWrite"}]
}
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
assert_eq!(schema_file.entity_types.len(), 5);
assert_eq!(schema_file.actions.len(), 6);
}
#[test]
fn test_parse_namespaces() {
let src = r#"
{
"foo::foo::bar::baz": {
"entityTypes": {},
"actions": {}
}
}"#;
let schema: SchemaFragment = serde_json::from_str(src).expect("Parse Error");
let (namespace, _descriptor) = schema.0.into_iter().next().unwrap();
assert_eq!(namespace, "foo::foo::bar::baz".to_string());
}
#[test]
#[should_panic(expected = "unknown field `requiredddddd`")]
fn test_schema_file_with_misspelled_required() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"favorite": {
"type": "Entity",
"name": "Photo",
"requiredddddd": false
}
}
}
}
},
"actions": {}
});
let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
println!("{:#?}", schema);
}
#[test]
#[should_panic(expected = "unknown field `nameeeeee`")]
fn test_schema_file_with_misspelled_field() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"favorite": {
"type": "Entity",
"nameeeeee": "Photo",
}
}
}
}
},
"actions": {}
});
let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
println!("{:#?}", schema);
}
#[test]
#[should_panic(expected = "unknown field `extra`")]
fn test_schema_file_with_extra_field() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"favorite": {
"type": "Entity",
"name": "Photo",
"extra": "Should not exist"
}
}
}
}
},
"actions": {}
});
let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
println!("{:#?}", schema);
}
#[test]
#[should_panic(expected = "unknown field `memberOfTypes`")]
fn test_schema_file_with_misplaced_field() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"shape": {
"memberOfTypes": [],
"type": "Record",
"attributes": {
"favorite": {
"type": "Entity",
"name": "Photo",
}
}
}
}
},
"actions": {}
});
let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
println!("{:#?}", schema);
}
#[test]
#[should_panic(expected = "missing field `name`")]
fn schema_file_with_missing_field() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"favorite": {
"type": "Entity",
}
}
}
}
},
"actions": {}
});
let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
println!("{:#?}", schema);
}
#[test]
#[should_panic(expected = "missing field `type`")]
fn schema_file_with_missing_type() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"shape": { }
}
},
"actions": {}
});
let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
println!("{:#?}", schema);
}
#[test]
#[should_panic(expected = "unknown field `attributes`")]
fn schema_file_unexpected_malformed_attribute() {
let src = serde_json::json!(
{
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"a": {
"type": "Long",
"attributes": {
"b": {"foo": "bar"}
}
}
}
}
}
},
"actions": {}
});
let schema: NamespaceDefinition = serde_json::from_value(src).unwrap();
println!("{:#?}", schema);
}
}
#[cfg(test)]
mod test_duplicates_error {
use super::*;
#[test]
#[should_panic(expected = "invalid entry: found duplicate key")]
fn namespace() {
let src = r#"{
"Foo": {
"entityTypes" : {},
"actions": {}
},
"Foo": {
"entityTypes" : {},
"actions": {}
}
}"#;
serde_json::from_str::<SchemaFragment>(src).unwrap();
}
#[test]
#[should_panic(expected = "invalid entry: found duplicate key")]
fn entity_type() {
let src = r#"{
"Foo": {
"entityTypes" : {
"Bar": {},
"Bar": {},
},
"actions": {}
}
}"#;
serde_json::from_str::<SchemaFragment>(src).unwrap();
}
#[test]
#[should_panic(expected = "invalid entry: found duplicate key")]
fn action() {
let src = r#"{
"Foo": {
"entityTypes" : {},
"actions": {
"Bar": {},
"Bar": {}
}
}
}"#;
serde_json::from_str::<SchemaFragment>(src).unwrap();
}
#[test]
#[should_panic(expected = "invalid entry: found duplicate key")]
fn common_types() {
let src = r#"{
"Foo": {
"entityTypes" : {},
"actions": { },
"commonTypes": {
"Bar": {"type": "Long"},
"Bar": {"type": "String"}
}
}
}"#;
serde_json::from_str::<SchemaFragment>(src).unwrap();
}
#[test]
#[should_panic(expected = "invalid entry: found duplicate key")]
fn record_type() {
let src = r#"{
"Foo": {
"entityTypes" : {
"Bar": {
"shape": {
"type": "Record",
"attributes": {
"Baz": {"type": "Long"},
"Baz": {"type": "String"}
}
}
}
},
"actions": { }
}
}"#;
serde_json::from_str::<SchemaFragment>(src).unwrap();
}
}