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