Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cedar-policy-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to Cedar CLI tool will be documented in this file.
Changes to the Cedar language, which are likely to affect users of the CLI, are documented separately in the [primary changelog](../cedar-policy/CHANGELOG.md).

## Unreleased
- Added a new option when converting cedar schemas to JSON that allows the caller to get back a JSON schema where all types are resolved to commontype or entity (never entityOrCommon).

## 4.8.2

Expand Down
26 changes: 26 additions & 0 deletions cedar-policy-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ pub enum SchemaTranslationDirection {
JsonToCedar,
/// Cedar schema syntax -> JSON
CedarToJson,
/// Cedar schema syntax -> JSON with all types resolved to entity or common.
///
/// In contrast to `cedar-to-json`, this option requires that every type
/// referenced in the schema is also defined.
CedarToJsonWithResolvedTypes,
}

#[derive(Debug, Default, Clone, Copy, ValueEnum)]
Expand Down Expand Up @@ -1251,10 +1256,31 @@ fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
Ok(output)
}

fn translate_schema_to_json_with_resolved_types(cedar_src: impl AsRef<str>) -> Result<String> {
match cedar_policy::schema_str_to_json_with_resolved_types(cedar_src.as_ref()) {
Ok((json_value, warnings)) => {
// Output warnings to stderr
for warning in &warnings {
eprintln!("{warning}");
}

// Serialize to JSON with pretty formatting
serde_json::to_string_pretty(&json_value).into_diagnostic()
}
Err(error) => {
// Convert CedarSchemaError to miette::Report to preserve all diagnostic information
Err(miette::Report::new(error))
}
}
}

fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
let translate = match args.direction {
SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
SchemaTranslationDirection::CedarToJsonWithResolvedTypes => {
translate_schema_to_json_with_resolved_types
}
};
read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
}
Expand Down
181 changes: 181 additions & 0 deletions cedar-policy-cli/tests/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,187 @@ fn test_translate_schema() {
.code(0);
}

#[test]
fn test_translate_schema_with_resolved_types() {
let cedar_filename = "sample-data/tiny_sandboxes/translate-schema/tinytodo.cedarschema";

// Test cedar -> json with resolved types
let output = cargo::cargo_bin_cmd!("cedar")
.arg("translate-schema")
.arg("--direction")
.arg("cedar-to-json-with-resolved-types")
.arg("-s")
.arg(cedar_filename)
.assert()
.code(0);

let json_output =
std::str::from_utf8(&output.get_output().stdout).expect("output should be decodable");

// Parse the JSON to ensure it's valid
let parsed_json: serde_json::Value =
serde_json::from_str(json_output).expect("output should be valid JSON");

// Verify that the output contains resolved types (no "EntityOrCommon" strings)
let json_str = serde_json::to_string(&parsed_json).unwrap();
assert!(
!json_str.contains("EntityOrCommon"),
"Output should not contain unresolved EntityOrCommon types: {}",
json_str
);

// Verify that the output contains expected entity types
assert!(
json_str.contains("List"),
"Output should contain List entity type"
);
assert!(
json_str.contains("User"),
"Output should contain User entity type"
);
assert!(
json_str.contains("Team"),
"Output should contain Team entity type"
);
assert!(
json_str.contains("Application"),
"Output should contain Application entity type"
);
}
#[test]

fn test_translate_schema_with_resolved_types_stdin() {
// Test with stdin input
let stdin_output = cargo::cargo_bin_cmd!("cedar")
.arg("translate-schema")
.arg("--direction")
.arg("cedar-to-json-with-resolved-types")
.write_stdin("entity User; action \"view\";")
.assert()
.code(0);

let stdin_json = std::str::from_utf8(&stdin_output.get_output().stdout)
.expect("stdin output should be decodable");

// Parse and verify stdin JSON
let stdin_parsed: serde_json::Value =
serde_json::from_str(stdin_json).expect("stdin output should be valid JSON");

let stdin_json_str = serde_json::to_string(&stdin_parsed).unwrap();
assert!(
!stdin_json_str.contains("EntityOrCommon"),
"Stdin output should not contain unresolved EntityOrCommon types"
);
assert!(
stdin_json_str.contains("User"),
"Stdin output should contain User entity type"
);
}

#[test]
fn test_translate_schema_with_resolved_types_invalid_input() {
// Test with invalid Cedar schema syntax
let invalid_output = cargo::cargo_bin_cmd!("cedar")
.arg("translate-schema")
.arg("--direction")
.arg("cedar-to-json-with-resolved-types")
.write_stdin("invalid cedar syntax {")
.assert()
.code(1); // Should fail with non-zero exit code

let stderr = std::str::from_utf8(&invalid_output.get_output().stderr)
.expect("stderr should be decodable");

// Should contain error message about parsing failure
assert!(
stderr.contains("error parsing schema"),
"Error message should indicate parsing failure: {}",
stderr
);
}

#[test]
fn test_translate_schema_with_resolved_types_warnings() {
// Create a schema that generates warnings (Shadowing a primitive type with an entity)
let schema_with_warnings = r#"
entity String;
"#;

let output = cargo::cargo_bin_cmd!("cedar")
.arg("translate-schema")
.arg("--direction")
.arg("cedar-to-json-with-resolved-types")
.write_stdin(schema_with_warnings)
.assert()
.code(0); // Should succeed despite warnings

let json_output =
std::str::from_utf8(&output.get_output().stdout).expect("output should be decodable");

// Should produce valid JSON
let _parsed_json: serde_json::Value =
serde_json::from_str(json_output).expect("output should be valid JSON");

// Warnings should be output to stderr (if any)
let _stderr =
std::str::from_utf8(&output.get_output().stderr).expect("stderr should be decodable");

assert!(_stderr.contains("The name `String` shadows a builtin Cedar name"));
}

#[test]
fn test_translate_schema_with_resolved_types_file_errors() {
// Test with non-existent input file
let nonexistent_output = cargo::cargo_bin_cmd!("cedar")
.arg("translate-schema")
.arg("--direction")
.arg("cedar-to-json-with-resolved-types")
.arg("-s")
.arg("nonexistent-file.cedarschema")
.assert()
.code(1); // Should fail with non-zero exit code

let stderr = std::str::from_utf8(&nonexistent_output.get_output().stderr)
.expect("stderr should be decodable");

// Should contain error message about file not found
assert!(
stderr.contains("nonexistent-file.cedarschema") || stderr.contains("No such file"),
"Error message should indicate file not found: {}",
stderr
);
}

#[test]
fn test_translate_schema_with_resolved_types_unresolvable_references() {
// Test with schema that has unresolvable type references
let unresolvable_schema = r#"
entity User = { "manager": Manager };
// Manager entity type is not defined
action "view";
"#;

let output = cargo::cargo_bin_cmd!("cedar")
.arg("translate-schema")
.arg("--direction")
.arg("cedar-to-json-with-resolved-types")
.write_stdin(unresolvable_schema)
.assert()
.code(1); // Should fail with non-zero exit code

let stderr =
std::str::from_utf8(&output.get_output().stderr).expect("stderr should be decodable");

// Should contain error message about unresolvable references
assert!(
stderr.contains("Failed to resolve schema types")
|| stderr.contains("Manager")
|| stderr.contains("resolve"),
"Error message should indicate unresolvable type reference: {}",
stderr
);
}

#[rstest]
fn visualize_entities_parses_as_dot(
#[files("sample-data/**/entities.json")]
Expand Down
15 changes: 7 additions & 8 deletions cedar-policy-core/src/validator/json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ impl Fragment<RawName> {
}

/// Convert this `Fragment<RawName>` to a `Fragment<InternalName>` where all the
/// entity or common type references have been resolved
/// entity or common type references have been resolved. If the fragment contains any type
/// references that are not defined, this will return Err.
pub fn to_internal_name_fragment_with_resolved_types(
&self,
) -> std::result::Result<Fragment<InternalName>, SchemaError> {
Expand All @@ -218,7 +219,12 @@ impl Fragment<RawName> {
.collect();

for tyname in &primitives_as_internal_names {
// Add __cedar-prefixed primitives as commontypes
all_defs.mark_as_defined_as_common_type(tyname.qualify_with(Some(&cedar_namespace)));
// Add aliases for primitive types in the empty namespace (so "String" resolves to "__cedar::String")
if !all_defs.is_defined_as_common(tyname) && !all_defs.is_defined_as_entity(tyname) {
all_defs.mark_as_defined_as_common_type(tyname.clone());
}
}

// Add extension types in __cedar namespace and also without
Expand All @@ -234,13 +240,6 @@ impl Fragment<RawName> {
}
}

// Add aliases for primitive types in the empty namespace (so "String" resolves to "__cedar::String")
for tyname in &primitives_as_internal_names {
if !all_defs.is_defined_as_common(tyname) && !all_defs.is_defined_as_entity(tyname) {
all_defs.mark_as_defined_as_common_type(tyname.clone());
}
}

// Step 1: Convert Fragment<RawName> to Fragment<ConditionalName>
let conditional_fragment = Fragment(
self.0
Expand Down
Loading