#![allow(
clippy::module_name_repetitions,
reason = "the natural FFI function names"
)]
use super::{utils::DetailedError, Schema};
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use tsify::{Ts, Tsify as _};
#[cfg(feature = "wasm")]
use wasm_bindgen::{prelude::wasm_bindgen, JsError};
#[cfg(feature = "wasm")]
extern crate tsify;
#[cfg(feature = "wasm")]
#[expect(
clippy::needless_pass_by_value,
reason = "FFI function which conventionally takes owned arguments"
)]
#[wasm_bindgen(js_name = "schemaCompleteness")]
pub fn schema_completeness_wasm(
schema: Ts<Schema>,
) -> Result<Ts<SchemaCompletenessAnswer>, JsError> {
Ok(schema_completeness(schema.to_rust()?).into_ts()?)
}
pub fn schema_completeness(schema: Schema) -> SchemaCompletenessAnswer {
match schema.parse() {
Ok((schema, _warnings)) => {
let (external_entity_types, external_actions) = external_declarations(&schema);
SchemaCompletenessAnswer::Success {
complete: schema.is_complete(),
external_entity_types,
external_actions,
}
}
Err(err) => SchemaCompletenessAnswer::Failure {
errors: vec![err.into()],
},
}
}
pub fn schema_completeness_json(
json: serde_json::Value,
) -> Result<serde_json::Value, serde_json::Error> {
let ans = schema_completeness(serde_json::from_value(json)?);
serde_json::to_value(ans)
}
pub fn schema_completeness_json_str(json: &str) -> Result<String, serde_json::Error> {
let ans = schema_completeness(serde_json::from_str(json)?);
serde_json::to_string(&ans)
}
pub(super) fn external_declarations(schema: &crate::Schema) -> (Vec<String>, Vec<String>) {
let mut entity_types: Vec<String> = schema
.external_entity_types()
.map(ToString::to_string)
.collect();
entity_types.sort();
let mut actions: Vec<String> = schema.external_actions().map(ToString::to_string).collect();
actions.sort();
(entity_types, actions)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
pub enum SchemaCompletenessAnswer {
#[serde(rename_all = "camelCase")]
Success {
complete: bool,
external_entity_types: Vec<String>,
external_actions: Vec<String>,
},
#[serde(rename_all = "camelCase")]
Failure {
errors: Vec<DetailedError>,
},
}
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use serde_json::json;
#[test]
fn schema_completeness_on_a_partial_schema() {
let cedar = json!(
"external entity User;\nexternal action audit;\nentity Document { owner: User };\naction view appliesTo { principal: [Document], resource: [Document] };"
);
let as_json = json!({ "": {
"entityTypes": {
"User": { "external": true },
"Document": { "shape": { "type": "Record", "attributes": {
"owner": { "type": "EntityOrCommon", "name": "User" }
} } }
},
"actions": {
"audit": { "external": true },
"view": { "appliesTo": {
"principalTypes": ["Document"], "resourceTypes": ["Document"]
} }
}
} });
for (syntax, call) in [("Cedar syntax", cedar), ("JSON syntax", as_json)] {
let ans = schema_completeness_json(call).unwrap();
assert_eq!(
ans,
json!({
"type": "success",
"complete": false,
"externalEntityTypes": ["User"],
"externalActions": ["Action::\"audit\""]
}),
"{syntax}"
);
}
}
#[test]
fn every_external_declaration_is_named() {
let ans = schema_completeness_json(json!(
"external entity Zebra;\nexternal entity Ant;\nexternal entity Mole;\nexternal action write;\nexternal action audit;\nentity Document { a: Ant, m: Mole, z: Zebra };"
))
.unwrap();
assert_eq!(
ans,
json!({
"type": "success",
"complete": false,
"externalEntityTypes": ["Ant", "Mole", "Zebra"],
"externalActions": ["Action::\"audit\"", "Action::\"write\""]
})
);
}
#[test]
fn a_namespaced_external_is_named_by_its_path() {
let ans = schema_completeness_json(json!(
"namespace N { external entity User; external action audit; }"
))
.unwrap();
assert_eq!(
ans,
json!({
"type": "success",
"complete": false,
"externalEntityTypes": ["N::User"],
"externalActions": ["N::Action::\"audit\""]
})
);
}
#[test]
fn schema_completeness_on_a_complete_schema() {
let ans = schema_completeness_json(json!(
"entity Color enum [\"red\"];\naction view appliesTo { principal: [Color], resource: [Color] };"
))
.unwrap();
assert_eq!(
ans,
json!({
"type": "success",
"complete": true,
"externalEntityTypes": [],
"externalActions": []
})
);
}
#[test]
fn the_alternatives_do_not_answer_the_question() {
let schema = json!("external entity User;\nentity Document { owner: User };");
assert_eq!(
crate::ffi::check_parse_schema_json(schema.clone()).unwrap(),
json!({ "type": "success" }),
"a partial schema parses, so `check_parse_schema` says nothing"
);
let ans = crate::ffi::validate_json(json!({
"schema": schema,
"policies": { "staticPolicies": {} }
}))
.unwrap();
assert_eq!(
ans,
json!({
"type": "success",
"validationErrors": [],
"validationWarnings": [],
"otherWarnings": []
}),
"an empty policy set has nothing to report against, so `validate` passes"
);
}
#[test]
fn schema_completeness_reports_a_parse_failure() {
let answer: SchemaCompletenessAnswer =
serde_json::from_value(schema_completeness_json(json!("external entity;")).unwrap())
.unwrap();
assert_matches!(answer, SchemaCompletenessAnswer::Failure { errors } => {
assert_eq!(errors.len(), 1, "unexpected errors: {errors:?}");
});
}
#[test]
fn a_dangling_reference_is_a_failure_not_an_incomplete_schema() {
let answer: SchemaCompletenessAnswer = serde_json::from_value(
schema_completeness_json(json!("external entity A in [Undeclared];")).unwrap(),
)
.unwrap();
assert_matches!(answer, SchemaCompletenessAnswer::Failure { .. });
}
#[test]
fn the_string_entry_point_agrees() {
let schema = "external entity User;";
let got: serde_json::Value = serde_json::from_str(
&schema_completeness_json_str(&serde_json::to_string(&json!(schema)).unwrap()).unwrap(),
)
.unwrap();
assert_eq!(got, schema_completeness_json(json!(schema)).unwrap());
}
}