1#![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#[derive(Debug, thiserror::Error)]
32pub enum ProtobufConversionError {
33 #[error("missing required field `{0}`")]
35 MissingField(String),
36 #[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 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 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 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 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 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}