Skip to main content

simple_zanzibar/schema/
mod.rs

1//! Typed schema IR, resolver, compiler, and validator.
2
3use std::{
4    collections::{HashMap, HashSet},
5    slice,
6    sync::Arc,
7};
8
9use thiserror::Error;
10
11use crate::{
12    domain::{ObjectType, RelationName, Relationship, SubjectRef},
13    error::ZanzibarError,
14    model::{NamespaceConfig, UsersetExpression as LegacyUsersetExpression},
15    parser::{self, LegacyNamespaceAst, LegacyRelationAst},
16};
17
18/// A source schema document.
19#[cfg_attr(
20    feature = "serde",
21    derive(serde::Serialize, serde::Deserialize),
22    serde(rename_all = "camelCase", deny_unknown_fields)
23)]
24#[derive(Debug, Clone, Copy)]
25pub struct SchemaSource<'a> {
26    /// Optional human-readable source name for diagnostics.
27    pub name: Option<&'a str>,
28    /// Schema source text.
29    pub text: &'a str,
30}
31
32/// Errors produced while compiling or validating schemas.
33#[derive(Debug, Clone, PartialEq, Eq, Error)]
34pub enum SchemaError {
35    /// The schema has two definitions with the same object type.
36    #[error("duplicate namespace definition '{namespace}'")]
37    DuplicateNamespace {
38        /// Duplicate namespace name.
39        namespace: String,
40    },
41
42    /// A namespace has two relations or permissions with the same name.
43    #[error("duplicate relation '{relation}' in namespace '{namespace}'")]
44    DuplicateRelation {
45        /// Namespace containing the duplicate.
46        namespace: String,
47        /// Duplicate relation name.
48        relation: String,
49    },
50
51    /// A namespace was requested but is not present in the schema.
52    #[error("namespace '{namespace}' not found")]
53    NamespaceNotFound {
54        /// Missing namespace name.
55        namespace: String,
56    },
57
58    /// A relation was requested but is not present in the namespace.
59    #[error("relation '{relation}' not found in namespace '{namespace}'")]
60    RelationNotFound {
61        /// Namespace searched.
62        namespace: String,
63        /// Missing relation name.
64        relation: String,
65    },
66
67    /// A schema expression references a relation that does not exist.
68    #[error(
69        "relation '{relation}' in '{namespace}.{owner}' references missing relation '{missing}'"
70    )]
71    MissingRelationReference {
72        /// Namespace containing the owner relation.
73        namespace: String,
74        /// Relation that owns the invalid expression.
75        owner: String,
76        /// Invalid expression edge.
77        relation: &'static str,
78        /// Missing relation name.
79        missing: String,
80    },
81
82    /// A tuple-to-userset target relation cannot be resolved from the known schema.
83    #[error(
84        "tuple-to-userset in '{namespace}.{owner}' references unavailable target relation \
85         '{missing}'"
86    )]
87    MissingTupleToUsersetTarget {
88        /// Namespace containing the owner relation.
89        namespace: String,
90        /// Relation that owns the invalid expression.
91        owner: String,
92        /// Missing target relation name.
93        missing: String,
94    },
95
96    /// A set operation does not contain enough operands.
97    #[error("{operator} in '{namespace}.{owner}' must contain at least {min_operands} operands")]
98    EmptyExpression {
99        /// Namespace containing the owner relation.
100        namespace: String,
101        /// Relation that owns the invalid expression.
102        owner: String,
103        /// Operator name.
104        operator: &'static str,
105        /// Minimum operand count.
106        min_operands: usize,
107    },
108}
109
110/// Immutable compiled schema.
111#[derive(Debug, Clone)]
112pub struct CompiledSchema {
113    definitions: Arc<[NamespaceDefinition]>,
114    resolver: SchemaResolver,
115}
116
117impl CompiledSchema {
118    fn new(definitions: Arc<[NamespaceDefinition]>) -> Result<Self, SchemaError> {
119        let resolver = SchemaResolver::new(Arc::clone(&definitions))?;
120        Ok(Self {
121            definitions,
122            resolver,
123        })
124    }
125
126    /// Creates and validates a compiled schema from typed namespace definitions.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`SchemaError`] when definitions are duplicated or expression references cannot be
131    /// resolved.
132    pub fn from_definitions(
133        definitions: impl IntoIterator<Item = NamespaceDefinition>,
134    ) -> Result<Self, SchemaError> {
135        let definitions = definitions.into_iter().collect::<Vec<_>>();
136        let compiled = Self::new(Arc::from(definitions.into_boxed_slice()))?;
137        validate_references(&compiled)?;
138        Self::new(Arc::from(
139            compile_resolved_definitions(&compiled)?.into_boxed_slice(),
140        ))
141    }
142
143    /// Returns all namespace definitions in source order.
144    #[must_use]
145    pub fn definitions(&self) -> &[NamespaceDefinition] {
146        &self.definitions
147    }
148
149    /// Returns the schema resolver.
150    #[must_use]
151    pub fn resolver(&self) -> &SchemaResolver {
152        &self.resolver
153    }
154
155    /// Validates that a relationship references a known resource namespace and relation.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`SchemaError::RelationNotFound`] or [`SchemaError::NamespaceNotFound`] when the
160    /// relationship resource does not match this schema.
161    pub fn validate_relationship(&self, relationship: &Relationship) -> Result<(), SchemaError> {
162        self.resolver.relation(
163            relationship.resource().object_type(),
164            relationship.relation(),
165        )?;
166        if let SubjectRef::Userset { object, relation } = relationship.subject() {
167            self.resolver.relation(object.object_type(), relation)?;
168        }
169        Ok(())
170    }
171}
172
173/// A typed namespace definition.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct NamespaceDefinition {
176    name: ObjectType,
177    relations: Arc<[RelationDefinition]>,
178}
179
180impl NamespaceDefinition {
181    /// Creates a namespace definition from validated fields.
182    #[must_use]
183    pub fn new(name: ObjectType, relations: Arc<[RelationDefinition]>) -> Self {
184        Self { name, relations }
185    }
186
187    /// Returns the namespace name.
188    #[must_use]
189    pub fn name(&self) -> &ObjectType {
190        &self.name
191    }
192
193    /// Returns relation definitions.
194    #[must_use]
195    pub fn relations(&self) -> &[RelationDefinition] {
196        &self.relations
197    }
198}
199
200/// A typed relation or permission definition.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct RelationDefinition {
203    name: RelationName,
204    allowed_subject_types: AllowedSubjectTypes,
205    userset_rewrite: Option<UsersetExpression>,
206    compiled_userset_rewrite: Option<CompiledUsersetExpression>,
207}
208
209impl RelationDefinition {
210    /// Creates a relation definition from validated fields.
211    #[must_use]
212    pub fn new(name: RelationName, userset_rewrite: Option<UsersetExpression>) -> Self {
213        Self {
214            name,
215            allowed_subject_types: AllowedSubjectTypes::Unspecified,
216            userset_rewrite,
217            compiled_userset_rewrite: None,
218        }
219    }
220
221    /// Creates a relation definition with explicit allowed subject object types.
222    #[must_use]
223    pub fn with_allowed_subject_types(
224        name: RelationName,
225        allowed_subject_types: Arc<[ObjectType]>,
226        userset_rewrite: Option<UsersetExpression>,
227    ) -> Self {
228        Self {
229            name,
230            allowed_subject_types: AllowedSubjectTypes::Explicit(allowed_subject_types),
231            userset_rewrite,
232            compiled_userset_rewrite: None,
233        }
234    }
235
236    /// Returns the relation name.
237    #[must_use]
238    pub fn name(&self) -> &RelationName {
239        &self.name
240    }
241
242    /// Returns allowed subject object type metadata.
243    #[must_use]
244    pub fn allowed_subject_types(&self) -> &AllowedSubjectTypes {
245        &self.allowed_subject_types
246    }
247
248    /// Returns the optional userset rewrite.
249    #[must_use]
250    pub fn userset_rewrite(&self) -> Option<&UsersetExpression> {
251        self.userset_rewrite.as_ref()
252    }
253
254    pub(crate) fn compiled_userset_rewrite(&self) -> Option<&CompiledUsersetExpression> {
255        self.compiled_userset_rewrite.as_ref()
256    }
257}
258
259/// Allowed subject object type metadata for a relation.
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum AllowedSubjectTypes {
262    /// Legacy schema source did not declare allowed subjects.
263    Unspecified,
264    /// The relation accepts userset subjects from these object types.
265    Explicit(Arc<[ObjectType]>),
266}
267
268/// Typed userset expression.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub enum UsersetExpression {
271    /// Direct relationships stored on this object and relation.
272    This,
273    /// A relation on the same object.
274    ComputedUserset {
275        /// Referenced relation.
276        relation: RelationName,
277    },
278    /// A relation reached through userset subjects on another relation.
279    TupleToUserset {
280        /// Relation containing intermediate userset subjects.
281        tupleset_relation: RelationName,
282        /// Relation to evaluate on each intermediate object.
283        computed_userset_relation: RelationName,
284    },
285    /// Union of child expressions.
286    Union(Vec<UsersetExpression>),
287    /// Intersection of child expressions.
288    Intersection(Vec<UsersetExpression>),
289    /// Exclusion of one expression from another.
290    Exclusion {
291        /// Base expression.
292        base: Box<UsersetExpression>,
293        /// Expression to subtract.
294        exclude: Box<UsersetExpression>,
295    },
296}
297
298/// Stable relation identifier inside one compiled schema.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
300pub(crate) struct SchemaRelationId {
301    namespace_index: usize,
302    relation_index: usize,
303}
304
305impl SchemaRelationId {
306    const fn new(namespace_index: usize, relation_index: usize) -> Self {
307        Self {
308            namespace_index,
309            relation_index,
310        }
311    }
312}
313
314/// Userset expression with same-namespace relation edges resolved to schema ids.
315#[derive(Debug, Clone, PartialEq, Eq)]
316pub(crate) enum CompiledUsersetExpression {
317    /// Direct relationships stored on this object and relation.
318    This,
319    /// A relation on the same object.
320    ComputedUserset {
321        /// Referenced relation name retained for store lookup.
322        relation: RelationName,
323        /// Referenced relation id retained for schema lookup.
324        relation_id: SchemaRelationId,
325        /// Whether the referenced relation has its own rewrite.
326        target_has_rewrite: bool,
327    },
328    /// A relation reached through userset subjects on another relation.
329    TupleToUserset {
330        /// Relation containing intermediate userset subjects.
331        tupleset_relation: RelationName,
332        /// Same-namespace id for the tupleset relation.
333        tupleset_relation_id: SchemaRelationId,
334        /// Relation to evaluate on each intermediate object. This remains name-based because the
335        /// intermediate object's namespace determines the target schema relation at evaluation
336        /// time.
337        computed_userset_relation: RelationName,
338    },
339    /// Union of child expressions.
340    Union(Vec<CompiledUsersetExpression>),
341    /// Intersection of child expressions.
342    Intersection(Vec<CompiledUsersetExpression>),
343    /// Exclusion of one expression from another.
344    Exclusion {
345        /// Base expression.
346        base: Box<CompiledUsersetExpression>,
347        /// Expression to subtract.
348        exclude: Box<CompiledUsersetExpression>,
349    },
350}
351
352/// Resolver for namespace and relation definitions.
353#[derive(Debug, Clone)]
354pub struct SchemaResolver {
355    definitions: Arc<[NamespaceDefinition]>,
356    namespace_indexes: HashMap<ObjectType, usize>,
357    relation_indexes: HashMap<ObjectType, HashMap<RelationName, usize>>,
358    sorted_relation_indexes: HashMap<ObjectType, Arc<[usize]>>,
359}
360
361impl SchemaResolver {
362    fn new(definitions: Arc<[NamespaceDefinition]>) -> Result<Self, SchemaError> {
363        let mut namespace_indexes = HashMap::with_capacity(definitions.len());
364        let mut relation_indexes = HashMap::with_capacity(definitions.len());
365        let mut sorted_relation_indexes = HashMap::with_capacity(definitions.len());
366
367        for (namespace_index, namespace) in definitions.iter().enumerate() {
368            let previous = namespace_indexes.insert(namespace.name.clone(), namespace_index);
369            if previous.is_some() {
370                return Err(SchemaError::DuplicateNamespace {
371                    namespace: namespace.name.to_string(),
372                });
373            }
374
375            let mut relations = HashMap::with_capacity(namespace.relations.len());
376            for (relation_index, relation) in namespace.relations.iter().enumerate() {
377                let previous = relations.insert(relation.name.clone(), relation_index);
378                if previous.is_some() {
379                    return Err(SchemaError::DuplicateRelation {
380                        namespace: namespace.name.to_string(),
381                        relation: relation.name.to_string(),
382                    });
383                }
384            }
385            let mut sorted_indexes = (0..namespace.relations.len()).collect::<Vec<_>>();
386            sorted_indexes.sort_by(|left, right| {
387                namespace
388                    .relations
389                    .get(*left)
390                    .map(RelationDefinition::name)
391                    .cmp(
392                        &namespace
393                            .relations
394                            .get(*right)
395                            .map(RelationDefinition::name),
396                    )
397            });
398            sorted_relation_indexes.insert(
399                namespace.name.clone(),
400                Arc::from(sorted_indexes.into_boxed_slice()),
401            );
402            relation_indexes.insert(namespace.name.clone(), relations);
403        }
404
405        Ok(Self {
406            definitions,
407            namespace_indexes,
408            relation_indexes,
409            sorted_relation_indexes,
410        })
411    }
412
413    /// Resolves a namespace definition.
414    ///
415    /// # Errors
416    ///
417    /// Returns [`SchemaError::NamespaceNotFound`] when the object type is unknown.
418    pub fn namespace(&self, object_type: &ObjectType) -> Result<&NamespaceDefinition, SchemaError> {
419        let index = self.namespace_indexes.get(object_type).ok_or_else(|| {
420            SchemaError::NamespaceNotFound {
421                namespace: object_type.to_string(),
422            }
423        })?;
424        self.definitions
425            .get(*index)
426            .ok_or_else(|| SchemaError::NamespaceNotFound {
427                namespace: object_type.to_string(),
428            })
429    }
430
431    /// Resolves a relation definition.
432    ///
433    /// # Errors
434    ///
435    /// Returns [`SchemaError::NamespaceNotFound`] when the object type is unknown or
436    /// [`SchemaError::RelationNotFound`] when the relation is unknown.
437    pub fn relation(
438        &self,
439        object_type: &ObjectType,
440        relation: &RelationName,
441    ) -> Result<&RelationDefinition, SchemaError> {
442        let namespace = self.namespace(object_type)?;
443        let relations = self.relation_indexes.get(object_type).ok_or_else(|| {
444            SchemaError::NamespaceNotFound {
445                namespace: object_type.to_string(),
446            }
447        })?;
448        let relation_index =
449            relations
450                .get(relation)
451                .ok_or_else(|| SchemaError::RelationNotFound {
452                    namespace: object_type.to_string(),
453                    relation: relation.to_string(),
454                })?;
455        namespace
456            .relations
457            .get(*relation_index)
458            .ok_or_else(|| SchemaError::RelationNotFound {
459                namespace: object_type.to_string(),
460                relation: relation.to_string(),
461            })
462    }
463
464    pub(crate) fn relation_id(
465        &self,
466        object_type: &ObjectType,
467        relation: &RelationName,
468    ) -> Result<SchemaRelationId, SchemaError> {
469        let namespace_index = self
470            .namespace_indexes
471            .get(object_type)
472            .copied()
473            .ok_or_else(|| SchemaError::NamespaceNotFound {
474                namespace: object_type.to_string(),
475            })?;
476        let relations = self.relation_indexes.get(object_type).ok_or_else(|| {
477            SchemaError::NamespaceNotFound {
478                namespace: object_type.to_string(),
479            }
480        })?;
481        let relation_index =
482            relations
483                .get(relation)
484                .copied()
485                .ok_or_else(|| SchemaError::RelationNotFound {
486                    namespace: object_type.to_string(),
487                    relation: relation.to_string(),
488                })?;
489        Ok(SchemaRelationId::new(namespace_index, relation_index))
490    }
491
492    pub(crate) fn relation_by_id(
493        &self,
494        relation_id: SchemaRelationId,
495    ) -> Option<&RelationDefinition> {
496        self.definitions
497            .get(relation_id.namespace_index)
498            .and_then(|namespace| namespace.relations().get(relation_id.relation_index))
499    }
500
501    pub(crate) fn sorted_relations(
502        &self,
503        object_type: &ObjectType,
504    ) -> Result<SortedRelations<'_>, SchemaError> {
505        let namespace = self.namespace(object_type)?;
506        let indexes = self
507            .sorted_relation_indexes
508            .get(object_type)
509            .ok_or_else(|| SchemaError::NamespaceNotFound {
510                namespace: object_type.to_string(),
511            })?;
512        Ok(SortedRelations {
513            namespace,
514            indexes: indexes.iter(),
515        })
516    }
517
518    fn relation_exists_anywhere(&self, relation: &RelationName) -> bool {
519        self.relation_indexes
520            .values()
521            .any(|relations| relations.contains_key(relation))
522    }
523}
524
525/// Iterator over relation definitions in stable name order.
526#[derive(Debug)]
527pub(crate) struct SortedRelations<'a> {
528    namespace: &'a NamespaceDefinition,
529    indexes: slice::Iter<'a, usize>,
530}
531
532impl<'a> Iterator for SortedRelations<'a> {
533    type Item = &'a RelationDefinition;
534
535    fn next(&mut self) -> Option<Self::Item> {
536        for index in self.indexes.by_ref() {
537            if let Some(relation) = self.namespace.relations().get(*index) {
538                return Some(relation);
539            }
540        }
541        None
542    }
543}
544
545/// Compiles legacy DSL source into a typed schema.
546///
547/// # Errors
548///
549/// Returns [`ZanzibarError::ParseError`] for syntax errors, [`crate::domain::DomainError`] for
550/// invalid identifiers, or [`SchemaError`] for invalid schema references.
551pub fn compile_legacy_dsl(source: &str) -> Result<CompiledSchema, ZanzibarError> {
552    compile_legacy_ast(parser::parse_dsl_ast(source)?)
553}
554
555/// Compiles legacy namespace configs into a typed schema.
556///
557/// # Errors
558///
559/// Returns [`crate::domain::DomainError`] for invalid identifiers or [`SchemaError`] for invalid
560/// schema references.
561pub fn compile_legacy_configs(
562    configs: impl IntoIterator<Item = NamespaceConfig>,
563) -> Result<CompiledSchema, ZanzibarError> {
564    let mut namespaces = Vec::new();
565    for config in configs {
566        let mut relations = Vec::new();
567        for relation in config.relations.into_values() {
568            relations.push(LegacyRelationAst {
569                name: relation.name.0,
570                rewrite: relation.userset_rewrite,
571            });
572        }
573        namespaces.push(LegacyNamespaceAst {
574            name: config.name,
575            relations,
576        });
577    }
578    compile_legacy_ast(namespaces)
579}
580
581fn compile_legacy_ast(
582    namespaces: Vec<LegacyNamespaceAst>,
583) -> Result<CompiledSchema, ZanzibarError> {
584    let mut definitions = Vec::with_capacity(namespaces.len());
585    for namespace in namespaces {
586        let mut relations = Vec::with_capacity(namespace.relations.len());
587        for relation in namespace.relations {
588            relations.push(compile_legacy_relation(relation)?);
589        }
590        definitions.push(NamespaceDefinition::new(
591            ObjectType::try_from(namespace.name.as_str())?,
592            Arc::from(relations.into_boxed_slice()),
593        ));
594    }
595
596    CompiledSchema::from_definitions(definitions).map_err(Into::into)
597}
598
599fn compile_legacy_relation(
600    relation: LegacyRelationAst,
601) -> Result<RelationDefinition, ZanzibarError> {
602    Ok(RelationDefinition::new(
603        RelationName::try_from(relation.name.as_str())?,
604        relation
605            .rewrite
606            .map(compile_legacy_expression)
607            .transpose()?,
608    ))
609}
610
611fn compile_legacy_expression(
612    expression: LegacyUsersetExpression,
613) -> Result<UsersetExpression, ZanzibarError> {
614    match expression {
615        LegacyUsersetExpression::This => Ok(UsersetExpression::This),
616        LegacyUsersetExpression::ComputedUserset { relation } => {
617            Ok(UsersetExpression::ComputedUserset {
618                relation: RelationName::try_from(relation.0.as_str())?,
619            })
620        }
621        LegacyUsersetExpression::TupleToUserset {
622            tupleset_relation,
623            computed_userset_relation,
624        } => Ok(UsersetExpression::TupleToUserset {
625            tupleset_relation: RelationName::try_from(tupleset_relation.0.as_str())?,
626            computed_userset_relation: RelationName::try_from(
627                computed_userset_relation.0.as_str(),
628            )?,
629        }),
630        LegacyUsersetExpression::Union(expressions) => expressions
631            .into_iter()
632            .map(compile_legacy_expression)
633            .collect::<Result<Vec<_>, _>>()
634            .map(UsersetExpression::Union),
635        LegacyUsersetExpression::Intersection(expressions) => expressions
636            .into_iter()
637            .map(compile_legacy_expression)
638            .collect::<Result<Vec<_>, _>>()
639            .map(UsersetExpression::Intersection),
640        LegacyUsersetExpression::Exclusion { base, exclude } => Ok(UsersetExpression::Exclusion {
641            base: Box::new(compile_legacy_expression(*base)?),
642            exclude: Box::new(compile_legacy_expression(*exclude)?),
643        }),
644    }
645}
646
647fn compile_resolved_definitions(
648    compiled: &CompiledSchema,
649) -> Result<Vec<NamespaceDefinition>, SchemaError> {
650    let mut namespaces = Vec::with_capacity(compiled.definitions().len());
651    for namespace in compiled.definitions() {
652        let mut relations = Vec::with_capacity(namespace.relations().len());
653        for relation in namespace.relations() {
654            let compiled_userset_rewrite = relation
655                .userset_rewrite()
656                .map(|expression| {
657                    compile_resolved_expression(compiled.resolver(), namespace.name(), expression)
658                })
659                .transpose()?;
660            relations.push(RelationDefinition {
661                name: relation.name.clone(),
662                allowed_subject_types: relation.allowed_subject_types.clone(),
663                userset_rewrite: relation.userset_rewrite.clone(),
664                compiled_userset_rewrite,
665            });
666        }
667        namespaces.push(NamespaceDefinition::new(
668            namespace.name.clone(),
669            Arc::from(relations.into_boxed_slice()),
670        ));
671    }
672    Ok(namespaces)
673}
674
675fn compile_resolved_expression(
676    resolver: &SchemaResolver,
677    namespace: &ObjectType,
678    expression: &UsersetExpression,
679) -> Result<CompiledUsersetExpression, SchemaError> {
680    match expression {
681        UsersetExpression::This => Ok(CompiledUsersetExpression::This),
682        UsersetExpression::ComputedUserset { relation } => {
683            let target_has_rewrite = resolver
684                .relation(namespace, relation)?
685                .userset_rewrite()
686                .is_some();
687            Ok(CompiledUsersetExpression::ComputedUserset {
688                relation: relation.clone(),
689                relation_id: resolver.relation_id(namespace, relation)?,
690                target_has_rewrite,
691            })
692        }
693        UsersetExpression::TupleToUserset {
694            tupleset_relation,
695            computed_userset_relation,
696        } => Ok(CompiledUsersetExpression::TupleToUserset {
697            tupleset_relation: tupleset_relation.clone(),
698            tupleset_relation_id: resolver.relation_id(namespace, tupleset_relation)?,
699            computed_userset_relation: computed_userset_relation.clone(),
700        }),
701        UsersetExpression::Union(expressions) => expressions
702            .iter()
703            .map(|expression| compile_resolved_expression(resolver, namespace, expression))
704            .collect::<Result<Vec<_>, _>>()
705            .map(CompiledUsersetExpression::Union),
706        UsersetExpression::Intersection(expressions) => expressions
707            .iter()
708            .map(|expression| compile_resolved_expression(resolver, namespace, expression))
709            .collect::<Result<Vec<_>, _>>()
710            .map(CompiledUsersetExpression::Intersection),
711        UsersetExpression::Exclusion { base, exclude } => {
712            Ok(CompiledUsersetExpression::Exclusion {
713                base: Box::new(compile_resolved_expression(resolver, namespace, base)?),
714                exclude: Box::new(compile_resolved_expression(resolver, namespace, exclude)?),
715            })
716        }
717    }
718}
719
720fn validate_references(compiled: &CompiledSchema) -> Result<(), SchemaError> {
721    for namespace in compiled.definitions() {
722        let mut relations = HashSet::with_capacity(namespace.relations().len());
723        for relation in namespace.relations() {
724            let inserted = relations.insert(relation.name().clone());
725            if !inserted {
726                return Err(SchemaError::DuplicateRelation {
727                    namespace: namespace.name().to_string(),
728                    relation: relation.name().to_string(),
729                });
730            }
731        }
732
733        for relation in namespace.relations() {
734            if let Some(expression) = relation.userset_rewrite() {
735                validate_expression(compiled, namespace, relation.name(), expression)?;
736            }
737        }
738    }
739    Ok(())
740}
741
742fn validate_expression(
743    compiled: &CompiledSchema,
744    namespace: &NamespaceDefinition,
745    owner: &RelationName,
746    expression: &UsersetExpression,
747) -> Result<(), SchemaError> {
748    match expression {
749        UsersetExpression::This => Ok(()),
750        UsersetExpression::ComputedUserset { relation } => ensure_relation_in_namespace(
751            compiled,
752            namespace.name(),
753            owner,
754            "computed userset",
755            relation,
756        )
757        .map(|_| ()),
758        UsersetExpression::TupleToUserset {
759            tupleset_relation,
760            computed_userset_relation,
761        } => {
762            let tupleset_relation_definition = ensure_relation_in_namespace(
763                compiled,
764                namespace.name(),
765                owner,
766                "tuple-to-userset tupleset",
767                tupleset_relation,
768            )?;
769            validate_tuple_to_userset_target(
770                compiled,
771                namespace,
772                owner,
773                tupleset_relation_definition,
774                computed_userset_relation,
775            )
776        }
777        UsersetExpression::Union(expressions) => {
778            validate_operands(namespace, owner, "union", 1, expressions)?;
779            for child in expressions {
780                validate_expression(compiled, namespace, owner, child)?;
781            }
782            Ok(())
783        }
784        UsersetExpression::Intersection(expressions) => {
785            validate_operands(namespace, owner, "intersection", 1, expressions)?;
786            for child in expressions {
787                validate_expression(compiled, namespace, owner, child)?;
788            }
789            Ok(())
790        }
791        UsersetExpression::Exclusion { base, exclude } => {
792            validate_expression(compiled, namespace, owner, base)?;
793            validate_expression(compiled, namespace, owner, exclude)
794        }
795    }
796}
797
798fn validate_operands(
799    namespace: &NamespaceDefinition,
800    owner: &RelationName,
801    operator: &'static str,
802    min_operands: usize,
803    expressions: &[UsersetExpression],
804) -> Result<(), SchemaError> {
805    if expressions.len() < min_operands {
806        return Err(SchemaError::EmptyExpression {
807            namespace: namespace.name().to_string(),
808            owner: owner.to_string(),
809            operator,
810            min_operands,
811        });
812    }
813    Ok(())
814}
815
816fn ensure_relation_in_namespace<'schema>(
817    compiled: &'schema CompiledSchema,
818    namespace: &ObjectType,
819    owner: &RelationName,
820    relation_kind: &'static str,
821    relation: &RelationName,
822) -> Result<&'schema RelationDefinition, SchemaError> {
823    match compiled.resolver().relation(namespace, relation) {
824        Ok(relation) => Ok(relation),
825        Err(SchemaError::RelationNotFound { .. }) => Err(SchemaError::MissingRelationReference {
826            namespace: namespace.to_string(),
827            owner: owner.to_string(),
828            relation: relation_kind,
829            missing: relation.to_string(),
830        }),
831        Err(error) => Err(error),
832    }
833}
834
835fn validate_tuple_to_userset_target(
836    compiled: &CompiledSchema,
837    namespace: &NamespaceDefinition,
838    owner: &RelationName,
839    tupleset_relation: &RelationDefinition,
840    computed_userset_relation: &RelationName,
841) -> Result<(), SchemaError> {
842    match tupleset_relation.allowed_subject_types() {
843        AllowedSubjectTypes::Explicit(object_types) => {
844            for object_type in object_types.iter() {
845                if compiled
846                    .resolver()
847                    .relation(object_type, computed_userset_relation)
848                    .is_err()
849                {
850                    return Err(SchemaError::MissingTupleToUsersetTarget {
851                        namespace: namespace.name().to_string(),
852                        owner: owner.to_string(),
853                        missing: computed_userset_relation.to_string(),
854                    });
855                }
856            }
857            Ok(())
858        }
859        AllowedSubjectTypes::Unspecified => {
860            if compiled
861                .resolver()
862                .relation_exists_anywhere(computed_userset_relation)
863            {
864                Ok(())
865            } else {
866                Err(SchemaError::MissingTupleToUsersetTarget {
867                    namespace: namespace.name().to_string(),
868                    owner: owner.to_string(),
869                    missing: computed_userset_relation.to_string(),
870                })
871            }
872        }
873    }
874}