From 9de0bf3245bf4fcd0a761046264210194ffdd93c Mon Sep 17 00:00:00 2001 From: Wendell Date: Mon, 2 Dec 2024 13:33:21 +0800 Subject: [PATCH 1/3] Add Encodable macro tests for enums and diagnostics --- .../CodableMacroTests+class+inheritance.swift | 605 ++++++++++++++++++ .../CodableMacroTests+class.swift | 591 +++++++++++++++++ .../CodableMacroTests+diagnostics.swift | 268 ++++++++ .../CodableMacroTests+enum.swift | 147 +++++ .../CodableMacroTests+struct.swift | 593 +++++++++++++++++ Tests/EncodableKitTests/Defines.swift | 23 + 6 files changed, 2227 insertions(+) create mode 100644 Tests/EncodableKitTests/CodableMacroTests+class+inheritance.swift create mode 100644 Tests/EncodableKitTests/CodableMacroTests+class.swift create mode 100644 Tests/EncodableKitTests/CodableMacroTests+diagnostics.swift create mode 100644 Tests/EncodableKitTests/CodableMacroTests+enum.swift create mode 100644 Tests/EncodableKitTests/CodableMacroTests+struct.swift create mode 100644 Tests/EncodableKitTests/Defines.swift diff --git a/Tests/EncodableKitTests/CodableMacroTests+class+inheritance.swift b/Tests/EncodableKitTests/CodableMacroTests+class+inheritance.swift new file mode 100644 index 0000000..21a3513 --- /dev/null +++ b/Tests/EncodableKitTests/CodableMacroTests+class+inheritance.swift @@ -0,0 +1,605 @@ +// +// CodableKitTestsForStruct.swift +// CodableKit +// +// Created by Wendell Wang on 2024/8/16. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForSubClass: XCTestCase { + func testMacros() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDefaultValue() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int = 24 + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int = 24 + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithCodableKeyAndDefaultValue() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + @CodableKey("currentAge") + var age: Int = 24 + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int = 24 + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age = "currentAge" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOptionalValue() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .ignored) + let thisPropertyWillBeIgnored: String + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + let thisPropertyWillBeIgnored: String + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithExplicitNil() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .explicitNil) + let explicitNil: String? + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + let explicitNil: String? + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + try container.encode(explicitNil, forKey: .explicitNil) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case explicitNil + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOneCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + let age: Int + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithTwoCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Encodable + public class User: MetaUser { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + @CodableKey("givenName", options: .generateCustomKey) + let name: String + let age: Int + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + + internal var givenName: String { + name + } + let age: Int + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name = "givenName" + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawString() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .transcodeRawString) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + let room: Room + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + let room: Room + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringWithDefaultValueAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + var room: Room = Room(id: UUID(), name: "Hello") + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + var room: Room = Room(id: UUID(), name: "Hello") + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithOptionUseDefaultOnFailure() throws { + + assertMacroExpansion( + """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + @Encodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .useDefaultOnFailure) + var role: Role = .unknown + } + """, + expandedSource: """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + var role: Role = .unknown + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try container.encode(role, forKey: .role) + try super.encode(to: encoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case role + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/EncodableKitTests/CodableMacroTests+class.swift b/Tests/EncodableKitTests/CodableMacroTests+class.swift new file mode 100644 index 0000000..2ad1bf6 --- /dev/null +++ b/Tests/EncodableKitTests/CodableMacroTests+class.swift @@ -0,0 +1,591 @@ +// +// CodableKitTestsForStruct.swift +// CodableKit +// +// Created by Wendell Wang on 2024/8/16. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForClass: XCTestCase { + func testMacros() throws { + assertMacroExpansion( + """ + @Encodable + public class User { + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + let age: Int + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + } + + func testMacroWithDefaultValue() throws { + + assertMacroExpansion( + """ + @Encodable + public class User { + let id: UUID + let name: String + var age: Int = 24 + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int = 24 + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithCodableKeyAndDefaultValue() throws { + + assertMacroExpansion( + """ + @Encodable + public class User { + let id: UUID + let name: String + @CodableKey("currentAge") + var age: Int = 24 + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int = 24 + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age = "currentAge" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOptionalValue() throws { + + assertMacroExpansion( + """ + @Encodable + public class User { + let id: UUID + let name: String + var age: Int? = 24 + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int? = 24 + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Encodable + public class User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .ignored) + let thisPropertyWillBeIgnored: String + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int? = 24 + let thisPropertyWillBeIgnored: String + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithExplicitNil() throws { + + assertMacroExpansion( + """ + @Encodable + public class User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .explicitNil) + let explicitNil: String? + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int? = 24 + let explicitNil: String? + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + try container.encode(explicitNil, forKey: .explicitNil) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case explicitNil + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOneCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Encodable + public class User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + let age: Int + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithTwoCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Encodable + public class User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + @CodableKey("givenName", options: .generateCustomKey) + let name: String + let age: Int + } + """, + expandedSource: """ + public class User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + + internal var givenName: String { + name + } + let age: Int + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name = "givenName" + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawString() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .transcodeRawString) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User { + let id: UUID + let name: String + let age: Int + let room: Room + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User { + let id: UUID + let name: String + let age: Int + let room: Room + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringWithDefaultValueAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + var room: Room = Room(id: UUID(), name: "Hello") + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User { + let id: UUID + let name: String + let age: Int + var room: Room = Room(id: UUID(), name: "Hello") + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithOptionUseDefaultOnFailure() throws { + + assertMacroExpansion( + """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + @Encodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .useDefaultOnFailure) + var role: Role = .unknown + } + """, + expandedSource: """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + public class User { + let id: UUID + let name: String + let age: Int + var role: Role = .unknown + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try container.encode(role, forKey: .role) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case role + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/EncodableKitTests/CodableMacroTests+diagnostics.swift b/Tests/EncodableKitTests/CodableMacroTests+diagnostics.swift new file mode 100644 index 0000000..7104e93 --- /dev/null +++ b/Tests/EncodableKitTests/CodableMacroTests+diagnostics.swift @@ -0,0 +1,268 @@ +// +// CodableMacroTests+Diagnostics.swift +// CodableKit +// +// Created by WendellXY on 2024/5/27 +// Copyright © 2024 WendellXY. All rights reserved. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitDiagnosticsTests: XCTestCase { + func testMacroWithNoTypeAnnotation() throws { + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age = 24 + } + """, + diagnostics: [ + .init(message: "Properties must have a type annotation", line: 1, column: 1) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + } + + func testMacroWithIgnoredPropertyTypeAnnotation() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .ignored) + var ignored: String = "Hello World" + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + let age: Int + var ignored: String = "Hello World" + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithStaticTypeAnnotation() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + let age: Int + + static let staticProperty = "Hello World" + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + let age: Int + + static let staticProperty = "Hello World" + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroOnComputeProperty() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + @CodableKey("hello") + var address: String { + "A" + } + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + var address: String { + "A" + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + diagnostics: [ + .init(message: "Only variable declarations with no accessor block are supported", line: 6, column: 3) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroOnStaticComputeProperty() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + @CodableKey("hello") + static var address: String { + "A" + } + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + static var address: String { + "A" + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + diagnostics: [ + .init(message: "Only variable declarations with no accessor block are supported", line: 6, column: 3) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroOnStaticProperty() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + @CodableKey("hello") + static var address: String = "A" + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + static var address: String = "A" + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + diagnostics: [ + .init(message: "Only non-static variable declarations are supported", line: 6, column: 3) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/EncodableKitTests/CodableMacroTests+enum.swift b/Tests/EncodableKitTests/CodableMacroTests+enum.swift new file mode 100644 index 0000000..82e88e3 --- /dev/null +++ b/Tests/EncodableKitTests/CodableMacroTests+enum.swift @@ -0,0 +1,147 @@ +// +// CodableMacroTests+enum.swift +// CodableKit +// +// Created by Wendell Wang on 2024/11/22. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForEnum: XCTestCase { + func testMacros() throws { + + assertMacroExpansion( + """ + @Encodable + public enum TestEnum { + case string(String) + case int(Int) + case none + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + } + + extension TestEnum: Encodable { + enum CodingKeys: String, CodingKey { + case string + case int + case none + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithCodableKey() throws { + + assertMacroExpansion( + """ + @Encodable + public enum TestEnum { + @CodableKey("str") case string(String) + case int(Int) + @CodableKey("empty") case none + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + } + + extension TestEnum: Encodable { + enum CodingKeys: String, CodingKey { + case string = "str" + case int + case none = "empty" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Encodable + public enum TestEnum { + @CodableKey("str") case string(String) + case int(Int) + @CodableKey(options: .ignored) case none + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + } + + extension TestEnum: Encodable { + enum CodingKeys: String, CodingKey { + case string = "str" + case int + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithIndirectCase() throws { + + assertMacroExpansion( + """ + @Encodable + public enum TestEnum { + @CodableKey("str") case string(String) + case int(Int) + @CodableKey("empty") case none + indirect case nestedA(TestEnum) + @CodableKey("b") indirect case nestedB(TestEnum) + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + indirect case nestedA(TestEnum) + indirect case nestedB(TestEnum) + } + + extension TestEnum: Encodable { + enum CodingKeys: String, CodingKey { + case string = "str" + case int + case none = "empty" + case nestedA + case nestedB = "b" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/EncodableKitTests/CodableMacroTests+struct.swift b/Tests/EncodableKitTests/CodableMacroTests+struct.swift new file mode 100644 index 0000000..160c25c --- /dev/null +++ b/Tests/EncodableKitTests/CodableMacroTests+struct.swift @@ -0,0 +1,593 @@ +// +// CodableMacroTests.swift +// CodableKitTests +// +// Created by Wendell on 3/30/24. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForStruct: XCTestCase { + func testMacros() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + let age: Int + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDefaultValue() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithCodableKeyAndDefaultValue() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + @CodableKey("currentAge") + var age: Int = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age = "currentAge" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOptionalValue() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .ignored) + let thisPropertyWillBeIgnored: String + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + let thisPropertyWillBeIgnored: String + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithExplicitNil() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .explicitNil) + let explicitNil: String? + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + let explicitNil: String? + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(age, forKey: .age) + try container.encode(explicitNil, forKey: .explicitNil) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case explicitNil + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOneCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public struct User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + let age: Int + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithTwoCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Encodable + public struct User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + @CodableKey("givenName", options: .generateCustomKey) + let name: String + let age: Int + } + """, + expandedSource: """ + public struct User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + + internal var givenName: String { + name + } + let age: Int + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name = "givenName" + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawString() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .transcodeRawString) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public struct User { + let id: UUID + let name: String + let age: Int + let room: Room + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public struct User { + let id: UUID + let name: String + let age: Int + let room: Room + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringWithDefaultValueAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Encodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + var room: Room = Room(id: UUID(), name: "Hello") + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public struct User { + let id: UUID + let name: String + let age: Int + var room: Room = Room(id: UUID(), name: "Hello") + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + let roomRawData = try JSONEncoder().encode(room) + if let roomRawString = String(data: roomRawData, encoding: .utf8) { + try container.encode(roomRawString, forKey: .room) + } else { + throw EncodingError.invalidValue( + roomRawData, + EncodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to transcode raw data to string" + ) + ) + } + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithOptionUseDefaultOnFailure() throws { + + assertMacroExpansion( + """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + @Encodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .useDefaultOnFailure) + var role: Role = .unknown + } + """, + expandedSource: """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + public struct User { + let id: UUID + let name: String + let age: Int + var role: Role = .unknown + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(age, forKey: .age) + try container.encode(role, forKey: .role) + } + } + + extension User: Encodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case role + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/EncodableKitTests/Defines.swift b/Tests/EncodableKitTests/Defines.swift new file mode 100644 index 0000000..884ccc8 --- /dev/null +++ b/Tests/EncodableKitTests/Defines.swift @@ -0,0 +1,23 @@ +// +// Defines.swift +// CodableKit +// +// Created by WendellXY on 2024/5/27 +// Copyright © 2024 WendellXY. All rights reserved. +// + +import CodableKitMacros +import SwiftSyntax +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport + +let macros: [String: any Macro.Type] = [ + "Codable": CodableMacro.self, + "CodableKey": CodableKeyMacro.self, +] + +let macroSpecs: [String: MacroSpec] = [ + "Encodable": MacroSpec(type: CodableMacro.self, conformances: ["Encodable"]), + "CodableKey": MacroSpec(type: CodableKeyMacro.self), +] From 85d9fba18880426494a5dbb31067a2ceba8b3f46 Mon Sep 17 00:00:00 2001 From: Wendell Date: Mon, 2 Dec 2024 14:16:38 +0800 Subject: [PATCH 2/3] Add Decodable macro tests for class inheritance --- .../CodableMacroTests+class+inheritance.swift | 605 ++++++++++++++++++ .../CodableMacroTests+class.swift | 591 +++++++++++++++++ .../CodableMacroTests+diagnostics.swift | 269 ++++++++ .../CodableMacroTests+enum.swift | 148 +++++ .../CodableMacroTests+struct.swift | 593 +++++++++++++++++ Tests/DecodableKitTests/Defines.swift | 23 + 6 files changed, 2229 insertions(+) create mode 100644 Tests/DecodableKitTests/CodableMacroTests+class+inheritance.swift create mode 100644 Tests/DecodableKitTests/CodableMacroTests+class.swift create mode 100644 Tests/DecodableKitTests/CodableMacroTests+diagnostics.swift create mode 100644 Tests/DecodableKitTests/CodableMacroTests+enum.swift create mode 100644 Tests/DecodableKitTests/CodableMacroTests+struct.swift create mode 100644 Tests/DecodableKitTests/Defines.swift diff --git a/Tests/DecodableKitTests/CodableMacroTests+class+inheritance.swift b/Tests/DecodableKitTests/CodableMacroTests+class+inheritance.swift new file mode 100644 index 0000000..df42b30 --- /dev/null +++ b/Tests/DecodableKitTests/CodableMacroTests+class+inheritance.swift @@ -0,0 +1,605 @@ +// +// CodableKitTestsForStruct.swift +// CodableKit +// +// Created by Wendell Wang on 2024/8/16. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForSubClass: XCTestCase { + func testMacros() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDefaultValue() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int = 24 + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int = 24 + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithCodableKeyAndDefaultValue() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + @CodableKey("currentAge") + var age: Int = 24 + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int = 24 + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age = "currentAge" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOptionalValue() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .ignored) + let thisPropertyWillBeIgnored: String + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + let thisPropertyWillBeIgnored: String + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithExplicitNil() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .explicitNil) + let explicitNil: String? + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + let name: String + var age: Int? = 24 + let explicitNil: String? + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + explicitNil = try container.decodeIfPresent(String?.self, forKey: .explicitNil) ?? nil + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case explicitNil + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOneCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + let age: Int + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithTwoCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Decodable + public class User: MetaUser { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + @CodableKey("givenName", options: .generateCustomKey) + let name: String + let age: Int + } + """, + expandedSource: """ + public class User: MetaUser { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + + internal var givenName: String { + name + } + let age: Int + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name = "givenName" + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawString() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .transcodeRawString) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + let room: Room + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = try container.decodeIfPresent(String.self, forKey: .room) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = try JSONDecoder().decode(Room.self, from: roomRawData) + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + let room: Room + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = (try? container.decodeIfPresent(String.self, forKey: .room)) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = try JSONDecoder().decode(Room.self, from: roomRawData) + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringWithDefaultValueAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + var room: Room = Room(id: UUID(), name: "Hello") + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + var room: Room = Room(id: UUID(), name: "Hello") + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = (try? container.decodeIfPresent(String.self, forKey: .room)) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = (try? JSONDecoder().decode(Room.self, from: roomRawData)) ?? Room(id: UUID(), name: "Hello") + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithOptionUseDefaultOnFailure() throws { + + assertMacroExpansion( + """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + @Decodable + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .useDefaultOnFailure) + var role: Role = .unknown + } + """, + expandedSource: """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + public class User: MetaUser { + let id: UUID + let name: String + let age: Int + var role: Role = .unknown + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + role = (try? container.decodeIfPresent(Role.self, forKey: .role)) ?? .unknown + try super.init(from: decoder) + } + } + + extension User { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case role + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/DecodableKitTests/CodableMacroTests+class.swift b/Tests/DecodableKitTests/CodableMacroTests+class.swift new file mode 100644 index 0000000..f03901e --- /dev/null +++ b/Tests/DecodableKitTests/CodableMacroTests+class.swift @@ -0,0 +1,591 @@ +// +// CodableKitTestsForStruct.swift +// CodableKit +// +// Created by Wendell Wang on 2024/8/16. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForClass: XCTestCase { + func testMacros() throws { + assertMacroExpansion( + """ + @Decodable + public class User { + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + let age: Int + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + } + + func testMacroWithDefaultValue() throws { + + assertMacroExpansion( + """ + @Decodable + public class User { + let id: UUID + let name: String + var age: Int = 24 + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int = 24 + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithCodableKeyAndDefaultValue() throws { + + assertMacroExpansion( + """ + @Decodable + public class User { + let id: UUID + let name: String + @CodableKey("currentAge") + var age: Int = 24 + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int = 24 + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age = "currentAge" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOptionalValue() throws { + + assertMacroExpansion( + """ + @Decodable + public class User { + let id: UUID + let name: String + var age: Int? = 24 + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int? = 24 + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Decodable + public class User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .ignored) + let thisPropertyWillBeIgnored: String + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int? = 24 + let thisPropertyWillBeIgnored: String + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithExplicitNil() throws { + + assertMacroExpansion( + """ + @Decodable + public class User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .explicitNil) + let explicitNil: String? + } + """, + expandedSource: """ + public class User { + let id: UUID + let name: String + var age: Int? = 24 + let explicitNil: String? + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + explicitNil = try container.decodeIfPresent(String?.self, forKey: .explicitNil) ?? nil + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case explicitNil + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOneCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Decodable + public class User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public class User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + let age: Int + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithTwoCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Decodable + public class User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + @CodableKey("givenName", options: .generateCustomKey) + let name: String + let age: Int + } + """, + expandedSource: """ + public class User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + + internal var givenName: String { + name + } + let age: Int + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name = "givenName" + case age + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawString() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .transcodeRawString) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User { + let id: UUID + let name: String + let age: Int + let room: Room + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = try container.decodeIfPresent(String.self, forKey: .room) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = try JSONDecoder().decode(Room.self, from: roomRawData) + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User { + let id: UUID + let name: String + let age: Int + let room: Room + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = (try? container.decodeIfPresent(String.self, forKey: .room)) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = try JSONDecoder().decode(Room.self, from: roomRawData) + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringWithDefaultValueAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + var room: Room = Room(id: UUID(), name: "Hello") + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public class User { + let id: UUID + let name: String + let age: Int + var room: Room = Room(id: UUID(), name: "Hello") + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = (try? container.decodeIfPresent(String.self, forKey: .room)) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = (try? JSONDecoder().decode(Room.self, from: roomRawData)) ?? Room(id: UUID(), name: "Hello") + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithOptionUseDefaultOnFailure() throws { + + assertMacroExpansion( + """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + @Decodable + public class User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .useDefaultOnFailure) + var role: Role = .unknown + } + """, + expandedSource: """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + public class User { + let id: UUID + let name: String + let age: Int + var role: Role = .unknown + + public required init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + role = (try? container.decodeIfPresent(Role.self, forKey: .role)) ?? .unknown + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case role + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/DecodableKitTests/CodableMacroTests+diagnostics.swift b/Tests/DecodableKitTests/CodableMacroTests+diagnostics.swift new file mode 100644 index 0000000..7f61c6f --- /dev/null +++ b/Tests/DecodableKitTests/CodableMacroTests+diagnostics.swift @@ -0,0 +1,269 @@ +// +// +// CodableMacroTests+Diagnostics.swift +// CodableKit +// +// Created by WendellXY on 2024/5/27 +// Copyright © 2024 WendellXY. All rights reserved. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitDiagnosticsTests: XCTestCase { + func testMacroWithNoTypeAnnotation() throws { + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age = 24 + } + """, + diagnostics: [ + .init(message: "Properties must have a type annotation", line: 1, column: 1) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + } + + func testMacroWithIgnoredPropertyTypeAnnotation() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .ignored) + var ignored: String = "Hello World" + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + let age: Int + var ignored: String = "Hello World" + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithStaticTypeAnnotation() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + let age: Int + + static let staticProperty = "Hello World" + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + let age: Int + + static let staticProperty = "Hello World" + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroOnComputeProperty() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + @CodableKey("hello") + var address: String { + "A" + } + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + var address: String { + "A" + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + } + } + """, + diagnostics: [ + .init(message: "Only variable declarations with no accessor block are supported", line: 6, column: 3) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroOnStaticComputeProperty() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + @CodableKey("hello") + static var address: String { + "A" + } + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + static var address: String { + "A" + } + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + } + } + """, + diagnostics: [ + .init(message: "Only variable declarations with no accessor block are supported", line: 6, column: 3) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroOnStaticProperty() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + @CodableKey("hello") + static var address: String = "A" + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + static var address: String = "A" + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + } + } + """, + diagnostics: [ + .init(message: "Only non-static variable declarations are supported", line: 6, column: 3) + ], + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/DecodableKitTests/CodableMacroTests+enum.swift b/Tests/DecodableKitTests/CodableMacroTests+enum.swift new file mode 100644 index 0000000..736cc41 --- /dev/null +++ b/Tests/DecodableKitTests/CodableMacroTests+enum.swift @@ -0,0 +1,148 @@ +// +// CodableMacroTests+enum.swift +// CodableKit +// +// Created by Wendell Wang on 2024/11/22. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForEnum: XCTestCase { + func testMacros() throws { + + assertMacroExpansion( + """ + @Decodable + public enum TestEnum { + case string(String) + case int(Int) + case none + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + } + + extension TestEnum: Decodable { + enum CodingKeys: String, CodingKey { + case string + case int + case none + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithCodableKey() throws { + + assertMacroExpansion( + """ + @Decodable + public enum TestEnum { + @CodableKey("str") case string(String) + case int(Int) + @CodableKey("empty") case none + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + } + + extension TestEnum: Decodable { + enum CodingKeys: String, CodingKey { + case string = "str" + case int + case none = "empty" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Decodable + public enum TestEnum { + @CodableKey("str") case string(String) + case int(Int) + @CodableKey(options: .ignored) case none + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + } + + extension TestEnum: Decodable { + enum CodingKeys: String, CodingKey { + case string = "str" + case int + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithIndirectCase() throws { + + assertMacroExpansion( + """ + @Decodable + public enum TestEnum { + @CodableKey("str") case string(String) + case int(Int) + @CodableKey("empty") case none + indirect case nestedA(TestEnum) + @CodableKey("b") indirect case nestedB(TestEnum) + } + """, + expandedSource: """ + public enum TestEnum { + case string(String) + case int(Int) + case none + indirect case nestedA(TestEnum) + indirect case nestedB(TestEnum) + } + + extension TestEnum: Decodable { + enum CodingKeys: String, CodingKey { + case string = "str" + case int + case none = "empty" + case nestedA + case nestedB = "b" + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/DecodableKitTests/CodableMacroTests+struct.swift b/Tests/DecodableKitTests/CodableMacroTests+struct.swift new file mode 100644 index 0000000..b1df834 --- /dev/null +++ b/Tests/DecodableKitTests/CodableMacroTests+struct.swift @@ -0,0 +1,593 @@ +// +// CodableMacroTests.swift +// CodableKitTests +// +// Created by Wendell on 3/30/24. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CodableKitTestsForStruct: XCTestCase { + func testMacros() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + let age: Int + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDefaultValue() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age: Int = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithCodableKeyAndDefaultValue() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + @CodableKey("currentAge") + var age: Int = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int = 24 + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age = "currentAge" + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 24 + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOptionalValue() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithIgnoredCodableKey() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .ignored) + let thisPropertyWillBeIgnored: String + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + let thisPropertyWillBeIgnored: String + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithExplicitNil() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + @CodableKey(options: .explicitNil) + let explicitNil: String? + } + """, + expandedSource: """ + public struct User { + let id: UUID + let name: String + var age: Int? = 24 + let explicitNil: String? + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case explicitNil + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decodeIfPresent(Int?.self, forKey: .age) ?? 24 + explicitNil = try container.decodeIfPresent(String?.self, forKey: .explicitNil) ?? nil + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithOneCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + let name: String + let age: Int + } + """, + expandedSource: """ + public struct User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + let age: Int + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithTwoCustomKeyGenerated() throws { + + assertMacroExpansion( + """ + @Decodable + public struct User { + @CodableKey("uid", options: .generateCustomKey) + let id: UUID + @CodableKey("givenName", options: .generateCustomKey) + let name: String + let age: Int + } + """, + expandedSource: """ + public struct User { + let id: UUID + + internal var uid: UUID { + id + } + let name: String + + internal var givenName: String { + name + } + let age: Int + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id = "uid" + case name = "givenName" + case age + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawString() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .transcodeRawString) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public struct User { + let id: UUID + let name: String + let age: Int + let room: Room + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = try container.decodeIfPresent(String.self, forKey: .room) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = try JSONDecoder().decode(Room.self, from: roomRawData) + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + let room: Room + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public struct User { + let id: UUID + let name: String + let age: Int + let room: Room + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = (try? container.decodeIfPresent(String.self, forKey: .room)) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = try JSONDecoder().decode(Room.self, from: roomRawData) + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacroWithDecodingRawStringWithDefaultValueAndIgnoreError() throws { + + assertMacroExpansion( + """ + struct Room: Codable { + let id: UUID + let name: String + } + @Decodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: [.useDefaultOnFailure, .transcodeRawString]) + var room: Room = Room(id: UUID(), name: "Hello") + } + """, + expandedSource: """ + struct Room: Codable { + let id: UUID + let name: String + } + public struct User { + let id: UUID + let name: String + let age: Int + var room: Room = Room(id: UUID(), name: "Hello") + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case room + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + let roomRawString = (try? container.decodeIfPresent(String.self, forKey: .room)) ?? "" + if let roomRawData = roomRawString.data(using: .utf8) { + room = (try? JSONDecoder().decode(Room.self, from: roomRawData)) ?? Room(id: UUID(), name: "Hello") + } else { + throw DecodingError.valueNotFound( + String.self, + DecodingError.Context( + codingPath: [CodingKeys.room], + debugDescription: "Failed to convert raw string to data" + ) + ) + } + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } + + func testMacrosWithOptionUseDefaultOnFailure() throws { + + assertMacroExpansion( + """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + @Decodable + public struct User { + let id: UUID + let name: String + let age: Int + @CodableKey(options: .useDefaultOnFailure) + var role: Role = .unknown + } + """, + expandedSource: """ + enum Role: UInt8, Codable { + case unknown = 255 + case admin = 0 + case user = 1 + } + public struct User { + let id: UUID + let name: String + let age: Int + var role: Role = .unknown + } + + extension User: Decodable { + enum CodingKeys: String, CodingKey { + case id + case name + case age + case role + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + age = try container.decode(Int.self, forKey: .age) + role = (try? container.decodeIfPresent(Role.self, forKey: .role)) ?? .unknown + } + } + """, + macroSpecs: macroSpecs, + indentationWidth: .spaces(2) + ) + + } +} diff --git a/Tests/DecodableKitTests/Defines.swift b/Tests/DecodableKitTests/Defines.swift new file mode 100644 index 0000000..07f4e33 --- /dev/null +++ b/Tests/DecodableKitTests/Defines.swift @@ -0,0 +1,23 @@ +// +// Defines.swift +// CodableKit +// +// Created by WendellXY on 2024/5/27 +// Copyright © 2024 WendellXY. All rights reserved. +// + +import CodableKitMacros +import SwiftSyntax +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport + +let macros: [String: any Macro.Type] = [ + "Codable": CodableMacro.self, + "CodableKey": CodableKeyMacro.self, +] + +let macroSpecs: [String: MacroSpec] = [ + "Decodable": MacroSpec(type: CodableMacro.self, conformances: ["Decodable"]), + "CodableKey": MacroSpec(type: CodableKeyMacro.self), +] From f959e97babc5034411d6fa7d80667ddf751d2e8c Mon Sep 17 00:00:00 2001 From: Wendell Date: Mon, 2 Dec 2024 14:17:22 +0800 Subject: [PATCH 3/3] Add test targets for Decodable & Encodable macro --- Package.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Package.swift b/Package.swift index 2b647e9..aede2f0 100644 --- a/Package.swift +++ b/Package.swift @@ -49,6 +49,24 @@ let package = Package( .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] ), + .testTarget( + name: "DecodableKitTests", + dependencies: [ + "CodableKitShared", + "CodableKitMacros", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + .testTarget( + name: "EncodableKitTests", + dependencies: [ + "CodableKitShared", + "CodableKitMacros", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), ] )