diff --git a/Sources/SyntaxKit/Class.swift b/Sources/SyntaxKit/Class.swift new file mode 100644 index 0000000..36d702c --- /dev/null +++ b/Sources/SyntaxKit/Class.swift @@ -0,0 +1,150 @@ +// +// Class.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `class` declaration. +public struct Class: CodeBlock { + private let name: String + private let members: [CodeBlock] + private var inheritance: [String] = [] + private var genericParameters: [String] = [] + private var isFinal: Bool = false + + /// Creates a `class` declaration. + /// - Parameters: + /// - name: The name of the class. + /// - generics: A list of generic parameters for the class. + /// - content: A ``CodeBlockBuilder`` that provides the members of the class. + public init( + _ name: String, + generics: [String] = [], + @CodeBlockBuilderResult _ content: () -> [CodeBlock] + ) { + self.name = name + self.members = content() + self.genericParameters = generics + } + + /// Sets one or more inherited types (superclass first followed by any protocols). + /// - Parameter types: The list of types to inherit from. + /// - Returns: A copy of the class with the inheritance set. + public func inherits(_ types: String...) -> Self { + var copy = self + copy.inheritance = types + return copy + } + + /// Marks the class declaration as `final`. + /// - Returns: A copy of the class marked as `final`. + public func final() -> Self { + var copy = self + copy.isFinal = true + return copy + } + + public var syntax: SyntaxProtocol { + let classKeyword = TokenSyntax.keyword(.class, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + // Generic parameter clause + var genericParameterClause: GenericParameterClauseSyntax? + if !genericParameters.isEmpty { + let parameterList = GenericParameterListSyntax( + genericParameters.enumerated().map { idx, name in + var param = GenericParameterSyntax(name: .identifier(name)) + if idx < genericParameters.count - 1 { + param = param.with( + \.trailingComma, + TokenSyntax.commaToken(trailingTrivia: .space) + ) + } + return param + } + ) + genericParameterClause = GenericParameterClauseSyntax( + leftAngle: .leftAngleToken(), + parameters: parameterList, + rightAngle: .rightAngleToken() + ) + } + + // Inheritance clause + var inheritanceClause: InheritanceClauseSyntax? + if !inheritance.isEmpty { + let inheritedTypes = inheritance.map { type in + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier(type))) + } + inheritanceClause = InheritanceClauseSyntax( + colon: .colonToken(), + inheritedTypes: InheritedTypeListSyntax( + inheritedTypes.enumerated().map { idx, inherited in + var inheritedType = inherited + if idx < inheritedTypes.count - 1 { + inheritedType = inheritedType.with( + \.trailingComma, + TokenSyntax.commaToken(trailingTrivia: .space) + ) + } + return inheritedType + } + ) + ) + } + + // Member block + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax( + members.compactMap { member in + guard let decl = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberBlockItemSyntax(decl: decl, trailingTrivia: .newline) + } + ), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + // Modifiers + var modifiers: DeclModifierListSyntax = [] + if isFinal { + modifiers = DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(.final, trailingTrivia: .space)) + ]) + } + + return ClassDeclSyntax( + modifiers: modifiers, + classKeyword: classKeyword, + name: identifier, + genericParameterClause: genericParameterClause, + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + } +} diff --git a/Sources/SyntaxKit/Documentation.docc/Documentation.md b/Sources/SyntaxKit/Documentation.docc/Documentation.md index 091cbee..cb4a2ca 100644 --- a/Sources/SyntaxKit/Documentation.docc/Documentation.md +++ b/Sources/SyntaxKit/Documentation.docc/Documentation.md @@ -198,6 +198,14 @@ struct BlackjackCard { - ``VariableDecl`` - ``Let`` - ``Variable`` +- ``Extension`` +- ``Class`` +- ``Protocol`` +- ``Tuple`` +- ``TypeAlias`` +- ``Infix`` +- ``PropertyRequirement`` +- ``FunctionRequirement`` ### Expressions & Statements - ``Assignment`` diff --git a/Sources/SyntaxKit/FunctionRequirement.swift b/Sources/SyntaxKit/FunctionRequirement.swift new file mode 100644 index 0000000..3faf7ae --- /dev/null +++ b/Sources/SyntaxKit/FunctionRequirement.swift @@ -0,0 +1,147 @@ +// +// FunctionRequirement.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A function requirement within a protocol declaration (no body). +public struct FunctionRequirement: CodeBlock { + private let name: String + private let parameters: [Parameter] + private let returnType: String? + private var isStatic: Bool = false + private var isMutating: Bool = false + + /// Creates a parameterless function requirement. + /// - Parameters: + /// - name: The function name. + /// - returnType: Optional return type. + public init(_ name: String, returns returnType: String? = nil) { + self.name = name + self.parameters = [] + self.returnType = returnType + } + + /// Creates a function requirement with parameters. + /// - Parameters: + /// - name: The function name. + /// - returnType: Optional return type. + /// - params: A ParameterBuilderResult providing the parameters. + public init( + _ name: String, returns returnType: String? = nil, + @ParameterBuilderResult _ params: () -> [Parameter] + ) { + self.name = name + self.parameters = params() + self.returnType = returnType + } + + /// Marks the function requirement as `static`. + public func `static`() -> Self { + var copy = self + copy.isStatic = true + return copy + } + + /// Marks the function requirement as `mutating`. + public func mutating() -> Self { + var copy = self + copy.isMutating = true + return copy + } + + public var syntax: SyntaxProtocol { + let funcKeyword = TokenSyntax.keyword(.func, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + // Parameters + let paramList: FunctionParameterListSyntax + if parameters.isEmpty { + paramList = FunctionParameterListSyntax([]) + } else { + paramList = FunctionParameterListSyntax( + parameters.enumerated().compactMap { index, param in + guard !param.name.isEmpty, !param.type.isEmpty else { return nil } + var paramSyntax = FunctionParameterSyntax( + firstName: param.isUnnamed + ? .wildcardToken(trailingTrivia: .space) : .identifier(param.name), + secondName: param.isUnnamed ? .identifier(param.name) : nil, + colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(param.type)), + defaultValue: param.defaultValue.map { + InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier($0))) + ) + } + ) + if index < parameters.count - 1 { + paramSyntax = paramSyntax.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return paramSyntax + }) + } + + // Return clause + var returnClause: ReturnClauseSyntax? + if let returnType = returnType { + returnClause = ReturnClauseSyntax( + arrow: .arrowToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(returnType)) + ) + } + + // Modifiers + var modifiers: DeclModifierListSyntax = [] + if isStatic { + modifiers = DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) + ]) + } + if isMutating { + modifiers = DeclModifierListSyntax( + modifiers + [DeclModifierSyntax(name: .keyword(.mutating, trailingTrivia: .space))] + ) + } + + return FunctionDeclSyntax( + attributes: AttributeListSyntax([]), + modifiers: modifiers, + funcKeyword: funcKeyword, + name: identifier, + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + leftParen: .leftParenToken(), parameters: paramList, rightParen: .rightParenToken() + ), + effectSpecifiers: nil, + returnClause: returnClause + ), + body: nil + ) + } +} diff --git a/Sources/SyntaxKit/PropertyRequirement.swift b/Sources/SyntaxKit/PropertyRequirement.swift new file mode 100644 index 0000000..0a41fb1 --- /dev/null +++ b/Sources/SyntaxKit/PropertyRequirement.swift @@ -0,0 +1,102 @@ +// +// PropertyRequirement.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A property requirement inside a protocol declaration. +public struct PropertyRequirement: CodeBlock { + /// The accessor options for the property. + public enum Access { + case get + case getSet + } + + private let name: String + private let type: String + private let access: Access + + /// Creates a property requirement. + /// - Parameters: + /// - name: The property name. + /// - type: The property type. + /// - access: Whether the property is get-only or get/set. + public init(_ name: String, type: String, access: Access = .get) { + self.name = name + self.type = type + self.access = access + } + + public var syntax: SyntaxProtocol { + let varKeyword = TokenSyntax.keyword(.var, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + let typeAnnotation = TypeAnnotationSyntax( + colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(type)) + ) + + // Build accessor list + let accessorList: AccessorDeclListSyntax = { + switch access { + case .get: + return AccessorDeclListSyntax([ + AccessorDeclSyntax( + accessorSpecifier: .keyword(.get, trailingTrivia: .space) + ) + ]) + case .getSet: + return AccessorDeclListSyntax([ + AccessorDeclSyntax( + accessorSpecifier: .keyword(.get, trailingTrivia: .space) + ), + AccessorDeclSyntax( + accessorSpecifier: .keyword(.set, trailingTrivia: .space) + ), + ]) + } + }() + + let accessorBlock = AccessorBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .space), + accessors: .accessors(accessorList), + rightBrace: .rightBraceToken(leadingTrivia: .space, trailingTrivia: .newline) + ) + + return VariableDeclSyntax( + bindingSpecifier: varKeyword, + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: identifier), + typeAnnotation: typeAnnotation, + accessorBlock: accessorBlock + ) + ]) + ) + } +} diff --git a/Sources/SyntaxKit/Protocol.swift b/Sources/SyntaxKit/Protocol.swift new file mode 100644 index 0000000..71945a8 --- /dev/null +++ b/Sources/SyntaxKit/Protocol.swift @@ -0,0 +1,104 @@ +// +// Protocol.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// A Swift `protocol` declaration. +public struct Protocol: CodeBlock { + private let name: String + private let members: [CodeBlock] + private var inheritance: [String] = [] + + /// Creates a `protocol` declaration. + /// - Parameters: + /// - name: The name of the protocol. + /// - content: A ``CodeBlockBuilder`` that provides the members of the protocol. + public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.name = name + self.members = content() + } + + /// Sets one or more inherited protocols. + /// - Parameter types: The list of protocols this protocol inherits from. + /// - Returns: A copy of the protocol with the inheritance set. + public func inherits(_ types: String...) -> Self { + var copy = self + copy.inheritance = types + return copy + } + + public var syntax: SyntaxProtocol { + let protocolKeyword = TokenSyntax.keyword(.protocol, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(name) + + // Inheritance clause + var inheritanceClause: InheritanceClauseSyntax? + if !inheritance.isEmpty { + let inheritedTypes = inheritance.map { type in + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier(type))) + } + inheritanceClause = InheritanceClauseSyntax( + colon: .colonToken(), + inheritedTypes: InheritedTypeListSyntax( + inheritedTypes.enumerated().map { idx, inherited in + var inheritedType = inherited + if idx < inheritedTypes.count - 1 { + inheritedType = inheritedType.with( + \.trailingComma, + TokenSyntax.commaToken(trailingTrivia: .space) + ) + } + return inheritedType + } + ) + ) + } + + // Member block + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax( + members.compactMap { member in + guard let decl = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberBlockItemSyntax(decl: decl, trailingTrivia: .newline) + } + ), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + return ProtocolDeclSyntax( + protocolKeyword: protocolKeyword, + name: identifier, + primaryAssociatedTypeClause: nil, + inheritanceClause: inheritanceClause, + genericWhereClause: nil, + memberBlock: memberBlock + ) + } +} diff --git a/Tests/SyntaxKitTests/ClassTests.swift b/Tests/SyntaxKitTests/ClassTests.swift new file mode 100644 index 0000000..f9c3836 --- /dev/null +++ b/Tests/SyntaxKitTests/ClassTests.swift @@ -0,0 +1,159 @@ +import Testing + +@testable import SyntaxKit + +struct ClassTests { + @Test func testClassWithInheritance() { + let carClass = Class("Car") { + Variable(.var, name: "brand", type: "String") + Variable(.var, name: "numberOfWheels", type: "Int") + }.inherits("Vehicle") + + let expected = """ + class Car: Vehicle { + var brand: String + var numberOfWheels: Int + } + """ + + let normalizedGenerated = carClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testEmptyClass() { + let emptyClass = Class("EmptyClass") {} + + let expected = """ + class EmptyClass { + } + """ + + let normalizedGenerated = emptyClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithGenerics() { + let genericClass = Class("Container", generics: ["T"]) { + Variable(.var, name: "value", type: "T") + } + + let expected = """ + class Container { + var value: T + } + """ + + let normalizedGenerated = genericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithMultipleGenerics() { + let multiGenericClass = Class("Pair", generics: ["T", "U"]) { + Variable(.var, name: "first", type: "T") + Variable(.var, name: "second", type: "U") + } + + let expected = """ + class Pair { + var first: T + var second: U + } + """ + + let normalizedGenerated = multiGenericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFinalClass() { + let finalClass = Class("FinalClass") { + Variable(.var, name: "value", type: "String") + }.final() + + let expected = """ + final class FinalClass { + var value: String + } + """ + + let normalizedGenerated = finalClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithMultipleInheritance() { + let classWithMultipleInheritance = Class("AdvancedVehicle") { + Variable(.var, name: "speed", type: "Int") + }.inherits("Vehicle", "Codable", "Equatable") + + let expected = """ + class AdvancedVehicle: Vehicle, Codable, Equatable { + var speed: Int + } + """ + + let normalizedGenerated = classWithMultipleInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithGenericsAndInheritance() { + let genericClassWithInheritance = Class("GenericContainer", generics: ["T"]) { + Variable(.var, name: "items", type: "[T]") + }.inherits("Collection") + + let expected = """ + class GenericContainer: Collection { + var items: [T] + } + """ + + let normalizedGenerated = genericClassWithInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFinalClassWithInheritanceAndGenerics() { + let finalGenericClass = Class("FinalGenericClass", generics: ["T"]) { + Variable(.var, name: "value", type: "T") + }.inherits("BaseClass").final() + + let expected = """ + final class FinalGenericClass: BaseClass { + var value: T + } + """ + + let normalizedGenerated = finalGenericClass.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testClassWithFunctions() { + let classWithFunctions = Class("Calculator") { + Function("add", returns: "Int") { + Parameter(name: "a", type: "Int") + Parameter(name: "b", type: "Int") + } _: { + Return { + VariableExp("a + b") + } + } + } + + let expected = """ + class Calculator { + func add(a: Int, b: Int) -> Int { + return a + b + } + } + """ + + let normalizedGenerated = classWithFunctions.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } +} diff --git a/Tests/SyntaxKitTests/ProtocolTests.swift b/Tests/SyntaxKitTests/ProtocolTests.swift new file mode 100644 index 0000000..31a3c55 --- /dev/null +++ b/Tests/SyntaxKitTests/ProtocolTests.swift @@ -0,0 +1,189 @@ +import Testing + +@testable import SyntaxKit + +struct ProtocolTests { + @Test func testSimpleProtocol() { + let vehicleProtocol = Protocol("Vehicle") { + PropertyRequirement("numberOfWheels", type: "Int", access: .get) + PropertyRequirement("brand", type: "String", access: .getSet) + FunctionRequirement("start") + FunctionRequirement("stop") + FunctionRequirement("speed", returns: "Int") + } + + let expected = """ + protocol Vehicle { + var numberOfWheels: Int { get } + var brand: String { get set } + func start() + func stop() + func speed() -> Int + } + """ + + let normalizedGenerated = vehicleProtocol.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testEmptyProtocol() { + let emptyProtocol = Protocol("EmptyProtocol") {} + + let expected = """ + protocol EmptyProtocol { + } + """ + + let normalizedGenerated = emptyProtocol.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testProtocolWithInheritance() { + let protocolWithInheritance = Protocol("MyProtocol") { + PropertyRequirement("value", type: "String", access: .getSet) + }.inherits("Equatable", "Hashable") + + let expected = """ + protocol MyProtocol: Equatable, Hashable { + var value: String { get set } + } + """ + + let normalizedGenerated = protocolWithInheritance.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFunctionRequirementWithParameters() { + let protocolWithFunction = Protocol("Calculator") { + FunctionRequirement("add", returns: "Int") { + Parameter(name: "a", type: "Int") + Parameter(name: "b", type: "Int") + } + } + + let expected = """ + protocol Calculator { + func add(a: Int, b: Int) -> Int + } + """ + + let normalizedGenerated = protocolWithFunction.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testStaticFunctionRequirement() { + let protocolWithStaticFunction = Protocol("Factory") { + FunctionRequirement("create", returns: "Self").static() + } + + let expected = """ + protocol Factory { + static func create() -> Self + } + """ + + let normalizedGenerated = protocolWithStaticFunction.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testMutatingFunctionRequirement() { + let protocolWithMutatingFunction = Protocol("Resettable") { + FunctionRequirement("reset").mutating() + } + + let expected = """ + protocol Resettable { + mutating func reset() + } + """ + + let normalizedGenerated = protocolWithMutatingFunction.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testPropertyRequirementGetOnly() { + let propertyReq = PropertyRequirement("readOnlyProperty", type: "String", access: .get) + let prtcl = Protocol("TestProtocol") { + propertyReq + } + + let expected = """ + protocol TestProtocol { + var readOnlyProperty: String { get } + } + """ + + let normalizedGenerated = prtcl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testPropertyRequirementGetSet() { + let propertyReq = PropertyRequirement("readWriteProperty", type: "Int", access: .getSet) + let prtcl = Protocol("TestProtocol") { + propertyReq + } + + let expected = """ + protocol TestProtocol { + var readWriteProperty: Int { get set } + } + """ + + let normalizedGenerated = prtcl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testFunctionRequirementWithDefaultParameters() { + let functionReq = FunctionRequirement("process", returns: "String") { + Parameter(name: "input", type: "String") + Parameter(name: "options", type: "ProcessingOptions", defaultValue: "ProcessingOptions()") + } + let prtcl = Protocol("TestProtocol") { + functionReq + } + + let expected = """ + protocol TestProtocol { + func process(input: String, options: ProcessingOptions = ProcessingOptions()) -> String + } + """ + + let normalizedGenerated = prtcl.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } + + @Test func testComplexProtocolWithMixedRequirements() { + let complexProtocol = Protocol("ComplexProtocol") { + PropertyRequirement("id", type: "UUID", access: .get) + PropertyRequirement("name", type: "String", access: .getSet) + FunctionRequirement("initialize").mutating() + FunctionRequirement("process", returns: "Result") { + Parameter(name: "input", type: "Data") + } + FunctionRequirement("factory", returns: "Self").static() + }.inherits("Identifiable") + + let expected = """ + protocol ComplexProtocol: Identifiable { + var id: UUID { get } + var name: String { get set } + mutating func initialize() + func process(input: Data) -> Result + static func factory() -> Self + } + """ + + let normalizedGenerated = complexProtocol.generateCode().normalize() + let normalizedExpected = expected.normalize() + #expect(normalizedGenerated == normalizedExpected) + } +}