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

Add enum stubs #71

Merged
merged 1 commit into from
Jun 24, 2024
Merged
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
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
Loading