diff --git a/Package.swift b/Package.swift index 8bf8f29..fe3937d 100644 --- a/Package.swift +++ b/Package.swift @@ -204,5 +204,10 @@ let package = Package( dependencies: ["SyntaxKit", "DocumentationHarness"], swiftSettings: swiftSettings ), + .testTarget( + name: "AiSTKitTests", + dependencies: ["AiSTKit"], + swiftSettings: swiftSettings + ), ] ) diff --git a/Sources/AiSTKit/AuthenticationMiddleware.swift b/Sources/AiSTKit/AuthenticationMiddleware.swift new file mode 100644 index 0000000..c70641d --- /dev/null +++ b/Sources/AiSTKit/AuthenticationMiddleware.swift @@ -0,0 +1,96 @@ +// +// AuthenticationMiddleware.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +public import HTTPTypes +public import OpenAPIRuntime + +#if os(Linux) + @preconcurrency public import struct Foundation.URL +#else + public import struct Foundation.URL +#endif + +/// OpenAPI client middleware that adds Anthropic API authentication headers. +/// +/// Injects the `x-api-key` and `anthropic-version` headers into every +/// outgoing request before forwarding it to the next handler in the chain. +public struct AuthenticationMiddleware: ClientMiddleware { + /// The Anthropic API version sent with every request. + private static let anthropicVersion = "2023-06-01" + + /// The `x-api-key` header name, constructed once. `"x-api-key"` is a valid + /// RFC 9110 token, so this is always non-`nil`. + private static let apiKeyFieldName = HTTPField.Name("x-api-key") + + /// The `anthropic-version` header name, constructed once. `"anthropic-version"` + /// is a valid RFC 9110 token, so this is always non-`nil`. + private static let anthropicVersionFieldName = HTTPField.Name("anthropic-version") + + /// The Anthropic API key used to authenticate requests. + private let apiKey: String + + /// Creates a middleware that authenticates requests with the given API key. + /// - Parameter apiKey: The Anthropic API key to send in the `x-api-key` header. + public init(apiKey: String) { + self.apiKey = apiKey + } + + /// Adds the Anthropic authentication headers, then forwards the request. + /// + /// Any `x-api-key` or `anthropic-version` header already present on the + /// request is silently overwritten with this middleware's values. + /// - Parameters: + /// - request: The outgoing HTTP request. + /// - body: The outgoing HTTP request body, if any. + /// - baseURL: The base URL of the server. + /// - operationID: The OpenAPI operation identifier for the request. + /// - next: The next handler in the middleware chain. + /// - Returns: The HTTP response and body from the next handler. + /// - Throws: Any error thrown by the `next` handler. + public func intercept( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String, + next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) { + var request = request + if let apiKeyFieldName = Self.apiKeyFieldName { + request.headerFields[apiKeyFieldName] = apiKey + } else { + assertionFailure("\"x-api-key\" is a valid header name and must never be nil.") + } + if let anthropicVersionFieldName = Self.anthropicVersionFieldName { + request.headerFields[anthropicVersionFieldName] = Self.anthropicVersion + } else { + assertionFailure("\"anthropic-version\" is a valid header name and must never be nil.") + } + return try await next(request, body, baseURL) + } +} diff --git a/Sources/SyntaxKit/Core/Architecture.swift b/Sources/SyntaxKit/Core/Architecture.swift new file mode 100644 index 0000000..6cd2823 --- /dev/null +++ b/Sources/SyntaxKit/Core/Architecture.swift @@ -0,0 +1,42 @@ +// +// Architecture.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +/// CPU-architecture identifiers used in `arch(...)` checks. +public enum Architecture: String, Sendable { + /// `arch(arm64)` + case arm64 + /// `arch(x86_64)` + case x86 = "x86_64" + /// `arch(i386)` + case i386 + /// `arch(arm)` + case arm + /// `arch(wasm32)` + case wasm32 +} diff --git a/Sources/SyntaxKit/Core/Comparison.swift b/Sources/SyntaxKit/Core/Comparison.swift new file mode 100644 index 0000000..fd9fa3e --- /dev/null +++ b/Sources/SyntaxKit/Core/Comparison.swift @@ -0,0 +1,43 @@ +// +// Comparison.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +/// The comparison operator used between a keyword and a version in a +/// `swift(...)` / `compiler(...)` version check. +public enum Comparison: String, Sendable { + /// `>=` + case greaterThanOrEqual = ">=" + /// `>` + case greaterThan = ">" + /// `<=` + case lessThanOrEqual = "<=" + /// `<` + case lessThan = "<" + /// `==` + case equal = "==" +} diff --git a/Sources/SyntaxKit/Core/OperatingSystem.swift b/Sources/SyntaxKit/Core/OperatingSystem.swift new file mode 100644 index 0000000..a57a03a --- /dev/null +++ b/Sources/SyntaxKit/Core/OperatingSystem.swift @@ -0,0 +1,54 @@ +// +// OperatingSystem.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +/// Operating-system identifiers used in `os(...)` checks. +public enum OperatingSystem: String, Sendable { + /// `os(iOS)` + case iOS + /// `os(macOS)` + case macOS + /// `os(tvOS)` + case tvOS + /// `os(watchOS)` + case watchOS + /// `os(visionOS)` + case visionOS + /// `os(anyAppleOS)` — matches any Apple operating system (Swift 6.4+). + case anyAppleOS + /// `os(Linux)` + case linux = "Linux" + /// `os(Windows)` + case windows = "Windows" + /// `os(FreeBSD)` + case freeBSD = "FreeBSD" + /// `os(Android)` + case android = "Android" + /// `os(WASI)` + case wasi = "WASI" +} diff --git a/Sources/SyntaxKit/Core/TargetEnvironment.swift b/Sources/SyntaxKit/Core/TargetEnvironment.swift new file mode 100644 index 0000000..97aae12 --- /dev/null +++ b/Sources/SyntaxKit/Core/TargetEnvironment.swift @@ -0,0 +1,36 @@ +// +// TargetEnvironment.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +/// Target-environment identifiers used in `targetEnvironment(...)` checks. +public enum TargetEnvironment: String, Sendable { + /// `targetEnvironment(simulator)` + case simulator + /// `targetEnvironment(macCatalyst)` + case macCatalyst +} diff --git a/Sources/SyntaxKit/Core/Version.swift b/Sources/SyntaxKit/Core/Version.swift new file mode 100644 index 0000000..eb90f92 --- /dev/null +++ b/Sources/SyntaxKit/Core/Version.swift @@ -0,0 +1,64 @@ +// +// Version.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +/// A `major.minor.patch` version, with optional minor and patch components. +public struct Version: Sendable, CustomStringConvertible { + /// The major version component. + public let major: Int + /// The optional minor version component. + public let minor: Int? + /// The optional patch version component. + public let patch: Int? + + /// The dotted string form, e.g. `5`, `5.9`, or `5.9.1`. + public var description: String { + var result = String(major) + if let minor = minor { + result += ".\(minor)" + if let patch = patch { + result += ".\(patch)" + } + } + return result + } + + /// The dotted string form, e.g. `5`, `5.9`, or `5.9.1`. + internal var versionString: String { description } + + /// Create a version from its components. + /// - Parameters: + /// - major: The major version component. + /// - minor: The optional minor version component. + /// - patch: The optional patch version component. + public init(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) { + self.major = major + self.minor = minor + self.patch = patch + } +} diff --git a/Sources/SyntaxKit/Core/VersionCheck.swift b/Sources/SyntaxKit/Core/VersionCheck.swift new file mode 100644 index 0000000..f7a56f8 --- /dev/null +++ b/Sources/SyntaxKit/Core/VersionCheck.swift @@ -0,0 +1,83 @@ +// +// VersionCheck.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +/// A `swift(>=5.9)` / `compiler(>=5.9)`-style version check pairing a +/// ``Comparison`` operator with a ``Version``. +public struct VersionCheck: Sendable { + /// The comparison operator that precedes the version. + public let comparison: Comparison + /// The version the comparison is applied to. + public let version: Version + + internal var rendered: String { + "\(comparison.rawValue)\(version.versionString)" + } + + /// Create a version check from a comparison and a version. + /// - Parameters: + /// - comparison: The comparison operator that precedes the version. + /// - version: The version the comparison is applied to. + public init(comparison: Comparison, version: Version) { + self.comparison = comparison + self.version = version + } + + /// Build a `>= major.minor[.patch]` check. + public static func greaterThanOrEqual(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) + -> VersionCheck + { + VersionCheck(comparison: .greaterThanOrEqual, version: Version(major, minor, patch)) + } + + /// Build a `> major.minor[.patch]` check. + public static func greaterThan(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) + -> VersionCheck + { + VersionCheck(comparison: .greaterThan, version: Version(major, minor, patch)) + } + + /// Build a `<= major.minor[.patch]` check. + public static func lessThanOrEqual(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) + -> VersionCheck + { + VersionCheck(comparison: .lessThanOrEqual, version: Version(major, minor, patch)) + } + + /// Build a `< major.minor[.patch]` check. + public static func lessThan(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) + -> VersionCheck + { + VersionCheck(comparison: .lessThan, version: Version(major, minor, patch)) + } + + /// Build an `== major.minor[.patch]` check. + public static func equal(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) -> VersionCheck { + VersionCheck(comparison: .equal, version: Version(major, minor, patch)) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf+Condition.swift b/Sources/SyntaxKit/Declarations/PoundIf+Condition.swift deleted file mode 100644 index 70fe247..0000000 --- a/Sources/SyntaxKit/Declarations/PoundIf+Condition.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// PoundIf+Condition.swift -// SyntaxKit -// -// Created by Leo Dion. -// Copyright © 2026 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. -// - -extension PoundIf { - // swiftlint:disable identifier_name - - /// Canonical `#if` checks, mirroring Swift's conditional-compilation grammar. - public indirect enum Condition: Sendable { - /// `canImport()` - case canImport(String) - /// A bare compilation flag such as `DEBUG`. - case flag(String) - /// `os()` - case os(OperatingSystem) - /// `arch()` - case arch(Architecture) - /// `targetEnvironment()` - case targetEnvironment(TargetEnvironment) - /// `swift(>=5.9)` and friends. - case swift(VersionCheck) - /// `compiler(>=5.9)` and friends. - case compiler(VersionCheck) - /// `hasFeature()` - case hasFeature(String) - /// `hasAttribute()` - case hasAttribute(String) - /// ` && ` - case and(Condition, Condition) - /// ` || ` - case or(Condition, Condition) - /// `!` - case not(Condition) - } - - // swiftlint:enable identifier_name - - /// Operating-system identifiers used in `os(...)` checks. - public enum OperatingSystem: String, Sendable { - /// `os(iOS)` - case iOS - /// `os(macOS)` - case macOS - /// `os(tvOS)` - case tvOS - /// `os(watchOS)` - case watchOS - /// `os(visionOS)` - case visionOS - /// `os(Linux)` - case linux = "Linux" - /// `os(Windows)` - case windows = "Windows" - /// `os(FreeBSD)` - case freeBSD = "FreeBSD" - /// `os(Android)` - case android = "Android" - /// `os(WASI)` - case wasi = "WASI" - } - - /// CPU-architecture identifiers used in `arch(...)` checks. - public enum Architecture: String, Sendable { - /// `arch(arm64)` - case arm64 - /// `arch(x86_64)` - case x86 = "x86_64" - /// `arch(i386)` - case i386 - /// `arch(arm)` - case arm - /// `arch(wasm32)` - case wasm32 - } - - /// Target-environment identifiers used in `targetEnvironment(...)` checks. - public enum TargetEnvironment: String, Sendable { - /// `targetEnvironment(simulator)` - case simulator - /// `targetEnvironment(macCatalyst)` - case macCatalyst - } - - /// A `swift(>=5.9)` / `compiler(>=5.9)`-style version check. - public struct VersionCheck: Sendable { - /// The comparison operator used between the keyword and the version. - public enum Comparison: String, Sendable { - /// `>=` - case greaterThanOrEqual = ">=" - /// `>` - case greaterThan = ">" - /// `<=` - case lessThanOrEqual = "<=" - /// `<` - case lessThan = "<" - /// `==` - case equal = "==" - } - - /// The comparison operator that precedes the version. - public let comparison: Comparison - /// The major version component. - public let major: Int - /// The optional minor version component. - public let minor: Int? - /// The optional patch version component. - public let patch: Int? - - internal var versionString: String { - var result = String(major) - if let minor = minor { - result += ".\(minor)" - if let patch = patch { - result += ".\(patch)" - } - } - return result - } - - internal var rendered: String { - "\(comparison.rawValue)\(versionString)" - } - - /// Build a `>= major.minor[.patch]` check. - public static func atLeast(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) - -> VersionCheck - { - VersionCheck(comparison: .greaterThanOrEqual, major: major, minor: minor, patch: patch) - } - - /// Build an `== major.minor[.patch]` check. - public static func exact(_ major: Int, _ minor: Int? = nil, _ patch: Int? = nil) -> VersionCheck - { - VersionCheck(comparison: .equal, major: major, minor: minor, patch: patch) - } - } -} diff --git a/Sources/SyntaxKit/Declarations/PoundIf+Rendering.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf+Rendering.swift similarity index 61% rename from Sources/SyntaxKit/Declarations/PoundIf+Rendering.swift rename to Sources/SyntaxKit/Declarations/PoundIf/PoundIf+Rendering.swift index 2a71131..daf7f7e 100644 --- a/Sources/SyntaxKit/Declarations/PoundIf+Rendering.swift +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf+Rendering.swift @@ -57,7 +57,7 @@ extension PoundIf { private static func renderCondition(_ form: ConditionForm) -> ExprSyntax? { switch form { case .helper(let condition): - return parseExpression(renderHelper(condition, atTopLevel: true)) + return parseExpression(condition.render(atTopLevel: true)) case .raw(let text): return parseExpression(text) case .codeBlock(let block): @@ -77,43 +77,22 @@ extension PoundIf { } return nil } +} - private static func renderHelper(_ condition: Condition, atTopLevel: Bool) -> String { - if let leaf = renderLeaf(condition) { - return leaf - } - return renderCombinator(condition, atTopLevel: atTopLevel) - } - - private static func renderLeaf(_ condition: Condition) -> String? { - switch condition { - case .canImport(let module): return "canImport(\(module))" - case .flag(let name): return name - case .os(let value): return "os(\(value.rawValue))" - case .arch(let value): return "arch(\(value.rawValue))" - case .targetEnvironment(let value): return "targetEnvironment(\(value.rawValue))" - case .swift(let check): return "swift(\(check.rendered))" - case .compiler(let check): return "compiler(\(check.rendered))" - case .hasFeature(let name): return "hasFeature(\(name))" - case .hasAttribute(let name): return "hasAttribute(\(name))" - case .and, .or, .not: return nil +extension PoundIf.LeafCondition { + /// Render the leaf as `keyword(argument)`, or bare `argument` for a flag. + public func render(atTopLevel _: Bool) -> String { + guard let keyword else { + return argument } + return "\(keyword)(\(argument))" } +} - private static func renderCombinator(_ condition: Condition, atTopLevel: Bool) -> String { - switch condition { - case .and(let lhs, let rhs): - let inner = - "\(renderHelper(lhs, atTopLevel: false)) && \(renderHelper(rhs, atTopLevel: false))" - return atTopLevel ? inner : "(\(inner))" - case .or(let lhs, let rhs): - let inner = - "\(renderHelper(lhs, atTopLevel: false)) || \(renderHelper(rhs, atTopLevel: false))" - return atTopLevel ? inner : "(\(inner))" - case .not(let operand): - return "!\(renderHelper(operand, atTopLevel: false))" - default: - return "" - } +extension PoundIf.BinaryCondition { + /// Render as `lhs symbol rhs`, wrapping in parentheses unless at the top level. + public func render(atTopLevel: Bool) -> String { + let inner = "\(lhs.render(atTopLevel: false)) \(symbol) \(rhs.render(atTopLevel: false))" + return atTopLevel ? inner : "(\(inner))" } } diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.AndCondition.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.AndCondition.swift new file mode 100644 index 0000000..444a19b --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.AndCondition.swift @@ -0,0 +1,54 @@ +// +// PoundIf.AndCondition.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// ` && ` + public struct AndCondition: BinaryCondition { + /// The left-hand operand. + public let lhs: any Condition + /// The right-hand operand. + public let rhs: any Condition + /// The `&&` operator. + public var symbol: String { "&&" } + } +} + +extension PoundIf.Condition where Self == PoundIf.AndCondition { + /// ` && ` + /// - Parameters: + /// - lhs: The left-hand condition. + /// - rhs: The right-hand condition. + /// - Returns: A conjunction condition. + public static func and( + _ lhs: L, + _ rhs: R + ) -> PoundIf.AndCondition { + .init(lhs: lhs, rhs: rhs) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.ArchCheck.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.ArchCheck.swift new file mode 100644 index 0000000..b4e6c98 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.ArchCheck.swift @@ -0,0 +1,49 @@ +// +// PoundIf.ArchCheck.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `arch()` + public struct ArchCheck: LeafCondition { + /// The CPU architecture being checked. + public let value: Architecture + /// The `arch` keyword. + public var keyword: String? { "arch" } + /// The architecture identifier rendered inside the parentheses. + public var argument: String { value.rawValue } + } +} + +extension PoundIf.Condition where Self == PoundIf.ArchCheck { + /// `arch()` + /// - Parameter value: The CPU architecture to test for. + /// - Returns: An `arch` condition. + public static func arch(_ value: Architecture) -> PoundIf.ArchCheck { + .init(value: value) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.BinaryCondition.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.BinaryCondition.swift new file mode 100644 index 0000000..04965ca --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.BinaryCondition.swift @@ -0,0 +1,43 @@ +// +// PoundIf.BinaryCondition.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// A `lhs rhs` combinator, parenthesized unless at the top level. + public protocol BinaryCondition: Condition { + /// The infix operator rendered between the operands, e.g. `&&`. + /// + /// Conformers must return a valid `#if` boolean operator (`&&` or `||`); + /// any other value produces invalid conditional-compilation syntax. + var symbol: String { get } + /// The left-hand operand. + var lhs: any Condition { get } + /// The right-hand operand. + var rhs: any Condition { get } + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.CanImport.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.CanImport.swift new file mode 100644 index 0000000..392123f --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.CanImport.swift @@ -0,0 +1,49 @@ +// +// PoundIf.CanImport.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `canImport()` + public struct CanImport: LeafCondition { + /// The module name passed to `canImport`. + public let module: String + /// The `canImport` keyword. + public var keyword: String? { "canImport" } + /// The module name rendered inside the parentheses. + public var argument: String { module } + } +} + +extension PoundIf.Condition where Self == PoundIf.CanImport { + /// `canImport()` + /// - Parameter module: The module name to test for. + /// - Returns: A `canImport` condition. + public static func canImport(_ module: String) -> PoundIf.CanImport { + .init(module: module) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.CompilerCheck.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.CompilerCheck.swift new file mode 100644 index 0000000..3edc830 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.CompilerCheck.swift @@ -0,0 +1,49 @@ +// +// PoundIf.CompilerCheck.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `compiler(>=5.9)` and friends. + public struct CompilerCheck: LeafCondition { + /// The compiler version check. + public let check: VersionCheck + /// The `compiler` keyword. + public var keyword: String? { "compiler" } + /// The version comparison rendered inside the parentheses. + public var argument: String { check.rendered } + } +} + +extension PoundIf.Condition where Self == PoundIf.CompilerCheck { + /// `compiler(>=5.9)` and friends. + /// - Parameter check: The compiler version comparison. + /// - Returns: A `compiler` version condition. + public static func compiler(_ check: VersionCheck) -> PoundIf.CompilerCheck { + .init(check: check) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Condition.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Condition.swift new file mode 100644 index 0000000..a46bb1b --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Condition.swift @@ -0,0 +1,44 @@ +// +// PoundIf.Condition.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// A canonical `#if` check that can render itself as conditional-compilation + /// syntax, mirroring Swift's grammar. + public protocol Condition: Sendable { + /// Render this condition as `#if` source text. + /// + /// This is part of the public API: a condition renders itself, so custom + /// types conforming to ``Condition`` (or ``LeafCondition``) participate in + /// rendering alongside the built-in conditions. + /// - Parameter atTopLevel: When `true`, a binary combinator omits its + /// surrounding parentheses (it is the outermost expression). + /// - Returns: The rendered conditional-compilation expression. + func render(atTopLevel: Bool) -> String + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Flag.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Flag.swift new file mode 100644 index 0000000..01ac87e --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Flag.swift @@ -0,0 +1,49 @@ +// +// PoundIf.Flag.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// A bare compilation flag such as `DEBUG`. + public struct Flag: LeafCondition { + /// The flag identifier. + public let name: String + /// A flag has no keyword and renders bare. + public var keyword: String? { nil } + /// The flag identifier rendered as-is. + public var argument: String { name } + } +} + +extension PoundIf.Condition where Self == PoundIf.Flag { + /// A bare compilation flag such as `DEBUG`. + /// - Parameter name: The flag identifier. + /// - Returns: A flag condition. + public static func flag(_ name: String) -> PoundIf.Flag { + .init(name: name) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.HasAttribute.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.HasAttribute.swift new file mode 100644 index 0000000..6c9f05a --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.HasAttribute.swift @@ -0,0 +1,49 @@ +// +// PoundIf.HasAttribute.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `hasAttribute()` + public struct HasAttribute: LeafCondition { + /// The attribute name. + public let name: String + /// The `hasAttribute` keyword. + public var keyword: String? { "hasAttribute" } + /// The attribute name rendered inside the parentheses. + public var argument: String { name } + } +} + +extension PoundIf.Condition where Self == PoundIf.HasAttribute { + /// `hasAttribute()` + /// - Parameter name: The attribute name to test for. + /// - Returns: A `hasAttribute` condition. + public static func hasAttribute(_ name: String) -> PoundIf.HasAttribute { + .init(name: name) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.HasFeature.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.HasFeature.swift new file mode 100644 index 0000000..4bcc179 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.HasFeature.swift @@ -0,0 +1,49 @@ +// +// PoundIf.HasFeature.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `hasFeature()` + public struct HasFeature: LeafCondition { + /// The upcoming/experimental feature name. + public let name: String + /// The `hasFeature` keyword. + public var keyword: String? { "hasFeature" } + /// The feature name rendered inside the parentheses. + public var argument: String { name } + } +} + +extension PoundIf.Condition where Self == PoundIf.HasFeature { + /// `hasFeature()` + /// - Parameter name: The feature name to test for. + /// - Returns: A `hasFeature` condition. + public static func hasFeature(_ name: String) -> PoundIf.HasFeature { + .init(name: name) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.LeafCondition.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.LeafCondition.swift new file mode 100644 index 0000000..8188841 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.LeafCondition.swift @@ -0,0 +1,39 @@ +// +// PoundIf.LeafCondition.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// A single check rendered as `keyword(argument)`, or a bare `argument` when + /// ``LeafCondition/keyword`` is `nil` (a flag such as `DEBUG`). + public protocol LeafCondition: Condition { + /// The leading keyword, e.g. `os`, or `nil` for a bare flag. + var keyword: String? { get } + /// The text placed inside the parentheses, or the bare flag identifier. + var argument: String { get } + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Not.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Not.swift new file mode 100644 index 0000000..7dccd32 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.Not.swift @@ -0,0 +1,50 @@ +// +// PoundIf.Not.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `!` + public struct Not: Condition { + /// The condition being negated. + public let operand: any Condition + + /// Render as `!operand`, with the operand parenthesized as needed. + public func render(atTopLevel _: Bool) -> String { + "!\(operand.render(atTopLevel: false))" + } + } +} + +extension PoundIf.Condition where Self == PoundIf.Not { + /// `!` + /// - Parameter operand: The condition to negate. + /// - Returns: A negation condition. + public static func not(_ operand: C) -> PoundIf.Not { + .init(operand: operand) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.OSCheck.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.OSCheck.swift new file mode 100644 index 0000000..a7e282f --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.OSCheck.swift @@ -0,0 +1,49 @@ +// +// PoundIf.OSCheck.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `os()` + public struct OSCheck: LeafCondition { + /// The operating system being checked. + public let value: OperatingSystem + /// The `os` keyword. + public var keyword: String? { "os" } + /// The operating-system identifier rendered inside the parentheses. + public var argument: String { value.rawValue } + } +} + +extension PoundIf.Condition where Self == PoundIf.OSCheck { + /// `os()` + /// - Parameter value: The operating system to test for. + /// - Returns: An `os` condition. + public static func os(_ value: OperatingSystem) -> PoundIf.OSCheck { + .init(value: value) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.OrCondition.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.OrCondition.swift new file mode 100644 index 0000000..1eb7f73 --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.OrCondition.swift @@ -0,0 +1,54 @@ +// +// PoundIf.OrCondition.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// ` || ` + public struct OrCondition: BinaryCondition { + /// The left-hand operand. + public let lhs: any Condition + /// The right-hand operand. + public let rhs: any Condition + /// The `||` operator. + public var symbol: String { "||" } + } +} + +extension PoundIf.Condition where Self == PoundIf.OrCondition { + /// ` || ` + /// - Parameters: + /// - lhs: The left-hand condition. + /// - rhs: The right-hand condition. + /// - Returns: A disjunction condition. + public static func or( + _ lhs: L, + _ rhs: R + ) -> PoundIf.OrCondition { + .init(lhs: lhs, rhs: rhs) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.SwiftCheck.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.SwiftCheck.swift new file mode 100644 index 0000000..29ee93b --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.SwiftCheck.swift @@ -0,0 +1,49 @@ +// +// PoundIf.SwiftCheck.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `swift(>=5.9)` and friends. + public struct SwiftCheck: LeafCondition { + /// The Swift language version check. + public let check: VersionCheck + /// The `swift` keyword. + public var keyword: String? { "swift" } + /// The version comparison rendered inside the parentheses. + public var argument: String { check.rendered } + } +} + +extension PoundIf.Condition where Self == PoundIf.SwiftCheck { + /// `swift(>=5.9)` and friends. + /// - Parameter check: The Swift language version comparison. + /// - Returns: A `swift` version condition. + public static func swift(_ check: VersionCheck) -> PoundIf.SwiftCheck { + .init(check: check) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.TargetEnvironmentCheck.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.TargetEnvironmentCheck.swift new file mode 100644 index 0000000..e32050f --- /dev/null +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.TargetEnvironmentCheck.swift @@ -0,0 +1,51 @@ +// +// PoundIf.TargetEnvironmentCheck.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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. +// + +extension PoundIf { + /// `targetEnvironment()` + public struct TargetEnvironmentCheck: LeafCondition { + /// The target environment being checked. + public let value: TargetEnvironment + /// The `targetEnvironment` keyword. + public var keyword: String? { "targetEnvironment" } + /// The environment identifier rendered inside the parentheses. + public var argument: String { value.rawValue } + } +} + +extension PoundIf.Condition where Self == PoundIf.TargetEnvironmentCheck { + /// `targetEnvironment()` + /// - Parameter value: The target environment to test for. + /// - Returns: A `targetEnvironment` condition. + public static func targetEnvironment( + _ value: TargetEnvironment + ) -> PoundIf.TargetEnvironmentCheck { + .init(value: value) + } +} diff --git a/Sources/SyntaxKit/Declarations/PoundIf.swift b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.swift similarity index 98% rename from Sources/SyntaxKit/Declarations/PoundIf.swift rename to Sources/SyntaxKit/Declarations/PoundIf/PoundIf.swift index a88d2f6..fdbb193 100644 --- a/Sources/SyntaxKit/Declarations/PoundIf.swift +++ b/Sources/SyntaxKit/Declarations/PoundIf/PoundIf.swift @@ -33,7 +33,7 @@ public import SwiftSyntax public struct PoundIf: CodeBlock, Sendable { /// One of the three accepted condition forms attached to a `#if` / `#elseif` clause. internal enum ConditionForm: Sendable { - case helper(Condition) + case helper(any Condition) case raw(String) case codeBlock(any CodeBlock) } @@ -89,7 +89,7 @@ public struct PoundIf: CodeBlock, Sendable { /// - condition: The structured condition for the `#if` clause. /// - content: The code blocks to emit when the condition is satisfied. public init( - _ condition: Condition, + _ condition: some Condition, @CodeBlockBuilderResult _ content: () -> [any CodeBlock] ) { self.head = Clause(condition: .helper(condition), body: content()) @@ -123,7 +123,7 @@ public struct PoundIf: CodeBlock, Sendable { /// - content: The code blocks to emit when the condition is satisfied. /// - Returns: A copy of `self` with the new clause appended. public func elseif( - _ condition: Condition, + _ condition: some Condition, @CodeBlockBuilderResult _ content: () -> [any CodeBlock] ) -> Self { var copy = self diff --git a/Tests/AiSTKitTests/AuthenticationMiddlewareTests.swift b/Tests/AiSTKitTests/AuthenticationMiddlewareTests.swift new file mode 100644 index 0000000..1477194 --- /dev/null +++ b/Tests/AiSTKitTests/AuthenticationMiddlewareTests.swift @@ -0,0 +1,164 @@ +// +// AuthenticationMiddlewareTests.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 Foundation +import HTTPTypes +import OpenAPIRuntime +import Testing + +@testable import AiSTKit + +internal struct AuthenticationMiddlewareTests { + // swiftlint:disable no_unchecked_sendable + + /// Thread-safe one-shot holder, so the `@Sendable` `next` closure can hand + /// the request it received back to the test. + private final class Box: @unchecked Sendable { + private let lock = NSLock() + private var stored: Value? + + var value: Value? { + lock.withLock { stored } + } + + func set(_ newValue: Value) { + lock.withLock { stored = newValue } + } + } + + /// Thread-safe call counter, so the `@Sendable` `next` closure can record how + /// many times it was invoked. + private final class CallCounter: @unchecked Sendable { + private let lock = NSLock() + private var stored = 0 + + var count: Int { + lock.withLock { stored } + } + + func increment() { + lock.withLock { stored += 1 } + } + } + // swiftlint:enable no_unchecked_sendable + + private static let baseURLString = "https://api.anthropic.com" + + /// Invokes the middleware with a stub `next` handler and returns the request + /// the handler received, so tests can assert on the injected headers. + private func interceptedRequest( + apiKey: String, + seedingRequest seed: (inout HTTPRequest) -> Void = { _ in } + ) async throws -> HTTPRequest { + let middleware = AuthenticationMiddleware(apiKey: apiKey) + var request = HTTPRequest( + method: .post, + scheme: "https", + authority: "api.anthropic.com", + path: "/v1/messages" + ) + seed(&request) + + let baseURL = try #require(URL(string: Self.baseURLString)) + let captured = Box() + _ = try await middleware.intercept( + request, + body: nil, + baseURL: baseURL, + operationID: "createMessage" + ) { forwarded, body, url in + captured.set(forwarded) + #expect(body == nil) + #expect(url == baseURL) + return (HTTPResponse(status: .ok), nil) + } + + return try #require(captured.value, "next handler was never invoked") + } + + @Test internal func injectsAPIKeyHeader() async throws { + let request = try await interceptedRequest(apiKey: "test-key-123") + #expect(request.headerFields[try #require(.init("x-api-key"))] == "test-key-123") + } + + @Test internal func injectsAnthropicVersionHeader() async throws { + let request = try await interceptedRequest(apiKey: "test-key-123") + #expect(request.headerFields[try #require(.init("anthropic-version"))] == "2023-06-01") + } + + @Test internal func preservesExistingHeaders() async throws { + let contentType = try #require(HTTPField.Name("content-type")) + let request = try await interceptedRequest(apiKey: "test-key-123") { request in + request.headerFields[contentType] = "application/json" + } + #expect(request.headerFields[contentType] == "application/json") + #expect(request.headerFields[try #require(.init("x-api-key"))] == "test-key-123") + } + + @Test internal func forwardsRequestToNextHandler() async throws { + let request = try await interceptedRequest(apiKey: "test-key-123") + #expect(request.method == .post) + #expect(request.path == "/v1/messages") + } + + @Test internal func overridesExistingAPIKeyHeader() async throws { + let apiKeyName = try #require(HTTPField.Name("x-api-key")) + let request = try await interceptedRequest(apiKey: "new-key") { request in + request.headerFields[apiKeyName] = "stale-key" + } + #expect(request.headerFields[apiKeyName] == "new-key") + } + + @Test internal func acceptsEmptyAPIKey() async throws { + let request = try await interceptedRequest(apiKey: "") + #expect(request.headerFields[try #require(.init("x-api-key"))]?.isEmpty == true) + } + + @Test internal func callsNextExactlyOnce() async throws { + let middleware = AuthenticationMiddleware(apiKey: "test-key-123") + let request = HTTPRequest( + method: .post, + scheme: "https", + authority: "api.anthropic.com", + path: "/v1/messages" + ) + let baseURL = try #require(URL(string: Self.baseURLString)) + let counter = CallCounter() + _ = try await middleware.intercept( + request, + body: nil, + baseURL: baseURL, + operationID: "createMessage" + ) { _, _, _ in + counter.increment() + return (HTTPResponse(status: .ok), nil) + } + #expect(counter.count == 1) + } +} diff --git a/Tests/SyntaxKitTests/Unit/Core/VersionCheckTests.swift b/Tests/SyntaxKitTests/Unit/Core/VersionCheckTests.swift new file mode 100644 index 0000000..f5d619b --- /dev/null +++ b/Tests/SyntaxKitTests/Unit/Core/VersionCheckTests.swift @@ -0,0 +1,60 @@ +// +// VersionCheckTests.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2026 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 Testing + +@testable import SyntaxKit + +internal struct VersionCheckTests { + @Test internal func greaterThanOrEqualRendersOperatorAndVersion() { + #expect(VersionCheck.greaterThanOrEqual(5, 9).rendered == ">=5.9") + } + + @Test internal func greaterThanRendersOperatorAndVersion() { + #expect(VersionCheck.greaterThan(6).rendered == ">6") + } + + @Test internal func lessThanOrEqualRendersOperatorAndVersion() { + #expect(VersionCheck.lessThanOrEqual(6, 1).rendered == "<=6.1") + } + + @Test internal func lessThanRendersOperatorAndVersion() { + #expect(VersionCheck.lessThan(7).rendered == "<7") + } + + @Test internal func equalRendersOperatorAndVersion() { + #expect(VersionCheck.equal(6, 0, 1).rendered == "==6.0.1") + } + + @Test internal func versionDescriptionMatchesDottedForm() { + #expect(Version(5).description == "5") + #expect(Version(5, 9).description == "5.9") + #expect(Version(5, 9, 1).description == "5.9.1") + } +} diff --git a/Tests/SyntaxKitTests/Unit/Declarations/PoundIfTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/PoundIfTests.swift index 9f68e7c..9e09282 100644 --- a/Tests/SyntaxKitTests/Unit/Declarations/PoundIfTests.swift +++ b/Tests/SyntaxKitTests/Unit/Declarations/PoundIfTests.swift @@ -33,6 +33,11 @@ import Testing @testable import SyntaxKit internal struct PoundIfTests { + /// Renders a `#if` block wrapping a single import, normalized for assertions. + private func rendered(_ condition: some PoundIf.Condition) -> String { + PoundIf(condition) { Import("Foundation") }.generateCode().normalize() + } + @Test internal func testCanImport() { let block = PoundIf(.canImport("SwiftUI")) { Import("SwiftUI") @@ -62,70 +67,64 @@ internal struct PoundIfTests { #expect(generated.contains("import UIKit")) } + @Test internal func testAnyAppleOS() { + #expect(rendered(.os(.anyAppleOS)).contains("#if os(anyAppleOS)")) + } + @Test internal func testArch() { - let block = PoundIf(.arch(.arm64)) { - Import("Foundation") - } - let generated = block.generateCode().normalize() - #expect(generated.contains("#if arch(arm64)")) + #expect(rendered(.arch(.arm64)).contains("#if arch(arm64)")) } @Test internal func testTargetEnvironment() { - let block = PoundIf(.targetEnvironment(.simulator)) { - Import("Foundation") - } - let generated = block.generateCode().normalize() - #expect(generated.contains("#if targetEnvironment(simulator)")) + #expect(rendered(.targetEnvironment(.simulator)).contains("#if targetEnvironment(simulator)")) + #expect( + rendered(.targetEnvironment(.macCatalyst)).contains("#if targetEnvironment(macCatalyst)")) } @Test internal func testSwiftVersion() { - let block = PoundIf(.swift(.atLeast(5, 9))) { - Import("Foundation") - } - let generated = block.generateCode().normalize() - #expect(generated.contains("#if swift(>=5.9)")) + #expect(rendered(.swift(.greaterThanOrEqual(5, 9))).contains("#if swift(>=5.9)")) } @Test internal func testCompilerVersion() { - let block = PoundIf(.compiler(.atLeast(5, 9))) { - Import("Foundation") - } - let generated = block.generateCode().normalize() - #expect(generated.contains("#if compiler(>=5.9)")) + #expect(rendered(.compiler(.greaterThanOrEqual(5, 9))).contains("#if compiler(>=5.9)")) + } + + @Test internal func testVersionComparisons() { + #expect(rendered(.swift(.greaterThan(6))).contains("#if swift(>6)")) + #expect(rendered(.compiler(.lessThanOrEqual(6, 1))).contains("#if compiler(<=6.1)")) + #expect(rendered(.swift(.lessThan(7))).contains("#if swift(<7)")) + #expect(rendered(.compiler(.equal(6, 0, 1))).contains("#if compiler(==6.0.1)")) } @Test internal func testHasFeature() { - let block = PoundIf(.hasFeature("StrictConcurrency")) { - Import("Foundation") - } - let generated = block.generateCode().normalize() - #expect(generated.contains("#if hasFeature(StrictConcurrency)")) + #expect( + rendered(.hasFeature("StrictConcurrency")).contains("#if hasFeature(StrictConcurrency)")) } @Test internal func testHasAttribute() { - let block = PoundIf(.hasAttribute("retroactive")) { - Import("Foundation") - } - let generated = block.generateCode().normalize() - #expect(generated.contains("#if hasAttribute(retroactive)")) + #expect(rendered(.hasAttribute("retroactive")).contains("#if hasAttribute(retroactive)")) } @Test internal func testAnd() { - let block = PoundIf(.and(.os(.iOS), .arch(.arm64))) { - Import("UIKit") - } - let generated = block.generateCode().normalize() + let generated = rendered(.and(.os(.iOS), .arch(.arm64))) #expect(generated.contains("os(iOS) && arch(arm64)")) } @Test internal func testOrNot() { - let block = PoundIf(.or(.canImport("UIKit"), .not(.os(.macOS)))) { - Import("Foundation") - } - let generated = block.generateCode().normalize() + let generated = rendered(.or(.canImport("UIKit"), .not(.os(.macOS)))) #expect(generated.contains("canImport(UIKit) || !os(macOS)")) } + @Test internal func testNestedBinaryParenthesization() { + let generated = rendered(.and(.or(.os(.iOS), .os(.macOS)), .arch(.arm64))) + #expect(generated.contains("(os(iOS) || os(macOS)) && arch(arm64)")) + } + + @Test internal func testNotOfBinary() { + let generated = rendered(.not(.and(.os(.iOS), .os(.macOS)))) + #expect(generated.contains("!(os(iOS) && os(macOS))")) + } + @Test internal func testRawStringCondition() { let block = PoundIf("CUSTOM_FLAG") { Import("Foundation")