Generate native core discovery from typed dispatch contracts
This commit is contained in:
@@ -48,8 +48,9 @@ pub fn client_rs(name: &str, doc: &Value) -> Result<String, String> {
|
||||
let mut result = vec![];
|
||||
for method in methods.as_array().into_iter().flatten() {
|
||||
let wire = method["name"].as_str().ok_or("method name missing")?;
|
||||
let ident = syn::parse_str::<syn::Ident>(wire)
|
||||
.or_else(|_| syn::parse_str::<syn::Ident>(&format!("r#{wire}")))
|
||||
let rust_name = wire.replace('.', "_");
|
||||
let ident = syn::parse_str::<syn::Ident>(&rust_name)
|
||||
.or_else(|_| syn::parse_str::<syn::Ident>(&format!("r#{rust_name}")))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut rs = method["result"]["schema"].clone();
|
||||
local_refs(&mut rs);
|
||||
|
||||
+46
-1
@@ -204,7 +204,7 @@ fn supported(sender: &Value, document: &Value, seen: &mut HashSet<usize>) -> Res
|
||||
supported(child, document, seen)?;
|
||||
}
|
||||
}
|
||||
"additionalProperties" if value.is_boolean() => (),
|
||||
"additionalProperties" => supported(value, document, seen)?,
|
||||
"enum"
|
||||
if value
|
||||
.as_array()
|
||||
@@ -447,6 +447,15 @@ fn compare_variants(
|
||||
{
|
||||
let error = if let Some(other) = receiver["properties"].get(key) {
|
||||
subset(child, other, sender_doc, receiver_doc, active).err()
|
||||
} else if receiver["additionalProperties"].is_object() {
|
||||
subset(
|
||||
child,
|
||||
&receiver["additionalProperties"],
|
||||
sender_doc,
|
||||
receiver_doc,
|
||||
active,
|
||||
)
|
||||
.err()
|
||||
} else if receiver["additionalProperties"] == false {
|
||||
Some((
|
||||
format!(
|
||||
@@ -471,6 +480,42 @@ fn compare_variants(
|
||||
));
|
||||
}
|
||||
}
|
||||
// A typed dictionary can send arbitrary keys, unlike a struct's
|
||||
// closed set of output fields. Check both receiver named fields
|
||||
// and its catch-all value contract.
|
||||
if let Some(extra) = sender.get("additionalProperties").filter(|v| v.is_object()) {
|
||||
let mut key = "extra".to_string();
|
||||
while sender["properties"].get(&key).is_some()
|
||||
|| receiver["properties"].get(&key).is_some()
|
||||
{
|
||||
key.push('!');
|
||||
}
|
||||
let catch_all = receiver
|
||||
.get("additionalProperties")
|
||||
.unwrap_or(&Value::Bool(true));
|
||||
let targets = receiver["properties"]
|
||||
.as_object()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|(key, _)| sender["properties"].get(*key).is_none())
|
||||
.chain(std::iter::once((&key, catch_all)));
|
||||
for (key, target) in targets {
|
||||
if let Err((message, example)) =
|
||||
subset(extra, target, sender_doc, receiver_doc, active)
|
||||
{
|
||||
let mut object = sample(sender, sender_doc, &mut HashSet::new());
|
||||
if let (Some(Value::Object(object)), Some(value)) = (&mut object, example) {
|
||||
object.insert(key.clone(), value);
|
||||
} else {
|
||||
object = None;
|
||||
}
|
||||
return Err((
|
||||
format!("the dictionary field {key:?} is incompatible: {message}"),
|
||||
object,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"array" => {
|
||||
if let Err((message, example)) = subset(
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Typed dictionary results retain their value contracts in both directions.
|
||||
use sausage_interface::compat::check;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn document(schema: Value) -> Value {
|
||||
json!({"methods":[{"name":"values","params":[],"result":{"schema":schema}}]})
|
||||
}
|
||||
fn map(value: Value) -> Value {
|
||||
json!({"type":"object","additionalProperties":value})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dictionary_values_are_covariant() {
|
||||
let strings = document(map(json!({"type":"string"})));
|
||||
let numbers = document(map(json!({"type":"integer"})));
|
||||
let short = document(map(json!({"type":"string","enum":["ok"]})));
|
||||
assert!(check(&strings, &strings).is_empty());
|
||||
assert!(check(&strings, &short).is_empty());
|
||||
assert!(!check(&short, &strings).is_empty());
|
||||
let errors = check(&strings, &numbers);
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(errors[0].counterexample, Some(json!({"extra":0})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dictionaries_and_named_fields_check_each_other() {
|
||||
let strings = document(map(json!({"type":"string"})));
|
||||
let named_string = document(json!({"type":"object","properties":{"name":{"type":"string"}}}));
|
||||
let named_number = document(json!({"type":"object","properties":{"name":{"type":"integer"}}}));
|
||||
assert!(check(&strings, &named_string).is_empty());
|
||||
assert!(check(&named_string, &strings).is_empty());
|
||||
assert!(!check(&strings, &named_number).is_empty());
|
||||
assert!(!check(&named_number, &strings).is_empty());
|
||||
let closed = document(json!({"type":"object","additionalProperties":false}));
|
||||
assert!(!check(&closed, &strings).is_empty());
|
||||
assert!(check(&strings, &closed).is_empty());
|
||||
let required = document(
|
||||
json!({"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}),
|
||||
);
|
||||
assert!(!check(&required, &strings).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dictionary_references_are_checked_not_ignored() {
|
||||
let mut expected = document(map(json!({"$ref":"#/components/schemas/Value"})));
|
||||
expected["components"]["schemas"]["Value"] = json!({"type":"string"});
|
||||
assert!(check(&expected, &expected).is_empty());
|
||||
let mut actual = expected.clone();
|
||||
actual["components"]["schemas"]["Value"] = json!({"type":"boolean"});
|
||||
assert!(!check(&expected, &actual).is_empty());
|
||||
actual["components"]["schemas"]["Value"] = json!({"pattern":"x"});
|
||||
assert!(
|
||||
check(&expected, &actual)[0]
|
||||
.message
|
||||
.contains("unsupported schema construct pattern")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user