-
Notifications
You must be signed in to change notification settings - Fork 26
feat(config): lenient etcd parsing with tri-state compatibility reporting #872
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b38aa98
498aaca
f0fcff6
04975bd
ded6208
7f3bc88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,38 +38,35 @@ fn main() { | |
| fs::create_dir_all(&out_dir).expect("create schemas/resources dir"); | ||
|
|
||
| // Every resource with a runtime validator goes through the SAME | ||
| // `*_root_schema()` producer the validator uses, so the published schema == | ||
| // the enforced schema by construction. `ensemble`/`rate_limit`/`routing` | ||
| // have no standalone validator (they are nested struct types) so they dump | ||
| // straight from the struct via `schema_for!`. | ||
| dump_value(&out_dir, "api_key", schema::apikey_root_schema()); | ||
| dump_value(&out_dir, "cache_policy", schema::cache_policy_root_schema()); | ||
| dump_value(&out_dir, "model", schema::model_root_schema()); | ||
| dump_value( | ||
| &out_dir, | ||
| // `resource_root_schema(name, strict: true)` producer the write-path | ||
| // validators compile, so the published schema == the enforced write | ||
| // contract by construction. The published files deliberately carry the | ||
| // STRICT shape: they document the Admin API write contract (unknown | ||
| // fields are a 400) and the etcd loader's lenient read tolerance is a | ||
| // runtime behavior, not a contract callers may write against. | ||
| // `ensemble`/`rate_limit`/`routing` have no standalone validator (they | ||
| // are nested struct types) so they dump straight from the struct via | ||
| // `schema_for!`, closed the same way. | ||
| for resource in [ | ||
| "api_key", | ||
| "cache_policy", | ||
| "model", | ||
| "rate_limit_policy", | ||
| schema::rate_limit_policy_root_schema(), | ||
| ); | ||
| dump_value(&out_dir, "provider_key", schema::provider_key_root_schema()); | ||
| dump_value( | ||
| &out_dir, | ||
| "provider_key", | ||
| "observability_exporter", | ||
| schema::observability_exporter_root_schema(), | ||
| ); | ||
| dump_value(&out_dir, "guardrail", schema::guardrail_root_schema()); | ||
| dump_value( | ||
| &out_dir, | ||
| "guardrail", | ||
| "guardrail_attachment", | ||
| schema::guardrail_attachment_root_schema(), | ||
| ); | ||
| dump_value(&out_dir, "mcp_server", schema::mcp_server_root_schema()); | ||
| dump_value(&out_dir, "mcp_policy", schema::mcp_policy_root_schema()); | ||
| dump_value(&out_dir, "a2a_agent", schema::a2a_agent_root_schema()); | ||
| dump_value( | ||
| &out_dir, | ||
| "mcp_server", | ||
| "mcp_policy", | ||
| "a2a_agent", | ||
| "oidc_provider", | ||
| schema::oidc_provider_root_schema(), | ||
| ); | ||
| ] { | ||
| dump_value( | ||
| &out_dir, | ||
| resource, | ||
| schema::resource_root_schema(resource, true), | ||
| ); | ||
| } | ||
|
|
||
| dump::<EnsembleConfig>(&out_dir, "ensemble"); | ||
| dump::<RateLimit>(&out_dir, "rate_limit"); | ||
|
|
@@ -81,14 +78,48 @@ fn main() { | |
| fn dump<T: JsonSchema>(out_dir: &Path, name: &str) { | ||
| // Serialize the `RootSchema` directly to preserve schemars' native key | ||
| // ordering. (Routing through `serde_json::Value` would re-sort keys.) | ||
| let mut json = | ||
| serde_json::to_string_pretty(&schemars::schema_for!(T)).expect("serialize schema"); | ||
| // These nested types belong to closed resources, so re-close the root | ||
| // and every struct-shaped definition on the typed schema — the same | ||
| // strictness `schema::close_unknown_fields` applies to the resource | ||
| // documents, kept typed here so the key order stays schemars-native. | ||
| let mut root = schemars::schema_for!(T); | ||
| close_object_schema(&mut root.schema); | ||
| for def in root.definitions.values_mut() { | ||
| if let schemars::schema::Schema::Object(obj) = def { | ||
| close_object_schema(obj); | ||
| } | ||
| } | ||
| let mut json = serde_json::to_string_pretty(&root).expect("serialize schema"); | ||
| json.push('\n'); | ||
| let path = out_dir.join(format!("{name}.schema.json")); | ||
| fs::write(&path, json).unwrap_or_else(|e| panic!("write {}: {e}", path.display())); | ||
| println!("wrote {}", path.display()); | ||
| } | ||
|
|
||
| /// Insert `additionalProperties: false` on a struct-shaped schema object | ||
| /// (one that lists `properties`), unless it already pins a value. Recurses | ||
| /// into `anyOf` branches so an untagged enum's object variant closes too — | ||
| /// serde silently swallows unknown fields inside untagged content, so the | ||
| /// schema closure is the only non-silent guard there (the resource | ||
| /// producers apply the same rule, e.g. `OnEmbeddingFailure` in `model`). | ||
| fn close_object_schema(schema: &mut schemars::schema::SchemaObject) { | ||
| if let Some(sub) = schema.subschemas.as_deref_mut() { | ||
| if let Some(any_of) = sub.any_of.as_mut() { | ||
| for branch in any_of.iter_mut() { | ||
| if let schemars::schema::Schema::Object(b) = branch { | ||
| close_object_schema(b); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| let Some(object) = schema.object.as_deref_mut() else { | ||
| return; | ||
| }; | ||
| if !object.properties.is_empty() && object.additional_properties.is_none() { | ||
| object.additional_properties = Some(Box::new(schemars::schema::Schema::Bool(false))); | ||
| } | ||
| } | ||
|
Comment on lines
78
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Show the committed nested-type schemas and check that every
# struct-shaped object carries additionalProperties: false.
set -euo pipefail
fd -t f '.*\.schema\.json$' schemas/resources | sort
for name in ensemble rate_limit routing semantic embedding; do
f="schemas/resources/${name}.schema.json"
[ -f "$f" ] || { echo "MISSING $f"; continue; }
echo "== $f =="
jq '{root_ap: .additionalProperties,
defs_missing_ap: (.definitions // {} | to_entries
| map(select(.value.properties != null and (.value | has("additionalProperties") | not)))
| map(.key))}' "$f"
done
# The strict resource schemas come from `resource_root_schema(_, true)`.
for f in $(fd -t f '.*\.schema\.json$' schemas/resources); do
jq -e 'if (.properties != null) then (.additionalProperties != null) else true end' "$f" >/dev/null \
|| echo "root not closed: $f"
doneRepository: api7/aisix Length of output: 1585 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== dump-schema.rs relevant content =="
sed -n '1,160p' crates/aisix-core/src/bin/dump-schema.rs | cat -n
echo "== schema file summary =="
for f in $(fd -t f '.*\.schema\.json$' schemas/resources | sort); do
status="OK"
jq -e 'if (.properties != null or (is_object and any(. != null))) then (.additionalProperties != null) else true end' "$f" >/dev/null || status="CLOSED_FALSE"
jq -e 'if (.properties != null or (is_object and any(. != null))) then .definitions as $d | to_entries | map(select(.value.properties != null and (has("additionalProperties") | not or .value.additionalProperties == null))) | length == 0; else true end' "$f" >/dev/null || status="DEF_MISSING_AP"
echo "$status:$f"
doneRepository: api7/aisix Length of output: 31347 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json, pathlib
for path in sorted(pathlib.Path('schemas/resources').glob('*.schema.json')):
data = {}
with open(path, 'r') as f:
data = json.load(f)
root = data.get('additionalProperties')
missing_defs = [
defname for defname, def_schema in (data.get('definitions') or {}).items()
if isinstance(def_schema, dict) and def_schema.get('properties')
and def_schema.get('additionalProperties') is None
]
root_status = 'present' if root is not None else 'missing'
print(f"{path}: root_additionalProperties={root_status}, missing_struct_defs={missing_defs}")
PY
echo "== schema resource producer search =="
rg -n "resource_root_schema|close_unknown_fields|deny_unknown_fields" crates/aisix-core/src/models crates/aisix-core/src -g '*.rs' | head -200Repository: api7/aisix Length of output: 11290 Regenerate the schema files before merging. The generator now closes struct-shaped schemas with 🤖 Prompt for AI Agents |
||
|
|
||
| /// Write a pre-assembled schema `Value`. Used for resources whose canonical | ||
| /// schema is built by a dedicated producer rather than a bare `schema_for!` | ||
| /// (e.g. `model`, which injects the cross-field `oneOf`). | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.