Skip to main content

cedar_policy/proto/
ast.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#![allow(clippy::use_self, reason = "readability")]
18
19use super::models;
20use cedar_policy_core::{
21    ast, evaluator::RestrictedEvaluator, extensions::Extensions, FromNormalizedStr,
22};
23use smol_str::ToSmolStr;
24use std::{collections::HashSet, sync::Arc};
25
26/// Error converting a protobuf model type into a Cedar type.
27///
28/// This indicates the protobuf message was well-formed at the wire level but
29/// contained semantically invalid data (e.g. missing required fields, invalid
30/// identifiers, unsupported features).
31#[derive(Debug, thiserror::Error)]
32pub enum ProtobufConversionError {
33    /// A required protobuf field was absent
34    #[error("missing required field `{0}`")]
35    MissingField(String),
36    /// A field was present but its value was semantically invalid
37    #[error("{0}")]
38    InvalidValue(String),
39}
40
41impl ProtobufConversionError {
42    pub(crate) fn missing(field: &str) -> Self {
43        Self::MissingField(field.to_string())
44    }
45}
46
47impl TryFrom<models::Name> for ast::InternalName {
48    type Error = ProtobufConversionError;
49    fn try_from(v: models::Name) -> Result<Self, Self::Error> {
50        let basename = ast::Id::from_normalized_str(&v.id).map_err(|e| {
51            ProtobufConversionError::InvalidValue(format!("invalid basename `{}`: {e}", v.id))
52        })?;
53        let path = v
54            .path
55            .into_iter()
56            .map(|id| {
57                ast::Id::from_normalized_str(&id).map_err(|e| {
58                    ProtobufConversionError::InvalidValue(format!(
59                        "invalid path component `{id}`: {e}"
60                    ))
61                })
62            })
63            .collect::<Result<Vec<_>, _>>()?;
64        Ok(ast::InternalName::new(basename, path, None))
65    }
66}
67
68impl TryFrom<models::Name> for ast::Name {
69    type Error = ProtobufConversionError;
70    fn try_from(v: models::Name) -> Result<Self, Self::Error> {
71        ast::Name::try_from(ast::InternalName::try_from(v)?)
72            .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid name: {e}")))
73    }
74}
75
76impl TryFrom<models::Name> for ast::EntityType {
77    type Error = ProtobufConversionError;
78    fn try_from(v: models::Name) -> Result<Self, Self::Error> {
79        Ok(ast::EntityType::from(ast::Name::try_from(v)?))
80    }
81}
82
83impl From<&ast::InternalName> for models::Name {
84    fn from(v: &ast::InternalName) -> Self {
85        Self {
86            id: v.basename().to_string(),
87            path: v
88                .namespace_components()
89                .map(|id| String::from(id.as_ref()))
90                .collect(),
91        }
92    }
93}
94
95impl From<&ast::Name> for models::Name {
96    fn from(v: &ast::Name) -> Self {
97        Self::from(v.as_ref())
98    }
99}
100
101impl From<&ast::EntityType> for models::Name {
102    fn from(v: &ast::EntityType) -> Self {
103        Self::from(v.as_ref())
104    }
105}
106
107impl TryFrom<models::EntityUid> for ast::EntityUID {
108    type Error = ProtobufConversionError;
109    fn try_from(v: models::EntityUid) -> Result<Self, ProtobufConversionError> {
110        Ok(Self::from_components(
111            ast::EntityType::try_from(v.ty.ok_or_else(|| ProtobufConversionError::missing("ty"))?)?,
112            ast::Eid::new(v.eid),
113            None,
114        ))
115    }
116}
117
118impl From<&ast::EntityUID> for models::EntityUid {
119    fn from(v: &ast::EntityUID) -> Self {
120        Self {
121            ty: Some(models::Name::from(v.entity_type())),
122            eid: <ast::Eid as AsRef<str>>::as_ref(v.eid()).into(),
123        }
124    }
125}
126
127impl TryFrom<models::EntityUid> for ast::EntityUIDEntry {
128    type Error = ProtobufConversionError;
129    fn try_from(v: models::EntityUid) -> Result<Self, Self::Error> {
130        Ok(ast::EntityUIDEntry::known(
131            ast::EntityUID::try_from(v)?,
132            None,
133        ))
134    }
135}
136
137impl From<&ast::EntityUIDEntry> for models::EntityUid {
138    #[expect(clippy::unimplemented, reason = "experimental feature")]
139    fn from(v: &ast::EntityUIDEntry) -> Self {
140        match v {
141            ast::EntityUIDEntry::Unknown { .. } => {
142                unimplemented!(
143                    "Unknown EntityUID is not currently supported by the Protobuf interface"
144                );
145            }
146            ast::EntityUIDEntry::Known { euid, .. } => models::EntityUid::from(euid.as_ref()),
147        }
148    }
149}
150
151impl TryFrom<models::Entity> for ast::Entity {
152    type Error = ProtobufConversionError;
153    fn try_from(v: models::Entity) -> Result<Self, Self::Error> {
154        let eval = RestrictedEvaluator::new(Extensions::all_available());
155
156        let attrs = v
157            .attrs
158            .into_iter()
159            .map(|(key, value)| {
160                let expr = ast::Expr::try_from(value)?;
161                let restricted = ast::BorrowedRestrictedExpr::new(&expr).map_err(|e| {
162                    ProtobufConversionError::InvalidValue(format!(
163                        "invalid restricted expr in attr `{key}`: {e}"
164                    ))
165                })?;
166                let pval = eval.partial_interpret(restricted).map_err(|e| {
167                    ProtobufConversionError::InvalidValue(format!(
168                        "error interpreting attr `{key}`: {e}"
169                    ))
170                })?;
171                Ok((key.into(), pval))
172            })
173            .collect::<Result<Vec<_>, ProtobufConversionError>>()?;
174
175        let ancestors = v
176            .ancestors
177            .into_iter()
178            .map(ast::EntityUID::try_from)
179            .collect::<Result<HashSet<_>, _>>()?;
180
181        let tags = v
182            .tags
183            .into_iter()
184            .map(|(key, value)| {
185                let expr = ast::Expr::try_from(value)?;
186                let restricted = ast::BorrowedRestrictedExpr::new(&expr).map_err(|e| {
187                    ProtobufConversionError::InvalidValue(format!(
188                        "invalid restricted expr in tag `{key}`: {e}"
189                    ))
190                })?;
191                let pval = eval.partial_interpret(restricted).map_err(|e| {
192                    ProtobufConversionError::InvalidValue(format!(
193                        "error interpreting tag `{key}`: {e}"
194                    ))
195                })?;
196                Ok((key.into(), pval))
197            })
198            .collect::<Result<Vec<_>, ProtobufConversionError>>()?;
199
200        Ok(Self::new_with_attr_partial_value(
201            ast::EntityUID::try_from(
202                v.uid
203                    .ok_or_else(|| ProtobufConversionError::missing("uid"))?,
204            )?,
205            attrs,
206            HashSet::new(),
207            ancestors,
208            tags,
209        ))
210    }
211}
212
213impl From<&ast::Entity> for models::Entity {
214    fn from(v: &ast::Entity) -> Self {
215        Self {
216            uid: Some(models::EntityUid::from(v.uid())),
217            attrs: v
218                .attrs()
219                .map(|(key, value)| {
220                    (
221                        key.to_string(),
222                        models::Expr::from(&ast::Expr::from(value.clone())),
223                    )
224                })
225                .collect(),
226            ancestors: v.ancestors().map(models::EntityUid::from).collect(),
227            tags: v
228                .tags()
229                .map(|(key, value)| {
230                    (
231                        key.to_string(),
232                        models::Expr::from(&ast::Expr::from(value.clone())),
233                    )
234                })
235                .collect(),
236        }
237    }
238}
239
240impl From<&Arc<ast::Entity>> for models::Entity {
241    fn from(v: &Arc<ast::Entity>) -> Self {
242        Self::from(v.as_ref())
243    }
244}
245
246#[expect(clippy::too_many_lines, reason = "models::ExprKind has many variants")]
247impl TryFrom<models::Expr> for ast::Expr {
248    type Error = ProtobufConversionError;
249    fn try_from(v: models::Expr) -> Result<Self, Self::Error> {
250        let kind = v
251            .expr_kind
252            .ok_or_else(|| ProtobufConversionError::missing("expr_kind"))?;
253
254        match kind {
255            models::expr::ExprKind::Lit(lit) => Ok(ast::Expr::val(ast::Literal::try_from(lit)?)),
256
257            models::expr::ExprKind::Var(var) => {
258                let pvar = models::expr::Var::try_from(var).map_err(|e| {
259                    ProtobufConversionError::InvalidValue(format!("invalid var: {e}"))
260                })?;
261                Ok(ast::Expr::var(ast::Var::from(pvar)))
262            }
263
264            models::expr::ExprKind::Slot(slot) => {
265                let pslot = models::SlotId::try_from(slot).map_err(|e| {
266                    ProtobufConversionError::InvalidValue(format!("invalid slot: {e}"))
267                })?;
268                Ok(ast::Expr::slot(ast::SlotId::from(pslot)))
269            }
270
271            models::expr::ExprKind::If(msg) => {
272                let test_expr = *msg
273                    .test_expr
274                    .ok_or_else(|| ProtobufConversionError::missing("test_expr"))?;
275                let then_expr = *msg
276                    .then_expr
277                    .ok_or_else(|| ProtobufConversionError::missing("then_expr"))?;
278                let else_expr = *msg
279                    .else_expr
280                    .ok_or_else(|| ProtobufConversionError::missing("else_expr"))?;
281                Ok(ast::Expr::ite(
282                    ast::Expr::try_from(test_expr)?,
283                    ast::Expr::try_from(then_expr)?,
284                    ast::Expr::try_from(else_expr)?,
285                ))
286            }
287
288            models::expr::ExprKind::And(msg) => {
289                let left = *msg
290                    .left
291                    .ok_or_else(|| ProtobufConversionError::missing("left"))?;
292                let right = *msg
293                    .right
294                    .ok_or_else(|| ProtobufConversionError::missing("right"))?;
295                Ok(ast::Expr::and(
296                    ast::Expr::try_from(left)?,
297                    ast::Expr::try_from(right)?,
298                ))
299            }
300
301            models::expr::ExprKind::Or(msg) => {
302                let left = *msg
303                    .left
304                    .ok_or_else(|| ProtobufConversionError::missing("left"))?;
305                let right = *msg
306                    .right
307                    .ok_or_else(|| ProtobufConversionError::missing("right"))?;
308                Ok(ast::Expr::or(
309                    ast::Expr::try_from(left)?,
310                    ast::Expr::try_from(right)?,
311                ))
312            }
313
314            models::expr::ExprKind::UApp(msg) => {
315                let arg = *msg
316                    .expr
317                    .ok_or_else(|| ProtobufConversionError::missing("expr"))?;
318                let puop = models::expr::unary_app::Op::try_from(msg.op).map_err(|e| {
319                    ProtobufConversionError::InvalidValue(format!("invalid unary op: {e}"))
320                })?;
321                Ok(ast::Expr::unary_app(
322                    ast::UnaryOp::from(puop),
323                    ast::Expr::try_from(arg)?,
324                ))
325            }
326
327            models::expr::ExprKind::BApp(msg) => {
328                let pbop = models::expr::binary_app::Op::try_from(msg.op).map_err(|e| {
329                    ProtobufConversionError::InvalidValue(format!("invalid binary op: {e}"))
330                })?;
331                let left = *msg
332                    .left
333                    .ok_or_else(|| ProtobufConversionError::missing("left"))?;
334                let right = *msg
335                    .right
336                    .ok_or_else(|| ProtobufConversionError::missing("right"))?;
337                Ok(ast::Expr::binary_app(
338                    ast::BinaryOp::from(pbop),
339                    ast::Expr::try_from(left)?,
340                    ast::Expr::try_from(right)?,
341                ))
342            }
343
344            models::expr::ExprKind::ExtApp(msg) => Ok(ast::Expr::call_extension_fn(
345                ast::Name::try_from(
346                    msg.fn_name
347                        .ok_or_else(|| ProtobufConversionError::missing("fn_name"))?,
348                )?,
349                msg.args
350                    .into_iter()
351                    .map(ast::Expr::try_from)
352                    .collect::<Result<_, _>>()?,
353            )),
354
355            models::expr::ExprKind::GetAttr(msg) => {
356                let arg = *msg
357                    .expr
358                    .ok_or_else(|| ProtobufConversionError::missing("expr"))?;
359                Ok(ast::Expr::get_attr(
360                    ast::Expr::try_from(arg)?,
361                    msg.attr.into(),
362                ))
363            }
364
365            models::expr::ExprKind::HasAttr(msg) => {
366                let arg = *msg
367                    .expr
368                    .ok_or_else(|| ProtobufConversionError::missing("expr"))?;
369                Ok(ast::Expr::has_attr(
370                    ast::Expr::try_from(arg)?,
371                    msg.attr.into(),
372                ))
373            }
374
375            models::expr::ExprKind::Like(msg) => {
376                let arg = *msg
377                    .expr
378                    .ok_or_else(|| ProtobufConversionError::missing("expr"))?;
379                Ok(ast::Expr::like(
380                    ast::Expr::try_from(arg)?,
381                    msg.pattern
382                        .into_iter()
383                        .map(ast::PatternElem::try_from)
384                        .collect::<Result<_, _>>()?,
385                ))
386            }
387
388            models::expr::ExprKind::Is(msg) => {
389                let arg = *msg
390                    .expr
391                    .ok_or_else(|| ProtobufConversionError::missing("expr"))?;
392                Ok(ast::Expr::is_entity_type(
393                    ast::Expr::try_from(arg)?,
394                    ast::EntityType::try_from(
395                        msg.entity_type
396                            .ok_or_else(|| ProtobufConversionError::missing("entity_type"))?,
397                    )?,
398                ))
399            }
400
401            models::expr::ExprKind::Set(msg) => Ok(ast::Expr::set(
402                msg.elements
403                    .into_iter()
404                    .map(ast::Expr::try_from)
405                    .collect::<Result<Vec<_>, _>>()?,
406            )),
407
408            models::expr::ExprKind::Record(msg) => {
409                let items = msg
410                    .items
411                    .into_iter()
412                    .map(|(key, value)| Ok((key.into(), ast::Expr::try_from(value)?)))
413                    .collect::<Result<Vec<_>, ProtobufConversionError>>()?;
414                ast::Expr::record(items).map_err(|e| {
415                    ProtobufConversionError::InvalidValue(format!("invalid record: {e}"))
416                })
417            }
418        }
419    }
420}
421
422impl From<&ast::Expr> for models::Expr {
423    #[expect(
424        clippy::unimplemented,
425        clippy::too_many_lines,
426        reason = "experimental feature"
427    )]
428    fn from(v: &ast::Expr) -> Self {
429        let expr_kind = match v.expr_kind() {
430            ast::ExprKind::Lit(l) => {
431                models::expr::ExprKind::Lit(models::expr::Literal::from(l))
432            }
433            ast::ExprKind::Var(v) => {
434                models::expr::ExprKind::Var(models::expr::Var::from(v).into())
435            }
436            ast::ExprKind::Slot(sid) => {
437                models::expr::ExprKind::Slot(models::SlotId::from(sid).into())
438            }
439
440            ast::ExprKind::Unknown(_u) => {
441                unimplemented!("Protobuffer interface does not support Unknown expressions")
442            }
443            ast::ExprKind::If {
444                test_expr,
445                then_expr,
446                else_expr,
447            } => models::expr::ExprKind::If(Box::new(models::expr::If {
448                test_expr: Some(Box::new(models::Expr::from(test_expr.as_ref()))),
449                then_expr: Some(Box::new(models::Expr::from(then_expr.as_ref()))),
450                else_expr: Some(Box::new(models::Expr::from(else_expr.as_ref()))),
451            })),
452            ast::ExprKind::And { left, right } => {
453                models::expr::ExprKind::And(Box::new(models::expr::And {
454                    left: Some(Box::new(models::Expr::from(left.as_ref()))),
455                    right: Some(Box::new(models::Expr::from(right.as_ref()))),
456                }))
457            }
458            ast::ExprKind::Or { left, right } => {
459                models::expr::ExprKind::Or(Box::new(models::expr::Or {
460                    left: Some(Box::new(models::Expr::from(left.as_ref()))),
461                    right: Some(Box::new(models::Expr::from(right.as_ref()))),
462                }))
463            }
464            ast::ExprKind::UnaryApp { op, arg } => {
465                models::expr::ExprKind::UApp(Box::new(models::expr::UnaryApp {
466                    op: models::expr::unary_app::Op::from(op).into(),
467                    expr: Some(Box::new(models::Expr::from(arg.as_ref()))),
468                }))
469            }
470            ast::ExprKind::BinaryApp { op, arg1, arg2 } => {
471                models::expr::ExprKind::BApp(Box::new(models::expr::BinaryApp {
472                    op: models::expr::binary_app::Op::from(op).into(),
473                    left: Some(Box::new(models::Expr::from(arg1.as_ref()))),
474                    right: Some(Box::new(models::Expr::from(arg2.as_ref()))),
475                }))
476            }
477            ast::ExprKind::ExtensionFunctionApp { fn_name, args } => {
478                let pargs: Vec<models::Expr> = args.iter().map(models::Expr::from).collect();
479                models::expr::ExprKind::ExtApp(models::expr::ExtensionFunctionApp {
480                    fn_name: Some(models::Name::from(fn_name)),
481                    args: pargs,
482                })
483            }
484            ast::ExprKind::GetAttr { expr, attr } => {
485                models::expr::ExprKind::GetAttr(Box::new(models::expr::GetAttr {
486                    attr: attr.to_string(),
487                    expr: Some(Box::new(models::Expr::from(expr.as_ref()))),
488                }))
489            }
490            ast::ExprKind::HasAttr { expr, attr } => {
491                models::expr::ExprKind::HasAttr(Box::new(models::expr::HasAttr {
492                    attr: attr.to_string(),
493                    expr: Some(Box::new(models::Expr::from(expr.as_ref()))),
494                }))
495            }
496            ast::ExprKind::Like { expr, pattern } => {
497                let mut ppattern: Vec<models::expr::like::PatternElem> =
498                    Vec::with_capacity(pattern.len());
499                for value in pattern.iter() {
500                    ppattern.push(models::expr::like::PatternElem::from(value));
501                }
502                models::expr::ExprKind::Like(Box::new(models::expr::Like {
503                    expr: Some(Box::new(models::Expr::from(expr.as_ref()))),
504                    pattern: ppattern,
505                }))
506            }
507            ast::ExprKind::Is { expr, entity_type } => {
508                models::expr::ExprKind::Is(Box::new(models::expr::Is {
509                    expr: Some(Box::new(models::Expr::from(expr.as_ref()))),
510                    entity_type: Some(models::Name::from(entity_type)),
511                }))
512            }
513            ast::ExprKind::Set(args) => {
514                let mut pargs: Vec<models::Expr> = Vec::with_capacity(args.as_ref().len());
515                for arg in args.as_ref() {
516                    pargs.push(models::Expr::from(arg));
517                }
518                models::expr::ExprKind::Set(models::expr::Set { elements: pargs })
519            }
520            ast::ExprKind::Record(record) => {
521                let precord = record
522                    .as_ref()
523                    .iter()
524                    .map(|(key, value)| (key.to_string(), models::Expr::from(value)))
525                    .collect();
526                models::expr::ExprKind::Record(models::expr::Record { items: precord })
527            },
528            #[cfg(feature="tolerant-ast")]
529            ast::ExprKind::Error { .. } => unimplemented!("Protobufs feature not compatible with ASTs that contain error nodes - this should never happen"),
530        };
531        Self {
532            expr_kind: Some(expr_kind),
533        }
534    }
535}
536
537impl From<&ast::Value> for models::Expr {
538    fn from(v: &ast::Value) -> Self {
539        (&ast::Expr::from(v.clone())).into()
540    }
541}
542
543impl From<models::expr::Var> for ast::Var {
544    fn from(v: models::expr::Var) -> Self {
545        match v {
546            models::expr::Var::Principal => ast::Var::Principal,
547            models::expr::Var::Action => ast::Var::Action,
548            models::expr::Var::Resource => ast::Var::Resource,
549            models::expr::Var::Context => ast::Var::Context,
550        }
551    }
552}
553
554impl From<&ast::Var> for models::expr::Var {
555    fn from(v: &ast::Var) -> Self {
556        match v {
557            ast::Var::Principal => models::expr::Var::Principal,
558            ast::Var::Action => models::expr::Var::Action,
559            ast::Var::Resource => models::expr::Var::Resource,
560            ast::Var::Context => models::expr::Var::Context,
561        }
562    }
563}
564
565impl TryFrom<models::expr::Literal> for ast::Literal {
566    type Error = ProtobufConversionError;
567    fn try_from(v: models::expr::Literal) -> Result<Self, Self::Error> {
568        match v
569            .lit
570            .ok_or_else(|| ProtobufConversionError::missing("lit"))?
571        {
572            models::expr::literal::Lit::B(b) => Ok(ast::Literal::Bool(b)),
573            models::expr::literal::Lit::I(l) => Ok(ast::Literal::Long(l)),
574            models::expr::literal::Lit::S(s) => Ok(ast::Literal::String(s.into())),
575            models::expr::literal::Lit::Euid(e) => {
576                Ok(ast::Literal::EntityUID(ast::EntityUID::try_from(e)?.into()))
577            }
578        }
579    }
580}
581
582impl From<&ast::Literal> for models::expr::Literal {
583    fn from(v: &ast::Literal) -> Self {
584        match v {
585            ast::Literal::Bool(b) => Self {
586                lit: Some(models::expr::literal::Lit::B(*b)),
587            },
588            ast::Literal::Long(l) => Self {
589                lit: Some(models::expr::literal::Lit::I(*l)),
590            },
591            ast::Literal::String(s) => Self {
592                lit: Some(models::expr::literal::Lit::S(s.to_string())),
593            },
594            ast::Literal::EntityUID(euid) => Self {
595                lit: Some(models::expr::literal::Lit::Euid(models::EntityUid::from(
596                    euid.as_ref(),
597                ))),
598            },
599        }
600    }
601}
602
603impl From<models::SlotId> for ast::SlotId {
604    fn from(v: models::SlotId) -> Self {
605        match v {
606            models::SlotId::Principal => ast::SlotId::principal(),
607            models::SlotId::Resource => ast::SlotId::resource(),
608        }
609    }
610}
611
612#[expect(clippy::fallible_impl_from, reason = "experimental feature")]
613impl From<&ast::SlotId> for models::SlotId {
614    #[expect(clippy::panic, reason = "experimental feature")]
615    fn from(v: &ast::SlotId) -> Self {
616        if v.is_principal() {
617            models::SlotId::Principal
618        } else if v.is_resource() {
619            models::SlotId::Resource
620        } else {
621            panic!("Slot other than principal or resource")
622        }
623    }
624}
625
626impl From<models::expr::unary_app::Op> for ast::UnaryOp {
627    fn from(v: models::expr::unary_app::Op) -> Self {
628        match v {
629            models::expr::unary_app::Op::Not => ast::UnaryOp::Not,
630            models::expr::unary_app::Op::Neg => ast::UnaryOp::Neg,
631            models::expr::unary_app::Op::IsEmpty => ast::UnaryOp::IsEmpty,
632        }
633    }
634}
635
636impl From<&ast::UnaryOp> for models::expr::unary_app::Op {
637    fn from(v: &ast::UnaryOp) -> Self {
638        match v {
639            ast::UnaryOp::Not => models::expr::unary_app::Op::Not,
640            ast::UnaryOp::Neg => models::expr::unary_app::Op::Neg,
641            ast::UnaryOp::IsEmpty => models::expr::unary_app::Op::IsEmpty,
642        }
643    }
644}
645
646impl From<models::expr::binary_app::Op> for ast::BinaryOp {
647    fn from(v: models::expr::binary_app::Op) -> Self {
648        match v {
649            models::expr::binary_app::Op::Eq => ast::BinaryOp::Eq,
650            models::expr::binary_app::Op::Less => ast::BinaryOp::Less,
651            models::expr::binary_app::Op::LessEq => ast::BinaryOp::LessEq,
652            models::expr::binary_app::Op::Add => ast::BinaryOp::Add,
653            models::expr::binary_app::Op::Sub => ast::BinaryOp::Sub,
654            models::expr::binary_app::Op::Mul => ast::BinaryOp::Mul,
655            models::expr::binary_app::Op::In => ast::BinaryOp::In,
656            models::expr::binary_app::Op::Contains => ast::BinaryOp::Contains,
657            models::expr::binary_app::Op::ContainsAll => ast::BinaryOp::ContainsAll,
658            models::expr::binary_app::Op::ContainsAny => ast::BinaryOp::ContainsAny,
659            models::expr::binary_app::Op::GetTag => ast::BinaryOp::GetTag,
660            models::expr::binary_app::Op::HasTag => ast::BinaryOp::HasTag,
661        }
662    }
663}
664
665impl From<&ast::BinaryOp> for models::expr::binary_app::Op {
666    fn from(v: &ast::BinaryOp) -> Self {
667        match v {
668            ast::BinaryOp::Eq => models::expr::binary_app::Op::Eq,
669            ast::BinaryOp::Less => models::expr::binary_app::Op::Less,
670            ast::BinaryOp::LessEq => models::expr::binary_app::Op::LessEq,
671            ast::BinaryOp::Add => models::expr::binary_app::Op::Add,
672            ast::BinaryOp::Sub => models::expr::binary_app::Op::Sub,
673            ast::BinaryOp::Mul => models::expr::binary_app::Op::Mul,
674            ast::BinaryOp::In => models::expr::binary_app::Op::In,
675            ast::BinaryOp::Contains => models::expr::binary_app::Op::Contains,
676            ast::BinaryOp::ContainsAll => models::expr::binary_app::Op::ContainsAll,
677            ast::BinaryOp::ContainsAny => models::expr::binary_app::Op::ContainsAny,
678            ast::BinaryOp::GetTag => models::expr::binary_app::Op::GetTag,
679            ast::BinaryOp::HasTag => models::expr::binary_app::Op::HasTag,
680        }
681    }
682}
683
684impl TryFrom<models::expr::like::PatternElem> for ast::PatternElem {
685    type Error = ProtobufConversionError;
686    fn try_from(v: models::expr::like::PatternElem) -> Result<Self, Self::Error> {
687        match v
688            .data
689            .ok_or_else(|| ProtobufConversionError::missing("data"))?
690        {
691            models::expr::like::pattern_elem::Data::C(c) => Ok(ast::PatternElem::Char(
692                c.chars().next().ok_or_else(|| {
693                    ProtobufConversionError::InvalidValue(
694                        "empty char in pattern element".to_string(),
695                    )
696                })?,
697            )),
698            models::expr::like::pattern_elem::Data::Wildcard(unit) => {
699                match models::expr::like::pattern_elem::Wildcard::try_from(unit).map_err(|e| {
700                    ProtobufConversionError::InvalidValue(format!("invalid wildcard: {e}"))
701                })? {
702                    models::expr::like::pattern_elem::Wildcard::Unit => {
703                        Ok(ast::PatternElem::Wildcard)
704                    }
705                }
706            }
707        }
708    }
709}
710
711impl From<&ast::PatternElem> for models::expr::like::PatternElem {
712    fn from(v: &ast::PatternElem) -> Self {
713        match v {
714            ast::PatternElem::Char(c) => Self {
715                data: Some(models::expr::like::pattern_elem::Data::C(c.to_string())),
716            },
717            ast::PatternElem::Wildcard => Self {
718                data: Some(models::expr::like::pattern_elem::Data::Wildcard(
719                    models::expr::like::pattern_elem::Wildcard::Unit.into(),
720                )),
721            },
722        }
723    }
724}
725
726impl TryFrom<models::Request> for ast::Request {
727    type Error = ProtobufConversionError;
728    fn try_from(v: models::Request) -> Result<Self, Self::Error> {
729        Ok(ast::Request::new_unchecked(
730            ast::EntityUIDEntry::try_from(
731                v.principal
732                    .ok_or_else(|| ProtobufConversionError::missing("principal"))?,
733            )?,
734            ast::EntityUIDEntry::try_from(
735                v.action
736                    .ok_or_else(|| ProtobufConversionError::missing("action"))?,
737            )?,
738            ast::EntityUIDEntry::try_from(
739                v.resource
740                    .ok_or_else(|| ProtobufConversionError::missing("resource"))?,
741            )?,
742            Some(
743                ast::Context::from_pairs(
744                    v.context
745                        .into_iter()
746                        .map(|(k, v)| {
747                            let expr = ast::Expr::try_from(v)?;
748                            let restricted = ast::RestrictedExpr::new(expr).map_err(|e| {
749                                ProtobufConversionError::InvalidValue(format!(
750                                    "invalid restricted expr in context key `{k}`: {e}"
751                                ))
752                            })?;
753                            Ok((k.to_smolstr(), restricted))
754                        })
755                        .collect::<Result<Vec<_>, ProtobufConversionError>>()?,
756                    Extensions::all_available(),
757                )
758                .map_err(|e| {
759                    ProtobufConversionError::InvalidValue(format!("invalid context: {e}"))
760                })?,
761            ),
762        ))
763    }
764}
765
766impl From<&ast::Request> for models::Request {
767    #[expect(clippy::expect_used, reason = "experimental feature")]
768    fn from(v: &ast::Request) -> Self {
769        Self {
770            principal: Some(models::EntityUid::from(v.principal())),
771            action: Some(models::EntityUid::from(v.action())),
772            resource: Some(models::EntityUid::from(v.resource())),
773            context: {
774                let ctx = v.context().expect(
775                    "Requests with unknown context currently cannot be modeled in protobuf",
776                );
777                match ctx {
778                    ast::Context::Value(map) => map
779                        .iter()
780                        .map(|(k, v)| (k.to_string(), models::Expr::from(v)))
781                        .collect(),
782                    ast::Context::RestrictedResidual(map) => map
783                        .iter()
784                        .map(|(k, v)| (k.to_string(), models::Expr::from(v)))
785                        .collect(),
786                }
787            },
788        }
789    }
790}
791
792impl TryFrom<models::Expr> for ast::Context {
793    type Error = ProtobufConversionError;
794    fn try_from(v: models::Expr) -> Result<Self, Self::Error> {
795        let expr = ast::Expr::try_from(v)?;
796        let restricted = ast::BorrowedRestrictedExpr::new(&expr).map_err(|e| {
797            ProtobufConversionError::InvalidValue(format!(
798                "invalid restricted expr in context: {e}"
799            ))
800        })?;
801        ast::Context::from_expr(restricted, Extensions::all_available())
802            .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid context: {e}")))
803    }
804}
805
806impl From<&ast::Context> for models::Expr {
807    fn from(v: &ast::Context) -> Self {
808        models::Expr::from(&ast::Expr::from(ast::PartialValue::from(v.to_owned())))
809    }
810}
811
812#[cfg(test)]
813mod test {
814    use crate::{Context, Entity, Request};
815
816    use super::*;
817    use cedar_policy_core::assert_deep_eq;
818    use cool_asserts::assert_matches;
819    use serde_json::json;
820
821    #[test]
822    fn name_and_slot_roundtrip() {
823        let orig_name = ast::Name::from_normalized_str("B::C::D").unwrap();
824        assert_eq!(
825            orig_name,
826            ast::Name::try_from(models::Name::from(&orig_name)).unwrap()
827        );
828
829        let orig_slot1 = ast::SlotId::principal();
830        assert_eq!(
831            orig_slot1,
832            ast::SlotId::from(models::SlotId::from(&orig_slot1))
833        );
834
835        let orig_slot2 = ast::SlotId::resource();
836        assert_eq!(
837            orig_slot2,
838            ast::SlotId::from(models::SlotId::from(&orig_slot2))
839        );
840    }
841
842    #[test]
843    fn entity_roundtrip() {
844        let name = ast::Name::from_normalized_str("B::C::D").unwrap();
845        let ety_specified = ast::EntityType::from(name);
846        assert_eq!(
847            ety_specified,
848            ast::EntityType::try_from(models::Name::from(&ety_specified)).unwrap()
849        );
850
851        let euid1 = ast::EntityUID::with_eid_and_type("A", "foo").unwrap();
852        assert_eq!(
853            euid1,
854            ast::EntityUID::try_from(models::EntityUid::from(&euid1)).unwrap()
855        );
856
857        let euid2 = ast::EntityUID::from_normalized_str("Foo::Action::\"view\"").unwrap();
858        assert_eq!(
859            euid2,
860            ast::EntityUID::try_from(models::EntityUid::from(&euid2)).unwrap()
861        );
862
863        let euid3 = ast::EntityUID::from_components(
864            ast::EntityType::from_normalized_str("A").unwrap(),
865            ast::Eid::new("\0\n \' \"+-$^!"),
866            None,
867        );
868        assert_eq!(
869            euid3,
870            ast::EntityUID::try_from(models::EntityUid::from(&euid3)).unwrap()
871        );
872
873        let attrs = (1..=7).map(|id| (format!("{id}").into(), ast::RestrictedExpr::val(true)));
874        let parent = ast::EntityUID::with_eid_and_type("Folder", "shared").unwrap();
875        let entity = ast::Entity::new(
876            r#"Foo::"bar""#.parse().unwrap(),
877            attrs,
878            HashSet::from([parent.clone()]),
879            HashSet::new(),
880            [],
881            Extensions::none(),
882        )
883        .unwrap();
884        assert_deep_eq!(
885            entity,
886            ast::Entity::try_from(models::Entity::from(&entity)).unwrap()
887        );
888        assert!(ast::Entity::try_from(models::Entity::from(&entity))
889            .unwrap()
890            .is_child_of(&parent));
891    }
892
893    #[test]
894    fn entity_tags_roundtrip() {
895        let tags = [
896            ("foo".into(), ast::RestrictedExpr::val(1)),
897            ("bar".into(), ast::RestrictedExpr::val("baz")),
898        ];
899        let entity = ast::Entity::new(
900            r#"Foo::"bar""#.parse().unwrap(),
901            [],
902            HashSet::new(),
903            HashSet::new(),
904            tags,
905            Extensions::none(),
906        )
907        .unwrap();
908        assert_deep_eq!(
909            entity,
910            ast::Entity::try_from(models::Entity::from(&entity)).unwrap()
911        );
912    }
913
914    #[test]
915    fn entity_ext_attr_value() {
916        #[track_caller]
917        fn assert_ext_roundtrip(ext: serde_json::Value) {
918            let entity = Entity::from_json_value(
919                json!({
920                    "uid": {"type": "User", "id": "alice"},
921                    "parents": [],
922                    "attrs": { "ext": {"__extn": ext}, },
923                }),
924                None,
925            )
926            .unwrap();
927            assert_deep_eq!(
928                entity,
929                Entity::try_from(models::Entity::from(&entity)).unwrap()
930            );
931        }
932        assert_ext_roundtrip(json!({"fn": "ip", "arg": "127.0.0.1"}));
933        assert_ext_roundtrip(json!({"fn": "decimal", "arg": "1.0"}));
934        assert_ext_roundtrip(json!({"fn": "datetime", "arg": "2024-10-15"}));
935        assert_ext_roundtrip(json!({"fn": "duration", "arg": "1s"}));
936    }
937
938    #[test]
939    fn entity_ext_tag_value() {
940        #[track_caller]
941        fn assert_ext_roundtrip(ext: serde_json::Value) {
942            let entity = Entity::from_json_value(
943                json!({
944                    "uid": {"type": "User", "id": "alice"},
945                    "parents": [],
946                    "attrs": {},
947                    "tags": { "ext": {"__extn": ext}, },
948                }),
949                None,
950            )
951            .unwrap();
952            assert_deep_eq!(
953                entity,
954                Entity::try_from(models::Entity::from(&entity)).unwrap()
955            );
956        }
957        assert_ext_roundtrip(json!({"fn": "ip", "arg": "127.0.0.1"}));
958        assert_ext_roundtrip(json!({"fn": "decimal", "arg": "1.0"}));
959        assert_ext_roundtrip(json!({"fn": "datetime", "arg": "2024-10-15"}));
960        assert_ext_roundtrip(json!({"fn": "duration", "arg": "1s"}));
961    }
962
963    #[test]
964    fn expr_roundtrip() {
965        let e1 = ast::Expr::val(33);
966        assert_eq!(e1, ast::Expr::try_from(models::Expr::from(&e1)).unwrap());
967        let e2 = ast::Expr::val("hello");
968        assert_eq!(e2, ast::Expr::try_from(models::Expr::from(&e2)).unwrap());
969        let e3 = ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "foo").unwrap());
970        assert_eq!(e3, ast::Expr::try_from(models::Expr::from(&e3)).unwrap());
971        let e4 = ast::Expr::var(ast::Var::Principal);
972        assert_eq!(e4, ast::Expr::try_from(models::Expr::from(&e4)).unwrap());
973        let e4 = ast::Expr::var(ast::Var::Action);
974        assert_eq!(e4, ast::Expr::try_from(models::Expr::from(&e4)).unwrap());
975        let e4 = ast::Expr::var(ast::Var::Resource);
976        assert_eq!(e4, ast::Expr::try_from(models::Expr::from(&e4)).unwrap());
977        let e4 = ast::Expr::var(ast::Var::Context);
978        assert_eq!(e4, ast::Expr::try_from(models::Expr::from(&e4)).unwrap());
979        let e5 = ast::Expr::ite(
980            ast::Expr::val(true),
981            ast::Expr::val(88),
982            ast::Expr::val(-100),
983        );
984        assert_eq!(e5, ast::Expr::try_from(models::Expr::from(&e5)).unwrap());
985        let e6 = ast::Expr::not(ast::Expr::val(false));
986        assert_eq!(e6, ast::Expr::try_from(models::Expr::from(&e6)).unwrap());
987        let e7 = ast::Expr::get_attr(
988            ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "foo").unwrap()),
989            "some_attr".into(),
990        );
991        assert_eq!(e7, ast::Expr::try_from(models::Expr::from(&e7)).unwrap());
992        let e8 = ast::Expr::has_attr(
993            ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "foo").unwrap()),
994            "some_attr".into(),
995        );
996        assert_eq!(e8, ast::Expr::try_from(models::Expr::from(&e8)).unwrap());
997        let e9 = ast::Expr::is_entity_type(
998            ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "foo").unwrap()),
999            "Type".parse().unwrap(),
1000        );
1001        assert_eq!(e9, ast::Expr::try_from(models::Expr::from(&e9)).unwrap());
1002        let e10 = ast::Expr::slot(ast::SlotId::principal());
1003        assert_eq!(e10, ast::Expr::try_from(models::Expr::from(&e10)).unwrap());
1004        let e11 = ast::Expr::slot(ast::SlotId::resource());
1005        assert_eq!(e11, ast::Expr::try_from(models::Expr::from(&e11)).unwrap());
1006        let e12 = ast::Expr::and(ast::Expr::val(false), ast::Expr::not(ast::Expr::val(true)));
1007        assert_eq!(e12, ast::Expr::try_from(models::Expr::from(&e12)).unwrap());
1008        let e13 = ast::Expr::or(
1009            ast::Expr::ite(
1010                ast::Expr::get_attr(ast::Expr::var(ast::Var::Context), "a".into()),
1011                ast::Expr::val(false),
1012                ast::Expr::not(ast::Expr::val(true)),
1013            ),
1014            ast::Expr::greater(ast::Expr::val(33), ast::Expr::val(-33)),
1015        );
1016        assert_eq!(e13, ast::Expr::try_from(models::Expr::from(&e13)).unwrap());
1017        let e14 = ast::Expr::contains(
1018            ast::Expr::set([ast::Expr::val("beans"), ast::Expr::val("carrots")]),
1019            ast::Expr::val("peas"),
1020        );
1021        assert_eq!(e14, ast::Expr::try_from(models::Expr::from(&e14)).unwrap());
1022        let e: ast::Expr = r#"ip("0.0.0.0").isInRange(ip("0.0.0.0"))"#.parse().unwrap();
1023        assert_eq!(e, ast::Expr::try_from(models::Expr::from(&e)).unwrap());
1024        let e: ast::Expr = r#"principal.foo like "bar*""#.parse().unwrap();
1025        assert_eq!(e, ast::Expr::try_from(models::Expr::from(&e)).unwrap());
1026        let e: ast::Expr = r#"principal.foo.isEmpty()"#.parse().unwrap();
1027        assert_eq!(e, ast::Expr::try_from(models::Expr::from(&e)).unwrap());
1028        let e: ast::Expr = r#"- principal.foo"#.parse().unwrap();
1029        assert_eq!(e, ast::Expr::try_from(models::Expr::from(&e)).unwrap());
1030    }
1031
1032    #[test]
1033    fn literal_roundtrip() {
1034        let bool_literal_f = ast::Literal::from(false);
1035        assert_eq!(
1036            bool_literal_f,
1037            ast::Literal::try_from(models::expr::Literal::from(&bool_literal_f)).unwrap()
1038        );
1039
1040        let bool_literal_t = ast::Literal::from(true);
1041        assert_eq!(
1042            bool_literal_t,
1043            ast::Literal::try_from(models::expr::Literal::from(&bool_literal_t)).unwrap()
1044        );
1045
1046        let long_literal0 = ast::Literal::from(0);
1047        assert_eq!(
1048            long_literal0,
1049            ast::Literal::try_from(models::expr::Literal::from(&long_literal0)).unwrap()
1050        );
1051
1052        let long_literal1 = ast::Literal::from(1);
1053        assert_eq!(
1054            long_literal1,
1055            ast::Literal::try_from(models::expr::Literal::from(&long_literal1)).unwrap()
1056        );
1057
1058        let str_literal0 = ast::Literal::from("");
1059        assert_eq!(
1060            str_literal0,
1061            ast::Literal::try_from(models::expr::Literal::from(&str_literal0)).unwrap()
1062        );
1063
1064        let str_literal1 = ast::Literal::from("foo");
1065        assert_eq!(
1066            str_literal1,
1067            ast::Literal::try_from(models::expr::Literal::from(&str_literal1)).unwrap()
1068        );
1069
1070        let euid_literal =
1071            ast::Literal::from(ast::EntityUID::with_eid_and_type("A", "foo").unwrap());
1072        assert_eq!(
1073            euid_literal,
1074            ast::Literal::try_from(models::expr::Literal::from(&euid_literal)).unwrap()
1075        );
1076    }
1077
1078    #[test]
1079    fn request_roundtrip() {
1080        let context = ast::Context::from_expr(
1081            ast::RestrictedExpr::record([("foo".into(), ast::RestrictedExpr::val(37))])
1082                .expect("Error creating restricted record.")
1083                .as_borrowed(),
1084            Extensions::none(),
1085        )
1086        .expect("Error creating context");
1087        let request = ast::Request::new_unchecked(
1088            ast::EntityUIDEntry::Known {
1089                euid: Arc::new(ast::EntityUID::with_eid_and_type("User", "andrew").unwrap()),
1090                loc: None,
1091            },
1092            ast::EntityUIDEntry::Known {
1093                euid: Arc::new(ast::EntityUID::with_eid_and_type("Action", "read").unwrap()),
1094                loc: None,
1095            },
1096            ast::EntityUIDEntry::Known {
1097                euid: Arc::new(
1098                    ast::EntityUID::with_eid_and_type("Book", "tale of two cities").unwrap(),
1099                ),
1100                loc: None,
1101            },
1102            Some(context.clone()),
1103        );
1104        let request_rt = ast::Request::try_from(models::Request::from(&request)).unwrap();
1105        assert_eq!(
1106            context,
1107            ast::Context::try_from(models::Expr::from(&context)).unwrap()
1108        );
1109        assert_eq!(request.principal().uid(), request_rt.principal().uid());
1110        assert_eq!(request.action().uid(), request_rt.action().uid());
1111        assert_eq!(request.resource().uid(), request_rt.resource().uid());
1112    }
1113
1114    #[test]
1115    fn context_ext_value() {
1116        #[track_caller]
1117        fn assert_ext_roundtrip(ext: serde_json::Value) {
1118            let ctx = Context::from_json_value(json!({ "ext": {"__extn": ext} }), None).unwrap();
1119            let req = Request::new(
1120                r#"User::"alice""#.parse().unwrap(),
1121                r#"Action::"view""#.parse().unwrap(),
1122                r#"Photo::"vacation.jpg""#.parse().unwrap(),
1123                ctx,
1124                None,
1125            )
1126            .unwrap();
1127            assert_eq!(req, Request::try_from(models::Request::from(&req)).unwrap());
1128        }
1129        assert_ext_roundtrip(json!({"fn": "ip", "arg": "127.0.0.1"}));
1130        assert_ext_roundtrip(json!({"fn": "decimal", "arg": "1.0"}));
1131        assert_ext_roundtrip(json!({"fn": "datetime", "arg": "2024-10-15"}));
1132        assert_ext_roundtrip(json!({"fn": "duration", "arg": "1s"}));
1133    }
1134
1135    #[test]
1136    fn name_try_from_invalid_basename() {
1137        let bad = models::Name {
1138            id: "".to_string(),
1139            path: vec![],
1140        };
1141        assert_matches!(
1142            ast::InternalName::try_from(bad),
1143            Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("invalid basename")
1144        );
1145    }
1146
1147    #[test]
1148    fn name_try_from_invalid_path_component() {
1149        let bad = models::Name {
1150            id: "A".to_string(),
1151            path: vec!["".to_string()],
1152        };
1153        assert_matches!(
1154            ast::InternalName::try_from(bad),
1155            Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("invalid path component")
1156        );
1157    }
1158
1159    #[test]
1160    fn test_when_missing_ty() {
1161        // models missing type field don't convert
1162        let bad = models::EntityUid {
1163            ty: None,
1164            eid: "foo".to_string(),
1165        };
1166        assert_matches!(
1167            ast::EntityUID::try_from(bad),
1168            Err(ProtobufConversionError::MissingField(f)) if f == "ty"
1169        );
1170        let bad = models::EntityUid {
1171            ty: None,
1172            eid: "foo".to_string(),
1173        };
1174        assert_matches!(
1175            ast::EntityUIDEntry::try_from(bad),
1176            Err(ProtobufConversionError::MissingField(f)) if f == "ty"
1177        );
1178    }
1179
1180    #[test]
1181    fn entity_try_from_missing_uid() {
1182        let bad = models::Entity {
1183            uid: None,
1184            attrs: Default::default(),
1185            ancestors: vec![],
1186            tags: Default::default(),
1187        };
1188        assert_matches!(
1189            ast::Entity::try_from(bad),
1190            Err(ProtobufConversionError::MissingField(f)) if f == "uid"
1191        );
1192    }
1193
1194    #[test]
1195    fn test_when_missing_expr_kind() {
1196        // Entity with invalid attr expr
1197        let bad = models::Entity {
1198            uid: Some(models::EntityUid {
1199                ty: Some(models::Name {
1200                    id: "A".to_string(),
1201                    path: vec![],
1202                }),
1203                eid: "x".to_string(),
1204            }),
1205            attrs: [("k".to_string(), models::Expr { expr_kind: None })]
1206                .into_iter()
1207                .collect(),
1208            ancestors: vec![],
1209            tags: Default::default(),
1210        };
1211        assert_matches!(
1212            ast::Entity::try_from(bad),
1213            Err(ProtobufConversionError::MissingField(f)) if f == "expr_kind"
1214        );
1215
1216        // Entity with invalid tag expr
1217        let bad = models::Entity {
1218            uid: Some(models::EntityUid {
1219                ty: Some(models::Name {
1220                    id: "A".to_string(),
1221                    path: vec![],
1222                }),
1223                eid: "x".to_string(),
1224            }),
1225            attrs: Default::default(),
1226            ancestors: vec![],
1227            tags: [("t".to_string(), models::Expr { expr_kind: None })]
1228                .into_iter()
1229                .collect(),
1230        };
1231        assert_matches!(
1232            ast::Entity::try_from(bad),
1233            Err(ProtobufConversionError::MissingField(f)) if f == "expr_kind"
1234        );
1235
1236        // Bare Expr missing expr_kind
1237        assert_matches!(
1238            ast::Expr::try_from(models::Expr { expr_kind: None }),
1239            Err(ProtobufConversionError::MissingField(f)) if f == "expr_kind"
1240        );
1241    }
1242
1243    #[test]
1244    fn expr_try_from_missing_required_fields() {
1245        // The models in each test case are missing a field, the field name is the string in the test case
1246        let cases: Vec<(models::Expr, &str)> = vec![
1247            (models::Expr { expr_kind: None }, "expr_kind"),
1248            (
1249                models::Expr {
1250                    expr_kind: Some(models::expr::ExprKind::If(Box::new(models::expr::If {
1251                        test_expr: None,
1252                        then_expr: None,
1253                        else_expr: None,
1254                    }))),
1255                },
1256                "test_expr",
1257            ),
1258            (
1259                models::Expr {
1260                    expr_kind: Some(models::expr::ExprKind::And(Box::new(models::expr::And {
1261                        left: None,
1262                        right: None,
1263                    }))),
1264                },
1265                "left",
1266            ),
1267            (
1268                models::Expr {
1269                    expr_kind: Some(models::expr::ExprKind::Or(Box::new(models::expr::Or {
1270                        left: None,
1271                        right: None,
1272                    }))),
1273                },
1274                "left",
1275            ),
1276            (
1277                models::Expr {
1278                    expr_kind: Some(models::expr::ExprKind::UApp(Box::new(
1279                        models::expr::UnaryApp {
1280                            op: models::expr::unary_app::Op::Not.into(),
1281                            expr: None,
1282                        },
1283                    ))),
1284                },
1285                "expr",
1286            ),
1287            (
1288                models::Expr {
1289                    expr_kind: Some(models::expr::ExprKind::BApp(Box::new(
1290                        models::expr::BinaryApp {
1291                            op: models::expr::binary_app::Op::Eq.into(),
1292                            left: None,
1293                            right: None,
1294                        },
1295                    ))),
1296                },
1297                "left",
1298            ),
1299            (
1300                models::Expr {
1301                    expr_kind: Some(models::expr::ExprKind::ExtApp(
1302                        models::expr::ExtensionFunctionApp {
1303                            fn_name: None,
1304                            args: vec![],
1305                        },
1306                    )),
1307                },
1308                "fn_name",
1309            ),
1310            (
1311                models::Expr {
1312                    expr_kind: Some(models::expr::ExprKind::GetAttr(Box::new(
1313                        models::expr::GetAttr {
1314                            expr: None,
1315                            attr: "a".to_string(),
1316                        },
1317                    ))),
1318                },
1319                "expr",
1320            ),
1321            (
1322                models::Expr {
1323                    expr_kind: Some(models::expr::ExprKind::HasAttr(Box::new(
1324                        models::expr::HasAttr {
1325                            expr: None,
1326                            attr: "a".to_string(),
1327                        },
1328                    ))),
1329                },
1330                "expr",
1331            ),
1332            (
1333                models::Expr {
1334                    expr_kind: Some(models::expr::ExprKind::Like(Box::new(models::expr::Like {
1335                        expr: None,
1336                        pattern: vec![],
1337                    }))),
1338                },
1339                "expr",
1340            ),
1341            (
1342                models::Expr {
1343                    expr_kind: Some(models::expr::ExprKind::Is(Box::new(models::expr::Is {
1344                        expr: None,
1345                        entity_type: None,
1346                    }))),
1347                },
1348                "expr",
1349            ),
1350        ];
1351
1352        for (bad, expected_field) in cases {
1353            assert_matches!(
1354                ast::Expr::try_from(bad),
1355                Err(ProtobufConversionError::MissingField(f)) if f == expected_field
1356            );
1357        }
1358    }
1359
1360    #[test]
1361    fn expr_try_from_invalid_enum_values() {
1362        let cases: Vec<(models::Expr, &str)> = vec![
1363            (
1364                models::Expr {
1365                    expr_kind: Some(models::expr::ExprKind::Var(999)),
1366                },
1367                "invalid var",
1368            ),
1369            (
1370                models::Expr {
1371                    expr_kind: Some(models::expr::ExprKind::Slot(999)),
1372                },
1373                "invalid slot",
1374            ),
1375        ];
1376
1377        for (bad, expected_msg) in cases {
1378            assert_matches!(
1379                ast::Expr::try_from(bad),
1380                Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains(expected_msg)
1381            );
1382        }
1383    }
1384
1385    #[test]
1386    fn literal_try_from_missing_lit() {
1387        assert_matches!(
1388            ast::Literal::try_from(models::expr::Literal { lit: None }),
1389            Err(ProtobufConversionError::MissingField(f)) if f == "lit"
1390        );
1391    }
1392
1393    #[test]
1394    fn pattern_elem_try_from_missing_data() {
1395        assert_matches!(
1396            ast::PatternElem::try_from(models::expr::like::PatternElem { data: None }),
1397            Err(ProtobufConversionError::MissingField(f)) if f == "data"
1398        );
1399    }
1400
1401    #[test]
1402    fn pattern_elem_try_from_empty_char() {
1403        let bad = models::expr::like::PatternElem {
1404            data: Some(models::expr::like::pattern_elem::Data::C(String::new())),
1405        };
1406        assert_matches!(
1407            ast::PatternElem::try_from(bad),
1408            Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("empty char")
1409        );
1410    }
1411
1412    #[test]
1413    fn request_try_from_missing_principal() {
1414        let bad = models::Request {
1415            principal: None,
1416            action: Some(models::EntityUid {
1417                ty: Some(models::Name {
1418                    id: "Action".to_string(),
1419                    path: vec![],
1420                }),
1421                eid: "a".to_string(),
1422            }),
1423            resource: Some(models::EntityUid {
1424                ty: Some(models::Name {
1425                    id: "R".to_string(),
1426                    path: vec![],
1427                }),
1428                eid: "r".to_string(),
1429            }),
1430            context: Default::default(),
1431        };
1432        assert_matches!(
1433            ast::Request::try_from(bad),
1434            Err(ProtobufConversionError::MissingField(f)) if f == "principal"
1435        );
1436    }
1437
1438    #[test]
1439    fn context_try_from_missing_expr_kind() {
1440        let bad = models::Expr { expr_kind: None };
1441        assert_matches!(
1442            ast::Context::try_from(bad),
1443            Err(ProtobufConversionError::MissingField(f)) if f == "expr_kind"
1444        );
1445    }
1446}