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
51 changes: 47 additions & 4 deletions src/binding_draft_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,16 +332,37 @@ fn validate_binding_artifact(
);
let observed_symbols = string_set(&inventory["symbols"]);
let observed_resources = string_set(&inventory["resources"]);
let candidates = inventory["framework_candidates"]
let mut candidates = inventory["framework_candidates"]
.as_array()
.into_iter()
.flatten()
.filter_map(|candidate| {
candidate["binding_key"]
.as_str()
.map(|key| (key, candidate))
.map(|key| (key.to_owned(), candidate.clone()))
})
.collect::<BTreeMap<_, _>>();
for observation in inventory["observations"]
.as_array()
.into_iter()
.flatten()
.filter(|observation| observation["kind"] == "other_method_call")
{
let Some(resource) = observation["resource"].as_str() else {
continue;
};
let Some(method) = observation["method"].as_str() else {
continue;
};
candidates
.entry(format!("{resource}.{method}"))
.or_insert_with(|| {
serde_json::json!({
"symbol": observation["symbol"],
"resource": resource,
})
});
}
let bound_symbols = validate_symbols(
&bindings["symbols"],
&observed_symbols,
Expand Down Expand Up @@ -506,7 +527,7 @@ fn validate_resources(
#[allow(clippy::too_many_arguments)]
fn validate_methods(
value: &Value,
candidates: &BTreeMap<&str, &Value>,
candidates: &BTreeMap<String, Value>,
bound_symbols: &BTreeSet<String>,
resource_refs: &BTreeMap<String, BTreeSet<String>>,
accepted: &BTreeSet<&str>,
Expand All @@ -524,7 +545,7 @@ fn validate_methods(
return;
};
for (physical, record) in records {
let candidate = candidates.get(physical.as_str()).copied();
let candidate = candidates.get(physical.as_str());
if candidate.is_none() {
issue(
issues,
Expand Down Expand Up @@ -758,6 +779,28 @@ fn validate_unused_bindings(
used_resources.insert(resource);
}
}
for observation in inventory["observations"]
.as_array()
.into_iter()
.flatten()
.filter(|observation| observation["kind"] == "other_method_call")
.filter(|observation| {
let Some(resource) = observation["resource"].as_str() else {
return false;
};
let Some(method) = observation["method"].as_str() else {
return false;
};
retained_methods.contains(format!("{resource}.{method}").as_str())
})
{
if let Some(symbol) = observation["symbol"].as_str() {
used_symbols.insert(symbol);
}
if let Some(resource) = observation["resource"].as_str() {
used_resources.insert(resource);
}
}
for symbol in bound_symbols {
if !used_symbols.contains(symbol.as_str()) {
issue(
Expand Down
17 changes: 17 additions & 0 deletions src/project_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,10 +561,21 @@ fn binding_artifact_template(
)
})
.collect::<BTreeSet<_>>();
let bound_resources = observations
.iter()
.filter(|observation| observation.kind != SourceObservationKind::OtherMethodCall)
.map(|observation| observation.resource.as_str())
.chain(
candidates
.iter()
.map(|candidate| candidate.resource.as_str()),
)
.collect::<BTreeSet<_>>();
let relevant = observations
.iter()
.filter(|observation| {
observation.kind != SourceObservationKind::OtherMethodCall
|| bound_resources.contains(observation.resource.as_str())
|| candidate_calls.contains(&(
observation.symbol.as_str(),
observation.resource.as_str(),
Expand Down Expand Up @@ -602,6 +613,12 @@ fn binding_artifact_template(
.iter()
.filter(|candidate| candidate.method_binding_required)
.map(|candidate| candidate.binding_key.clone())
.chain(
relevant
.iter()
.filter(|observation| observation.kind == SourceObservationKind::OtherMethodCall)
.map(|observation| format!("{}.{}", observation.resource, observation.method)),
)
.collect::<BTreeSet<_>>();
let methods = method_keys
.into_iter()
Expand Down
80 changes: 80 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,86 @@ fn project_observe_refuses_a_symlinked_draft_output() {
let _ = fs::remove_file(outside);
}

#[test]
fn project_observe_offers_unclassified_methods_on_a_resource_with_a_builtin_effect() {
let project = TestProject::new();
fs::write(
project.root.join("src/place_order.py"),
"def place_order(order):\n existing = orders.select()\n orders.insert(order)\n return existing\n",
)
.unwrap();
let draft_relative = ".adf/repository-observation.draft.yaml";
let observed = project.run(&[
"project",
"observe",
"--analysis-root",
"src",
"--output",
draft_relative,
]);
assert_success(&observed);

let draft_path = project.root.join(draft_relative);
let mut draft = read_yaml(&draft_path);
let binding = draft["binding_artifacts"]
.as_array_mut()
.unwrap()
.iter_mut()
.find(|artifact| artifact["path"] == "src/place_order.py")
.unwrap();
assert!(binding["bindings"]["methods"]["orders.select"].is_object());
binding["bindings"] = json!({
"symbols": {
"place_order": {
"logical_ref": "operation.place-order",
"owner": "team.ordering",
"authority_ref": "decision.repository-bindings"
}
},
"resources": {
"orders": {
"logical_refs": {"data": "data.orders"},
"owner": "team.ordering",
"authority_ref": "decision.repository-bindings"
}
},
"methods": {
"orders.select": {
"fact_kinds": ["sensitive_data_access"],
"owner": "team.ordering",
"authority_ref": "decision.repository-bindings"
}
}
});
let publish_binding = draft["binding_artifacts"]
.as_array_mut()
.unwrap()
.iter_mut()
.find(|artifact| artifact["path"] == "src/publish_order.py")
.unwrap();
publish_binding["bindings"] = json!({
"symbols": {
"publish_order": {
"logical_ref": "operation.place-order",
"owner": "team.ordering",
"authority_ref": "decision.repository-bindings"
}
},
"resources": {
"order_events": {
"logical_refs": {"integration": "integration.order-events"},
"owner": "team.ordering",
"authority_ref": "decision.repository-bindings"
}
},
"methods": {}
});
write_yaml(&draft_path, &draft);

let validation = project.run(&["project", "validate-bindings", "--draft", draft_relative]);
assert_success(&validation);
}

#[test]
fn project_validates_and_promotes_a_reviewed_draft_safely() {
let project = TestProject::new();
Expand Down
Loading