cedar_policy_validator/human_schema/
fmt.rs1use std::{collections::HashSet, fmt::Display};
18
19use cedar_policy_core::ast::Name;
20use itertools::Itertools;
21use miette::Diagnostic;
22use nonempty::NonEmpty;
23use smol_str::{SmolStr, ToSmolStr};
24use thiserror::Error;
25
26use crate::{
27 ActionType, EntityType, NamespaceDefinition, SchemaFragment, SchemaType, SchemaTypeVariant,
28};
29
30impl Display for SchemaFragment {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 for (ns, def) in &self.0 {
33 match ns {
34 None => write!(f, "{def}")?,
35 Some(ns) => write!(f, "namespace {ns} {{{def}}}")?,
36 }
37 }
38 Ok(())
39 }
40}
41
42impl Display for NamespaceDefinition {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 for (n, ty) in &self.common_types {
45 writeln!(f, "type {n} = {ty};")?
46 }
47 for (n, ty) in &self.entity_types {
48 writeln!(f, "entity {n}{ty};")?
49 }
50 for (n, a) in &self.actions {
51 writeln!(f, "action \"{}\"{a};", n.escape_debug())?
52 }
53 Ok(())
54 }
55}
56
57impl Display for SchemaType {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 SchemaType::Type(ty) => match ty {
61 SchemaTypeVariant::Boolean => write!(f, "__cedar::Bool"),
62 SchemaTypeVariant::Entity { name } => write!(f, "{name}"),
63 SchemaTypeVariant::Extension { name } => write!(f, "__cedar::{name}"),
64 SchemaTypeVariant::Long => write!(f, "__cedar::Long"),
65 SchemaTypeVariant::Record {
66 attributes,
67 additional_attributes: _,
68 } => {
69 write!(f, "{{")?;
70 for (i, (n, ty)) in attributes.iter().enumerate() {
71 write!(
72 f,
73 "\"{}\"{}: {}",
74 n.escape_debug(),
75 if ty.required { "" } else { "?" },
76 ty.ty
77 )?;
78 if i < (attributes.len() - 1) {
79 write!(f, ", ")?;
80 }
81 }
82 write!(f, "}}")?;
83 Ok(())
84 }
85 SchemaTypeVariant::Set { element } => write!(f, "Set < {element} >"),
86 SchemaTypeVariant::String => write!(f, "__cedar::String"),
87 },
88 SchemaType::TypeDef { type_name } => write!(f, "{type_name}"),
89 }
90 }
91}
92
93fn non_empty_slice<T>(v: &[T]) -> Option<NonEmpty<&T>> {
95 let vs: Vec<&T> = v.iter().collect();
96 NonEmpty::from_vec(vs)
97}
98
99fn fmt_vec<T: Display>(f: &mut std::fmt::Formatter<'_>, ets: NonEmpty<T>) -> std::fmt::Result {
100 let contents = ets.iter().map(T::to_string).join(", ");
101 write!(f, "[{contents}]")
102}
103
104impl Display for EntityType {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 if let Some(non_empty) = non_empty_slice(&self.member_of_types) {
107 write!(f, " in ")?;
108 fmt_vec(f, non_empty)?;
109 }
110
111 let ty = &self.shape.0;
112 if !ty.is_empty_record() {
114 write!(f, " = {ty}")?;
115 }
116
117 Ok(())
118 }
119}
120
121impl Display for ActionType {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 if let Some(parents) = self
124 .member_of
125 .as_ref()
126 .and_then(|refs| non_empty_slice(refs.as_slice()))
127 {
128 write!(f, " in ")?;
129 fmt_vec(f, parents)?;
130 }
131 if let Some(spec) = &self.applies_to {
132 match (
133 spec.principal_types
134 .as_ref()
135 .map(|refs| non_empty_slice(refs.as_slice())),
136 spec.resource_types
137 .as_ref()
138 .map(|refs| non_empty_slice(refs.as_slice())),
139 ) {
140 (Some(None), _) | (_, Some(None)) => {
144 write!(f, "")?;
145 }
146 (Some(Some(ps)), Some(Some(rs))) => {
148 write!(f, " appliesTo {{")?;
149 write!(f, "\n principal: ")?;
150 fmt_vec(f, ps)?;
151 write!(f, ",\n resource: ")?;
152 fmt_vec(f, rs)?;
153 write!(f, ",\n context: {}", &spec.context.0)?;
154 write!(f, "\n}}")?;
155 }
156 (Some(Some(ps)), None) => {
158 write!(f, " appliesTo {{")?;
159 write!(f, "\n principal: ")?;
160 fmt_vec(f, ps)?;
161 write!(f, ",\n context: {}", &spec.context.0)?;
162 write!(f, "\n}}")?;
163 }
164 (None, Some(Some(rs))) => {
166 write!(f, " appliesTo {{")?;
167 write!(f, "\n resource: ")?;
168 fmt_vec(f, rs)?;
169 write!(f, ",\n context: {}", &spec.context.0)?;
170 write!(f, "\n}}")?;
171 }
172 (None, None) => {
174 write!(f, " appliesTo {{")?;
175 write!(f, "\n context: {}", &spec.context.0)?;
176 write!(f, "\n}}")?;
177 }
178 }
179 } else {
180 write!(f, " appliesTo {{")?;
182 write!(f, "\n context: {{}}")?;
184 write!(f, "\n}}")?;
185 }
186 Ok(())
187 }
188}
189
190#[derive(Debug, Diagnostic, Error)]
191pub enum ToHumanSchemaStrError {
192 #[error("There exist type name collisions: {:?}", .0)]
193 NameCollisions(NonEmpty<SmolStr>),
194}
195
196pub fn json_schema_to_custom_schema_str(
197 json_schema: &SchemaFragment,
198) -> Result<String, ToHumanSchemaStrError> {
199 let mut name_collisions: Vec<SmolStr> = Vec::new();
200 for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_none()) {
201 let entity_types: HashSet<SmolStr> = ns
202 .entity_types
203 .keys()
204 .map(|ty_name| {
205 Name::unqualified_name(ty_name.clone())
206 .prefix_namespace_if_unqualified(name.clone())
207 .to_smolstr()
208 })
209 .collect();
210 let common_types: HashSet<SmolStr> = ns
211 .common_types
212 .keys()
213 .map(|ty_name| {
214 Name::unqualified_name(ty_name.clone())
215 .prefix_namespace_if_unqualified(name.clone())
216 .to_smolstr()
217 })
218 .collect();
219 name_collisions.extend(entity_types.intersection(&common_types).cloned());
220 }
221 if let Some((head, tail)) = name_collisions.split_first() {
222 return Err(ToHumanSchemaStrError::NameCollisions(NonEmpty {
223 head: head.clone(),
224 tail: tail.to_vec(),
225 }));
226 }
227 Ok(json_schema.to_string())
228}