swagger-api / swagger-codegen-generators

Apache License 2.0
279 stars 418 forks source link

Swift4 Codegen Date and DateTime error #403

Open gotev opened 5 years ago

gotev commented 5 years ago

Hi everybody, when using Swift4 Swagger Codegen, there's a problem in distinguishing Date and DateTime types declared in swagger, which gets both converted to Date: https://github.com/swagger-api/swagger-codegen-generators/blob/master/src/main/java/io/swagger/codegen/v3/generators/swift/Swift4Codegen.java#L80-L81

This wasn't a problem in Swift 3, where the the two types were different: https://github.com/swagger-api/swagger-codegen-generators/blob/master/src/main/java/io/swagger/codegen/v3/generators/swift/Swift3Codegen.java#L38-L39

Has anyone had the same problem? Most of all, how should we address it and solve it?

HugoMario commented 5 years ago

hello @gotev , can you propose a PR? i can help you to get it merged

gotev commented 5 years ago

Hi @HugoMario before starting the PR I wanted to make sure the solution is ok and up to swagger codegen standards. Since Swift Date type corresponds to swagger's date-time, the mapping for date-time will remain unchanged. What needs to be done is a custom type to represent just the ISO 8601 Full Date, as described in RFC 3339 section 5.6. I gave a look at what was done for Swift 3, but since then many things had changed in Swift 4, so I made a new type from scratch. Here it is:

import Foundation

public struct YearMonthDay {
    public let year: Int
    public let month: Int
    public let day: Int
}

extension YearMonthDay: Codable, Equatable, Hashable, Comparable {
    public init(from date: Date = Date()) {
        let calendar = Calendar(identifier: .iso8601)

        year = calendar.component(.year, from: date)
        month = calendar.component(.month, from: date)
        day = calendar.component(.day, from: date)
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawDate = try container.decode(String.self)
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = .withFullDate
        guard let date = formatter.date(from: rawDate)
            else { throw DecodingError.dataCorruptedError(in: container,
                                                      debugDescription: "Cannot decode date string \(rawDate)") }

        let dateComponents = Calendar(identifier: .iso8601).dateComponents([.year, .month, .day], from: date)

        guard let year = dateComponents.year,
            let month = dateComponents.month,
            let day = dateComponents.day
            else { throw DecodingError.dataCorruptedError(in: container,
                                                          debugDescription: "Cannot decode date string \(rawDate)") }

        self.year = year
        self.month = month
        self.day = day
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        let rawDate = String(describing: self)

        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = .withFullDate
        guard formatter.date(from: rawDate) != nil
            else { throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: container.codingPath,
                                                                                debugDescription: "Cannot encode \(self)")) }
        try container.encode(rawDate)
    }

    public static func < (lhs: YearMonthDay, rhs: YearMonthDay) -> Bool {
        if lhs.year < rhs.year {
            return true
        } else if lhs.year > rhs.year {
            return false
        } else {
            if lhs.month < rhs.month {
                return true
            } else if lhs.month > rhs.month {
                return false
            } else {
                return lhs.day < rhs.day
            }
        }
    }

    public func add(years: Int = 0, months: Int = 0, days: Int = 0) -> YearMonthDay? {
        var dateComponents = DateComponents()
        dateComponents.year = year + years
        dateComponents.month = month + months
        dateComponents.day = day + days

        guard let date = Calendar(identifier: .iso8601).date(from: dateComponents)
            else { return nil }

        return YearMonthDay(from: date)
    }
}

extension YearMonthDay: CustomStringConvertible {
    public var description: String {
        return [String(format: "%04d", year), String(format: "%02d", month), String(format: "%02d", day)].joined(separator: "-")
    }
}

And if you want to try it in an Xcode Playground just copy and paste the above and this:

struct FakePayload : Codable {
    let date: YearMonthDay
}

let encoder = JSONEncoder()
let testDate = YearMonthDay(year: 2019, month: 12, day: 20)
let data = try encoder.encode(FakePayload(date: testDate))
print(String(data: data, encoding: .utf8)!)

let decoder = JSONDecoder()
let decodedPayload = try decoder.decode(FakePayload.self, from: data)
print(decodedPayload.date.year)
print(decodedPayload.date.month)
print(decodedPayload.date.day)

The idea is to add this new type as it is to the Swift4Codegen Supporting Files and then change the mapping in Swift4Codegen, updating failing tests. If it's ok, I'll go forward with the PR, otherwise let me know 😉