Skip to content

Commit

Permalink
Clearable KidsAgeBand on AgeRatingDeclarationUpdateRequest (#191)
Browse files Browse the repository at this point in the history
* Add ClearableCodable property wrapper

* Patch spec to mark kidsAgeBand property as clearable

* Render clearable properties

* Remove property wrapper

* Move Clearable

* Regenerate
  • Loading branch information
MortenGregersen authored Jun 30, 2024
1 parent e590b04 commit ce4ad35
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 34 deletions.
22 changes: 22 additions & 0 deletions Sources/Bagbutik-Core/Clearable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

/// Wrapper for a property that can be cleared. Used in `UpdateRequest`s.
public enum Clearable<Value>: Codable, Equatable where Value: Codable & Equatable {
/// The value to set
case value(Value)
/// Clear current value
case clear

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = try .value(container.decode(Value.self))
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .value(let value): try container.encode(value)
case .clear: try container.encodeNil()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public struct AgeRatingDeclarationUpdateRequest: Codable, RequestBody {
/// Declaration for horror or fear themed content.
public var horrorOrFearThemes: AgeRatingDeclaration.Attributes.HorrorOrFearThemes?
/// Declaration for the Kids Age Band value.
public var kidsAgeBand: KidsAgeBand?
public var kidsAgeBand: Clearable<KidsAgeBand>?
/// Declaration for mature or suggestive themes.
public var matureOrSuggestiveThemes: AgeRatingDeclaration.Attributes.MatureOrSuggestiveThemes?
/// Declaration for medical or treatment-focused content.
Expand Down Expand Up @@ -120,7 +120,7 @@ public struct AgeRatingDeclarationUpdateRequest: Codable, RequestBody {
gamblingAndContests: Bool? = nil,
gamblingSimulated: AgeRatingDeclaration.Attributes.GamblingSimulated? = nil,
horrorOrFearThemes: AgeRatingDeclaration.Attributes.HorrorOrFearThemes? = nil,
kidsAgeBand: KidsAgeBand? = nil,
kidsAgeBand: Clearable<KidsAgeBand>? = nil,
matureOrSuggestiveThemes: AgeRatingDeclaration.Attributes.MatureOrSuggestiveThemes? = nil,
medicalOrTreatmentInformation: AgeRatingDeclaration.Attributes.MedicalOrTreatmentInformation? = nil,
profanityOrCrudeHumor: AgeRatingDeclaration.Attributes.ProfanityOrCrudeHumor? = nil,
Expand Down Expand Up @@ -158,7 +158,7 @@ public struct AgeRatingDeclarationUpdateRequest: Codable, RequestBody {
gambling: Bool? = nil,
gamblingSimulated: AgeRatingDeclaration.Attributes.GamblingSimulated? = nil,
horrorOrFearThemes: AgeRatingDeclaration.Attributes.HorrorOrFearThemes? = nil,
kidsAgeBand: KidsAgeBand? = nil,
kidsAgeBand: Clearable<KidsAgeBand>? = nil,
matureOrSuggestiveThemes: AgeRatingDeclaration.Attributes.MatureOrSuggestiveThemes? = nil,
medicalOrTreatmentInformation: AgeRatingDeclaration.Attributes.MedicalOrTreatmentInformation? = nil,
profanityOrCrudeHumor: AgeRatingDeclaration.Attributes.ProfanityOrCrudeHumor? = nil,
Expand Down Expand Up @@ -196,7 +196,7 @@ public struct AgeRatingDeclarationUpdateRequest: Codable, RequestBody {
gamblingAndContests = try container.decodeIfPresent(Bool.self, forKey: "gamblingAndContests")
gamblingSimulated = try container.decodeIfPresent(AgeRatingDeclaration.Attributes.GamblingSimulated.self, forKey: "gamblingSimulated")
horrorOrFearThemes = try container.decodeIfPresent(AgeRatingDeclaration.Attributes.HorrorOrFearThemes.self, forKey: "horrorOrFearThemes")
kidsAgeBand = try container.decodeIfPresent(KidsAgeBand.self, forKey: "kidsAgeBand")
kidsAgeBand = try container.decodeIfPresent(Clearable<KidsAgeBand>.self, forKey: "kidsAgeBand")
matureOrSuggestiveThemes = try container.decodeIfPresent(AgeRatingDeclaration.Attributes.MatureOrSuggestiveThemes.self, forKey: "matureOrSuggestiveThemes")
medicalOrTreatmentInformation = try container.decodeIfPresent(AgeRatingDeclaration.Attributes.MedicalOrTreatmentInformation.self, forKey: "medicalOrTreatmentInformation")
profanityOrCrudeHumor = try container.decodeIfPresent(AgeRatingDeclaration.Attributes.ProfanityOrCrudeHumor.self, forKey: "profanityOrCrudeHumor")
Expand Down
48 changes: 26 additions & 22 deletions Sources/BagbutikGenerator/Renderers/ObjectSchemaRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ public class ObjectSchemaRenderer: Renderer {
structContent.append(renderInitializer(parameters: [.init(prefix: "from", name: "decoder", type: "Decoder")], throwing: true, content: {
var functionContent = "let container = try decoder.container(keyedBy: AnyCodingKey.self)\n"
functionContent += propertiesInfo.decodableProperties.map { decodableProperty in
if decodableProperty.optional {
return "\(decodableProperty.name.safeName) = try container.decodeIfPresent(\(decodableProperty.type).self, forKey: \"\(decodableProperty.name.idealName)\")"
if decodableProperty.clearable {
"\(decodableProperty.name.safeName) = try container.decodeIfPresent(Clearable<\(decodableProperty.type)>.self, forKey: \"\(decodableProperty.name.idealName)\")"
} else if decodableProperty.optional {
"\(decodableProperty.name.safeName) = try container.decodeIfPresent(\(decodableProperty.type).self, forKey: \"\(decodableProperty.name.idealName)\")"
} else {
return "\(decodableProperty.name.safeName) = try container.decode(\(decodableProperty.type).self, forKey: \"\(decodableProperty.name.idealName)\")"
"\(decodableProperty.name.safeName) = try container.decode(\(decodableProperty.type).self, forKey: \"\(decodableProperty.name.idealName)\")"
}
}.joined(separator: "\n")
if propertiesInfo.hasTypeConstant {
Expand All @@ -84,23 +86,23 @@ public class ObjectSchemaRenderer: Renderer {
structContent.append(renderFunction(named: "encode", parameters: [.init(prefix: "to", name: "encoder", type: "Encoder")], throwing: true, content: {
var functionContent = "var container = encoder.container(keyedBy: AnyCodingKey.self)\n"
functionContent += propertiesInfo.encodableProperties.map { encodableProperty in
if encodableProperty.optional, !encodableProperty.nullCodable {
return "try container.encodeIfPresent(\(encodableProperty.name.safeName), forKey: \"\(encodableProperty.name.idealName)\")"
if encodableProperty.clearable || encodableProperty.optional, !encodableProperty.nullCodable {
"try container.encodeIfPresent(\(encodableProperty.name.safeName), forKey: \"\(encodableProperty.name.idealName)\")"
} else {
return "try container.encode(\(encodableProperty.name.safeName), forKey: \"\(encodableProperty.name.idealName)\")"
"try container.encode(\(encodableProperty.name.safeName), forKey: \"\(encodableProperty.name.idealName)\")"
}
}.joined(separator: "\n")
return functionContent
}))
structContent.append(contentsOf: createIncludedGetters(for: objectSchema, otherSchemas: otherSchemas))
structContent.append(contentsOf: try objectSchema.subSchemas.map { subSchema -> String in
try structContent.append(contentsOf: objectSchema.subSchemas.map { subSchema -> String in
switch subSchema {
case .objectSchema(let objectSchema):
return try render(objectSchema: objectSchema, otherSchemas: otherSchemas)
try render(objectSchema: objectSchema, otherSchemas: otherSchemas)
case .enumSchema(let enumSchema):
return try EnumSchemaRenderer(docsLoader: docsLoader).render(enumSchema: enumSchema)
try EnumSchemaRenderer(docsLoader: docsLoader).render(enumSchema: enumSchema)
case .oneOf(let name, let oneOfSchema):
return try! OneOfSchemaRenderer(docsLoader: docsLoader).render(name: name, oneOfSchema: oneOfSchema)
try! OneOfSchemaRenderer(docsLoader: docsLoader).render(name: name, oneOfSchema: oneOfSchema)
}
})
return structContent.joined(separator: "\n\n")
Expand Down Expand Up @@ -141,7 +143,7 @@ public class ObjectSchemaRenderer: Renderer {
let type = property.value.type.description
if id == "data" && objectSchema.name.hasSuffix("LinkageRequest") {
// The `data` property is marked as required, it is required, but the value is really optional.
// I can be nil, but it still needs to be in the encoded data.
// It can be nil, but it still needs to be in the encoded data.
objectSchema.requiredProperties.removeAll(where: { $0 == property.key })
}
let isOptional = !objectSchema.requiredProperties.contains(property.key)
Expand All @@ -154,7 +156,8 @@ public class ObjectSchemaRenderer: Renderer {
type: type,
optional: isOptional,
isSimpleType: property.value.type.isSimple,
deprecated: property.value.deprecated)
deprecated: property.value.deprecated,
clearable: property.value.clearable)
}
let propertyDocumentation = documentation?.properties[property.key]
return RenderProperty(rendered: rendered, documentation: propertyDocumentation, deprecated: property.value.deprecated)
Expand All @@ -173,7 +176,8 @@ public class ObjectSchemaRenderer: Renderer {
name: propertyName,
type: $0.value.type.description,
optional: !objectSchema.requiredProperties.contains($0.key) && $0.key != "type",
nullCodable: propertyName.safeName == "data" && type == "Data" || type == "[Data]" || type == "[Item]")
nullCodable: propertyName.safeName == "data" && type == "Data" || type == "[Data]" || type == "[Item]",
clearable: $0.value.clearable)
}
decodableProperties = encodableProperties.filter { !hasTypeConstant || $0.name.idealName != "type" }
}
Expand All @@ -193,7 +197,8 @@ public class ObjectSchemaRenderer: Renderer {
return FunctionParameter(prefix: prefix,
name: name,
type: $0.value.type.description.capitalizingFirstLetter(),
optional: !requiredProperties.contains($0.key))
optional: !requiredProperties.contains($0.key),
clearable: $0.value.clearable)
}
}
}
Expand Down Expand Up @@ -253,11 +258,10 @@ public class ObjectSchemaRenderer: Renderer {
let functionContent: String
if isArrayReturnType {
let relationshipSingular = relationship.key.singularized()
let guardIds: String
if isPagedGetter {
guardIds = "guard let \(relationshipSingular)Ids = \(pagedType).relationships?.\(relationship.key)?.data?.map(\\.id),"
let guardIds = if isPagedGetter {
"guard let \(relationshipSingular)Ids = \(pagedType).relationships?.\(relationship.key)?.data?.map(\\.id),"
} else {
guardIds = "guard let \(relationshipSingular)Ids = data.relationships?.\(relationship.key)?.data?.map(\\.id),"
"guard let \(relationshipSingular)Ids = data.relationships?.\(relationship.key)?.data?.map(\\.id),"
}
functionContent = """
\(guardIds)
Expand All @@ -270,11 +274,10 @@ public class ObjectSchemaRenderer: Renderer {
return \(relationship.key)
"""
} else {
let firstFilter: String
if isPagedGetter {
firstFilter = ".first { $0.id == \(pagedType).relationships?.\(relationship.key)?.data?.id }"
let firstFilter = if isPagedGetter {
".first { $0.id == \(pagedType).relationships?.\(relationship.key)?.data?.id }"
} else {
firstFilter = ".first { $0.id == data.relationships?.\(relationship.key)?.data?.id }"
".first { $0.id == data.relationships?.\(relationship.key)?.data?.id }"
}
functionContent = """
included?.compactMap { relationship -> \(includedSchemaName)? in
Expand Down Expand Up @@ -310,5 +313,6 @@ public class ObjectSchemaRenderer: Renderer {
let type: String
let optional: Bool
let nullCodable: Bool
let clearable: Bool
}
}
9 changes: 7 additions & 2 deletions Sources/BagbutikGenerator/Renderers/PropertyRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
internal class PropertyRenderer: Renderer {
func renderProperty(id: String, type: String, access: String = "public", optional: Bool, isSimpleType: Bool, deprecated: Bool = false) -> String {
func renderProperty(id: String, type: String, access: String = "public", optional: Bool, isSimpleType: Bool, deprecated: Bool = false, clearable: Bool = false) -> String {
var rendered = ""
if deprecated {
rendered += #"@available(*, deprecated, message: "Apple has marked this property deprecated and it will be removed sometime in the future.")"#
rendered += "\n"
}
let propertyType = deprecated || optional ? "var" : "let"
rendered += "public \(propertyType) \(escapeReservedKeywords(in: id)): \(type.capitalizingFirstLetter())"
rendered += "public \(propertyType) \(escapeReservedKeywords(in: id)): "
if clearable {
rendered += "Clearable<\(type.capitalizingFirstLetter())>"
} else {
rendered += type.capitalizingFirstLetter()
}
if optional {
rendered += "?"
}
Expand Down
12 changes: 10 additions & 2 deletions Sources/BagbutikGenerator/Renderers/Renderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,14 @@ public class Renderer {
let name: String
let type: String
let optional: Bool
let clearable: Bool

init(prefix: String? = nil, name: String, type: String, optional: Bool = false) {
init(prefix: String? = nil, name: String, type: String, optional: Bool = false, clearable: Bool = false) {
self.prefix = prefix
self.name = name
self.type = type
self.optional = optional
self.clearable = clearable
}
}

Expand All @@ -178,12 +180,18 @@ private extension Collection where Element == Renderer.FunctionParameter {
var formatted: String {
reduce(into: [String]()) { partialResult, parameter in
let name: String
let type: String
if let prefix = parameter.prefix {
name = "\(prefix) \(parameter.name)"
} else {
name = parameter.name
}
partialResult.append("\(name): \(parameter.type)\(parameter.optional ? "? = nil" : "")")
if parameter.clearable {
type = "Clearable<\(parameter.type)>"
} else {
type = parameter.type
}
partialResult.append("\(name): \(type)\(parameter.optional ? "? = nil" : "")")
}.joined(separator: ",\n")
}
}
6 changes: 5 additions & 1 deletion Sources/BagbutikSpecDecoder/Schemas/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ public struct Property: Equatable {
public var type: PropertyType
/// Tells if the property is deprecated
public let deprecated: Bool
/// Tells if the property can be cleared (in update requests)
public let clearable: Bool

/**
Initialize a new property
- Parameters:
- type: The type of the property
- deprecated: Tells if the property is deprecated
- clearable: Tells if the property can be cleared (in `UpdateRequest`s)
*/
public init(type: PropertyType, deprecated: Bool = false) {
public init(type: PropertyType, deprecated: Bool = false, clearable: Bool = false) {
self.type = type
self.deprecated = deprecated
self.clearable = clearable
}
}
20 changes: 20 additions & 0 deletions Sources/BagbutikSpecDecoder/Spec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,26 @@ public struct Spec: Decodable {
errorResponseSchema.properties["errors"]?.type = .arrayOfSubSchema(errorSchema)
components.schemas["ErrorResponse"] = .object(errorResponseSchema)
patchedSchemas.append(.object(errorResponseSchema))

// Marks the `kidsAgeBand` property on `AgeRatingDeclarationUpdateRequest.Data.Attributes` as clearable.
// Apple's OpenAPI spec has no information about how to clear a value in an update request.
// To tell Apple to clear a value, it has to be `null`, but properties with `null` values normally get omitted.
if case .object(var ageRatingDeclarationUpdateRequestSchema) = components.schemas["AgeRatingDeclarationUpdateRequest"],
var ageRatingDeclarationUpdateRequestDataSchema: ObjectSchema = ageRatingDeclarationUpdateRequestSchema.subSchemas.compactMap({
guard case .objectSchema(let subSchema) = $0, subSchema.name == "Data" else { return nil }
return subSchema
}).first,
var ageRatingDeclarationUpdateRequestDataAttributesSchema: ObjectSchema = ageRatingDeclarationUpdateRequestDataSchema.subSchemas.compactMap({
guard case .objectSchema(let subSchema) = $0, subSchema.name == "Attributes" else { return nil }
return subSchema
}).first,
let kidsAgeBandProperty = ageRatingDeclarationUpdateRequestDataAttributesSchema.properties["kidsAgeBand"] {
ageRatingDeclarationUpdateRequestDataAttributesSchema.properties["kidsAgeBand"] = .init(type: kidsAgeBandProperty.type, deprecated: kidsAgeBandProperty.deprecated, clearable: true)
ageRatingDeclarationUpdateRequestDataSchema.properties["attributes"]?.type = .schema(ageRatingDeclarationUpdateRequestDataAttributesSchema)
ageRatingDeclarationUpdateRequestSchema.properties["data"]?.type = .schema(ageRatingDeclarationUpdateRequestDataSchema)
components.schemas["AgeRatingDeclarationUpdateRequest"] = .object(ageRatingDeclarationUpdateRequestSchema)
patchedSchemas.append(.object(ageRatingDeclarationUpdateRequestSchema))
}
}

/// A wrapper for schemas to ease decoding
Expand Down
Loading

0 comments on commit ce4ad35

Please sign in to comment.