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