diff --git a/README.md b/README.md index 9fce3ba..c7a5d73 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ import PackageDescription let package = Package( name: "SomeProject", dependencies: [ - .package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.1.3") + .package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.1.4") ], targets: [ .target( diff --git a/SwagGen_Template/APIClient/Coding.swift b/SwagGen_Template/APIClient/Coding.swift new file mode 100644 index 0000000..bd78620 --- /dev/null +++ b/SwagGen_Template/APIClient/Coding.swift @@ -0,0 +1,14 @@ +import Foundation + +public typealias APIModel = Codable & Equatable + +public typealias DateTime = Date +public typealias File = Data +public typealias ID = UUID + +extension Encodable { + + func encode() -> String { + (self as? String) ?? "\(self)" + } +} diff --git a/SwagGen_Template/Includes/Enum.stencil b/SwagGen_Template/Includes/Enum.stencil new file mode 100644 index 0000000..20c0ffe --- /dev/null +++ b/SwagGen_Template/Includes/Enum.stencil @@ -0,0 +1,21 @@ +{% if description %} +/** {{ description }} */ +{% endif %} +public enum {{ enumName }}: {{ type }}, Codable, Equatable, CaseIterable{% if type == "String" %}, CustomStringConvertible{% endif %} { + {% for enumCase in enums %} + case {{ enumCase.name }} = {% if type == "String" %}"{% endif %}{{enumCase.value}}{% if type == "String" %}"{% endif %} + {% endfor %} + {% if options.enumUndecodedCase %} + case undecoded + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode({{ type }}.self) + self = {{ enumName }}(rawValue: rawValue) ?? .undecoded + } + {% endif %} + + {% if type == "String" %} + public var description: String { rawValue } + {% endif %} +} diff --git a/SwagGen_Template/Includes/Model.stencil b/SwagGen_Template/Includes/Model.stencil new file mode 100644 index 0000000..af8c8d6 --- /dev/null +++ b/SwagGen_Template/Includes/Model.stencil @@ -0,0 +1,158 @@ +{% macro propertyType property %}{% if property.type == "DateTime" or property.type == "DateDay" %}Date{% else %}{% if property.raw.format == "timestamp" %}Date{% else %}{{ property.type }}{% endif %}{% endif %}{% if property.optional or property.raw.nullable %}?{% endif %}{% endmacro %} +{% macro propertyName property %}{% if property.value|hasPrefix:"_" %}_{{ property.name }}{% else %}{{ property.name }}{% endif %}{% endmacro %} +{% if options.excludeTypes[type] == true %} + +{% else %} +{% if description %} +/** {{ description }} */ +{% endif %} +{% if enum %} +{% include "Includes/Enum.stencil" enum %} +{% elif aliasType %} +{% if type != "String" %} +public typealias {{ type }} = {{ aliasType }} +{% endif %} +{% elif additionalPropertiesType and allProperties.count == 0 %} +public typealias {{ type }} = [String: {{ additionalPropertiesType }}] +{% elif discriminatorType %} +public enum {{ type }}: {% if options.modelProtocol %}{{ options.modelProtocol }}{% else %}Codable, Equatable{% endif %} { + + {% for subType in discriminatorType.subTypes %} + case {{ subType.name }}({{ subType.type }}) + {% endfor %} + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: PlainCodingKey.self) + let discriminator: String = try container.decode(path: "{{ discriminatorType.discriminatorProperty }}".components(separatedBy: ".").map { PlainCodingKey($0) }) + switch discriminator { + {% for name, subType in discriminatorType.mapping %} + case "{{ name }}": + self = .{{ subType.name}}(try {{ subType.type }}(from: decoder)) + {% endfor %} + default: + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Couldn't find type to decode with discriminator \"\(discriminator)\"")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + {% for subType in discriminatorType.subTypes %} + case .{{ subType.name}}(let content): + try container.encode(content) + {% endfor %} + } + } + + {% for subType in discriminatorType.subTypes %} + public var {{ subType.name }}: {{ subType.type }}? { + if case .{{ subType.name }}(let value) = self { + return value + } + return nil + } + {% endfor %} +} +{% else %} +public {{ options.modelType }} {{ type }}: {% if parent %}{{ parent.type }}{% else %}{% if options.modelProtocol %}{{ options.modelProtocol }}{% else %}Codable, Equatable{% endif %}{% endif %} { + + {% for property in properties %} + {% if not property.raw.deprecated %} + {% if property.description %} + /// {{ property.description }} + {% endif %} + public {% if options.mutableModels %}var{% else %}let{% endif %} {% call propertyName property %}: {% call propertyType property %} + {% endif %} + {% endfor %} + {% if additionalPropertiesType %} + + public {% if options.mutableModels %}var{% else %}let{% endif %} additionalProperties: [String: {{ additionalPropertiesType }}] = [:] + {% endif %} + + public enum CodingKeys: String, CodingKey { + + {% for property in properties %} + {% if not property.raw.deprecated %} + case {% call propertyName property %}{% if property.name != property.value %} = "{{ property.value }}"{% endif %} + {% endif %} + {% endfor %} + } + + public {% if parent %}{% if properties.count == 0 %}override {% endif %}{% endif %}init( + {% for property in allProperties %} + {% if not property.raw.deprecated %} + {% call propertyName property %}: {% call propertyType property %}{% if property.optional or property.raw.nullable %} = nil{% endif %}{% ifnot forloop.last %},{% endif %} + {% endif %} + {% endfor %} + ) { + {% for property in properties %} + {% if not property.raw.deprecated %} + self.{% call propertyName property %} = {% call propertyName property %} + {% endif %} + {% endfor %} + {% if parent %} + super.init({% for property in parent.allProperties %}{% call propertyName property %}: {% call propertyName property %}{% ifnot forloop.last %}, {% endif %}{% endfor %}) + {% endif %} + } + {% if additionalPropertiesType %} + + public subscript(key: String) -> {{ additionalPropertiesType }}? { + get { + return additionalProperties[key] + } + set { + additionalProperties[key] = newValue + } + } + {% endif %} + {% if options.modelType == "class" %} + + {% if parent %}override {% endif %}public func isEqual(to object: Any?) -> Bool { + {% if properties.count > 0 or additionalPropertiesType %} + guard let object = object as? {{ type }} else { return false } + {% else %} + guard object is {{ type }} else { return false } + {% endif %} + {% for property in properties %} + {% if property.type == "[String: Any]" %} + guard NSDictionary(dictionary: self.{% call propertyName property %} {% if property.optional %}?? [:]{% endif %}).isEqual(to: object.{% call propertyName property %}{% if property.optional %} ?? [:]{% endif %}) else { return false } + {% else %} + guard self.{% call propertyName property %} == object.{% call propertyName property %} else { return false } + {% endif %} + {% endfor %} + {% if additionalPropertiesType %} + guard NSDictionary(dictionary: self.additionalProperties).isEqual(to: object.additionalProperties) else { return false } + {% endif %} + {% if parent %} + return super.isEqual(to: object) + {% else %} + return true + {% endif %} + } + {% if not parent %} + + public static func == (lhs: {{ type }}, rhs: {{ type }}) -> Bool { + return lhs.isEqual(to: rhs) + } + {% endif %} + {% endif %} + {% for enum in enums %} + {% if not enum.isGlobal %} + + {% filter indent:4 %}{% include "Includes/Enum.stencil" enum %}{% endfilter %} + {% endif %} + {% endfor %} + {% for schema in schemas %} + {% if options.globals[schema.type] != true %} + {% if options.typealiases[type][schema.type] != null %} + + public typealias {{ schema.type }} = {{ options.typealiases[type][schema.type] }} + {% else %} + + {% filter indent:4 %}{% include "Includes/Model.stencil" schema %}{% endfilter %} + {% endif %} + {% endif %} + {% endfor %} +} +{% endif %} +{% endif %} diff --git a/SwagGen_Template/Sources/APIModule.swift b/SwagGen_Template/Sources/APIModule.swift new file mode 100644 index 0000000..5624f2d --- /dev/null +++ b/SwagGen_Template/Sources/APIModule.swift @@ -0,0 +1,112 @@ +// swiftlint:disable all +import Foundation +import SwiftAPIClient + +{% if info.description %} +/** {{ info.description }} */ +{% endif %} +public struct {{ options.name }} { + + {% if info.version %} + public static let version = "{{ info.version }}" + {% endif %} + public var client: APIClient + + public init(client: APIClient) { + self.client = client + } +} +{% if servers %} +extension {{ options.name }} { + + public struct Server: Hashable { + + /// URL of the server + public var url: URL + + public init(_ url: URL) { + self.url = url + } + + {% ifnot servers[0].variables %} + public static var `default` = {{ options.name }}.Server.{{ servers[0].name }} + {% endif %} + {% for server in servers %} + + {% if server.description %} + /** {{ server.description }} */ + {% endif %} + {% if server.variables %} + public static func {{ server.name }}({% for variable in server.variables %}{{ variable.name|replace:'-','_' }}: String = "{{ variable.defaultValue }}"{% ifnot forloop.last %}, {% endif %}{% endfor %}) -> {{ options.name }}.Server { + var urlString = "{{ server.url }}" + {% for variable in server.variables %} + urlString = urlString.replacingOccurrences(of: {{'"{'}}{{variable.name}}{{'}"'}}, with: {{variable.name|replace:'-','_'}}) + {% endfor %} + return {{ options.name }}.Server(URL(string: urlString)!) + } + {% else %} + public static let {{ server.name }} = {{ options.name }}.Server(URL(string: "{{ server.url }}")!) + {% endif %} + {% endfor %} + } +} + +extension APIClient.Configs { + + /// {{ options.name }} server + public var {{ options.name|lowerFirstWord }}Server: {{ options.name }}.Server{% if servers[0].variables %}?{% endif %} { + get { self[\.{{ options.name|lowerFirstWord }}Server]{% ifnot servers[0].variables %} ?? .default{% endif %} } + set { self[\.{{ options.name|lowerFirstWord }}Server] = newValue } + } +} + +{% else %} + +// No servers defined in swagger. Documentation for adding them: https://swagger.io/specification/#schema +{% endif %} +{% if options.groupingType == "path" %} +{% macro pathTypeName path %}{{ path|basename|upperFirstLetter|replace:"{","By_"|replace:"}",""|swiftIdentifier:"pretty" }}{% endmacro %} +{% macro pathAsType path %}{% if path != "/" and path != "" %}{% call pathAsType path|dirname %}.{% call pathTypeName path %}{% endif %}{% endmacro %} + +{% macro pathVarAndType path allPaths definedPath %} +{% set currentPath path|dirname %} +{% if path != "/" and path != "" %} +{% set _path %}|{{path}}|{% endset %} +{% if definedPath|contains:_path == false %} +extension {{ options.name }}{% call pathAsType currentPath %} { + {% set name path|basename %} + /// {{ path }} + {% if name|contains:"{" %} + public func callAsFunction(_ path: String) -> {% call pathTypeName path %} { {% call pathTypeName path %}(client: client(path)) } + {% else %} + public var {{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}: {% call pathTypeName path %} { {% call pathTypeName path %}(client: client("{{name}}")) } + {% endif %} + public struct {% call pathTypeName path %} { public var client: APIClient } +} +{% set newDefinedPaths %}{{_path}}{{definedPath}}{% endset %} +{% call pathVarAndType currentPath allPaths newDefinedPaths %} +{% else %} +{% call pathVarAndType currentPath allPaths definedPath %} +{% endif %} +{% else %} +{% if allPaths != "/" and allPaths != "" %} +{% set path %}{{ allPaths|basename|replace:"$","/" }}{% endset %} +{% set newAllPaths allPaths|dirname %} +{% call pathVarAndType path newAllPaths definedPath %} +{% endif %} +{% endif %} +{% endmacro %} + +{% map paths into pathArray %}{{maploop.item.path|replace:"/","$"}}{% endmap %} +{% set allPaths %}/{{ pathArray|join:"/" }}{% endset %} + +{% set path %}{{ allPaths|basename|replace:"$","/" }}{% endset %} +{% call pathVarAndType path allPaths "" %} +{% elif options.groupingType == "tag" and tags %} +{% for tag in tags %} +extension {{ options.name }} { + public var {{ tag|swiftIdentifier|lowerFirstLetter }}: {{ tag|swiftIdentifier }} { {{ tag|swiftIdentifier }}(client: client) } + public struct {{ tag|swiftIdentifier }} { var client: APIClient } +} +{% endfor %} +{% endif %} diff --git a/SwagGen_Template/Sources/Enum.swift b/SwagGen_Template/Sources/Enum.swift new file mode 100644 index 0000000..ca192fe --- /dev/null +++ b/SwagGen_Template/Sources/Enum.swift @@ -0,0 +1 @@ +{% include "Includes/Enum.stencil" %} diff --git a/SwagGen_Template/Sources/Model.swift b/SwagGen_Template/Sources/Model.swift new file mode 100644 index 0000000..8f38d35 --- /dev/null +++ b/SwagGen_Template/Sources/Model.swift @@ -0,0 +1,4 @@ +import Foundation +import SwiftAPIClient + +{% include "Includes/Model.stencil" %} diff --git a/SwagGen_Template/Sources/Request.swift b/SwagGen_Template/Sources/Request.swift new file mode 100644 index 0000000..d028ccf --- /dev/null +++ b/SwagGen_Template/Sources/Request.swift @@ -0,0 +1,90 @@ +{% if options.excludeTypes[type] == false %} + +{% else %} +// swiftlint:disable all +import Foundation +import SwiftAPIClient + +{% set tagType %}{{ tag|swiftIdentifier }}{% endset %} +{% macro pathTypeName path %}{{ path|basename|upperFirstLetter|replace:"{","By_"|replace:"}",""|swiftIdentifier:"pretty" }}{% endmacro %} +{% if options.groupingType == "path" %} +{% macro pathPart path %}{% if path != "/" and path != "" %}{% call pathPart path|dirname %}.{% call pathTypeName path %}{% endif %}{% endmacro %} +extension {{ options.name }}{% call pathPart path %} { +{% elif options.groupingType == "tag" and tag %} +extension {{ options.name }}.{{ tagType }} { +{% else %} +extension {{ options.name }} { +{% endif %} + + /** + {% if summary %} + {{ summary }} + + {% endif %} + {% if description %} + {{ description }} + + {% endif %} + **{{ method|uppercase }}** {{ path }} + */ + {% set funcName %}{% if options.groupingType == "path" %}{{ method|lowercase|escapeReservedKeywords }}{% elif options.groupingType == "tag" and tag %}{{ type|replace:tagType,""|lowerFirstLetter }}{% else %}{{ type|lowerFirstLetter }}{% endif %}{% endset %} + public func {{ funcName }}({% if options.groupingType != "path" %}{% for param in pathParams %}{{ param.name }}: {{ param.type }}, {% endfor %}{% endif %}{% for param in queryParams %}{{ param.name }}: {{ param.optionalType }}{% ifnot param.required %} = nil{% endif %}, {% endfor %}{% for param in headerParams %}{% if options.excludeHeaders[param.value] != true %}{{ param.name }}: {{ param.optionalType }}{% ifnot param.required %} = nil{% endif %}, {% endif %}{% endfor %}{% if body %}{{ body.name }}: {{ body.optionalType }}{% ifnot body.required %} = nil{% endif %}, {% endif %}fileID: String = #fileID, line: UInt = #line) async throws -> {{ successType|default:"Void" }} { + try await client + {% if options.groupingType != "path" %} + .path("{{ path|replace:"{","\("|replace:"}",")" }}") + {% endif %} + .method(.{{ method|lowercase }}) + {% if queryParams %} + .query([ + {% for param in queryParams %} + "{{ param.value }}": {{ param.value }}{% ifnot forloop.last %},{% endif %} + {% endfor %} + ]) + {% endif %} + {% if headerParams %} + {% for param in headerParams %} + {% if options.excludeHeaders[param.value] != true %} + .header(HTTPField.Name("{{ param.value }}")!, {{ param.encodedValue }}) + {% endif %} + {% endfor %} + {% endif %} + .auth(enabled: {% if securityRequirements %}true{% else %}false{% endif %}) + {% if body %} + .body(body) + {% endif %} + .call( + .http, + as: .{% if successType == "String" %}string{% elif successType == "Data" or successType == "File" %}identity{% elif successType %}decodable{% else %}void{% endif %}, + fileID: fileID, + line: line + ) + } + + {% if requestEnums or requestSchemas %} + public enum {{ type }} { + {% for enum in requestEnums %} + {% if not enum.isGlobal %} + + {% filter indent:8 %}{% include "Includes/Enum.stencil" enum %}{% endfilter %} + {% endif %} + {% endfor %} + {% for schema in requestSchemas %} + + {% filter indent:12 %}{% include "Includes/Model.stencil" schema %}{% endfilter %} + {% endfor %} + + {% for schema in responseSchemas %} + + {% filter indent:8 %}{% include "Includes/Model.stencil" schema %}{% endfilter %} + + {% endfor %} + {% for enum in responseEnums %} + {% if not enum.isGlobal %} + + {% filter indent:8 %}{% include "Includes/Enum.stencil" enum %}{% endfilter %} + {% endif %} + {% endfor %} + } + {% endif %} +} +{% endif %} diff --git a/SwagGen_Template/template.yml b/SwagGen_Template/template.yml new file mode 100644 index 0000000..0dcde2b --- /dev/null +++ b/SwagGen_Template/template.yml @@ -0,0 +1,39 @@ +formatter: swift +options: + name: API + fixedWidthIntegers: false # whether to use types like Int32 and Int64 + mutableModels: true # whether model properties are mutable + safeOptionalDecoding: false # set invalid optionals to nil instead of throwing + modelPrefix: null # applied to model classes and enums + modelSuffix: null # applied to model classes + modelNames: {} # override model type names + enumNames: {} # override enum type names + enumUndecodedCase: true # whether to add undecodable case to enums + codableResponses: true # constrains all responses/model to be Codable + propertyNames: {} # override property names + anyType: JSON # override Any in generated models + numberType: Decimal # number type without format + groupingType: tag # how to group requests, can be path, tag or none + excrsionCheck: false + excludeTypes: {} # whether to exclude types from the autogenerated models, example: { Color: true } + excludeHeaders: {} # whether to exclude headers from the autogenerated requests, example: { ContentType: true } + dependencies: + - name: SwiftAPIClient + github: dankinsoid/swift-api-client + version: 1.1.0 + - name: SwiftJSON + github: dankinsoid/swift-json + version: 1.1.0 +templateFiles: + - path: Sources/APIModule.swift + destination: "{{options.name}}/{{options.name}}.swift" + - path: Sources/Enum.swift + context: enums + destination: "Enums/{{ enumName }}.swift" + - path: Sources/Model.swift + context: schemas + destination: "Models/{{ type }}.swift" + - path: Sources/Request.swift + context: operations + destination: "{{options.name}}/Requests{% if tag %}/{{ tag|upperCamelCase }}{% endif %}/{{ type }}.swift" +copiedFiles: ["Models", "APIClient"]