Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift4: make generated models structs instead of classes #7345

Merged
merged 6 commits into from
Jan 25, 2018

Conversation

ehyche
Copy link
Contributor

@ehyche ehyche commented Jan 8, 2018

PR checklist

  • Read the contribution guidelines.
  • Ran the shell script under ./bin/ to update Petstore sample so that CIs can verify the change. (For instance, only need to run ./bin/{LANG}-petstore.sh and ./bin/security/{LANG}-petstore.sh if updating the {LANG} (e.g. php, ruby, python, etc) code generator or {LANG} client's mustache templates). Windows batch files can be found in .\bin\windows\.
  • Filed the PR against the correct branch: 3.0.0 branch for changes related to OpenAPI spec 3.0. Default: master.
  • Copied the technical committee to review the pull request if your PR is targeting a particular programming language.

Description of the PR

This addresses this issue: #6941

Previously, our generated model objects were classes. For example, if we had the following definition:

    "ErrorInfo": {
      "type": "object",
      "properties": {
        "code": {
          "type": "integer",
          "format": "int32"
        },
        "message": {
          "type": "string"
        },
        "details": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      "description": "Example Error object"
    },

then our generated model would be a class like this:

open class ErrorInfo: Codable {

    public var code: Int?
    public var message: String?
    public var details: [String]?


    
    public init(code: Int?, message: String?, details: [String]?) {
        self.code = code
        self.message = message
        self.details = details
    }
    

    // Encodable protocol methods
    public func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: String.self)

        try container.encodeIfPresent(code, forKey: "code")
        try container.encodeIfPresent(message, forKey: "message")
        try container.encodeIfPresent(details, forKey: "details")
    }

    // Decodable protocol methods
    public required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: String.self)

        code = try container.decodeIfPresent(Int.self, forKey: "code")
        message = try container.decodeIfPresent(String.self, forKey: "message")
        details = try container.decodeIfPresent([String].self, forKey: "details")
    }
}

There are a couple of things we would like to improve here:

  1. We would prefer to have struct's instead of class's, since struct's have value-type semantics in Swift rather than reference-type semantics.
  2. In the case of this definition above, the default implementation of the Codable protocol that the Swift standard library provides is essentially identical to our implementation, so we would therefore just like to use that default implementation.

In other words, we would like our generated model to just look like:

public struct ErrorInfo: Codable {
    public var code: Int?
    public var message: String?
    public var details: [String]?
}

For simple cases like this, just claiming conformance to the Codable protocol in Swift is all that is needed. Just by doing this we get complete serialization and de-serialization support.

However, there are a few cases where we have to provide an additional implementation beyond just claiming Codable conformance as we do above. These are listed in the sections below.

When the struct property is not the same as the JSON property

If we had the following definition:

    "VariableNameTest": {
      "description": "This object contains property names which we know will be different from their variable name. Examples of this include snake case property names and property names which are Swift 4 reserved words.",
      "type": "object",
      "properties": {
        "example_name": {
          "description": "This snake-case examle_name property name should be converted to a camelCase variable name like exampleName",
          "type": "string"
        },
        "for": {
          "description": "This property name is a reserved word in most languages, including Swift 4.",
          "type": "string"
        },
        "normalName": {
          "description": "This model object property name should be unchanged from the JSON property name.",
          "type": "string"
        }
      }
    },

Then this has a JSON property called "for". However, when we create the property on the struct, then we cannot call the struct property "for" since that is a Swift reserved word. So we escape the JSON property "for" into a struct property called "_for".

We have to tell the Codable protocol when something like this happens - when the name of the struct property is different than the name of the JSON property. We do this by providing a string enum that conforms to the CodingKey protocol.

So therefore our VariableNameTest definition generates this struct:

public struct VariableNameTest: Codable {
    public var exampleName: String?
    public var _for: String?
    public var normalName: String?

    public enum CodingKeys: String, CodingKey { 
        case exampleName = "example_name"
        case _for = "for"
        case normalName
    }
}

In this PR we also make a change to the codegen module to:

  • Detect when the struct property name is not the same as the JSON property name and mark that property with a "x-codegen-escaped-property-name" vendor extension.
  • Detect when we have a model which has at least one property which has the above vendor extension. Models like this get marked with a "x-codegen-has-escaped-property-names" vendor extension.

When we have additionalProperties

Suppose we have a definition with both properties and additionalProperties:

    "ModelWithPropertiesAndAdditionalProperties": {
      "type": "object",
      "properties": {
        "myInteger": {
          "type": "integer"
        }
      },
      "additionalProperties": {
        "type": "string"
      }
    },

The generated struct would look like this:

public struct ModelWithPropertiesAndAdditionalProperties: Codable {
    public var myInteger: Int?

    public var additionalProperties: [String:String] = [:]

}

However, the default implementation of Codable that the Swift Standard Library provides will not work in this case, because it will attempt to serialize/de-serialize a dictionary called "additionalProperties", which is not how additionalProperties is supposed to be serialized.

So therefore, in the case where we have additionalProperties, then we must provide our own implementation of the Codable protocol.

When we have polymorphism

When we have polymorphism in our definitions, like this:

    "BaseCard": {
      "type": "object",
      "discriminator": "cardType",
      "required": [
        "cardType"
      ],
      "properties": {
        "cardType": {
          "type": "string"
        }
      }
    },
    "PersonCard": {
      "allOf": [
        {
          "$ref": "#/definitions/BaseCard"
        },
        {
          "type": "object",
          "properties": {
            "firstName": {
              "type": "string"
            },
            "lastName": {
              "type": "string"
            }
          }
        }
      ]
    },
    "PlaceCard": {
      "allOf": [
        {
          "$ref": "#/definitions/BaseCard"
        },
        {
          "type": "object",
          "properties": {
            "placeName": {
              "type": "string"
            },
            "placeAddress": {
              "type": "string"
            }
          }
        }
      ]
    }

Then previously we generated classes and used inheritance:

open class BaseCard: Codable {
    public var cardType: String

   ...
}

open class PersonCard: BaseCard {
    public var firstName: String?
    public var lastName: String?

   ...
}

open class PlaceCard: BaseCard {
    public var placeName: String?
    public var placeAddress: String?

   ...
}

But in Swift, as with most languages, structs cannot use inheritance. So therefore, since we are using structs, then we will simply duplicate the properties in the base definition in the derived definitions:

public struct BaseCard: Codable {
    public var cardType: String
}

public struct PersonCard: Codable {
    public var cardType: String
    public var firstName: String?
    public var lastName: String?
}

public struct PlaceCard: Codable {
    public var cardType: String
    public var placeName: String?
    public var placeAddress: String?
}

This means that we had to change our templates to use "allVars" (which include parent properties) instead of "vars" (which only includes the properties defined on the definition itself).

Eric Hyche added 6 commits January 2, 2018 12:20
This fixes issue swagger-api#6941.

In this change, we make our Swift4 generated model objects struct instead of class. However, in order to do this, we needed to handle the following edge cases:

* Inheritance and polymorphism (allOf)
  * With classes, we use inheritance. So therefore, the parent properties are ONLY on the parent generated class, and the model object which derives from the parent class picks up those properties through inheritance.
  * However, structs do not support inheritance. So we simply duplicate the parent allOf properties in the child struct.
* We have to handle the case where the property name on the struct may be different than the property name in the JSON. By default, the Codable protocol assumes that the JSON property name is the same as the struct property name. If they need to be different, then we generate a CodingKeys string enum, which contains the mapping between struct property name and JSON property name.
* additionalProperties. We cannot use the default Codable implementation for the additionalProperties, since it will look for an actual dictionary called "additionalProperties" in the JSON. Therefore, for model objects which have additionalProperties, we must generate our own implementation for the Decodable and Encodable protocols.

I have run ./bin/swift4-all.sh and ./bin/swift4-test.sh to re-generate all of the sources, and I have verified that the generated code in samples/clients/test/swift4/default builds and the unit tests pass.
@ehyche
Copy link
Contributor Author

ehyche commented Jan 8, 2018

@jgavris @Edubits @jaz-ah : please review

Copy link
Contributor

@jaz-ah jaz-ah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool change! thx @ehyche

@wing328 wing328 closed this Jan 22, 2018
@wing328 wing328 added this to the v2.4.0 milestone Jan 22, 2018
@wing328 wing328 changed the base branch from 2.4.0 to master January 22, 2018 07:25
@wing328 wing328 reopened this Jan 22, 2018
@victorNavas
Copy link

victorNavas commented Jan 25, 2018

Hello @ehyche !

Really nice work, I have started a few weeks ago to look into the swift4 generator.

Was wondering if this PR includes check for required fields on the swagger file, and generates the objects adding "!" and "?"

I see in one of you examples:

public struct PersonCard: Codable { public var cardType: String public var firstName: String? public var lastName: String? }

cardType there be nor optional right?

Thanks in advance!

@wing328 wing328 merged commit a3d0f1d into swagger-api:master Jan 25, 2018
@fl034
Copy link

fl034 commented Feb 23, 2018

Hello @ehyche
I tried your

enums can contain the case "open", also properties can be named like that.
Can you change this so that "open" would not be replaced by "_open" inside enums?

Or does anything speak against this suggestion?

Big thanks for your great work!

@Zexbe
Copy link

Zexbe commented Mar 9, 2018

@ehyche
Can you look into handling recursion?
#7788

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants