#![allow(
clippy::module_name_repetitions,
reason = "the natural FFI function names"
)]
use super::utils::JsonValueWithNoDuplicateKeys;
use super::{completeness::external_declarations, 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 = "linkSchemas")]
pub fn link_schemas_wasm(call: Ts<LinkSchemasCall>) -> Result<Ts<LinkSchemasAnswer>, JsError> {
Ok(link_schemas(call.to_rust()?).into_ts()?)
}
pub fn link_schemas(call: LinkSchemasCall) -> LinkSchemasAnswer {
let mut parsed = Vec::with_capacity(call.schemas.len());
let mut warnings = Vec::new();
let mut errors = Vec::new();
for schema in call.schemas {
match schema.parse() {
Ok((schema, new_warnings)) => {
warnings.extend(new_warnings.map(|w| miette::Report::new(w).into()));
parsed.push(schema);
}
Err(err) => errors.push(err.into()),
}
}
if !errors.is_empty() {
return LinkSchemasAnswer::Failure { errors };
}
let linked = match crate::Schema::schema_link(parsed) {
Ok(linked) => linked,
Err(err) => {
return LinkSchemasAnswer::Failure {
errors: vec![(&err).into()],
};
}
};
let json = match linked
.0
.to_json_schema()
.map_err(miette::Report::msg)
.and_then(|fragment| serde_json::to_value(fragment).map_err(miette::Report::msg))
{
Ok(json) => json,
Err(err) => {
return LinkSchemasAnswer::Failure {
errors: vec![err.into()],
};
}
};
let (external_entity_types, external_actions) = external_declarations(&linked);
LinkSchemasAnswer::Success {
json: json.into(),
complete: linked.is_complete(),
external_entity_types,
external_actions,
warnings,
}
}
pub fn link_schemas_json(json: serde_json::Value) -> Result<serde_json::Value, serde_json::Error> {
let ans = link_schemas(serde_json::from_value(json)?);
serde_json::to_value(ans)
}
pub fn link_schemas_json_str(json: &str) -> Result<String, serde_json::Error> {
let ans = link_schemas(serde_json::from_str(json)?);
serde_json::to_string(&ans)
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct LinkSchemasCall {
#[cfg_attr(feature = "wasm", tsify(type = "Schema[]"))]
pub schemas: Vec<Schema>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
pub enum LinkSchemasAnswer {
#[serde(rename_all = "camelCase")]
Success {
#[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
json: JsonValueWithNoDuplicateKeys,
complete: bool,
external_entity_types: Vec<String>,
external_actions: Vec<String>,
warnings: Vec<DetailedError>,
},
#[serde(rename_all = "camelCase")]
Failure {
errors: Vec<DetailedError>,
},
}
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use serde_json::json;
#[track_caller]
fn link(schemas: Vec<serde_json::Value>) -> serde_json::Value {
link_schemas_json(json!({ "schemas": schemas })).unwrap()
}
#[track_caller]
fn assert_links(schemas: Vec<serde_json::Value>) -> (serde_json::Value, bool) {
let answer: LinkSchemasAnswer = serde_json::from_value(link(schemas)).unwrap();
assert_matches!(answer, LinkSchemasAnswer::Success { json, complete, .. } => {
(json.into(), complete)
})
}
#[track_caller]
fn assert_link_fails(schemas: Vec<serde_json::Value>) -> Vec<DetailedError> {
let answer: LinkSchemasAnswer = serde_json::from_value(link(schemas)).unwrap();
assert_matches!(answer, LinkSchemasAnswer::Failure { errors } => errors)
}
#[test]
fn an_entity_definition_replaces_an_external() {
let (json, complete) = assert_links(vec![
json!("external entity B;\nentity A { b: B };"),
json!("entity B;"),
]);
assert!(complete);
assert_eq!(
json,
json!({ "": {
"entityTypes": {
"A": { "shape": { "type": "Record", "attributes": {
"b": { "type": "EntityOrCommon", "name": "B" }
} } },
"B": {}
},
"actions": {}
} })
);
}
#[test]
fn an_unresolved_external_remains_external() {
let answer: LinkSchemasAnswer = serde_json::from_value(link(vec![
json!("external entity B;\nentity A { b: B };"),
json!("external entity B;\nentity D { b: B };"),
]))
.unwrap();
assert_matches!(answer, LinkSchemasAnswer::Success {
complete, external_entity_types, external_actions, ..
} => {
assert!(!complete);
assert_eq!(external_entity_types, ["B"]);
assert!(external_actions.is_empty());
});
}
#[test]
fn mutual_externals_resolve() {
let (_, complete) = assert_links(vec![
json!("external entity B;\nentity A { b: B };"),
json!("external entity A;\nentity B { a: A };"),
]);
assert!(complete);
}
#[test]
fn an_external_action_links_to_a_definition() {
let (_, complete) = assert_links(vec![
json!("entity User;\nentity Doc;\nexternal action write;"),
json!(
"external entity User;\nexternal entity Doc;\naction write appliesTo { principal: User, resource: Doc };"
),
]);
assert!(complete);
}
#[test]
fn the_two_schema_syntaxes_link() {
let (_, complete) = assert_links(vec![
json!("external entity B;\nentity A { b: B };"),
json!({ "": { "entityTypes": { "B": {} }, "actions": {} } }),
]);
assert!(complete);
}
#[test]
fn order_does_not_matter() {
let p = json!("external entity B;\nexternal action audit;\nentity A { b: B };");
let c = json!("entity B;");
assert_eq!(
link(vec![p.clone(), c.clone()]),
link(vec![c, p]),
"the same schemas in the other order"
);
}
#[test]
fn one_schema_links_to_itself() {
let (_, complete) = assert_links(vec![json!("external entity B;\nentity A { b: B };")]);
assert!(!complete);
}
#[test]
fn no_schemas_link_to_the_empty_schema() {
let (json, complete) = assert_links(vec![]);
assert!(complete);
assert_eq!(json, json!({}));
}
#[test]
fn duplicate_entity_definitions_are_refused() {
let errors = assert_link_fails(vec![json!("entity A;"), json!("entity A;")]);
assert_eq!(errors.len(), 1, "unexpected errors: {errors:?}");
}
#[test]
fn the_string_entry_point_agrees() {
let call = json!({ "schemas": [
"external entity B;\nentity A { b: B };",
"entity B;"
] });
let got: serde_json::Value = serde_json::from_str(
&link_schemas_json_str(&serde_json::to_string(&call).unwrap()).unwrap(),
)
.unwrap();
assert_eq!(got, link_schemas_json(call).unwrap());
}
#[test]
fn an_unparseable_input_is_a_failure() {
let errors = assert_link_fails(vec![json!("entity A;"), json!("external entity;")]);
assert_eq!(errors.len(), 1, "unexpected errors: {errors:?}");
}
#[test]
fn every_unparseable_input_is_reported() {
let errors = assert_link_fails(vec![
json!("external entity;"),
json!("entity A;"),
json!("action;"),
]);
assert_eq!(errors.len(), 2, "unexpected errors: {errors:?}");
}
#[test]
fn a_dangling_reference_is_a_failure() {
let errors = assert_link_fails(vec![
json!("entity A;"),
json!("external entity B in [Undeclared];"),
]);
assert_eq!(errors.len(), 1, "unexpected errors: {errors:?}");
}
#[test]
fn an_unknown_field_is_rejected() {
assert_matches!(
link_schemas_json(json!({ "schemas": [], "fragments": [] })),
Err(_)
);
}
}