cedar_policy_validator/schema/
namespace_def.rs

1//! This module contains the definition of `ValidatorNamespaceDef` and of types
2//! it relies on
3
4use std::collections::{HashMap, HashSet};
5
6use cedar_policy_core::{
7    ast::{
8        Eid, EntityAttrEvaluationError, EntityType, EntityUID, Id, Name,
9        PartialValueSerializedAsExpr,
10    },
11    entities::{CedarValueJson, JsonDeserializationErrorContext},
12    evaluator::RestrictedEvaluator,
13    extensions::Extensions,
14    parser::err::ParseErrors,
15    FromNormalizedStr,
16};
17use smol_str::SmolStr;
18
19use super::ValidatorApplySpec;
20use crate::types::OpenTag;
21use crate::{
22    err::*,
23    schema_file_format,
24    types::{AttributeType, Attributes, Type},
25    ActionBehavior, ActionEntityUID, ActionType, NamespaceDefinition, SchemaType,
26    SchemaTypeVariant, TypeOfAttribute, SCHEMA_TYPE_VARIANT_TAGS,
27};
28
29/// The current schema format specification does not include multiple action entity
30/// types. All action entities are required to use a single `Action` entity
31/// type. However, the action entity type may be namespaced, so an action entity
32/// may have a fully qualified entity type `My::Namespace::Action`.
33/// This string must be parsable by as an entity type name.
34pub(crate) static ACTION_ENTITY_TYPE: &str = "Action";
35
36#[test]
37fn action_entity_type_parses() {
38    Id::from_normalized_str(ACTION_ENTITY_TYPE).unwrap();
39}
40
41/// Return true when an entity type is an action entity type. This compares the
42/// base name for the type, so this will return true for any entity type named
43/// `Action` regardless of namespaces.
44pub(crate) fn is_action_entity_type(ty: &Name) -> bool {
45    ty.basename().as_ref() == ACTION_ENTITY_TYPE
46}
47
48/// A single namespace definition from the schema json processed into a form
49/// which is closer to that used by the validator. The processing includes
50/// detection of some errors, for example, parse errors in entity type names or
51/// entity type which are declared multiple times. This does not detect
52/// references to undeclared entity types because any entity type may be
53/// declared in a different fragment that will only be known about when building
54/// the complete `ValidatorSchema`.
55#[derive(Debug)]
56pub struct ValidatorNamespaceDef {
57    /// The namespace declared for the schema fragment. We track a namespace for
58    /// fragments because they have at most one namespace that is applied
59    /// everywhere. It would be less useful to track all namespaces for a
60    /// complete schema.
61    namespace: Option<Name>,
62    /// Preprocessed common type definitions which can be used to define entity
63    /// type attributes and action contexts.
64    pub(super) type_defs: TypeDefs,
65    /// The preprocessed entity type declarations from the schema fragment json.
66    pub(super) entity_types: EntityTypesDef,
67    /// The preprocessed action declarations from the schema fragment json.
68    pub(super) actions: ActionsDef,
69}
70
71/// Holds a map from `Name`s of common type definitions to their corresponding
72/// `Type`.
73#[derive(Debug)]
74pub struct TypeDefs {
75    pub(super) type_defs: HashMap<Name, Type>,
76}
77
78/// Entity type declarations held in a `ValidatorNamespaceDef`. Entity type
79/// parents and attributes may reference undeclared entity types.
80#[derive(Debug)]
81pub struct EntityTypesDef {
82    pub(super) entity_types: HashMap<Name, EntityTypeFragment>,
83}
84
85/// Defines an EntityType where we have not resolved typedefs occurring in the
86/// attributes or verified that the parent entity types and entity types
87/// occurring in attributes are defined.
88#[derive(Debug)]
89pub struct EntityTypeFragment {
90    /// The attributes record type for this entity type.  The type is wrapped in
91    /// a `WithUnresolvedTypeDefs` because it may contain typedefs which are not
92    /// defined in this schema fragment. All entity type `Name` keys in this map
93    /// are declared in this schema fragment.
94    pub(super) attributes: WithUnresolvedTypeDefs<Type>,
95    /// The direct parent entity types for this entity type come from the
96    /// `memberOfTypes` list. These types might be declared in a different
97    /// namespace, so we will check if they are declared in any fragment when
98    /// constructing a `ValidatorSchema`.
99    pub(super) parents: HashSet<Name>,
100}
101
102/// Action declarations held in a `ValidatorNamespaceDef`. Entity types
103/// referenced here do not need to be declared in the schema.
104#[derive(Debug)]
105pub struct ActionsDef {
106    pub(super) actions: HashMap<EntityUID, ActionFragment>,
107}
108
109#[derive(Debug)]
110pub struct ActionFragment {
111    /// The type of the context record for this actions. The types is wrapped in
112    /// a `WithUnresolvedTypeDefs` because it may refer to common types which
113    /// are not defined in this fragment.
114    pub(super) context: WithUnresolvedTypeDefs<Type>,
115    /// The principals and resources that an action can be applied to.
116    pub(super) applies_to: ValidatorApplySpec,
117    /// The direct parent action entities for this action.
118    pub(super) parents: HashSet<EntityUID>,
119    /// The types for the attributes defined for this actions entity.
120    pub(super) attribute_types: Attributes,
121    /// The values for the attributes defined for this actions entity, stored
122    /// separately so that we can later extract use these values to construct
123    /// the actual `Entity` objects defined by the schema.
124    pub(super) attributes: HashMap<SmolStr, PartialValueSerializedAsExpr>,
125}
126
127type ResolveFunc<T> = dyn FnOnce(&HashMap<Name, Type>) -> Result<T>;
128/// Represent a type that might be defined in terms of some type definitions
129/// which are not necessarily available in the current namespace.
130pub enum WithUnresolvedTypeDefs<T> {
131    WithUnresolved(Box<ResolveFunc<T>>),
132    WithoutUnresolved(T),
133}
134
135impl<T: 'static> WithUnresolvedTypeDefs<T> {
136    pub fn new(f: impl FnOnce(&HashMap<Name, Type>) -> Result<T> + 'static) -> Self {
137        Self::WithUnresolved(Box::new(f))
138    }
139
140    pub fn map<U: 'static>(self, f: impl FnOnce(T) -> U + 'static) -> WithUnresolvedTypeDefs<U> {
141        match self {
142            Self::WithUnresolved(_) => {
143                WithUnresolvedTypeDefs::new(|type_defs| self.resolve_type_defs(type_defs).map(f))
144            }
145            Self::WithoutUnresolved(v) => WithUnresolvedTypeDefs::WithoutUnresolved(f(v)),
146        }
147    }
148
149    /// Instantiate any names referencing types with the definition of the type
150    /// from the input HashMap.
151    pub fn resolve_type_defs(self, type_defs: &HashMap<Name, Type>) -> Result<T> {
152        match self {
153            WithUnresolvedTypeDefs::WithUnresolved(f) => f(type_defs),
154            WithUnresolvedTypeDefs::WithoutUnresolved(v) => Ok(v),
155        }
156    }
157}
158
159impl<T: 'static> From<T> for WithUnresolvedTypeDefs<T> {
160    fn from(value: T) -> Self {
161        Self::WithoutUnresolved(value)
162    }
163}
164
165impl<T: std::fmt::Debug> std::fmt::Debug for WithUnresolvedTypeDefs<T> {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        match self {
168            WithUnresolvedTypeDefs::WithUnresolved(_) => f.debug_tuple("WithUnresolved").finish(),
169            WithUnresolvedTypeDefs::WithoutUnresolved(v) => {
170                f.debug_tuple("WithoutUnresolved").field(v).finish()
171            }
172        }
173    }
174}
175
176impl TryInto<ValidatorNamespaceDef> for NamespaceDefinition {
177    type Error = SchemaError;
178
179    fn try_into(self) -> Result<ValidatorNamespaceDef> {
180        ValidatorNamespaceDef::from_namespace_definition(
181            None,
182            self,
183            ActionBehavior::default(),
184            Extensions::all_available(),
185        )
186    }
187}
188
189impl ValidatorNamespaceDef {
190    // We need to treat this as if it had `pub(crate)` visibility to avoid sharing
191    // the file format. However, our fuzzing library currently needs it to be public.
192    /// Construct a new `ValidatorSchema` from the underlying `SchemaFragment`.
193    pub fn from_namespace_definition(
194        namespace: Option<SmolStr>,
195        namespace_def: NamespaceDefinition,
196        action_behavior: ActionBehavior,
197        extensions: Extensions<'_>,
198    ) -> Result<ValidatorNamespaceDef> {
199        // Check that each entity types and action is only declared once.
200        let mut e_types_ids: HashSet<SmolStr> = HashSet::new();
201        for name in namespace_def.entity_types.keys() {
202            if !e_types_ids.insert(name.clone()) {
203                // insert returns false for duplicates
204                return Err(SchemaError::DuplicateEntityType(name.to_string()));
205            }
206        }
207        let mut a_name_eids: HashSet<SmolStr> = HashSet::new();
208        for name in namespace_def.actions.keys() {
209            if !a_name_eids.insert(name.clone()) {
210                // insert returns false for duplicates
211                return Err(SchemaError::DuplicateAction(name.to_string()));
212            }
213        }
214
215        let schema_namespace = match namespace.as_deref() {
216            None => None,
217            Some("") => None, // we consider "" to be the same as the empty namespace for this purpose
218            Some(ns) => Some(Name::from_normalized_str(ns).map_err(SchemaError::ParseNamespace)?),
219        };
220
221        // Return early with an error if actions cannot be in groups or have
222        // attributes, but the schema contains action groups or attributes.
223        Self::check_action_behavior(&namespace_def, action_behavior)?;
224
225        // Convert the type defs, actions and entity types from the schema file
226        // into the representation used by the validator.
227        let type_defs =
228            Self::build_type_defs(namespace_def.common_types, schema_namespace.as_ref())?;
229        let actions =
230            Self::build_action_ids(namespace_def.actions, schema_namespace.as_ref(), extensions)?;
231        let entity_types =
232            Self::build_entity_types(namespace_def.entity_types, schema_namespace.as_ref())?;
233
234        Ok(ValidatorNamespaceDef {
235            namespace: schema_namespace,
236            type_defs,
237            entity_types,
238            actions,
239        })
240    }
241
242    fn is_builtin_type_name(name: &SmolStr) -> bool {
243        SCHEMA_TYPE_VARIANT_TAGS
244            .iter()
245            .any(|type_name| name == type_name)
246    }
247
248    fn build_type_defs(
249        schema_file_type_def: HashMap<SmolStr, SchemaType>,
250        schema_namespace: Option<&Name>,
251    ) -> Result<TypeDefs> {
252        let type_defs = schema_file_type_def
253            .into_iter()
254            .map(|(name_str, schema_ty)| -> Result<_> {
255                if Self::is_builtin_type_name(&name_str) {
256                    return Err(SchemaError::DuplicateCommonType(name_str.to_string()));
257                }
258                let name = Self::parse_unqualified_name_with_namespace(
259                    &name_str,
260                    schema_namespace.cloned(),
261                )
262                .map_err(SchemaError::ParseCommonType)?;
263                let ty = Self::try_schema_type_into_validator_type(schema_namespace, schema_ty)?
264                    .resolve_type_defs(&HashMap::new())?;
265                Ok((name, ty))
266            })
267            .collect::<Result<HashMap<_, _>>>()?;
268        Ok(TypeDefs { type_defs })
269    }
270
271    // Transform the schema data structures for entity types into the structures
272    // used internally by the validator. This is mostly accomplished by directly
273    // copying data between fields.
274    fn build_entity_types(
275        schema_files_types: HashMap<SmolStr, schema_file_format::EntityType>,
276        schema_namespace: Option<&Name>,
277    ) -> Result<EntityTypesDef> {
278        Ok(EntityTypesDef {
279            entity_types: schema_files_types
280                .into_iter()
281                .map(|(name_str, entity_type)| -> Result<_> {
282                    let name = Self::parse_unqualified_name_with_namespace(
283                        &name_str,
284                        schema_namespace.cloned(),
285                    )
286                    .map_err(SchemaError::ParseEntityType)?;
287
288                    let parents = entity_type
289                        .member_of_types
290                        .iter()
291                        .map(|parent| -> Result<_> {
292                            Self::parse_possibly_qualified_name_with_default_namespace(
293                                parent,
294                                schema_namespace,
295                            )
296                            .map_err(SchemaError::ParseEntityType)
297                        })
298                        .collect::<Result<HashSet<_>>>()?;
299
300                    let attributes = Self::try_schema_type_into_validator_type(
301                        schema_namespace,
302                        entity_type.shape.into_inner(),
303                    )?;
304
305                    Ok((
306                        name,
307                        EntityTypeFragment {
308                            attributes,
309                            parents,
310                        },
311                    ))
312                })
313                .collect::<Result<HashMap<_, _>>>()?,
314        })
315    }
316
317    // Helper to get types from `CedarValueJson`s. Currently doesn't support all
318    // `CedarValueJson` types. Note: If this function is extended to cover move
319    // `CedarValueJson`s, we must update `convert_attr_jsonval_map_to_attributes` to
320    // handle errors that may occur when parsing these values. This will require
321    // a breaking change in the `SchemaError` type in the public API.
322    fn jsonval_to_type_helper(v: &CedarValueJson, action_id: &EntityUID) -> Result<Type> {
323        match v {
324            CedarValueJson::Bool(_) => Ok(Type::primitive_boolean()),
325            CedarValueJson::Long(_) => Ok(Type::primitive_long()),
326            CedarValueJson::String(_) => Ok(Type::primitive_string()),
327            CedarValueJson::Record(r) => {
328                let mut required_attrs: HashMap<SmolStr, Type> = HashMap::new();
329                for (k, v_prime) in r {
330                    let t = Self::jsonval_to_type_helper(v_prime, action_id);
331                    match t {
332                        Ok(ty) => required_attrs.insert(k.clone(), ty),
333                        Err(e) => return Err(e),
334                    };
335                }
336                Ok(Type::record_with_required_attributes(
337                    required_attrs,
338                    OpenTag::ClosedAttributes,
339                ))
340            }
341            CedarValueJson::Set(v) => match v.get(0) {
342                //sets with elements of different types will be rejected elsewhere
343                None => Err(SchemaError::ActionAttributesContainEmptySet(
344                    action_id.clone(),
345                )),
346                Some(element) => {
347                    let element_type = Self::jsonval_to_type_helper(element, action_id);
348                    match element_type {
349                        Ok(t) => Ok(Type::Set {
350                            element_type: Some(Box::new(t)),
351                        }),
352                        Err(_) => element_type,
353                    }
354                }
355            },
356            CedarValueJson::EntityEscape { __entity: _ } => {
357                Err(SchemaError::UnsupportedActionAttribute(
358                    action_id.clone(),
359                    "entity escape (`__entity`)".to_owned(),
360                ))
361            }
362            CedarValueJson::ExprEscape { __expr: _ } => {
363                Err(SchemaError::UnsupportedActionAttribute(
364                    action_id.clone(),
365                    "expression escape (`__expr`)".to_owned(),
366                ))
367            }
368            CedarValueJson::ExtnEscape { __extn: _ } => {
369                Err(SchemaError::UnsupportedActionAttribute(
370                    action_id.clone(),
371                    "extension function escape (`__extn`)".to_owned(),
372                ))
373            }
374        }
375    }
376
377    //Convert jsonval map to attributes
378    fn convert_attr_jsonval_map_to_attributes(
379        m: HashMap<SmolStr, CedarValueJson>,
380        action_id: &EntityUID,
381        extensions: Extensions<'_>,
382    ) -> Result<(Attributes, HashMap<SmolStr, PartialValueSerializedAsExpr>)> {
383        let mut attr_types: HashMap<SmolStr, Type> = HashMap::new();
384        let mut attr_values: HashMap<SmolStr, PartialValueSerializedAsExpr> = HashMap::new();
385        let evaluator = RestrictedEvaluator::new(&extensions);
386
387        for (k, v) in m {
388            let t = Self::jsonval_to_type_helper(&v, action_id);
389            match t {
390                Ok(ty) => attr_types.insert(k.clone(), ty),
391                Err(e) => return Err(e),
392            };
393
394            // As an artifact of the limited `CedarValueJson` variants accepted by
395            // `Self::jsonval_to_type_helper`, we know that this function will
396            // never error. Also note that this is only ever executed when
397            // action attributes are enabled, but they cannot be enabled when
398            // using Cedar through the public API. This is fortunate because
399            // handling an error here would mean adding a new error variant to
400            // `SchemaError` in the public API, but we didn't make that enum
401            // `non_exhaustive`, so any new variants are a breaking change.
402            // PANIC SAFETY: see above
403            #[allow(clippy::expect_used)]
404            let e = v.into_expr(|| JsonDeserializationErrorContext::EntityAttribute { uid: action_id.clone(), attr: k.clone() }).expect("`Self::jsonval_to_type_helper` will always return `Err` for a `CedarValueJson` that might make `into_expr` return `Err`");
405            let pv = evaluator
406                .partial_interpret(e.as_borrowed())
407                .map_err(|err| {
408                    SchemaError::ActionAttrEval(EntityAttrEvaluationError {
409                        uid: action_id.clone(),
410                        attr: k.clone(),
411                        err,
412                    })
413                })?;
414            attr_values.insert(k.clone(), pv.into());
415        }
416        Ok((
417            Attributes::with_required_attributes(attr_types),
418            attr_values,
419        ))
420    }
421
422    // Transform the schema data structures for actions into the structures used
423    // internally by the validator. This is mostly accomplished by directly
424    // copying data between fields.
425    fn build_action_ids(
426        schema_file_actions: HashMap<SmolStr, ActionType>,
427        schema_namespace: Option<&Name>,
428        extensions: Extensions<'_>,
429    ) -> Result<ActionsDef> {
430        Ok(ActionsDef {
431            actions: schema_file_actions
432                .into_iter()
433                .map(|(action_id_str, action_type)| -> Result<_> {
434                    let action_id = Self::parse_action_id_with_namespace(
435                        &ActionEntityUID::default_type(action_id_str),
436                        schema_namespace,
437                    )?;
438
439                    let (principal_types, resource_types, context) = action_type
440                        .applies_to
441                        .map(|applies_to| {
442                            (
443                                applies_to.principal_types,
444                                applies_to.resource_types,
445                                applies_to.context,
446                            )
447                        })
448                        .unwrap_or_default();
449
450                    // Convert the entries in the `appliesTo` lists into sets of
451                    // `EntityTypes`. If one of the lists is `None` (absent from the
452                    // schema), then the specification is undefined.
453                    let applies_to = ValidatorApplySpec::new(
454                        Self::parse_apply_spec_type_list(principal_types, schema_namespace)?,
455                        Self::parse_apply_spec_type_list(resource_types, schema_namespace)?,
456                    );
457
458                    let context = Self::try_schema_type_into_validator_type(
459                        schema_namespace,
460                        context.into_inner(),
461                    )?;
462
463                    let parents = action_type
464                        .member_of
465                        .unwrap_or_default()
466                        .iter()
467                        .map(|parent| -> Result<_> {
468                            Self::parse_action_id_with_namespace(parent, schema_namespace)
469                        })
470                        .collect::<Result<HashSet<_>>>()?;
471
472                    let (attribute_types, attributes) =
473                        Self::convert_attr_jsonval_map_to_attributes(
474                            action_type.attributes.unwrap_or_default(),
475                            &action_id,
476                            extensions,
477                        )?;
478
479                    Ok((
480                        action_id,
481                        ActionFragment {
482                            context,
483                            applies_to,
484                            parents,
485                            attribute_types,
486                            attributes,
487                        },
488                    ))
489                })
490                .collect::<Result<HashMap<_, _>>>()?,
491        })
492    }
493
494    // Check that `schema_file` uses actions in a way consistent with the
495    // specified `action_behavior`. When the behavior specifies that actions
496    // should not be used in groups and should not have attributes, then this
497    // function will return `Err` if it sees any action groups or attributes
498    // declared in the schema.
499    fn check_action_behavior(
500        schema_file: &NamespaceDefinition,
501        action_behavior: ActionBehavior,
502    ) -> Result<()> {
503        if schema_file
504            .entity_types
505            .iter()
506            // The `name` in an entity type declaration cannot be qualified
507            // with a namespace (it always implicitly takes the schema
508            // namespace), so we do this comparison directly.
509            .any(|(name, _)| name == ACTION_ENTITY_TYPE)
510        {
511            return Err(SchemaError::ActionEntityTypeDeclared);
512        }
513        if action_behavior == ActionBehavior::ProhibitAttributes {
514            let mut actions_with_attributes: Vec<String> = Vec::new();
515            for (name, a) in &schema_file.actions {
516                if a.attributes.is_some() {
517                    actions_with_attributes.push(name.to_string());
518                }
519            }
520            if !actions_with_attributes.is_empty() {
521                return Err(SchemaError::UnsupportedFeature(
522                    UnsupportedFeature::ActionAttributes(actions_with_attributes),
523                ));
524            }
525        }
526
527        Ok(())
528    }
529
530    /// Given the attributes for an entity type or action context as written in
531    /// a schema file, convert the types of the attributes into the `Type` data
532    /// structure used by the typechecker, and return the result as a map from
533    /// attribute name to type.
534    fn parse_record_attributes(
535        schema_namespace: Option<&Name>,
536        attrs: impl IntoIterator<Item = (SmolStr, TypeOfAttribute)>,
537    ) -> Result<WithUnresolvedTypeDefs<Attributes>> {
538        let attrs_with_type_defs = attrs
539            .into_iter()
540            .map(|(attr, ty)| -> Result<_> {
541                Ok((
542                    attr,
543                    (
544                        Self::try_schema_type_into_validator_type(schema_namespace, ty.ty)?,
545                        ty.required,
546                    ),
547                ))
548            })
549            .collect::<Result<Vec<_>>>()?;
550        Ok(WithUnresolvedTypeDefs::new(|typ_defs| {
551            attrs_with_type_defs
552                .into_iter()
553                .map(|(s, (attr_ty, is_req))| {
554                    attr_ty
555                        .resolve_type_defs(typ_defs)
556                        .map(|ty| (s, AttributeType::new(ty, is_req)))
557                })
558                .collect::<Result<Vec<_>>>()
559                .map(Attributes::with_attributes)
560        }))
561    }
562
563    /// Take an optional list of entity type name strings from an action apply
564    /// spec and parse it into a set of `Name`s for those entity types. If any
565    /// of the entity type names cannot be parsed, then the `Err` case is
566    /// returned, and it will indicate which name did not parse.
567    fn parse_apply_spec_type_list(
568        types: Option<Vec<SmolStr>>,
569        namespace: Option<&Name>,
570    ) -> Result<HashSet<EntityType>> {
571        types
572            .map(|types| {
573                types
574                    .iter()
575                    // Parse each type name string into a `Name`, generating an
576                    // `EntityTypeParseError` when the string is not a valid
577                    // name.
578                    .map(|ty_str| {
579                        Ok(EntityType::Specified(
580                            Self::parse_possibly_qualified_name_with_default_namespace(
581                                ty_str, namespace,
582                            )
583                            .map_err(SchemaError::ParseEntityType)?,
584                        ))
585                    })
586                    // Fail if any of the types failed.
587                    .collect::<Result<HashSet<_>>>()
588            })
589            .unwrap_or_else(|| Ok(HashSet::from([EntityType::Unspecified])))
590    }
591
592    // Parse a `Name` from a string (possibly including namespaces). If it is
593    // not qualified with any namespace, then apply the  default namespace to
594    // create a qualified name.  Do not modify any existing namespace on the
595    // type.
596    pub(crate) fn parse_possibly_qualified_name_with_default_namespace(
597        name_str: &SmolStr,
598        default_namespace: Option<&Name>,
599    ) -> std::result::Result<Name, ParseErrors> {
600        let name = Name::from_normalized_str(name_str)?;
601
602        let qualified_name = if name.namespace_components().next().is_some() {
603            // The name is already qualified. Don't touch it.
604            name
605        } else {
606            // The name does not have a namespace, so qualify the type to
607            // use the default.
608            match default_namespace {
609                Some(namespace) => {
610                    Name::type_in_namespace(name.basename().clone(), namespace.clone())
611                }
612                None => name,
613            }
614        };
615
616        Ok(qualified_name)
617    }
618
619    /// Parse a name from a string into the `Id` (basename only).  Then
620    /// initialize the namespace for this type with the provided namespace vec
621    /// to create the qualified `Name`.
622    fn parse_unqualified_name_with_namespace(
623        type_name: impl AsRef<str>,
624        namespace: Option<Name>,
625    ) -> std::result::Result<Name, ParseErrors> {
626        let type_name = Id::from_normalized_str(type_name.as_ref())?;
627        match namespace {
628            Some(namespace) => Ok(Name::type_in_namespace(type_name, namespace)),
629            None => Ok(Name::unqualified_name(type_name)),
630        }
631    }
632
633    /// Take an action identifier as a string and use it to construct an
634    /// EntityUID for that action. The entity type of the action will always
635    /// have the base type `Action`. The type will be qualified with any
636    /// namespace provided in the `namespace` argument or with the namespace
637    /// inside the ActionEntityUID if one is present.
638    fn parse_action_id_with_namespace(
639        action_id: &ActionEntityUID,
640        namespace: Option<&Name>,
641    ) -> Result<EntityUID> {
642        let namespaced_action_type = if let Some(action_ty) = &action_id.ty {
643            Self::parse_possibly_qualified_name_with_default_namespace(action_ty, namespace)
644                .map_err(SchemaError::ParseEntityType)?
645        } else {
646            // PANIC SAFETY: The constant ACTION_ENTITY_TYPE is valid entity type.
647            #[allow(clippy::expect_used)]
648            let id = Id::from_normalized_str(ACTION_ENTITY_TYPE).expect(
649                "Expected that the constant ACTION_ENTITY_TYPE would be a valid entity type.",
650            );
651            match namespace {
652                Some(namespace) => Name::type_in_namespace(id, namespace.clone()),
653                None => Name::unqualified_name(id),
654            }
655        };
656        Ok(EntityUID::from_components(
657            namespaced_action_type,
658            Eid::new(action_id.id.clone()),
659        ))
660    }
661
662    /// Implemented to convert a type as written in the schema json format into the
663    /// `Type` type used by the validator. Conversion can fail if an entity or
664    /// record attribute name is invalid. It will also fail for some types that can
665    /// be written in the schema, but are not yet implemented in the typechecking
666    /// logic.
667    pub(crate) fn try_schema_type_into_validator_type(
668        default_namespace: Option<&Name>,
669        schema_ty: SchemaType,
670    ) -> Result<WithUnresolvedTypeDefs<Type>> {
671        match schema_ty {
672            SchemaType::Type(SchemaTypeVariant::String) => Ok(Type::primitive_string().into()),
673            SchemaType::Type(SchemaTypeVariant::Long) => Ok(Type::primitive_long().into()),
674            SchemaType::Type(SchemaTypeVariant::Boolean) => Ok(Type::primitive_boolean().into()),
675            SchemaType::Type(SchemaTypeVariant::Set { element }) => Ok(
676                Self::try_schema_type_into_validator_type(default_namespace, *element)?
677                    .map(Type::set),
678            ),
679            SchemaType::Type(SchemaTypeVariant::Record {
680                attributes,
681                additional_attributes,
682            }) => {
683                if cfg!(not(feature = "partial-validate")) && additional_attributes {
684                    Err(SchemaError::UnsupportedFeature(
685                        UnsupportedFeature::OpenRecordsAndEntities,
686                    ))
687                } else {
688                    Ok(
689                        Self::parse_record_attributes(default_namespace, attributes)?.map(
690                            move |attrs| {
691                                Type::record_with_attributes(
692                                    attrs,
693                                    if additional_attributes {
694                                        OpenTag::OpenAttributes
695                                    } else {
696                                        OpenTag::ClosedAttributes
697                                    },
698                                )
699                            },
700                        ),
701                    )
702                }
703            }
704            SchemaType::Type(SchemaTypeVariant::Entity { name }) => {
705                let entity_type_name = Self::parse_possibly_qualified_name_with_default_namespace(
706                    &name,
707                    default_namespace,
708                )
709                .map_err(SchemaError::ParseEntityType)?;
710                Ok(Type::named_entity_reference(entity_type_name).into())
711            }
712            SchemaType::Type(SchemaTypeVariant::Extension { name }) => {
713                let extension_type_name =
714                    Name::from_normalized_str(&name).map_err(SchemaError::ParseExtensionType)?;
715                Ok(Type::extension(extension_type_name).into())
716            }
717            SchemaType::TypeDef { type_name } => {
718                let defined_type_name = Self::parse_possibly_qualified_name_with_default_namespace(
719                    &type_name,
720                    default_namespace,
721                )
722                .map_err(SchemaError::ParseCommonType)?;
723                Ok(WithUnresolvedTypeDefs::new(move |typ_defs| {
724                    typ_defs.get(&defined_type_name).cloned().ok_or(
725                        SchemaError::UndeclaredCommonTypes(HashSet::from([type_name.to_string()])),
726                    )
727                }))
728            }
729        }
730    }
731
732    /// Access the `Name` for the namespace of this definition.
733    pub fn namespace(&self) -> &Option<Name> {
734        &self.namespace
735    }
736}