cedar_policy_validator/schema/
namespace_def.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! This module contains the definition of `ValidatorNamespaceDef` and of types
18//! it relies on
19
20use std::collections::{hash_map::Entry, BTreeMap, HashMap, HashSet};
21
22use cedar_policy_core::{
23    ast::{
24        EntityAttrEvaluationError, EntityType, EntityUID, InternalName, Name, PartialValue,
25        UnreservedId,
26    },
27    entities::{json::err::JsonDeserializationErrorContext, CedarValueJson},
28    evaluator::RestrictedEvaluator,
29    extensions::Extensions,
30    fuzzy_match::fuzzy_search,
31    parser::Loc,
32};
33use itertools::Itertools;
34use nonempty::{nonempty, NonEmpty};
35use smol_str::{SmolStr, ToSmolStr};
36
37use super::{internal_name_to_entity_type, AllDefs, ValidatorApplySpec, ValidatorType};
38use crate::{
39    err::{schema_errors::*, SchemaError},
40    json_schema::{self, CommonTypeId, EntityTypeKind},
41    partition_nonempty::PartitionNonEmpty,
42    types::{AttributeType, Attributes, OpenTag, Type},
43    ActionBehavior, ConditionalName, RawName, ReferenceType,
44};
45
46/// A single namespace definition from the schema JSON or Cedar syntax,
47/// processed into a form which is closer to that used by the validator.
48/// The processing includes detection of some errors, for example, parse errors
49/// in entity/common type names or entity/common types which are declared
50/// multiple times.
51///
52/// In this representation, there may still be references to undeclared
53/// entity/common types, because any entity/common type may be declared in a
54/// different fragment that will only be known about when building the complete
55/// [`crate::ValidatorSchema`].
56///
57/// The parameter `N` is the type of entity type names and common type names in
58/// attributes/parents fields in this [`ValidatorNamespaceDef`], including
59/// recursively. (It doesn't affect the type of common and entity type names
60/// _that are being declared here_, which are already fully-qualified in this
61/// representation. It only affects the type of common and entity type
62/// _references_.)
63/// For example:
64/// - `N` = [`ConditionalName`]: References to entity/common types are not
65///     yet fully qualified/disambiguated
66/// - `N` = [`InternalName`]: All references to entity/common types have been
67///     resolved into fully-qualified [`InternalName`]s
68///
69/// `A` is like `N`, but `A` governs typenames in `appliesTo` fields, while
70/// `N` governs all other type references.
71#[derive(Debug, Clone)]
72pub struct ValidatorNamespaceDef<N, A> {
73    /// The (fully-qualified) name of the namespace this is a definition of, or
74    /// `None` if this is a definition for the empty namespace.
75    ///
76    /// This is informational only; it does not change the semantics of any
77    /// definition in `common_types`, `entity_types`, or `actions`. All
78    /// entity/common type names in `common_types`, `entity_types`, and
79    /// `actions` are already either fully qualified/disambiguated, or stored in
80    /// [`ConditionalName`] format which does not require referencing the
81    /// implicit `namespace` directly any longer.
82    /// This `namespace` field is used only in tests and by the `cedar_policy`
83    /// function `SchemaFragment::namespaces()`.
84    namespace: Option<InternalName>,
85    /// Common type definitions, which can be used to define entity
86    /// type attributes, action contexts, and other common types.
87    pub(super) common_types: CommonTypeDefs<N>,
88    /// Entity type declarations.
89    pub(super) entity_types: EntityTypesDef<N>,
90    /// Action declarations.
91    pub(super) actions: ActionsDef<N, A>,
92    #[cfg(feature = "extended-schema")]
93    pub(super) loc: Option<Loc>,
94}
95
96impl<N, A> ValidatorNamespaceDef<N, A> {
97    /// Get the fully-qualified [`InternalName`]s of all entity types declared
98    /// in this [`ValidatorNamespaceDef`].
99    pub fn all_declared_entity_type_names(&self) -> impl Iterator<Item = &InternalName> {
100        self.entity_types
101            .defs
102            .keys()
103            .map(|ety| ety.as_ref().as_ref())
104    }
105
106    /// Get the fully-qualified [`InternalName`]s of all common types declared
107    /// in this [`ValidatorNamespaceDef`].
108    pub fn all_declared_common_type_names(&self) -> impl Iterator<Item = &InternalName> {
109        self.common_types.defs.keys()
110    }
111
112    /// Get the fully-qualified [`EntityUID`]s of all actions declared in this
113    /// [`ValidatorNamespaceDef`].
114    pub fn all_declared_action_names(&self) -> impl Iterator<Item = &EntityUID> {
115        self.actions.actions.keys()
116    }
117
118    /// The fully-qualified [`InternalName`] of the namespace this is a definition of.
119    /// `None` indicates this definition is for the empty namespace.
120    pub fn namespace(&self) -> Option<&InternalName> {
121        self.namespace.as_ref()
122    }
123}
124
125impl ValidatorNamespaceDef<ConditionalName, ConditionalName> {
126    /// Construct a new [`ValidatorNamespaceDef<ConditionalName>`] from the raw [`json_schema::NamespaceDefinition`]
127    pub fn from_namespace_definition(
128        namespace: Option<InternalName>,
129        namespace_def: json_schema::NamespaceDefinition<RawName>,
130        action_behavior: ActionBehavior,
131        extensions: &Extensions<'_>,
132    ) -> crate::err::Result<ValidatorNamespaceDef<ConditionalName, ConditionalName>> {
133        // Return early with an error if actions cannot be in groups or have
134        // attributes, but the schema contains action groups or attributes.
135        Self::check_action_behavior(&namespace_def, action_behavior)?;
136
137        // Convert the common types, actions and entity types from the schema
138        // file into the representation used by the validator.
139        let common_types =
140            CommonTypeDefs::from_raw_common_types(namespace_def.common_types, namespace.as_ref())?;
141        let actions =
142            ActionsDef::from_raw_actions(namespace_def.actions, namespace.as_ref(), extensions)?;
143        let entity_types =
144            EntityTypesDef::from_raw_entity_types(namespace_def.entity_types, namespace.as_ref())?;
145
146        Ok(ValidatorNamespaceDef {
147            namespace,
148            common_types,
149            entity_types,
150            actions,
151            #[cfg(feature = "extended-schema")]
152            loc: namespace_def.loc,
153        })
154    }
155
156    /// Construct a new [`ValidatorNamespaceDef<ConditionalName>`] containing
157    /// only the given common-type definitions, which are already given in
158    /// terms of [`ConditionalName`]s.
159    pub fn from_common_type_defs(
160        namespace: Option<InternalName>,
161        defs: HashMap<UnreservedId, json_schema::Type<ConditionalName>>,
162    ) -> crate::err::Result<ValidatorNamespaceDef<ConditionalName, ConditionalName>> {
163        let common_types = CommonTypeDefs::from_conditionalname_typedefs(defs, namespace.as_ref())?;
164        Ok(ValidatorNamespaceDef {
165            namespace,
166            common_types,
167            entity_types: EntityTypesDef::new(),
168            actions: ActionsDef::new(),
169            #[cfg(feature = "extended-schema")]
170            loc: None,
171        })
172    }
173
174    /// Construct a new [`ValidatorNamespaceDef<ConditionalName>`] containing
175    /// only a single given common-type definition, which is already given in
176    /// terms of [`ConditionalName`]s.
177    ///
178    /// Unlike `from_common_type_defs()`, this function cannot fail, because
179    /// there is only one def so it cannot have a name collision with itself
180    pub fn from_common_type_def(
181        namespace: Option<InternalName>,
182        def: (UnreservedId, json_schema::Type<ConditionalName>),
183    ) -> ValidatorNamespaceDef<ConditionalName, ConditionalName> {
184        let common_types = CommonTypeDefs::from_conditionalname_typedef(def, namespace.as_ref());
185        ValidatorNamespaceDef {
186            namespace,
187            common_types,
188            entity_types: EntityTypesDef::new(),
189            actions: ActionsDef::new(),
190            #[cfg(feature = "extended-schema")]
191            loc: None,
192        }
193    }
194
195    /// Convert this [`ValidatorNamespaceDef<ConditionalName>`] into a
196    /// [`ValidatorNamespaceDef<InternalName>`] by fully-qualifying all
197    /// typenames that appear anywhere in any definitions.
198    ///
199    /// `all_defs` needs to contain the full set of all fully-qualified typenames
200    /// and actions that are defined in the schema (in all schema fragments).
201    pub fn fully_qualify_type_references(
202        self,
203        all_defs: &AllDefs,
204    ) -> Result<ValidatorNamespaceDef<InternalName, EntityType>, SchemaError> {
205        match (
206            self.common_types.fully_qualify_type_references(all_defs),
207            self.entity_types.fully_qualify_type_references(all_defs),
208            self.actions.fully_qualify_type_references(all_defs),
209        ) {
210            (Ok(common_types), Ok(entity_types), Ok(actions)) => Ok(ValidatorNamespaceDef {
211                namespace: self.namespace,
212                common_types,
213                entity_types,
214                actions,
215                #[cfg(feature = "extended-schema")]
216                loc: self.loc,
217            }),
218            (res1, res2, res3) => {
219                // PANIC SAFETY: at least one of the results is `Err`, so the input to `NonEmpty::collect()` cannot be an empty iterator
220                #[allow(clippy::expect_used)]
221                let errs = NonEmpty::collect(
222                    res1.err()
223                        .into_iter()
224                        .map(SchemaError::from)
225                        .chain(res2.err().map(SchemaError::from))
226                        .chain(res3.err()),
227                )
228                .expect("there must be an error");
229                Err(SchemaError::join_nonempty(errs))
230            }
231        }
232    }
233
234    /// Check that `schema_nsdef` uses actions in a way consistent with the
235    /// specified `action_behavior`. When the behavior specifies that actions
236    /// should not be used in groups and should not have attributes, then this
237    /// function will return `Err` if it sees any action groups or attributes
238    /// declared in the schema.
239    fn check_action_behavior<N>(
240        schema_nsdef: &json_schema::NamespaceDefinition<N>,
241        action_behavior: ActionBehavior,
242    ) -> crate::err::Result<()> {
243        if schema_nsdef
244            .entity_types
245            .iter()
246            // The `name` in an entity type declaration cannot be qualified
247            // with a namespace (it always implicitly takes the schema
248            // namespace), so we do this comparison directly.
249            .any(|(name, _)| name.to_smolstr() == cedar_policy_core::ast::ACTION_ENTITY_TYPE)
250        {
251            return Err(ActionEntityTypeDeclaredError {}.into());
252        }
253        if action_behavior == ActionBehavior::ProhibitAttributes {
254            let mut actions_with_attributes: Vec<String> = Vec::new();
255            for (name, a) in &schema_nsdef.actions {
256                if a.attributes.is_some() {
257                    actions_with_attributes.push(name.to_string());
258                }
259            }
260            if !actions_with_attributes.is_empty() {
261                actions_with_attributes.sort(); // TODO(#833): sort required for deterministic error messages
262                return Err(
263                    UnsupportedFeatureError(UnsupportedFeature::ActionAttributes(
264                        actions_with_attributes,
265                    ))
266                    .into(),
267                );
268            }
269        }
270
271        Ok(())
272    }
273}
274
275/// Holds a map from (fully qualified) [`InternalName`]s of common type
276/// definitions to their corresponding [`json_schema::Type`]. The common type
277/// [`InternalName`]s (keys in the map) are fully qualified, but inside the
278/// [`json_schema::Type`]s (values in the map), entity/common type references may or
279/// may not be fully qualified yet, depending on `N`; see notes on
280/// [`json_schema::Type`].
281#[derive(Debug, Clone)]
282pub struct CommonTypeDefs<N> {
283    pub(super) defs: HashMap<InternalName, json_schema::Type<N>>,
284}
285
286impl CommonTypeDefs<ConditionalName> {
287    /// Construct a [`CommonTypeDefs<ConditionalName>`] by converting the
288    /// structures used by the schema format to those used internally by the
289    /// validator.
290    pub(crate) fn from_raw_common_types(
291        schema_file_type_def: impl IntoIterator<Item = (CommonTypeId, json_schema::CommonType<RawName>)>,
292        schema_namespace: Option<&InternalName>,
293    ) -> crate::err::Result<Self> {
294        let mut defs = HashMap::new();
295        for (id, schema_ty) in schema_file_type_def {
296            let name = RawName::new_from_unreserved(id.into(), schema_ty.loc)
297                .qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
298            match defs.entry(name) {
299                Entry::Vacant(ventry) => {
300                    ventry.insert(
301                        schema_ty
302                            .ty
303                            .conditionally_qualify_type_references(schema_namespace),
304                    );
305                }
306                Entry::Occupied(oentry) => {
307                    return Err(SchemaError::DuplicateCommonType(DuplicateCommonTypeError {
308                        ty: oentry.key().clone(),
309                    }));
310                }
311            }
312        }
313        Ok(Self { defs })
314    }
315
316    /// Construct a [`CommonTypeDefs<ConditionalName>`] by converting the
317    /// structures used by the schema format to those used internally by the
318    /// validator; but unlike `from_raw_common_types()`, this function allows you to
319    /// directly supply [`ConditionalName`]s in the typedefs
320    pub(crate) fn from_conditionalname_typedefs(
321        input_type_defs: HashMap<UnreservedId, json_schema::Type<ConditionalName>>,
322        schema_namespace: Option<&InternalName>,
323    ) -> crate::err::Result<Self> {
324        let mut defs = HashMap::with_capacity(input_type_defs.len());
325        for (id, schema_ty) in input_type_defs {
326            let name = RawName::new_from_unreserved(id, None).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace
327            match defs.entry(name) {
328                Entry::Vacant(ventry) => {
329                    ventry.insert(schema_ty);
330                }
331                Entry::Occupied(oentry) => {
332                    return Err(SchemaError::DuplicateCommonType(DuplicateCommonTypeError {
333                        ty: oentry.key().clone(),
334                    }));
335                }
336            }
337        }
338        Ok(Self { defs })
339    }
340
341    /// Construct a [`CommonTypeDefs<ConditionalName>`] representing a single
342    /// typedef in the given namespace.
343    ///
344    /// Unlike [`from_conditionalname_typedefs()`], this function cannot fail,
345    /// because there is only one typedef so it cannot have a name collision
346    /// with itself
347    pub(crate) fn from_conditionalname_typedef(
348        (id, schema_ty): (UnreservedId, json_schema::Type<ConditionalName>),
349        schema_namespace: Option<&InternalName>,
350    ) -> Self {
351        Self {
352            defs: HashMap::from_iter([(
353                RawName::new_from_unreserved(id, None).qualify_with(schema_namespace),
354                schema_ty,
355            )]),
356        }
357    }
358
359    /// Convert this [`CommonTypeDefs<ConditionalName>`] into a
360    /// [`CommonTypeDefs<InternalName>`] by fully-qualifying all typenames that
361    /// appear anywhere in any definitions.
362    ///
363    /// `all_defs` needs to contain the full set of all fully-qualified typenames
364    /// and actions that are defined in the schema (in all schema fragments).
365    pub fn fully_qualify_type_references(
366        self,
367        all_defs: &AllDefs,
368    ) -> Result<CommonTypeDefs<InternalName>, TypeNotDefinedError> {
369        Ok(CommonTypeDefs {
370            defs: self
371                .defs
372                .into_iter()
373                .map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
374                .partition_nonempty()?,
375        })
376    }
377}
378
379/// Holds a map from (fully qualified) [`EntityType`]s (names of entity types) to
380/// their corresponding [`EntityTypeFragment`]. The [`EntityType`] keys in
381/// the map are fully qualified, but inside the [`EntityTypeFragment`]s (values
382/// in the map), entity/common type references may or may not be fully qualified
383/// yet, depending on `N`; see notes on [`EntityTypeFragment`].
384///
385/// Inside the [`EntityTypeFragment`]s, entity type parents and attributes may
386/// reference undeclared entity/common types (that will be declared in a
387/// different schema fragment).
388///
389/// All [`EntityType`] keys in this map are declared in this schema fragment.
390#[derive(Debug, Clone)]
391pub struct EntityTypesDef<N> {
392    pub(super) defs: HashMap<EntityType, EntityTypeFragment<N>>,
393}
394
395impl<N> EntityTypesDef<N> {
396    /// Construct an empty [`EntityTypesDef`] defining no entity types.
397    pub fn new() -> Self {
398        Self {
399            defs: HashMap::new(),
400        }
401    }
402}
403
404impl EntityTypesDef<ConditionalName> {
405    /// Construct a [`EntityTypesDef<ConditionalName>`] by converting the
406    /// structures used by the schema format to those used internally by the
407    /// validator.
408    pub(crate) fn from_raw_entity_types(
409        schema_files_types: impl IntoIterator<Item = (UnreservedId, json_schema::EntityType<RawName>)>,
410        schema_namespace: Option<&InternalName>,
411    ) -> crate::err::Result<Self> {
412        let mut defs: HashMap<EntityType, _> = HashMap::new();
413        for (id, entity_type) in schema_files_types {
414            let ety = internal_name_to_entity_type(
415                RawName::new_from_unreserved(id, entity_type.loc.clone())
416                    .qualify_with(schema_namespace), // the declaration name is always (unconditionally) prefixed by the current/active namespace
417            )?;
418            match defs.entry(ety) {
419                Entry::Vacant(ventry) => {
420                    ventry.insert(EntityTypeFragment::from_raw_entity_type(
421                        entity_type,
422                        schema_namespace,
423                    ));
424                }
425                Entry::Occupied(entry) => {
426                    return Err(DuplicateEntityTypeError {
427                        ty: entry.key().clone(),
428                    }
429                    .into());
430                }
431            }
432        }
433        Ok(EntityTypesDef { defs })
434    }
435
436    /// Convert this [`EntityTypesDef<ConditionalName>`] into a
437    /// [`EntityTypesDef<InternalName>`] by fully-qualifying all typenames that
438    /// appear anywhere in any definitions.
439    ///
440    /// `all_defs` needs to contain the full set of all fully-qualified typenames
441    /// and actions that are defined in the schema (in all schema fragments).
442    pub fn fully_qualify_type_references(
443        self,
444        all_defs: &AllDefs,
445    ) -> Result<EntityTypesDef<InternalName>, TypeNotDefinedError> {
446        Ok(EntityTypesDef {
447            defs: self
448                .defs
449                .into_iter()
450                .map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?)))
451                .partition_nonempty()?,
452        })
453    }
454}
455
456/// Holds the attributes and parents information for an entity type definition.
457///
458/// In this representation, references to common types may not yet have been
459/// fully resolved/inlined, and `parents`, `attributes`, and `tags` may all
460/// reference undeclared entity/common types. Furthermore, entity/common type
461/// references in `parents`, `attributes`, and `tags` may or may not be fully
462/// qualified yet, depending on `N`.
463#[derive(Debug, Clone)]
464pub enum EntityTypeFragment<N> {
465    Standard {
466        /// Description of the attribute types for this entity type.
467        ///
468        /// This may contain references to common types which have not yet been
469        /// resolved/inlined (e.g., because they are not defined in this schema
470        /// fragment).
471        /// In the extreme case, this may itself be just a common type pointing to a
472        /// `Record` type defined in another fragment.
473        attributes: json_schema::AttributesOrContext<N>,
474        /// Direct parent entity types for this entity type.
475        /// These entity types may be declared in a different namespace or schema
476        /// fragment.
477        ///
478        /// We will check for undeclared parent types when combining fragments into
479        /// a [`crate::ValidatorSchema`].
480        parents: HashSet<N>,
481        /// Tag type for this entity type. `None` means no tags are allowed on this
482        /// entity type.
483        ///
484        /// This may contain references to common types which have not yet been
485        /// resolved/inlined (e.g., because they are not defined in this schema
486        /// fragment).
487        tags: Option<json_schema::Type<N>>,
488    },
489    Enum(NonEmpty<SmolStr>),
490}
491
492impl<N> EntityTypeFragment<N> {
493    pub(crate) fn parents(&self) -> Box<dyn Iterator<Item = &N> + '_> {
494        match self {
495            Self::Standard { parents, .. } => Box::new(parents.iter()),
496            Self::Enum(_) => Box::new(std::iter::empty()),
497        }
498    }
499}
500
501impl EntityTypeFragment<ConditionalName> {
502    /// Construct a [`EntityTypeFragment<ConditionalName>`] by converting the
503    /// structures used by the schema format to those used internally by the
504    /// validator.
505    pub(crate) fn from_raw_entity_type(
506        schema_file_type: json_schema::EntityType<RawName>,
507        schema_namespace: Option<&InternalName>,
508    ) -> Self {
509        match schema_file_type.kind {
510            EntityTypeKind::Enum { choices } => Self::Enum(choices),
511            EntityTypeKind::Standard(ty) => {
512                Self::Standard {
513                    attributes: ty
514                        .shape
515                        .conditionally_qualify_type_references(schema_namespace),
516                    parents: ty
517                        .member_of_types
518                        .into_iter()
519                        .map(|raw_name| {
520                            // Only entity, not common, here for now; see #1064
521                            raw_name
522                                .conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
523                        })
524                        .collect(),
525                    tags: ty
526                        .tags
527                        .map(|tags| tags.conditionally_qualify_type_references(schema_namespace)),
528                }
529            }
530        }
531    }
532
533    /// Convert this [`EntityTypeFragment<ConditionalName>`] into a
534    /// [`EntityTypeFragment<InternalName>`] by fully-qualifying all typenames that
535    /// appear anywhere in any definitions.
536    ///
537    /// `all_defs` needs to contain the full set of all fully-qualified typenames
538    /// and actions that are defined in the schema (in all schema fragments).
539    pub fn fully_qualify_type_references(
540        self,
541        all_defs: &AllDefs,
542    ) -> Result<EntityTypeFragment<InternalName>, TypeNotDefinedError> {
543        match self {
544            Self::Enum(choices) => Ok(EntityTypeFragment::Enum(choices)),
545            Self::Standard {
546                attributes,
547                parents,
548                tags,
549            } => {
550                // Fully qualify typenames appearing in `attributes`
551                let fully_qual_attributes = attributes.fully_qualify_type_references(all_defs);
552                // Fully qualify typenames appearing in `parents`
553                let parents: HashSet<InternalName> = parents
554                    .into_iter()
555                    .map(|parent| parent.resolve(all_defs))
556                    .partition_nonempty()?;
557                // Fully qualify typenames appearing in `tags`
558                let fully_qual_tags = tags
559                    .map(|tags| tags.fully_qualify_type_references(all_defs))
560                    .transpose();
561                // Now is the time to check whether any parents are dangling, i.e.,
562                // refer to entity types that are not declared in any fragment (since we
563                // now have the set of typenames that are declared in all fragments).
564                let undeclared_parents: Option<NonEmpty<ConditionalName>> = NonEmpty::collect(
565                    parents
566                        .iter()
567                        .filter(|ety| !all_defs.is_defined_as_entity(ety))
568                        .map(|ety| {
569                            ConditionalName::unconditional(ety.clone(), ReferenceType::Entity)
570                        }),
571                );
572                match (fully_qual_attributes, fully_qual_tags, undeclared_parents) {
573                    (Ok(attributes), Ok(tags), None) => Ok(EntityTypeFragment::Standard {
574                        attributes,
575                        parents,
576                        tags,
577                    }),
578                    (Ok(_), Ok(_), Some(undeclared_parents)) => Err(TypeNotDefinedError {
579                        undefined_types: undeclared_parents,
580                    }),
581                    (Err(e), Ok(_), None) | (Ok(_), Err(e), None) => Err(e),
582                    (Err(e1), Err(e2), None) => {
583                        Err(TypeNotDefinedError::join_nonempty(nonempty![e1, e2]))
584                    }
585                    (Err(e), Ok(_), Some(mut undeclared))
586                    | (Ok(_), Err(e), Some(mut undeclared)) => {
587                        undeclared.extend(e.undefined_types);
588                        Err(TypeNotDefinedError {
589                            undefined_types: undeclared,
590                        })
591                    }
592                    (Err(e1), Err(e2), Some(mut undeclared)) => {
593                        undeclared.extend(e1.undefined_types);
594                        undeclared.extend(e2.undefined_types);
595                        Err(TypeNotDefinedError {
596                            undefined_types: undeclared,
597                        })
598                    }
599                }
600            }
601        }
602    }
603}
604
605/// Holds a map from (fully qualified) [`EntityUID`]s of action definitions
606/// to their corresponding [`ActionFragment`]. The action [`EntityUID`]s (keys
607/// in the map) are fully qualified, but inside the [`ActionFragment`]s (values
608/// in the map), entity/common type references (including references to other actions)
609/// may or may not be fully qualified yet, depending on `N` and `A`. See notes
610/// on [`ActionFragment`].
611///
612/// The [`ActionFragment`]s may also reference undeclared entity/common types
613/// and actions (that will be declared in a different schema fragment).
614///
615/// The current schema format specification does not include multiple action entity
616/// types. All action entities are required to use a single `Action` entity
617/// type. However, the action entity type may be namespaced, so an action entity
618/// may have a fully qualified entity type `My::Namespace::Action`.
619#[derive(Debug, Clone)]
620pub struct ActionsDef<N, A> {
621    pub(super) actions: HashMap<EntityUID, ActionFragment<N, A>>,
622}
623
624impl<N, A> ActionsDef<N, A> {
625    /// Construct an empty [`ActionsDef`] defining no entity types.
626    pub fn new() -> Self {
627        Self {
628            actions: HashMap::new(),
629        }
630    }
631}
632
633#[cfg_attr(not(feature = "extended-schema"), allow(unused_variables))]
634fn create_action_entity_uid_default_type(
635    action_name: &SmolStr,
636    action_type: &json_schema::ActionType<RawName>,
637    schema_namespace: Option<&InternalName>,
638) -> json_schema::ActionEntityUID<InternalName> {
639    let action_id_str = action_name.clone();
640    #[cfg(feature = "extended-schema")]
641    let action_id_loc = action_type.defn_loc.clone();
642    #[cfg(feature = "extended-schema")]
643    // the declaration name is always (unconditionally) prefixed by the current/active namespace
644    return json_schema::ActionEntityUID::default_type_with_loc(action_id_str, action_id_loc)
645        .qualify_with(schema_namespace);
646
647    #[cfg(not(feature = "extended-schema"))]
648    // the declaration name is always (unconditionally) prefixed by the current/active namespace
649    json_schema::ActionEntityUID::default_type(action_id_str).qualify_with(schema_namespace)
650}
651
652impl ActionsDef<ConditionalName, ConditionalName> {
653    /// Construct an [`ActionsDef<ConditionalName>`] by converting the structures used by the
654    /// schema format to those used internally by the validator.
655    pub(crate) fn from_raw_actions(
656        schema_file_actions: impl IntoIterator<Item = (SmolStr, json_schema::ActionType<RawName>)>,
657        schema_namespace: Option<&InternalName>,
658        extensions: &Extensions<'_>,
659    ) -> crate::err::Result<Self> {
660        let mut actions = HashMap::new();
661        for (action_name, action_type) in schema_file_actions {
662            let action_uid =
663                create_action_entity_uid_default_type(&action_name, &action_type, schema_namespace);
664            match actions.entry(action_uid.clone().try_into()?) {
665                Entry::Vacant(ventry) => {
666                    let frag = ActionFragment::from_raw_action(
667                        ventry.key(),
668                        action_type.clone(),
669                        schema_namespace,
670                        extensions,
671                        action_type.loc.as_ref(),
672                    )?;
673                    ventry.insert(frag);
674                }
675                Entry::Occupied(_) => {
676                    return Err(DuplicateActionError(action_name).into());
677                }
678            }
679        }
680        Ok(Self { actions })
681    }
682
683    /// Convert this [`ActionsDef<ConditionalName>`] into a
684    /// [`ActionsDef<InternalName>`] by fully-qualifying all typenames that
685    /// appear anywhere in any definitions.
686    ///
687    /// `all_defs` needs to contain the full set of all fully-qualified typenames
688    /// and actions that are defined in the schema (in all schema fragments).
689    pub fn fully_qualify_type_references(
690        self,
691        all_defs: &AllDefs,
692    ) -> Result<ActionsDef<InternalName, EntityType>, SchemaError> {
693        Ok(ActionsDef {
694            actions: self
695                .actions
696                .into_iter()
697                .map(|(k, v)| v.fully_qualify_type_references(all_defs).map(|v| (k, v)))
698                .partition_nonempty()?,
699        })
700    }
701}
702
703/// Holds the information about an action that comprises an action definition.
704///
705/// In this representation, references to common types may not yet have been
706/// fully resolved/inlined, and entity/common type references (including
707/// references to other actions) may not yet be fully qualified, depending on
708/// `N` and `A`. This [`ActionFragment`] may also reference undeclared entity/common
709/// types and actions (that will be declared in a different schema fragment).
710///
711/// `A` is used for typenames in `applies_to`, and `N` is used for all other
712/// type references.
713#[derive(Debug, Clone)]
714pub struct ActionFragment<N, A> {
715    /// The type of the context record for this action. This may contain
716    /// references to common types which have not yet been resolved/inlined
717    /// (e.g., because they are not defined in this schema fragment).
718    pub(super) context: json_schema::Type<N>,
719    /// The principals and resources that an action can be applied to.
720    pub(super) applies_to: ValidatorApplySpec<A>,
721    /// The direct parent action entities for this action.
722    /// These may be actions declared in a different namespace or schema
723    /// fragment, and thus not declared yet.
724    /// We will check for undeclared parents when combining fragments into a
725    /// [`crate::ValidatorSchema`].
726    pub(super) parents: HashSet<json_schema::ActionEntityUID<N>>,
727    /// The types for the attributes defined for this actions entity.
728    /// Here, common types have been fully resolved/inlined.
729    pub(super) attribute_types: Attributes,
730    /// The values for the attributes defined for this actions entity, stored
731    /// separately so that we can later extract these values to construct the
732    /// actual `Entity` objects defined by the schema.
733    pub(super) attributes: BTreeMap<SmolStr, PartialValue>,
734    /// Source location - if available
735    pub(super) loc: Option<Loc>,
736}
737
738impl ActionFragment<ConditionalName, ConditionalName> {
739    pub(crate) fn from_raw_action(
740        action_uid: &EntityUID,
741        action_type: json_schema::ActionType<RawName>,
742        schema_namespace: Option<&InternalName>,
743        extensions: &Extensions<'_>,
744        loc: Option<&Loc>,
745    ) -> crate::err::Result<Self> {
746        let (principal_types, resource_types, context) = action_type
747            .applies_to
748            .map(|applies_to| {
749                (
750                    applies_to.principal_types,
751                    applies_to.resource_types,
752                    applies_to.context,
753                )
754            })
755            .unwrap_or_default();
756        let (attribute_types, attributes) = Self::convert_attr_jsonval_map_to_attributes(
757            action_type.attributes.unwrap_or_default(),
758            action_uid,
759            extensions,
760        )?;
761        Ok(Self {
762            context: context
763                .into_inner()
764                .conditionally_qualify_type_references(schema_namespace),
765            applies_to: ValidatorApplySpec::<ConditionalName>::new(
766                principal_types
767                    .into_iter()
768                    .map(|pty| {
769                        pty.conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
770                    })
771                    .collect(),
772                resource_types
773                    .into_iter()
774                    .map(|rty| {
775                        rty.conditionally_qualify_with(schema_namespace, ReferenceType::Entity)
776                    })
777                    .collect(),
778            ),
779            parents: action_type
780                .member_of
781                .unwrap_or_default()
782                .into_iter()
783                .map(|parent| parent.conditionally_qualify_type_references(schema_namespace))
784                .collect(),
785            attribute_types,
786            attributes,
787            loc: loc.cloned(),
788        })
789    }
790
791    /// Convert this [`ActionFragment<ConditionalName>`] into an
792    /// [`ActionFragment<InternalName>`] by fully-qualifying all typenames that
793    /// appear anywhere in any definitions.
794    ///
795    /// `all_defs` needs to contain the full set of all fully-qualified typenames
796    /// and actions that are defined in the schema (in all schema fragments).
797    pub fn fully_qualify_type_references(
798        self,
799        all_defs: &AllDefs,
800    ) -> Result<ActionFragment<InternalName, EntityType>, SchemaError> {
801        Ok(ActionFragment {
802            context: self.context.fully_qualify_type_references(all_defs)?,
803            applies_to: self.applies_to.fully_qualify_type_references(all_defs)?,
804            parents: self
805                .parents
806                .into_iter()
807                .map(|parent| parent.fully_qualify_type_references(all_defs))
808                .partition_nonempty()?,
809            attribute_types: self.attribute_types,
810            attributes: self.attributes,
811            loc: self.loc,
812        })
813    }
814
815    fn convert_attr_jsonval_map_to_attributes(
816        m: HashMap<SmolStr, CedarValueJson>,
817        action_id: &EntityUID,
818        extensions: &Extensions<'_>,
819    ) -> crate::err::Result<(Attributes, BTreeMap<SmolStr, PartialValue>)> {
820        let mut attr_types: HashMap<SmolStr, Type> = HashMap::with_capacity(m.len());
821        let mut attr_values: BTreeMap<SmolStr, PartialValue> = BTreeMap::new();
822        let evaluator = RestrictedEvaluator::new(extensions);
823
824        for (k, v) in m {
825            let t = Self::jsonval_to_type_helper(&v, action_id);
826            match t {
827                Ok(ty) => attr_types.insert(k.clone(), ty),
828                Err(e) => return Err(e),
829            };
830
831            // As an artifact of the limited `CedarValueJson` variants accepted by
832            // `Self::jsonval_to_type_helper`, we know that this function will
833            // never error. Also note that this is only ever executed when
834            // action attributes are enabled, but they cannot be enabled when
835            // using Cedar through the public API. This is fortunate because
836            // handling an error here would mean adding a new error variant to
837            // `SchemaError` in the public API, but we didn't make that enum
838            // `non_exhaustive`, so any new variants are a breaking change.
839            // PANIC SAFETY: see above
840            #[allow(clippy::expect_used)]
841            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`");
842            let pv = evaluator
843                .partial_interpret(e.as_borrowed())
844                .map_err(|err| {
845                    ActionAttrEvalError(EntityAttrEvaluationError {
846                        uid: action_id.clone(),
847                        attr_or_tag: k.clone(),
848                        was_attr: true,
849                        err,
850                    })
851                })?;
852            attr_values.insert(k, pv);
853        }
854        Ok((
855            Attributes::with_required_attributes(attr_types),
856            attr_values,
857        ))
858    }
859
860    /// Helper to get types from `CedarValueJson`s. Currently doesn't support all
861    /// `CedarValueJson` types. Note: If this function is extended to cover move
862    /// `CedarValueJson`s, we must update `convert_attr_jsonval_map_to_attributes` to
863    /// handle errors that may occur when parsing these values. This will require
864    /// a breaking change in the `SchemaError` type in the public API.
865    fn jsonval_to_type_helper(
866        v: &CedarValueJson,
867        action_id: &EntityUID,
868    ) -> crate::err::Result<Type> {
869        match v {
870            CedarValueJson::Bool(_) => Ok(Type::primitive_boolean()),
871            CedarValueJson::Long(_) => Ok(Type::primitive_long()),
872            CedarValueJson::String(_) => Ok(Type::primitive_string()),
873            CedarValueJson::Record(r) => {
874                let mut required_attrs: HashMap<SmolStr, Type> = HashMap::with_capacity(r.len());
875                for (k, v_prime) in r {
876                    let t = Self::jsonval_to_type_helper(v_prime, action_id);
877                    match t {
878                        Ok(ty) => required_attrs.insert(k.clone(), ty),
879                        Err(e) => return Err(e),
880                    };
881                }
882                Ok(Type::record_with_required_attributes(
883                    required_attrs,
884                    OpenTag::ClosedAttributes,
885                ))
886            }
887            CedarValueJson::Set(v) => match v.first() {
888                //sets with elements of different types will be rejected elsewhere
889                None => Err(ActionAttributesContainEmptySetError {
890                    uid: action_id.clone(),
891                }
892                .into()),
893                Some(element) => {
894                    let element_type = Self::jsonval_to_type_helper(element, action_id);
895                    match element_type {
896                        Ok(t) => Ok(Type::Set {
897                            element_type: Some(Box::new(t)),
898                        }),
899                        Err(_) => element_type,
900                    }
901                }
902            },
903            CedarValueJson::EntityEscape { __entity: _ } => Err(UnsupportedActionAttributeError {
904                uid: action_id.clone(),
905                attr: "entity escape (`__entity`)".into(),
906            }
907            .into()),
908            CedarValueJson::ExprEscape { __expr: _ } => Err(UnsupportedActionAttributeError {
909                uid: action_id.clone(),
910                attr: "expression escape (`__expr`)".into(),
911            }
912            .into()),
913            CedarValueJson::ExtnEscape { __extn: _ } => Err(UnsupportedActionAttributeError {
914                uid: action_id.clone(),
915                attr: "extension function escape (`__extn`)".into(),
916            }
917            .into()),
918            CedarValueJson::Null => Err(UnsupportedActionAttributeError {
919                uid: action_id.clone(),
920                attr: "null".into(),
921            }
922            .into()),
923        }
924    }
925}
926
927type ResolveFunc<T> = dyn FnOnce(&HashMap<&InternalName, ValidatorType>) -> crate::err::Result<T>;
928/// Represent a type that might be defined in terms of some common-type
929/// definitions which are not necessarily available in the current namespace.
930pub(crate) enum WithUnresolvedCommonTypeRefs<T> {
931    WithUnresolved(Box<ResolveFunc<T>>, Option<Loc>),
932    WithoutUnresolved(T, Option<Loc>),
933}
934
935impl<T: 'static> WithUnresolvedCommonTypeRefs<T> {
936    pub fn new(
937        f: impl FnOnce(&HashMap<&InternalName, ValidatorType>) -> crate::err::Result<T> + 'static,
938        loc: Option<Loc>,
939    ) -> Self {
940        Self::WithUnresolved(Box::new(f), loc)
941    }
942
943    pub fn loc(&self) -> Option<&Loc> {
944        match self {
945            WithUnresolvedCommonTypeRefs::WithUnresolved(_, loc) => loc.as_ref(),
946            WithUnresolvedCommonTypeRefs::WithoutUnresolved(_, loc) => loc.as_ref(),
947        }
948    }
949
950    pub fn map<U: 'static>(
951        self,
952        f: impl FnOnce(T) -> U + 'static,
953    ) -> WithUnresolvedCommonTypeRefs<U> {
954        match self {
955            Self::WithUnresolved(_, ref loc) => {
956                let loc = loc.clone();
957                WithUnresolvedCommonTypeRefs::new(
958                    |common_type_defs| self.resolve_common_type_refs(common_type_defs).map(f),
959                    loc,
960                )
961            }
962            Self::WithoutUnresolved(v, loc) => {
963                WithUnresolvedCommonTypeRefs::WithoutUnresolved(f(v), loc)
964            }
965        }
966    }
967
968    /// Resolve references to common types by inlining their definitions from
969    /// the given `HashMap`.
970    ///
971    /// Be warned that `common_type_defs` should contain all definitions, from
972    /// all schema fragments.
973    /// If `self` references any type not in `common_type_defs`, this will
974    /// return a `TypeNotDefinedError`.
975    pub fn resolve_common_type_refs(
976        self,
977        common_type_defs: &HashMap<&InternalName, ValidatorType>,
978    ) -> crate::err::Result<T> {
979        match self {
980            WithUnresolvedCommonTypeRefs::WithUnresolved(f, _loc) => f(common_type_defs),
981            WithUnresolvedCommonTypeRefs::WithoutUnresolved(v, _loc) => Ok(v),
982        }
983    }
984}
985
986impl<T: 'static> From<T> for WithUnresolvedCommonTypeRefs<T> {
987    fn from(value: T) -> Self {
988        Self::WithoutUnresolved(value, None)
989    }
990}
991
992impl From<Type> for WithUnresolvedCommonTypeRefs<ValidatorType> {
993    fn from(value: Type) -> Self {
994        Self::WithoutUnresolved(
995            ValidatorType {
996                ty: value,
997                #[cfg(feature = "extended-schema")]
998                loc: None,
999            },
1000            None,
1001        )
1002    }
1003}
1004
1005impl<T: std::fmt::Debug> std::fmt::Debug for WithUnresolvedCommonTypeRefs<T> {
1006    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1007        match self {
1008            WithUnresolvedCommonTypeRefs::WithUnresolved(_, _) => {
1009                f.debug_tuple("WithUnresolved").finish()
1010            }
1011            WithUnresolvedCommonTypeRefs::WithoutUnresolved(v, _) => {
1012                f.debug_tuple("WithoutUnresolved").field(v).finish()
1013            }
1014        }
1015    }
1016}
1017
1018impl TryInto<ValidatorNamespaceDef<ConditionalName, ConditionalName>>
1019    for json_schema::NamespaceDefinition<RawName>
1020{
1021    type Error = SchemaError;
1022
1023    fn try_into(
1024        self,
1025    ) -> crate::err::Result<ValidatorNamespaceDef<ConditionalName, ConditionalName>> {
1026        ValidatorNamespaceDef::from_namespace_definition(
1027            None,
1028            self,
1029            ActionBehavior::default(),
1030            Extensions::all_available(),
1031        )
1032    }
1033}
1034
1035/// Convert a [`json_schema::Type`] (with fully-qualified names) into the
1036/// [`Type`] type used by the validator.
1037///
1038/// Conversion can fail if an entity or record attribute name is invalid. It
1039/// will also fail for some types that can be written in the schema, but are
1040/// not yet implemented in the typechecking logic.
1041pub(crate) fn try_jsonschema_type_into_validator_type(
1042    schema_ty: json_schema::Type<InternalName>,
1043    extensions: &Extensions<'_>,
1044    loc: Option<Loc>,
1045) -> crate::err::Result<WithUnresolvedCommonTypeRefs<ValidatorType>> {
1046    match schema_ty {
1047        json_schema::Type::Type {
1048            ty: json_schema::TypeVariant::String,
1049            ..
1050        } => Ok(WithUnresolvedCommonTypeRefs::WithoutUnresolved(
1051            ValidatorType {
1052                ty: Type::primitive_string(),
1053                #[cfg(feature = "extended-schema")]
1054                loc: loc.clone(),
1055            },
1056            loc,
1057        )),
1058        json_schema::Type::Type {
1059            ty: json_schema::TypeVariant::Long,
1060            ..
1061        } => Ok(WithUnresolvedCommonTypeRefs::WithoutUnresolved(
1062            ValidatorType {
1063                ty: Type::primitive_long(),
1064                #[cfg(feature = "extended-schema")]
1065                loc: loc.clone(),
1066            },
1067            loc,
1068        )),
1069        json_schema::Type::Type {
1070            ty: json_schema::TypeVariant::Boolean,
1071            ..
1072        } => Ok(WithUnresolvedCommonTypeRefs::WithoutUnresolved(
1073            ValidatorType {
1074                ty: Type::primitive_boolean(),
1075                #[cfg(feature = "extended-schema")]
1076                loc: loc.clone(),
1077            },
1078            loc,
1079        )),
1080        json_schema::Type::Type {
1081            ty: json_schema::TypeVariant::Set { element },
1082            ..
1083        } => Ok(
1084            try_jsonschema_type_into_validator_type(*element, extensions, loc)?.map(|vt| {
1085                ValidatorType {
1086                    ty: Type::set(vt.ty),
1087                    #[cfg(feature = "extended-schema")]
1088                    loc: vt.loc,
1089                }
1090            }),
1091        ),
1092        json_schema::Type::Type {
1093            ty: json_schema::TypeVariant::Record(rty),
1094            ..
1095        } => try_record_type_into_validator_type(rty, extensions, loc),
1096        json_schema::Type::Type {
1097            ty: json_schema::TypeVariant::Entity { name },
1098            ..
1099        } => Ok(WithUnresolvedCommonTypeRefs::WithoutUnresolved(
1100            ValidatorType {
1101                ty: Type::named_entity_reference(internal_name_to_entity_type(name)?),
1102                #[cfg(feature = "extended-schema")]
1103                loc: loc.clone(),
1104            },
1105            loc,
1106        )),
1107        json_schema::Type::Type {
1108            ty: json_schema::TypeVariant::Extension { name },
1109            ..
1110        } => {
1111            let extension_type_name = Name::unqualified_name(name);
1112            if extensions.ext_types().contains(&extension_type_name) {
1113                Ok(Type::extension(extension_type_name).into())
1114            } else {
1115                let suggested_replacement = fuzzy_search(
1116                    &extension_type_name.to_string(),
1117                    &extensions
1118                        .ext_types()
1119                        .map(|n| n.to_string())
1120                        .collect::<Vec<_>>(),
1121                );
1122                Err(SchemaError::UnknownExtensionType(
1123                    UnknownExtensionTypeError {
1124                        actual: extension_type_name,
1125                        suggested_replacement,
1126                    },
1127                ))
1128            }
1129        }
1130        json_schema::Type::CommonTypeRef { type_name, .. } => {
1131            Ok(WithUnresolvedCommonTypeRefs::new(
1132                move |common_type_defs| {
1133                    common_type_defs
1134                        .get(&type_name)
1135                        .cloned()
1136                        // We should always have `Some` here, because if the common type
1137                        // wasn't defined, that error should have been caught earlier,
1138                        // when the `json_schema::Type<InternalName>` was created by
1139                        // resolving a `ConditionalName` into a fully-qualified
1140                        // `InternalName`.
1141                        // Nonetheless, instead of panicking if that internal
1142                        // invariant is violated, it's easy to return this dynamic
1143                        // error instead.
1144                        .ok_or_else(|| CommonTypeInvariantViolationError { name: type_name }.into())
1145                },
1146                loc,
1147            ))
1148        }
1149        json_schema::Type::Type {
1150            ty: json_schema::TypeVariant::EntityOrCommon { type_name },
1151            ..
1152        } => {
1153            let loc_clone = loc.clone();
1154            Ok(WithUnresolvedCommonTypeRefs::new(
1155                move |common_type_defs| {
1156                    #[cfg_attr(not(feature = "extended-schema"), allow(unused_variables))]
1157                    let loc: Option<Loc> = loc.clone();
1158
1159                    // First check if it's a common type, because in the edge case where
1160                    // the name is both a valid common type name and a valid entity type
1161                    // name, we give preference to the common type (see RFC 24).
1162                    match common_type_defs.get(&type_name) {
1163                        Some(def) => Ok(def.clone()),
1164                        None => {
1165                            // It wasn't a common type, so we assume it must be a valid
1166                            // entity type. Otherwise, we would have had an error earlier,
1167                            // when the `json_schema::Type<InternalName>` was created by
1168                            // resolving a `ConditionalName` into a fully-qualified
1169                            // `InternalName`.
1170                            Ok(ValidatorType {
1171                                ty: Type::named_entity_reference(internal_name_to_entity_type(
1172                                    type_name,
1173                                )?),
1174                                #[cfg(feature = "extended-schema")]
1175                                loc,
1176                            })
1177                        }
1178                    }
1179                },
1180                loc_clone,
1181            ))
1182        }
1183    }
1184}
1185
1186/// Convert a [`json_schema::RecordType`] (with fully qualified names) into the
1187/// [`Type`] type used by the validator.
1188#[cfg_attr(not(feature = "extended-schema"), allow(unused_variables))]
1189pub(crate) fn try_record_type_into_validator_type(
1190    rty: json_schema::RecordType<InternalName>,
1191    extensions: &Extensions<'_>,
1192    loc: Option<Loc>,
1193) -> crate::err::Result<WithUnresolvedCommonTypeRefs<ValidatorType>> {
1194    if cfg!(not(feature = "partial-validate")) && rty.additional_attributes {
1195        Err(UnsupportedFeatureError(UnsupportedFeature::OpenRecordsAndEntities).into())
1196    } else {
1197        #[cfg(feature = "extended-schema")]
1198        let attr_loc = loc.clone();
1199        #[cfg(not(feature = "extended-schema"))]
1200        let attr_loc = None;
1201        Ok(
1202            parse_record_attributes(rty.attributes.into_iter(), extensions, attr_loc)?.map(
1203                move |attrs| ValidatorType {
1204                    ty: Type::record_with_attributes(
1205                        attrs,
1206                        if rty.additional_attributes {
1207                            OpenTag::OpenAttributes
1208                        } else {
1209                            OpenTag::ClosedAttributes
1210                        },
1211                    ),
1212                    #[cfg(feature = "extended-schema")]
1213                    loc,
1214                },
1215            ),
1216        )
1217    }
1218}
1219
1220/// Given the attributes for an entity or record type in the schema file format
1221/// structures (but with fully-qualified names), convert the types of the
1222/// attributes into the [`Type`] data structure used by the validator, and
1223/// return the result as an [`Attributes`] structure.
1224#[cfg_attr(not(feature = "extended-schema"), allow(unused_variables))]
1225fn parse_record_attributes(
1226    attrs: impl IntoIterator<Item = (SmolStr, json_schema::TypeOfAttribute<InternalName>)>,
1227    extensions: &Extensions<'_>,
1228    loc: Option<Loc>,
1229) -> crate::err::Result<WithUnresolvedCommonTypeRefs<Attributes>> {
1230    let attrs_with_common_type_refs = attrs
1231        .into_iter()
1232        .map(|(attr, ty)| -> crate::err::Result<_> {
1233            #[cfg(feature = "extended-schema")]
1234            let loc = ty.loc;
1235            #[cfg(not(feature = "extended-schema"))]
1236            let loc = None;
1237            Ok((
1238                attr,
1239                (
1240                    try_jsonschema_type_into_validator_type(ty.ty.clone(), extensions, loc)?,
1241                    ty.required,
1242                ),
1243            ))
1244        })
1245        .collect::<crate::err::Result<Vec<_>>>()?;
1246
1247    Ok(WithUnresolvedCommonTypeRefs::new(
1248        |common_type_defs| {
1249            attrs_with_common_type_refs
1250                .into_iter()
1251                .map(|(s, (attr_ty, is_req))| {
1252                    let loc = attr_ty.loc().cloned();
1253                    attr_ty
1254                        .resolve_common_type_refs(common_type_defs)
1255                        .map(|ty| {
1256                            #[cfg(feature = "extended-schema")]
1257                            let ty_pair = (s, AttributeType::new_with_loc(ty.ty, is_req, loc));
1258                            #[cfg(not(feature = "extended-schema"))]
1259                            let ty_pair = (s, AttributeType::new(ty.ty, is_req));
1260                            ty_pair
1261                        })
1262                })
1263                .collect::<crate::err::Result<Vec<_>>>()
1264                .map(Attributes::with_attributes)
1265        },
1266        loc,
1267    ))
1268}