From 7b847fce498fe93ecfaea4800e98f85393712246 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Mon, 15 Jul 2024 18:04:42 +0330 Subject: [PATCH] add `EKeyValue`, `EOptional` and comments-reading --- .../EnumeratorMacroImpl/Types/EArray.swift | 24 +++++ Sources/EnumeratorMacroImpl/Types/ECase.swift | 8 ++ .../EnumeratorMacroImpl/Types/EKeyValue.swift | 30 +++++++ .../EnumeratorMacroImpl/Types/EOptional.swift | 90 +++++++++++++++++++ .../Types/EOptionalsArray.swift | 27 +++++- .../EnumeratorMacroImpl/Types/EString.swift | 21 +++++ Sources/EnumeratorMacroImpl/Types/Utils.swift | 20 ++++- .../ConvertToCustomTypesTests.swift | 13 ++- .../EnumeratorMacroTests.swift | 45 ++++++++++ 9 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 Sources/EnumeratorMacroImpl/Types/EKeyValue.swift create mode 100644 Sources/EnumeratorMacroImpl/Types/EOptional.swift diff --git a/Sources/EnumeratorMacroImpl/Types/EArray.swift b/Sources/EnumeratorMacroImpl/Types/EArray.swift index f07945a..0e009ab 100644 --- a/Sources/EnumeratorMacroImpl/Types/EArray.swift +++ b/Sources/EnumeratorMacroImpl/Types/EArray.swift @@ -1,4 +1,5 @@ import Mustache +import Foundation struct EArray { let underlying: [Element] @@ -43,7 +44,30 @@ extension EArray: MustacheTransformable { .joined(separator: ", ") let string = EString(joined) return string + case "keyValues": + let split: [EKeyValue] = self.underlying + .map { String(describing: $0) } + .compactMap { string -> EKeyValue? in + let split = string.split( + separator: ":", + maxSplits: 1 + ).map { + $0.trimmingCharacters(in: .whitespacesAndNewlines) + } + guard split.count > 0 else { + return nil + } + return EKeyValue( + key: EString(split[0]), + value: EString(split.count > 1 ? split[1] : "") + ) + } + return EArray(underlying: split) default: + if let keyValues = self as? EArray { + let value = keyValues.first(where: { $0.key == name })?.value + return EOptional(value) + } return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/ECase.swift b/Sources/EnumeratorMacroImpl/Types/ECase.swift index 89efd11..9c99bb3 100644 --- a/Sources/EnumeratorMacroImpl/Types/ECase.swift +++ b/Sources/EnumeratorMacroImpl/Types/ECase.swift @@ -6,6 +6,7 @@ struct ECase { let index: Int let name: EString let parameters: EParameters + let comments: EArray init(index: Int, from element: EnumCaseElementSyntax) throws { self.index = index @@ -14,5 +15,12 @@ struct ECase { self.parameters = .init( underlying: parameters.map(EParameter.init(parameter:)) ) + let keyValueParts = element.trailingTrivia + .description + .replacingOccurrences(of: "///", with: "") /// remove comment signs + .replacingOccurrences(of: "//", with: "") /// remove comment signs + .split(separator: ";") /// separator of parameters + + self.comments = .init(underlying: keyValueParts.map(EString.init)) } } diff --git a/Sources/EnumeratorMacroImpl/Types/EKeyValue.swift b/Sources/EnumeratorMacroImpl/Types/EKeyValue.swift new file mode 100644 index 0000000..6706871 --- /dev/null +++ b/Sources/EnumeratorMacroImpl/Types/EKeyValue.swift @@ -0,0 +1,30 @@ +import Mustache + +struct EKeyValue { + let key: EString + let value: EString + + init(key: EString, value: EString) { + self.key = key + self.value = value + } +} + +extension EKeyValue: CustomStringConvertible { + var description: String { + "(key: \(key), value: \(value))" + } +} + +extension EKeyValue: MustacheTransformable { + func transform(_ name: String) -> Any? { + switch name { + case "key": + return self.key + case "value": + return self.value + default: + return nil + } + } +} diff --git a/Sources/EnumeratorMacroImpl/Types/EOptional.swift b/Sources/EnumeratorMacroImpl/Types/EOptional.swift new file mode 100644 index 0000000..81cba50 --- /dev/null +++ b/Sources/EnumeratorMacroImpl/Types/EOptional.swift @@ -0,0 +1,90 @@ +import Mustache + +enum EOptional { + case none + case some(Wrapped) + + init(_ optional: Optional) { + switch optional { + case .none: + self = .none + case let .some(value): + self = .some(value) + } + } + + func toOptional() -> Optional { + switch self { + case .none: + return .none + case let .some(value): + return .some(value) + } + } + + func map(_ transform: (Wrapped) throws -> U) rethrows -> EOptional { + switch self { + case .none: + return .none + case .some(let wrapped): + return .some( + try transform(wrapped) + ) + } + } + + func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> EOptional { + switch self { + case .none: + return .none + case .some(let wrapped): + let transformed = try transform(wrapped) + switch transformed { + case let .some(value): + return .some(value) + case .none: + return .none + } + } + } + + static func ?? (lhs: Self, rhs: Wrapped) -> Wrapped { + switch lhs { + case .none: + return rhs + case .some(let wrapped): + return wrapped + } + } +} + +extension EOptional: CustomStringConvertible { + var description: String { + switch self { + case .none: + return "" + case .some(let wrapped): + return String(describing: wrapped) + } + } +} + +extension EOptional: MustacheTransformable { + func transform(_ name: String) -> Any? { + switch self { + case .none: + switch name { + case "bool": + return false + default: + return nil + } + case let .some(value): + if let value = value as? MustacheTransformable { + return value.transform(name) + } else { + return nil + } + } + } +} diff --git a/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift b/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift index cafec4d..8c36410 100644 --- a/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift +++ b/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift @@ -1,9 +1,13 @@ import Mustache struct EOptionalsArray { - fileprivate let underlying: [Element?] + fileprivate let underlying: [EOptional] init(underlying: [Element?]) { + self.underlying = underlying.map(EOptional.init) + } + + init(underlying: [EOptional]) { self.underlying = underlying } @@ -14,7 +18,7 @@ struct EOptionalsArray { } extension EOptionalsArray: Sequence, MustacheSequence { - func makeIterator() -> Array.Iterator { + func makeIterator() -> Array>.Iterator { self.underlying.makeIterator() } } @@ -44,6 +48,25 @@ extension EOptionalsArray: MustacheTransformable { .joined(separator: ", ") let string = EString(joined) return string + case "keyValues": + let split: [EKeyValue] = self.underlying + .compactMap { $0.toOptional().map { String(describing: $0) } } + .compactMap { string -> EKeyValue? in + let split = string.split( + separator: ":", + maxSplits: 1 + ).map { + $0.trimmingCharacters(in: .whitespacesAndNewlines) + } + guard split.count == 2 else { + return nil + } + return EKeyValue( + key: EString(split[0]), + value: EString(split[1]) + ) + } + return EArray(underlying: split) default: return nil } diff --git a/Sources/EnumeratorMacroImpl/Types/EString.swift b/Sources/EnumeratorMacroImpl/Types/EString.swift index c6fa5f3..2b9d5d6 100644 --- a/Sources/EnumeratorMacroImpl/Types/EString.swift +++ b/Sources/EnumeratorMacroImpl/Types/EString.swift @@ -38,6 +38,27 @@ extension EString: MustacheTransformable { return self.convertToCamelCase() case "withParens": return self.isEmpty ? self : "(\(self))" + case "bool": + switch self.lowercased() { + case "true", "1", "yes", "y", "on", "": + return true + default: + return false + } + case "keyValue": + let split = self.split( + separator: ":", + maxSplits: 1 + ).map { + $0.trimmingCharacters(in: .whitespacesAndNewlines) + } + guard split.count > 0 else { + return nil + } + return EKeyValue( + key: EString(split[0]), + value: EString(split.count > 1 ? split[1] : "") + ) default: return nil } diff --git a/Sources/EnumeratorMacroImpl/Types/Utils.swift b/Sources/EnumeratorMacroImpl/Types/Utils.swift index ff88cc1..b5f5721 100644 --- a/Sources/EnumeratorMacroImpl/Types/Utils.swift +++ b/Sources/EnumeratorMacroImpl/Types/Utils.swift @@ -1,5 +1,7 @@ func convertToCustomTypesIfPossible(_ value: Any) -> Any { switch value { + case let `optional` as OptionalProtocol: + return `optional`.asConvertedOptionalAny() case let string as any StringProtocol: return EString(string.description) case let seq as any Sequence: @@ -18,7 +20,7 @@ func convertToCustomTypesIfPossible(_ value: Any) -> Any { enum Elements { case anys([Any]) - case optionalAnys([Any?]) + case optionalAnys([EOptional]) } private func convertHomogeneousArrayToCustomTypes(_ values: [Any]) -> Elements { @@ -58,11 +60,21 @@ private func convertHomogeneousArrayToCustomTypes(_ values: [Any]) -> Elements { } private protocol OptionalProtocol { - func asConvertedOptionalAny() -> Optional + func asConvertedOptionalAny() -> EOptional } extension Optional: OptionalProtocol { - func asConvertedOptionalAny() -> Optional { - self.map { convertToCustomTypesIfPossible($0) } + func asConvertedOptionalAny() -> EOptional { + EOptional(self).map { + convertToCustomTypesIfPossible($0) + } + } +} + +extension EOptional: OptionalProtocol { + func asConvertedOptionalAny() -> EOptional { + self.map { + convertToCustomTypesIfPossible($0) + } } } diff --git a/Tests/EnumeratorMacroTests/ConvertToCustomTypesTests.swift b/Tests/EnumeratorMacroTests/ConvertToCustomTypesTests.swift index 77f8ae4..609f600 100644 --- a/Tests/EnumeratorMacroTests/ConvertToCustomTypesTests.swift +++ b/Tests/EnumeratorMacroTests/ConvertToCustomTypesTests.swift @@ -16,6 +16,13 @@ final class ConvertToCustomTypesTests: XCTestCase { XCTAssertTrue(convertedType is EParameters.Type, "\(converted); \(convertedType)") } + func testConvertsToEOptional() { + let value: String? = nil + let converted = convertToCustomTypesIfPossible(value) + let convertedType = type(of: converted) + XCTAssertTrue(convertedType is EOptional.Type, "\(converted); \(convertedType)") + } + func testConvertsToEArray() throws { let value: [Int] = [1] let converted = convertToCustomTypesIfPossible(value) @@ -51,7 +58,7 @@ final class ConvertToCustomTypesTests: XCTestCase { let array = try XCTUnwrap(converted as? EOptionalsArray) var iterator = array.makeIterator() let element = try XCTUnwrap(iterator.next()) - let unwrappedElement = try XCTUnwrap(element) + let unwrappedElement = try XCTUnwrap(element.toOptional()) let unwrappedElementType = type(of: unwrappedElement) XCTAssertTrue(unwrappedElementType is EString.Type, "\(unwrappedElement); \(unwrappedElementType)") } @@ -66,10 +73,10 @@ final class ConvertToCustomTypesTests: XCTestCase { var iterator = array.makeIterator() let element1 = try XCTUnwrap(iterator.next()) - XCTAssertTrue(element1 == nil) + XCTAssertNil(element1.toOptional()) let element2 = try XCTUnwrap(iterator.next()) - let unwrappedElement2 = try XCTUnwrap(element2) + let unwrappedElement2 = try XCTUnwrap(element2.toOptional()) let unwrappedElement2Type = type(of: unwrappedElement2) XCTAssertTrue(unwrappedElement2Type is EString.Type, "\(unwrappedElement2); \(unwrappedElement2Type)") } diff --git a/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift b/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift index a3637e2..4b77691 100644 --- a/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift +++ b/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift @@ -242,6 +242,51 @@ final class EnumeratorMacroTests: XCTestCase { ) } + func testProperlyReadsComments() throws { + assertMacroExpansion( + #""" + @Enumerator(""" + public var isBusinessLogicError: Bool { + switch self { + {{#cases}} + case let .{{name}}: + return {{bool(business_error(keyValues(comments)))}} + {{/cases}} + } + } + """) + public enum ErrorMessage { + case case1 // business_error + case case2 + case somethingSomething(integration: String) + case otherCase(error: Error, isViolation: Bool) // business_error; l8n_params: + } + """#, + expandedSource: #""" + public enum ErrorMessage { + case case1 // business_error + case case2 + case somethingSomething(integration: String) + case otherCase(error: Error, isViolation: Bool) // business_error; l8n_params: + + public var isBusinessLogicError: Bool { + switch self { + case .case1: + return true + case .case2: + return false + case .somethingSomething: + return false + case .otherCase: + return true + } + } + } + """#, + macros: EnumeratorMacroEntryPoint.macros + ) + } + func testDiagnosesNotAnEnum() throws { assertMacroExpansion( #"""