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
48 changes: 43 additions & 5 deletions crates/registry-coordinator/src/authoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,9 @@ enum AuthoredStep {
choose: AuthoredMapping,
/// Explicit label-to-Step edges; no implicit fall-through exists.
cases: BTreeMap<LocalId, LocalId>,
/// The everyday case for readers and diagram tools; it has no execution or revision effect.
#[serde(default)]
expected: Option<LocalId>,
/// An optional human title; it changes neither stable identity nor behavior revision.
#[serde(default, deserialize_with = "optional_display_text")]
#[cfg_attr(feature = "schema", schemars(with = "Option<DisplayText>"))]
Expand Down Expand Up @@ -967,7 +970,28 @@ fn project_integrity_diagnostics(
));
}
for (id, step) in &project.workflow.steps {
if let AuthoredStep::Choose { cases, .. } = step {
if let AuthoredStep::Choose {
cases, expected, ..
} = step
{
if let Some(expected) = expected
.as_ref()
.filter(|label| !cases.contains_key(*label))
{
errors.push(document.diagnostic_at_value(
Severity::Error,
"coordinator.definition.expected-case",
&format!("/workflow/steps/{}/expected", id.as_str()),
&format!(
"expected case {} is not declared in this step's cases",
expected.as_str()
),
&format!(
"Name one of this step's cases ({}), or omit expected.",
definition::listed(cases.keys().map(LocalId::as_str))
),
));
}
errors.extend(collisions(
document,
"coordinator.project.choose-case-id-collision",
Expand Down Expand Up @@ -1459,6 +1483,12 @@ fn compile(project: CoordinatorProject, path: &Path) -> Result<Workflow> {
};
for (id, step) in project.workflow.steps {
let id = id.into_string();
let expected = match &step {
AuthoredStep::Choose { expected, .. } => {
expected.as_ref().map(LocalId::as_str).map(str::to_owned)
}
_ => None,
};
let (step, title, description) = match step {
AuthoredStep::WaitUntil {
wait_until,
Expand Down Expand Up @@ -1528,6 +1558,7 @@ fn compile(project: CoordinatorProject, path: &Path) -> Result<Workflow> {
cases,
title,
description,
..
} => (
Step::Choose {
choose: choose.into(),
Expand All @@ -1553,10 +1584,12 @@ fn compile(project: CoordinatorProject, path: &Path) -> Result<Workflow> {
description,
),
};
if title.is_some() || description.is_some() {
presentation
.steps
.insert(id.clone(), json!({"title":title,"description":description}));
if title.is_some() || description.is_some() || expected.is_some() {
let mut metadata = json!({"title":title,"description":description});
if let Some(expected) = expected {
metadata["expected"] = json!(expected);
}
presentation.steps.insert(id.clone(), metadata);
}
steps.insert(id, step);
}
Expand Down Expand Up @@ -1665,6 +1698,11 @@ fn reference_sites(document: &Document) -> BTreeMap<String, definition::SourceRe
(_, Some(id)) if !schema && path.ends_with("/cases") => {
Some(format!("/workflow/steps/{id}"))
}
("expected", Some(label))
if !schema && path.starts_with("/workflow/steps/") =>
{
Some(format!("{path}/cases/{label}"))
}
("start", Some(id)) if !schema && path == "/workflow" => {
Some(format!("/workflow/steps/{id}"))
}
Expand Down
62 changes: 44 additions & 18 deletions crates/registry-coordinator/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,10 +853,10 @@ pub fn contract(operation: Operation) -> &'static OperationContract {
.map(|(id, (mut input, mut output))| {
derived::finish(&mut input, &format!("{id} input"));
derived::finish(&mut output, &format!("{id} output"));
let declared_refusal = crate::operations::descriptor(id)
.expect("every derived contract names a catalog operation")
.declared_refusal
.then(derived::output::<crate::protocol::DeclaredRefusal>);
let declared_refusal = derived_declared_refusal(
crate::operations::descriptor(id)
.expect("every derived contract names a catalog operation"),
);
(
id.to_owned(),
OperationContract::new(input, output, declared_refusal, None)
Expand Down Expand Up @@ -911,6 +911,28 @@ fn derived_contracts() -> BTreeMap<&'static str, (Value, Value)> {
crate::operations::operation_catalog!(schema_catalog)
}

/// The finalized declared-refusal schema for one catalog operation.
#[cfg(feature = "schema")]
fn derived_declared_refusal(descriptor: &crate::operations::OperationDescriptor) -> Option<Value> {
if !descriptor.declared_refusal {
return None;
}
let mut refusal = derived::output::<crate::protocol::DeclaredRefusal>();
let title = if descriptor.id == Operation::InvokeBregAction.as_str() {
"invoke-breg-action declared-refusal output".to_owned()
} else {
format!("{} declared-refusal output", descriptor.id)
};
if descriptor.id == Operation::LookupRecord.as_str() {
refusal["properties"]["refusalCode"] = serde_json::json!({
"type": "string",
"const": crate::record_read::UNRESOLVED
});
}
derived::finish(&mut refusal, &title);
Some(refusal)
}

/// Generated contract files and their reference, keyed by file name.
#[cfg(feature = "schema")]
pub fn generate() -> Result<BTreeMap<String, String>, serde_json::Error> {
Expand Down Expand Up @@ -961,22 +983,9 @@ pub fn generate() -> Result<BTreeMap<String, String>, serde_json::Error> {
text,
);
for descriptor in crate::operations::descriptors() {
if !descriptor.declared_refusal {
let Some(refusal) = derived_declared_refusal(descriptor) else {
continue;
}
let mut refusal = derived::output::<crate::protocol::DeclaredRefusal>();
let title = if descriptor.id == Operation::InvokeBregAction.as_str() {
"invoke-breg-action declared-refusal output".to_owned()
} else {
format!("{} declared-refusal output", descriptor.id)
};
if descriptor.id == Operation::LookupRecord.as_str() {
refusal["properties"]["refusalCode"] = serde_json::json!({
"type": "string",
"const": crate::record_read::UNRESOLVED
});
}
derived::finish(&mut refusal, &title);
let mut text = serde_json::to_string_pretty(&refusal)?;
text.push('\n');
files.insert(
Expand Down Expand Up @@ -2335,6 +2344,23 @@ mod tests {
);
}

#[cfg(feature = "schema")]
#[test]
fn in_memory_declared_refusals_match_the_generated_catalog() {
let files = generate().unwrap();
let catalog: BTreeMap<String, Value> =
serde_json::from_str(&files["catalog.json"]).unwrap();
for descriptor in crate::operations::descriptors() {
let operation = Operation::parse(descriptor.id).unwrap();
assert_eq!(
contract(operation).declared_refusal.as_ref(),
catalog[descriptor.id].get("declaredRefusal"),
"{}",
descriptor.id
);
}
}

#[cfg(feature = "schema")]
#[test]
fn committed_contracts_match_their_generator() {
Expand Down
13 changes: 12 additions & 1 deletion crates/registry-coordinator/src/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,18 @@ impl Definition {
item["onTimeout"] = serde_json::json!(on_timeout);
item["onCancel"] = serde_json::json!(on_cancel);
}
Step::Choose { cases, .. } => item["cases"] = serde_json::json!(cases),
Step::Choose { cases, .. } => {
item["cases"] = serde_json::json!(cases);
if let Some(expected) = self
.workflow
.presentation
.steps
.get(id)
.and_then(|metadata| metadata.get("expected"))
{
item["expected"] = expected.clone();
}
}
Step::Finish { finish, .. } => item["outcome"] = serde_json::json!(finish),
_ => {}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/registry-coordinator/tests/authored_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,10 @@ fn project_schema_matches_local_keys_bounds_and_foreign_schema_members() {
.iter()
.map(|variant| variant["properties"]["type"]["const"].as_str().unwrap())
.collect::<Vec<_>>();
assert_eq!(tags, ["function", "fields", "input", "step", "literal"]);
assert_eq!(
tags,
["function", "fields", "compare", "input", "step", "literal"]
);
let function = variants
.iter()
.find(|variant| variant["properties"]["type"]["const"] == "function")
Expand Down
Loading
Loading