use crate::{
ast::{Eid, EntityUID, InternalName, Name, UnreservedId},
entities::CedarValueJson,
est::Annotations,
extensions::Extensions,
parser::Loc,
validator::{SchemaError, ValidatorSchemaFragment},
FromNormalizedStr,
};
use educe::Educe;
use itertools::Itertools;
use nonempty::{nonempty, NonEmpty};
use serde::{
de::{MapAccess, Visitor},
ser::SerializeMap,
Deserialize, Deserializer, Serialize, Serializer,
};
use serde_with::serde_as;
use smol_str::{SmolStr, ToSmolStr};
use std::hash::Hash;
use std::{
collections::{BTreeMap, HashMap, HashSet},
fmt::Display,
marker::PhantomData,
str::FromStr,
};
use thiserror::Error;
use crate::validator::{
cedar_schema::{
self, fmt::ToCedarSchemaSyntaxError, parser::parse_cedar_schema_fragment, SchemaWarning,
},
err::{schema_errors::*, Result},
AllDefs, CedarSchemaError, CedarSchemaParseError, ConditionalName, RawName, ReferenceType,
};
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
pub struct CommonType<N> {
#[serde(flatten)]
pub ty: Type<N>,
#[serde(default)]
#[serde(skip_serializing_if = "Annotations::is_empty")]
pub annotations: Annotations,
#[serde(skip)]
#[educe(PartialEq(ignore))]
pub loc: Option<Loc>,
}
#[derive(Educe, Debug, Clone, Deserialize)]
#[educe(PartialEq, Eq)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(transparent)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[cfg_attr(feature = "wasm", serde(rename = "SchemaJson"))]
pub struct Fragment<N>(
#[serde(deserialize_with = "deserialize_schema_fragment")]
#[cfg_attr(
feature = "wasm",
tsify(type = "Record<string, NamespaceDefinition<N>>")
)]
pub BTreeMap<Option<Name>, NamespaceDefinition<N>>,
);
fn deserialize_schema_fragment<'de, D, N: Deserialize<'de> + From<RawName>>(
deserializer: D,
) -> std::result::Result<BTreeMap<Option<Name>, NamespaceDefinition<N>>, D::Error>
where
D: Deserializer<'de>,
{
let raw: BTreeMap<SmolStr, NamespaceDefinition<N>> =
serde_with::rust::maps_duplicate_key_is_error::deserialize(deserializer)?;
Ok(BTreeMap::from_iter(
raw.into_iter()
.map(|(key, value)| {
let key = if key.is_empty() {
if !value.annotations.is_empty() {
Err(serde::de::Error::custom(
"annotations are not allowed on the empty namespace".to_string(),
))?
}
None
} else {
Some(Name::from_normalized_str(&key).map_err(|err| {
serde::de::Error::custom(format!("invalid namespace `{key}`: {err}"))
})?)
};
Ok((key, value))
})
.collect::<std::result::Result<Vec<(Option<Name>, NamespaceDefinition<N>)>, D::Error>>(
)?,
))
}
impl<N: Serialize> Serialize for Fragment<N> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (k, v) in &self.0 {
let k: SmolStr = match k {
None => "".into(),
Some(name) => name.to_smolstr(),
};
map.serialize_entry(&k, &v)?;
}
map.end()
}
}
impl Fragment<RawName> {
pub fn from_json_str(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| JsonDeserializationError::new(e, Some(json)).into())
}
pub fn from_json_value(json: serde_json::Value) -> Result<Self> {
serde_json::from_value(json).map_err(|e| JsonDeserializationError::new(e, None).into())
}
pub fn from_json_file(file: impl std::io::Read) -> Result<Self> {
serde_json::from_reader(file).map_err(|e| JsonDeserializationError::new(e, None).into())
}
pub fn from_cedarschema_str<'a>(
src: &str,
extensions: &Extensions<'a>,
) -> std::result::Result<(Self, impl Iterator<Item = SchemaWarning> + 'a), CedarSchemaError>
{
parse_cedar_schema_fragment(src, extensions)
.map_err(|e| CedarSchemaParseError::new(e, src).into())
}
pub fn from_cedarschema_file<'a>(
mut file: impl std::io::Read,
extensions: &'a Extensions<'_>,
) -> std::result::Result<(Self, impl Iterator<Item = SchemaWarning> + 'a), CedarSchemaError>
{
let mut src = String::new();
file.read_to_string(&mut src)?;
Self::from_cedarschema_str(&src, extensions)
}
pub fn to_internal_name_fragment_with_resolved_types(
&self,
) -> std::result::Result<Fragment<InternalName>, SchemaError> {
let validator_fragment = ValidatorSchemaFragment::from_schema_fragment(self.clone())?;
let mut all_defs = AllDefs::single_fragment(&validator_fragment);
let cedar_namespace = InternalName::__cedar();
let primitives_as_internal_names: Vec<InternalName> = ["Bool", "Long", "String"]
.into_iter()
.map(|n| {
#[expect(clippy::unwrap_used, reason = "these are all valid InternalName's")]
InternalName::parse_unqualified_name(n).unwrap()
})
.collect();
for tyname in &primitives_as_internal_names {
all_defs.mark_as_defined_as_common_type(tyname.qualify_with(Some(&cedar_namespace)));
if !all_defs.is_defined_as_common(tyname) && !all_defs.is_defined_as_entity(tyname) {
all_defs.mark_as_defined_as_common_type(tyname.clone());
}
}
for ext_type in Extensions::all_available().ext_types() {
all_defs.mark_as_defined_as_common_type(
ext_type.as_ref().qualify_with(Some(&cedar_namespace)),
);
if !all_defs.is_defined_as_common(ext_type.as_ref())
&& !all_defs.is_defined_as_entity(ext_type.as_ref())
{
all_defs.mark_as_defined_as_common_type(ext_type.as_ref().qualify_with(None));
}
}
let conditional_fragment = Fragment(
self.0
.iter()
.map(|(ns_name, ns_def)| {
let internal_ns_name = ns_name.as_ref().map(|name| name.clone().into());
let conditional_ns_def = ns_def
.clone()
.conditionally_qualify_type_references(internal_ns_name.as_ref());
(ns_name.clone(), conditional_ns_def)
})
.collect(),
);
let internal_name_fragment = Fragment(
conditional_fragment
.0
.into_iter()
.map(|(ns_name, ns_def)| {
ns_def
.fully_qualify_type_references(&all_defs)
.map(|resolved_ns_def| (ns_name, resolved_ns_def))
})
.collect::<Result<BTreeMap<_, _>>>()?,
);
internal_name_fragment.resolve_entity_or_common_types(&all_defs)
}
}
impl<N: Display> Fragment<N> {
pub fn to_cedarschema(&self) -> std::result::Result<String, ToCedarSchemaSyntaxError> {
let src = cedar_schema::fmt::json_schema_to_cedar_schema_str(self)?;
Ok(src)
}
}
impl Fragment<InternalName> {
pub fn resolve_entity_or_common_types(
self,
all_defs: &AllDefs,
) -> Result<Fragment<InternalName>> {
Ok(Fragment(
self.0
.into_iter()
.map(|(ns_name, ns_def)| {
Ok((ns_name, ns_def.resolve_entity_or_common_types(all_defs)?))
})
.collect::<Result<_>>()?,
))
}
}
#[derive(Educe, Debug, Clone, Serialize)]
#[educe(PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct CommonTypeId(#[cfg_attr(feature = "wasm", tsify(type = "string"))] UnreservedId);
impl From<CommonTypeId> for UnreservedId {
fn from(value: CommonTypeId) -> Self {
value.0
}
}
impl AsRef<UnreservedId> for CommonTypeId {
fn as_ref(&self) -> &UnreservedId {
&self.0
}
}
impl CommonTypeId {
pub fn new(id: UnreservedId) -> std::result::Result<Self, ReservedCommonTypeBasenameError> {
if Self::is_reserved_schema_keyword(&id) {
Err(ReservedCommonTypeBasenameError { id })
} else {
Ok(Self(id))
}
}
pub fn unchecked(id: UnreservedId) -> Self {
Self(id)
}
fn is_reserved_schema_keyword(id: &UnreservedId) -> bool {
matches!(
id.as_ref(),
"Bool" | "Boolean" | "Entity" | "Extension" | "Long" | "Record" | "Set" | "String"
)
}
#[cfg(feature = "arbitrary")]
fn make_into_valid_common_type_id(id: &UnreservedId) -> Self {
Self::new(id.clone()).unwrap_or_else(|_| {
#[expect(
clippy::unwrap_used,
reason = "`_Bool`, `_Record`, and etc are valid unreserved names."
)]
let new_id = format!("_{id}").parse().unwrap();
#[expect(
clippy::unwrap_used,
reason = "`_Bool`, `_Record`, and etc are valid common type basenames."
)]
Self::new(new_id).unwrap()
})
}
}
impl Display for CommonTypeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for CommonTypeId {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let id: UnreservedId = u.arbitrary()?;
Ok(CommonTypeId::make_into_valid_common_type_id(&id))
}
fn size_hint(depth: usize) -> (usize, Option<usize>) {
<UnreservedId as arbitrary::Arbitrary>::size_hint(depth)
}
}
impl<'de> Deserialize<'de> for CommonTypeId {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
UnreservedId::deserialize(deserializer).and_then(|id| {
CommonTypeId::new(id).map_err(|e| serde::de::Error::custom(format!("{e}")))
})
}
}
#[derive(Debug, Error, PartialEq, Eq, Clone)]
#[error("this is reserved and cannot be the basename of a common-type declaration: {id}")]
pub struct ReservedCommonTypeBasenameError {
pub(crate) id: UnreservedId,
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq)]
#[serde_as]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(bound(serialize = "N: Serialize"))]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[doc(hidden)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct NamespaceDefinition<N> {
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
pub common_types: BTreeMap<CommonTypeId, CommonType<N>>,
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
pub entity_types: BTreeMap<UnreservedId, EntityType<N>>,
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
pub actions: BTreeMap<SmolStr, ActionType<N>>,
#[serde(default)]
#[serde(skip_serializing_if = "Annotations::is_empty")]
pub annotations: Annotations,
#[cfg(feature = "extended-schema")]
#[serde(skip)]
#[educe(Eq(ignore))]
pub loc: Option<Loc>,
}
#[cfg(test)]
impl<N> NamespaceDefinition<N> {
pub fn new(
entity_types: impl IntoIterator<Item = (UnreservedId, EntityType<N>)>,
actions: impl IntoIterator<Item = (SmolStr, ActionType<N>)>,
) -> Self {
Self {
common_types: BTreeMap::new(),
entity_types: entity_types.into_iter().collect(),
actions: actions.into_iter().collect(),
annotations: Annotations::new(),
#[cfg(feature = "extended-schema")]
loc: None,
}
}
}
impl NamespaceDefinition<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> NamespaceDefinition<ConditionalName> {
NamespaceDefinition {
common_types: self
.common_types
.into_iter()
.map(|(k, v)| {
(
k,
CommonType {
ty: v.ty.conditionally_qualify_type_references(ns),
annotations: v.annotations,
loc: v.loc,
},
)
})
.collect(),
entity_types: self
.entity_types
.into_iter()
.map(|(k, v)| (k, v.conditionally_qualify_type_references(ns)))
.collect(),
actions: self
.actions
.into_iter()
.map(|(k, v)| (k, v.conditionally_qualify_type_references(ns)))
.collect(),
annotations: self.annotations,
#[cfg(feature = "extended-schema")]
loc: self.loc,
}
}
}
impl NamespaceDefinition<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> Result<NamespaceDefinition<InternalName>> {
Ok(NamespaceDefinition {
common_types: self
.common_types
.into_iter()
.map(|(k, v)| {
Ok((
k,
CommonType {
ty: v.ty.fully_qualify_type_references(all_defs)?,
annotations: v.annotations,
loc: v.loc,
},
))
})
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
entity_types: self
.entity_types
.into_iter()
.map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
actions: self
.actions
.into_iter()
.map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
.collect::<Result<_>>()?,
annotations: self.annotations,
#[cfg(feature = "extended-schema")]
loc: self.loc,
})
}
}
enum ResolvedTypeVariant {
TypeVariant(TypeVariant<InternalName>),
CommonTypeRef(InternalName),
}
impl NamespaceDefinition<InternalName> {
pub fn resolve_entity_or_common_types(
self,
all_defs: &AllDefs,
) -> Result<NamespaceDefinition<InternalName>> {
Ok(NamespaceDefinition {
common_types: self
.common_types
.into_iter()
.map(|(k, v)| {
Ok((
k,
CommonType {
ty: v.ty.resolve_entity_or_common_type(all_defs)?,
annotations: v.annotations,
loc: v.loc,
},
))
})
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
entity_types: self
.entity_types
.into_iter()
.map(|(k, v)| Ok((k, v.resolve_entity_type_entity_or_common(all_defs)?)))
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
actions: self
.actions
.into_iter()
.map(|(k, v)| Ok((k, v.resolve_action_type_entity_or_common(all_defs)?)))
.collect::<Result<_>>()?,
annotations: self.annotations,
#[cfg(feature = "extended-schema")]
loc: self.loc,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(untagged)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum EntityTypeKind<N> {
Standard(StandardEntityType<N>),
Enum {
#[serde(rename = "enum")]
choices: NonEmpty<SmolStr>,
},
}
#[derive(Educe, Debug, Clone, Serialize)]
#[educe(PartialEq, Eq)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
pub struct EntityType<N> {
#[serde(flatten)]
pub kind: EntityTypeKind<N>,
#[serde(default)]
#[serde(skip_serializing_if = "Annotations::is_empty")]
pub annotations: Annotations,
#[serde(skip)]
#[educe(PartialEq(ignore))]
pub loc: Option<Loc>,
}
impl<'de, N: Deserialize<'de> + From<RawName>> Deserialize<'de> for EntityType<N> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Default)]
enum RealOption<T> {
Some(T),
#[default]
None,
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for RealOption<T> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
T::deserialize(deserializer).map(Self::Some)
}
}
impl<T> From<RealOption<T>> for Option<T> {
fn from(value: RealOption<T>) -> Self {
match value {
RealOption::Some(v) => Self::Some(v),
RealOption::None => None,
}
}
}
#[derive(Deserialize)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
struct Everything<N> {
#[serde(default)]
member_of_types: RealOption<Vec<N>>,
#[serde(default)]
shape: RealOption<AttributesOrContext<N>>,
#[serde(default)]
tags: RealOption<Type<N>>,
#[serde(default)]
#[serde(rename = "enum")]
choices: RealOption<NonEmpty<SmolStr>>,
#[serde(default)]
annotations: Annotations,
}
let value: Everything<N> = Everything::deserialize(deserializer)?;
if let Some(choices) = value.choices.into() {
let mut unexpected_fields: Vec<&str> = vec![];
if Option::<Vec<N>>::from(value.member_of_types).is_some() {
unexpected_fields.push("memberOfTypes");
}
if Option::<AttributesOrContext<N>>::from(value.shape).is_some() {
unexpected_fields.push("shape");
}
if Option::<Type<N>>::from(value.tags).is_some() {
unexpected_fields.push("tags");
}
if !unexpected_fields.is_empty() {
return Err(serde::de::Error::custom(format!(
"unexpected field: {}",
unexpected_fields.into_iter().join(", ")
)));
}
Ok(EntityType {
kind: EntityTypeKind::Enum { choices },
annotations: value.annotations,
loc: None,
})
} else {
Ok(EntityType {
kind: EntityTypeKind::Standard(StandardEntityType {
member_of_types: Option::from(value.member_of_types).unwrap_or_default(),
shape: Option::from(value.shape).unwrap_or_default(),
tags: Option::from(value.tags),
}),
annotations: value.annotations,
loc: None,
})
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Educe)]
#[educe(PartialEq, Eq)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct StandardEntityType<N> {
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub member_of_types: Vec<N>,
#[serde(skip_serializing_if = "AttributesOrContext::is_empty_record")]
#[serde(default)]
pub shape: AttributesOrContext<N>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub tags: Option<Type<N>>,
}
#[cfg(test)]
impl<N> From<StandardEntityType<N>> for EntityType<N> {
fn from(value: StandardEntityType<N>) -> Self {
Self {
kind: EntityTypeKind::Standard(value),
annotations: Annotations::new(),
loc: None,
}
}
}
impl EntityType<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> EntityType<ConditionalName> {
let Self {
kind,
annotations,
loc,
} = self;
match kind {
EntityTypeKind::Enum { choices } => EntityType {
kind: EntityTypeKind::Enum { choices },
annotations,
loc,
},
EntityTypeKind::Standard(ty) => EntityType {
kind: EntityTypeKind::Standard(StandardEntityType {
member_of_types: ty
.member_of_types
.into_iter()
.map(|rname| rname.conditionally_qualify_with(ns, ReferenceType::Entity)) .collect(),
shape: ty.shape.conditionally_qualify_type_references(ns),
tags: ty
.tags
.map(|ty| ty.conditionally_qualify_type_references(ns)),
}),
annotations,
loc,
},
}
}
}
impl EntityType<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<EntityType<InternalName>, TypeNotDefinedError> {
let Self {
kind,
annotations,
loc,
} = self;
Ok(match kind {
EntityTypeKind::Enum { choices } => EntityType {
kind: EntityTypeKind::Enum { choices },
annotations,
loc,
},
EntityTypeKind::Standard(ty) => EntityType {
kind: EntityTypeKind::Standard(StandardEntityType {
member_of_types: ty
.member_of_types
.into_iter()
.map(|cname| cname.resolve(all_defs))
.collect::<std::result::Result<_, _>>()?,
shape: ty.shape.fully_qualify_type_references(all_defs)?,
tags: ty
.tags
.map(|ty| ty.fully_qualify_type_references(all_defs))
.transpose()?,
}),
annotations,
loc,
},
})
}
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(transparent)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct AttributesOrContext<N>(
pub Type<N>,
);
impl<N> AttributesOrContext<N> {
pub fn into_inner(self) -> Type<N> {
self.0
}
pub fn is_empty_record(&self) -> bool {
self.0.is_empty_record()
}
pub fn loc(&self) -> Option<&Loc> {
self.0.loc()
}
}
impl<N> Default for AttributesOrContext<N> {
fn default() -> Self {
Self::from(RecordType::default())
}
}
impl<N: Display> Display for AttributesOrContext<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<N> From<RecordType<N>> for AttributesOrContext<N> {
fn from(rty: RecordType<N>) -> AttributesOrContext<N> {
Self(Type::Type {
ty: TypeVariant::Record(rty),
loc: None,
})
}
}
impl AttributesOrContext<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> AttributesOrContext<ConditionalName> {
AttributesOrContext(self.0.conditionally_qualify_type_references(ns))
}
}
impl AttributesOrContext<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<AttributesOrContext<InternalName>, TypeNotDefinedError> {
Ok(AttributesOrContext(
self.0.fully_qualify_type_references(all_defs)?,
))
}
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ActionType<N> {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<SmolStr, CedarValueJson>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub applies_to: Option<ApplySpec<N>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub member_of: Option<Vec<ActionEntityUID<N>>>,
#[serde(default)]
#[serde(skip_serializing_if = "Annotations::is_empty")]
pub annotations: Annotations,
#[serde(skip)]
#[educe(PartialEq(ignore))]
pub loc: Option<Loc>,
#[cfg(feature = "extended-schema")]
#[serde(skip)]
#[educe(PartialEq(ignore))]
pub(crate) defn_loc: Option<Loc>,
}
impl ActionType<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> ActionType<ConditionalName> {
ActionType {
attributes: self.attributes,
applies_to: self
.applies_to
.map(|applyspec| applyspec.conditionally_qualify_type_references(ns)),
member_of: self.member_of.map(|v| {
v.into_iter()
.map(|aeuid| aeuid.conditionally_qualify_type_references(ns))
.collect()
}),
annotations: self.annotations,
loc: self.loc,
#[cfg(feature = "extended-schema")]
defn_loc: self.defn_loc,
}
}
}
impl ActionType<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> Result<ActionType<InternalName>> {
Ok(ActionType {
attributes: self.attributes,
applies_to: self
.applies_to
.map(|applyspec| applyspec.fully_qualify_type_references(all_defs))
.transpose()?,
member_of: self
.member_of
.map(|v| {
v.into_iter()
.map(|aeuid| aeuid.fully_qualify_type_references(all_defs))
.collect::<std::result::Result<_, ActionNotDefinedError>>()
})
.transpose()?,
annotations: self.annotations,
loc: self.loc,
#[cfg(feature = "extended-schema")]
defn_loc: self.defn_loc,
})
}
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ApplySpec<N> {
pub resource_types: Vec<N>,
pub principal_types: Vec<N>,
#[serde(default)]
#[serde(skip_serializing_if = "AttributesOrContext::is_empty_record")]
pub context: AttributesOrContext<N>,
}
impl ApplySpec<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> ApplySpec<ConditionalName> {
ApplySpec {
resource_types: self
.resource_types
.into_iter()
.map(|rname| rname.conditionally_qualify_with(ns, ReferenceType::Entity)) .collect(),
principal_types: self
.principal_types
.into_iter()
.map(|rname| rname.conditionally_qualify_with(ns, ReferenceType::Entity)) .collect(),
context: self.context.conditionally_qualify_type_references(ns),
}
}
}
impl ApplySpec<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<ApplySpec<InternalName>, TypeNotDefinedError> {
Ok(ApplySpec {
resource_types: self
.resource_types
.into_iter()
.map(|cname| cname.resolve(all_defs))
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
principal_types: self
.principal_types
.into_iter()
.map(|cname| cname.resolve(all_defs))
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
context: self.context.fully_qualify_type_references(all_defs)?,
})
}
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq, Hash)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct ActionEntityUID<N> {
pub id: SmolStr,
#[serde(rename = "type")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ty: Option<N>,
#[cfg(feature = "extended-schema")]
#[serde(skip)]
pub loc: Option<Loc>,
}
impl ActionEntityUID<RawName> {
pub fn new(ty: Option<RawName>, id: SmolStr) -> Self {
Self {
id,
ty,
#[cfg(feature = "extended-schema")]
loc: None,
}
}
pub fn default_type(id: SmolStr) -> Self {
Self {
id,
ty: None,
#[cfg(feature = "extended-schema")]
loc: None,
}
}
#[cfg(feature = "extended-schema")]
pub fn default_type_with_loc(id: SmolStr, loc: Option<Loc>) -> Self {
Self { id, ty: None, loc }
}
}
impl<N: std::fmt::Display> std::fmt::Display for ActionEntityUID<N> {
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())
}
}
impl ActionEntityUID<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> ActionEntityUID<ConditionalName> {
ActionEntityUID {
id: self.id,
ty: {
#[expect(clippy::expect_used, reason = "this is a valid raw name")]
let raw_name = self
.ty
.unwrap_or_else(|| RawName::from_str("Action").expect("valid raw name"));
Some(raw_name.conditionally_qualify_with(ns, ReferenceType::Entity))
},
#[cfg(feature = "extended-schema")]
loc: None,
}
}
pub fn qualify_with(self, ns: Option<&InternalName>) -> ActionEntityUID<InternalName> {
ActionEntityUID {
id: self.id,
ty: {
#[expect(clippy::expect_used, reason = "this is a valid raw name")]
let raw_name = self
.ty
.unwrap_or_else(|| RawName::from_str("Action").expect("valid raw name"));
Some(raw_name.qualify_with(ns))
},
#[cfg(feature = "extended-schema")]
loc: self.loc,
}
}
}
impl ActionEntityUID<ConditionalName> {
pub fn ty(&self) -> &ConditionalName {
#[expect(clippy::expect_used, reason = "by INVARIANT on self.ty")]
self.ty.as_ref().expect("by INVARIANT on self.ty")
}
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<ActionEntityUID<InternalName>, ActionNotDefinedError> {
for possibility in self.possibilities() {
if let Ok(euid) = EntityUID::try_from(possibility.clone()) {
if all_defs.is_defined_as_action(&euid) {
return Ok(possibility);
}
}
}
Err(ActionNotDefinedError(nonempty!(self)))
}
pub(crate) fn possibilities(&self) -> impl Iterator<Item = ActionEntityUID<InternalName>> + '_ {
self.ty()
.possibilities()
.map(|possibility| ActionEntityUID {
id: self.id.clone(),
ty: Some(possibility.clone()),
#[cfg(feature = "extended-schema")]
loc: None,
})
}
pub(crate) fn as_raw(&self) -> ActionEntityUID<RawName> {
ActionEntityUID {
id: self.id.clone(),
ty: self.ty.as_ref().map(|ty| ty.raw().clone()),
#[cfg(feature = "extended-schema")]
loc: None,
}
}
}
impl ActionEntityUID<Name> {
pub fn ty(&self) -> &Name {
#[expect(clippy::expect_used, reason = "by INVARIANT on self.ty")]
self.ty.as_ref().expect("by INVARIANT on self.ty")
}
}
impl ActionEntityUID<InternalName> {
pub fn ty(&self) -> &InternalName {
#[expect(clippy::expect_used, reason = "by INVARIANT on self.ty")]
self.ty.as_ref().expect("by INVARIANT on self.ty")
}
}
impl From<ActionEntityUID<Name>> for EntityUID {
fn from(aeuid: ActionEntityUID<Name>) -> Self {
EntityUID::from_components(aeuid.ty().clone().into(), Eid::new(aeuid.id), None)
}
}
impl TryFrom<ActionEntityUID<InternalName>> for EntityUID {
type Error = <InternalName as TryInto<Name>>::Error;
fn try_from(
aeuid: ActionEntityUID<InternalName>,
) -> std::result::Result<Self, <InternalName as TryInto<Name>>::Error> {
let ty = Name::try_from(aeuid.ty().clone())?;
#[cfg(feature = "extended-schema")]
let loc = aeuid.loc;
#[cfg(not(feature = "extended-schema"))]
let loc = None;
Ok(EntityUID::from_components(
ty.into(),
Eid::new(aeuid.id),
loc,
))
}
}
impl From<EntityUID> for ActionEntityUID<Name> {
fn from(euid: EntityUID) -> Self {
let (ty, id) = euid.components();
ActionEntityUID {
ty: Some(ty.into()),
id: id.into_smolstr(),
#[cfg(feature = "extended-schema")]
loc: None,
}
}
}
#[derive(Educe, Debug, Clone, Serialize)]
#[educe(PartialEq(bound(N: PartialEq)), Eq, PartialOrd, Ord(bound(N: Ord)))]
#[serde(untagged)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Type<N> {
Type {
#[serde(flatten)]
ty: TypeVariant<N>,
#[serde(skip)]
#[educe(PartialEq(ignore))]
#[educe(PartialOrd(ignore))]
loc: Option<Loc>,
},
CommonTypeRef {
#[serde(rename = "type")]
type_name: N,
#[serde(skip)]
#[educe(PartialEq(ignore))]
#[educe(PartialOrd(ignore))]
loc: Option<Loc>,
},
}
impl<N> Type<N> {
pub(crate) fn common_type_references(&self) -> Box<dyn Iterator<Item = &N> + '_> {
match self {
Type::Type {
ty: TypeVariant::Record(RecordType { attributes, .. }),
..
} => attributes
.values()
.map(|ty| ty.ty.common_type_references())
.fold(Box::new(std::iter::empty()), |it, tys| {
Box::new(it.chain(tys))
}),
Type::Type {
ty: TypeVariant::Set { element },
..
} => element.common_type_references(),
Type::Type {
ty: TypeVariant::EntityOrCommon { type_name },
..
} => Box::new(std::iter::once(type_name)),
Type::CommonTypeRef { type_name, .. } => Box::new(std::iter::once(type_name)),
_ => Box::new(std::iter::empty()),
}
}
pub fn is_extension(&self) -> Option<bool> {
match self {
Self::Type {
ty: TypeVariant::Extension { .. },
..
} => Some(true),
Self::Type {
ty: TypeVariant::Set { element },
..
} => element.is_extension(),
Self::Type {
ty: TypeVariant::Record(RecordType { 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::CommonTypeRef { .. } => None,
}
}
pub fn is_empty_record(&self) -> bool {
match self {
Self::Type {
ty: TypeVariant::Record(rty),
..
} => rty.is_empty_record(),
_ => false,
}
}
pub fn loc(&self) -> Option<&Loc> {
match self {
Self::Type { loc, .. } => loc.as_ref(),
Self::CommonTypeRef { loc, .. } => loc.as_ref(),
}
}
pub fn with_loc(self, new_loc: Option<Loc>) -> Self {
match self {
Self::Type { ty, loc: _loc } => Self::Type { ty, loc: new_loc },
Self::CommonTypeRef {
type_name,
loc: _loc,
} => Self::CommonTypeRef {
type_name,
loc: new_loc,
},
}
}
}
impl Type<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> Type<ConditionalName> {
match self {
Self::Type { ty, loc } => Type::Type {
ty: ty.conditionally_qualify_type_references(ns),
loc,
},
Self::CommonTypeRef { type_name, loc } => Type::CommonTypeRef {
type_name: type_name.conditionally_qualify_with(ns, ReferenceType::Common),
loc,
},
}
}
fn into_n<N: From<RawName>>(self) -> Type<N> {
match self {
Self::Type { ty, loc } => Type::Type {
ty: ty.into_n(),
loc,
},
Self::CommonTypeRef { type_name, loc } => Type::CommonTypeRef {
type_name: type_name.into(),
loc,
},
}
}
}
impl Type<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<Type<InternalName>, TypeNotDefinedError> {
match self {
Self::Type { ty, loc } => Ok(Type::Type {
ty: ty.fully_qualify_type_references(all_defs)?,
loc,
}),
Self::CommonTypeRef { type_name, loc } => Ok(Type::CommonTypeRef {
type_name: type_name.resolve(all_defs)?,
loc,
}),
}
}
}
impl Type<InternalName> {
pub fn resolve_entity_or_common_type(
self,
all_defs: &AllDefs,
) -> std::result::Result<Type<InternalName>, TypeNotDefinedError> {
match self {
Type::Type { ty, loc } => match ty.resolve_type_variant_entity_or_common(all_defs)? {
ResolvedTypeVariant::TypeVariant(resolved_ty) => Ok(Type::Type {
ty: resolved_ty,
loc,
}),
ResolvedTypeVariant::CommonTypeRef(type_name) => {
Ok(Type::CommonTypeRef { type_name, loc })
}
},
Type::CommonTypeRef { type_name, loc } => Ok(Type::CommonTypeRef { type_name, loc }),
}
}
}
impl TypeVariant<InternalName> {
fn resolve_type_variant_entity_or_common(
self,
all_defs: &AllDefs,
) -> std::result::Result<ResolvedTypeVariant, TypeNotDefinedError> {
match self {
TypeVariant::EntityOrCommon { type_name } => {
if all_defs.is_defined_as_common(&type_name) {
Ok(ResolvedTypeVariant::CommonTypeRef(type_name))
} else if all_defs.is_defined_as_entity(&type_name) {
Ok(ResolvedTypeVariant::TypeVariant(TypeVariant::Entity {
name: type_name,
}))
} else {
Err(TypeNotDefinedError {
undefined_types: NonEmpty {
head: ConditionalName::unconditional(
type_name,
ReferenceType::CommonOrEntity,
),
tail: vec![],
},
})
}
}
TypeVariant::Set { element } => {
Ok(ResolvedTypeVariant::TypeVariant(TypeVariant::Set {
element: Box::new(element.resolve_entity_or_common_type(all_defs)?),
}))
}
TypeVariant::Record(record_type) => Ok(ResolvedTypeVariant::TypeVariant(
TypeVariant::Record(record_type.resolve_record_type_entity_or_common(all_defs)?),
)),
other => Ok(ResolvedTypeVariant::TypeVariant(other)),
}
}
}
impl RecordType<InternalName> {
fn resolve_record_type_entity_or_common(
self,
all_defs: &AllDefs,
) -> std::result::Result<RecordType<InternalName>, TypeNotDefinedError> {
Ok(RecordType {
attributes: self
.attributes
.into_iter()
.map(|(k, v)| Ok((k, v.resolve_type_of_attribute_entity_or_common(all_defs)?)))
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
additional_attributes: self.additional_attributes,
})
}
}
impl TypeOfAttribute<InternalName> {
fn resolve_type_of_attribute_entity_or_common(
self,
all_defs: &AllDefs,
) -> std::result::Result<TypeOfAttribute<InternalName>, TypeNotDefinedError> {
Ok(TypeOfAttribute {
ty: self.ty.resolve_entity_or_common_type(all_defs)?,
required: self.required,
annotations: self.annotations,
#[cfg(feature = "extended-schema")]
loc: self.loc,
})
}
}
impl EntityType<InternalName> {
fn resolve_entity_type_entity_or_common(
self,
all_defs: &AllDefs,
) -> std::result::Result<EntityType<InternalName>, TypeNotDefinedError> {
Ok(EntityType {
kind: match self.kind {
EntityTypeKind::Standard(standard) => {
EntityTypeKind::Standard(StandardEntityType {
member_of_types: standard.member_of_types, shape: standard
.shape
.resolve_attributes_or_context_entity_or_common(all_defs)?,
tags: standard
.tags
.map(|tags| tags.resolve_entity_or_common_type(all_defs))
.transpose()?,
})
}
EntityTypeKind::Enum { choices } => EntityTypeKind::Enum { choices },
},
annotations: self.annotations,
loc: self.loc,
})
}
}
impl ActionType<InternalName> {
fn resolve_action_type_entity_or_common(
self,
all_defs: &AllDefs,
) -> std::result::Result<ActionType<InternalName>, TypeNotDefinedError> {
let new_apply_spec = self
.applies_to
.clone()
.map(|apply_spec| apply_spec.resolve_apply_spec_entity_or_common(all_defs))
.transpose()?;
Ok(ActionType::<InternalName> {
applies_to: new_apply_spec,
..self
})
}
}
impl ApplySpec<InternalName> {
fn resolve_apply_spec_entity_or_common(
self,
all_defs: &AllDefs,
) -> std::result::Result<ApplySpec<InternalName>, TypeNotDefinedError> {
Ok(ApplySpec {
resource_types: self.resource_types, principal_types: self.principal_types, context: self
.context
.resolve_attributes_or_context_entity_or_common(all_defs)?,
})
}
}
impl AttributesOrContext<InternalName> {
fn resolve_attributes_or_context_entity_or_common(
self,
all_defs: &AllDefs,
) -> std::result::Result<AttributesOrContext<InternalName>, TypeNotDefinedError> {
Ok(AttributesOrContext(
self.0.resolve_entity_or_common_type(all_defs)?,
))
}
}
impl<'de, N: Deserialize<'de> + From<RawName>> Deserialize<'de> for Type<N> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(TypeVisitor {
_phantom: PhantomData,
})
}
}
#[derive(Debug, Clone, 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(Debug, Deserialize)]
struct AttributesTypeMap(
#[serde(with = "serde_with::rust::maps_duplicate_key_is_error")]
BTreeMap<SmolStr, TypeOfAttribute<RawName>>,
);
struct TypeVisitor<N> {
_phantom: PhantomData<N>,
}
impl<'de, N: Deserialize<'de> + From<RawName>> Visitor<'de> for TypeVisitor<N> {
type Value = Type<N>;
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::{AdditionalAttributes, Attributes, Element, Name, Type as TypeField};
let mut type_name: Option<SmolStr> = None;
let mut element: Option<Type<N>> = None;
let mut attributes: Option<AttributesTypeMap> = None;
let mut additional_attributes: Option<bool> = None;
let mut name: Option<SmolStr> = None;
while let Some(key) = map.next_key()? {
match key {
TypeField => {
if type_name.is_some() {
return Err(serde::de::Error::duplicate_field(TypeField.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.as_ref(),
element,
attributes,
additional_attributes,
name,
)
}
}
impl<'de, N: Deserialize<'de> + From<RawName>> TypeVisitor<N> {
fn build_schema_type<M>(
type_name: Option<&SmolStr>,
element: Option<Type<N>>,
attributes: Option<AttributesTypeMap>,
additional_attributes: Option<bool>,
name: Option<SmolStr>,
) -> std::result::Result<Type<N>, M::Error>
where
M: MapAccess<'de>,
{
use TypeFields::{AdditionalAttributes, Attributes, Element, Name, Type as TypeField};
let mut remaining_fields = [
(TypeField, 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<_>>();
match type_name {
Some(s) => {
remaining_fields.remove(&TypeField);
let error_if_fields = |fs: &[TypeFields],
expected: &'static [&'static str]|
-> std::result::Result<(), M::Error> {
for f in fs {
if remaining_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 s.as_str() {
"String" => {
error_if_any_fields()?;
Ok(Type::Type {
ty: TypeVariant::String,
loc: None,
})
}
"Long" => {
error_if_any_fields()?;
Ok(Type::Type {
ty: TypeVariant::Long,
loc: None,
})
}
"Boolean" => {
error_if_any_fields()?;
Ok(Type::Type {
ty: TypeVariant::Boolean,
loc: None,
})
}
"Set" => {
error_if_fields(
&[Attributes, AdditionalAttributes, Name],
&[type_field_name!(Element)],
)?;
match element {
Some(element) => Ok(Type::Type {
ty: TypeVariant::Set {
element: Box::new(element),
},
loc: None,
}),
None => Err(serde::de::Error::missing_field(Element.as_str())),
}
}
"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_else(partial_schema_default);
Ok(Type::Type {
ty: TypeVariant::Record(RecordType {
attributes: attributes
.0
.into_iter()
.map(
|(
k,
TypeOfAttribute {
ty,
required,
annotations,
#[cfg(feature = "extended-schema")]
loc,
},
)| {
(
k,
TypeOfAttribute {
ty: ty.into_n(),
required,
annotations,
#[cfg(feature = "extended-schema")]
loc,
},
)
},
)
.collect(),
additional_attributes,
}),
loc: None,
})
} else {
Err(serde::de::Error::missing_field(Attributes.as_str()))
}
}
"Entity" => {
error_if_fields(
&[Element, Attributes, AdditionalAttributes],
&[type_field_name!(Name)],
)?;
match name {
Some(name) => Ok(Type::Type {
ty: TypeVariant::Entity {
name: RawName::from_normalized_str(&name)
.map_err(|err| {
serde::de::Error::custom(format!(
"invalid entity type `{name}`: {err}"
))
})?
.into(),
},
loc: None,
}),
None => Err(serde::de::Error::missing_field(Name.as_str())),
}
}
"EntityOrCommon" => {
error_if_fields(
&[Element, Attributes, AdditionalAttributes],
&[type_field_name!(Name)],
)?;
match name {
Some(name) => Ok(Type::Type {
ty: TypeVariant::EntityOrCommon {
type_name: RawName::from_normalized_str(&name)
.map_err(|err| {
serde::de::Error::custom(format!(
"invalid entity or common type `{name}`: {err}"
))
})?
.into(),
},
loc: None,
}),
None => Err(serde::de::Error::missing_field(Name.as_str())),
}
}
"Extension" => {
error_if_fields(
&[Element, Attributes, AdditionalAttributes],
&[type_field_name!(Name)],
)?;
match name {
Some(name) => Ok(Type::Type {
ty: TypeVariant::Extension {
name: UnreservedId::from_normalized_str(&name).map_err(
|err| {
serde::de::Error::custom(format!(
"invalid extension type `{name}`: {err}"
))
},
)?,
},
loc: None,
}),
None => Err(serde::de::Error::missing_field(Name.as_str())),
}
}
type_name => {
error_if_any_fields()?;
Ok(Type::CommonTypeRef {
type_name: N::from(RawName::from_normalized_str(type_name).map_err(
|err| {
serde::de::Error::custom(format!(
"invalid common type `{type_name}`: {err}"
))
},
)?),
loc: None,
})
}
}
}
None => Err(serde::de::Error::missing_field(TypeField.as_str())),
}
}
}
impl<N> From<TypeVariant<N>> for Type<N> {
fn from(ty: TypeVariant<N>) -> Self {
Self::Type { ty, loc: None }
}
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq, PartialOrd, Ord)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct RecordType<N> {
pub attributes: BTreeMap<SmolStr, TypeOfAttribute<N>>,
#[serde(default = "partial_schema_default")]
#[serde(skip_serializing_if = "is_partial_schema_default")]
pub additional_attributes: bool,
}
impl<N> Default for RecordType<N> {
fn default() -> Self {
Self {
attributes: BTreeMap::new(),
additional_attributes: partial_schema_default(),
}
}
}
impl<N> RecordType<N> {
pub fn is_empty_record(&self) -> bool {
self.additional_attributes == partial_schema_default() && self.attributes.is_empty()
}
}
impl RecordType<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> RecordType<ConditionalName> {
RecordType {
attributes: self
.attributes
.into_iter()
.map(|(k, v)| (k, v.conditionally_qualify_type_references(ns)))
.collect(),
additional_attributes: self.additional_attributes,
}
}
}
impl RecordType<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<RecordType<InternalName>, TypeNotDefinedError> {
Ok(RecordType {
attributes: self
.attributes
.into_iter()
.map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
.collect::<std::result::Result<_, TypeNotDefinedError>>()?,
additional_attributes: self.additional_attributes,
})
}
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq(bound(N: PartialEq)), Eq, PartialOrd, Ord(bound(N: Ord)))]
#[serde(tag = "type")]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum TypeVariant<N> {
String,
Long,
Boolean,
Set {
element: Box<Type<N>>,
},
Record(RecordType<N>),
Entity {
name: N,
},
EntityOrCommon {
#[serde(rename = "name")]
type_name: N,
},
Extension {
name: UnreservedId,
},
}
impl TypeVariant<RawName> {
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> TypeVariant<ConditionalName> {
match self {
Self::Boolean => TypeVariant::Boolean,
Self::Long => TypeVariant::Long,
Self::String => TypeVariant::String,
Self::Extension { name } => TypeVariant::Extension { name },
Self::Entity { name } => TypeVariant::Entity {
name: name.conditionally_qualify_with(ns, ReferenceType::Entity), },
Self::EntityOrCommon { type_name } => TypeVariant::EntityOrCommon {
type_name: type_name.conditionally_qualify_with(ns, ReferenceType::CommonOrEntity),
},
Self::Set { element } => TypeVariant::Set {
element: Box::new(element.conditionally_qualify_type_references(ns)),
},
Self::Record(RecordType {
attributes,
additional_attributes,
}) => TypeVariant::Record(RecordType {
attributes: BTreeMap::from_iter(attributes.into_iter().map(
|(
attr,
TypeOfAttribute {
ty,
required,
annotations,
#[cfg(feature = "extended-schema")]
loc,
},
)| {
(
attr,
TypeOfAttribute {
ty: ty.conditionally_qualify_type_references(ns),
required,
annotations,
#[cfg(feature = "extended-schema")]
loc,
},
)
},
)),
additional_attributes,
}),
}
}
fn into_n<N: From<RawName>>(self) -> TypeVariant<N> {
match self {
Self::Boolean => TypeVariant::Boolean,
Self::Long => TypeVariant::Long,
Self::String => TypeVariant::String,
Self::Entity { name } => TypeVariant::Entity { name: name.into() },
Self::EntityOrCommon { type_name } => TypeVariant::EntityOrCommon {
type_name: type_name.into(),
},
Self::Record(RecordType {
attributes,
additional_attributes,
}) => TypeVariant::Record(RecordType {
attributes: attributes
.into_iter()
.map(|(k, v)| (k, v.into_n()))
.collect(),
additional_attributes,
}),
Self::Set { element } => TypeVariant::Set {
element: Box::new(element.into_n()),
},
Self::Extension { name } => TypeVariant::Extension { name },
}
}
}
impl TypeVariant<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<TypeVariant<InternalName>, TypeNotDefinedError> {
match self {
Self::Boolean => Ok(TypeVariant::Boolean),
Self::Long => Ok(TypeVariant::Long),
Self::String => Ok(TypeVariant::String),
Self::Extension { name } => Ok(TypeVariant::Extension { name }),
Self::Entity { name } => Ok(TypeVariant::Entity {
name: name.resolve(all_defs)?,
}),
Self::EntityOrCommon { type_name } => Ok(TypeVariant::EntityOrCommon {
type_name: type_name.resolve(all_defs)?,
}),
Self::Set { element } => Ok(TypeVariant::Set {
element: Box::new(element.fully_qualify_type_references(all_defs)?),
}),
Self::Record(RecordType {
attributes,
additional_attributes,
}) => Ok(TypeVariant::Record(RecordType {
attributes: attributes
.into_iter()
.map(
|(
attr,
TypeOfAttribute {
ty,
required,
annotations,
#[cfg(feature = "extended-schema")]
loc,
},
)| {
Ok((
attr,
TypeOfAttribute {
ty: ty.fully_qualify_type_references(all_defs)?,
required,
annotations,
#[cfg(feature = "extended-schema")]
loc,
},
))
},
)
.collect::<std::result::Result<BTreeMap<_, _>, TypeNotDefinedError>>()?,
additional_attributes,
})),
}
}
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "Reference required to work with derived serde serialize implementation"
)]
fn is_partial_schema_default(b: &bool) -> bool {
*b == partial_schema_default()
}
#[cfg(feature = "arbitrary")]
#[expect(clippy::panic, reason = "property testing code")]
impl<'a> arbitrary::Arbitrary<'a> for Type<RawName> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Type<RawName>> {
use std::collections::BTreeSet;
Ok(Type::Type {
ty: match u.int_in_range::<u8>(1..=8)? {
1 => TypeVariant::String,
2 => TypeVariant::Long,
3 => TypeVariant::Boolean,
4 => TypeVariant::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::<TypeOfAttribute<RawName>>()?))
})
.collect::<arbitrary::Result<_>>()?
};
TypeVariant::Record(RecordType {
attributes,
additional_attributes: u.arbitrary()?,
})
}
6 => TypeVariant::Entity {
name: u.arbitrary()?,
},
7 => TypeVariant::Extension {
#[expect(clippy::unwrap_used, reason = "`ipaddr` is a valid `UnreservedId`")]
name: "ipaddr".parse().unwrap(),
},
8 => TypeVariant::Extension {
#[expect(clippy::unwrap_used, reason = "`decimal` is a valid `UnreservedId`")]
name: "decimal".parse().unwrap(),
},
n => panic!("bad index: {n}"),
},
loc: None,
})
}
fn size_hint(_depth: usize) -> (usize, Option<usize>) {
(1, None) }
}
#[derive(Educe, Debug, Clone, Serialize, Deserialize)]
#[educe(PartialEq, Eq, PartialOrd, Ord)]
#[serde(bound(deserialize = "N: Deserialize<'de> + From<RawName>"))]
pub struct TypeOfAttribute<N> {
#[serde(flatten)]
pub ty: Type<N>,
#[serde(default)]
#[serde(skip_serializing_if = "Annotations::is_empty")]
pub annotations: Annotations,
#[serde(default = "record_attribute_required_default")]
#[serde(skip_serializing_if = "is_record_attribute_required_default")]
pub required: bool,
#[cfg(feature = "extended-schema")]
#[educe(Eq(ignore))]
#[serde(skip)]
pub loc: Option<Loc>,
}
impl TypeOfAttribute<RawName> {
fn into_n<N: From<RawName>>(self) -> TypeOfAttribute<N> {
TypeOfAttribute {
ty: self.ty.into_n(),
required: self.required,
annotations: self.annotations,
#[cfg(feature = "extended-schema")]
loc: self.loc,
}
}
pub fn conditionally_qualify_type_references(
self,
ns: Option<&InternalName>,
) -> TypeOfAttribute<ConditionalName> {
TypeOfAttribute {
ty: self.ty.conditionally_qualify_type_references(ns),
required: self.required,
annotations: self.annotations,
#[cfg(feature = "extended-schema")]
loc: self.loc,
}
}
}
impl TypeOfAttribute<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> std::result::Result<TypeOfAttribute<InternalName>, TypeNotDefinedError> {
Ok(TypeOfAttribute {
ty: self.ty.fully_qualify_type_references(all_defs)?,
required: self.required,
annotations: self.annotations,
#[cfg(feature = "extended-schema")]
loc: self.loc,
})
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for TypeOfAttribute<RawName> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Self {
ty: u.arbitrary::<Type<RawName>>()?,
required: u.arbitrary()?,
annotations: u.arbitrary()?,
#[cfg(feature = "extended-schema")]
loc: None,
})
}
fn size_hint(depth: usize) -> (usize, Option<usize>) {
arbitrary::size_hint::and_all(&[
<Type<RawName> as arbitrary::Arbitrary>::size_hint(depth),
<bool as arbitrary::Arbitrary>::size_hint(depth),
<crate::est::Annotations as arbitrary::Arbitrary>::size_hint(depth),
])
}
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "Reference required to work with derived serde serialize implementation"
)]
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 crate::{
extensions::Extensions,
test_utils::{expect_err, ExpectedErrorMessageBuilder},
};
use cool_asserts::assert_matches;
use crate::validator::ValidatorSchema;
use super::*;
#[test]
fn test_entity_type_parser1() {
let user = r#"
{
"memberOfTypes" : ["UserGroup"]
}
"#;
assert_matches!(serde_json::from_str::<EntityType<RawName>>(user), Ok(EntityType { kind: EntityTypeKind::Standard(et), .. }) => {
assert_eq!(et.member_of_types, vec!["UserGroup".parse().unwrap()]);
assert_eq!(
et.shape,
AttributesOrContext(Type::Type {
ty: TypeVariant::Record(RecordType {
attributes: BTreeMap::new(),
additional_attributes: false
}),
loc: None
}),
);});
}
#[test]
fn test_entity_type_parser2() {
let src = r#"
{ }
"#;
assert_matches!(serde_json::from_str::<EntityType<RawName>>(src), Ok(EntityType { kind: EntityTypeKind::Standard(et), .. }) => {
assert_eq!(et.member_of_types.len(), 0);
assert_eq!(
et.shape,
AttributesOrContext(Type::Type {
ty: TypeVariant::Record(RecordType {
attributes: BTreeMap::new(),
additional_attributes: false
}),
loc: None
}),
);});
}
#[test]
fn test_action_type_parser1() {
let src = r#"
{
"appliesTo" : {
"resourceTypes": ["Album"],
"principalTypes": ["User"]
},
"memberOf": [{"id": "readWrite"}]
}
"#;
let at: ActionType<RawName> = serde_json::from_str(src).expect("Parse Error");
let spec = ApplySpec {
resource_types: vec!["Album".parse().unwrap()],
principal_types: vec!["User".parse().unwrap()],
context: AttributesOrContext::default(),
};
assert_eq!(at.applies_to, Some(spec));
assert_eq!(
at.member_of,
Some(vec![ActionEntityUID {
ty: None,
id: "readWrite".into(),
#[cfg(feature = "extended-schema")]
loc: None
}])
);
}
#[test]
fn test_action_type_parser2() {
let src = r#"
{ }
"#;
let at: ActionType<RawName> = 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<RawName> =
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: Fragment<RawName> = serde_json::from_str(src).expect("Parse Error");
let (namespace, _descriptor) = schema.0.into_iter().next().unwrap();
assert_eq!(namespace, Some("foo::foo::bar::baz".parse().unwrap()));
}
#[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<RawName> = 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<RawName> = 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<RawName> = 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<RawName> = serde_json::from_value(src).unwrap();
println!("{schema:#?}");
}
#[test]
fn schema_file_with_missing_field() {
let src = serde_json::json!(
{
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"favorite": {
"type": "Entity",
}
}
}
}
},
"actions": {}
}
});
let schema = ValidatorSchema::from_json_value(src.clone(), Extensions::all_available());
assert_matches!(schema, Err(e) => {
expect_err(
&src,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"missing field `name`"#)
.build());
});
}
#[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<RawName> = serde_json::from_value(src).unwrap();
println!("{schema:#?}");
}
#[test]
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 = ValidatorSchema::from_json_value(src, Extensions::all_available());
assert_matches!(schema, Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"unknown field `foo`, expected one of `type`, `element`, `attributes`, `additionalAttributes`, `name`"#).build()
);
});
}
#[test]
fn error_in_nested_attribute_fails_fast_top_level_attr() {
let src = serde_json::json!(
{
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"foo": {
"type": "Record",
"element": { "type": "Long" }
},
"bar": { "type": "Long" }
}
}
}
},
"actions": {}
}
}
);
let schema = ValidatorSchema::from_json_value(src, Extensions::all_available());
assert_matches!(schema, Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"unknown field `element`, expected `attributes` or `additionalAttributes`"#).build()
);
});
}
#[test]
fn error_in_nested_attribute_fails_fast_nested_attr() {
let src = serde_json::json!(
{ "": {
"entityTypes": {
"a": {
"shape": {
"type": "Record",
"attributes": {
"foo": { "type": "Entity", "name": "b" },
"baz": { "type": "Record",
"attributes": {
"z": "Boolean"
}
}
}
}
},
"b": {}
}
} }
);
let schema = ValidatorSchema::from_json_value(src, Extensions::all_available());
assert_matches!(schema, Err(e) => {
expect_err(
"",
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"invalid type: string "Boolean", expected struct TypeOfAttribute"#).build()
);
});
}
#[test]
fn missing_namespace() {
let src = r#"
{
"entityTypes": { "User": { } },
"actions": {}
}"#;
let schema = ValidatorSchema::from_json_str(src, Extensions::all_available());
assert_matches!(schema, Err(e) => {
expect_err(
src,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error(r#"unknown field `User`, expected one of `commonTypes`, `entityTypes`, `actions`, `annotations` at line 3 column 35"#)
.help("JSON formatted schema must specify a namespace. If you want to use the empty namespace, explicitly specify it with `{ \"\": {..} }`")
.build());
});
}
#[test]
fn test_to_internal_name_fragment_with_resolved_types() {
let schema_str = r#"
entity User = { "name": String };
action sendMessage appliesTo {principal: User, resource: User};
"#;
let (json_schema_fragment, _warnings) =
parse_cedar_schema_fragment(schema_str, &Extensions::all_available()).unwrap();
let result = json_schema_fragment.to_internal_name_fragment_with_resolved_types();
assert_matches!(result, Ok(resolved_fragment) => {
let json_value = serde_json::to_value(&resolved_fragment).unwrap();
let json_str = serde_json::to_string(&json_value).unwrap();
assert!(!json_str.contains("EntityOrCommon"));
assert!(json_str.contains("User"));
assert!(json_str.contains("sendMessage"));
});
}
}
#[cfg(test)]
mod strengthened_types {
use cool_asserts::assert_matches;
use super::{
ActionEntityUID, ApplySpec, EntityType, Fragment, NamespaceDefinition, RawName, Type,
};
#[track_caller] fn assert_error_matches<T: std::fmt::Debug>(result: Result<T, serde_json::Error>, msg: &str) {
assert_matches!(result, Err(err) => assert_eq!(&err.to_string(), msg));
}
#[test]
fn invalid_namespace() {
let src = serde_json::json!(
{
"\n" : {
"entityTypes": {},
"actions": {}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid namespace `\n`: unexpected end of input");
let src = serde_json::json!(
{
"1" : {
"entityTypes": {},
"actions": {}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid namespace `1`: unexpected token `1`");
let src = serde_json::json!(
{
"*1" : {
"entityTypes": {},
"actions": {}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid namespace `*1`: unexpected token `*`");
let src = serde_json::json!(
{
"::" : {
"entityTypes": {},
"actions": {}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid namespace `::`: unexpected token `::`");
let src = serde_json::json!(
{
"A::" : {
"entityTypes": {},
"actions": {}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid namespace `A::`: unexpected end of input");
}
#[test]
fn invalid_common_type() {
let src = serde_json::json!(
{
"entityTypes": {},
"actions": {},
"commonTypes": {
"" : {
"type": "String"
}
}
});
let schema: Result<NamespaceDefinition<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid id ``: unexpected end of input");
let src = serde_json::json!(
{
"entityTypes": {},
"actions": {},
"commonTypes": {
"~" : {
"type": "String"
}
}
});
let schema: Result<NamespaceDefinition<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid id `~`: invalid token");
let src = serde_json::json!(
{
"entityTypes": {},
"actions": {},
"commonTypes": {
"A::B" : {
"type": "String"
}
}
});
let schema: Result<NamespaceDefinition<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid id `A::B`: unexpected token `::`");
}
#[test]
fn invalid_entity_type() {
let src = serde_json::json!(
{
"entityTypes": {
"": {}
},
"actions": {}
});
let schema: Result<NamespaceDefinition<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid id ``: unexpected end of input");
let src = serde_json::json!(
{
"entityTypes": {
"*": {}
},
"actions": {}
});
let schema: Result<NamespaceDefinition<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid id `*`: unexpected token `*`");
let src = serde_json::json!(
{
"entityTypes": {
"A::B": {}
},
"actions": {}
});
let schema: Result<NamespaceDefinition<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid id `A::B`: unexpected token `::`");
}
#[test]
fn invalid_member_of_types() {
let src = serde_json::json!(
{
"memberOfTypes": [""]
});
let schema: Result<EntityType<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name ``: unexpected end of input");
let src = serde_json::json!(
{
"memberOfTypes": ["*"]
});
let schema: Result<EntityType<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `*`: unexpected token `*`");
let src = serde_json::json!(
{
"memberOfTypes": ["A::"]
});
let schema: Result<EntityType<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `A::`: unexpected end of input");
let src = serde_json::json!(
{
"memberOfTypes": ["::A"]
});
let schema: Result<EntityType<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `::A`: unexpected token `::`");
}
#[test]
fn invalid_apply_spec() {
let src = serde_json::json!(
{
"resourceTypes": [""]
});
let schema: Result<ApplySpec<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name ``: unexpected end of input");
let src = serde_json::json!(
{
"resourceTypes": ["*"]
});
let schema: Result<ApplySpec<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `*`: unexpected token `*`");
let src = serde_json::json!(
{
"resourceTypes": ["A::"]
});
let schema: Result<ApplySpec<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `A::`: unexpected end of input");
let src = serde_json::json!(
{
"resourceTypes": ["::A"]
});
let schema: Result<ApplySpec<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `::A`: unexpected token `::`");
}
#[test]
fn invalid_schema_entity_types() {
let src = serde_json::json!(
{
"type": "Entity",
"name": ""
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid entity type ``: unexpected end of input");
let src = serde_json::json!(
{
"type": "Entity",
"name": "*"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid entity type `*`: unexpected token `*`");
let src = serde_json::json!(
{
"type": "Entity",
"name": "::A"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid entity type `::A`: unexpected token `::`");
let src = serde_json::json!(
{
"type": "Entity",
"name": "A::"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid entity type `A::`: unexpected end of input");
}
#[test]
fn invalid_action_euid() {
let src = serde_json::json!(
{
"id": "action",
"type": ""
});
let schema: Result<ActionEntityUID<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name ``: unexpected end of input");
let src = serde_json::json!(
{
"id": "action",
"type": "*"
});
let schema: Result<ActionEntityUID<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `*`: unexpected token `*`");
let src = serde_json::json!(
{
"id": "action",
"type": "Action::"
});
let schema: Result<ActionEntityUID<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `Action::`: unexpected end of input");
let src = serde_json::json!(
{
"id": "action",
"type": "::Action"
});
let schema: Result<ActionEntityUID<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid name `::Action`: unexpected token `::`");
}
#[test]
fn invalid_schema_common_types() {
let src = serde_json::json!(
{
"type": ""
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid common type ``: unexpected end of input");
let src = serde_json::json!(
{
"type": "*"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid common type `*`: unexpected token `*`");
let src = serde_json::json!(
{
"type": "::A"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid common type `::A`: unexpected token `::`");
let src = serde_json::json!(
{
"type": "A::"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid common type `A::`: unexpected end of input");
}
#[test]
fn invalid_schema_extension_types() {
let src = serde_json::json!(
{
"type": "Extension",
"name": ""
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid extension type ``: unexpected end of input");
let src = serde_json::json!(
{
"type": "Extension",
"name": "*"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(schema, "invalid extension type `*`: unexpected token `*`");
let src = serde_json::json!(
{
"type": "Extension",
"name": "__cedar::decimal"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(
schema,
"invalid extension type `__cedar::decimal`: unexpected token `::`",
);
let src = serde_json::json!(
{
"type": "Extension",
"name": "__cedar::"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(
schema,
"invalid extension type `__cedar::`: unexpected token `::`",
);
let src = serde_json::json!(
{
"type": "Extension",
"name": "::__cedar"
});
let schema: Result<Type<RawName>, _> = serde_json::from_value(src);
assert_error_matches(
schema,
"invalid extension type `::__cedar`: unexpected token `::`",
);
}
}
#[cfg(test)]
mod entity_tags {
use super::*;
use crate::test_utils::{expect_err, ExpectedErrorMessageBuilder};
use cool_asserts::assert_matches;
use serde_json::json;
#[track_caller]
fn example_json_schema() -> serde_json::Value {
json!({"": {
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"jobLevel" : {
"type" : "Long"
},
}
},
"tags" : {
"type" : "Set",
"element": { "type": "String" }
}
},
"Document" : {
"shape" : {
"type" : "Record",
"attributes" : {
"owner" : {
"type" : "Entity",
"name" : "User"
},
}
},
"tags" : {
"type" : "Set",
"element": { "type": "String" }
}
}
},
"actions": {}
}})
}
#[test]
fn roundtrip() {
let json = example_json_schema();
let json_schema = Fragment::from_json_value(json.clone()).expect("should be valid");
let serialized_json_schema = serde_json::to_value(json_schema).expect("should be valid");
assert_eq!(json, serialized_json_schema);
}
#[test]
fn basic() {
let json = example_json_schema();
assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
assert_matches!(frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(), EntityType { kind: EntityTypeKind::Standard(user), ..} => {
assert_matches!(&user.tags, Some(Type::Type { ty: TypeVariant::Set { element }, ..}) => {
assert_matches!(&**element, Type::Type{ ty: TypeVariant::String, ..}); });});
assert_matches!(frag.0.get(&None).unwrap().entity_types.get(&"Document".parse().unwrap()).unwrap(), EntityType { kind: EntityTypeKind::Standard(doc), ..} => {
assert_matches!(&doc.tags, Some(Type::Type { ty: TypeVariant::Set { element }, ..}) => {
assert_matches!(&**element, Type::Type{ ty: TypeVariant::String, ..}); });
})})
}
#[test]
fn tag_type_is_common_type() {
let json = json!({"": {
"commonTypes": {
"T": { "type": "String" },
},
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"jobLevel" : {
"type" : "Long"
},
}
},
"tags" : { "type" : "T" },
},
},
"actions": {}
}});
assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
assert_matches!(frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(), EntityType {kind: EntityTypeKind::Standard(user), ..} => {
assert_matches!(&user.tags, Some(Type::CommonTypeRef { type_name, .. }) => {
assert_eq!(&format!("{type_name}"), "T");
});
})});
}
#[test]
fn tag_type_is_entity_type() {
let json = json!({"": {
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"jobLevel" : {
"type" : "Long"
},
}
},
"tags" : { "type" : "Entity", "name": "User" },
},
},
"actions": {}
}});
assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
assert_matches!(frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(), EntityType { kind: EntityTypeKind::Standard(user), ..} => {
assert_matches!(&user.tags, Some(Type::Type{ ty: TypeVariant::Entity{ name }, ..}) => {
assert_eq!(&format!("{name}"), "User");
});
})});
}
#[test]
fn bad_tags() {
let json = json!({"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"jobLevel": {
"type": "Long"
},
},
"tags": { "type": "String" },
}
},
},
"actions": {}
}});
assert_matches!(Fragment::from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("unknown field `tags`, expected one of `type`, `element`, `attributes`, `additionalAttributes`, `name`")
.build(),
);
});
}
}
#[cfg(test)]
mod test_json_roundtrip {
use super::*;
#[track_caller] fn roundtrip(schema: &Fragment<RawName>) {
let json = serde_json::to_value(schema.clone()).unwrap();
let new_schema: Fragment<RawName> = serde_json::from_value(json).unwrap();
assert_eq!(schema, &new_schema);
}
#[test]
fn empty_namespace() {
let fragment = Fragment(BTreeMap::from([(None, NamespaceDefinition::new([], []))]));
roundtrip(&fragment);
}
#[test]
fn nonempty_namespace() {
let fragment = Fragment(BTreeMap::from([(
Some("a".parse().unwrap()),
NamespaceDefinition::new([], []),
)]));
roundtrip(&fragment);
}
#[test]
fn nonempty_entity_types() {
let fragment = Fragment(BTreeMap::from([(
None,
NamespaceDefinition::new(
[(
"a".parse().unwrap(),
EntityType {
kind: EntityTypeKind::Standard(StandardEntityType {
member_of_types: vec!["a".parse().unwrap()],
shape: AttributesOrContext(Type::Type {
ty: TypeVariant::Record(RecordType {
attributes: BTreeMap::new(),
additional_attributes: false,
}),
loc: None,
}),
tags: None,
}),
annotations: Annotations::new(),
loc: None,
},
)],
[(
"action".into(),
ActionType {
attributes: None,
applies_to: Some(ApplySpec {
resource_types: vec!["a".parse().unwrap()],
principal_types: vec!["a".parse().unwrap()],
context: AttributesOrContext(Type::Type {
ty: TypeVariant::Record(RecordType {
attributes: BTreeMap::new(),
additional_attributes: false,
}),
loc: None,
}),
}),
member_of: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
),
)]));
roundtrip(&fragment);
}
#[test]
fn multiple_namespaces() {
let fragment = Fragment(BTreeMap::from([
(
Some("foo".parse().unwrap()),
NamespaceDefinition::new(
[(
"a".parse().unwrap(),
EntityType {
kind: EntityTypeKind::Standard(StandardEntityType {
member_of_types: vec!["a".parse().unwrap()],
shape: AttributesOrContext(Type::Type {
ty: TypeVariant::Record(RecordType {
attributes: BTreeMap::new(),
additional_attributes: false,
}),
loc: None,
}),
tags: None,
}),
annotations: Annotations::new(),
loc: None,
},
)],
[],
),
),
(
None,
NamespaceDefinition::new(
[],
[(
"action".into(),
ActionType {
attributes: None,
applies_to: Some(ApplySpec {
resource_types: vec!["foo::a".parse().unwrap()],
principal_types: vec!["foo::a".parse().unwrap()],
context: AttributesOrContext(Type::Type {
ty: TypeVariant::Record(RecordType {
attributes: BTreeMap::new(),
additional_attributes: false,
}),
loc: None,
}),
}),
member_of: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
),
),
]));
roundtrip(&fragment);
}
}
#[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": {}
}
}"#;
Fragment::from_json_str(src).unwrap();
}
#[test]
#[should_panic(expected = "invalid entry: found duplicate key")]
fn entity_type() {
let src = r#"{
"Foo": {
"entityTypes" : {
"Bar": {},
"Bar": {}
},
"actions": {}
}
}"#;
Fragment::from_json_str(src).unwrap();
}
#[test]
#[should_panic(expected = "invalid entry: found duplicate key")]
fn action() {
let src = r#"{
"Foo": {
"entityTypes" : {},
"actions": {
"Bar": {},
"Bar": {}
}
}
}"#;
Fragment::from_json_str(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"}
}
}
}"#;
Fragment::from_json_str(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": { }
}
}"#;
Fragment::from_json_str(src).unwrap();
}
#[test]
#[should_panic(expected = "missing field `resourceTypes`")]
fn missing_resource() {
let src = r#"{
"Foo": {
"entityTypes" : {},
"actions": {
"foo" : {
"appliesTo" : {
"principalTypes" : ["a"]
}
}
}
}
}"#;
Fragment::from_json_str(src).unwrap();
}
#[test]
#[should_panic(expected = "missing field `principalTypes`")]
fn missing_principal() {
let src = r#"{
"Foo": {
"entityTypes" : {},
"actions": {
"foo" : {
"appliesTo" : {
"resourceTypes" : ["a"]
}
}
}
}
}"#;
Fragment::from_json_str(src).unwrap();
}
#[test]
#[should_panic(expected = "missing field `resourceTypes`")]
fn missing_both() {
let src = r#"{
"Foo": {
"entityTypes" : {},
"actions": {
"foo" : {
"appliesTo" : {
}
}
}
}
}"#;
Fragment::from_json_str(src).unwrap();
}
}
#[cfg(test)]
mod annotations {
use crate::validator::RawName;
use cool_asserts::assert_matches;
use super::Fragment;
#[test]
fn empty_namespace() {
let src = serde_json::json!(
{
"" : {
"entityTypes": {},
"actions": {},
"annotations": {
"doc": "this is a doc"
}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Err(err) => {
assert_eq!(&err.to_string(), "annotations are not allowed on the empty namespace");
});
}
#[test]
fn basic() {
let src = serde_json::json!(
{
"N" : {
"entityTypes": {},
"actions": {},
"annotations": {
"doc": "this is a doc"
}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Ok(_));
let src = serde_json::json!(
{
"N" : {
"entityTypes": {
"a": {
"annotations": {
"a": "",
"d": null,
"b": "c",
},
"shape": {
"type": "Long",
}
}
},
"actions": {},
"annotations": {
"doc": "this is a doc"
}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Ok(_));
let src = serde_json::json!(
{
"N" : {
"entityTypes": {
"a": {
"annotations": {
"a": "",
"b": "c",
},
"shape": {
"type": "Long",
}
}
},
"actions": {
"a": {
"annotations": {
"doc": "this is a doc"
},
"appliesTo": {
"principalTypes": ["A"],
"resourceTypes": ["B"],
}
},
},
"annotations": {
"doc": "this is a doc"
}
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Ok(_));
let src = serde_json::json!({
"N": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"Task": {
"annotations": {
"doc": "a common type representing a task"
},
"type": "Record",
"attributes": {
"id": {
"type": "Long",
"annotations": {
"doc": "task id"
}
},
"name": {
"type": "String"
},
"state": {
"type": "String"
}
}
}}}});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Ok(_));
let src = serde_json::json!({
"N": {
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"name" : {
"annotations": {
"a": null,
},
"type" : "String"
},
"age" : {
"type" : "Long"
}
}
}
}
},
"actions": {},
"commonTypes": {}
}});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Ok(_));
let src = serde_json::json!({
"N": {
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"name" : {
"annotations": {
"first_layer": "b"
},
"type" : "Record",
"attributes": {
"a": {
"type": "Record",
"annotations": {
"second_layer": "d"
},
"attributes": {
"...": {
"annotations": {
"last_layer": null,
},
"type": "Long"
}
}
}
}
},
"age" : {
"type" : "Long"
}
}
}
}
},
"actions": {},
"commonTypes": {}
}});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Ok(_));
}
#[track_caller]
fn test_unknown_fields(src: serde_json::Value, field: &str, expected: &str) {
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Err(errs) => {
assert_eq!(errs.to_string(), format!("unknown field {field}, expected one of {expected}"));
});
}
const ENTITY_TYPE_EXPECTED_ATTRIBUTES: &str =
"`memberOfTypes`, `shape`, `tags`, `enum`, `annotations`";
const NAMESPACE_EXPECTED_ATTRIBUTES: &str =
"`commonTypes`, `entityTypes`, `actions`, `annotations`";
const ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES: &str =
"`type`, `element`, `attributes`, `additionalAttributes`, `name`";
const APPLIES_TO_EXPECTED_ATTRIBUTES: &str = "`resourceTypes`, `principalTypes`, `context`";
#[test]
fn unknown_fields() {
let src = serde_json::json!(
{
"N": {
"entityTypes": {
"UserGroup": {
"shape44": {
"type": "Record",
"attributes": {}
},
"memberOfTypes": [
"UserGroup"
]
}},
"actions": {},
}});
test_unknown_fields(src, "`shape44`", ENTITY_TYPE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"C": {
"type": "Set",
"element": {
"annotations": {
"doc": "this is a doc"
},
"type": "Long"
}
}
}}});
test_unknown_fields(src, "`annotations`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"C": {
"type": "Long",
"foo": 1,
"annotations": {
"doc": "this is a doc"
},
}}}});
test_unknown_fields(src, "`foo`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"C": {
"type": "Record",
"attributes": {
"a": {
"annotations": {
"doc": "this is a doc"
},
"type": "Long",
"foo": 2,
"required": true,
}
},
}}}});
test_unknown_fields(src, "`foo`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"C": {
"type": "Record",
"attributes": {
"a": {
"annotations": {
"doc": "this is a doc"
},
"type": "Record",
"attributes": {
"b": {
"annotations": {
"doc": "this is a doc"
},
"type": "Long",
"bar": 3,
},
},
"required": true,
}
},
}}}});
test_unknown_fields(src, "`bar`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N": {
"entityTypes": {
"UserGroup": {
"shape": {
"annotations": {
"doc": "this is a doc"
},
"type": "Record",
"attributes": {}
},
"memberOfTypes": [
"UserGroup"
]
}},
"actions": {},
}});
test_unknown_fields(src, "`annotations`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N": {
"entityTypes": {},
"actions": {
"a": {
"appliesTo": {
"annotations": {
"doc": "this is a doc"
},
"principalTypes": ["A"],
"resourceTypes": ["B"],
}
},
},
}});
test_unknown_fields(src, "`annotations`", APPLIES_TO_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N" : {
"entityTypes": {},
"actions": {},
"foo": "",
"annotations": {
"doc": "this is a doc"
}
}
});
test_unknown_fields(src, "`foo`", NAMESPACE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"" : {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a": {
"type": "Long",
"annotations": {
"foo": ""
},
"bar": 1,
}
}
}
});
test_unknown_fields(src, "`bar`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES);
let src = serde_json::json!(
{
"N" : {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a": {
"type": "Record",
"annotations": {
"foo": ""
},
"attributes": {
"a": {
"bar": 1,
"type": "Long"
}
}
}
}
}
});
test_unknown_fields(src, "`bar`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES);
}
}
#[cfg(test)]
#[expect(clippy::collection_is_never_read, reason = "testing code")]
mod ord {
use super::{InternalName, RawName, Type, TypeVariant};
use std::collections::BTreeSet;
#[test]
fn type_ord() {
let mut set: BTreeSet<Type<RawName>> = BTreeSet::default();
set.insert(Type::Type {
ty: TypeVariant::String,
loc: None,
});
let mut set: BTreeSet<Type<InternalName>> = BTreeSet::default();
set.insert(Type::Type {
ty: TypeVariant::String,
loc: None,
});
}
}
#[cfg(test)]
#[expect(clippy::indexing_slicing, reason = "tests")]
mod enumerated_entity_types {
use cool_asserts::assert_matches;
use crate::validator::{
json_schema::{EntityType, EntityTypeKind, Fragment},
RawName,
};
#[test]
fn basic() {
let src = serde_json::json!({
"": {
"entityTypes": {
"Foo": {
"enum": ["foo", "bar"],
"annotations": {
"a": "b",
}
},
},
"actions": {},
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Ok(frag) => {
assert_matches!(&frag.0[&None].entity_types[&"Foo".parse().unwrap()], EntityType {
kind: EntityTypeKind::Enum {choices},
..
} => {
assert_eq!(Vec::from(choices.clone()), ["foo", "bar"]);
});
});
let src = serde_json::json!({
"": {
"entityTypes": {
"Foo": {
"enum": [],
"annotations": {
"a": "b",
}
},
},
"actions": {},
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Err(errs) => {
assert_eq!(errs.to_string(), "the vector provided was empty, NonEmpty needs at least one element");
});
let src = serde_json::json!({
"": {
"entityTypes": {
"Foo": {
"enum": null,
},
},
"actions": {},
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Err(errs) => {
assert_eq!(errs.to_string(), "invalid type: null, expected a sequence");
});
let src = serde_json::json!({
"": {
"entityTypes": {
"Foo": {
"enum": ["foo"],
"memberOfTypes": ["bar"],
},
},
"actions": {},
}
});
let schema: Result<Fragment<RawName>, _> = serde_json::from_value(src);
assert_matches!(schema, Err(errs) => {
assert_eq!(errs.to_string(), "unexpected field: memberOfTypes");
});
}
}