apple / swift-openapi-generator

Generate Swift client and server code from an OpenAPI document.
https://swiftpackageindex.com/apple/swift-openapi-generator/documentation
Apache License 2.0
1.3k stars 93 forks source link

Revisit server1/server2 and case1/case2 and value1/value2 naming in Servers, oneOf, allOf/anyOf #28

Open czechboy0 opened 1 year ago

czechboy0 commented 1 year ago

Revisit server1/server2 and case1/case2 and value1/value2 naming in Servers, oneOf, allOf/anyOf.

We don't like the names, we should derive better unique names, e.g. based on:

We just have to keep in mind that in many cases (e.g. inline schemas), we don't have a unique name to use, and might need to derive one - there we need to watch out for potential naming conflicts.

czechboy0 commented 11 months ago

@simonjbeaumont to gather some data.

simonjbeaumont commented 10 months ago

Precedence for geneating types using schema composition

Background

Inputs

I decided to use the following snippet of OpenAPI YAML for this research:

components:
  schemas:
    Badger:
      type: object
      properties:
        b:
          type: boolean
    Mushroom:
      type: object
      properties:
        m:
          type: integer
    Snake:
      type: object
      properties:
        s:
          type: string
    MyAnyOf:
      anyOf: &schemas
      - $ref: '#/components/schemas/Badger'
      - $ref: '#/components/schemas/Mushroom'
      - $ref: '#/components/schemas/Snake'
      - type: string
      - type: string
        pattern: '\d'
      - type: string
        enum:
          - foo
      - type: integer
      - type: object
      - type: object
        properties:
          foo:
            type: string
      - type: array
        items:
          type: integer
    MyAllOf:
      allOf: *schemas
    MyOneOf:
      oneOf: *schemas

Generators

I'll use the following set of generators initially:

Outputs

I've tried to capture just enough of the output for each generator to draw themes about the question at hand.

openapi-generator -g swift

public struct MyAnyOf: Codable, JSONEncodable, Hashable {

    public var b: Bool?
    public var m: Int?
    public var s: String?
    public var foo: String?
}

public struct MyAllOf: Codable, JSONEncodable, Hashable {

    public var b: Bool?
    public var m: Int?
    public var s: String?
    public var foo: String?
}

public enum MyOneOf: Codable, JSONEncodable, Hashable {
    case typeAnyCodable(AnyCodable)
    case typeBadger(Badger)
    case typeInt(Int)
    case typeMushroom(Mushroom)
    case typeMyAnyOfAnyOf(MyAnyOfAnyOf)
    case typeSnake(Snake)
    case typeString(String)
    case type[Int]([Int])
}

openapi-generator -g go

type MyAnyOf struct {
    Badger *Badger
    Mushroom *Mushroom
    MyAnyOfAnyOf *MyAnyOfAnyOf
    Snake *Snake
    []int32 *[]int32
    int32 *int32
    map[string]interface{} *map[string]interface{}
    string *string
}

type MyAllOf struct {
    B *bool `json:"b,omitempty"`
    M *int32 `json:"m,omitempty"`
    S *string `json:"s,omitempty"`
    Foo *string `json:"foo,omitempty"`
}

type MyOneOf struct {
    Badger *Badger
    Mushroom *Mushroom
    MyAnyOfAnyOf *MyAnyOfAnyOf
    Snake *Snake
    ArrayOfInt32 *[]int32
    Int32 *int32
    MapmapOfStringinterface{} *map[string]interface{}
    String *string
}

openapi-generator -g rust

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MyAnyOf {
    #[serde(rename = "b", skip_serializing_if = "Option::is_none")]
    pub b: Option<bool>,
    #[serde(rename = "m", skip_serializing_if = "Option::is_none")]
    pub m: Option<i32>,
    #[serde(rename = "s", skip_serializing_if = "Option::is_none")]
    pub s: Option<String>,
    #[serde(rename = "foo", skip_serializing_if = "Option::is_none")]
    pub foo: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MyAllOf {
    #[serde(rename = "b", skip_serializing_if = "Option::is_none")]
    pub b: Option<bool>,
    #[serde(rename = "m", skip_serializing_if = "Option::is_none")]
    pub m: Option<i32>,
    #[serde(rename = "s", skip_serializing_if = "Option::is_none")]
    pub s: Option<String>,
    #[serde(rename = "foo", skip_serializing_if = "Option::is_none")]
    pub foo: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MyOneOf {
    #[serde(rename = "b", skip_serializing_if = "Option::is_none")]
    pub b: Option<bool>,
    #[serde(rename = "m", skip_serializing_if = "Option::is_none")]
    pub m: Option<i32>,
    #[serde(rename = "s", skip_serializing_if = "Option::is_none")]
    pub s: Option<String>,
    #[serde(rename = "foo", skip_serializing_if = "Option::is_none")]
    pub foo: Option<String>,
}

openapi-typescript

export interface components {
  schemas: {
    Badger: {
      b?: boolean;
    };
    Mushroom: {
      m?: number;
    };
    Snake: {
      s?: string;
    };
    MyAnyOf: components["schemas"]["Badger"] | components["schemas"]["Mushroom"] | components["schemas"]["Snake"] | string | "foo" | number | Record<string, never> | {
      foo?: string;
    } | number[];
    MyAllOf: components["schemas"]["Badger"] & components["schemas"]["Mushroom"] & components["schemas"]["Snake"] & string & string & "foo" & number & Record<string, never> & {
      foo?: string;
    } & number[];
    MyOneOf: components["schemas"]["Badger"] | components["schemas"]["Mushroom"] | components["schemas"]["Snake"] | string | "foo" | number | Record<string, never> | {
      foo?: string;
    } | number[];
  };
  responses: never;
  parameters: never;
  requestBodies: never;
  headers: never;
  pathItems: never;
}

openapi-typescript-codegen

export type MyAnyOf = (Badger | Mushroom | Snake | string | 'foo' | number | Record<string, any> | {
    foo?: string;
} | Array<number>);
export type MyAllOf = (Badger & Mushroom & Snake & string & 'foo' & number & Record<string, any> & {
    foo?: string;
} & Array<number>);
export type MyOneOf = (Badger | Mushroom | Snake | string | 'foo' | number | Record<string, any> | {
    foo?: string;
} | Array<number>);

Learnings

The above outputs show different strategies for handling schema composition, including flattened types, union types, variant types, etc.

However, none of them seem to need to make use of an index-based naming, e.g. case1, case2, etc.

czechboy0 commented 10 months ago

Yeah if constrained to just references to objects as subschemas, those can be handled pretty well, because they have a global name.

But all of these aggregate types (except for oneOfs with a discriminator) can have any schema as a subschemas, and those types can be inlined (in which case you don't have a global name) and don't even have to be unique (you can have two string schemas that only differ by their regex constraint).

Curious to see the output for the following additional subschemas:

- type: string
- type: string
  pattern: '\d'
- type: string
  enum:
    - foo
- type: integer
- type: object
- type: object
  properties:
    foo:
      type: string
- type: array
- type: array
  items:
    type: integer

I also suspect that variadic generics in Swift could help us here, but all this was designed before they were available.

simonjbeaumont commented 10 months ago

Those set of subschemas won't work for all of the schema composition methods, because they cannot be used in conjunction in some cases in an OAS-compliant way. However, I threw them into the generators and updated the comment with the findings.

Still no index-based type names.

czechboy0 commented 10 months ago

Sure, in the case of allOf and anyOf, there are some mutually incompatible schemas, but the oneOf should be able to support all 10 schemas. From the above, it seems none of the generators were able to generate more than 8, and some stopped at 7.

And getting all the way to 10 is where they'd have to choose either an order-based name, or create a unique name from the contents, such as string_with_pattern_backslash_d_minItems_10_... etc. The order-based name is a lot simpler to implement, but the content-based name would also work.

Would love to see a generator that can actually handle all 10 cases, and which method of naming they chose.

simonjbeaumont commented 10 months ago

but the oneOf should be able to support all 10 schemas.

I don't think that's true. For example you suggested these:

- type: string
- type: string
  pattern: '\d'
- type: string
  enum:
    - foo

which cannot work because it needs to satisfy (exactly one)^1. But the first is always satisfied, which is why I think some of the generators skipped it (there was some diagnostic output to this effect too).

czechboy0 commented 10 months ago

Good catch, let me amend to:

- type: string
  pattern: '\s'
- type: string
  pattern: '\d'