import Foundation @main enum GiftDecodingContractCheck { static var assertions = 0 static func check(_ condition: Bool, _ message: String) { guard condition else { print("FAIL: \(message)"); exit(1) } assertions += 1 } static func decode(_ type: T.Type, _ json: String) throws -> T { try JSONDecoder().decode(type, from: Data(json.utf8)) } static func main() { let form = #"{"success":true,"message":"Options loaded","data":{"sizes":[{"sizeCode":"S","name":"Small","basePriceCan":100,"salePriceCan":80}],"categories":[{"categoryId":7,"name":"Flowers","requiresDamageWaiver":true}]}}"# let create = #"{"success":true,"message":"Gift created","data":{"applicationNo":"G-1001","status":"CREATED","statusName":"Created","priceCan":80,"trackingDeadlineAt":"2026-10-10T12:00:00Z"}}"# let list = #"{"success":true,"message":"Gifts loaded","data":{"totalCount":1,"items":[{"applicationNo":"G-1001","direction":"SENT","status":"CREATED","statusName":"Created","priceCan":80,"categoryName":"Flowers","sizeName":"Small","counterpartMemberId":42,"counterpartNickname":"Mina","counterpartProfileImageUrl":null,"createdAt":"2026-10-07T12:00:00Z"}],"page":1,"size":20,"hasNext":false}}"# let detail = #"{"success":true,"message":"Gift detail loaded","data":{"applicationNo":"G-1001","direction":"SENT","status":"CREATED","statusName":"Created","giftInfo":{"recipientCreatorNickname":"Mina","senderNickname":"Jae","sizeName":"Small","categoryName":"Flowers","applicationNo":null,"paidCan":80,"tracking":"123456","shippingRequestedAt":null},"senderInfo":{"name":"Jae","phoneNumber":"010-1234-5678","address":"Seoul"},"recipientAddress":null,"mailbox":{"name":"Mina","address":"Busan","phoneNumber":"010-9876-5432"},"trackingRequired":true,"recipientAddressRequired":false,"recipientAddressDeadlineAt":null,"delivery":{"canceledAt":null,"undeliverableAt":null,"undeliverableReason":null},"statusTimeline":[{"status":"CREATED","statusName":"Created","occurredAt":"2026-10-07T12:00:00Z"}]}}"# let options = try! decode(ApiResponse.self, form) check(options.success, "form success") check(options.data?.sizes.first?.sizeCode == "S" && options.data?.sizes.first?.name == "Small", "size fields") check(options.data?.categories.first?.categoryId == 7 && options.data?.categories.first?.name == "Flowers", "category fields") check(options.data?.categories.first?.requiresDamageWaiver == true, "waiver") let created = try! decode(ApiResponse.self, create) check(created.data?.applicationNo == "G-1001" && created.data?.statusName == "Created", "create fields") check(created.data?.priceCan == 80 && created.data?.trackingDeadlineAt == "2026-10-10T12:00:00Z", "create values") let gifts = try! decode(ApiResponse.self, list) check(gifts.data?.totalCount == 1 && gifts.data?.page == 1 && gifts.data?.size == 20 && gifts.data?.hasNext == false, "list paging") check(gifts.data?.items.first?.categoryName == "Flowers" && gifts.data?.items.first?.counterpartNickname == "Mina", "list values") check(gifts.data?.items.first?.counterpartProfileImageUrl == nil, "list nullable profile image") let gift = try! decode(ApiResponse.self, detail).data! check(gift.applicationNo == "G-1001" && gift.giftInfo.categoryName == "Flowers", "detail values") check(gift.applicationNo == "G-1001" && gift.giftInfo.applicationNo == nil, "detail application numbers") check(gift.senderInfo?.name == "Jae" && gift.mailbox?.name == "Mina", "detail nested values") check(gift.recipientAddress == nil && gift.recipientAddressDeadlineAt == nil && gift.giftInfo.shippingRequestedAt == nil, "detail nullable values") check(gift.statusTimeline.first?.statusName == "Created", "timeline status name") for json in [ #"{"success":true,"message":"Gift canceled","data":{"applicationNo":"G-1001","status":"CANCELED","statusName":"Canceled","priceCan":80,"canceledAt":"2026-10-07T13:00:00Z"}}"#, #"{"success":true,"message":"Tracking registered","data":{"applicationNo":"G-1001","status":"TRACKING_REGISTERED","statusName":"Tracking registered","courierCompanyName":"Fast Express","trackingNumber":"123456","trackingRegisteredAt":"2026-10-07T13:00:00Z","recipientAddressDeadlineAt":"2026-10-10T12:00:00Z"}}"#, #"{"success":true,"message":"Recipient address registered","data":{"applicationNo":"G-1001","status":"ADDRESS_REGISTERED","statusName":"Address registered","recipientAddressRegisteredAt":"2026-10-07T14:00:00Z"}}"# ] { check(try! decode(ApiResponseWithoutData.self, json).success, "mutation success envelope") } check(try! decode(ApiResponseWithoutData.self, #"{"success":true}"#).success, "envelope without data") check(!(try! decode(ApiResponseWithoutData.self, #"{"success":false,"data":{"applicationNo":"G-1001"}}"#).success), "false success") let size = try! decode(GiftSizeOptionResponse.self, #"{"sizeCode":"S","name":"Small","basePriceCan":100,"salePriceCan":80}"#) check(size.name == "Small", "direct size.name") let category = try! decode(GiftCategoryOptionResponse.self, #"{"categoryId":7,"name":"Flowers","requiresDamageWaiver":true}"#) check(category.name == "Flowers", "direct category.name") let timeline = try! decode(GiftStatusTimelineResponse.self, #"{"status":"CREATED","statusName":"Created","occurredAt":"2026-10-07T12:00:00Z"}"#) check(timeline.statusName == "Created", "direct timeline.statusName") print("PASS: \(assertions) assertions") } }