Skip to content

Commit

Permalink
Add enum stubs
Browse files Browse the repository at this point in the history
  • Loading branch information
Kryštof Matěj committed Jun 24, 2024
1 parent c5e4194 commit 5c1555e
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Sources/Rubicon/Domain/EnumDeclaration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public struct EnumDeclaration: Equatable {
public let name: String
public let cases: [String]
public let notes: [String]
public let accessLevel: AccessLevel
}
66 changes: 66 additions & 0 deletions Sources/Rubicon/Generator/EnumStubGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
public protocol EnumStubGenerator {
func generate(from enumType: EnumDeclaration, functionName: String) -> String
}

final class EnumStubGeneratorImpl: EnumStubGenerator {
private let extensionGenerator: ExtensionGenerator
private let functionGenerator: FunctionGenerator
private let indentationGenerator: IndentationGenerator
private let defaultValueGenerator: DefaultValueGenerator

init(
extensionGenerator: ExtensionGenerator,
functionGenerator: FunctionGenerator,
indentationGenerator: IndentationGenerator,
defaultValueGenerator: DefaultValueGenerator
) {
self.extensionGenerator = extensionGenerator
self.functionGenerator = functionGenerator
self.indentationGenerator = indentationGenerator
self.defaultValueGenerator = defaultValueGenerator
}

func generate(from enumType: EnumDeclaration, functionName: String) -> String {
let content = generateBody(from: enumType, functionName: functionName)
return extensionGenerator.make(
name: enumType.name,
content: content
).joined(separator: "\n") + "\n"
}

private func generateBody(from enumType: EnumDeclaration, functionName: String) -> [String] {
let content = makeContent(from: enumType)
let functionDeclaration = FunctionDeclaration(
name: functionName,
arguments: [],
isThrowing: false,
isAsync: false,
isStatic: true,
returnType: TypeDeclaration(name: enumType.name, prefix: [], composedType: .plain)
)
return functionGenerator.makeCode(
from: functionDeclaration,
content: content,
isEachArgumentOnNewLineEnabled: true
)
}

private func makeContent(from enumType: EnumDeclaration) -> [String] {
let firstCase = enumType.cases.first.map { "." + $0 } ?? ""
return [
"return \(firstCase)"
]
}

private func makeAssigment(of variable: VarDeclaration, isLast: Bool) -> String {
return "\(variable.identifier): \(variable.identifier)\(isLast ? "" : ",")"
}

private func makeArgument(from varDeclaration: VarDeclaration) -> ArgumentDeclaration {
ArgumentDeclaration(
name: varDeclaration.identifier,
type: varDeclaration.type,
defaultValue: defaultValueGenerator.makeDefaultValue(for: varDeclaration)
)
}
}
28 changes: 28 additions & 0 deletions Sources/Rubicon/Integration/Rubicon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,32 @@ public final class Rubicon {
)
)
}

public func makeEnumParser() -> EnumParser {
return EnumParserImpl()
}

public func makeEnumGenerator(for configuration: StructStubConfiguration) -> EnumStubGenerator {
let dependencies = makeDependencies(
for: configuration.accessLevel,
indentStep: configuration.indentStep
)
return EnumStubGeneratorImpl(
extensionGenerator: ExtensionGeneratorImpl(
accessLevelGenerator: dependencies.accessLevelGenerator,
indentationGenerator: dependencies.indentationGenerator
),
functionGenerator: FunctionGeneratorImpl(
accessLevelGenerator: dependencies.accessLevelGenerator,
typeGenerator: dependencies.typeGenerator,
argumentGenerator: dependencies.argumentGenerator,
indentationGenerator: dependencies.indentationGenerator
),
indentationGenerator: dependencies.indentationGenerator,
defaultValueGenerator: DefaultValueGeneratorImpl(
unknownDefaultType: configuration.defaultValue,
customDefaultTypes: configuration.customDefaultValues
)
)
}
}
115 changes: 115 additions & 0 deletions Sources/Rubicon/Syntactic analysis/EnumParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import SwiftParser
import SwiftSyntax

public protocol EnumParser {
func parse(text: String) throws -> [EnumDeclaration]
}

final class EnumParserImpl: EnumParser {
func parse(text: String) throws -> [EnumDeclaration] {
let text = SwiftParser.Parser.parse(source: text)
let visitor = EnumVisitor(
nestedInItemsNames: []
)
return visitor.execute(node: text)
}
}

private class EnumVisitor: SyntaxVisitor {
private var result = [EnumDeclaration]()
private var nestedInItemsNames: [String]

public init(
nestedInItemsNames: [String]
) {
self.nestedInItemsNames = nestedInItemsNames
super.init(viewMode: .sourceAccurate)
}

func execute(node: some SyntaxProtocol) -> [EnumDeclaration] {
walk(node)
return result
}

override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {

let name = node.name.text

result.append(
EnumDeclaration(
name: (nestedInItemsNames + [name]).joined(separator: "."),
cases: parseCases(node: node.memberBlock),
notes: node.leadingTrivia.pieces.compactMap(makeLineComment),
accessLevel: parseAccessLevel(from: node)
)
)

let subVisitor = EnumVisitor(nestedInItemsNames: nestedInItemsNames + [name])
result += subVisitor.execute(node: node.memberBlock.members)

return .skipChildren
}

private func parseCases(node: MemberBlockSyntax) -> [String] {
let casesParser = CasesVisitor(viewMode: .sourceAccurate)
return casesParser.execute(node: node.members)
}

private func parseAccessLevel(from node: EnumDeclSyntax) -> AccessLevel {
let modifiers = node.modifiers.map { $0.name.tokenKind }

if modifiers.contains(.keyword(.public)) {
return .public
} else if modifiers.contains(.keyword(.private)) {
return .private
} else {
return .internal
}
}

private func makeLineComment(from triviaPiece: TriviaPiece) -> String? {
switch triviaPiece {
case .lineComment(let text):
text
default:
nil
}
}

private func parseParent(from inheridedTypeSyntax: InheritedTypeSyntax) -> String? {
guard let type = inheridedTypeSyntax.type.as(IdentifierTypeSyntax.self) else {
return nil
}

return type.name.text
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
let name = node.name.text
let subVisitor = EnumVisitor(nestedInItemsNames: nestedInItemsNames + [name])
result += subVisitor.execute(node: node.memberBlock.members)
return .skipChildren
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let name = node.name.text
let subVisitor = EnumVisitor(nestedInItemsNames: nestedInItemsNames + [name])
result += subVisitor.execute(node: node.memberBlock.members)
return .skipChildren
}
}

private class CasesVisitor: SyntaxVisitor {
private var result = [String]()

func execute(node: some SyntaxProtocol) -> [String] {
walk(node)
return result
}

override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind {
let declaration = node.elements.map(\.name.text)
result += declaration
return .visitChildren
}
}
69 changes: 69 additions & 0 deletions Tests/RubiconTests/Generator/EnumStubGeneratorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@testable import Rubicon
import XCTest

final class EnumStubGeneratorTests: XCTestCase {
private var extensionGeneratorSpy: ExtensionGeneratorSpy!
private var functionGeneratorSpy: FunctionGeneratorSpy!
private var indentationGeneratorStub: IndentationGeneratorStub!
private var defaultValueGeneratorSpy: DefaultValueGeneratorSpy!
private var sut: EnumStubGeneratorImpl!

override func setUp() {
super.setUp()
extensionGeneratorSpy = ExtensionGeneratorSpy(makeReturn: ["extension"])
functionGeneratorSpy = FunctionGeneratorSpy(makeCodeReturn: ["function"])
indentationGeneratorStub = IndentationGeneratorStub()
defaultValueGeneratorSpy = DefaultValueGeneratorSpy(makeDefaultValueReturn: "default")
sut = EnumStubGeneratorImpl(
extensionGenerator: extensionGeneratorSpy,
functionGenerator: functionGeneratorSpy,
indentationGenerator: IndentationGeneratorStub(),
defaultValueGenerator: defaultValueGeneratorSpy
)
}

func test_givenEmptyEnum_whenMakeCode_thenReturnCode() {
let declaration = EnumDeclaration.makeStub(cases: [])

let code = sut.generate(from: declaration, functionName: "functionName")

XCTAssertEqual(code, "extension\n")
XCTAssertEqual(extensionGeneratorSpy.make.count, 1)
XCTAssertEqual(extensionGeneratorSpy.make.first?.content, ["function"])
XCTAssertEqual(functionGeneratorSpy.makeCode.count, 1)
XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.name, "functionName")
XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.isThrowing, false)
XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.isAsync, false)
XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.isStatic, true)
XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.arguments.count, 0)
XCTAssertEqual(functionGeneratorSpy.makeCode.first?.declaration.returnType, .makeStub(name: "EnumName"))
XCTAssertEqual(functionGeneratorSpy.makeCode.first?.content, ["return "])
}

func test_givenVariableEnum_whenMakeCode_thenReturnCode() {
let declaration = EnumDeclaration.makeStub(cases: [
"a",
"b"
])

_ = sut.generate(from: declaration, functionName: "functionName")

XCTAssertEqual(functionGeneratorSpy.makeCode.first?.content, ["return .a"])
}
}

extension EnumDeclaration {
static func makeStub(
cases: [String] = []
) -> EnumDeclaration {
return EnumDeclaration(
name: "EnumName",
cases: cases,
notes: [
"note1",
"note2"
],
accessLevel: .internal
)
}
}
Loading

0 comments on commit 5c1555e

Please sign in to comment.