use serde_json::Value;
use crate::contract::{PackageContract, SignalContract};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnenforceableSchema {
pub declaration: String,
pub reason: String,
}
impl std::fmt::Display for UnenforceableSchema {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}: {}", self.declaration, self.reason)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum AdmissionError {
#[error("the declared schema is not a valid JSON Schema: {reason}")]
UnusableSchema {
reason: String,
},
#[error("{violations}")]
Mismatch {
violations: String,
},
}
#[must_use]
pub fn declares_nothing(schema: &Value) -> bool {
match schema {
Value::Null | Value::Bool(true) => true,
Value::Object(members) => members.is_empty(),
_ => false,
}
}
pub fn schema_is_usable(schema: &Value) -> Result<(), String> {
if declares_nothing(schema) {
return Ok(());
}
jsonschema::validator_for(schema)
.map(|_| ())
.map_err(|error| error.to_string())
}
pub fn admit_value(schema: &Value, value: &Value) -> Result<(), AdmissionError> {
let validator =
jsonschema::validator_for(schema).map_err(|error| AdmissionError::UnusableSchema {
reason: error.to_string(),
})?;
if validator.is_valid(value) {
return Ok(());
}
let violations = validator
.iter_errors(value)
.map(|error| {
let location = error.instance_path().to_string();
if location.is_empty() {
format!("<root>: {error}")
} else {
format!("{location}: {error}")
}
})
.collect::<Vec<_>>()
.join("; ");
Err(AdmissionError::Mismatch { violations })
}
impl PackageContract {
#[must_use]
pub fn unenforceable_schemas(&self) -> Vec<UnenforceableSchema> {
let mut found = Vec::new();
let mut check = |declaration: String, schema: &Value| {
if let Err(reason) = schema_is_usable(schema) {
found.push(UnenforceableSchema {
declaration,
reason,
});
}
};
check("the workflow input type".to_owned(), &self.input_schema);
check("the workflow result type".to_owned(), &self.output_schema);
for entry in &self.additional_workflows {
let workflow_type = &entry.workflow_type;
check(
format!("the input type of workflow `{workflow_type}`"),
&entry.input_schema,
);
check(
format!("the result type of workflow `{workflow_type}`"),
&entry.output_schema,
);
}
for signal in &self.signals {
let name = &signal.name;
check(
format!("the payload type of signal `{name}`"),
&signal.input_schema,
);
}
for child in &self.children {
let name = &child.name;
check(
format!("the input type of child workflow `{name}`"),
&child.input_schema,
);
check(
format!("the result type of child workflow `{name}`"),
&child.output_schema,
);
}
for worker in &self.workers {
let queue = &worker.task_queue;
for action in &worker.actions {
let name = &action.name;
check(
format!("the parameter types of activity `{name}` on queue `{queue}`"),
&action.input_schema,
);
check(
format!("the result type of activity `{name}` on queue `{queue}`"),
&action.output_schema,
);
}
}
found
}
#[must_use]
pub fn declared_signal(&self, signal_name: &str) -> Option<&SignalContract> {
self.signals
.iter()
.find(|signal| signal.name == signal_name)
}
#[must_use]
pub fn declared_signal_names(&self) -> Vec<&str> {
let mut names = self
.signals
.iter()
.map(|signal| signal.name.as_str())
.collect::<Vec<_>>();
names.sort_unstable();
names
}
#[must_use]
pub fn entry_input_schema(&self, workflow_type: &str) -> &Value {
self.additional_workflows
.iter()
.find(|entry| entry.workflow_type == workflow_type)
.map_or(&self.input_schema, |entry| &entry.input_schema)
}
}
#[cfg(test)]
#[path = "admission_tests.rs"]
mod tests;