Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial addition and single file implementation for Swift-Testing #58

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/multiplatform-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Build
run: swift build -v
run: swift build -v
- name: Run swift tests
run: swift test -v
run: swift test --enable-experimental-swift-testing -v
9 changes: 6 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,24 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "13.0.0")),
.package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "7.6.0")),
// .package(url: "https://github.com/swiftlang/swift-testing.git", .upToNextMajor(from: "6.0.0")),
// swiftlint is kinda big to pull in and build right now...maybe later
// .package(url: "https://github.com/realm/SwiftLint.git", .upToNextMajor(from: "0.52.0")),
.package(url: "https://github.com/swiftlang/swift-syntax.git", "508.0.0"..."600.0.1"),
// For testing different versions of swift-syntax
// .package(url: "https://github.com/apple/swift-syntax.git", .upToNextMajor(from: "509.0.0"))
// .package(url: "https://github.com/apple/swift-syntax.git", .upToNextMajor(from: "510.0.0"))

],
targets: [
.target(
name: "CodableWrappers",
dependencies: ["CodableWrapperMacros"],
dependencies: ["CodableWrapperMacros",
// .product(name: "Testing", package: "swift-testing"),
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]),
// plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]),
// plugins: [.plugin(name: "SwiftLintPlugin", package: "SwifutLint")]),

.testTarget(
name: "CodableWrappersTests",
Expand Down
49 changes: 31 additions & 18 deletions Sources/CodableWrapperMacros/CodingKeys/CodingKeyTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,35 +82,43 @@ enum CodingKeyCase {
/// custom casing
case custom((String) -> (String))

var separator: String {
var separator: String? {
switch self {
case .noChanges, .camelCase, .flatCase, .pascalCase, .upperCase:
""
case .snakeCase, .camelSnakeCase, .pascalSnakeCase, .screamingSnakeCase:
"_"
case .kebabCase, .camelKebabCase, .pascalKebabCase, .screamingKebabCase:
"-"
case .custom(_):
""
case .custom:
nil
}
}

var caseVariant: CaseVariant? {
switch self {
case .flatCase, .snakeCase, .kebabCase:
.lowerCase
case .camelCase, .camelSnakeCase, .camelKebabCase:
.camelCase
case .pascalCase, .pascalSnakeCase, .pascalKebabCase:
.pascalCase
case .upperCase, .screamingSnakeCase, .screamingKebabCase:
.upperCase
case .custom(_), .noChanges:
nil
}
}

func makeKeyValue(from value: String) -> String {
switch self {
case .noChanges: value
case .camelCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .camelCase)
case .flatCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .lowerCase)
case .pascalCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .pascalCase)
case .upperCase: KeyConverter.plainCaseConverter.convert(value: value, variant: .upperCase)
case .snakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .lowerCase)
case .camelSnakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .camelCase)
case .pascalSnakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .pascalCase)
case .screamingSnakeCase: KeyConverter.snakeCaseConverter.convert(value: value, variant: .upperCase)
case .kebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .lowerCase)
case .camelKebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .camelCase)
case .pascalKebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .pascalCase)
case .screamingKebabCase: KeyConverter.kebabCaseConverter.convert(value: value, variant: .upperCase)
case .custom(let converter): converter(value)
case .noChanges: return value
case .custom(let converter): return converter(value)
default:
guard let keyConverter = KeyConverter(keyCase: self), let caseVariant else {
return value
}
return keyConverter.convert(value: value, variant: caseVariant)
}
}
}
Expand All @@ -131,7 +139,12 @@ struct KeyConverter: Sendable {
init(separator: String) {
self.separator = separator
}


init?(keyCase: CodingKeyCase) {
guard let separator = keyCase.separator else { return nil }
self.init(separator: separator)
}

func convert(value: String, variant: CaseVariant) -> String {
// Remove any special characters at the beginning/end
let isAllCaps = value.isAllCaps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ extension MemberBlockItemListSyntax.Element {
decl.as(VariableDeclSyntax.self)?.attributes.matching(matching: T.self) ?? []
}

func attributeSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
decl.as(VariableDeclSyntax.self)?.attributes.matchingSyntax(matching: T.self) ?? []
}
// Not currently used
// func attributeSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
// decl.as(VariableDeclSyntax.self)?.attributes.matchingSyntax(matching: T.self) ?? []
// }

func attributeSyntax(named name: String) -> AttributeSyntax? {
attribute(named: name)?.as(AttributeSyntax.self)
Expand Down Expand Up @@ -57,14 +58,15 @@ extension AttributeListSyntax {
}
}

func matchingSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
compactMap {
guard let attributeName = $0.identifierName?.trimmingCharacters(in: .whitespacesAndNewlines), let syntax = $0.as(AttributeSyntax.self), let type = T(rawValue: attributeName) else {
return nil
}
return (type, syntax)
}
}
// Not currently used
// func matchingSyntax<T: RawRepresentable>(matching rawType: T.Type) -> [(T, AttributeSyntax)] where T.RawValue == String {
// compactMap {
// guard let attributeName = $0.identifierName?.trimmingCharacters(in: .whitespacesAndNewlines), let syntax = $0.as(AttributeSyntax.self), let type = T(rawValue: attributeName) else {
// return nil
// }
// return (type, syntax)
// }
// }
}
extension AttributeListSyntax.Element {
var identifierName: String? {
Expand Down
7 changes: 2 additions & 5 deletions Sources/CodableWrapperMacros/ErrorHandling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,9 @@ extension AttributeSyntax {
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .codingKeyValueRequired)])
}
let argument = argumentList[argumentList.index(argumentList.startIndex, offsetBy: index)]
guard argument.expression.is(StringLiteralExprSyntax.self) else {
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .mustBeStringLiteral)])
}
// Uses the value in the Macro
// Get the value of the macro
guard let customKeyValue = argument.expression.as(StringLiteralExprSyntax.self) else {
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .codingKeyValueRequired)])
throw DiagnosticsError(diagnostics: [.init(node: self, syntaxError: .mustBeStringLiteral)])
}

return ExprSyntax(customKeyValue)
Expand Down
26 changes: 14 additions & 12 deletions Sources/CodableWrapperMacros/TypeMacroContainers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ struct CodableMacroStructContainer {
}

func attributeNameCanGenerate(name: String?) -> Bool {
switch name {
case .none: false
case CustomCodable.macroName:
true
case CodingKeyPrefix.macroName:
!codableDefined
case CodingKeySuffix.macroName:
attributeNameCanGenerate(name: CodingKeyPrefix.macroName) && codingKeyPrefix == nil
default:
attributeNameCanGenerate(name: CodingKeySuffix.macroName)
&& name == codableAttributes.first?.attributeType.rawValue
}
name == CustomCodable.macroName
// Future work for other attributres being able to generate
// switch name {
// case .none: false
// case CustomCodable.macroName:
// true
// case CodingKeyPrefix.macroName:
// !codableDefined
// case CodingKeySuffix.macroName:
// attributeNameCanGenerate(name: CodingKeyPrefix.macroName) && codingKeyPrefix == nil
// default:
// attributeNameCanGenerate(name: CodingKeySuffix.macroName)
// && name == codableAttributes.first?.attributeType.rawValue
// }
}
}

Expand Down
1 change: 0 additions & 1 deletion Sources/CodableWrappers/StaticCoders/BoolCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public struct BoolAsStringValueProvider: NonConformingBoolValueProvider {
case "true": return true
case "false": return false
default:
print("Failed to convert \(typeValue) to Boolean return nil")
return nil
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodableWrappers/StaticCoders/DateCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ extension DateFormatterStaticDecoder {
let stringValue = try String(from: decoder)

guard let value = dateFormatter.date(from: stringValue) else {
let description = "Expected \(Data.self) but could not convert \(stringValue) to Data"
let description = "Expected \(Date.self) but could not convert \(stringValue) to Date"
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: description))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ public protocol NonConformingDecimalValueProvider {

/// Uses the `ValueProvider` for (de)serialization of a non-conforming `Float`
public struct NonConformingFloatStaticCoder<ValueProvider: NonConformingDecimalValueProvider>: StaticCoder {
private init() { }

public static func decode(from decoder: Decoder) throws -> Float {
guard let stringValue = try? String(from: decoder) else {
return try Float(from: decoder)
Expand Down Expand Up @@ -55,8 +53,6 @@ public struct NonConformingFloatStaticCoder<ValueProvider: NonConformingDecimalV

/// Uses the `ValueProvider` for (de)serialization of a non-conforming `Double`
public struct NonConformingDoubleStaticCoder<ValueProvider: NonConformingDecimalValueProvider>: StaticCoder {
private init() { }

public static func decode(from decoder: Decoder) throws -> Double {
guard let stringValue = try? String(from: decoder) else {
return try Double(from: decoder)
Expand Down
104 changes: 102 additions & 2 deletions Tests/CodableWrapperMacrosTests/CodingKeyMacroErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ final class CodingKeyMacroErrorTests: XCTestCase {
// macros: testMacros)
}

func testThrowsErrorWhenCodingKeyImplemented() throws {
func testThrowsErrorWhenCodingKeysImplemented() throws {
assertMacroExpansion(
"""
@CustomCodable struct TestCodable: Codable {
Expand Down Expand Up @@ -215,6 +215,21 @@ final class CodingKeyMacroErrorTests: XCTestCase {
""",
diagnostics: [.init(error: .mustBeStringLiteral, line: 3, column: 5)],
macros: testMacros)

assertMacroExpansion(
"""
@CustomCodable struct TestCodable: Codable {
@CustomCodingKey(1)
let originalKey: String
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .mustBeStringLiteral, line: 2, column: 5)],
macros: testMacros)
}

func testThrowsDueToEmptyStringCodingKey() throws {
Expand All @@ -234,7 +249,7 @@ final class CodingKeyMacroErrorTests: XCTestCase {
macros: testMacros)
}

func testThrowsDueToNoeCodableMacro() throws {
func testThrowsDueToNoCodableMacro() throws {
assertMacroExpansion(
"""
@SnakeCase struct TestCodable: Codable {
Expand All @@ -250,6 +265,21 @@ final class CodingKeyMacroErrorTests: XCTestCase {
diagnostics: [.init(error: .requiresCodableMacro(macroName: "SnakeCase"), line: 1, column: 1)],
macros: testMacros)

assertMacroExpansion(
"""
@CodingKeySuffix struct TestCodable: Codable {
@CustomCodingKey("")
let originalKey: String
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .requiresCodableMacro(macroName: "CodingKeySuffix"), line: 1, column: 1)],
macros: testMacros)

assertMacroExpansion(
"""
@CodingKeyPrefix struct TestCodable: Codable {
Expand All @@ -265,6 +295,76 @@ final class CodingKeyMacroErrorTests: XCTestCase {
diagnostics: [.init(error: .requiresCodableMacro(macroName: "CodingKeyPrefix"), line: 1, column: 1)],
macros: testMacros)
}

func testThrowsWhenNotStruct() throws {
assertMacroExpansion(
"""
@CustomCodable class TestCodable: Codable {
let originalKey: String
}
""",
expandedSource: """
class TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .canOnlyBeAttachedToStruct(name: "@CustomCodable"), line: 1, column: 1)],
macros: testMacros)

assertMacroExpansion(
"""
@SnakeCase enum TestCodable: Codable {
case test
}
""",
expandedSource: """
enum TestCodable: Codable {
case test
}
""",
diagnostics: [.init(error: .canOnlyBeAttachedToPropertiesAndStructs(name: "@SnakeCase"), line: 1, column: 1)],
macros: testMacros)
}

func testThrowsWithEmptyCodingKey() throws {
assertMacroExpansion(
"""
@CustomCodable @SnakeCase struct TestCodable: Codable {
@CustomCodingKey()
let originalKey: String
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
}
""",
diagnostics: [.init(error: .codingKeyValueRequired, line: 2, column: 5)],
macros: testMacros)
}

func testThrowsWhenAttachedToFunction() throws {
assertMacroExpansion(
"""
@CustomCodable struct TestCodable: Codable {
let originalKey: String
@CustomCodingKey("test")
func test() -> String { "" }
}
""",
expandedSource: """
struct TestCodable: Codable {
let originalKey: String
func test() -> String { "" }

private enum CodingKeys: String, CodingKey {
case originalKey = "originalKey"
}
}
""",
diagnostics: [.init(error: .canOnlyBeAttachedToProperty(name: "@CustomCodingKey"), line: 3, column: 5)],
macros: testMacros)
}
}

#endif
14 changes: 10 additions & 4 deletions Tests/CodableWrapperMacrosTests/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import XCTest
@testable import CodableWrapperMacros

extension DiagnosticSpec {
init(warning: SyntaxWarning, line: Int, column: Int) {
self.init(message: warning.localizedDescription, line: line, column: column, severity: .warning)
init(warning: SyntaxWarning, line: Int, column: Int,
originatorFile: StaticString = #filePath,
originatorLine: UInt = #line) {
self.init(message: warning.localizedDescription, line: line, column: column, severity: .warning,
originatorFile: originatorFile, originatorLine: originatorLine)
}

init(error: SyntaxError, line: Int, column: Int) {
self.init(message: error.localizedDescription, line: line, column: column, severity: .error)
init(error: SyntaxError, line: Int, column: Int,
originatorFile: StaticString = #filePath,
originatorLine: UInt = #line) {
self.init(message: error.localizedDescription, line: line, column: column, severity: .error,
originatorFile: originatorFile, originatorLine: originatorLine)
}
}

Expand Down
Loading
Loading