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        };
144        (
145            qualified.into_iter(),
146            Some(Annotated {
147                data: unqual,
148                annotations: Annotations::new(),
149            }),
150        )
151    }
152}
153
154/// Converts a CST namespace to a JSON namespace
155fn convert_namespace(
156    namespace: Annotated<Namespace>,
157) -> Result<(Option<Name>, json_schema::NamespaceDefinition<RawName>), ToJsonSchemaErrors> {
158    let ns_name = namespace
159        .data
160        .name
161        .clone()
162        .map(|p| {
163            let internal_name = RawName::from(p.clone()).qualify_with(None); // namespace names are always written already-fully-qualified in the Cedar schema syntax
164            Name::try_from(internal_name)
165                .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), p.loc().clone()))
166        })
167        .transpose()?;
168    let def = namespace.try_into()?;
169    Ok((ns_name, def))
170}
171
172impl TryFrom<Annotated<Namespace>> for json_schema::NamespaceDefinition<RawName> {
173    type Error = ToJsonSchemaErrors;
174
175    fn try_from(
176        n: Annotated<Namespace>,
177    ) -> Result<json_schema::NamespaceDefinition<RawName>, Self::Error> {
178        // Partition the decls into entities, actions, and common types
179        let (entity_types, action, common_types) = into_partition_decls(n.data.decls);
180
181        // Convert entity type decls, collecting all errors
182        let entity_types = collect_all_errors(entity_types.into_iter().map(convert_entity_decl))?
183            .flatten()
184            .collect();
185
186        // Convert action decls, collecting all errors
187        let actions = collect_all_errors(action.into_iter().map(convert_action_decl))?
188            .flatten()
189            .collect();
190
191        // Convert common type decls
192        let common_types = common_types
193            .into_iter()
194            .map(|decl| {
195                let name_loc = decl.data.node.name.loc.clone();
196                let id = UnreservedId::try_from(decl.data.node.name.node)
197                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), name_loc.clone()))?;
198                let ctid = json_schema::CommonTypeId::new(id)
199                    .map_err(|e| ToJsonSchemaError::reserved_keyword(&e.id, name_loc))?;
200                Ok((
201                    ctid,
202                    CommonType {
203                        ty: cedar_type_to_json_type(decl.data.node.def),
204                        annotations: decl.annotations.into(),
205                        loc: Some(decl.data.loc),
206                    },
207                ))
208            })
209            .collect::<Result<_, ToJsonSchemaError>>()?;
210
211        Ok(json_schema::NamespaceDefinition {
212            common_types,
213            entity_types,
214            actions,
215            annotations: n.annotations.into(),
216        })
217    }
218}
219
220/// Converts action type decls
221fn convert_action_decl(
222    a: Annotated<Node<ActionDecl>>,
223) -> Result<impl Iterator<Item = (SmolStr, json_schema::ActionType<RawName>)>, ToJsonSchemaErrors> {
224    let ActionDecl {
225        names,
226        parents,
227        app_decls,
228    } = a.data.node;
229    // Create the internal type from the 'applies_to' clause and 'member_of'
230    let applies_to = app_decls
231        .map(|decls| convert_app_decls(&names.first().node, &names.first().loc, decls))
232        .transpose()?
233        .unwrap_or_else(|| json_schema::ApplySpec {
234            resource_types: vec![],
235            principal_types: vec![],
236            context: json_schema::AttributesOrContext::default(),
237        });
238    let member_of = parents.map(|parents| parents.into_iter().map(convert_qual_name).collect());
239    let ty = json_schema::ActionType {
240        attributes: None, // Action attributes are currently unsupported in the Cedar schema format
241        applies_to: Some(applies_to),
242        member_of,
243        annotations: a.annotations.into(),
244        loc: Some(a.data.loc),
245    };
246    // Then map that type across all of the bound names
247    Ok(names.into_iter().map(move |name| (name.node, ty.clone())))
248}
249
250fn convert_qual_name(qn: Node<QualName>) -> json_schema::ActionEntityUID<RawName> {
251    json_schema::ActionEntityUID::new(qn.node.path.map(Into::into), qn.node.eid)
252}
253
254/// Convert the applies to decls
255/// # Arguments
256/// * `name` - The (first) name of the action being declared
257/// * `name_loc` - The location of that first name
258fn convert_app_decls(
259    name: &SmolStr,
260    name_loc: &Loc,
261    decls: Node<NonEmpty<Node<AppDecl>>>,
262) -> Result<json_schema::ApplySpec<RawName>, ToJsonSchemaErrors> {
263    // Split AppDecl's into context/principal/resource decls
264    let (decls, _) = decls.into_inner();
265    let mut principal_types: Option<Node<Vec<RawName>>> = None;
266    let mut resource_types: Option<Node<Vec<RawName>>> = None;
267    let mut context: Option<Node<json_schema::AttributesOrContext<RawName>>> = None;
268
269    for decl in decls {
270        match decl {
271            Node {
272                node: AppDecl::Context(context_decl),
273                loc,
274            } => match context {
275                Some(existing_context) => {
276                    return Err(ToJsonSchemaError::duplicate_context(
277                        name,
278                        existing_context.loc,
279                        loc,
280                    )
281                    .into());
282                }
283                None => {
284                    context = Some(Node::with_source_loc(
285                        convert_context_decl(context_decl),
286                        loc,
287                    ));
288                }
289            },
290            Node {
291                node:
292                    AppDecl::PR(PRAppDecl {
293                        kind:
294                            Node {
295                                node: PR::Principal,
296                                ..
297                            },
298                        entity_tys,
299                    }),
300                loc,
301            } => match principal_types {
302                Some(existing_tys) => {
303                    return Err(ToJsonSchemaError::duplicate_principal(
304                        name,
305                        existing_tys.loc,
306                        loc,
307                    )
308                    .into());
309                }
310                None => {
311                    principal_types = Some(Node::with_source_loc(
312                        entity_tys.iter().map(|n| n.clone().into()).collect(),
313                        loc,
314                    ))
315                }
316            },
317            Node {
318                node:
319                    AppDecl::PR(PRAppDecl {
320                        kind:
321                            Node {
322                                node: PR::Resource, ..
323                            },
324                        entity_tys,
325                    }),
326                loc,
327            } => match resource_types {
328                Some(existing_tys) => {
329                    return Err(
330                        ToJsonSchemaError::duplicate_resource(name, existing_tys.loc, loc).into(),
331                    );
332                }
333                None => {
334                    resource_types = Some(Node::with_source_loc(
335                        entity_tys.iter().map(|n| n.clone().into()).collect(),
336                        loc,
337                    ))
338                }
339            },
340        }
341    }
342    Ok(json_schema::ApplySpec {
343        resource_types: resource_types
344            .map(|node| node.node)
345            .ok_or_else(|| ToJsonSchemaError::no_resource(&name, name_loc.clone()))?,
346        principal_types: principal_types
347            .map(|node| node.node)
348            .ok_or_else(|| ToJsonSchemaError::no_principal(&name, name_loc.clone()))?,
349        context: context.map(|c| c.node).unwrap_or_default(),
350    })
351}
352
353fn convert_id(node: Node<Id>) -> Result<UnreservedId, ToJsonSchemaError> {
354    UnreservedId::try_from(node.node)
355        .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), node.loc))
356}
357
358/// Convert Entity declarations
359fn convert_entity_decl(
360    e: Annotated<Node<EntityDecl>>,
361) -> Result<
362    impl Iterator<Item = (UnreservedId, json_schema::EntityType<RawName>)>,
363    ToJsonSchemaErrors,
364> {
365    // First build up the defined entity type
366    let etype = json_schema::EntityType {
367        member_of_types: e
368            .data
369            .node
370            .member_of_types
371            .into_iter()
372            .map(RawName::from)
373            .collect(),
374        shape: convert_attr_decls(e.data.node.attrs),
375        tags: e.data.node.tags.map(cedar_type_to_json_type),
376        annotations: e.annotations.into(),
377        loc: Some(e.data.loc),
378    };
379
380    // Then map over all of the bound names
381    collect_all_errors(e.data.node.names.into_iter().map(
382        move |name| -> Result<_, ToJsonSchemaErrors> { Ok((convert_id(name)?, etype.clone())) },
383    ))
384}
385
386/// Create a [`json_schema::AttributesOrContext`] from a series of `AttrDecl`s
387fn convert_attr_decls(
388    attrs: Node<impl IntoIterator<Item = Node<Annotated<AttrDecl>>>>,
389) -> json_schema::AttributesOrContext<RawName> {
390    json_schema::AttributesOrContext(json_schema::Type::Type {
391        ty: json_schema::TypeVariant::Record(json_schema::RecordType {
392            attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
393            additional_attributes: false,
394        }),
395        loc: Some(attrs.loc),
396    })
397}
398
399/// Create a context decl
400fn convert_context_decl(
401    decl: Either<Path, Node<Vec<Node<Annotated<AttrDecl>>>>>,
402) -> json_schema::AttributesOrContext<RawName> {
403    json_schema::AttributesOrContext(match decl {
404        Either::Left(p) => json_schema::Type::CommonTypeRef {
405            loc: Some(p.loc().clone()),
406            type_name: p.into(),
407        },
408        Either::Right(attrs) => json_schema::Type::Type {
409            ty: json_schema::TypeVariant::Record(json_schema::RecordType {
410                attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
411                additional_attributes: false,
412            }),
413            loc: Some(attrs.loc),
414        },
415    })
416}
417
418/// Convert an attribute type from an `AttrDecl`
419fn convert_attr_decl(
420    attr: Node<Annotated<AttrDecl>>,
421) -> (SmolStr, json_schema::TypeOfAttribute<RawName>) {
422    (
423        attr.node.data.name.node,
424        json_schema::TypeOfAttribute {
425            ty: cedar_type_to_json_type(attr.node.data.ty),
426            required: attr.node.data.required,
427            annotations: attr.node.annotations.into(),
428        },
429    )
430}
431
432/// Takes a collection of results returning multiple errors
433/// Behaves similarly to `::collect()` over results, except instead of failing
434/// on the first error, keeps going to ensure all of the errors are accumulated
435fn collect_all_errors<A, E>(
436    iter: impl IntoIterator<Item = Result<A, E>>,
437) -> Result<impl Iterator<Item = A>, ToJsonSchemaErrors>
438where
439    E: IntoIterator<Item = ToJsonSchemaError>,
440{
441    let mut answers = vec![];
442    let mut errs = vec![];
443    for r in iter.into_iter() {
444        match r {
445            Ok(a) => {
446                answers.push(a);
447            }
448            Err(e) => {
449                let mut v = e.into_iter().collect::<Vec<_>>();
450                errs.append(&mut v)
451            }
452        }
453    }
454    match NonEmpty::collect(errs) {
455        None => Ok(answers.into_iter()),
456        Some(errs) => Err(ToJsonSchemaErrors::new(errs)),
457    }
458}
459
460#[derive(Default)]
461struct NamespaceRecord {
462    entities: HashMap<Id, Node<()>>,
463    common_types: HashMap<Id, Node<()>>,
464    loc: Option<Loc>,
465}
466
467impl NamespaceRecord {
468    fn new(namespace: &Namespace) -> Result<(Option<Name>, Self), ToJsonSchemaErrors> {
469        let ns = namespace
470            .name
471            .clone()
472            .map(|n| {
473                let internal_name = RawName::from(n.clone()).qualify_with(None); // namespace names are already fully-qualified
474                Name::try_from(internal_name)
475                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), n.loc().clone()))
476            })
477            .transpose()?;
478        let (entities, actions, types) = partition_decls(&namespace.decls);
479
480        let entities = collect_decls(
481            entities
482                .into_iter()
483                .flat_map(|decl| decl.names.clone())
484                .map(extract_name),
485        )?;
486        // Ensure no duplicate actions
487        collect_decls(
488            actions
489                .into_iter()
490                .flat_map(ActionDecl::names)
491                .map(extract_name),
492        )?;
493        let common_types = collect_decls(
494            types
495                .into_iter()
496                .flat_map(|decl| std::iter::once(decl.name.clone()))
497                .map(extract_name),
498        )?;
499
500        let record = NamespaceRecord {
501            entities,
502            common_types,
503            loc: namespace.name.as_ref().map(|n| n.loc().clone()),
504        };
505
506        Ok((ns, record))
507    }
508}
509
510fn collect_decls<N>(
511    i: impl Iterator<Item = (N, Node<()>)>,
512) -> Result<HashMap<N, Node<()>>, ToJsonSchemaErrors>
513where
514    N: std::cmp::Eq + std::hash::Hash + Clone + ToSmolStr,
515{
516    let mut map: HashMap<N, Node<()>> = HashMap::new();
517    for (key, node) in i {
518        match map.entry(key.clone()) {
519            Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_decls(
520                &key,
521                entry.get().loc.clone(),
522                node.loc,
523            )),
524            Entry::Vacant(entry) => {
525                entry.insert(node);
526                Ok(())
527            }
528        }?;
529    }
530    Ok(map)
531}
532
533fn compute_namespace_warnings<'a>(
534    fragment: &'a HashMap<Option<Name>, NamespaceRecord>,
535    extensions: &'a Extensions<'a>,
536) -> impl Iterator<Item = SchemaWarning> + 'a {
537    fragment
538        .values()
539        .flat_map(move |nr| make_warning_for_shadowing(nr, extensions))
540}
541
542fn make_warning_for_shadowing<'a>(
543    n: &'a NamespaceRecord,
544    extensions: &'a Extensions<'a>,
545) -> impl Iterator<Item = SchemaWarning> + 'a {
546    let mut warnings = vec![];
547    for (common_name, common_src_node) in n.common_types.iter() {
548        // Check if it shadows a entity name in the same namespace
549        if let Some(entity_src_node) = n.entities.get(common_name) {
550            let warning = schema_warnings::ShadowsEntityWarning {
551                name: common_name.to_smolstr(),
552                entity_loc: entity_src_node.loc.clone(),
553                common_loc: common_src_node.loc.clone(),
554            }
555            .into();
556            warnings.push(warning);
557        }
558        // Check if it shadows a builtin
559        if let Some(warning) = shadows_builtin(common_name, common_src_node, extensions) {
560            warnings.push(warning);
561        }
562    }
563    let entity_shadows = n
564        .entities
565        .iter()
566        .filter_map(move |(name, node)| shadows_builtin(name, node, extensions));
567    warnings.into_iter().chain(entity_shadows)
568}
569
570fn extract_name<N: Clone>(n: Node<N>) -> (N, Node<()>) {
571    (n.node.clone(), n.map(|_| ()))
572}
573
574fn shadows_builtin(
575    name: &Id,
576    node: &Node<()>,
577    extensions: &Extensions<'_>,
578) -> Option<SchemaWarning> {
579    if is_valid_ext_type(name, extensions) || BUILTIN_TYPES.contains(&name.as_ref()) {
580        Some(
581            schema_warnings::ShadowsBuiltinWarning {
582                name: name.to_smolstr(),
583                loc: node.loc.clone(),
584            }
585            .into(),
586        )
587    } else {
588        None
589    }
590}
591
592// Essentially index `NamespaceRecord`s by the namespace
593fn build_namespace_bindings<'a>(
594    namespaces: impl Iterator<Item = &'a Namespace>,
595) -> Result<HashMap<Option<Name>, NamespaceRecord>, ToJsonSchemaErrors> {
596    let mut map = HashMap::new();
597    for (name, record) in collect_all_errors(namespaces.map(NamespaceRecord::new))? {
598        update_namespace_record(&mut map, name, record)?;
599    }
600    Ok(map)
601}
602
603fn update_namespace_record(
604    map: &mut HashMap<Option<Name>, NamespaceRecord>,
605    name: Option<Name>,
606    record: NamespaceRecord,
607) -> Result<(), ToJsonSchemaErrors> {
608    match map.entry(name.clone()) {
609        Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_namespace(
610            &name.map_or("".into(), |n| n.to_smolstr()),
611            record.loc,
612            entry.get().loc.clone(),
613        )
614        .into()),
615        Entry::Vacant(entry) => {
616            entry.insert(record);
617            Ok(())
618        }
619    }
620}
621
622fn partition_decls(
623    decls: &[Annotated<Node<Declaration>>],
624) -> (Vec<&EntityDecl>, Vec<&ActionDecl>, Vec<&TypeDecl>) {
625    let mut entities = vec![];
626    let mut actions = vec![];
627    let mut types = vec![];
628
629    for decl in decls.iter() {
630        match &decl.data.node {
631            Declaration::Entity(e) => entities.push(e),
632            Declaration::Action(a) => actions.push(a),
633            Declaration::Type(t) => types.push(t),
634        }
635    }
636
637    (entities, actions, types)
638}
639
640fn into_partition_decls(
641    decls: impl IntoIterator<Item = Annotated<Node<Declaration>>>,
642) -> (
643    Vec<Annotated<Node<EntityDecl>>>,
644    Vec<Annotated<Node<ActionDecl>>>,
645    Vec<Annotated<Node<TypeDecl>>>,
646) {
647    let mut entities = vec![];
648    let mut actions = vec![];
649    let mut types = vec![];
650
651    for decl in decls.into_iter() {
652        let loc = decl.data.loc;
653        match decl.data.node {
654            Declaration::Entity(e) => entities.push(Annotated {
655                data: Node { node: e, loc },
656                annotations: decl.annotations,
657            }),
658            Declaration::Action(a) => actions.push(Annotated {
659                data: Node { node: a, loc },
660                annotations: decl.annotations,
661            }),
662            Declaration::Type(t) => types.push(Annotated {
663                data: Node { node: t, loc },
664                annotations: decl.annotations,
665            }),
666        }
667    }
668
669    (entities, actions, types)
670}
671
672#[cfg(test)]
673mod preserves_source_locations {
674    use super::*;
675    use cool_asserts::assert_matches;
676
677    #[test]
678    fn entity_action_and_common_type_decls() {
679        let (schema, _) = json_schema::Fragment::from_cedarschema_str(
680            r#"
681        namespace NS {
682            type S = String;
683            entity A;
684            entity B in A;
685            entity C in A {
686                bool: Bool,
687                s: S,
688                a: Set<A>,
689                b: { inner: B },
690            };
691            type AA = A;
692            action Read, Write;
693            action List in Read appliesTo {
694                principal: [A],
695                resource: [B, C],
696                context: {
697                    s: Set<S>,
698                    ab: { a: AA, b: B },
699                }
700            };
701        }
702        "#,
703            Extensions::all_available(),
704        )
705        .unwrap();
706        let ns = schema
707            .0
708            .get(&Some(Name::parse_unqualified_name("NS").unwrap()))
709            .expect("couldn't find namespace NS");
710
711        let entityA = ns
712            .entity_types
713            .get(&"A".parse().unwrap())
714            .expect("couldn't find entity A");
715        let entityB = ns
716            .entity_types
717            .get(&"B".parse().unwrap())
718            .expect("couldn't find entity B");
719        let entityC = ns
720            .entity_types
721            .get(&"C".parse().unwrap())
722            .expect("couldn't find entity C");
723        let ctypeS = ns
724            .common_types
725            .get(&json_schema::CommonTypeId::new("S".parse().unwrap()).unwrap())
726            .expect("couldn't find common type S");
727        let ctypeAA = ns
728            .common_types
729            .get(&json_schema::CommonTypeId::new("AA".parse().unwrap()).unwrap())
730            .expect("couldn't find common type AA");
731        let actionRead = ns.actions.get("Read").expect("couldn't find action Read");
732        let actionWrite = ns.actions.get("Write").expect("couldn't find action Write");
733        let actionList = ns.actions.get("List").expect("couldn't find action List");
734
735        assert_matches!(&entityA.loc, Some(loc) => assert_matches!(loc.snippet(),
736            Some("entity A;")
737        ));
738        assert_matches!(&entityB.loc, Some(loc) => assert_matches!(loc.snippet(),
739            Some("entity B in A;")
740        ));
741        assert_matches!(&entityC.loc, Some(loc) => assert_matches!(loc.snippet(),
742            Some("entity C in A {\n                bool: Bool,\n                s: S,\n                a: Set<A>,\n                b: { inner: B },\n            };")
743        ));
744        assert_matches!(&ctypeS.loc, Some(loc) => assert_matches!(loc.snippet(),
745            Some("type S = String;")
746        ));
747        assert_matches!(&ctypeAA.loc, Some(loc) => assert_matches!(loc.snippet(),
748            Some("type AA = A;")
749        ));
750        assert_matches!(&actionRead.loc, Some(loc) => assert_matches!(loc.snippet(),
751            Some("action Read, Write;")
752        ));
753        assert_matches!(&actionWrite.loc, Some(loc) => assert_matches!(loc.snippet(),
754            Some("action Read, Write;")
755        ));
756        assert_matches!(&actionList.loc, Some(loc) => assert_matches!(loc.snippet(),
757            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            };")
758        ));
759    }
760
761    #[test]
762    fn types() {
763        let (schema, _) = json_schema::Fragment::from_cedarschema_str(
764            r#"
765        namespace NS {
766            type S = String;
767            entity A;
768            entity B in A;
769            entity C in A {
770                bool: Bool,
771                s: S,
772                a: Set<A>,
773                b: { inner: B },
774            };
775            type AA = A;
776            action Read, Write;
777            action List in Read appliesTo {
778                principal: [A],
779                resource: [B, C],
780                context: {
781                    s: Set<S>,
782                    ab: { a: AA, b: B },
783                }
784            };
785        }
786        "#,
787            Extensions::all_available(),
788        )
789        .unwrap();
790        let ns = schema
791            .0
792            .get(&Some(Name::parse_unqualified_name("NS").unwrap()))
793            .expect("couldn't find namespace NS");
794
795        let entityC = ns
796            .entity_types
797            .get(&"C".parse().unwrap())
798            .expect("couldn't find entity C");
799        assert_matches!(entityC.member_of_types.first().unwrap().loc(), Some(loc) => {
800            assert_matches!(loc.snippet(), Some("A"));
801        });
802        assert_matches!(entityC.shape.0.loc(), Some(loc) => {
803            assert_matches!(loc.snippet(), Some("{\n                bool: Bool,\n                s: S,\n                a: Set<A>,\n                b: { inner: B },\n            }"));
804        });
805        assert_matches!(&entityC.shape.0, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(rty), .. } => {
806            let b = rty.attributes.get("bool").expect("couldn't find attribute `bool` on entity C");
807            assert_matches!(b.ty.loc(), Some(loc) => {
808                assert_matches!(loc.snippet(), Some("Bool"));
809            });
810            let s = rty.attributes.get("s").expect("couldn't find attribute `s` on entity C");
811            assert_matches!(s.ty.loc(), Some(loc) => {
812                assert_matches!(loc.snippet(), Some("S"));
813            });
814            let a = rty.attributes.get("a").expect("couldn't find attribute `a` on entity C");
815            assert_matches!(a.ty.loc(), Some(loc) => {
816                assert_matches!(loc.snippet(), Some("Set<A>"));
817            });
818            assert_matches!(&a.ty, json_schema::Type::Type { ty: json_schema::TypeVariant::Set { element }, .. } => {
819                assert_matches!(element.loc(), Some(loc) => {
820                    assert_matches!(loc.snippet(), Some("A"));
821                });
822            });
823            let b = rty.attributes.get("b").expect("couldn't find attribute `b` on entity C");
824            assert_matches!(b.ty.loc(), Some(loc) => {
825                assert_matches!(loc.snippet(), Some("{ inner: B }"));
826            });
827            assert_matches!(&b.ty, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(b_rty), .. } => {
828                let inner = b_rty.attributes.get("inner").expect("couldn't find inner attribute");
829                assert_matches!(inner.ty.loc(), Some(loc) => {
830                    assert_matches!(loc.snippet(), Some("B"));
831                });
832            });
833        });
834
835        let ctypeAA = ns
836            .common_types
837            .get(&json_schema::CommonTypeId::new("AA".parse().unwrap()).unwrap())
838            .expect("couldn't find common type AA");
839        assert_matches!(ctypeAA.ty.loc(), Some(loc) => {
840            assert_matches!(loc.snippet(), Some("A"));
841        });
842
843        let actionList = ns.actions.get("List").expect("couldn't find action List");
844        assert_matches!(&actionList.applies_to, Some(appliesto) => {
845            assert_matches!(appliesto.principal_types.first().expect("principal types were empty").loc(), Some(loc) => {
846                assert_matches!(loc.snippet(), Some("A"));
847            });
848            assert_matches!(appliesto.resource_types.first().expect("resource types were empty").loc(), Some(loc) => {
849                assert_matches!(loc.snippet(), Some("B"));
850            });
851            assert_matches!(appliesto.context.loc(), Some(loc) => {
852                assert_matches!(loc.snippet(), Some("{\n                    s: Set<S>,\n                    ab: { a: AA, b: B },\n                }"));
853            });
854            assert_matches!(&appliesto.context.0, json_schema::Type::Type { ty: json_schema::TypeVariant::Record(rty), .. } => {
855                let s = rty.attributes.get("s").expect("couldn't find attribute `s` on context");
856                assert_matches!(s.ty.loc(), Some(loc) => {
857                    assert_matches!(loc.snippet(), Some("Set<S>"));
858                });
859                let ab = rty.attributes.get("ab").expect("couldn't find attribute `ab` on context");
860                assert_matches!(ab.ty.loc(), Some(loc) => {
861                    assert_matches!(loc.snippet(), Some("{ a: AA, b: B }"));
862                });
863            });
864        });
865    }
866}