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
2 changes: 1 addition & 1 deletion cedar-policy-validator/src/human_schema/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ AppDecls: Node<NonEmpty<Node<AppDecl>>> = {
// SetType := 'Set' '<' Type '>'
// RecType := '{' [AttrDecls] '}'
// Type := PRIMTYPE | Path | SetType | RecType
Type: Node<SType> = {
pub Type: Node<SType> = {
<p:Path>
=> { let loc = p.loc().clone(); Node::with_source_loc(SType::Ident(p), loc) },
<l:@L> SET "<" <t:Type> ">" <r:@R>
Expand Down
15 changes: 10 additions & 5 deletions cedar-policy-validator/src/human_schema/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ use lalrpop_util::lalrpop_mod;
use miette::Diagnostic;
use thiserror::Error;

use crate::human_schema::to_json_schema::custom_schema_to_json_schema;

use super::{
ast::Schema,
err::{self, ParseError, ParseErrors, SchemaWarning, ToJsonSchemaErrors},
to_json_schema::{custom_schema_to_json_schema, custom_type_to_json_type},
};

lalrpop_mod!(
Expand Down Expand Up @@ -76,7 +75,8 @@ fn parse_collect_errors<'a, P, T>(

// Thread-safe "global" parsers, initialized at first use
lazy_static::lazy_static! {
static ref POLICIES_PARSER: grammar::SchemaParser = grammar::SchemaParser::new();
static ref SCHEMA_PARSER: grammar::SchemaParser = grammar::SchemaParser::new();
static ref TYPE_PARSER: grammar::TypeParser = grammar::TypeParser::new();
}

#[derive(Debug, Diagnostic, Error)]
Expand All @@ -89,15 +89,20 @@ pub enum HumanSyntaxParseErrors {
JsonError(#[from] ToJsonSchemaErrors),
}

pub fn parse_type(src: &str) -> Result<crate::SchemaType, HumanSyntaxParseErrors> {
let ty = parse_collect_errors(&*TYPE_PARSER, grammar::TypeParser::parse, src)?;
Ok(custom_type_to_json_type(ty)?)
}

pub fn parse_natural_schema_fragment(
src: &str,
) -> Result<(crate::SchemaFragment, impl Iterator<Item = SchemaWarning>), HumanSyntaxParseErrors> {
let ast: Schema = parse_collect_errors(&*POLICIES_PARSER, grammar::SchemaParser::parse, src)?;
let ast: Schema = parse_collect_errors(&*SCHEMA_PARSER, grammar::SchemaParser::parse, src)?;
let tuple = custom_schema_to_json_schema(ast)?;
Ok(tuple)
}

/// Parse schema from text
pub fn parse_schema(text: &str) -> Result<Schema, err::ParseErrors> {
parse_collect_errors(&*POLICIES_PARSER, grammar::SchemaParser::parse, text)
parse_collect_errors(&*SCHEMA_PARSER, grammar::SchemaParser::parse, text)
}
14 changes: 14 additions & 0 deletions cedar-policy-validator/src/human_schema/to_json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ pub fn custom_schema_to_json_schema(
Ok((SchemaFragment(fragment), warnings.into_iter()))
}

/// Convert a custom type AST into the JSON representation of the type.
/// Conversion is done in an empty context.
pub fn custom_type_to_json_type(ty: Node<Type>) -> Result<SchemaType, ToJsonSchemaErrors> {
Comment on lines +50 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why "custom" type? Isn't it just the internal Cedar type?

(Context: I was confused by the name at first because I was trying to figure out how it related to commonType definitions in the JSON / type declarations in the new schema syntax)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just copied the phrasing from another function in this file. We could perhaps do a consistency pass so that we call the new schema format one name everywhere.

let names = HashMap::from([("".into(), NamespaceRecord::default())]);
let context = ConversionContext::new(
&names,
&Namespace {
name: None,
decls: vec![],
},
);
context.convert_type(ty)
}

fn split_unqualified_namespace(
namespaces: impl IntoIterator<Item = Namespace>,
) -> (impl Iterator<Item = Namespace>, Option<Namespace>) {
Expand Down
248 changes: 95 additions & 153 deletions cedar-policy-validator/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! Defines the type structure for typechecking and various utilities for
//! constructing and manipulating types.

use itertools::Itertools;
use serde::Serialize;
use smol_str::SmolStr;
use std::{
Expand Down Expand Up @@ -497,98 +498,6 @@ impl Type {
)
}

fn json_type(type_name: &str) -> serde_json::value::Map<String, serde_json::value::Value> {
[("type".to_string(), type_name.into())]
.into_iter()
.collect()
}

fn to_type_json(&self) -> serde_json::value::Map<String, serde_json::value::Value> {
match self {
Type::Never => Type::json_type("Never"),
Type::True => Type::json_type("True"),
Type::False => Type::json_type("False"),
Type::Primitive {
primitive_type: Primitive::Bool,
} => Type::json_type("Boolean"),
Type::Primitive {
primitive_type: Primitive::Long,
} => Type::json_type("Long"),
Type::Primitive {
primitive_type: Primitive::String,
} => Type::json_type("String"),
Type::Set { element_type } => {
let mut set_json = Type::json_type("Set");
match element_type {
Some(e_ty) => {
set_json.insert("element".to_string(), (*e_ty).to_type_json().into());
}
None => (),
}
set_json
}
Type::EntityOrRecord(rk) => {
let mut record_json = match rk {
EntityRecordKind::Record { .. } => Type::json_type("Record"),
EntityRecordKind::AnyEntity => Type::json_type("Entity"),
EntityRecordKind::Entity(entities) => entities.to_type_json(),
EntityRecordKind::ActionEntity { .. } => Type::json_type("ActionEntity"),
};
match rk {
EntityRecordKind::Record {
attrs,
open_attributes,
} => {
let attr_json = attrs
.iter()
.map(|(attr, attr_ty)| {
(attr.to_string(), {
let mut attr_ty_json = attr_ty.attr_type.to_type_json();
attr_ty_json
.insert("required".to_string(), attr_ty.is_required.into());
attr_ty_json.into()
})
})
.collect::<serde_json::value::Map<_, _>>();
record_json.insert("attributes".to_string(), attr_json.into());
if open_attributes.is_open() {
record_json.insert(
"additionalAttributes".to_string(),
open_attributes.is_open().into(),
);
}
}
EntityRecordKind::ActionEntity { name, attrs } => {
let attr_json = attrs
.iter()
.map(|(attr, attr_ty)| {
(attr.to_string(), {
let mut attr_ty_json = attr_ty.attr_type.to_type_json();
attr_ty_json
.insert("required".to_string(), attr_ty.is_required.into());
attr_ty_json.into()
})
})
.collect::<serde_json::value::Map<_, _>>();
record_json.insert("attributes".to_string(), attr_json.into());
record_json.insert("name".to_string(), name.to_string().into());
}
// In these case, we don't need to record attributes.
// `AnyEntity` does not have attributes while `Entity(_)`
// attributes are specified by the list of entities in the
// LUB.
EntityRecordKind::AnyEntity | EntityRecordKind::Entity(_) => {}
}
record_json
}
Type::ExtensionType { name } => {
let mut ext_json = Type::json_type("Extension");
ext_json.insert("name".into(), name.to_string().into());
ext_json
}
}
}

/// Is this validator type "consistent with" the given Core SchemaType.
/// Meaning, is there at least some value that could have this SchemaType and
/// this validator type simultaneously.
Expand Down Expand Up @@ -877,11 +786,60 @@ impl Type {

impl Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
serde_json::value::Value::Object(self.to_type_json())
)
match self {
Type::Never => write!(f, "__cedar::internal::Never"),
Type::True => write!(f, "__cedar::internal::True"),
Type::False => write!(f, "__cedar::internal::False"),
Type::Primitive {
primitive_type: Primitive::Long,
} => write!(f, "Long"),
Type::Primitive {
primitive_type: Primitive::Bool,
} => write!(f, "Bool"),
Type::Primitive {
primitive_type: Primitive::String,
} => write!(f, "String"),
Type::Set { element_type } => match element_type {
Some(element_type) => write!(f, "Set<{element_type}>"),
None => write!(f, "Set<__cedar::internal::Any>"),
},
Type::EntityOrRecord(EntityRecordKind::AnyEntity) => {
write!(f, "__cedar::internal::AnyEntity")
}
// Ignoring action attributes for display purposes.
Type::EntityOrRecord(EntityRecordKind::ActionEntity {
name,
attrs: _attrs,
}) => write!(f, "{name}"),
Type::EntityOrRecord(EntityRecordKind::Entity(elub)) => {
match elub.get_single_entity() {
Some(e) => write!(f, "{e}"),
None => write!(
f,
"__cedar::internal::Union<{}>",
elub.iter().map(ToString::to_string).join(", ")
),
}
}
Type::EntityOrRecord(EntityRecordKind::Record {
attrs,
open_attributes,
}) => {
if open_attributes.is_open() {
write!(f, "__cedar::internal::OpenRecord")?;
}
write!(f, "{{")?;
for (name, ty) in attrs.iter() {
write!(f, "{name}")?;
if !ty.is_required {
write!(f, "?")?;
}
write!(f, ": {},", ty.attr_type)?;
}
write!(f, "}}")
}
Type::ExtensionType { name } => write!(f, "{name}"),
}
}
}

Expand Down Expand Up @@ -1077,35 +1035,6 @@ impl EntityLUB {
pub(crate) fn contains_entity_type(&self, ety: &Name) -> bool {
self.lub_elements.contains(ety)
}

fn to_type_json(&self) -> serde_json::value::Map<String, serde_json::value::Value> {
let mut ordered_lub_elems = self.lub_elements.iter().collect::<Vec<_>>();
// We want the display order of elements of the set to be consistent.
ordered_lub_elems.sort();

let mut lub_element_objs = ordered_lub_elems.iter().map(|name| {
[
("type".to_string(), "Entity".into()),
("name".to_string(), name.to_string().into()),
]
.into_iter()
.collect()
});
if self.lub_elements.len() == 1 {
// PANIC SAFETY: Invariant on `lub_elements` guarantees the set is non-empty.
#[allow(clippy::expect_used)]
lub_element_objs
.next()
.expect("Invariant violated: EntityLUB set must be non-empty.")
} else {
let mut entities_json = Type::json_type("Union");
entities_json.insert(
"elements".to_string(),
lub_element_objs.collect::<Vec<_>>().into(),
);
entities_json
}
}
}

/// Represents the attributes of a record or entity type. Each attribute has an
Expand Down Expand Up @@ -1655,7 +1584,7 @@ impl<'a> Effect<'a> {
#[cfg(test)]
mod test {
use super::*;
use crate::{ActionBehavior, SchemaType, ValidatorNamespaceDef};
use crate::{human_schema::parser::parse_type, ActionBehavior, ValidatorNamespaceDef};
use cool_asserts::assert_matches;
use std::collections::HashMap;

Expand Down Expand Up @@ -2342,11 +2271,11 @@ mod test {
}

#[track_caller] // report the caller's location as the location of the panic, not the location in this function
fn assert_json_parses_to_schema_type(ty: Type) {
let json_str = serde_json::value::Value::Object(ty.to_type_json()).to_string();
println!("{json_str}");
let parsed_schema_type: SchemaType = serde_json::from_str(&json_str)
.expect("JSON representation should have parsed into a schema type");
fn assert_type_display_roundtrip(ty: Type) {
let type_str = ty.to_string();
println!("{type_str}");
let parsed_schema_type = parse_type(&type_str)
.expect("String representation should have parsed into a schema type");
let type_from_schema_type =
ValidatorNamespaceDef::try_schema_type_into_validator_type(None, parsed_schema_type)
.expect("Schema type should have converted to type.")
Expand All @@ -2356,22 +2285,19 @@ mod test {
}

#[test]
fn json_display_of_schema_type_parses_to_schema_type() {
assert_json_parses_to_schema_type(Type::primitive_boolean());
assert_json_parses_to_schema_type(Type::primitive_long());
assert_json_parses_to_schema_type(Type::primitive_string());
assert_json_parses_to_schema_type(Type::set(Type::primitive_boolean()));
assert_json_parses_to_schema_type(Type::set(Type::primitive_string()));
assert_json_parses_to_schema_type(Type::set(Type::primitive_long()));
assert_json_parses_to_schema_type(Type::named_entity_reference_from_str("Foo"));
assert_json_parses_to_schema_type(Type::named_entity_reference_from_str("Foo::Bar"));
assert_json_parses_to_schema_type(Type::named_entity_reference_from_str("Foo::Bar::Baz"));
assert_json_parses_to_schema_type(Type::closed_record_with_attributes(None));
assert_json_parses_to_schema_type(Type::closed_record_with_attributes([(
fn type_display_roundtrip() {
assert_type_display_roundtrip(Type::primitive_boolean());
assert_type_display_roundtrip(Type::primitive_long());
assert_type_display_roundtrip(Type::primitive_string());
assert_type_display_roundtrip(Type::set(Type::primitive_boolean()));
assert_type_display_roundtrip(Type::set(Type::primitive_string()));
assert_type_display_roundtrip(Type::set(Type::primitive_long()));
assert_type_display_roundtrip(Type::closed_record_with_attributes(None));
assert_type_display_roundtrip(Type::closed_record_with_attributes([(
"a".into(),
AttributeType::required_attribute(Type::primitive_boolean()),
)]));
assert_json_parses_to_schema_type(Type::closed_record_with_attributes([
assert_type_display_roundtrip(Type::closed_record_with_attributes([
(
"a".into(),
AttributeType::required_attribute(Type::primitive_boolean()),
Expand All @@ -2393,13 +2319,29 @@ mod test {
);
}

// Test display for types that don't roundtrip.
#[test]
fn test_non_schema_type_display() {
assert_displays_as(Type::Never, r#"{"type":"Never"}"#);
assert_displays_as(Type::True, r#"{"type":"True"}"#);
assert_displays_as(Type::False, r#"{"type":"False"}"#);
assert_displays_as(Type::any_set(), r#"{"type":"Set"}"#);
assert_displays_as(Type::any_entity_reference(), r#"{"type":"Entity"}"#);
fn test_type_display() {
// Entity types don't roundtrip because the human format type parser
// checks that they are defined already, so we'd need to provide a
// complete schema. TODO: the final stage of schema parsing already does
// this. Can we remove duplicated checks from human schema parsing?
Comment on lines +2327 to +2328
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to address this TODO in the PR, or file an issue for later & add the issue number to the comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not necessarily an easy change. #711

assert_displays_as(Type::named_entity_reference_from_str("Foo"), "Foo");
assert_displays_as(
Type::named_entity_reference_from_str("Foo::Bar"),
"Foo::Bar",
);
assert_displays_as(
Type::named_entity_reference_from_str("Foo::Bar::Baz"),
"Foo::Bar::Baz",
);

// These type aren't representable in a schema.
assert_displays_as(Type::Never, "__cedar::internal::Never");
assert_displays_as(Type::True, "__cedar::internal::True");
assert_displays_as(Type::False, "__cedar::internal::False");
assert_displays_as(Type::any_set(), "Set<__cedar::internal::Any>");
assert_displays_as(Type::any_entity_reference(), "__cedar::internal::AnyEntity");
assert_displays_as(
Type::least_upper_bound(
&ValidatorSchema::empty(),
Expand All @@ -2408,14 +2350,14 @@ mod test {
ValidationMode::Permissive,
)
.expect("Expected a least upper bound to exist."),
r#"{"type":"Union","elements":[{"type":"Entity","name":"Bar"},{"type":"Entity","name":"Foo"}]}"#,
"__cedar::internal::Union<Bar, Foo>",
);
}

#[test]
#[cfg(feature = "ipaddr")]
fn text_extension_type_dislay() {
let ipaddr = Name::parse_unqualified_name("ipaddr").expect("should be a valid identifier");
assert_json_parses_to_schema_type(Type::extension(ipaddr));
assert_type_display_roundtrip(Type::extension(ipaddr));
}
}
Loading