From e590b046709ede6788aef76b97135fbc2c2c7cf9 Mon Sep 17 00:00:00 2001 From: Morten Bjerg Gregersen Date: Sun, 23 Jun 2024 01:48:43 +0200 Subject: [PATCH] Correctly handle properties with array of enum values (#190) * Better decoding of array properties of enums * Regenerate --- .../AppStore/TerritoryAvailability.swift | 8 ++--- .../CiScheduledStartCondition.swift | 20 ++++++------- .../Schemas/EnumSchema.swift | 2 +- .../Schemas/ObjectSchema.swift | 2 ++ .../Schemas/PropertyType.swift | 30 +++++++++++-------- .../Renderers/ObjectSchemaRendererTests.swift | 15 ++++++++-- .../Schemas/ObjectSchemaTests.swift | 27 +++++++++++++++++ .../Schemas/PropertyTypeTests.swift | 4 +-- 8 files changed, 77 insertions(+), 31 deletions(-) diff --git a/Sources/Bagbutik-Models/AppStore/TerritoryAvailability.swift b/Sources/Bagbutik-Models/AppStore/TerritoryAvailability.swift index 68240e32e..c1afbb257 100644 --- a/Sources/Bagbutik-Models/AppStore/TerritoryAvailability.swift +++ b/Sources/Bagbutik-Models/AppStore/TerritoryAvailability.swift @@ -48,13 +48,13 @@ public struct TerritoryAvailability: Codable, Identifiable { public struct Attributes: Codable { public var available: Bool? - public var contentStatuses: Items? + public var contentStatuses: [ContentStatuses]? public var preOrderEnabled: Bool? public var preOrderPublishDate: String? public var releaseDate: String? public init(available: Bool? = nil, - contentStatuses: Items? = nil, + contentStatuses: [ContentStatuses]? = nil, preOrderEnabled: Bool? = nil, preOrderPublishDate: String? = nil, releaseDate: String? = nil) @@ -69,7 +69,7 @@ public struct TerritoryAvailability: Codable, Identifiable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: AnyCodingKey.self) available = try container.decodeIfPresent(Bool.self, forKey: "available") - contentStatuses = try container.decodeIfPresent(Items.self, forKey: "contentStatuses") + contentStatuses = try container.decodeIfPresent([ContentStatuses].self, forKey: "contentStatuses") preOrderEnabled = try container.decodeIfPresent(Bool.self, forKey: "preOrderEnabled") preOrderPublishDate = try container.decodeIfPresent(String.self, forKey: "preOrderPublishDate") releaseDate = try container.decodeIfPresent(String.self, forKey: "releaseDate") @@ -84,7 +84,7 @@ public struct TerritoryAvailability: Codable, Identifiable { try container.encodeIfPresent(releaseDate, forKey: "releaseDate") } - public enum Items: String, Codable, CaseIterable { + public enum ContentStatuses: String, Codable, CaseIterable { case available = "AVAILABLE" case availableForPreorder = "AVAILABLE_FOR_PREORDER" case availableForPreorderOnDate = "AVAILABLE_FOR_PREORDER_ON_DATE" diff --git a/Sources/Bagbutik-Models/XcodeCloud/CiScheduledStartCondition.swift b/Sources/Bagbutik-Models/XcodeCloud/CiScheduledStartCondition.swift index 7ef1c1e9c..c57438d7b 100644 --- a/Sources/Bagbutik-Models/XcodeCloud/CiScheduledStartCondition.swift +++ b/Sources/Bagbutik-Models/XcodeCloud/CiScheduledStartCondition.swift @@ -42,7 +42,7 @@ public struct CiScheduledStartCondition: Codable { */ public struct Schedule: Codable { /// A list of days you configure for the start condition that starts a new build on a schedule. - public var days: Items? + public var days: [Days]? /// A string indicating the frequency of the start condition that starts a new build on a schedule. public var frequency: Frequency? /// An integer that represents the hour you configure for the start condition that starts a new build on a schedule. @@ -52,7 +52,7 @@ public struct CiScheduledStartCondition: Codable { /// A string that represents the time zone you configure for the start condition that starts a new build on a schedule. public var timezone: String? - public init(days: Items? = nil, + public init(days: [Days]? = nil, frequency: Frequency? = nil, hour: Int? = nil, minute: Int? = nil, @@ -67,7 +67,7 @@ public struct CiScheduledStartCondition: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: AnyCodingKey.self) - days = try container.decodeIfPresent(Items.self, forKey: "days") + days = try container.decodeIfPresent([Days].self, forKey: "days") frequency = try container.decodeIfPresent(Frequency.self, forKey: "frequency") hour = try container.decodeIfPresent(Int.self, forKey: "hour") minute = try container.decodeIfPresent(Int.self, forKey: "minute") @@ -83,13 +83,7 @@ public struct CiScheduledStartCondition: Codable { try container.encodeIfPresent(timezone, forKey: "timezone") } - public enum Frequency: String, Codable, CaseIterable { - case daily = "DAILY" - case hourly = "HOURLY" - case weekly = "WEEKLY" - } - - public enum Items: String, Codable, CaseIterable { + public enum Days: String, Codable, CaseIterable { case friday = "FRIDAY" case monday = "MONDAY" case saturday = "SATURDAY" @@ -98,5 +92,11 @@ public struct CiScheduledStartCondition: Codable { case tuesday = "TUESDAY" case wednesday = "WEDNESDAY" } + + public enum Frequency: String, Codable, CaseIterable { + case daily = "DAILY" + case hourly = "HOURLY" + case weekly = "WEEKLY" + } } } diff --git a/Sources/BagbutikSpecDecoder/Schemas/EnumSchema.swift b/Sources/BagbutikSpecDecoder/Schemas/EnumSchema.swift index 690a6804c..a8b207852 100644 --- a/Sources/BagbutikSpecDecoder/Schemas/EnumSchema.swift +++ b/Sources/BagbutikSpecDecoder/Schemas/EnumSchema.swift @@ -38,7 +38,7 @@ public struct EnumSchema: Decodable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - var name = container.codingPath.last!.stringValue.capitalizingFirstLetter() + var name = container.codingPath.last(where: { $0.stringValue != "items"})!.stringValue.capitalizingFirstLetter() if name == "Type" { var parentType = container.codingPath.dropLast(1).last!.stringValue if parentType == "properties" { diff --git a/Sources/BagbutikSpecDecoder/Schemas/ObjectSchema.swift b/Sources/BagbutikSpecDecoder/Schemas/ObjectSchema.swift index 3427ddbd5..371be31e1 100644 --- a/Sources/BagbutikSpecDecoder/Schemas/ObjectSchema.swift +++ b/Sources/BagbutikSpecDecoder/Schemas/ObjectSchema.swift @@ -21,6 +21,8 @@ public struct ObjectSchema: Decodable, Equatable { return .oneOf(name: name, schema: schema) case .enumSchema(let schema): return .enumSchema(schema) + case .arrayOfEnumSchema(let schema): + return .enumSchema(schema) default: return nil } diff --git a/Sources/BagbutikSpecDecoder/Schemas/PropertyType.swift b/Sources/BagbutikSpecDecoder/Schemas/PropertyType.swift index a7640be2b..854d745fb 100644 --- a/Sources/BagbutikSpecDecoder/Schemas/PropertyType.swift +++ b/Sources/BagbutikSpecDecoder/Schemas/PropertyType.swift @@ -12,6 +12,8 @@ public indirect enum PropertyType: Decodable, Equatable, CustomStringConvertible case schema(ObjectSchema) /// An enum schema case enumSchema(EnumSchema) + /// An array of enum schema + case arrayOfEnumSchema(EnumSchema) /// An array of schema case arrayOfSchemaRef(String) /// An array of object schema @@ -44,27 +46,29 @@ public indirect enum PropertyType: Decodable, Equatable, CustomStringConvertible public var description: String { switch self { case .simple(let simplePropertyType): - return simplePropertyType.description + simplePropertyType.description case .constant: - return SimplePropertyType.string.description + SimplePropertyType.string.description case .schemaRef(let schemaName): - return schemaName + schemaName case .schema(let schema): - return schema.name + schema.name case .enumSchema(let schema): - return schema.name + schema.name + case .arrayOfEnumSchema(let schema): + "[\(schema.name)]" case .arrayOfSchemaRef(let schemaName): - return "[\(schemaName)]" + "[\(schemaName)]" case .arrayOfSubSchema(let schema): - return "[\(schema.name)]" + "[\(schema.name)]" case .arrayOfSimple(let simplePropertyType): - return "[\(simplePropertyType.description)]" + "[\(simplePropertyType.description)]" case .oneOf(let name, _): - return name + name case .arrayOfOneOf(let name, _): - return "[\(name)]" + "[\(name)]" case .dictionary(let propertyType): - return "[String: \(propertyType.description)]" + "[String: \(propertyType.description)]" } } @@ -102,6 +106,8 @@ public indirect enum PropertyType: Decodable, Equatable, CustomStringConvertible let propertyType = try container.decode(PropertyType.self, forKey: .items) if propertyType.isSimple { self = .arrayOfSimple(.init(type: propertyType.description)) + } else if case .enumSchema(let enumSchema) = propertyType { + self = .arrayOfEnumSchema(enumSchema) } else { self = propertyType } @@ -110,7 +116,7 @@ public indirect enum PropertyType: Decodable, Equatable, CustomStringConvertible if let dictionaryValueType = try container.decodeIfPresent(PropertyType.self, forKey: .additionalProperties) { self = .dictionary(dictionaryValueType) } else { - self = .schema(try ObjectSchema(from: decoder)) + self = try .schema(ObjectSchema(from: decoder)) } } else { if type == "number" { type = "Double" } diff --git a/Tests/BagbutikGeneratorTests/Renderers/ObjectSchemaRendererTests.swift b/Tests/BagbutikGeneratorTests/Renderers/ObjectSchemaRendererTests.swift index f61936bb3..030c7c81d 100644 --- a/Tests/BagbutikGeneratorTests/Renderers/ObjectSchemaRendererTests.swift +++ b/Tests/BagbutikGeneratorTests/Renderers/ObjectSchemaRendererTests.swift @@ -280,7 +280,8 @@ final class ObjectSchemaRendererTests: XCTestCase { properties: ["firstName": .init(type: .simple(.init(type: "string"))), "lastName": .init(type: .simple(.init(type: "string"))), "self": .init(type: .simple(.init(type: "string"))), - "id": .init(type: .constant("person"))], + "id": .init(type: .constant("person")), + "days": .init(type: .arrayOfEnumSchema(.init(name: "Days", type: "string", caseValues: ["MONDAY", "TUESDAY"])))], requiredProperties: ["firstName"]) // When let rendered = try renderer.render(objectSchema: schema, otherSchemas: [:]) @@ -294,6 +295,7 @@ final class ObjectSchemaRendererTests: XCTestCase { */ public struct Person: Codable, Identifiable { + public var days: [Days]? /// The firstname of the person public let firstName: String /// The unique id for the person @@ -302,10 +304,12 @@ final class ObjectSchemaRendererTests: XCTestCase { /// A reference to the person public var itself: String? - public init(firstName: String, + public init(days: [Days]? = nil, + firstName: String, lastName: String? = nil, self itself: String? = nil) { + self.days = days self.firstName = firstName self.lastName = lastName self.itself = itself @@ -313,6 +317,7 @@ final class ObjectSchemaRendererTests: XCTestCase { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: AnyCodingKey.self) + days = try container.decodeIfPresent([Days].self, forKey: "days") firstName = try container.decode(String.self, forKey: "firstName") id = try container.decodeIfPresent(String.self, forKey: "id") lastName = try container.decodeIfPresent(String.self, forKey: "lastName") @@ -321,11 +326,17 @@ final class ObjectSchemaRendererTests: XCTestCase { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encodeIfPresent(days, forKey: "days") try container.encode(firstName, forKey: "firstName") try container.encodeIfPresent(id, forKey: "id") try container.encodeIfPresent(lastName, forKey: "lastName") try container.encodeIfPresent(itself, forKey: "self") } + + public enum Days: String, Codable, CaseIterable { + case monday = "MONDAY" + case tuesday = "TUESDAY" + } } """#) diff --git a/Tests/BagbutikSpecDecoderTests/Schemas/ObjectSchemaTests.swift b/Tests/BagbutikSpecDecoderTests/Schemas/ObjectSchemaTests.swift index cf4ca1c90..9cf25a156 100644 --- a/Tests/BagbutikSpecDecoderTests/Schemas/ObjectSchemaTests.swift +++ b/Tests/BagbutikSpecDecoderTests/Schemas/ObjectSchemaTests.swift @@ -202,6 +202,33 @@ final class ObjectSchemaTests: XCTestCase { XCTAssertEqual(associatedOneOfSchema.options[0].typeName, "String") XCTAssertEqual(associatedOneOfSchema.options[1].typeName, "Properties") } + + func testDecodingPropertyOfStringEnumArray() throws { + // https://github.com/MortenGregersen/Bagbutik/issues/189 + // Given + let json = #""" + { + "Schedule" : { + "type" : "object", + "properties" : { + "days" : { + "type" : "array", + "items" : { + "type" : "string", + "enum" : [ "MONDAY", "TUESDAY" ] + } + } + } + } + } + """# + // When + let objectSchema = try decodeObjectSchema(from: json) + // Then + guard case .arrayOfEnumSchema(let enumSchema) = objectSchema.properties["days"]?.type else { XCTFail(); return } + XCTAssertEqual(enumSchema.name, "Days") + XCTAssertEqual(enumSchema.cases, [.init(id: "monday", value: "MONDAY"), .init(id: "tuesday", value: "TUESDAY")]) + } func testDecodingUnknownPropertyType() throws { // Given diff --git a/Tests/BagbutikSpecDecoderTests/Schemas/PropertyTypeTests.swift b/Tests/BagbutikSpecDecoderTests/Schemas/PropertyTypeTests.swift index d90bfd323..e74eb3551 100644 --- a/Tests/BagbutikSpecDecoderTests/Schemas/PropertyTypeTests.swift +++ b/Tests/BagbutikSpecDecoderTests/Schemas/PropertyTypeTests.swift @@ -298,10 +298,10 @@ final class PropertyTypeTests: XCTestCase { } XCTAssertEqual(metaPropertyType.description, "[String: String]") - guard let daysPropertyType = propertyTypes["days"], case let .enumSchema(daysSchema) = daysPropertyType else { + guard let daysPropertyType = propertyTypes["days"], case let .arrayOfEnumSchema(daysSchema) = daysPropertyType else { return XCTFail("Wrong property type") } - XCTAssertEqual(daysSchema.name, "Items") + XCTAssertEqual(daysSchema.name, "Days") XCTAssertEqual(daysSchema.cases.count, 7) } }