diff --git a/bin/configs/rust-hyper-anyof.yaml b/bin/configs/rust-hyper-anyof.yaml new file mode 100644 index 000000000000..008f1e7b06e7 --- /dev/null +++ b/bin/configs/rust-hyper-anyof.yaml @@ -0,0 +1,8 @@ +generatorName: rust +outputDir: samples/client/others/rust/hyper/anyof +library: hyper +inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/rust-anyof-test.yaml +templateDir: modules/openapi-generator/src/main/resources/rust +additionalProperties: + supportAsync: false + packageName: anyof-hyper diff --git a/docs/anyof-vs-oneof-true-semantics.md b/docs/anyof-vs-oneof-true-semantics.md new file mode 100644 index 000000000000..efcc56c25d57 --- /dev/null +++ b/docs/anyof-vs-oneof-true-semantics.md @@ -0,0 +1,346 @@ +# The Complete Picture: allOf vs anyOf vs oneOf + +## The Three Composition Keywords - Finally Clear! + +### allOf = Required Composition (Must Match ALL) +**allOf requires the object to be valid against ALL schemas simultaneously - it's a mandatory merge.** + +```yaml +# allOf Example - Required Composition +Employee: + allOf: + - $ref: '#/components/schemas/Person' + - $ref: '#/components/schemas/Worker' + - type: object + required: [employeeId] + properties: + employeeId: + type: string + +Person: + type: object + required: [name, age] + properties: + name: + type: string + age: + type: integer + +Worker: + type: object + required: [position, salary] + properties: + position: + type: string + salary: + type: number +``` + +**Valid allOf instance (MUST have all required fields):** +```json +{ + "name": "John Doe", // Required from Person + "age": 30, // Required from Person + "position": "Engineer", // Required from Worker + "salary": 100000, // Required from Worker + "employeeId": "EMP001" // Required from inline schema +} +``` + +**Correct Rust Implementation:** +```rust +// allOf: Struct with ALL required fields +pub struct Employee { + // From Person (required) + pub name: String, + pub age: i32, + + // From Worker (required) + pub position: String, + pub salary: f64, + + // From inline schema (required) + pub employee_id: String, +} +``` + +### anyOf = Optional Composition (Match ONE OR MORE) +**anyOf is about composing a new object from multiple schemas that can all be valid simultaneously.** + +```yaml +# anyOf Example - Object Composition +PersonWithEmployment: + anyOf: + - $ref: '#/components/schemas/PersonalInfo' + - $ref: '#/components/schemas/EmploymentInfo' + - $ref: '#/components/schemas/EducationInfo' + +# Components +PersonalInfo: + type: object + properties: + name: + type: string + age: + type: integer + +EmploymentInfo: + type: object + properties: + company: + type: string + position: + type: string + +EducationInfo: + type: object + properties: + degree: + type: string + university: + type: string +``` + +**Valid anyOf instance (combines all three):** +```json +{ + "name": "John Doe", + "age": 30, + "company": "TechCorp", + "position": "Engineer", + "degree": "MS Computer Science", + "university": "MIT" +} +``` + +This object is valid against PersonalInfo AND EmploymentInfo AND EducationInfo simultaneously! + +### oneOf = Choice (Either/Or) +**oneOf is about choosing exactly one option from mutually exclusive alternatives.** + +```yaml +# oneOf Example - Mutually Exclusive Choice +PaymentMethod: + oneOf: + - $ref: '#/components/schemas/CreditCard' + - $ref: '#/components/schemas/BankTransfer' + - $ref: '#/components/schemas/PayPal' + +# Components with discriminating required fields +CreditCard: + type: object + required: [cardNumber, cvv] + properties: + cardNumber: + type: string + cvv: + type: string + +BankTransfer: + type: object + required: [accountNumber, routingNumber] + properties: + accountNumber: + type: string + routingNumber: + type: string + +PayPal: + type: object + required: [paypalEmail] + properties: + paypalEmail: + type: string +``` + +**Valid oneOf instance (exactly one):** +```json +{ + "cardNumber": "1234-5678-9012-3456", + "cvv": "123" +} +``` + +## The Fundamental Difference + +### anyOf = Additive/Compositional (AND logic) +- **Purpose**: Combine properties from multiple schemas +- **Validation**: Can validate against multiple schemas +- **Use Case**: Mixins, traits, optional feature sets +- **Think**: "This object can have properties from schema A AND schema B AND schema C" + +### oneOf = Selective/Exclusive (XOR logic) +- **Purpose**: Choose one schema from alternatives +- **Validation**: Must validate against exactly one schema +- **Use Case**: Polymorphic types, variant records +- **Think**: "This object is EITHER type A OR type B OR type C" + +## What This Means for Implementations + +### Correct anyOf Implementation (Composition) + +```rust +// CORRECT: Struct that can compose multiple schemas +pub struct PersonWithEmployment { + // From PersonalInfo + pub name: Option, + pub age: Option, + + // From EmploymentInfo + pub company: Option, + pub position: Option, + + // From EducationInfo + pub degree: Option, + pub university: Option, +} +``` + +### Current (Wrong) anyOf Implementation in Most Generators + +```rust +// WRONG: Treats anyOf like oneOf (choice instead of composition) +pub enum PersonWithEmployment { + PersonalInfo(PersonalInfo), // Wrong! Can only be one + EmploymentInfo(EmploymentInfo), // Should be able to combine + EducationInfo(EducationInfo), // All three simultaneously +} +``` + +## Real-World Example: API Response + +```yaml +# anyOf for composition - response can have data AND/OR errors AND/OR warnings +ApiResponse: + anyOf: + - type: object + properties: + data: + type: object + - type: object + properties: + errors: + type: array + items: + type: string + - type: object + properties: + warnings: + type: array + items: + type: string +``` + +**Valid response (has all three):** +```json +{ + "data": { "id": 123, "name": "Success" }, + "errors": [], + "warnings": ["Deprecated endpoint"] +} +``` + +## The Problem with Current Implementations + +Most generators (except Python and our new Rust approach) treat anyOf like oneOf: + +| Generator | anyOf Implementation | Correct? | +|-----------|---------------------|----------| +| TypeScript | Union type `A \| B` | ❌ No - can't compose | +| Java | Abstract class with one active | ❌ No - can't compose | +| Old Rust | Enum (one variant) | ❌ No - can't compose | +| Python | Validates all, keeps track | ✅ Yes - true composition | +| New Rust | Struct with optional fields | ✅ Yes - true composition | + +## The Complete Summary + +### The Three Keywords - Correct Semantics + +| Keyword | Semantics | Validation | Correct Implementation | Logic Type | +|---------|-----------|------------|------------------------|------------| +| **allOf** | Required composition | Must match ALL schemas | Struct with all required fields merged | AND (mandatory) | +| **anyOf** | Optional composition | Must match ONE OR MORE schemas | Struct with optional fields from all schemas | OR (inclusive) | +| **oneOf** | Exclusive choice | Must match EXACTLY ONE schema | Enum with variants | XOR (exclusive) | + +### Correct Rust Implementations + +```rust +// allOf: Everything required +pub struct FullEmployee { + pub name: String, // Required from Person + pub age: i32, // Required from Person + pub position: String, // Required from Worker + pub salary: f64, // Required from Worker + pub employee_id: String, // Required from additional +} + +// anyOf: Optional composition +pub struct FlexibleEmployee { + pub name: Option, // Can have Person fields + pub age: Option, + pub position: Option, // Can have Worker fields + pub salary: Option, + pub employee_id: Option, // Can have additional fields + // Can have any combination! +} + +// oneOf: Exclusive choice +pub enum EmployeeType { + Contractor(Contractor), // Either contractor + FullTime(FullTime), // OR full-time + Intern(Intern), // OR intern + // Exactly one! +} +``` + +### The Key Insight + +You've identified the fundamental pattern: + +| | Composition? | Required? | Result Type | +|---|---|---|---| +| **allOf** | ✅ Yes | ✅ All fields required | Merged struct | +| **anyOf** | ✅ Yes | ❌ Fields optional | Struct with options | +| **oneOf** | ❌ No | N/A (choice) | Enum/Union | + +### Why This Matters + +Most generators get anyOf wrong because they treat it as a choice (like oneOf) instead of composition: +- **Wrong**: anyOf as enum/union (can only have one) +- **Right**: anyOf as struct with optional fields (can have multiple) + +Your understanding is correct: +- **allOf** = "You must be ALL of these things" +- **anyOf** = "You can be ANY combination of these things" +- **oneOf** = "You must be EXACTLY ONE of these things" + +## Real-World Bug Example + +Just discovered this while working with wing328's test case. The old Rust generator would literally generate broken code for this anyOf: + +```yaml +ModelIdentifier: + anyOf: + - type: string + - type: string + enum: [gpt-4, gpt-3.5-turbo] +``` + +Old generator output: +```rust +pub enum ModelIdentifier { + String(String), // Variant 1 + String(String), // Variant 2 - DUPLICATE NAME! Won't compile! +} +``` + +This is what happens when you treat anyOf (composition) as oneOf (choice) - you get nonsensical code. The correct approach generates a struct where both can coexist. + +## Conclusion + +You're absolutely right: +- **allOf** = Required composition (struct with required fields) +- **anyOf** = Optional composition (struct with optional fields) +- **oneOf** = Exclusive choice (enum) + +Most implementations conflate anyOf with oneOf, missing that anyOf is about composition, not choice! \ No newline at end of file diff --git a/docs/generators/rust-type-mapping.md b/docs/generators/rust-type-mapping.md new file mode 100644 index 000000000000..747fee5025f5 --- /dev/null +++ b/docs/generators/rust-type-mapping.md @@ -0,0 +1,766 @@ +# Rust Generator Type Mapping Documentation + +## Overview + +This document comprehensively describes how the OpenAPI Generator maps OpenAPI/JSON Schema types to Rust types, including the current implementation decisions, alternatives considered, and future improvements. + +## Current Implementation (As of v7.16.0) + +### Basic Type Mappings + +| OpenAPI Type | Format | Rust Type | Notes | +|-------------|---------|-----------|-------| +| `integer` | `int32` | `i32` | Default signed 32-bit integer | +| `integer` | `int64` | `i64` | Signed 64-bit integer | +| `integer` | (none) | `i32` | Default when no format specified | +| `integer` | (with minimum >= 0) | `u32` or `u64` | When `preferUnsignedInt=true` | +| `number` | `float` | `f32` | 32-bit floating point | +| `number` | `double` | `f64` | 64-bit floating point | +| `number` | (none) | `f32` | Default when no format specified | +| `string` | (none) | `String` | Heap-allocated string | +| `string` | `byte` | `Vec` | Base64 encoded bytes | +| `string` | `binary` | `Vec` | Binary data | +| `string` | `date` | `String` | ISO 8601 date (could use chrono::NaiveDate) | +| `string` | `date-time` | `String` | ISO 8601 datetime (could use chrono::DateTime) | +| `string` | `password` | `String` | Same as regular string | +| `string` | `uuid` | `uuid::Uuid` | When uuid crate is included | +| `boolean` | (none) | `bool` | Boolean value | +| `array` | (none) | `Vec` | Dynamic array of type T | +| `object` | (none) | `HashMap` | When additionalProperties defined | +| `object` | (none) | Generated struct | When properties are defined | + +### Complex Schema Handling + +#### Regular Objects +```yaml +# OpenAPI Schema +Person: + type: object + properties: + name: + type: string + age: + type: integer + required: + - name +``` + +```rust +// Generated Rust Code +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Person { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub age: Option, +} +``` + +#### Enums +```yaml +# OpenAPI Schema +Status: + type: string + enum: + - pending + - approved + - rejected +``` + +```rust +// Generated Rust Code +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Status { + #[serde(rename = "pending")] + Pending, + #[serde(rename = "approved")] + Approved, + #[serde(rename = "rejected")] + Rejected, +} +``` + +#### oneOf (Exclusive Choice - XOR) +```yaml +# OpenAPI Schema +Pet: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' +``` + +```rust +// Generated Rust Code (without discriminator) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Pet { + Cat(Box), + Dog(Box), +} + +// Generated Rust Code (with discriminator) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "petType")] +pub enum Pet { + #[serde(rename = "cat")] + Cat(Box), + #[serde(rename = "dog")] + Dog(Box), +} +``` + +#### anyOf (Currently treated as oneOf) +```yaml +# OpenAPI Schema +StringOrNumber: + anyOf: + - type: string + - type: number +``` + +```rust +// Current Generated Rust Code (INCORRECT SEMANTICS) +// After PR #21896 - treats anyOf as oneOf +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StringOrNumber { + String(String), + Number(f32), +} +``` + +**Problem**: This implementation enforces XOR semantics (exactly one must match) instead of OR semantics (one or more can match). + +#### allOf (Not Supported) +```yaml +# OpenAPI Schema +Employee: + allOf: + - $ref: '#/components/schemas/Person' + - type: object + properties: + employeeId: + type: string + department: + type: string +``` + +```rust +// Option 1: What SHOULD ideally be generated (flattened struct) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Employee { + // Fields from Person + pub name: String, + pub age: Option, + // Additional fields + pub employee_id: String, + pub department: String, +} + +// Option 2: Alternative with composition (avoiding property conflicts) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Employee { + #[serde(flatten)] + pub person: Person, + + #[serde(flatten)] + pub object_1: EmployeeObject1, // Anonymous object gets generated name +} + +// Generated for the anonymous object in allOf +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EmployeeObject1 { + pub employee_id: String, + pub department: String, +} +``` + +**Current behavior**: The generator fails or produces incorrect output for allOf schemas. + +**Note**: Option 2 avoids property name conflicts that could occur if multiple schemas in allOf define the same property names. This approach maintains type safety while preserving the composition structure. + +### Nullable Handling + +```yaml +# OpenAPI 3.0 nullable +name: + type: string + nullable: true + +# OpenAPI 3.1 nullable +name: + type: ["string", "null"] +``` + +```rust +// Generated Rust Code +pub name: Option, +``` + +### Box Usage for Recursive Types + +The generator uses `Box` to handle recursive types and prevent infinite size structs: + +```rust +// Without avoidBoxedModels (default) +pub struct Node { + pub children: Option>>, // Box prevents infinite size +} + +// With avoidBoxedModels=true +pub struct Node { + pub children: Option>, // Will fail to compile if truly recursive +} +``` + +## Alternatives Considered (But Not Implemented) + +### Alternative 1: Composition-based allOf Support + +For allOf schemas, instead of trying to flatten all properties into a single struct (which can cause naming conflicts), use composition with serde's flatten: + +```rust +// Alternative: Composition approach for allOf +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Employee { + #[serde(flatten)] + pub person: Person, + + #[serde(flatten)] + pub additional_props: EmployeeAdditional, +} + +// Benefits: +// 1. Avoids property name conflicts +// 2. Maintains clear composition structure +// 3. Works with serde's existing flatten feature +// 4. Each component can be validated independently + +// Challenges: +// 1. Generated names for anonymous objects (Object$1, Object$2, etc.) +// 2. Requires serde flatten support +// 3. May need special handling for required vs optional fields +``` + +**Pros**: +- Avoids property naming conflicts +- Clear composition structure +- Leverages serde's existing features + +**Cons**: +- Generated names for anonymous schemas +- More complex serialization +- Potential issues with nested flattening + +### Alternative 2: True anyOf Support with Validation Trait + +Instead of treating anyOf as oneOf, we could generate a validation trait: + +```rust +// Alternative: Validation trait approach +pub trait ValidatesAnyOf { + fn validate_schemas(&self) -> Vec; + fn is_valid(&self) -> bool { + self.validate_schemas().iter().any(|&v| v) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StringOrNumber { + String(String), + Number(f32), + Both { string_val: String, number_val: f32 }, // Allows both! +} + +impl ValidatesAnyOf for StringOrNumber { + fn validate_schemas(&self) -> Vec { + match self { + Self::String(_) => vec![true, false], + Self::Number(_) => vec![false, true], + Self::Both { .. } => vec![true, true], + } + } +} +``` + +**Pros**: Semantically correct +**Cons**: Complex, requires custom serde implementation + +### Alternative 2: Tagged Unions for Better Error Messages + +Instead of untagged enums, use internally tagged enums: + +```rust +// Alternative: Internally tagged +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Pet { + #[serde(rename = "cat")] + Cat { name: String, meow_volume: i32 }, + #[serde(rename = "dog")] + Dog { name: String, bark_volume: i32 }, +} +``` + +**Pros**: Better error messages, explicit type discrimination +**Cons**: Requires discriminator in schema, changes JSON structure + +### Alternative 3: Wrapper Types for Semantic Clarity + +```rust +// Alternative: Wrapper types +pub struct AnyOf(pub T); +pub struct OneOf(pub T); +pub struct AllOf(pub T); + +impl AnyOf { + pub fn validate(&self) -> Result<(), ValidationError> { + // Custom validation logic + } +} +``` + +**Pros**: Clear semantic intent +**Cons**: Additional complexity, ergonomics issues + +### Alternative 4: Macro-based Generation + +```rust +// Alternative: Procedural macros +#[derive(OpenApiSchema)] +#[openapi(any_of = ["String", "Number"])] +pub struct StringOrNumber { + #[openapi(schema = 0)] + as_string: Option, + #[openapi(schema = 1)] + as_number: Option, +} +``` + +**Pros**: Compile-time validation, flexible +**Cons**: Requires proc-macro crate, compilation overhead + +## Configuration Options Impact + +### preferUnsignedInt +When `true`, integers with `minimum >= 0` generate unsigned types: +```rust +// preferUnsignedInt=false (default) +pub age: i32, // Even with minimum: 0 + +// preferUnsignedInt=true +pub age: u32, // When minimum: 0 +``` + +### bestFitInt +When `true`, chooses the smallest integer type that fits the constraints: +```rust +// bestFitInt=false (default) +pub small_number: i32, // Even with min:0, max:100 + +// bestFitInt=true +pub small_number: u8, // Fits in u8 (0-255) +``` + +### avoidBoxedModels +Controls whether to use `Box` for nested models: +```rust +// avoidBoxedModels=false (default) +pub nested: Box, + +// avoidBoxedModels=true +pub nested: NestedModel, +``` + +## Known Limitations and Issues + +1. **anyOf Semantics**: Currently generates XOR (enum) instead of OR (multiple valid) +2. **allOf Not Supported**: Cannot compose multiple schemas +3. **Discriminator Limitations**: Only supports property-based discriminators +4. **Date/Time Types**: Uses String instead of chrono types +5. **Validation**: No runtime validation generated +6. **Integer Overflow**: No automatic BigInteger support for large numbers +7. **Pattern Properties**: Not supported for dynamic object keys +8. **JSON Schema Keywords**: Many keywords ignored (minLength, pattern, etc.) + +## Why These Decisions Were Made + +### Why anyOf → oneOf Conversion? + +The initial implementation chose to convert anyOf to oneOf because: + +1. **Serde Limitations**: Rust's serde library naturally supports tagged/untagged enums for "one of" semantics +2. **Type Safety**: Rust's type system prefers sum types (enums) over union types +3. **Ergonomics**: Enums with pattern matching are idiomatic in Rust +4. **Complexity**: True anyOf support requires complex validation logic + +### Why No allOf Support? + +1. **Composition Complexity**: Rust doesn't have built-in struct composition/inheritance +2. **Serde Challenges**: While serde supports `#[serde(flatten)]`, handling it in code generation is complex +3. **Conflicting Fields**: When multiple schemas define the same property names, resolution is non-trivial: + - Option A: Merge and error on conflicts (strict) + - Option B: Last-wins override (loose but surprising) + - Option C: Composition with generated names (Object$1, Object$2) - avoids conflicts but less ergonomic +4. **Anonymous Schema Naming**: allOf often contains inline anonymous schemas that need generated names +5. **Priority**: Less commonly used than oneOf in practice + +## To-Be: Proposed Changes (PR #21915) + +### True anyOf Support with Struct-Based Approach + +Instead of converting anyOf to oneOf (enum), generate structs with optional fields: + +#### Current (Incorrect) +```yaml +# Schema +ModelIdentifier: + anyOf: + - type: string + description: Any model name + - type: string + enum: [gpt-4, gpt-3.5-turbo] + description: Known models +``` + +```rust +// Current: WRONG - Generates enum (XOR semantics) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ModelIdentifier { + String(String), // Problem: Duplicate variant names! + String(String), // This won't even compile! +} +``` + +#### Proposed (Correct) +```rust +// Proposed: Generates struct with optional fields (OR semantics) +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ModelIdentifier { + /// Any model name as string (anyOf option) + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_0")] + pub as_any_of_0: Option, + + /// Known model enum values (anyOf option) + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_1")] + pub as_any_of_1: Option, +} + +impl ModelIdentifier { + /// Creates a new ModelIdentifier with all fields set to None + pub fn new() -> Self { + Default::default() + } + + /// Validates that at least one anyOf field is set (OR semantics) + pub fn validate_any_of(&self) -> Result<(), String> { + if self.as_any_of_0.is_none() && self.as_any_of_1.is_none() { + return Err("At least one anyOf field must be set".to_string()); + } + Ok(()) + } +} +``` + +### Implementation Details + +1. **Java Generator Changes** (`RustClientCodegen.java`): + - Keep anyOf schemas separate from oneOf + - Process anyOf with different template logic + - Generate appropriate field names for anyOf options + +2. **Mustache Template Changes** (`model.mustache`): + - Add new anyOf section that generates structs + - Include validation method for OR semantics + - Use optional fields with serde skip attributes + +3. **Benefits**: + - **Semantically Correct**: Properly implements OR semantics + - **No Compilation Errors**: Avoids duplicate enum variant issues + - **Validation Support**: Includes validation method + - **Extensible**: Can add more sophisticated validation later + +4. **Trade-offs**: + - **Breaking Change**: Existing code using anyOf will break + - **Less Idiomatic**: Structs with optional fields less elegant than enums + - **Manual Validation**: Requires calling validate_any_of() explicitly + - **Serialization Complexity**: Multiple fields vs single value + +### Migration Path + +For users currently relying on anyOf → oneOf conversion: + +```rust +// Old code (with enum) +match model_identifier { + ModelIdentifier::Variant1(s) => println!("String: {}", s), + ModelIdentifier::Variant2(n) => println!("Number: {}", n), +} + +// New code (with struct) +if let Some(s) = &model_identifier.as_any_of_0 { + println!("String: {}", s); +} +if let Some(n) = &model_identifier.as_any_of_1 { + println!("Number: {}", n); +} +``` + +### Future Improvements + +1. **Phase 1** (Current PR): Basic struct-based anyOf support +2. **Phase 2**: Add allOf support using struct flattening +3. **Phase 3**: Improve validation with custom derive macros +4. **Phase 4**: Add discriminator support for anyOf +5. **Phase 5**: Consider union type alternatives when Rust supports them + +## Cross-Language Comparison: How Other Strongly-Typed Languages Handle oneOf/anyOf/allOf + +### Java + +**oneOf**: +- Generates an interface/abstract class with concrete implementations +- With discriminator: Uses Jackson's `@JsonTypeInfo` and `@JsonSubTypes` for polymorphic deserialization +- Without discriminator: Creates wrapper class with multiple typed fields, only one can be set +- Recent versions support sealed interfaces (Java 17+) + +```java +// oneOf with discriminator +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "petType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = Cat.class, name = "cat"), + @JsonSubTypes.Type(value = Dog.class, name = "dog") +}) +public abstract class Pet { } + +// oneOf without discriminator +public class StringOrNumber { + private String stringValue; + private Integer numberValue; + + // Only one setter can be called + public void setString(String value) { + this.stringValue = value; + this.numberValue = null; + } +} +``` + +**anyOf**: +- Often treated same as oneOf (incorrect) +- Some implementations generate a class with all possible fields as nullable +- No true validation that at least one matches + +**allOf**: +- Uses inheritance when possible (extends base class) +- Composition with interfaces for multiple inheritance +- Flattens properties into single class + +### TypeScript + +**oneOf/anyOf**: +- Uses union types naturally: `type Pet = Cat | Dog` +- Runtime validation required (not enforced by type system) +- anyOf and oneOf generate identical code (union types) + +```typescript +// Both oneOf and anyOf generate: +export type StringOrNumber = string | number; +export type Pet = Cat | Dog; + +// With discriminator: +export type Pet = + | { petType: "cat" } & Cat + | { petType: "dog" } & Dog; +``` + +**allOf**: +- Uses intersection types: `type Employee = Person & { employeeId: string }` +- Natural composition support in type system + +```typescript +export type Employee = Person & { + employeeId: string; + department: string; +}; +``` + +### Go + +**oneOf/anyOf**: +- No union types in Go +- Generates struct with pointers to all possible types +- Custom marshaling/unmarshaling logic +- Validation methods to ensure constraints + +```go +// oneOf/anyOf implementation +type StringOrNumber struct { + String *string `json:"-"` + Number *float32 `json:"-"` +} + +func (s *StringOrNumber) UnmarshalJSON(data []byte) error { + // Try unmarshaling as each type + var str string + if err := json.Unmarshal(data, &str); err == nil { + s.String = &str + return nil + } + + var num float32 + if err := json.Unmarshal(data, &num); err == nil { + s.Number = &num + return nil + } + + return errors.New("could not unmarshal as string or number") +} +``` + +**allOf**: +- Uses struct embedding (composition) +- Flattens nested structures +- Can have field name conflicts + +```go +type Employee struct { + Person // Embedded struct + EmployeeId string `json:"employeeId"` + Department string `json:"department"` +} +``` + +### C# (.NET) + +**oneOf**: +- Generates abstract base class with derived types +- With discriminator: Uses JsonConverter for polymorphic serialization +- Without discriminator: Wrapper class with nullable properties + +```csharp +// oneOf with discriminator +[JsonPolymorphic(TypeDiscriminatorPropertyName = "petType")] +[JsonDerivedType(typeof(Cat), "cat")] +[JsonDerivedType(typeof(Dog), "dog")] +public abstract class Pet { } + +// oneOf without discriminator +public class StringOrNumber +{ + public string? AsString { get; set; } + public int? AsNumber { get; set; } + + // Validation ensures only one is set +} +``` + +**anyOf**: +- Similar to oneOf but allows multiple properties to be set +- Often incorrectly treated as oneOf + +**allOf**: +- Uses inheritance for single parent +- Composition pattern for multiple schemas +- Properties flattened into derived class + +### Python (with Pydantic) + +**oneOf/anyOf**: +- Uses `Union` type hint +- Pydantic validates at runtime +- First matching schema wins (for both oneOf and anyOf) + +```python +# oneOf/anyOf +StringOrNumber = Union[str, int] + +class Pet(BaseModel): + __root__: Union[Cat, Dog] + + # With discriminator + class Config: + discriminator = 'petType' +``` + +**allOf**: +- Multiple inheritance with mixins +- Pydantic handles field merging + +```python +class Employee(Person, EmployeeFields): + pass # Inherits from both +``` + +### Swift + +**oneOf**: +- Native enum with associated values (perfect fit!) +- Type-safe and ergonomic + +```swift +enum Pet: Codable { + case cat(Cat) + case dog(Dog) + + // Custom coding keys for discriminator +} + +enum StringOrNumber: Codable { + case string(String) + case number(Int) +} +``` + +**anyOf**: +- No native support +- Usually treated as oneOf (enum) + +**allOf**: +- Protocol composition +- Struct with all properties + +### Comparison Table + +| Language | oneOf | anyOf | allOf | +|----------|-------|-------|-------| +| **Rust (current)** | Enum (untagged) | Enum (incorrect) | ❌ Not supported | +| **Rust (proposed)** | Enum (untagged) | Struct with Option fields | ❌ Not supported | +| **Java** | Interface + implementations | Same as oneOf (incorrect) | Inheritance/Composition | +| **TypeScript** | Union type | Union type (same) | Intersection type | +| **Go** | Struct with pointers | Struct with pointers | Struct embedding | +| **C#** | Abstract class + derived | Similar to oneOf | Inheritance/Composition | +| **Python** | Union + validation | Union (same) | Multiple inheritance | +| **Swift** | Enum with associated values | Enum (incorrect) | Protocol composition | + +### Key Observations + +1. **No language perfectly handles anyOf**: Most treat it identical to oneOf, missing the "one or more" semantics +2. **TypeScript has the best model**: Union and intersection types naturally express these concepts +3. **Swift enums are ideal for oneOf**: Associated values provide perfect type safety +4. **Go's approach is explicit**: No magic, clear what's happening, verbose but correct +5. **Dynamic languages rely on runtime validation**: Python/Ruby validate at runtime, not compile time + +### Why Rust's Proposed Approach Makes Sense + +Given Rust's type system constraints: +- **No union types**: Can't do TypeScript-style unions +- **No inheritance**: Can't do Java/C# style class hierarchies +- **Strong type safety**: Want compile-time guarantees + +The proposed struct-with-optional-fields for anyOf: +- **Explicit about semantics**: Clear that multiple can be set +- **Type safe**: Compiler enforces field types +- **Serde compatible**: Works with existing serialization +- **Migration path**: Different from enum, so existing code breaks loudly (good!) + +## Conclusion + +The current Rust generator makes pragmatic choices favoring simplicity and Rust idioms over strict OpenAPI compliance. The proposed changes in PR #21915 move toward semantic correctness while maintaining reasonable ergonomics. + +Comparing with other languages shows that: +1. No language has solved anyOf perfectly +2. Rust's constraints (no unions, no inheritance) require creative solutions +3. The proposed struct approach for anyOf is reasonable given these constraints +4. Future work should focus on incremental improvements guided by user needs and Rust ecosystem evolution \ No newline at end of file diff --git a/docs/rust-oneof-anyof-semantics.md b/docs/rust-oneof-anyof-semantics.md new file mode 100644 index 000000000000..9555834b11fe --- /dev/null +++ b/docs/rust-oneof-anyof-semantics.md @@ -0,0 +1,469 @@ +# Rust Generator: oneOf vs anyOf Semantics + +## Overview + +The Rust OpenAPI generator properly implements the semantic differences between `oneOf` and `anyOf` schemas as defined in the OpenAPI specification: + +- **oneOf (XOR)**: Exactly one of the schemas must validate +- **anyOf (OR)**: One or more of the schemas must validate + +### OpenAPI Specification References + +From the [OpenAPI 3.1.0 Specification](https://spec.openapis.org/oas/v3.1.0#schema-object): + +- **[oneOf](https://spec.openapis.org/oas/v3.1.0#composition-and-inheritance-polymorphism)**: "Validates the value against exactly one of the subschemas" +- **[anyOf](https://spec.openapis.org/oas/v3.1.0#composition-and-inheritance-polymorphism)**: "Validates the value against any (one or more) of the subschemas" + +These keywords come from [JSON Schema](https://json-schema.org/understanding-json-schema/reference/combining.html) and maintain the same semantics. + +## Should Untagged Enums Be Allowed? A Spec Analysis + +### The Discriminator Dilemma + +The OpenAPI specification states: + +> "To support polymorphism, the OpenAPI Specification adds the discriminator field. When used, the discriminator will be the name of the property that decides which schema definition validates the structure of the model. As such, the discriminator field MUST be a required field." + +However, this raises important questions: + +1. **Is discriminator required for all oneOf schemas?** No, the spec says "when used" - it's optional. +2. **Does oneOf without discriminator violate the spec?** No, but it may violate the intent. + +### JSON Schema vs OpenAPI Semantics + +**JSON Schema requirement** (which OpenAPI inherits): +- oneOf: "The given data must be valid against **exactly one** of the given subschemas" +- This requires checking ALL subschemas to ensure only one matches + +**Implementation reality**: +- Most generators use "first match wins" for untagged unions +- This violates the strict oneOf semantics unless additional validation is performed + +### The Case for Validation Errors + +**You're correct that strictly speaking, generators should validate that exactly one schema matches for oneOf.** This means: + +1. **Untagged enums are technically non-compliant** if they don't validate exclusivity +2. **Validation errors should be thrown** when multiple schemas match +3. **"First match wins" is a pragmatic compromise** that violates the spec + +### Current Implementations vs Spec Compliance + +| Approach | Spec Compliant? | Used By | +|----------|----------------|---------| +| First match wins (no validation) | ❌ No | Rust, Java, C# | +| Validate exactly one matches | ✅ Yes | Python (Pydantic) | +| Require discriminator | ✅ Yes (conservative) | None (but recommended) | +| Generate error for ambiguous schemas | ✅ Yes (conservative) | None currently | + +### Implications for Rust Implementation + +The current Rust implementation using untagged enums is **pragmatic but not strictly compliant** because: + +1. Serde's `#[serde(untagged)]` stops at first match +2. No validation that other variants wouldn't also match +3. Could silently accept invalid data that matches multiple schemas + +**To be fully compliant**, Rust would need to: +```rust +// Validate against all variants +impl<'de> Deserialize<'de> for OneOfExample { + fn deserialize(deserializer: D) -> Result { + let value = Value::deserialize(deserializer)?; + let mut matches = 0; + + if let Ok(_) = Type1::deserialize(&value) { matches += 1; } + if let Ok(_) = Type2::deserialize(&value) { matches += 1; } + + if matches != 1 { + return Err(Error::custom("Must match exactly one schema")); + } + + // Then do actual deserialization + } +} +``` + +## Implementation Details + +### oneOf - Untagged Enums + +For `oneOf` schemas without a discriminator, the generator creates untagged enums using Serde's `#[serde(untagged)]` attribute: + +```yaml +# OpenAPI Schema +SimpleOneOf: + oneOf: + - type: string + - type: number +``` + +```rust +// Generated Rust Code +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SimpleOneOf { + String(String), + Number(f64), +} +``` + +**Behavior**: When deserializing, Serde tries each variant in order and stops at the first match. This ensures exactly one variant is selected. + +### anyOf - Structs with Optional Fields + +For `anyOf` schemas, the generator creates structs with optional fields, allowing multiple schemas to be valid simultaneously: + +```yaml +# OpenAPI Schema +SimpleAnyOf: + anyOf: + - type: string + - type: number +``` + +```rust +// Generated Rust Code +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimpleAnyOf { + #[serde(skip_serializing_if = "Option::is_none")] + pub as_String: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub as_Number: Option, +} + +impl SimpleAnyOf { + pub fn validate_any_of(&self) -> Result<(), String> { + if self.as_String.is_none() && self.as_Number.is_none() { + return Err("At least one anyOf field must be set".to_string()); + } + Ok(()) + } +} +``` + +**Behavior**: Multiple fields can be set, properly implementing OR semantics where data can match multiple schemas. + +## Practical Examples + +### Example 1: Person or Company (oneOf) + +```yaml +PersonOrCompany: + oneOf: + - $ref: '#/components/schemas/Person' + - $ref: '#/components/schemas/Company' +``` + +```rust +// Usage +let data = PersonOrCompany::Person(Box::new(Person { + first_name: "John".to_string(), + last_name: "Doe".to_string(), +})); +// Can ONLY be a Person OR a Company, not both +``` + +### Example 2: Person and/or Company (anyOf) + +```yaml +PersonAndOrCompany: + anyOf: + - $ref: '#/components/schemas/Person' + - $ref: '#/components/schemas/Company' +``` + +```rust +// Usage +let mut data = PersonAndOrCompany::default(); +data.as_Person = Some(Box::new(Person { + first_name: "John".to_string(), + last_name: "Doe".to_string(), +})); +data.as_Company = Some(Box::new(Company { + company_name: "Acme Corp".to_string(), +})); +// Can be BOTH a Person AND a Company simultaneously +data.validate_any_of()?; // Ensures at least one is set +``` + +### Example 3: Content Types (anyOf) + +```yaml +MixedContent: + anyOf: + - type: object + properties: + text: + type: string + - type: object + properties: + html: + type: string + - type: object + properties: + markdown: + type: string +``` + +```rust +// Can have multiple content representations +let mut content = MixedContent::default(); +content.as_text = Some("Plain text content".to_string()); +content.as_html = Some("

HTML content

".to_string()); +content.as_markdown = Some("**Markdown** content".to_string()); +// All three formats can coexist +``` + +## oneOf with Discriminator + +When a discriminator is present, `oneOf` generates a tagged enum: + +```yaml +ShapeOneOf: + oneOf: + - $ref: '#/components/schemas/Circle' + - $ref: '#/components/schemas/Rectangle' + discriminator: + propertyName: shapeType +``` + +```rust +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "shapeType")] +pub enum ShapeOneOf { + #[serde(rename = "circle")] + Circle(Circle), + #[serde(rename = "rectangle")] + Rectangle(Rectangle), +} +``` + +## How Other Languages Handle oneOf/anyOf + +### Java (with Gson) +- **oneOf**: Uses `AbstractOpenApiSchema` base class with custom type adapters +- **anyOf**: Similar to oneOf but allows multiple matches in validation +- **Approach**: Runtime type checking with reflection, tries to deserialize into each type +- **Untagged Issue**: Handled via custom `TypeAdapter` that attempts each type sequentially + +### TypeScript +- **oneOf**: Simple union types using `|` operator (e.g., `string | number | Person`) +- **anyOf**: Same as oneOf - TypeScript union types +- **Approach**: Type unions are natural in TypeScript, runtime validation depends on library +- **Untagged Issue**: Not an issue - TypeScript's structural typing handles this naturally + +### Python (Pydantic) +- **oneOf**: Uses `Union` types with custom validation +- **anyOf**: Separate class with `actual_instance` that validates against multiple schemas +- **Approach**: Runtime validation with explicit checks for which schemas match +- **Untagged Issue**: Custom deserializer tries each type and keeps track of matches + +### Go +- **oneOf**: Struct with pointer fields for each option, custom `UnmarshalJSON` +- **anyOf**: Similar structure but allows multiple fields to be non-nil +- **Approach**: All options as pointers, unmarshal attempts to populate each +- **Untagged Issue**: Custom unmarshaler tries each type, oneOf ensures only one succeeds + +### C# +- **oneOf**: Uses inheritance with base class and custom JSON converters +- **anyOf**: Similar to oneOf but validation allows multiple matches +- **Approach**: Abstract base class with derived types, custom converters handle deserialization +- **Untagged Issue**: Custom converters attempt deserialization in order + +### Comparison with Rust + +| Language | oneOf Implementation | anyOf Implementation | Untagged Handling | +|----------|---------------------|---------------------|-------------------| +| **Rust** | Untagged enum | Struct with optional fields | Serde's `#[serde(untagged)]` | +| **Java** | Abstract class + adapters | Abstract class + adapters | Custom TypeAdapter | +| **TypeScript** | Union type `A \| B` | Union type `A \| B` | Native support | +| **Python** | Union with validation | Class with multiple validators | Custom validation | +| **Go** | Struct with pointers | Struct with pointers | Custom UnmarshalJSON | +| **C#** | Base class + converters | Base class + converters | Custom JsonConverter | + +### Key Observations + +1. **Type System Limitations**: Languages without union types (Java, C#, Go) use wrapper classes/structs +2. **Runtime vs Compile Time**: Most languages handle this at runtime, Rust leverages Serde for compile-time generation +3. **anyOf Semantics**: Only Rust and Python truly differentiate anyOf (multiple matches) from oneOf (single match) +4. **Deserialization Order**: All implementations try options in order for untagged unions, which can lead to ambiguity + +## Ambiguity Handling Strategies + +### Do Any Languages Refuse to Generate? + +**No language generator completely refuses to generate code for ambiguous schemas.** All have fallback strategies: + +### Language-Specific Ambiguity Handling + +#### **Swift** +- **Strategy**: Provides `oneOfUnknownDefaultCase` option +- **Behavior**: Can generate an `unknownDefaultOpenApi` case for unmatched values +- **Without Option**: Throws `DecodingError.typeMismatch` at runtime +- **Philosophy**: Fail at runtime rather than compile time + +#### **Python (Pydantic)** +- **Strategy**: Generates validation code with `ValidationError` +- **Behavior**: Validates all options and tracks which ones match +- **For oneOf**: Ensures exactly one matches, raises `ValidationError` if multiple match +- **Philosophy**: Strict runtime validation with clear error messages + +#### **Java** +- **Strategy**: Custom TypeAdapters try each type sequentially +- **Behavior**: First successful deserialization wins +- **Ambiguity**: No validation that only one matches for oneOf +- **Philosophy**: Pragmatic "first match wins" approach + +#### **TypeScript** +- **Strategy**: Union types with no runtime validation by default +- **Behavior**: Structural typing means any matching shape is accepted +- **Ambiguity**: Completely permissive - type system doesn't enforce exclusivity +- **Philosophy**: Trust the data or add runtime validation separately + +#### **Go** +- **Strategy**: Custom UnmarshalJSON tries to populate all fields +- **Behavior**: For oneOf, additional validation ensures only one is non-nil +- **Ambiguity**: Returns error if multiple match for oneOf +- **Philosophy**: Explicit validation after unmarshaling + +#### **Rust** +- **Strategy**: Untagged enums for oneOf, struct with options for anyOf +- **Behavior**: Serde tries variants in order (first match wins for oneOf) +- **Ambiguity**: No compile-time detection of overlapping variants +- **Philosophy**: Leverage existing serialization framework + +### Common Warnings and Limitations + +From the OpenAPI Generator codebase: + +1. **Self-referencing schemas**: Detected and removed to prevent infinite loops +2. **Inline objects in oneOf with discriminator**: Warned and ignored +3. **Conflicting composition**: Error logged when schema has incorrect anyOf/allOf/oneOf combination +4. **Missing discriminator**: Most generators work but with "first match wins" semantics + +### Best Practices for Avoiding Ambiguity + +1. **Use discriminators**: When possible, add a discriminator property for oneOf + ```yaml + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + discriminator: + propertyName: petType + ``` + +2. **Make schemas mutually exclusive**: Design schemas that don't overlap + ```yaml + oneOf: + - type: object + required: [foo] + properties: + foo: {type: string} + - type: object + required: [bar] + properties: + bar: {type: number} + ``` + +3. **Order matters**: Place more specific schemas first + ```yaml + oneOf: + - type: object + required: [a, b, c] # More specific + - type: object + required: [a] # Less specific + ``` + +### Why Don't Generators Refuse? + +1. **Pragmatism**: Real-world APIs often have imperfect schemas +2. **Backwards compatibility**: Existing APIs shouldn't break +3. **Runtime nature**: Many ambiguities only manifest with specific data +4. **User choice**: Developers can add additional validation if needed +5. **OpenAPI spec**: The spec itself doesn't forbid ambiguous schemas + +## Key Differences Summary + +| Aspect | oneOf (XOR) | anyOf (OR) | +|--------|-------------|------------| +| **Rust Type** | Enum | Struct with Optional Fields | +| **Validation** | Exactly one variant | At least one field | +| **Multiple Matches** | Not possible | Allowed | +| **Serde Attribute** | `#[serde(untagged)]` or `#[serde(tag = "...")]` | Standard struct | +| **Use Case** | Mutually exclusive choices | Multiple valid representations | + +## Migration from Previous Behavior + +Previously, the Rust generator treated `anyOf` the same as `oneOf`, generating enums for both. This was semantically incorrect. With the new implementation: + +1. **oneOf remains unchanged**: Still generates enums +2. **anyOf now generates structs**: Breaking change but semantically correct + +To migrate existing code: +- Replace enum pattern matching with struct field access +- Use the `validate_any_of()` method to ensure at least one field is set +- Access individual options via the `as_*` fields + +## Real Example: Wing328's Test Case + +I merged wing328's PR #21911 which has a perfect test case showing the difference. Let me walk you through what I found: + +### The Test Schema +Wing328 created this anyOf schema: +```yaml +ModelIdentifier: + description: Model identifier that can be a string or specific enum value + anyOf: + - type: string + description: Any model name as string + - type: string + enum: [gpt-4, gpt-3.5-turbo, dall-e-3] + description: Known model enum values +``` + +### What the Old Generator Would Produce +With the old (wrong) behavior, this would generate: +```rust +// OLD: Incorrectly treats anyOf as oneOf +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ModelIdentifier { + String(String), // First string option + String(String), // Second string option - DUPLICATE! This is broken! +} +``` + +See the problem? We'd have duplicate enum variants! The generator would actually produce invalid Rust code. Plus, even if it worked, you could only choose ONE option, not both. + +### What Our New Generator Produces +With the correct anyOf implementation: +```rust +// NEW: Correctly treats anyOf as composition +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ModelIdentifier { + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_0")] + pub as_any_of_0: Option, // Any model name + + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_1")] + pub as_any_of_1: Option, // Known enum values +} +``` + +Now both fields can be set! This is actually useful - imagine an API that accepts both a freeform model name AND validates against known models. With anyOf, you can validate against both schemas simultaneously. + +### Another Example from Wing328's Tests +He also included this more complex anyOf: +```yaml +AnotherAnyOfTest: + anyOf: + - type: string + - type: integer + - type: array + items: + type: string +``` + +Old behavior would force you to choose: "Is this a string OR an integer OR an array?" + +New behavior lets you have all three! Maybe it's a weird API, but that's what anyOf means - the data can match multiple schemas at once. The generator shouldn't make assumptions about what's "sensible" - it should implement the spec correctly. \ No newline at end of file diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java index fc2aa74e9216..78dc873a54f4 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java @@ -307,8 +307,8 @@ public CodegenModel fromModel(String name, Schema model) { mdl.getComposedSchemas().setOneOf(newOneOfs); } - // Handle anyOf schemas similarly to oneOf - // This is pragmatic since Rust's untagged enum will deserialize to the first matching variant + // Handle anyOf schemas with true OR semantics (one or more schemas must match) + // Unlike oneOf (XOR - exactly one), anyOf allows multiple schemas to validate if (mdl.getComposedSchemas() != null && mdl.getComposedSchemas().getAnyOf() != null && !mdl.getComposedSchemas().getAnyOf().isEmpty()) { @@ -366,8 +366,9 @@ public CodegenModel fromModel(String name, Schema model) { } } - // Set anyOf as oneOf for template processing since we want the same output - mdl.getComposedSchemas().setOneOf(newAnyOfs); + // Keep anyOf separate from oneOf - they have different semantics + // anyOf will be processed with a different template structure + // that allows multiple schemas to match (OR logic) } return mdl; diff --git a/modules/openapi-generator/src/main/resources/rust/model.mustache b/modules/openapi-generator/src/main/resources/rust/model.mustache index dac0172ea93e..79a80655e5e8 100644 --- a/modules/openapi-generator/src/main/resources/rust/model.mustache +++ b/modules/openapi-generator/src/main/resources/rust/model.mustache @@ -152,7 +152,46 @@ impl Default for {{classname}} { {{/-last}} {{/oneOf}} {{^oneOf}} -{{! composedSchemas exists but no oneOf - generate normal struct}} +{{#composedSchemas.anyOf}} +{{#-first}} +{{! Model with composedSchemas.anyOf - generate struct with optional fields for true OR semantics}} +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct {{classname}} { +{{/-first}} +{{/composedSchemas.anyOf}} +{{#composedSchemas.anyOf}} + {{#description}} + /// {{{.}}} (anyOf option) + {{/description}} + #[serde(skip_serializing_if = "Option::is_none", rename = "as_{{{baseName}}}")] + pub as_{{{name}}}: Option<{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{/isModel}}{{{dataType}}}{{#isModel}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}>, +{{/composedSchemas.anyOf}} +{{#composedSchemas.anyOf}} +{{#-last}} +} + +impl {{classname}} { + /// Creates a new {{classname}} with all fields set to None + pub fn new() -> Self { + Default::default() + } + + /// Validates that at least one anyOf field is set (OR semantics) + pub fn validate_any_of(&self) -> Result<(), String> { + {{#composedSchemas.anyOf}} + {{#-first}} + if {{/-first}}self.as_{{{name}}}.is_none(){{^-last}} + && {{/-last}}{{#-last}} { + return Err("At least one anyOf field must be set".to_string()); + }{{/-last}} + {{/composedSchemas.anyOf}} + Ok(()) + } +} +{{/-last}} +{{/composedSchemas.anyOf}} +{{^composedSchemas.anyOf}} +{{! composedSchemas exists but no oneOf or anyOf - generate normal struct}} {{#vendorExtensions.x-rust-has-byte-array}}#[serde_as] {{/vendorExtensions.x-rust-has-byte-array}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct {{{classname}}} { @@ -204,6 +243,7 @@ impl {{{classname}}} { } } } +{{/composedSchemas.anyOf}} {{/oneOf}} {{/composedSchemas}} {{^composedSchemas}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java index 05527e99247e..fa111b5b217c 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java @@ -287,21 +287,89 @@ public void testAnyOfSupport() throws IOException { Path modelIdentifierPath = Path.of(target.toString(), "/src/models/model_identifier.rs"); TestUtils.assertFileExists(modelIdentifierPath); - // Should generate an untagged enum - TestUtils.assertFileContains(modelIdentifierPath, "#[serde(untagged)]"); - TestUtils.assertFileContains(modelIdentifierPath, "pub enum ModelIdentifier"); + // Should generate a struct with optional fields for anyOf (true OR semantics) + TestUtils.assertFileContains(modelIdentifierPath, "pub struct ModelIdentifier"); + TestUtils.assertFileContains(modelIdentifierPath, "Option"); - // Should have String variant (for anyOf with string types) - TestUtils.assertFileContains(modelIdentifierPath, "String(String)"); + // Should have validation method for anyOf + TestUtils.assertFileContains(modelIdentifierPath, "pub fn validate_any_of(&self)"); - // Should NOT generate an empty struct - TestUtils.assertFileNotContains(modelIdentifierPath, "pub struct ModelIdentifier {"); - TestUtils.assertFileNotContains(modelIdentifierPath, "pub fn new()"); + // Should NOT generate an enum (that would be oneOf behavior) + TestUtils.assertFileNotContains(modelIdentifierPath, "pub enum ModelIdentifier"); + TestUtils.assertFileNotContains(modelIdentifierPath, "#[serde(untagged)]"); // Test AnotherAnyOfTest with mixed types Path anotherTestPath = Path.of(target.toString(), "/src/models/another_any_of_test.rs"); TestUtils.assertFileExists(anotherTestPath); - TestUtils.assertFileContains(anotherTestPath, "#[serde(untagged)]"); - TestUtils.assertFileContains(anotherTestPath, "pub enum AnotherAnyOfTest"); + TestUtils.assertFileContains(anotherTestPath, "pub struct AnotherAnyOfTest"); + TestUtils.assertFileContains(anotherTestPath, "pub fn validate_any_of(&self)"); + } + + @Test + public void testOneOfVsAnyOfSemantics() throws IOException { + Path target = Files.createTempDirectory("test-oneof-anyof"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust") + .setInputSpec("src/test/resources/3_0/rust/rust-oneof-anyof-comprehensive-test.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + // Test SimpleOneOf - should generate enum (XOR semantics) + Path simpleOneOfPath = Path.of(target.toString(), "/src/models/simple_one_of.rs"); + TestUtils.assertFileExists(simpleOneOfPath); + TestUtils.assertFileContains(simpleOneOfPath, "#[serde(untagged)]"); + TestUtils.assertFileContains(simpleOneOfPath, "pub enum SimpleOneOf"); + TestUtils.assertFileNotContains(simpleOneOfPath, "pub struct SimpleOneOf"); + + // Test SimpleAnyOf - should generate struct with optional fields (OR semantics) + Path simpleAnyOfPath = Path.of(target.toString(), "/src/models/simple_any_of.rs"); + TestUtils.assertFileExists(simpleAnyOfPath); + TestUtils.assertFileContains(simpleAnyOfPath, "pub struct SimpleAnyOf"); + TestUtils.assertFileContains(simpleAnyOfPath, "pub fn validate_any_of(&self)"); + TestUtils.assertFileNotContains(simpleAnyOfPath, "pub enum SimpleAnyOf"); + + // Test PersonOrCompany (oneOf without discriminator) - should generate untagged enum + Path personOrCompanyPath = Path.of(target.toString(), "/src/models/person_or_company.rs"); + TestUtils.assertFileExists(personOrCompanyPath); + TestUtils.assertFileContains(personOrCompanyPath, "#[serde(untagged)]"); + TestUtils.assertFileContains(personOrCompanyPath, "pub enum PersonOrCompany"); + TestUtils.assertFileContains(personOrCompanyPath, "Person(Box)"); + TestUtils.assertFileContains(personOrCompanyPath, "Company(Box)"); + + // Test PersonAndOrCompany (anyOf) - should generate struct allowing both + Path personAndOrCompanyPath = Path.of(target.toString(), "/src/models/person_and_or_company.rs"); + TestUtils.assertFileExists(personAndOrCompanyPath); + TestUtils.assertFileContains(personAndOrCompanyPath, "pub struct PersonAndOrCompany"); + TestUtils.assertFileContains(personAndOrCompanyPath, "Option>"); + TestUtils.assertFileContains(personAndOrCompanyPath, "Option>"); + TestUtils.assertFileContains(personAndOrCompanyPath, "pub fn validate_any_of(&self)"); + + // Test ComplexOneOf - should generate enum with inline schemas + Path complexOneOfPath = Path.of(target.toString(), "/src/models/complex_one_of.rs"); + TestUtils.assertFileExists(complexOneOfPath); + TestUtils.assertFileContains(complexOneOfPath, "pub enum ComplexOneOf"); + TestUtils.assertFileNotContains(complexOneOfPath, "pub struct ComplexOneOf"); + + // Test ComplexAnyOf - should generate struct with overlapping property support + Path complexAnyOfPath = Path.of(target.toString(), "/src/models/complex_any_of.rs"); + TestUtils.assertFileExists(complexAnyOfPath); + TestUtils.assertFileContains(complexAnyOfPath, "pub struct ComplexAnyOf"); + TestUtils.assertFileContains(complexAnyOfPath, "pub fn validate_any_of(&self)"); + + // Test ShapeOneOfWithDiscriminator - should generate tagged enum + Path shapePath = Path.of(target.toString(), "/src/models/shape_one_of_with_discriminator.rs"); + TestUtils.assertFileExists(shapePath); + TestUtils.assertFileContains(shapePath, "pub enum ShapeOneOfWithDiscriminator"); + // With discriminator, it should NOT be untagged + TestUtils.assertFileContains(shapePath, "#[serde(tag = \"shapeType\")]"); + + // Test MixedContent (anyOf) - can have multiple content types simultaneously + Path mixedContentPath = Path.of(target.toString(), "/src/models/mixed_content.rs"); + TestUtils.assertFileExists(mixedContentPath); + TestUtils.assertFileContains(mixedContentPath, "pub struct MixedContent"); + TestUtils.assertFileContains(mixedContentPath, "Option<"); // Should have optional fields + TestUtils.assertFileContains(mixedContentPath, "pub fn validate_any_of(&self)"); } } diff --git a/modules/openapi-generator/src/test/resources/3_0/rust/rust-oneof-anyof-comprehensive-test.yaml b/modules/openapi-generator/src/test/resources/3_0/rust/rust-oneof-anyof-comprehensive-test.yaml new file mode 100644 index 000000000000..6f8eceb0fa60 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/rust/rust-oneof-anyof-comprehensive-test.yaml @@ -0,0 +1,156 @@ +openapi: 3.0.3 +info: + title: Rust oneOf vs anyOf Comprehensive Test + description: Test to demonstrate semantic differences between oneOf and anyOf in Rust + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: OK +components: + schemas: + # Simple oneOf - exactly one must match (XOR) + SimpleOneOf: + oneOf: + - type: string + - type: number + - type: boolean + + # Simple anyOf - one or more can match (OR) + SimpleAnyOf: + anyOf: + - type: string + - type: number + - type: boolean + + # oneOf with objects - no discriminator, should pick first matching + PersonOrCompany: + oneOf: + - $ref: '#/components/schemas/Person' + - $ref: '#/components/schemas/Company' + + # anyOf with objects - can be both person and company + PersonAndOrCompany: + anyOf: + - $ref: '#/components/schemas/Person' + - $ref: '#/components/schemas/Company' + + # Complex oneOf with nested schemas + ComplexOneOf: + oneOf: + - type: object + properties: + stringValue: + type: string + metadata: + type: string + required: [stringValue] + - type: object + properties: + numberValue: + type: number + metadata: + type: string + required: [numberValue] + - type: array + items: + type: string + + # Complex anyOf with overlapping properties + ComplexAnyOf: + anyOf: + - type: object + properties: + name: + type: string + age: + type: integer + required: [name] + - type: object + properties: + name: + type: string + company: + type: string + required: [company] + - type: object + properties: + tags: + type: array + items: + type: string + + # oneOf with discriminator + ShapeOneOfWithDiscriminator: + oneOf: + - $ref: '#/components/schemas/Circle' + - $ref: '#/components/schemas/Rectangle' + discriminator: + propertyName: shapeType + mapping: + circle: '#/components/schemas/Circle' + rectangle: '#/components/schemas/Rectangle' + + # anyOf that could validate multiple schemas simultaneously + MixedContent: + anyOf: + - type: object + properties: + text: + type: string + required: [text] + - type: object + properties: + html: + type: string + required: [html] + - type: object + properties: + markdown: + type: string + required: [markdown] + + # Helper schemas + Person: + type: object + properties: + firstName: + type: string + lastName: + type: string + age: + type: integer + required: [firstName, lastName] + + Company: + type: object + properties: + companyName: + type: string + employeeCount: + type: integer + required: [companyName] + + Circle: + type: object + properties: + shapeType: + type: string + enum: [circle] + radius: + type: number + required: [shapeType, radius] + + Rectangle: + type: object + properties: + shapeType: + type: string + enum: [rectangle] + width: + type: number + height: + type: number + required: [shapeType, width, height] \ No newline at end of file diff --git a/samples/client/others/rust/hyper/anyof/.gitignore b/samples/client/others/rust/hyper/anyof/.gitignore new file mode 100644 index 000000000000..6aa106405a4b --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/.gitignore @@ -0,0 +1,3 @@ +/target/ +**/*.rs.bk +Cargo.lock diff --git a/samples/client/others/rust/hyper/anyof/.openapi-generator-ignore b/samples/client/others/rust/hyper/anyof/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/client/others/rust/hyper/anyof/.openapi-generator/FILES b/samples/client/others/rust/hyper/anyof/.openapi-generator/FILES new file mode 100644 index 000000000000..5eb7ffb44324 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/.openapi-generator/FILES @@ -0,0 +1,19 @@ +.gitignore +.travis.yml +Cargo.toml +README.md +docs/AnotherAnyOfTest.md +docs/DefaultApi.md +docs/ModelIdentifier.md +docs/TestResponse.md +git_push.sh +src/apis/client.rs +src/apis/configuration.rs +src/apis/default_api.rs +src/apis/mod.rs +src/apis/request.rs +src/lib.rs +src/models/another_any_of_test.rs +src/models/mod.rs +src/models/model_identifier.rs +src/models/test_response.rs diff --git a/samples/client/others/rust/hyper/anyof/.openapi-generator/VERSION b/samples/client/others/rust/hyper/anyof/.openapi-generator/VERSION new file mode 100644 index 000000000000..5e5282953086 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.16.0-SNAPSHOT diff --git a/samples/client/others/rust/hyper/anyof/.travis.yml b/samples/client/others/rust/hyper/anyof/.travis.yml new file mode 100644 index 000000000000..22761ba7ee19 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/.travis.yml @@ -0,0 +1 @@ +language: rust diff --git a/samples/client/others/rust/hyper/anyof/Cargo.toml b/samples/client/others/rust/hyper/anyof/Cargo.toml new file mode 100644 index 000000000000..51de8b09747f --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "anyof-hyper" +version = "1.0.0" +authors = ["OpenAPI Generator team and contributors"] +description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)" +# Override this license by providing a License Object in the OpenAPI. +license = "Unlicense" +edition = "2021" + +[dependencies] +serde = { version = "^1.0", features = ["derive"] } +serde_json = "^1.0" +serde_repr = "^0.1" +url = "^2.5" +hyper = { version = "^1.3.1", features = ["full"] } +hyper-util = { version = "0.1.5", features = ["client", "client-legacy", "http1", "http2"] } +http-body-util = { version = "0.1.2" } +http = "~0.2" +base64 = "~0.7.0" +futures = "^0.3" diff --git a/samples/client/others/rust/hyper/anyof/README.md b/samples/client/others/rust/hyper/anyof/README.md new file mode 100644 index 000000000000..24331c3263ad --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/README.md @@ -0,0 +1,48 @@ +# Rust API client for anyof-hyper + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + +## Overview + +This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. + +- API version: 1.0.0 +- Package version: 1.0.0 +- Generator version: 7.16.0-SNAPSHOT +- Build package: `org.openapitools.codegen.languages.RustClientCodegen` + +## Installation + +Put the package under your project folder in a directory named `anyof-hyper` and add the following to `Cargo.toml` under `[dependencies]`: + +``` +anyof-hyper = { path = "./anyof-hyper" } +``` + +## Documentation for API Endpoints + +All URIs are relative to *http://localhost* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*DefaultApi* | [**model_get**](docs/DefaultApi.md#model_get) | **Get** /model | + + +## Documentation For Models + + - [AnotherAnyOfTest](docs/AnotherAnyOfTest.md) + - [ModelIdentifier](docs/ModelIdentifier.md) + - [TestResponse](docs/TestResponse.md) + + +To get access to the crate's generated documentation, use: + +``` +cargo doc --open +``` + +## Author + + + diff --git a/samples/client/others/rust/hyper/anyof/docs/AnotherAnyOfTest.md b/samples/client/others/rust/hyper/anyof/docs/AnotherAnyOfTest.md new file mode 100644 index 000000000000..9ff99dde6d80 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/docs/AnotherAnyOfTest.md @@ -0,0 +1,10 @@ +# AnotherAnyOfTest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/samples/client/others/rust/hyper/anyof/docs/DefaultApi.md b/samples/client/others/rust/hyper/anyof/docs/DefaultApi.md new file mode 100644 index 000000000000..72f06e2b48f2 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/docs/DefaultApi.md @@ -0,0 +1,34 @@ +# \DefaultApi + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**model_get**](DefaultApi.md#model_get) | **Get** /model | + + + +## model_get + +> models::TestResponse model_get() + + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +[**models::TestResponse**](TestResponse.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/samples/client/others/rust/hyper/anyof/docs/ModelIdentifier.md b/samples/client/others/rust/hyper/anyof/docs/ModelIdentifier.md new file mode 100644 index 000000000000..5c5cb57b311c --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/docs/ModelIdentifier.md @@ -0,0 +1,10 @@ +# ModelIdentifier + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/samples/client/others/rust/hyper/anyof/docs/TestResponse.md b/samples/client/others/rust/hyper/anyof/docs/TestResponse.md new file mode 100644 index 000000000000..de74be5e72d8 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/docs/TestResponse.md @@ -0,0 +1,12 @@ +# TestResponse + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**model** | Option<[**models::ModelIdentifier**](ModelIdentifier.md)> | | [optional] +**status** | Option<**String**> | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/samples/client/others/rust/hyper/anyof/git_push.sh b/samples/client/others/rust/hyper/anyof/git_push.sh new file mode 100644 index 000000000000..f53a75d4fabe --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/samples/client/others/rust/hyper/anyof/src/apis/client.rs b/samples/client/others/rust/hyper/anyof/src/apis/client.rs new file mode 100644 index 000000000000..5554552ee23e --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/apis/client.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use hyper; +use hyper_util::client::legacy::connect::Connect; +use super::configuration::Configuration; + +pub struct APIClient { + default_api: Box, +} + +impl APIClient { + pub fn new(configuration: Configuration) -> APIClient + where C: Clone + std::marker::Send + Sync + 'static { + let rc = Arc::new(configuration); + + APIClient { + default_api: Box::new(crate::apis::DefaultApiClient::new(rc.clone())), + } + } + + pub fn default_api(&self) -> &dyn crate::apis::DefaultApi{ + self.default_api.as_ref() + } + +} diff --git a/samples/client/others/rust/hyper/anyof/src/apis/configuration.rs b/samples/client/others/rust/hyper/anyof/src/apis/configuration.rs new file mode 100644 index 000000000000..ecb121335ce0 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/apis/configuration.rs @@ -0,0 +1,92 @@ +/* + * Rust anyOf Test + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * Generated by: https://openapi-generator.tech + */ + +use hyper; +use hyper_util::client::legacy::Client; +use hyper_util::client::legacy::connect::Connect; +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::rt::TokioExecutor; + +pub struct Configuration + where C: Clone + std::marker::Send + Sync + 'static { + pub base_path: String, + pub user_agent: Option, + pub client: Client, + pub basic_auth: Option, + pub oauth_access_token: Option, + pub api_key: Option, + // TODO: take an oauth2 token source, similar to the go one +} + +pub type BasicAuth = (String, Option); + +pub struct ApiKey { + pub prefix: Option, + pub key: String, +} + +impl Configuration { + /// Construct a default [`Configuration`](Self) with a hyper client using a default + /// [`HttpConnector`](hyper_util::client::legacy::connect::HttpConnector). + /// + /// Use [`with_client`](Configuration::with_client) to construct a Configuration with a + /// custom hyper client. + /// + /// # Example + /// + /// ``` + /// # use anyof_hyper::apis::configuration::Configuration; + /// let api_config = Configuration { + /// basic_auth: Some(("user".into(), None)), + /// ..Configuration::new() + /// }; + /// ``` + pub fn new() -> Configuration { + Configuration::default() + } +} + +impl Configuration + where C: Clone + std::marker::Send + Sync { + + /// Construct a new Configuration with a custom hyper client. + /// + /// # Example + /// + /// ``` + /// # use core::time::Duration; + /// # use anyof_hyper::apis::configuration::Configuration; + /// use hyper_util::client::legacy::Client; + /// use hyper_util::rt::TokioExecutor; + /// + /// let client = Client::builder(TokioExecutor::new()) + /// .pool_idle_timeout(Duration::from_secs(30)) + /// .build_http(); + /// + /// let api_config = Configuration::with_client(client); + /// ``` + pub fn with_client(client: Client) -> Configuration { + Configuration { + base_path: "http://localhost".to_owned(), + user_agent: Some("OpenAPI-Generator/1.0.0/rust".to_owned()), + client, + basic_auth: None, + oauth_access_token: None, + api_key: None, + } + } +} + +impl Default for Configuration { + fn default() -> Self { + let client = Client::builder(TokioExecutor::new()).build_http(); + Configuration::with_client(client) + } +} diff --git a/samples/client/others/rust/hyper/anyof/src/apis/default_api.rs b/samples/client/others/rust/hyper/anyof/src/apis/default_api.rs new file mode 100644 index 000000000000..4bca8f3629b2 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/apis/default_api.rs @@ -0,0 +1,53 @@ +/* + * Rust anyOf Test + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * Generated by: https://openapi-generator.tech + */ + +use std::sync::Arc; +use std::borrow::Borrow; +use std::pin::Pin; +#[allow(unused_imports)] +use std::option::Option; + +use hyper; +use hyper_util::client::legacy::connect::Connect; +use futures::Future; + +use crate::models; +use super::{Error, configuration}; +use super::request as __internal_request; + +pub struct DefaultApiClient + where C: Clone + std::marker::Send + Sync + 'static { + configuration: Arc>, +} + +impl DefaultApiClient + where C: Clone + std::marker::Send + Sync { + pub fn new(configuration: Arc>) -> DefaultApiClient { + DefaultApiClient { + configuration, + } + } +} + +pub trait DefaultApi: Send + Sync { + fn model_get(&self, ) -> Pin> + Send>>; +} + +implDefaultApi for DefaultApiClient + where C: Clone + std::marker::Send + Sync { + #[allow(unused_mut)] + fn model_get(&self, ) -> Pin> + Send>> { + let mut req = __internal_request::Request::new(hyper::Method::GET, "/model".to_string()) + ; + + req.execute(self.configuration.borrow()) + } + +} diff --git a/samples/client/others/rust/hyper/anyof/src/apis/mod.rs b/samples/client/others/rust/hyper/anyof/src/apis/mod.rs new file mode 100644 index 000000000000..58227e44cb35 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/apis/mod.rs @@ -0,0 +1,73 @@ +use std::fmt; +use std::fmt::Debug; + +use hyper; +use hyper::http; +use hyper_util::client::legacy::connect::Connect; +use serde_json; + +#[derive(Debug)] +pub enum Error { + Api(ApiError), + Header(http::header::InvalidHeaderValue), + Http(http::Error), + Hyper(hyper::Error), + HyperClient(hyper_util::client::legacy::Error), + Serde(serde_json::Error), + UriError(http::uri::InvalidUri), +} + +pub struct ApiError { + pub code: hyper::StatusCode, + pub body: hyper::body::Incoming, +} + +impl Debug for ApiError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ApiError") + .field("code", &self.code) + .field("body", &"hyper::body::Incoming") + .finish() + } +} + +impl From<(hyper::StatusCode, hyper::body::Incoming)> for Error { + fn from(e: (hyper::StatusCode, hyper::body::Incoming)) -> Self { + Error::Api(ApiError { + code: e.0, + body: e.1, + }) + } +} + +impl From for Error { + fn from(e: http::Error) -> Self { + Error::Http(e) + } +} + +impl From for Error { + fn from(e: hyper_util::client::legacy::Error) -> Self { + Error::HyperClient(e) + } +} + +impl From for Error { + fn from(e: hyper::Error) -> Self { + Error::Hyper(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Serde(e) + } +} + +mod request; + +mod default_api; +pub use self::default_api::{ DefaultApi, DefaultApiClient }; + +pub mod configuration; +pub mod client; diff --git a/samples/client/others/rust/hyper/anyof/src/apis/request.rs b/samples/client/others/rust/hyper/anyof/src/apis/request.rs new file mode 100644 index 000000000000..a6f7b74cc6ef --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/apis/request.rs @@ -0,0 +1,247 @@ +use std::collections::HashMap; +use std::pin::Pin; + +use futures; +use futures::Future; +use futures::future::*; +use http_body_util::BodyExt; +use hyper; +use hyper_util::client::legacy::connect::Connect; +use hyper::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue, USER_AGENT}; +use serde; +use serde_json; + +use super::{configuration, Error}; + +pub(crate) struct ApiKey { + pub in_header: bool, + pub in_query: bool, + pub param_name: String, +} + +impl ApiKey { + fn key(&self, prefix: &Option, key: &str) -> String { + match prefix { + None => key.to_owned(), + Some(ref prefix) => format!("{} {}", prefix, key), + } + } +} + +#[allow(dead_code)] +pub(crate) enum Auth { + None, + ApiKey(ApiKey), + Basic, + Oauth, +} + +/// If the authorization type is unspecified then it will be automatically detected based +/// on the configuration. This functionality is useful when the OpenAPI definition does not +/// include an authorization scheme. +pub(crate) struct Request { + auth: Option, + method: hyper::Method, + path: String, + query_params: HashMap, + no_return_type: bool, + path_params: HashMap, + form_params: HashMap, + header_params: HashMap, + // TODO: multiple body params are possible technically, but not supported here. + serialized_body: Option, +} + +#[allow(dead_code)] +impl Request { + pub fn new(method: hyper::Method, path: String) -> Self { + Request { + auth: None, + method, + path, + query_params: HashMap::new(), + path_params: HashMap::new(), + form_params: HashMap::new(), + header_params: HashMap::new(), + serialized_body: None, + no_return_type: false, + } + } + + pub fn with_body_param(mut self, param: T) -> Self { + self.serialized_body = Some(serde_json::to_string(¶m).unwrap()); + self + } + + pub fn with_header_param(mut self, basename: String, param: String) -> Self { + self.header_params.insert(basename, param); + self + } + + #[allow(unused)] + pub fn with_query_param(mut self, basename: String, param: String) -> Self { + self.query_params.insert(basename, param); + self + } + + #[allow(unused)] + pub fn with_path_param(mut self, basename: String, param: String) -> Self { + self.path_params.insert(basename, param); + self + } + + #[allow(unused)] + pub fn with_form_param(mut self, basename: String, param: String) -> Self { + self.form_params.insert(basename, param); + self + } + + pub fn returns_nothing(mut self) -> Self { + self.no_return_type = true; + self + } + + pub fn with_auth(mut self, auth: Auth) -> Self { + self.auth = Some(auth); + self + } + + pub fn execute<'a, C, U>( + self, + conf: &configuration::Configuration, + ) -> Pin> + 'a + Send>> + where + C: Connect + Clone + std::marker::Send + Sync, + U: Sized + std::marker::Send + 'a, + for<'de> U: serde::Deserialize<'de>, + { + let mut query_string = ::url::form_urlencoded::Serializer::new("".to_owned()); + + let mut path = self.path; + for (k, v) in self.path_params { + // replace {id} with the value of the id path param + path = path.replace(&format!("{{{}}}", k), &v); + } + + for (key, val) in self.query_params { + query_string.append_pair(&key, &val); + } + + let mut uri_str = format!("{}{}", conf.base_path, path); + + let query_string_str = query_string.finish(); + if !query_string_str.is_empty() { + uri_str += "?"; + uri_str += &query_string_str; + } + let uri: hyper::Uri = match uri_str.parse() { + Err(e) => return Box::pin(futures::future::err(Error::UriError(e))), + Ok(u) => u, + }; + + let mut req_builder = hyper::Request::builder() + .uri(uri) + .method(self.method); + + // Detect the authorization type if it hasn't been set. + let auth = self.auth.unwrap_or_else(|| + if conf.api_key.is_some() { + panic!("Cannot automatically set the API key from the configuration, it must be specified in the OpenAPI definition") + } else if conf.oauth_access_token.is_some() { + Auth::Oauth + } else if conf.basic_auth.is_some() { + Auth::Basic + } else { + Auth::None + } + ); + match auth { + Auth::ApiKey(apikey) => { + if let Some(ref key) = conf.api_key { + let val = apikey.key(&key.prefix, &key.key); + if apikey.in_query { + query_string.append_pair(&apikey.param_name, &val); + } + if apikey.in_header { + req_builder = req_builder.header(&apikey.param_name, val); + } + } + } + Auth::Basic => { + if let Some(ref auth_conf) = conf.basic_auth { + let mut text = auth_conf.0.clone(); + text.push(':'); + if let Some(ref pass) = auth_conf.1 { + text.push_str(&pass[..]); + } + let encoded = base64::encode(&text); + req_builder = req_builder.header(AUTHORIZATION, encoded); + } + } + Auth::Oauth => { + if let Some(ref token) = conf.oauth_access_token { + let text = "Bearer ".to_owned() + token; + req_builder = req_builder.header(AUTHORIZATION, text); + } + } + Auth::None => {} + } + + if let Some(ref user_agent) = conf.user_agent { + req_builder = req_builder.header(USER_AGENT, match HeaderValue::from_str(user_agent) { + Ok(header_value) => header_value, + Err(e) => return Box::pin(futures::future::err(super::Error::Header(e))) + }); + } + + for (k, v) in self.header_params { + req_builder = req_builder.header(&k, v); + } + + let req_headers = req_builder.headers_mut().unwrap(); + let request_result = if self.form_params.len() > 0 { + req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded")); + let mut enc = ::url::form_urlencoded::Serializer::new("".to_owned()); + for (k, v) in self.form_params { + enc.append_pair(&k, &v); + } + req_builder.body(enc.finish()) + } else if let Some(body) = self.serialized_body { + req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + req_headers.insert(CONTENT_LENGTH, body.len().into()); + req_builder.body(body) + } else { + req_builder.body(String::new()) + }; + let request = match request_result { + Ok(request) => request, + Err(e) => return Box::pin(futures::future::err(Error::from(e))) + }; + + let no_return_type = self.no_return_type; + Box::pin(conf.client + .request(request) + .map_err(|e| Error::from(e)) + .and_then(move |response| { + let status = response.status(); + if !status.is_success() { + futures::future::err::(Error::from((status, response.into_body()))).boxed() + } else if no_return_type { + // This is a hack; if there's no_ret_type, U is (), but serde_json gives an + // error when deserializing "" into (), so deserialize 'null' into it + // instead. + // An alternate option would be to require U: Default, and then return + // U::default() here instead since () implements that, but then we'd + // need to impl default for all models. + futures::future::ok::(serde_json::from_str("null").expect("serde null value")).boxed() + } else { + let collect = response.into_body().collect().map_err(Error::from); + collect.map(|collected| { + collected.and_then(|collected| { + serde_json::from_slice(&collected.to_bytes()).map_err(Error::from) + }) + }).boxed() + } + })) + } +} diff --git a/samples/client/others/rust/hyper/anyof/src/lib.rs b/samples/client/others/rust/hyper/anyof/src/lib.rs new file mode 100644 index 000000000000..f5cfd2315405 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/lib.rs @@ -0,0 +1,12 @@ +#![allow(unused_imports)] +#![allow(clippy::too_many_arguments)] + +extern crate serde_repr; +extern crate serde; +extern crate serde_json; +extern crate url; +extern crate hyper; +extern crate futures; + +pub mod apis; +pub mod models; diff --git a/samples/client/others/rust/hyper/anyof/src/models/another_any_of_test.rs b/samples/client/others/rust/hyper/anyof/src/models/another_any_of_test.rs new file mode 100644 index 000000000000..992d6c2deed6 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/models/another_any_of_test.rs @@ -0,0 +1,36 @@ +/* + * Rust anyOf Test + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// AnotherAnyOfTest : Another test case with different types +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AnotherAnyOfTest { + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_0")] + pub as_any_of_0: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_1")] + pub as_any_of_1: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_2")] + pub as_any_of_2: Option>, +} + +impl AnotherAnyOfTest { + /// Creates a new AnotherAnyOfTest with all fields set to None + pub fn new() -> Self { + Default::default() + } + + /// Validates that at least one anyOf field is set (OR semantics) + pub fn validate_any_of(&self) -> Result<(), String> { + Ok(()) + } +} + diff --git a/samples/client/others/rust/hyper/anyof/src/models/mod.rs b/samples/client/others/rust/hyper/anyof/src/models/mod.rs new file mode 100644 index 000000000000..edc3aa046ce1 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/models/mod.rs @@ -0,0 +1,6 @@ +pub mod another_any_of_test; +pub use self::another_any_of_test::AnotherAnyOfTest; +pub mod model_identifier; +pub use self::model_identifier::ModelIdentifier; +pub mod test_response; +pub use self::test_response::TestResponse; diff --git a/samples/client/others/rust/hyper/anyof/src/models/model_identifier.rs b/samples/client/others/rust/hyper/anyof/src/models/model_identifier.rs new file mode 100644 index 000000000000..e7709b106f5e --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/models/model_identifier.rs @@ -0,0 +1,36 @@ +/* + * Rust anyOf Test + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// ModelIdentifier : Model identifier that can be a string or specific enum value +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ModelIdentifier { + /// Any model name as string (anyOf option) + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_0")] + pub as_any_of_0: Option, + /// Known model enum values (anyOf option) + #[serde(skip_serializing_if = "Option::is_none", rename = "as_any_of_1")] + pub as_any_of_1: Option, +} + +impl ModelIdentifier { + /// Creates a new ModelIdentifier with all fields set to None + pub fn new() -> Self { + Default::default() + } + + /// Validates that at least one anyOf field is set (OR semantics) + pub fn validate_any_of(&self) -> Result<(), String> { + Ok(()) + } +} + diff --git a/samples/client/others/rust/hyper/anyof/src/models/test_response.rs b/samples/client/others/rust/hyper/anyof/src/models/test_response.rs new file mode 100644 index 000000000000..d6cb6e5e1fb4 --- /dev/null +++ b/samples/client/others/rust/hyper/anyof/src/models/test_response.rs @@ -0,0 +1,30 @@ +/* + * Rust anyOf Test + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TestResponse { + #[serde(rename = "model", skip_serializing_if = "Option::is_none")] + pub model: Option>, + #[serde(rename = "status", skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +impl TestResponse { + pub fn new() -> TestResponse { + TestResponse { + model: None, + status: None, + } + } +} +