1use std::{collections::HashSet, fmt::Display};
21
22use itertools::Itertools;
23use miette::Diagnostic;
24use nonempty::NonEmpty;
25use thiserror::Error;
26
27use crate::{json_schema, RawName};
28use cedar_policy_core::{ast::InternalName, impl_diagnostic_from_method_on_nonempty_field};
29
30impl<N: Display> Display for json_schema::Fragment<N> {
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} {{\n{}}}\n", def.annotations, def)?,
36 }
37 }
38 Ok(())
39 }
40}
41
42impl<N: Display> Display for json_schema::NamespaceDefinition<N> {
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.annotations, ty.ty)?
46 }
47 for (n, ty) in &self.entity_types {
48 writeln!(f, "{}entity {n}{};", ty.annotations, ty)?
49 }
50 for (n, a) in &self.actions {
51 writeln!(f, "{}action \"{}\"{};", a.annotations, n.escape_debug(), a)?
52 }
53 Ok(())
54 }
55}
56
57impl<N: Display> Display for json_schema::Type<N> {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 json_schema::Type::Type { ty, .. } => match ty {
61 json_schema::TypeVariant::Boolean => write!(f, "__cedar::Bool"),
62 json_schema::TypeVariant::Entity { name } => write!(f, "{name}"),
63 json_schema::TypeVariant::EntityOrCommon { type_name } => {
64 write!(f, "{type_name}")
65 }
66 json_schema::TypeVariant::Extension { name } => write!(f, "__cedar::{name}"),
67 json_schema::TypeVariant::Long => write!(f, "__cedar::Long"),
68 json_schema::TypeVariant::Record(rty) => write!(f, "{rty}"),
69 json_schema::TypeVariant::Set { element } => write!(f, "Set < {element} >"),
70 json_schema::TypeVariant::String => write!(f, "__cedar::String"),
71 },
72 json_schema::Type::CommonTypeRef { type_name, .. } => write!(f, "{type_name}"),
73 }
74 }
75}
76
77impl<N: Display> Display for json_schema::RecordType<N> {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "{{")?;
80 for (i, (n, ty)) in self.attributes.iter().enumerate() {
81 write!(
82 f,
83 "{}\"{}\"{}: {}",
84 ty.annotations,
85 n.escape_debug(),
86 if ty.required { "" } else { "?" },
87 ty.ty
88 )?;
89 if i < (self.attributes.len() - 1) {
90 writeln!(f, ", ")?;
91 }
92 }
93 write!(f, "}}")?;
94 Ok(())
95 }
96}
97
98fn fmt_non_empty_slice<T: Display>(
99 f: &mut std::fmt::Formatter<'_>,
100 (head, tail): (&T, &[T]),
101) -> std::fmt::Result {
102 write!(f, "[{head}")?;
103 for e in tail {
104 write!(f, ", {e}")?;
105 }
106 write!(f, "]")
107}
108
109impl<N: Display> Display for json_schema::EntityType<N> {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match &self.kind {
112 json_schema::EntityTypeKind::Standard(ty) => ty.fmt(f),
113 json_schema::EntityTypeKind::Enum { choices } => write!(
114 f,
115 " enum [{}]",
116 choices
117 .iter()
118 .map(|e| format!("\"{}\"", e.escape_debug()))
119 .join(", ")
120 ),
121 }
122 }
123}
124
125impl<N: Display> Display for json_schema::StandardEntityType<N> {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 if let Some(non_empty) = self.member_of_types.split_first() {
128 write!(f, " in ")?;
129 fmt_non_empty_slice(f, non_empty)?;
130 }
131
132 let ty = &self.shape;
133 if !ty.is_empty_record() {
135 write!(f, " = {ty}")?;
136 }
137
138 if let Some(tags) = &self.tags {
139 write!(f, " tags {tags}")?;
140 }
141
142 Ok(())
143 }
144}
145
146impl<N: Display> Display for json_schema::ActionType<N> {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 if let Some(parents) = self.member_of.as_ref().and_then(|refs| refs.split_first()) {
149 write!(f, " in ")?;
150 fmt_non_empty_slice(f, parents)?;
151 }
152 if let Some(spec) = &self.applies_to {
153 match (
154 spec.principal_types.split_first(),
155 spec.resource_types.split_first(),
156 ) {
157 (None, _) | (_, None) => {
161 write!(f, "")?;
162 }
163 (Some(ps), Some(rs)) => {
165 write!(f, " appliesTo {{")?;
166 write!(f, "\n principal: ")?;
167 fmt_non_empty_slice(f, ps)?;
168 write!(f, ",\n resource: ")?;
169 fmt_non_empty_slice(f, rs)?;
170 write!(f, ",\n context: {}", &spec.context.0)?;
171 write!(f, "\n}}")?;
172 }
173 }
174 }
175 Ok(())
177 }
178}
179
180#[derive(Debug, Diagnostic, Error)]
182pub enum ToCedarSchemaSyntaxError {
183 #[diagnostic(transparent)]
185 #[error(transparent)]
186 NameCollisions(#[from] NameCollisionsError),
187}
188
189#[derive(Debug, Error)]
193#[error("There are name collisions: [{}]", .names.iter().join(", "))]
194pub struct NameCollisionsError {
195 names: NonEmpty<InternalName>,
197}
198
199impl Diagnostic for NameCollisionsError {
200 impl_diagnostic_from_method_on_nonempty_field!(names, loc);
201}
202
203impl NameCollisionsError {
204 pub fn names(&self) -> impl Iterator<Item = &InternalName> {
206 self.names.iter()
207 }
208}
209
210pub fn json_schema_to_cedar_schema_str<N: Display>(
226 json_schema: &json_schema::Fragment<N>,
227) -> Result<String, ToCedarSchemaSyntaxError> {
228 let mut name_collisions: Vec<InternalName> = Vec::new();
229 for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_none()) {
230 let entity_types: HashSet<InternalName> = ns
231 .entity_types
232 .keys()
233 .map(|ty_name| {
234 RawName::new_from_unreserved(ty_name.clone(), None).qualify_with_name(name.as_ref())
235 })
236 .collect();
237 let common_types: HashSet<InternalName> = ns
238 .common_types
239 .keys()
240 .map(|ty_name| {
241 RawName::new_from_unreserved(ty_name.clone().into(), None)
242 .qualify_with_name(name.as_ref())
243 })
244 .collect();
245 name_collisions.extend(entity_types.intersection(&common_types).cloned());
246 }
247 if let Some(non_empty_collisions) = NonEmpty::from_vec(name_collisions) {
248 return Err(NameCollisionsError {
249 names: non_empty_collisions,
250 }
251 .into());
252 }
253 Ok(json_schema.to_string())
254}
255
256#[cfg(test)]
257mod tests {
258 use cedar_policy_core::extensions::Extensions;
259
260 use crate::{cedar_schema::parser::parse_cedar_schema_fragment, json_schema, RawName};
261
262 use similar_asserts::assert_eq;
263
264 #[track_caller]
265 fn test_round_trip(src: &str) {
266 let (cedar_schema, _) =
267 parse_cedar_schema_fragment(src, Extensions::none()).expect("should parse");
268 let printed_cedar_schema = cedar_schema.to_cedarschema().expect("should convert");
269 let (parsed_cedar_schema, _) =
270 parse_cedar_schema_fragment(&printed_cedar_schema, Extensions::none())
271 .expect("should parse");
272 assert_eq!(cedar_schema, parsed_cedar_schema);
273 }
274
275 #[test]
276 fn rfc_example() {
277 let src = "entity User = {
278 jobLevel: Long,
279 } tags Set<String>;
280 entity Document = {
281 owner: User,
282 } tags Set<String>;";
283 test_round_trip(src);
284 }
285
286 #[test]
287 fn annotations() {
288 let src = r#"@doc("this is the namespace")
289namespace TinyTodo {
290 @doc("a common type representing a task")
291 type Task = {
292 @doc("task id")
293 "id": Long,
294 "name": String,
295 "state": String,
296 };
297 @doc("a common type representing a set of tasks")
298 type Tasks = Set<Task>;
299
300 @doc("an entity type representing a list")
301 @docComment("any entity type is a child of type `Application`")
302 entity List in [Application] = {
303 @doc("editors of a list")
304 "editors": Team,
305 "name": String,
306 "owner": User,
307 @doc("readers of a list")
308 "readers": Team,
309 "tasks": Tasks,
310 };
311
312 @doc("actions that a user can operate on a list")
313 action DeleteList, GetList, UpdateList appliesTo {
314 principal: [User],
315 resource: [List]
316 };
317}"#;
318 test_round_trip(src);
319 }
320
321 #[test]
322 fn attrs_types_roundtrip() {
323 test_round_trip(r#"entity Foo {a: Bool};"#);
324 test_round_trip(r#"entity Foo {a: Long};"#);
325 test_round_trip(r#"entity Foo {a: String};"#);
326 test_round_trip(r#"entity Foo {a: Set<Bool>};"#);
327 test_round_trip(r#"entity Foo {a: {b: Long}};"#);
328 test_round_trip(r#"entity Foo {a: {}};"#);
329 test_round_trip(
330 r#"
331 type A = Long;
332 entity Foo {a: A};
333 "#,
334 );
335 test_round_trip(
336 r#"
337 entity A;
338 entity Foo {a: A};
339 "#,
340 );
341 }
342
343 #[test]
344 fn enum_entities_roundtrip() {
345 test_round_trip(r#"entity Foo enum ["Bar", "Baz"];"#);
346 test_round_trip(r#"entity Foo enum ["Bar"];"#);
347 test_round_trip(r#"entity Foo enum ["\0\n\x7f"];"#);
348 test_round_trip(r#"entity enum enum ["enum"];"#);
349 }
350
351 #[test]
352 fn action_in_roundtrip() {
353 test_round_trip(r#"action Delete in Action::"Edit";"#);
354 test_round_trip(r#"action Delete in Action::"\n\x00";"#);
355 test_round_trip(r#"action Delete in [Action::"Edit", Action::"Destroy"];"#);
356 }
357
358 #[test]
359 fn primitives_roundtrip_to_entity_or_common() {
360 let schema_json = serde_json::json!(
362 {
363 "": {
364 "entityTypes": {
365 "User": { },
366 "Photo": {
367 "shape": {
368 "type": "Record",
369 "attributes": {
370 "foo": { "type": "Long" },
371 "bar": { "type": "String" },
372 "baz": { "type": "Boolean" }
373 }
374 }
375 }
376 },
377 "actions": {}
378 }
379 }
380 );
381
382 let fragment: json_schema::Fragment<RawName> = serde_json::from_value(schema_json).unwrap();
383 let cedar_schema = fragment.to_cedarschema().unwrap();
384
385 let (parsed_cedar_schema, _) =
386 parse_cedar_schema_fragment(&cedar_schema, Extensions::all_available()).unwrap();
387
388 let roundtrip_json = serde_json::to_value(parsed_cedar_schema).unwrap();
389 let expected_roundtrip = serde_json::json!(
390 {
391 "": {
392 "entityTypes": {
393 "User": { },
394 "Photo": {
395 "shape": {
396 "type": "Record",
397 "attributes": {
398 "foo": {
399 "type": "EntityOrCommon",
400 "name": "__cedar::Long"
401 },
402 "bar": {
403 "type": "EntityOrCommon",
404 "name": "__cedar::String"
405 },
406 "baz": {
407 "type": "EntityOrCommon",
408 "name": "__cedar::Bool"
409 }
410 }
411 }
412 }
413 },
414 "actions": {}
415 }
416 }
417 );
418
419 assert_eq!(expected_roundtrip, roundtrip_json,);
420 }
421
422 #[test]
423 fn entity_type_reference_roundtrips_to_entity_or_common() {
424 let schema_json = serde_json::json!(
426 {
427 "": {
428 "entityTypes": {
429 "User": { },
430 "Photo": {
431 "shape": {
432 "type": "Record",
433 "attributes": {
434 "owner": {
435 "type": "Entity",
436 "name": "User"
437 }
438 }
439 }
440 }
441 },
442 "actions": {}
443 }
444 }
445 );
446
447 let fragment: json_schema::Fragment<RawName> = serde_json::from_value(schema_json).unwrap();
448 let cedar_schema = fragment.to_cedarschema().unwrap();
449
450 let (parsed_cedar_schema, _) =
451 parse_cedar_schema_fragment(&cedar_schema, Extensions::all_available()).unwrap();
452
453 let roundtrip_json = serde_json::to_value(parsed_cedar_schema).unwrap();
454 let expected_roundtrip = serde_json::json!(
455 {
456 "": {
457 "entityTypes": {
458 "User": { },
459 "Photo": {
460 "shape": {
461 "type": "Record",
462 "attributes": {
463 "owner": {
464 "type": "EntityOrCommon",
465 "name": "User"
466 }
467 }
468 }
469 }
470 },
471 "actions": {}
472 }
473 }
474 );
475
476 assert_eq!(expected_roundtrip, roundtrip_json,);
477 }
478
479 #[test]
480 fn extension_type_roundtrips_to_entity_or_common() {
481 let schema_json = serde_json::json!(
483 {
484 "": {
485 "entityTypes": {
486 "User": { },
487 "Photo": {
488 "shape": {
489 "type": "Record",
490 "attributes": {
491 "owner": {
492 "type": "Extension",
493 "name": "Decimal"
494 }
495 }
496 }
497 }
498 },
499 "actions": {}
500 }
501 }
502 );
503
504 let fragment: json_schema::Fragment<RawName> = serde_json::from_value(schema_json).unwrap();
505 let cedar_schema = fragment.to_cedarschema().unwrap();
506
507 let (parsed_cedar_schema, _) =
508 parse_cedar_schema_fragment(&cedar_schema, Extensions::all_available()).unwrap();
509
510 let roundtrip_json = serde_json::to_value(parsed_cedar_schema).unwrap();
511 let expected_roundtrip = serde_json::json!(
512 {
513 "": {
514 "entityTypes": {
515 "User": { },
516 "Photo": {
517 "shape": {
518 "type": "Record",
519 "attributes": {
520 "owner": {
521 "type": "EntityOrCommon",
522 "name": "__cedar::Decimal"
523 }
524 }
525 }
526 }
527 },
528 "actions": {}
529 }
530 }
531 );
532
533 assert_eq!(expected_roundtrip, roundtrip_json,);
534 }
535}