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
45 changes: 45 additions & 0 deletions src/ir/lower/v30.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1692,4 +1692,49 @@ components:
panic!("Expected Named alias");
}
}

#[test]
fn test_lower_tagged_union_with_plain_internal_variants() {
let ir = lower_yaml(
r#"
openapi: "3.0.0"
info:
title: Test
version: "1.0"
paths: {}
components:
schemas:
ScheduleRule:
oneOf:
- type: object
required: [type, at]
properties:
type:
type: string
enum: [once]
at:
type: string
- type: object
required: [type, every_minutes, anchor_at]
properties:
type:
type: string
enum: [interval]
every_minutes:
type: integer
format: int32
anchor_at:
type: string
"#,
);
let schedule_rule = &ir.schemas["ScheduleRule"];
let IrSchemaKind::TaggedUnion(tu) = &schedule_rule.kind else {
panic!("Expected TaggedUnion, got {:?}", schedule_rule.kind);
};
assert!(matches!(tu.tagging, TaggingStyle::Internal));
assert_eq!(tu.discriminator_field, "type");
assert_eq!(tu.variants.len(), 2);
assert_eq!(tu.variants[0].discriminator_value, "once");
assert_eq!(tu.variants[1].discriminator_value, "interval");
}
}
55 changes: 55 additions & 0 deletions src/ir/lower/v31.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1824,4 +1824,59 @@ components:
panic!("Expected Named alias");
}
}

#[test]
fn test_lower_tagged_union_with_plain_internal_variants() {
let ir = lower_yaml(
r#"
openapi: "3.1.0"
info:
title: Test
version: "1.0"
components:
schemas:
ScheduleRule:
oneOf:
- type: object
required: [type, at]
properties:
type:
type: string
enum: [once]
at:
type: string
- type: object
required: [type, every_minutes, anchor_at]
properties:
type:
type: string
enum: [interval]
every_minutes:
type: integer
format: int32
anchor_at:
type: string
- type: object
required: [type, expression, timezone]
properties:
type:
type: string
enum: [cron]
expression:
type: string
timezone:
type: string
"#,
);
let schedule_rule = &ir.schemas["ScheduleRule"];
let IrSchemaKind::TaggedUnion(tu) = &schedule_rule.kind else {
panic!("Expected TaggedUnion, got {:?}", schedule_rule.kind);
};
assert!(matches!(tu.tagging, TaggingStyle::Internal));
assert_eq!(tu.discriminator_field, "type");
assert_eq!(tu.variants.len(), 3);
assert_eq!(tu.variants[0].discriminator_value, "once");
assert_eq!(tu.variants[1].discriminator_value, "interval");
assert_eq!(tu.variants[2].discriminator_value, "cron");
}
}
45 changes: 45 additions & 0 deletions src/ir/lower/v32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1886,4 +1886,49 @@ paths:
assert!(!resp.item_content.is_empty());
assert!(resp.item_content.contains_key("text/event-stream"));
}

#[test]
fn test_lower_tagged_union_with_plain_internal_variants() {
let ir = lower_yaml(
r#"
openapi: "3.2.0"
info:
title: Test
version: "1.0"
paths: {}
components:
schemas:
ScheduleRule:
oneOf:
- type: object
required: [type, at]
properties:
type:
type: string
enum: [once]
at:
type: string
- type: object
required: [type, every_minutes, anchor_at]
properties:
type:
type: string
enum: [interval]
every_minutes:
type: integer
format: int32
anchor_at:
type: string
"#,
);
let schedule_rule = &ir.schemas["ScheduleRule"];
let IrSchemaKind::TaggedUnion(tu) = &schedule_rule.kind else {
panic!("Expected TaggedUnion, got {:?}", schedule_rule.kind);
};
assert!(matches!(tu.tagging, TaggingStyle::Internal));
assert_eq!(tu.discriminator_field, "type");
assert_eq!(tu.variants.len(), 2);
assert_eq!(tu.variants[0].discriminator_value, "once");
assert_eq!(tu.variants[1].discriminator_value, "interval");
}
}
85 changes: 84 additions & 1 deletion src/ir/tagged_enum_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ impl TaggedEnumPattern {
///
/// - Externally tagged: Single required property becomes the variant name
/// - Adjacently tagged: Object with exactly 2 properties - one string enum (tag) and one object/ref (content)
/// - Internally tagged: allOf schema with a string enum property (tag field)
/// - Internally tagged: allOf schema with a string enum property (tag field),
/// or a plain object with a required single-value string enum tag field
/// alongside its content properties
/// - Untagged: Schema reference to a component schema
pub fn detect_from_schema(schema_ref: &ObjectOrReference<ObjectSchema>) -> Option<Self> {
match schema_ref {
Expand Down Expand Up @@ -151,6 +153,33 @@ impl TaggedEnumPattern {
});
}
}
ObjectOrReference::Object(obj_schema)
if obj_schema.properties.len() > 2 && obj_schema.all_of.is_empty() =>
{
let mut tag_field: Option<String> = None;
let mut enum_value: Option<String> = None;
for (prop_name, prop_schema) in &obj_schema.properties {
if !obj_schema.required.contains(prop_name) {
continue;
}
if let ObjectOrReference::Object(prop_obj) = prop_schema
&& prop_obj.enum_values.len() == 1
&& let Some(serde_json::Value::String(enum_val)) =
prop_obj.enum_values.first()
{
tag_field = Some(prop_name.clone());
enum_value = Some(enum_val.clone());
break;
}
}
if let (Some(tag_field), Some(enum_val)) = (tag_field, enum_value) {
let variant_name = enum_val.to_pascal_case();
return Some(TaggedEnumPattern::InternallyTagged {
variant_name,
tag_field,
});
}
}
ObjectOrReference::Object(obj_schema) if !obj_schema.all_of.is_empty() => {
for item in &obj_schema.all_of {
if let ObjectOrReference::Object(item_schema) = item {
Expand Down Expand Up @@ -264,6 +293,33 @@ impl TaggedEnumPattern {
});
}
}
ObjectOrReference32::Object(obj_schema)
if obj_schema.properties.len() > 2 && obj_schema.all_of.is_empty() =>
{
let mut tag_field: Option<String> = None;
let mut enum_value: Option<String> = None;
for (prop_name, prop_schema) in &obj_schema.properties {
if !obj_schema.required.contains(prop_name) {
continue;
}
if let ObjectOrReference32::Object(prop_obj) = prop_schema
&& prop_obj.enum_values.len() == 1
&& let Some(serde_json::Value::String(enum_val)) =
prop_obj.enum_values.first()
{
tag_field = Some(prop_name.clone());
enum_value = Some(enum_val.clone());
break;
}
}
if let (Some(tag_field), Some(enum_val)) = (tag_field, enum_value) {
let variant_name = enum_val.to_pascal_case();
return Some(TaggedEnumPattern::InternallyTagged {
variant_name,
tag_field,
});
}
}
ObjectOrReference32::Object(obj_schema) if !obj_schema.all_of.is_empty() => {
for item in &obj_schema.all_of {
if let ObjectOrReference32::Object(item_schema) = item {
Expand Down Expand Up @@ -358,6 +414,33 @@ impl TaggedEnumPattern {
});
}
}
ObjectOrReference30::Object(obj_schema)
if obj_schema.properties.len() > 2 && obj_schema.all_of.is_empty() =>
{
let mut tag_field: Option<String> = None;
let mut enum_value: Option<String> = None;
for (prop_name, prop_schema) in &obj_schema.properties {
if !obj_schema.required.contains(prop_name) {
continue;
}
if let ObjectOrReference30::Object(prop_obj) = prop_schema
&& prop_obj.enum_values.len() == 1
&& let Some(serde_json::Value::String(enum_val)) =
prop_obj.enum_values.first()
{
tag_field = Some(prop_name.clone());
enum_value = Some(enum_val.clone());
break;
}
}
if let (Some(tag_field), Some(enum_val)) = (tag_field, enum_value) {
let variant_name = enum_val.to_pascal_case();
return Some(TaggedEnumPattern::InternallyTagged {
variant_name,
tag_field,
});
}
}
ObjectOrReference30::Object(obj_schema) if !obj_schema.all_of.is_empty() => {
for item in &obj_schema.all_of {
if let ObjectOrReference30::Object(item_schema) = item {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
openapi: 3.1.0
info:
title: Discriminated Union Plain Internal Tagging Test
description: |
Internally tagged oneOf whose inline variant objects carry the required
discriminator plus their content properties directly (no allOf wrapper).
version: 1.0.0
paths:
/preview:
post:
summary: Preview a schedule rule
operationId: preview
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PreviewRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PreviewRequest'
components:
schemas:
ScheduleRule:
oneOf:
- type: object
required: [type, at]
properties:
type:
type: string
enum: [once]
at:
type: string
- type: object
required: [type, every_minutes, anchor_at]
properties:
type:
type: string
enum: [interval]
every_minutes:
type: integer
format: int32
anchor_at:
type: string
- type: object
required: [type, expression, timezone]
properties:
type:
type: string
enum: [cron]
expression:
type: string
timezone:
type: string
PreviewRequest:
type: object
required: [rule]
properties:
rule:
$ref: '#/components/schemas/ScheduleRule'
Loading
Loading