Skip to content

Commit 10f2ba1

Browse files
feat(schema): J4-J8, H11 — JSON Schema 2020-12 keywords + Path Item refs
J4: prefixItems J5: patternProperties + propertyNames J6: unevaluatedProperties + unevaluatedItems J7: dependentRequired + dependentSchemas J8: contains + minContains + maxContains + contentEncoding + contentMediaType + contentSchema All landed as typed fields on SchemaDetails, plus the conditional keywords (`if`/`then`/`else`/`not`), the `$defs` map (J2 partial), `$comment`, `$schema`, `examples`, `example`, `title`, `deprecated`, `readOnly`/`writeOnly`. Spec keywords no longer hide in `extra`. Updated `should_use_dynamic_json` in analysis.rs to read these typed fields instead of doing string lookups in `details.extra`. H11: Path Item $ref resolution - resolve_path_item follows `$ref: "#/components/pathItems/X"` against spec.components.path_items at analysis time so referenced path items contribute their operations to the generated client. All 205 tests still pass; insta snapshots updated for the new typed fields where they show up in pretty-printed schemas. Refs #14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c760b87 commit 10f2ba1

2 files changed

Lines changed: 122 additions & 18 deletions

File tree

src/analysis.rs

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3468,25 +3468,21 @@ impl SchemaAnalyzer {
34683468
.unwrap_or(true);
34693469

34703470
if no_properties {
3471-
// Check for constraints that would make this a structured type
3471+
// Check for constraints that would make this a structured type.
3472+
// After J5–J8, these are typed fields rather than `extra` lookups.
34723473
let has_structural_constraints =
3473-
// Has required fields (other than just 'type')
34743474
details.required.as_ref()
34753475
.map(|req| req.iter().any(|r| r != "type"))
34763476
.unwrap_or(false)
3477-
// Has pattern-based property definitions
3478-
|| details.extra.contains_key("patternProperties")
3479-
// Has property name schema
3480-
|| details.extra.contains_key("propertyNames")
3481-
// Has min/max property constraints
3482-
|| details.extra.contains_key("minProperties")
3483-
|| details.extra.contains_key("maxProperties")
3484-
// Has specific property dependencies
3485-
|| details.extra.contains_key("dependencies")
3486-
// Has conditional schemas
3487-
|| details.extra.contains_key("if")
3488-
|| details.extra.contains_key("then")
3489-
|| details.extra.contains_key("else");
3477+
|| details.pattern_properties.is_some()
3478+
|| details.property_names.is_some()
3479+
|| details.min_properties.is_some()
3480+
|| details.max_properties.is_some()
3481+
|| details.dependent_required.is_some()
3482+
|| details.dependent_schemas.is_some()
3483+
|| details.if_schema.is_some()
3484+
|| details.then_schema.is_some()
3485+
|| details.else_schema.is_some();
34903486

34913487
return !has_structural_constraints;
34923488
}
@@ -3513,7 +3509,10 @@ impl SchemaAnalyzer {
35133509

35143510
if let Some(paths) = &spec.paths {
35153511
for (path, path_item) in paths {
3516-
self.ingest_path_item_operations(path, path_item, analysis)?;
3512+
// H11: Path Item may be a $ref to components/pathItems. Resolve here.
3513+
let resolved = self.resolve_path_item(path_item, &spec)?;
3514+
let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
3515+
self.ingest_path_item_operations(path, pi, analysis)?;
35173516
}
35183517
}
35193518
// T4: walk webhooks the same way as paths. Per OAS 3.1+, webhooks are
@@ -3531,6 +3530,37 @@ impl SchemaAnalyzer {
35313530
Ok(())
35323531
}
35333532

3533+
/// H11: Resolve a Path Item's `$ref` (3.1+ allows them) against
3534+
/// `components/pathItems`. Returns Some(resolved) when a ref was followed,
3535+
/// or None when the input is already inline.
3536+
fn resolve_path_item(
3537+
&self,
3538+
path_item: &crate::openapi::PathItem,
3539+
spec: &crate::openapi::OpenApiSpec,
3540+
) -> Result<Option<crate::openapi::PathItem>> {
3541+
let Some(reference) = &path_item.reference else {
3542+
return Ok(None);
3543+
};
3544+
let target_name = reference
3545+
.strip_prefix("#/components/pathItems/")
3546+
.ok_or_else(|| {
3547+
GeneratorError::UnresolvedReference(format!(
3548+
"Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
3549+
))
3550+
})?;
3551+
let pi = spec
3552+
.components
3553+
.as_ref()
3554+
.and_then(|c| c.path_items.as_ref())
3555+
.and_then(|map| map.get(target_name))
3556+
.ok_or_else(|| {
3557+
GeneratorError::UnresolvedReference(format!(
3558+
"Path Item ref {reference} not found in components/pathItems"
3559+
))
3560+
})?;
3561+
Ok(Some(pi.clone()))
3562+
}
3563+
35343564
fn ingest_path_item_operations(
35353565
&mut self,
35363566
path: &str,

src/openapi.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,82 @@ pub struct SchemaDetails {
212212
#[serde(rename = "maxLength")]
213213
pub max_length: Option<u64>,
214214
pub pattern: Option<String>,
215-
216-
// Extensions and unknown fields
215+
#[serde(rename = "exclusiveMinimum")]
216+
pub exclusive_minimum: Option<f64>,
217+
#[serde(rename = "exclusiveMaximum")]
218+
pub exclusive_maximum: Option<f64>,
219+
#[serde(rename = "multipleOf")]
220+
pub multiple_of: Option<f64>,
221+
#[serde(rename = "minItems")]
222+
pub min_items: Option<u64>,
223+
#[serde(rename = "maxItems")]
224+
pub max_items: Option<u64>,
225+
#[serde(rename = "uniqueItems")]
226+
pub unique_items: Option<bool>,
227+
#[serde(rename = "minProperties")]
228+
pub min_properties: Option<u64>,
229+
#[serde(rename = "maxProperties")]
230+
pub max_properties: Option<u64>,
231+
232+
// JSON Schema 2020-12 array keywords (J4, J8).
233+
#[serde(rename = "prefixItems")]
234+
pub prefix_items: Option<Vec<Schema>>,
235+
pub contains: Option<Box<Schema>>,
236+
#[serde(rename = "minContains")]
237+
pub min_contains: Option<u64>,
238+
#[serde(rename = "maxContains")]
239+
pub max_contains: Option<u64>,
240+
241+
// JSON Schema 2020-12 object keywords (J5, J6, J7).
242+
#[serde(rename = "patternProperties")]
243+
pub pattern_properties: Option<BTreeMap<String, Schema>>,
244+
#[serde(rename = "propertyNames")]
245+
pub property_names: Option<Box<Schema>>,
246+
#[serde(rename = "unevaluatedProperties")]
247+
pub unevaluated_properties: Option<AdditionalProperties>,
248+
#[serde(rename = "unevaluatedItems")]
249+
pub unevaluated_items: Option<AdditionalProperties>,
250+
#[serde(rename = "dependentRequired")]
251+
pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
252+
#[serde(rename = "dependentSchemas")]
253+
pub dependent_schemas: Option<BTreeMap<String, Schema>>,
254+
255+
// JSON Schema 2020-12 content keywords (J8).
256+
#[serde(rename = "contentEncoding")]
257+
pub content_encoding: Option<String>,
258+
#[serde(rename = "contentMediaType")]
259+
pub content_media_type: Option<String>,
260+
#[serde(rename = "contentSchema")]
261+
pub content_schema: Option<Box<Schema>>,
262+
263+
// JSON Schema 2020-12 conditional keywords.
264+
#[serde(rename = "if")]
265+
pub if_schema: Option<Box<Schema>>,
266+
#[serde(rename = "then")]
267+
pub then_schema: Option<Box<Schema>>,
268+
#[serde(rename = "else")]
269+
pub else_schema: Option<Box<Schema>>,
270+
pub not: Option<Box<Schema>>,
271+
272+
// 3.0 deprecated annotations now first-class (kept since openai-responses fixture is OAS 3.0).
273+
pub title: Option<String>,
274+
pub deprecated: Option<bool>,
275+
#[serde(rename = "readOnly")]
276+
pub read_only: Option<bool>,
277+
#[serde(rename = "writeOnly")]
278+
pub write_only: Option<bool>,
279+
pub examples: Option<Vec<Value>>,
280+
pub example: Option<Value>,
281+
/// JSON Schema annotation `$comment`.
282+
#[serde(rename = "$comment")]
283+
pub comment: Option<String>,
284+
#[serde(rename = "$schema")]
285+
pub schema_keyword: Option<String>,
286+
#[serde(rename = "$defs")]
287+
pub defs: Option<BTreeMap<String, Schema>>,
288+
289+
// Extensions and unknown fields. After J5–J8 above this should be x-*-only
290+
// for well-formed OAS 3.1+ specs.
217291
#[serde(flatten)]
218292
pub extra: BTreeMap<String, Value>,
219293
}

0 commit comments

Comments
 (0)