cedar_policy_validator/cedar_schema/
to_json_schema.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//! Convert a schema into the JSON format
18
19use std::collections::HashMap;
20
21use cedar_policy_core::{
22    ast::{Annotations, Id, Name, UnreservedId},
23    extensions::Extensions,
24    parser::{Loc, Node},
25};
26use itertools::Either;
27use nonempty::NonEmpty;
28use smol_str::{SmolStr, ToSmolStr};
29use std::collections::hash_map::Entry;
30
31use super::{
32    ast::{
33        ActionDecl, Annotated, AppDecl, AttrDecl, Decl, Declaration, EntityDecl, Namespace,
34        PRAppDecl, Path, QualName, Schema, Type, TypeDecl, BUILTIN_TYPES, PR,
35    },
36    err::{schema_warnings, SchemaWarning, ToJsonSchemaError, ToJsonSchemaErrors},
37};
38use crate::{
39    cedar_schema,
40    json_schema::{self, CommonType},
41    RawName,
42};
43
44impl From<cedar_schema::Path> for RawName {
45    fn from(p: cedar_schema::Path) -> Self {
46        RawName::from_name(p.into())
47    }
48}
49
50/// Convert a schema AST into the JSON representation.
51/// This will let you subsequently decode that into the Validator AST for Schemas ([`crate::ValidatorSchema`]).
52/// On success, this function returns a tuple containing:
53///     * The `json_schema::Fragment`
54///     * An iterator of warnings that were generated
55///
56/// TODO(#1085): These warnings should be generated later in the process, such
57/// that we apply the same checks to JSON and Cedar schemas
58pub fn cedar_schema_to_json_schema(
59    schema: Schema,
60    extensions: &Extensions<'_>,
61) -> Result<
62    (
63        json_schema::Fragment<RawName>,
64        impl Iterator<Item = SchemaWarning>,
65    ),
66    ToJsonSchemaErrors,
67> {
68    // combine all of the declarations in unqualified (empty) namespaces into a
69    // single unqualified namespace
70    //
71    // TODO(#1086): If we want to allow reopening a namespace within the same
72    // (Cedar) schema fragment, then in this step we would also need to combine
73    // namespaces with matching non-empty names, so that all definitions from
74    // that namespace make it into the JSON schema structure under that
75    // namespace's key.
76    let (qualified_namespaces, unqualified_namespace) = split_unqualified_namespace(schema);
77    // Create a single iterator for all namespaces
78    let all_namespaces = qualified_namespaces
79        .chain(unqualified_namespace)
80        .collect::<Vec<_>>();
81
82    let names = build_namespace_bindings(all_namespaces.iter().map(|ns| &ns.data))?;
83    let warnings = compute_namespace_warnings(&names, extensions);
84    let fragment = collect_all_errors(all_namespaces.into_iter().map(convert_namespace))?.collect();
85    Ok((
86        json_schema::Fragment(fragment),
87        warnings.collect::<Vec<_>>().into_iter(),
88    ))
89}
90
91/// Is the given [`Id`] the name of a valid extension type, given the currently active [`Extensions`]
92fn is_valid_ext_type(ty: &Id, extensions: &Extensions<'_>) -> bool {
93    extensions
94        .ext_types()
95        .filter(|ext_ty| ext_ty.as_ref().is_unqualified()) // if there are any qualified extension type names, we don't care, because we're looking for an unqualified name `ty`
96        .any(|ext_ty| ty == ext_ty.basename_as_ref())
97}
98
99/// Convert a `Type` into the JSON representation of the type.
100pub fn cedar_type_to_json_type(ty: Node<Type>) -> json_schema::Type<RawName> {
101    let variant = match ty.node {
102        Type::Set(t) => json_schema::TypeVariant::Set {
103            element: Box::new(cedar_type_to_json_type(*t)),
104        },
105        Type::Ident(p) => json_schema::TypeVariant::EntityOrCommon {
106            type_name: RawName::from(p),
107        },
108        Type::Record(fields) => json_schema::TypeVariant::Record(json_schema::RecordType {
109            attributes: fields.into_iter().map(convert_attr_decl).collect(),
110            additional_attributes: false,
111        }),
112    };
113    json_schema::Type::Type {
114        ty: variant,
115        loc: Some(ty.loc),
116    }
117}
118
119// Split namespaces into two groups: named namespaces and the implicit unqualified namespace
120// The rhs of the tuple will be [`None`] if there are no items in the unqualified namespace.
121fn split_unqualified_namespace(
122    namespaces: impl IntoIterator<Item = Annotated<Namespace>>,
123) -> (
124    impl Iterator<Item = Annotated<Namespace>>,
125    Option<Annotated<Namespace>>,
126) {
127    // First split every namespace into those with explicit names and those without
128    let (qualified, unqualified): (Vec<_>, Vec<_>) =
129        namespaces.into_iter().partition(|n| n.data.name.is_some());
130
131    // Now combine all the decls in namespaces without names into one unqualified namespace
132    let mut unqualified_decls = vec![];
133    for mut unqualified_namespace in unqualified.into_iter() {
134        unqualified_decls.append(&mut unqualified_namespace.data.decls);
135    }
136
137    if unqualified_decls.is_empty() {
138        (qualified.into_iter(), None)
139    } else {
140        let unqual = Namespace {
141            name: None,
142            decls: unqualified_decls,
143            loc: None,
144        };
145        (
146            qualified.into_iter(),
147            Some(Annotated {
148                data: unqual,
149                annotations: Annotations::new(),
150            }),
151        )
152    }
153}
154
155/// Converts a CST namespace to a JSON namespace
156fn convert_namespace(
157    namespace: Annotated<Namespace>,
158) -> Result<(Option<Name>, json_schema::NamespaceDefinition<RawName>), ToJsonSchemaErrors> {
159    let ns_name = namespace
160        .data
161        .name
162        .clone()
163        .map(|p| {
164            let internal_name = RawName::from(p.clone()).qualify_with(None); // namespace names are always written already-fully-qualified in the Cedar schema syntax
165            Name::try_from(internal_name)
166                .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), p.loc().clone()))
167        })
168        .transpose()?;
169    let def = namespace.try_into()?;
170    Ok((ns_name, def))
171}
172
173impl TryFrom<Annotated<Namespace>> for json_schema::NamespaceDefinition<RawName> {
174    type Error = ToJsonSchemaErrors;
175
176    fn try_from(
177        n: Annotated<Namespace>,
178    ) -> Result<json_schema::NamespaceDefinition<RawName>, Self::Error> {
179        // Partition the decls into entities, actions, and common types
180        let (entity_types, action, common_types) = into_partition_decls(n.data.decls);
181
182        // Convert entity type decls, collecting all errors
183        let entity_types = collect_all_errors(entity_types.into_iter().map(convert_entity_decl))?
184            .flatten()
185            .collect();
186
187        // Convert action decls, collecting all errors
188        let actions = collect_all_errors(action.into_iter().map(convert_action_decl))?
189            .flatten()
190            .collect();
191
192        // Convert common type decls
193        let common_types = common_types
194            .into_iter()
195            .map(|decl| {
196                let name_loc = decl.data.node.name.loc.clone();
197                let id = UnreservedId::try_from(decl.data.node.name.node)
198                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), name_loc.clone()))?;
199                let ctid = json_schema::CommonTypeId::new(id)
200                    .map_err(|e| ToJsonSchemaError::reserved_keyword(&e.id, name_loc))?;
201                Ok((
202                    ctid,
203                    CommonType {
204                        ty: cedar_type_to_json_type(decl.data.node.def),
205                        annotations: decl.annotations.into(),
206                        loc: Some(decl.data.loc),
207                    },
208                ))
209            })
210            .collect::<Result<_, ToJsonSchemaError>>()?;
211
212        Ok(json_schema::NamespaceDefinition {
213            common_types,
214            entity_types,
215            actions,
216            annotations: n.annotations.into(),
217            #[cfg(feature = "extended-schema")]
218            loc: n.data.loc,
219        })
220    }
221}
222
223/// Converts action type decls
224fn convert_action_decl(
225    a: Annotated<Node<ActionDecl>>,
226) -> Result<impl Iterator<Item = (SmolStr, json_schema::ActionType<RawName>)>, ToJsonSchemaErrors> {
227    let ActionDecl {
228        names,
229        parents,
230        app_decls,
231    } = a.data.node;
232    // Create the internal type from the 'applies_to' clause and 'member_of'
233    let applies_to = app_decls
234        .map(|decls| convert_app_decls(&names.first().node, &names.first().loc, decls))
235        .transpose()?
236        .unwrap_or_else(|| json_schema::ApplySpec {
237            resource_types: vec![],
238            principal_types: vec![],
239            context: json_schema::AttributesOrContext::default(),
240        });
241    let member_of = parents.map(|parents| parents.into_iter().map(convert_qual_name).collect());
242
243    Ok(names.into_iter().map(move |name| {
244        let ty = json_schema::ActionType {
245            attributes: None, // Action attributes are currently unsupported in the Cedar schema format
246            applies_to: Some(applies_to.clone()),
247            member_of: member_of.clone(),
248            annotations: a.annotations.clone().into(),
249            loc: Some(a.data.loc.clone()),
250            #[cfg(feature = "extended-schema")]
251            defn_loc: Some(name.loc),
252        };
253        (name.node, ty)
254    }))
255}
256
257fn convert_qual_name(qn: Node<QualName>) -> json_schema::ActionEntityUID<RawName> {
258    json_schema::ActionEntityUID::new(qn.node.path.map(Into::into), qn.node.eid)
259}
260
261/// Convert the applies to decls
262/// # Arguments
263/// * `name` - The (first) name of the action being declared
264/// * `name_loc` - The location of that first name
265fn convert_app_decls(
266    name: &SmolStr,
267    name_loc: &Loc,
268    decls: Node<NonEmpty<Node<AppDecl>>>,
269) -> Result<json_schema::ApplySpec<RawName>, ToJsonSchemaErrors> {
270    // Split AppDecl's into context/principal/resource decls
271    let (decls, _) = decls.into_inner();
272    let mut principal_types: Option<Node<Vec<RawName>>> = None;
273    let mut resource_types: Option<Node<Vec<RawName>>> = None;
274    let mut context: Option<Node<json_schema::AttributesOrContext<RawName>>> = None;
275
276    for decl in decls {
277        match decl {
278            Node {
279                node: AppDecl::Context(context_decl),
280                loc,
281            } => match context {
282                Some(existing_context) => {
283                    return Err(ToJsonSchemaError::duplicate_context(
284                        name,
285                        existing_context.loc,
286                        loc,
287                    )
288                    .into());
289                }
290                None => {
291                    context = Some(Node::with_source_loc(
292                        convert_context_decl(context_decl),
293                        loc,
294                    ));
295                }
296            },
297            Node {
298                node:
299                    AppDecl::PR(PRAppDecl {
300                        kind:
301                            Node {
302                                node: PR::Principal,
303                                ..
304                            },
305                        entity_tys,
306                    }),
307                loc,
308            } => match principal_types {
309                Some(existing_tys) => {
310                    return Err(ToJsonSchemaError::duplicate_principal(
311                        name,
312                        existing_tys.loc,
313                        loc,
314                    )
315                    .into());
316                }
317                None => match entity_tys {
318                    None => {
319                        return Err(
320                            ToJsonSchemaError::empty_principal(name, name_loc.clone(), loc).into(),
321                        )
322                    }
323                    Some(entity_tys) => {
324                        principal_types = Some(Node::with_source_loc(
325                            entity_tys.iter().map(|n| n.clone().into()).collect(),
326                            loc,
327                        ))
328                    }
329                },
330            },
331            Node {
332                node:
333                    AppDecl::PR(PRAppDecl {
334                        kind:
335                            Node {
336                                node: PR::Resource, ..
337                            },
338                        entity_tys,
339                    }),
340                loc,
341            } => match resource_types {
342                Some(existing_tys) => {
343                    return Err(
344                        ToJsonSchemaError::duplicate_resource(name, existing_tys.loc, loc).into(),
345                    );
346                }
347                None => match entity_tys {
348                    None => {
349                        return Err(
350                            ToJsonSchemaError::empty_resource(name, name_loc.clone(), loc).into(),
351                        )
352                    }
353                    Some(entity_tys) => {
354                        resource_types = Some(Node::with_source_loc(
355                            entity_tys.iter().map(|n| n.clone().into()).collect(),
356                            loc,
357                        ))
358                    }
359                },
360            },
361        }
362    }
363    Ok(json_schema::ApplySpec {
364        resource_types: resource_types
365            .map(|node| node.node)
366            .ok_or_else(|| ToJsonSchemaError::no_resource(&name, name_loc.clone()))?,
367        principal_types: principal_types
368            .map(|node| node.node)
369            .ok_or_else(|| ToJsonSchemaError::no_principal(&name, name_loc.clone()))?,
370        context: context.map(|c| c.node).unwrap_or_default(),
371    })
372}
373
374fn convert_id(node: Node<Id>) -> Result<UnreservedId, ToJsonSchemaError> {
375    UnreservedId::try_from(node.node)
376        .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), node.loc))
377}
378
379/// Convert Entity declarations
380fn convert_entity_decl(
381    e: Annotated<Node<EntityDecl>>,
382) -> Result<
383    impl Iterator<Item = (UnreservedId, json_schema::EntityType<RawName>)>,
384    ToJsonSchemaErrors,
385> {
386    // 2025-02-28: this Clippy nursery lint is bugged, makes a suggestion that does not compile
387    #[allow(clippy::needless_collect)]
388    let names: Vec<Node<Id>> = e.data.node.names().cloned().collect();
389    let etype = json_schema::EntityType {
390        kind: match e.data.node {
391            EntityDecl::Enum(d) => json_schema::EntityTypeKind::Enum {
392                choices: d.choices.map(|n| n.node),
393            },
394            EntityDecl::Standard(d) => {
395                // First build up the defined entity type
396                json_schema::EntityTypeKind::Standard(json_schema::StandardEntityType {
397                    member_of_types: d.member_of_types.into_iter().map(RawName::from).collect(),
398                    shape: convert_attr_decls(d.attrs),
399                    tags: d.tags.map(cedar_type_to_json_type),
400                })
401            }
402        },
403        annotations: e.annotations.into(),
404        loc: Some(e.data.loc.clone()),
405    };
406
407    // Then map over all of the bound names
408    collect_all_errors(
409        names
410            .into_iter()
411            .map(move |name| -> Result<_, ToJsonSchemaErrors> {
412                Ok((convert_id(name)?, etype.clone()))
413            }),
414    )
415}
416
417/// Create a [`json_schema::AttributesOrContext`] from a series of `AttrDecl`s
418fn convert_attr_decls(
419    attrs: Node<impl IntoIterator<Item = Node<Annotated<AttrDecl>>>>,
420) -> json_schema::AttributesOrContext<RawName> {
421    json_schema::AttributesOrContext(json_schema::Type::Type {
422        ty: json_schema::TypeVariant::Record(json_schema::RecordType {
423            attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
424            additional_attributes: false,
425        }),
426        loc: Some(attrs.loc),
427    })
428}
429
430/// Create a context decl
431fn convert_context_decl(
432    decl: Either<Path, Node<Vec<Node<Annotated<AttrDecl>>>>>,
433) -> json_schema::AttributesOrContext<RawName> {
434    json_schema::AttributesOrContext(match decl {
435        Either::Left(p) => json_schema::Type::CommonTypeRef {
436            loc: Some(p.loc().clone()),
437            type_name: p.into(),
438        },
439        Either::Right(attrs) => json_schema::Type::Type {
440            ty: json_schema::TypeVariant::Record(json_schema::RecordType {
441                attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
442                additional_attributes: false,
443            }),
444            loc: Some(attrs.loc),
445        },
446    })
447}
448
449/// Convert an attribute type from an `AttrDecl`
450fn convert_attr_decl(
451    attr: Node<Annotated<AttrDecl>>,
452) -> (SmolStr, json_schema::TypeOfAttribute<RawName>) {
453    (
454        attr.node.data.name.node,
455        json_schema::TypeOfAttribute {
456            ty: cedar_type_to_json_type(attr.node.data.ty),
457            required: attr.node.data.required,
458            annotations: attr.node.annotations.into(),
459            #[cfg(feature = "extended-schema")]
460            loc: Some(attr.loc),
461        },
462    )
463}
464
465/// Takes a collection of results returning multiple errors
466/// Behaves similarly to `::collect()` over results, except instead of failing
467/// on the first error, keeps going to ensure all of the errors are accumulated
468fn collect_all_errors<A, E>(
469    iter: impl IntoIterator<Item = Result<A, E>>,
470) -> Result<impl Iterator<Item = A>, ToJsonSchemaErrors>
471where
472    E: IntoIterator<Item = ToJsonSchemaError>,
473{
474    let mut answers = vec![];
475    let mut errs = vec![];
476    for r in iter.into_iter() {
477        match r {
478            Ok(a) => {
479                answers.push(a);
480            }
481            Err(e) => {
482                let mut v = e.into_iter().collect::<Vec<_>>();
483                errs.append(&mut v)
484            }
485        }
486    }
487    match NonEmpty::collect(errs) {
488        None => Ok(answers.into_iter()),
489        Some(errs) => Err(ToJsonSchemaErrors::new(errs)),
490    }
491}
492
493#[derive(Default)]
494struct NamespaceRecord {
495    entities: HashMap<Id, Node<()>>,
496    common_types: HashMap<Id, Node<()>>,
497    loc: Option<Loc>,
498}
499
500impl NamespaceRecord {
501    fn new(namespace: &Namespace) -> Result<(Option<Name>, Self), ToJsonSchemaErrors> {
502        let ns = namespace
503            .name
504            .clone()
505            .map(|n| {
506                let internal_name = RawName::from(n.clone()).qualify_with(None); // namespace names are already fully-qualified
507                Name::try_from(internal_name)
508                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), n.loc().clone()))
509            })
510            .transpose()?;
511        let (entities, actions, types) = partition_decls(&namespace.decls);
512
513        let entities = collect_decls(
514            entities
515                .into_iter()
516                .flat_map(|decl| decl.names().cloned())
517                .map(extract_name),
518        )?;
519        // Ensure no duplicate actions
520        collect_decls(
521            actions
522                .into_iter()
523                .flat_map(ActionDecl::names)
524                .map(extract_name),
525        )?;
526        let common_types = collect_decls(
527            types
528                .into_iter()
529                .flat_map(|decl| std::iter::once(decl.name.clone()))
530                .map(extract_name),
531        )?;
532
533        let record = NamespaceRecord {
534            entities,
535            common_types,
536            loc: namespace.name.as_ref().map(|n| n.loc().clone()),
537        };
538
539        Ok((ns, record))
540    }
541}
542
543fn collect_decls<N>(
544    i: impl Iterator<Item = (N, Node<()>)>,
545) -> Result<HashMap<N, Node<()>>, ToJsonSchemaErrors>
546where
547    N: std::cmp::Eq + std::hash::Hash + Clone + ToSmolStr,
548{
549    let mut map: HashMap<N, Node<()>> = HashMap::new();
550    for (key, node) in i {
551        match map.entry(key.clone()) {
552            Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_decls(
553                &key,
554                entry.get().loc.clone(),
555                node.loc,
556            )),
557            Entry::Vacant(entry) => {
558                entry.insert(node);
559                Ok(())
560            }
561        }?;
562    }
563    Ok(map)
564}
565
566fn compute_namespace_warnings<'a>(
567    fragment: &'a HashMap<Option<Name>, NamespaceRecord>,
568    extensions: &'a Extensions<'a>,
569) -> impl Iterator<Item = SchemaWarning> + 'a {
570    fragment
571        .values()
572        .flat_map(move |nr| make_warning_for_shadowing(nr, extensions))
573}
574
575fn make_warning_for_shadowing<'a>(
576    n: &'a NamespaceRecord,
577    extensions: &'a Extensions<'a>,
578) -> impl Iterator<Item = SchemaWarning> + 'a {
579    let mut warnings = vec![];
580    for (common_name, common_src_node) in n.common_types.iter() {
581        // Check if it shadows a entity name in the same namespace
582        if let Some(entity_src_node) = n.entities.get(common_name) {
583            let warning = schema_warnings::ShadowsEntityWarning {
584                name: common_name.to_smolstr(),
585                entity_loc: entity_src_node.loc.clone(),
586                common_loc: common_src_node.loc.clone(),
587            }
588            .into();
589            warnings.push(warning);
590        }
591        // Check if it shadows a builtin
592        if let Some(warning) = shadows_builtin(common_name, common_src_node, extensions) {
593            warnings.push(warning);
594        }
595    }
596    let entity_shadows = n
597        .entities
598        .iter()
599        .filter_map(move |(name, node)| shadows_builtin(name, node, extensions));
600    warnings.into_iter().chain(entity_shadows)
601}
602
603fn extract_name<N: Clone>(n: Node<N>) -> (N, Node<()>) {
604    (n.node.clone(), n.map(|_| ()))
605}
606
607fn shadows_builtin(
608    name: &Id,
609    node: &Node<()>,
610    extensions: &Extensions<'_>,
611) -> Option<SchemaWarning> {
612    if is_valid_ext_type(name, extensions) || BUILTIN_TYPES.contains(&name.as_ref()) {
613        Some(
614            schema_warnings::ShadowsBuiltinWarning {
615                name: name.to_smolstr(),
616                loc: node.loc.clone(),
617            }
618            .into(),
619        )
620    } else {
621        None
622    }
623}
624
625// Essentially index `NamespaceRecord`s by the namespace
626fn build_namespace_bindings<'a>(
627    namespaces: impl Iterator<Item = &'a Namespace>,
628) -> Result<HashMap<Option<Name>, NamespaceRecord>, ToJsonSchemaErrors> {
629    let mut map = HashMap::new();
630    for (name, record) in collect_all_errors(namespaces.map(NamespaceRecord::new))? {
631        update_namespace_record(&mut map, name, record)?;
632    }
633    Ok(map)
634}
635
636fn update_namespace_record(
637    map: &mut HashMap<Option<Name>, NamespaceRecord>,
638    name: Option<Name>,
639    record: NamespaceRecord,
640) -> Result<(), ToJsonSchemaErrors> {
641    match map.entry(name.clone()) {
642        Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_namespace(
643            &name.map_or("".into(), |n| n.to_smolstr()),
644            record.loc,
645            entry.get().loc.clone(),
646        )
647        .into()),
648        Entry::Vacant(entry) => {
649            entry.insert(record);
650            Ok(())
651        }
652    }
653}
654
655fn partition_decls(
656    decls: &[Annotated<Node<Declaration>>],
657) -> (Vec<&EntityDecl>, Vec<&ActionDecl>, Vec<&TypeDecl>) {
658    let mut entities = vec![];
659    let mut actions = vec![];
660    let mut types = vec![];
661
662    for decl in decls.iter() {
663        match &decl.data.node {
664            Declaration::Entity(e) => entities.push(e),
665            Declaration::Action(a) => actions.push(a),
666            Declaration::Type(t) => types.push(t),
667        }
668    }
669
670    (entities, actions, types)
671}
672
673#[allow(clippy::type_complexity)]
674fn into_partition_decls(
675    decls: impl IntoIterator<Item = Annotated<Node<Declaration>>>,
676) -> (
677    Vec<Annotated<Node<EntityDecl>>>,
678    Vec<Annotated<Node<ActionDecl>>>,
679    Vec<Annotated<Node<TypeDecl>>>,
680) {
681    let mut entities = vec![];
682    let mut actions = vec![];
683    let mut types = vec![];
684
685    for decl in decls.into_iter() {
686        let loc = decl.data.loc;
687        match decl.data.node {
688            Declaration::Entity(e) => entities.push(Annotated {
689                data: Node { node: e, loc },
690                annotations: decl.annotations,
691            }),
692            Declaration::Action(a) => actions.push(Annotated {
693                data: Node { node: a, loc },
694                annotations: decl.annotations,
695            }),
696            Declaration::Type(t) => types.push(Annotated {
697                data: Node { node: t, loc },
698                annotations: decl.annotations,
699            }),
700        }
701    }
702
703    (entities, actions, types)
704}
705
706#[cfg(test)]
707mod preserves_source_locations {
708    use super::*;
709    use cool_asserts::assert_matches;
710    use json_schema::{EntityType, EntityTypeKind};
711
712    #[test]
713    #[allow(clippy::cognitive_complexity)]
714    fn entity_action_and_common_type_decls() {
715        let (schema, _) = json_schema::Fragment::from_cedarschema_str(
716            r#"
717        namespace NS {
718            type S = String;
719            entity A;
720            entity B in A;
721            entity C in A {
722                bool: Bool,
723                s: S,
724                a: Set<A>,
725                b: { inner: B },
726            };
727            type AA = A;
728            action Read, Write;
729            action List in Read appliesTo {
730                principal: [A],
731                resource: [B, C],
732                context: {
733                    s: Set<S>,
734                    ab: { a: AA, b: B },
735                }
736            };
737        }
738        "#,
739            Extensions::all_available(),
740        )
741        .unwrap();
742        let ns = schema
743            .0
744            .get(&Some(Name::parse_unqualified_name("NS").unwrap()))
745            .expect("couldn't find namespace NS");
746
747        let entity_a = ns
748            .entity_types
749            .get(&"A".parse().unwrap())
750            .expect("couldn't find entity A");
751        let entity_b = ns
752            .entity_types
753            .get(&"B".parse().unwrap())
754            .expect("couldn't find entity B");
755        let entity_c = ns
756            .entity_types
757            .get(&"C".parse().unwrap())
758            .expect("couldn't find entity C");
759        let ctype_s = ns
760            .common_types
761            .get(&json_schema::CommonTypeId::new("S".parse().unwrap()).unwrap())
762            .expect("couldn't find common type S");
763        let ctype_aa = ns
764            .common_types
765            .get(&json_schema::CommonTypeId::new("AA".parse().unwrap()).unwrap())
766            .expect("couldn't find common type AA");
767        let action_read = ns.actions.get("Read").expect("couldn't find action Read");
768        let action_write = ns.actions.get("Write").expect("couldn't find action Write");
769        let action_list = ns.actions.get("List").expect("couldn't find action List");
770
771        assert_matches!(&entity_a.loc, Some(loc) => assert_matches!(loc.snippet(),
772            Some("entity A;")
773        ));
774        assert_matches!(&entity_b.loc, Some(loc) => assert_matches!(loc.snippet(),
775            Some("entity B in A;")
776        ));
777        assert_matches!(&entity_c.loc, Some(loc) => assert_matches!(loc.snippet(),
778            Some("entity C in A {\n                bool: Bool,\n                s: S,\n                a: Set<A>,\n                b: { inner: B },\n            };")
779        ));
780        assert_matches!(&ctype_s.loc, Some(loc) => assert_matches!(loc.snippet(),
781            Some("type S = String;")
782        ));
783        assert_matches!(&ctype_aa.loc, Some(loc) => assert_matches!(loc.snippet(),
784            Some("type AA = A;")
785        ));
786        assert_matches!(&action_read.loc, Some(loc) => assert_matches!(loc.snippet(),
787            Some("action Read, Write;")
788        ));
789        assert_matches!(&action_write.loc, Some(loc) => assert_matches!(loc.snippet(),
790            Some("action Read, Write;")
791        ));
792        assert_matches!(&action_list.loc, Some(loc) => assert_matches!(loc.snippet(),
793            Some("action List in Read appliesTo {\n                principal: [A],\n                resource: [B, C],\n                context: {\n                    s: Set<S>,\n                    ab: { a: AA, b: B },\n                }\n            };")
794        ));
795    }
796
797    #[test]
798    #[allow(clippy::cognitive_complexity)]
799    fn types() {
800        let (schema, _) = json_schema::Fragment::from_cedarschema_str(
801            r#"
802        namespace NS {
803            type S = String;
804            entity A;
805            entity B in A;
806            entity C in A {
807                bool: Bool,
808                s: S,
809                a: Set<A>,
810                b: { inner: B },
811            };
812            type AA = A;
813            action Read, Write;
814            action List in Read appliesTo {
815                principal: [A],
816                resource: [B, C],
817                context: {
818                    s: Set<S>,
819                    ab: { a: AA, b: B },
820                }
821            };
822        }
823        "#,
824            Extensions::all_available(),
825        )
826        .unwrap();
827        let ns = schema
828            .0
829            .get(&Some(Name::parse_unqualified_name("NS").unwrap()))
830            .expect("couldn't find namespace NS");
831
832        assert_matches!(ns
833            .entity_types
834            .get(&"C".parse().unwrap())
835            .expect("couldn't find entity C"), EntityType { kind: EntityTypeKind::Standard(entityC), ..} => {
836        assert_matches!(entityC.member_of_types.first().unwrap().loc(), Some(loc) => {
837            assert_matches!(loc.snippet(), Some("A"));
838        });
839        assert_matches!(entityC.shape.0.loc(), Some(loc) => {
840            assert_matches!(loc.snippet(), Some("{\n                bool: Bool,\n                s: S,\n                a: Set<A>,\n                b: { inner: B },\n            }"));
841        });
842        assert_matches!(&entityC.shape.0, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(rty), .. } => {
843            let b = rty.attributes.get("bool").expect("couldn't find attribute `bool` on entity C");
844            assert_matches!(b.ty.loc(), Some(loc) => {
845                assert_matches!(loc.snippet(), Some("Bool"));
846            });
847            let s = rty.attributes.get("s").expect("couldn't find attribute `s` on entity C");
848            assert_matches!(s.ty.loc(), Some(loc) => {
849                assert_matches!(loc.snippet(), Some("S"));
850            });
851            let a = rty.attributes.get("a").expect("couldn't find attribute `a` on entity C");
852            assert_matches!(a.ty.loc(), Some(loc) => {
853                assert_matches!(loc.snippet(), Some("Set<A>"));
854            });
855            assert_matches!(&a.ty, json_schema::Type::Type { ty: json_schema::TypeVariant::Set { element }, .. } => {
856                assert_matches!(element.loc(), Some(loc) => {
857                    assert_matches!(loc.snippet(), Some("A"));
858                });
859            });
860            let b = rty.attributes.get("b").expect("couldn't find attribute `b` on entity C");
861            assert_matches!(b.ty.loc(), Some(loc) => {
862                assert_matches!(loc.snippet(), Some("{ inner: B }"));
863            });
864            assert_matches!(&b.ty, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(b_rty), .. } => {
865                let inner = b_rty.attributes.get("inner").expect("couldn't find inner attribute");
866                assert_matches!(inner.ty.loc(), Some(loc) => {
867                    assert_matches!(loc.snippet(), Some("B"));
868                });
869            });
870        });});
871
872        let ctype_aa = ns
873            .common_types
874            .get(&json_schema::CommonTypeId::new("AA".parse().unwrap()).unwrap())
875            .expect("couldn't find common type AA");
876        assert_matches!(ctype_aa.ty.loc(), Some(loc) => {
877            assert_matches!(loc.snippet(), Some("A"));
878        });
879
880        let action_list = ns.actions.get("List").expect("couldn't find action List");
881        assert_matches!(&action_list.applies_to, Some(appliesto) => {
882            assert_matches!(appliesto.principal_types.first().expect("principal types were empty").loc(), Some(loc) => {
883                assert_matches!(loc.snippet(), Some("A"));
884            });
885            assert_matches!(appliesto.resource_types.first().expect("resource types were empty").loc(), Some(loc) => {
886                assert_matches!(loc.snippet(), Some("B"));
887            });
888            assert_matches!(appliesto.context.loc(), Some(loc) => {
889                assert_matches!(loc.snippet(), Some("{\n                    s: Set<S>,\n                    ab: { a: AA, b: B },\n                }"));
890            });
891            assert_matches!(&appliesto.context.0, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(rty), .. } => {
892                let s = rty.attributes.get("s").expect("couldn't find attribute `s` on context");
893                assert_matches!(s.ty.loc(), Some(loc) => {
894                    assert_matches!(loc.snippet(), Some("Set<S>"));
895                });
896                let ab = rty.attributes.get("ab").expect("couldn't find attribute `ab` on context");
897                assert_matches!(ab.ty.loc(), Some(loc) => {
898                    assert_matches!(loc.snippet(), Some("{ a: AA, b: B }"));
899                });
900            });
901        });
902    }
903}