Skip to content

Commit

Permalink
add EKeyValue, EOptional and comments-reading
Browse files Browse the repository at this point in the history
  • Loading branch information
MahdiBM committed Jul 15, 2024
1 parent 45a316d commit 7b847fc
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 9 deletions.
24 changes: 24 additions & 0 deletions Sources/EnumeratorMacroImpl/Types/EArray.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Mustache
import Foundation

struct EArray<Element> {
let underlying: [Element]
Expand Down Expand Up @@ -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<EKeyValue>(underlying: split)
default:
if let keyValues = self as? EArray<EKeyValue> {
let value = keyValues.first(where: { $0.key == name })?.value
return EOptional(value)
}
return nil
}
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/EnumeratorMacroImpl/Types/ECase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct ECase {
let index: Int
let name: EString
let parameters: EParameters
let comments: EArray<EString>

init(index: Int, from element: EnumCaseElementSyntax) throws {
self.index = index
Expand All @@ -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))
}
}
30 changes: 30 additions & 0 deletions Sources/EnumeratorMacroImpl/Types/EKeyValue.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
90 changes: 90 additions & 0 deletions Sources/EnumeratorMacroImpl/Types/EOptional.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Mustache

enum EOptional<Wrapped> {
case none
case some(Wrapped)

init(_ optional: Optional<Wrapped>) {
switch optional {
case .none:
self = .none
case let .some(value):
self = .some(value)
}
}

func toOptional() -> Optional<Wrapped> {
switch self {
case .none:
return .none
case let .some(value):
return .some(value)
}
}

func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> EOptional<U> {
switch self {
case .none:
return .none
case .some(let wrapped):
return .some(
try transform(wrapped)
)
}
}

func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> EOptional<U> {
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
}
}
}
}
27 changes: 25 additions & 2 deletions Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import Mustache

struct EOptionalsArray<Element> {
fileprivate let underlying: [Element?]
fileprivate let underlying: [EOptional<Element>]

init(underlying: [Element?]) {
self.underlying = underlying.map(EOptional.init)
}

init(underlying: [EOptional<Element>]) {
self.underlying = underlying
}

Expand All @@ -14,7 +18,7 @@ struct EOptionalsArray<Element> {
}

extension EOptionalsArray: Sequence, MustacheSequence {
func makeIterator() -> Array<Element?>.Iterator {
func makeIterator() -> Array<EOptional<Element>>.Iterator {
self.underlying.makeIterator()
}
}
Expand Down Expand Up @@ -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<EKeyValue>(underlying: split)
default:
return nil
}
Expand Down
21 changes: 21 additions & 0 deletions Sources/EnumeratorMacroImpl/Types/EString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
20 changes: 16 additions & 4 deletions Sources/EnumeratorMacroImpl/Types/Utils.swift
Original file line number Diff line number Diff line change
@@ -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<EParameter>:
Expand All @@ -18,7 +20,7 @@ func convertToCustomTypesIfPossible(_ value: Any) -> Any {

enum Elements {
case anys([Any])
case optionalAnys([Any?])
case optionalAnys([EOptional<Any>])
}

private func convertHomogeneousArrayToCustomTypes(_ values: [Any]) -> Elements {
Expand Down Expand Up @@ -58,11 +60,21 @@ private func convertHomogeneousArrayToCustomTypes(_ values: [Any]) -> Elements {
}

private protocol OptionalProtocol {
func asConvertedOptionalAny() -> Optional<Any>
func asConvertedOptionalAny() -> EOptional<Any>
}

extension Optional: OptionalProtocol {
func asConvertedOptionalAny() -> Optional<Any> {
self.map { convertToCustomTypesIfPossible($0) }
func asConvertedOptionalAny() -> EOptional<Any> {
EOptional(self).map {
convertToCustomTypesIfPossible($0)
}
}
}

extension EOptional: OptionalProtocol {
func asConvertedOptionalAny() -> EOptional<Any> {
self.map {
convertToCustomTypesIfPossible($0)
}
}
}
13 changes: 10 additions & 3 deletions Tests/EnumeratorMacroTests/ConvertToCustomTypesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any>.Type, "\(converted); \(convertedType)")
}

func testConvertsToEArray() throws {
let value: [Int] = [1]
let converted = convertToCustomTypesIfPossible(value)
Expand Down Expand Up @@ -51,7 +58,7 @@ final class ConvertToCustomTypesTests: XCTestCase {
let array = try XCTUnwrap(converted as? EOptionalsArray<Any>)
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)")
}
Expand All @@ -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)")
}
Expand Down
45 changes: 45 additions & 0 deletions Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
#"""
Expand Down

0 comments on commit 7b847fc

Please sign in to comment.