From 21b71a542c5f7d5c94ff88753945e6f306d154fd Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 9 Sep 2024 12:02:49 +1000 Subject: [PATCH 01/24] Remove package.resolved --- Package.resolved | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 9ad5a83..0000000 --- a/Package.resolved +++ /dev/null @@ -1,25 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "SwiftDocCPlugin", - "repositoryURL": "https://github.com/apple/swift-docc-plugin", - "state": { - "branch": null, - "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", - "version": "1.3.0" - } - }, - { - "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", - "state": { - "branch": null, - "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", - "version": "1.0.0" - } - } - ] - }, - "version": 1 -} From 4c4740b9511a6949d879f240f2442b45f229610d Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 9 Sep 2024 12:11:17 +1000 Subject: [PATCH 02/24] bump swiftformat from 0.53.10 to 0.54.4 --- Mintfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mintfile b/Mintfile index fd88089..5336060 100644 --- a/Mintfile +++ b/Mintfile @@ -1,2 +1,2 @@ realm/SwiftLint@0.55.1 -nicklockwood/SwiftFormat@0.53.10 +nicklockwood/SwiftFormat@0.54.4 From ce888a5b5972c33598256bc9bba7e9e4985c60d5 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 9 Sep 2024 12:19:01 +1000 Subject: [PATCH 03/24] bump swiftlint from 0.55.1 to 0.57.0 --- Mintfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mintfile b/Mintfile index 5336060..50f4726 100644 --- a/Mintfile +++ b/Mintfile @@ -1,2 +1,2 @@ -realm/SwiftLint@0.55.1 +realm/SwiftLint@0.57.0 nicklockwood/SwiftFormat@0.54.4 From eacea2923ef70a554848680ed03ca97306871cdc Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 9 Sep 2024 17:25:56 +1000 Subject: [PATCH 04/24] add privacy manifest --- Package.swift | 3 ++- Sources/ScreamURITemplate/PrivacyInfo.xcprivacy | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Sources/ScreamURITemplate/PrivacyInfo.xcprivacy diff --git a/Package.swift b/Package.swift index 5006247..12683f9 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,8 @@ let package = Package( targets: [ .target( name: "ScreamURITemplate", - dependencies: []), + dependencies: [], + resources: [.process("PrivacyInfo.xcprivacy")]), .testTarget( name: "ScreamURITemplateTests", dependencies: ["ScreamURITemplate"], diff --git a/Sources/ScreamURITemplate/PrivacyInfo.xcprivacy b/Sources/ScreamURITemplate/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..e08a130 --- /dev/null +++ b/Sources/ScreamURITemplate/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + From 8011bafe2ac54054bfe1db5ddf7f5396da11c2e6 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 9 Sep 2024 17:26:30 +1000 Subject: [PATCH 05/24] add Package.resolved to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7c3862a..046f489 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .build .swiftpm +Package.resolved From aaf049dba77546e955aa93d7e14efaafd6113fc7 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Fri, 29 Nov 2024 21:32:28 +1100 Subject: [PATCH 06/24] bump swiftformat from 0.54.4 to 0.55.3 --- Mintfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mintfile b/Mintfile index 50f4726..9ac7bb2 100644 --- a/Mintfile +++ b/Mintfile @@ -1,2 +1,2 @@ realm/SwiftLint@0.57.0 -nicklockwood/SwiftFormat@0.54.4 +nicklockwood/SwiftFormat@0.55.3 From 1ffe8e4aa7769bc0f105e8e6dbe02e7bbf0a84c8 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Fri, 29 Nov 2024 21:32:50 +1100 Subject: [PATCH 07/24] bump swiftlint from 0.57.0 to 0.57.1 --- Mintfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mintfile b/Mintfile index 9ac7bb2..9ae35f9 100644 --- a/Mintfile +++ b/Mintfile @@ -1,2 +1,2 @@ -realm/SwiftLint@0.57.0 +realm/SwiftLint@0.57.1 nicklockwood/SwiftFormat@0.55.3 From 9f5baa06b412440fe28534b5ff0bf3bb4728b7fe Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Sat, 30 Nov 2024 09:42:16 +1100 Subject: [PATCH 08/24] Update to swift 6 --- .github/workflows/ci.yml | 2 ++ Package.swift | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9f6dbb..063dacb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ on: jobs: build: runs-on: macos-latest + env: + DEVELOPER_DIR: /Applications/Xcode_16.1.app/Contents/Developer steps: - uses: actions/checkout@v4 with: diff --git a/Package.swift b/Package.swift index 12683f9..69239da 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription @@ -37,4 +37,4 @@ let package = Package( name: "ScreamURITemplateExample", dependencies: ["ScreamURITemplate"]), ], - swiftLanguageVersions: [.v5]) + swiftLanguageModes: [.v6]) From e7efe39a8ee3ddf40cbfa36d1c4a88bb44b31483 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Sun, 1 Dec 2024 14:01:38 +1100 Subject: [PATCH 09/24] Use a struct for FormatError --- Sources/ScreamURITemplate/Internal/Components.swift | 4 ++-- .../ScreamURITemplate/Internal/ValueFormatting.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ScreamURITemplate/Internal/Components.swift b/Sources/ScreamURITemplate/Internal/Components.swift index 4280d4f..3f4633a 100644 --- a/Sources/ScreamURITemplate/Internal/Components.swift +++ b/Sources/ScreamURITemplate/Internal/Components.swift @@ -72,8 +72,8 @@ struct ExpressionComponent: Component { } do { return try value.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration) - } catch let FormatError.failure(reason) { - throw URITemplate.Error.expansionFailure(position: templatePosition, reason: "Failed expanding variable \"\(variableSpec.name)\": \(reason)") + } catch let error as FormatError { + throw URITemplate.Error.expansionFailure(position: templatePosition, reason: "Failed expanding variable \"\(variableSpec.name)\": \(error.reason)") } } diff --git a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift index bf6d7bc..bd0785c 100644 --- a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift +++ b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift @@ -14,8 +14,8 @@ import Foundation -enum FormatError: Error { - case failure(reason: String) +struct FormatError: Error { + let reason: String } extension TypedVariableValue { @@ -26,7 +26,7 @@ extension TypedVariableValue { case let .list(arrayValue): switch variableSpec.modifier { case .prefix: - throw FormatError.failure(reason: "Prefix operator can only be applied to string") + throw FormatError(reason: "Prefix operator can only be applied to string") case .explode: return try arrayValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration) case .none: @@ -35,7 +35,7 @@ extension TypedVariableValue { case let .associativeArray(associativeArrayValue): switch variableSpec.modifier { case .prefix: - throw FormatError.failure(reason: "Prefix operator can only be applied to string") + throw FormatError(reason: "Prefix operator can only be applied to string") case .explode: return try associativeArrayValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration) case .none: @@ -47,7 +47,7 @@ extension TypedVariableValue { private func percentEncode(string: String, withAllowedCharacters allowedCharacterSet: CharacterSet, allowPercentEncodedTriplets: Bool) throws -> String { guard var encoded = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) else { - throw FormatError.failure(reason: "Percent Encoding Failed") + throw FormatError(reason: "Percent Encoding Failed") } if allowPercentEncodedTriplets { // Revert where any percent-encode-triplets had their % encoded (to %25) From b97572baabbb994962103f82c9c9600ee2c5b2e3 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Sun, 1 Dec 2024 21:24:19 +1100 Subject: [PATCH 10/24] Use a struct for URITemplate.Error --- .swiftlint.yml | 3 +++ README.md | 6 ++--- .../Internal/Components.swift | 4 +-- .../ScreamURITemplate/Internal/Scanner.swift | 26 +++++++++--------- Sources/ScreamURITemplate/URITemplate.swift | 27 ++++++++++++------- .../TestFileTests.swift | 14 +++------- 6 files changed, 42 insertions(+), 38 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 63ffd9b..9ba9a41 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -15,3 +15,6 @@ trailing_comma: vertical_whitespace: max_empty_lines: 2 + +nesting: + type_level: 2 \ No newline at end of file diff --git a/README.md b/README.md index 0669356..af4bc0c 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ The error cases contain associated values specifying a string reason for the err ```swift do { _ = try URITemplate(string: "https://api.github.com/repos/{}/{repository}") -} catch URITemplate.Error.malformedTemplate(let position, let reason) { - // reason = "Empty Variable Name" - // position = 29th character +} catch let error as URITemplate.Error { + // error.reason = "Empty Variable Name" + // error.position = 29th character } ``` diff --git a/Sources/ScreamURITemplate/Internal/Components.swift b/Sources/ScreamURITemplate/Internal/Components.swift index 3f4633a..2bde086 100644 --- a/Sources/ScreamURITemplate/Internal/Components.swift +++ b/Sources/ScreamURITemplate/Internal/Components.swift @@ -36,7 +36,7 @@ struct LiteralComponent: Component { func expand(variables _: TypedVariableProvider) throws -> String { let expansion = String(literal) guard let encodedExpansion = expansion.addingPercentEncoding(withAllowedCharacters: reservedAndUnreservedCharacterSet) else { - throw URITemplate.Error.expansionFailure(position: literal.startIndex, reason: "Percent Encoding Failed") + throw URITemplate.Error(type: .expansionFailure, position: literal.startIndex, reason: "Percent Encoding Failed") } return encodedExpansion } @@ -73,7 +73,7 @@ struct ExpressionComponent: Component { do { return try value.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration) } catch let error as FormatError { - throw URITemplate.Error.expansionFailure(position: templatePosition, reason: "Failed expanding variable \"\(variableSpec.name)\": \(error.reason)") + throw URITemplate.Error(type: .expansionFailure, position: templatePosition, reason: "Failed expanding variable \"\(variableSpec.name)\": \(error.reason)") } } diff --git a/Sources/ScreamURITemplate/Internal/Scanner.swift b/Sources/ScreamURITemplate/Internal/Scanner.swift index 24af5cd..c775ce9 100644 --- a/Sources/ScreamURITemplate/Internal/Scanner.swift +++ b/Sources/ScreamURITemplate/Internal/Scanner.swift @@ -44,7 +44,7 @@ struct Scanner { case literalCharacterSet: return try scanLiteralComponent() default: - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "Unexpected character") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "Unexpected character") } } @@ -63,7 +63,7 @@ struct Scanner { let expressionOperator: ExpressionOperator if expressionOperatorCharacterSet.contains(unicodeScalars[currentIndex]) { guard let `operator` = ExpressionOperator(rawValue: unicodeScalars[currentIndex]) else { - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "Unsupported Operator") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "Unsupported Operator") } expressionOperator = `operator` currentIndex = unicodeScalars.index(after: currentIndex) @@ -81,13 +81,13 @@ struct Scanner { let variableName = try scanVariableName() if currentIndex == unicodeScalars.endIndex { - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "Unterminated Expression") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "Unterminated Expression") } let modifier = try scanVariableModifier() if currentIndex == unicodeScalars.endIndex { - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "Unterminated Expression") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "Unterminated Expression") } variableList.append(VariableSpec(name: variableName, modifier: modifier)) @@ -99,7 +99,7 @@ struct Scanner { currentIndex = unicodeScalars.index(after: currentIndex) complete = true default: - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "Unexpected Character in Expression") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "Unexpected Character in Expression") } } @@ -110,9 +110,9 @@ struct Scanner { let endIndex = scanUpTo(characterSet: invertedVarnameCharacterSet) let variableName = string[currentIndex.. 4 { - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "Prefix modifier length too large") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "Prefix modifier length too large") } guard let length = Int(lengthString) else { - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "Cannot parse prefix modifier length") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "Cannot parse prefix modifier length") } currentIndex = endIndex return .prefix(length: length) @@ -175,7 +175,7 @@ struct Scanner { if !hexCharacterSet.contains(unicodeScalars[secondIndex]) || !hexCharacterSet.contains(unicodeScalars[thirdIndex]) { - throw URITemplate.Error.malformedTemplate(position: currentIndex, reason: "% must be percent-encoded in literal") + throw URITemplate.Error(type: .malformedTemplate, position: currentIndex, reason: "% must be percent-encoded in literal") } currentIndex = unicodeScalars.index(after: thirdIndex) diff --git a/Sources/ScreamURITemplate/URITemplate.swift b/Sources/ScreamURITemplate/URITemplate.swift index 2ec7c13..c9aceb6 100644 --- a/Sources/ScreamURITemplate/URITemplate.swift +++ b/Sources/ScreamURITemplate/URITemplate.swift @@ -17,11 +17,20 @@ import Foundation /// An [RFC6570](https://tools.ietf.org/html/rfc6570) URI Template public struct URITemplate { /// An error that may be thrown when parsing or processing a template - public enum Error: Swift.Error { - /// Represents an error parsing a string into a URI Template - case malformedTemplate(position: String.Index, reason: String) - /// Represents an error processing a template - case expansionFailure(position: String.Index, reason: String) + public struct Error: Swift.Error { + public enum ErrorType: Sendable { + /// Represents an error parsing a string into a URI Template + case malformedTemplate + /// Represents an error processing a template + case expansionFailure + } + + /// The type of the error + public let type: ErrorType + /// The position in the template that the error occurred + public let position: String.Index + /// The reason for the error + public let reason: String } private let string: String @@ -30,7 +39,7 @@ public struct URITemplate { /// Initializes a URITemplate from a string /// - Parameter string: the string representation of the URI Template /// - /// - Throws: `URITemplate.Error.malformedTemplate` if the string is not a valid URI Template + /// - Throws: `URITemplate.Error` with `type = .malformedTemplate` if the string is not a valid URI Template public init(string: String) throws { var components: [Component] = [] var scanner = Scanner(string: string) @@ -46,7 +55,7 @@ public struct URITemplate { /// /// - Returns: The result of processing the template /// - /// - Throws: `URITemplate.Error.expansionFailure` if an error occurs processing the template + /// - Throws: `URITemplate.Error` with `type = .expansionFailure` if an error occurs processing the template public func process(variables: TypedVariableProvider) throws -> String { var result = "" for component in components { @@ -63,7 +72,7 @@ public struct URITemplate { /// /// - Returns: The result of processing the template /// - /// - Throws: `URITemplate.Error.expansionFailure` if an error occurs processing the template + /// - Throws: `URITemplate.Error` with `type = .expansionFailure` if an error occurs processing the template public func process(variables: VariableProvider) throws -> String { struct TypedVariableProviderWrapper: TypedVariableProvider { let variables: VariableProvider @@ -84,7 +93,7 @@ public struct URITemplate { /// /// - Returns: The result of processing the template /// - /// - Throws: `URITemplate.Error.expansionFailure` if an error occurs processing the template + /// - Throws: `URITemplate.Error` with `type = .expansionFailure` if an error occurs processing the template public func process(variables: [String: String]) throws -> String { return try process(variables: variables as VariableDictionary) } diff --git a/Tests/ScreamURITemplateTests/TestFileTests.swift b/Tests/ScreamURITemplateTests/TestFileTests.swift index f96bdff..f865743 100644 --- a/Tests/ScreamURITemplateTests/TestFileTests.swift +++ b/Tests/ScreamURITemplateTests/TestFileTests.swift @@ -37,20 +37,12 @@ class TestFileTests: XCTestCase { let template = try URITemplate(string: templateString) _ = try template.process(variables: variables) XCTFail("Did not throw") - } catch let URITemplate.Error.malformedTemplate(position, reason) { + } catch let error as URITemplate.Error { if failReason != nil { - XCTAssertEqual(failReason, reason) + XCTAssertEqual(failReason, error.reason) } if failPosition != nil { - let characters = templateString[.. Date: Sun, 1 Dec 2024 21:46:02 +1100 Subject: [PATCH 11/24] Leverage Typed Throws --- README.md | 2 +- .../Internal/Components.swift | 47 +++++++++++-------- .../ScreamURITemplate/Internal/Scanner.swift | 16 +++---- .../Internal/ValueFormatting.swift | 22 ++++----- Sources/ScreamURITemplate/URITemplate.swift | 8 ++-- .../TestFileTests.swift | 4 +- 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index af4bc0c..071a2c2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The error cases contain associated values specifying a string reason for the err ```swift do { _ = try URITemplate(string: "https://api.github.com/repos/{}/{repository}") -} catch let error as URITemplate.Error { +} catch { // error.reason = "Empty Variable Name" // error.position = 29th character } diff --git a/Sources/ScreamURITemplate/Internal/Components.swift b/Sources/ScreamURITemplate/Internal/Components.swift index 2bde086..3f70de1 100644 --- a/Sources/ScreamURITemplate/Internal/Components.swift +++ b/Sources/ScreamURITemplate/Internal/Components.swift @@ -17,7 +17,7 @@ import Foundation typealias ComponentBase = Sendable protocol Component: ComponentBase { - func expand(variables: TypedVariableProvider) throws -> String + func expand(variables: TypedVariableProvider) throws(URITemplate.Error) -> String var variableNames: [String] { get } } @@ -33,7 +33,7 @@ struct LiteralComponent: Component { literal = string } - func expand(variables _: TypedVariableProvider) throws -> String { + func expand(variables _: TypedVariableProvider) throws(URITemplate.Error) -> String { let expansion = String(literal) guard let encodedExpansion = expansion.addingPercentEncoding(withAllowedCharacters: reservedAndUnreservedCharacterSet) else { throw URITemplate.Error(type: .expansionFailure, position: literal.startIndex, reason: "Percent Encoding Failed") @@ -48,7 +48,7 @@ struct LiteralPercentEncodedTripletComponent: Component { literal = string } - func expand(variables _: TypedVariableProvider) throws -> String { + func expand(variables _: TypedVariableProvider) throws(URITemplate.Error) -> String { return String(literal) } } @@ -64,28 +64,35 @@ struct ExpressionComponent: Component { self.templatePosition = templatePosition } - func expand(variables: TypedVariableProvider) throws -> String { + func expand(variables: TypedVariableProvider) throws(URITemplate.Error) -> String { let configuration = expressionOperator.expansionConfiguration() - let expansions = try variableList.compactMap { variableSpec -> String? in - guard let value = variables[String(variableSpec.name)] else { - return nil + do { + let expansions = try variableList.compactMap { variableSpec throws(URITemplate.Error) -> String? in + guard let value = variables[String(variableSpec.name)] else { + return nil + } + do throws(FormatError) { + return try value.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration) + } catch { + throw URITemplate.Error(type: .expansionFailure, position: templatePosition, reason: "Failed expanding variable \"\(variableSpec.name)\": \(error.reason)") + } } - do { - return try value.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration) - } catch let error as FormatError { - throw URITemplate.Error(type: .expansionFailure, position: templatePosition, reason: "Failed expanding variable \"\(variableSpec.name)\": \(error.reason)") - } - } - if expansions.count == 0 { - return "" - } + if expansions.count == 0 { + return "" + } - let joinedExpansions = expansions.joined(separator: configuration.separator) - if let prefix = configuration.prefix { - return prefix + joinedExpansions + let joinedExpansions = expansions.joined(separator: configuration.separator) + if let prefix = configuration.prefix { + return prefix + joinedExpansions + } + return joinedExpansions + } catch let error as URITemplate.Error { + throw error + } catch { + // compactMap is not marked up for Typed Throws, the compiler therefore does not know that this is not possible + throw URITemplate.Error(type: .expansionFailure, position: templatePosition, reason: "Failed expanding variable: \(error.localizedDescription)") } - return joinedExpansions } var variableNames: [String] { diff --git a/Sources/ScreamURITemplate/Internal/Scanner.swift b/Sources/ScreamURITemplate/Internal/Scanner.swift index c775ce9..69ccc01 100644 --- a/Sources/ScreamURITemplate/Internal/Scanner.swift +++ b/Sources/ScreamURITemplate/Internal/Scanner.swift @@ -33,7 +33,7 @@ struct Scanner { return currentIndex >= unicodeScalars.endIndex } - mutating func scanComponent() throws -> Component { + mutating func scanComponent() throws(URITemplate.Error) -> Component { let nextScalar = unicodeScalars[currentIndex] switch nextScalar { @@ -48,7 +48,7 @@ struct Scanner { } } - private mutating func scanExpressionComponent() throws -> Component { + private mutating func scanExpressionComponent() throws(URITemplate.Error) -> Component { assert(unicodeScalars[currentIndex] == "{") let expressionStartIndex = currentIndex currentIndex = unicodeScalars.index(after: currentIndex) @@ -59,7 +59,7 @@ struct Scanner { return ExpressionComponent(expressionOperator: expressionOperator, variableList: variableList, templatePosition: expressionStartIndex) } - private mutating func scanExpressionOperator() throws -> ExpressionOperator { + private mutating func scanExpressionOperator() throws(URITemplate.Error) -> ExpressionOperator { let expressionOperator: ExpressionOperator if expressionOperatorCharacterSet.contains(unicodeScalars[currentIndex]) { guard let `operator` = ExpressionOperator(rawValue: unicodeScalars[currentIndex]) else { @@ -73,7 +73,7 @@ struct Scanner { return expressionOperator } - private mutating func scanVariableList() throws -> [VariableSpec] { + private mutating func scanVariableList() throws(URITemplate.Error) -> [VariableSpec] { var variableList: [VariableSpec] = [] var complete = false @@ -106,7 +106,7 @@ struct Scanner { return variableList } - private mutating func scanVariableName() throws -> Substring { + private mutating func scanVariableName() throws(URITemplate.Error) -> Substring { let endIndex = scanUpTo(characterSet: invertedVarnameCharacterSet) let variableName = string[currentIndex.. VariableSpec.Modifier { + private mutating func scanVariableModifier() throws(URITemplate.Error) -> VariableSpec.Modifier { switch unicodeScalars[currentIndex] { case "*": currentIndex = unicodeScalars.index(after: currentIndex) @@ -157,7 +157,7 @@ struct Scanner { } } - private mutating func scanLiteralComponent() throws -> Component { + private mutating func scanLiteralComponent() throws(URITemplate.Error) -> Component { assert(literalCharacterSet.contains(unicodeScalars[currentIndex])) let startIndex = currentIndex @@ -166,7 +166,7 @@ struct Scanner { return LiteralComponent(string[startIndex.. Component { + private mutating func scanPercentEncodingComponent() throws(URITemplate.Error) -> Component { assert(unicodeScalars[currentIndex] == "%") let startIndex = currentIndex diff --git a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift index bd0785c..9efc478 100644 --- a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift +++ b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift @@ -19,7 +19,7 @@ struct FormatError: Error { } extension TypedVariableValue { - func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration configuration: ExpansionConfiguration) throws -> String? { + func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration configuration: ExpansionConfiguration) throws(FormatError) -> String? { switch self { case let .string(plainValue): return try plainValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration) @@ -45,7 +45,7 @@ extension TypedVariableValue { } } -private func percentEncode(string: String, withAllowedCharacters allowedCharacterSet: CharacterSet, allowPercentEncodedTriplets: Bool) throws -> String { +private func percentEncode(string: String, withAllowedCharacters allowedCharacterSet: CharacterSet, allowPercentEncodedTriplets: Bool) throws(FormatError) -> String { guard var encoded = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) else { throw FormatError(reason: "Percent Encoding Failed") } @@ -73,7 +73,7 @@ private func percentEncode(string: String, withAllowedCharacters allowedCharacte } private extension StringProtocol { - func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String { + func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws(FormatError) -> String { let modifiedValue = if let prefixLength = variableSpec.prefixLength() { String(prefix(prefixLength)) } else { @@ -91,9 +91,9 @@ private extension StringProtocol { } private extension Array where Element: StringProtocol { - func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? { + func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws(FormatError) -> String? { let separator = "," - let encodedExpansions = try map { element -> String in + let encodedExpansions = try map { element throws(FormatError) -> String in return try percentEncode(string: String(element), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets) } if encodedExpansions.count == 0 { @@ -109,9 +109,9 @@ private extension Array where Element: StringProtocol { return expansion } - func explodeForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? { + func explodeForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws(FormatError) -> String? { let separator = expansionConfiguration.separator - let encodedExpansions = try map { element -> String in + let encodedExpansions = try map { element throws(FormatError) -> String in let encodedElement = try percentEncode(string: String(element), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets) if expansionConfiguration.named { if encodedElement.isEmpty && expansionConfiguration.omitOrphanedEquals { @@ -129,8 +129,8 @@ private extension Array where Element: StringProtocol { } private extension [(key: String, value: String)] { - func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? { - let encodedExpansions = try map { key, value -> String in + func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws(FormatError) -> String? { + let encodedExpansions = try map { key, value throws(FormatError) -> String in let encodedKey = try percentEncode(string: String(key), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets) let encodedValue = try percentEncode(string: String(value), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets) return "\(encodedKey),\(encodedValue)" @@ -145,9 +145,9 @@ private extension [(key: String, value: String)] { return expansion } - func explodeForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? { + func explodeForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws(FormatError) -> String? { let separator = expansionConfiguration.separator - let encodedExpansions = try map { key, value -> String in + let encodedExpansions = try map { key, value throws(FormatError) -> String in let encodedKey = try percentEncode(string: String(key), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets) let encodedValue = try percentEncode(string: String(value), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets) if expansionConfiguration.named && encodedValue.isEmpty && expansionConfiguration.omitOrphanedEquals { diff --git a/Sources/ScreamURITemplate/URITemplate.swift b/Sources/ScreamURITemplate/URITemplate.swift index c9aceb6..fc05ec8 100644 --- a/Sources/ScreamURITemplate/URITemplate.swift +++ b/Sources/ScreamURITemplate/URITemplate.swift @@ -40,7 +40,7 @@ public struct URITemplate { /// - Parameter string: the string representation of the URI Template /// /// - Throws: `URITemplate.Error` with `type = .malformedTemplate` if the string is not a valid URI Template - public init(string: String) throws { + public init(string: String) throws(URITemplate.Error) { var components: [Component] = [] var scanner = Scanner(string: string) while !scanner.isComplete { @@ -56,7 +56,7 @@ public struct URITemplate { /// - Returns: The result of processing the template /// /// - Throws: `URITemplate.Error` with `type = .expansionFailure` if an error occurs processing the template - public func process(variables: TypedVariableProvider) throws -> String { + public func process(variables: TypedVariableProvider) throws(URITemplate.Error) -> String { var result = "" for component in components { result += try component.expand(variables: variables) @@ -73,7 +73,7 @@ public struct URITemplate { /// - Returns: The result of processing the template /// /// - Throws: `URITemplate.Error` with `type = .expansionFailure` if an error occurs processing the template - public func process(variables: VariableProvider) throws -> String { + public func process(variables: VariableProvider) throws(URITemplate.Error) -> String { struct TypedVariableProviderWrapper: TypedVariableProvider { let variables: VariableProvider @@ -94,7 +94,7 @@ public struct URITemplate { /// - Returns: The result of processing the template /// /// - Throws: `URITemplate.Error` with `type = .expansionFailure` if an error occurs processing the template - public func process(variables: [String: String]) throws -> String { + public func process(variables: [String: String]) throws(URITemplate.Error) -> String { return try process(variables: variables as VariableDictionary) } diff --git a/Tests/ScreamURITemplateTests/TestFileTests.swift b/Tests/ScreamURITemplateTests/TestFileTests.swift index f865743..b51f1ba 100644 --- a/Tests/ScreamURITemplateTests/TestFileTests.swift +++ b/Tests/ScreamURITemplateTests/TestFileTests.swift @@ -37,7 +37,7 @@ class TestFileTests: XCTestCase { let template = try URITemplate(string: templateString) _ = try template.process(variables: variables) XCTFail("Did not throw") - } catch let error as URITemplate.Error { + } catch { if failReason != nil { XCTAssertEqual(failReason, error.reason) } @@ -45,8 +45,6 @@ class TestFileTests: XCTestCase { let characters = templateString[.. Date: Mon, 2 Dec 2024 16:38:01 +1100 Subject: [PATCH 12/24] Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 063dacb..91091b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,8 @@ jobs: - name: Prepare coverage file run: xcrun llvm-cov export -format="lcov" .build/debug/ScreamURITemplatePackageTests.xctest/Contents/MacOS/ScreamURITemplatePackageTests -instr-profile .build/debug/codecov/default.profdata > info.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: codecov/codecov-action@v5 with: + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true From 59dc884c7f2c6bda5d7540fc2ce58fbbe4c64c24 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 2 Dec 2024 21:42:40 +1100 Subject: [PATCH 13/24] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 071a2c2..2ba7483 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ScreamURITemplate -A robust and performant Swift 5 implementation of [RFC6570](https://tools.ietf.org/html/rfc6570) URI Template. Full Level 4 support is provided. +A robust and performant Swift 6 implementation of [RFC6570](https://tools.ietf.org/html/rfc6570) URI Template. Full Level 4 support is provided. [![CI](https://github.com/SwiftScream/URITemplate/actions/workflows/ci.yml/badge.svg)](https://github.com/SwiftScream/URITemplate/actions/workflows/ci.yml) [![Codecov branch](https://img.shields.io/codecov/c/github/SwiftScream/URITemplate/master.svg)](https://codecov.io/gh/SwiftScream/URITemplate/branch/master) @@ -13,7 +13,7 @@ A robust and performant Swift 5 implementation of [RFC6570](https://tools.ietf.o ## Getting Started ### Swift Package Manager -Add `.package(url: "https://github.com/SwiftScream/URITemplate.git", from: "3.0.0")` to your Package.swift dependencies +Add `.package(url: "https://github.com/SwiftScream/URITemplate.git", from: "5.0.0")` to your Package.swift dependencies ## Usage From bbeca5c5a5af3ddc5a755c8bfc2b6d686b9291d0 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Wed, 4 Dec 2024 09:26:23 +1100 Subject: [PATCH 14/24] Add URITemplate freestanding expression macro --- .spi.yml | 2 +- Package.swift | 25 ++++++- .../URITemplateCompilerPlugin.swift | 21 ++++++ .../URITemplateMacro.swift | 49 +++++++++++++ Sources/ScreamURITemplateExample/main.swift | 4 + Sources/ScreamURITemplateMacros/Macros.swift | 26 +++++++ Tests/ScreamURITemplateTests/MacroTests.swift | 73 +++++++++++++++++++ 7 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift create mode 100644 Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift create mode 100644 Sources/ScreamURITemplateMacros/Macros.swift create mode 100644 Tests/ScreamURITemplateTests/MacroTests.swift diff --git a/.spi.yml b/.spi.yml index 5ff4ae0..54245db 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [ScreamURITemplate] \ No newline at end of file + - documentation_targets: [ScreamURITemplate, ScreamURITemplateMacros] diff --git a/Package.swift b/Package.swift index 69239da..03a7c2e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,25 +1,44 @@ // swift-tools-version: 6.0 +import CompilerPluginSupport import PackageDescription let package = Package( name: "ScreamURITemplate", + platforms: [.macOS(.v13)], products: [ .library( name: "ScreamURITemplate", targets: ["ScreamURITemplate"]), + .library( + name: "ScreamURITemplateMacros", + targets: ["ScreamURITemplateMacros"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-syntax", from: "600.0.1"), ], targets: [ .target( name: "ScreamURITemplate", - dependencies: [], resources: [.process("PrivacyInfo.xcprivacy")]), + .target( + name: "ScreamURITemplateMacros", + dependencies: ["ScreamURITemplate", "ScreamURITemplateCompilerPlugin"]), + .macro( + name: "ScreamURITemplateCompilerPlugin", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + "ScreamURITemplate", + ]), .testTarget( name: "ScreamURITemplateTests", - dependencies: ["ScreamURITemplate"], + dependencies: [ + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + "ScreamURITemplate", + "ScreamURITemplateCompilerPlugin", + ], exclude: [ "data/uritemplate-test/json2xml.xslt", "data/uritemplate-test/LICENSE", @@ -35,6 +54,6 @@ let package = Package( ]), .executableTarget( name: "ScreamURITemplateExample", - dependencies: ["ScreamURITemplate"]), + dependencies: ["ScreamURITemplate", "ScreamURITemplateMacros"]), ], swiftLanguageModes: [.v6]) diff --git a/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift new file mode 100644 index 0000000..1e17c9e --- /dev/null +++ b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift @@ -0,0 +1,21 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct URITemplateCompilerPlugin: CompilerPlugin { + var providingMacros: [Macro.Type] = [URITemplateMacro.self] +} diff --git a/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift new file mode 100644 index 0000000..ea55601 --- /dev/null +++ b/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift @@ -0,0 +1,49 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +import ScreamURITemplate + +public struct URITemplateMacro: ExpressionMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in _: some MacroExpansionContext) throws -> ExprSyntax { + guard let argument = node.arguments.first?.expression, + let segments = argument.as(StringLiteralExprSyntax.self)?.segments, + segments.count == 1, + case let .stringSegment(literalSegment)? = segments.first + else { + throw DiagnosticsError(diagnostics: [ + Diagnostic(node: node, + message: MacroExpansionErrorMessage("#URITemplate requires a static string literal")), + ]) + } + + let uriTemplateString = literalSegment.content.text + do { + _ = try URITemplate(string: uriTemplateString) + } catch { + throw DiagnosticsError(diagnostics: [ + Diagnostic(node: literalSegment, + message: MacroExpansionErrorMessage("Invalid URI template: \(error.reason) at \"\(uriTemplateString.suffix(from: error.position).prefix(50))\"")), + ]) + } + + return "try! URITemplate(string: \(argument))" + } +} diff --git a/Sources/ScreamURITemplateExample/main.swift b/Sources/ScreamURITemplateExample/main.swift index 13d8d9d..097d488 100644 --- a/Sources/ScreamURITemplateExample/main.swift +++ b/Sources/ScreamURITemplateExample/main.swift @@ -14,6 +14,7 @@ import Foundation import ScreamURITemplate +import ScreamURITemplateMacros let template = try URITemplate(string: "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}") let variables = [ @@ -27,3 +28,6 @@ let urlString = try template.process(variables: variables) let url = URL(string: urlString)! print("Expanding \(template)\n with \(variables):\n") print(url.absoluteString) + +let macroExpansion = #URITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}") +print(macroExpansion) diff --git a/Sources/ScreamURITemplateMacros/Macros.swift b/Sources/ScreamURITemplateMacros/Macros.swift new file mode 100644 index 0000000..6bedda6 --- /dev/null +++ b/Sources/ScreamURITemplateMacros/Macros.swift @@ -0,0 +1,26 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ScreamURITemplate + +/// Macro providing compile-time validation of a URITemplate represented by a string literal +/// Example: +/// ```swift +/// let template = #URITemplate("https://api.github.com/repos/{owner}") +/// ``` +/// - Parameters: +/// - : A string literal representing the URI Template +/// - Returns: A `URITemplate` constructed from the string literal +@freestanding(expression) +public macro URITemplate(_ stringLiteral: StaticString) -> URITemplate = #externalMacro(module: "ScreamURITemplateCompilerPlugin", type: "URITemplateMacro") diff --git a/Tests/ScreamURITemplateTests/MacroTests.swift b/Tests/ScreamURITemplateTests/MacroTests.swift new file mode 100644 index 0000000..593782b --- /dev/null +++ b/Tests/ScreamURITemplateTests/MacroTests.swift @@ -0,0 +1,73 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(ScreamURITemplateCompilerPlugin) + import ScreamURITemplateCompilerPlugin + + import SwiftSyntaxMacros + import SwiftSyntaxMacrosTestSupport + + import XCTest + + class MacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "URITemplate": URITemplateMacro.self, + ] + + func testValidURITemplateMacro() throws { + assertMacroExpansion( + #""" + #URITemplate("https://api.github.com/repos/{owner}") + """#, + expandedSource: + #""" + try! URITemplate(string: "https://api.github.com/repos/{owner}") + """#, + diagnostics: [], + macros: testMacros) + } + + func testInvalidURITemplateMacro() throws { + assertMacroExpansion( + #""" + #URITemplate("https://api.github.com/repos/{}/{repo}") + """#, + expandedSource: + #""" + #URITemplate("https://api.github.com/repos/{}/{repo}") + """#, + diagnostics: [ + DiagnosticSpec(message: "Invalid URI template: Empty Variable Name at \"}/{repo}\"", line: 1, column: 15), + ], + macros: testMacros) + } + + func testMisusedURITemplateMacro() throws { + assertMacroExpansion( + #""" + let s: StaticString = "https://api.github.com/repos/{owner}" + #URITemplate(s) + """#, + expandedSource: + #""" + let s: StaticString = "https://api.github.com/repos/{owner}" + #URITemplate(s) + """#, + diagnostics: [ + DiagnosticSpec(message: "#URITemplate requires a static string literal", line: 2, column: 1), + ], + macros: testMacros) + } + } +#endif From c14d9d00114a63a14f2dd532b8401831df69d92b Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Tue, 3 Dec 2024 21:53:20 +1100 Subject: [PATCH 15/24] fix some documentation warnings --- Sources/ScreamURITemplate/VariableProvider.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/ScreamURITemplate/VariableProvider.swift b/Sources/ScreamURITemplate/VariableProvider.swift index d969a61..d13b4ae 100644 --- a/Sources/ScreamURITemplate/VariableProvider.swift +++ b/Sources/ScreamURITemplate/VariableProvider.swift @@ -21,8 +21,7 @@ public protocol VariableProvider { /// Get the ``VariableValue`` for a given variable /// /// - Parameters: - /// - _: the name of the variable - /// + /// - : the name of the variable /// - Returns: the ``VariableValue`` for the variable, or `nil` if the variable has no value subscript(_: String) -> VariableValue? { get } } @@ -36,7 +35,7 @@ public protocol TypedVariableProvider { /// Get the ``TypedVariableValue`` for a given variable /// /// - Parameters: - /// - _: the name of the variable + /// - : the name of the variable /// /// - Returns: the ``TypedVariableValue`` for the variable, or `nil` if the variable has no value subscript(_: String) -> TypedVariableValue? { get } From cf23d67f89669bf97a8b1eb5f14eebcb31d78119 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Wed, 4 Dec 2024 09:40:12 +1100 Subject: [PATCH 16/24] Add .vscode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 046f489..55a8df4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store .build .swiftpm +.vscode Package.resolved From b3f89259b7de24dcd03392c3a0ea98e5d7b71e56 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Wed, 4 Dec 2024 22:19:22 +1100 Subject: [PATCH 17/24] Add a macro helper to get string literal value --- .../ExprSyntax+Literals.swift | 27 +++++++++++++++++++ .../URITemplateMacro.swift | 7 ++--- Tests/ScreamURITemplateTests/MacroTests.swift | 2 +- 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift diff --git a/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift b/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift new file mode 100644 index 0000000..b693d82 --- /dev/null +++ b/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift @@ -0,0 +1,27 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftSyntax + +extension ExprSyntax { + func stringLiteral() -> String? { + guard let segments = self.as(StringLiteralExprSyntax.self)?.segments, + segments.count == 1, + case let .stringSegment(literalSegment)? = segments.first else { + return nil + } + + return literalSegment.content.text + } +} diff --git a/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift index ea55601..1762d9c 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift @@ -24,9 +24,7 @@ public struct URITemplateMacro: ExpressionMacro { of node: some FreestandingMacroExpansionSyntax, in _: some MacroExpansionContext) throws -> ExprSyntax { guard let argument = node.arguments.first?.expression, - let segments = argument.as(StringLiteralExprSyntax.self)?.segments, - segments.count == 1, - case let .stringSegment(literalSegment)? = segments.first + let uriTemplateString = argument.stringLiteral() else { throw DiagnosticsError(diagnostics: [ Diagnostic(node: node, @@ -34,12 +32,11 @@ public struct URITemplateMacro: ExpressionMacro { ]) } - let uriTemplateString = literalSegment.content.text do { _ = try URITemplate(string: uriTemplateString) } catch { throw DiagnosticsError(diagnostics: [ - Diagnostic(node: literalSegment, + Diagnostic(node: argument, message: MacroExpansionErrorMessage("Invalid URI template: \(error.reason) at \"\(uriTemplateString.suffix(from: error.position).prefix(50))\"")), ]) } diff --git a/Tests/ScreamURITemplateTests/MacroTests.swift b/Tests/ScreamURITemplateTests/MacroTests.swift index 593782b..06f0659 100644 --- a/Tests/ScreamURITemplateTests/MacroTests.swift +++ b/Tests/ScreamURITemplateTests/MacroTests.swift @@ -48,7 +48,7 @@ #URITemplate("https://api.github.com/repos/{}/{repo}") """#, diagnostics: [ - DiagnosticSpec(message: "Invalid URI template: Empty Variable Name at \"}/{repo}\"", line: 1, column: 15), + DiagnosticSpec(message: "Invalid URI template: Empty Variable Name at \"}/{repo}\"", line: 1, column: 14), ], macros: testMacros) } From 5f4950fc630231cbe3ea718142a11cdb2323e97e Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Thu, 5 Dec 2024 10:28:19 +1100 Subject: [PATCH 18/24] Tidy URITemplateMacro tests --- .../{MacroTests.swift => URITemplateMacroTests.swift} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename Tests/ScreamURITemplateTests/{MacroTests.swift => URITemplateMacroTests.swift} (92%) diff --git a/Tests/ScreamURITemplateTests/MacroTests.swift b/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift similarity index 92% rename from Tests/ScreamURITemplateTests/MacroTests.swift rename to Tests/ScreamURITemplateTests/URITemplateMacroTests.swift index 06f0659..d2bb43e 100644 --- a/Tests/ScreamURITemplateTests/MacroTests.swift +++ b/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift @@ -20,12 +20,12 @@ import XCTest - class MacroTests: XCTestCase { + class URITemplateMacroTests: XCTestCase { let testMacros: [String: Macro.Type] = [ "URITemplate": URITemplateMacro.self, ] - func testValidURITemplateMacro() throws { + func testValid() throws { assertMacroExpansion( #""" #URITemplate("https://api.github.com/repos/{owner}") @@ -38,7 +38,7 @@ macros: testMacros) } - func testInvalidURITemplateMacro() throws { + func testInvalid() throws { assertMacroExpansion( #""" #URITemplate("https://api.github.com/repos/{}/{repo}") @@ -53,7 +53,7 @@ macros: testMacros) } - func testMisusedURITemplateMacro() throws { + func testMisused() throws { assertMacroExpansion( #""" let s: StaticString = "https://api.github.com/repos/{owner}" From bc7174a1502256b6e484e7f5ea3bcec0816c4e1f Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Thu, 5 Dec 2024 17:26:37 +1100 Subject: [PATCH 19/24] Add macro to perform compile-time template processing --- .../ExprSyntax+Literals.swift | 17 ++ .../URITemplateCompilerPlugin.swift | 5 +- .../URLByExpandingURITemplateMacro.swift | 72 ++++++++ Sources/ScreamURITemplateExample/main.swift | 4 + Sources/ScreamURITemplateMacros/Macros.swift | 13 ++ .../URITemplateMacroTests.swift | 7 +- .../URLByExpandingURITemplateMacroTests.swift | 172 ++++++++++++++++++ 7 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift create mode 100644 Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift diff --git a/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift b/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift index b693d82..7d8d198 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift @@ -24,4 +24,21 @@ extension ExprSyntax { return literalSegment.content.text } + + func dictionaryLiteral() -> [String: String]? { + guard let elements = self.as(DictionaryExprSyntax.self)?.content.as(DictionaryElementListSyntax.self) else { + return nil + } + + var result: [String: String] = [:] + for element in elements { + guard let key = element.key.stringLiteral(), + let value = element.value.stringLiteral() else { + return nil + } + result[key] = value + } + + return result + } } diff --git a/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift index 1e17c9e..09af1e4 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift @@ -17,5 +17,8 @@ import SwiftSyntaxMacros @main struct URITemplateCompilerPlugin: CompilerPlugin { - var providingMacros: [Macro.Type] = [URITemplateMacro.self] + var providingMacros: [Macro.Type] = [ + URITemplateMacro.self, + URLByExpandingURITemplateMacro.self, + ] } diff --git a/Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift new file mode 100644 index 0000000..2df7d5b --- /dev/null +++ b/Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift @@ -0,0 +1,72 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +import ScreamURITemplate + +public struct URLByExpandingURITemplateMacro: ExpressionMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in _: some MacroExpansionContext) throws -> ExprSyntax { + guard let templateArgument = node.arguments.first?.expression, + let uriTemplateString = templateArgument.stringLiteral() else { + throw DiagnosticsError(diagnostics: [ + Diagnostic(node: node, + message: MacroExpansionErrorMessage("#URLByExpandingURITemplate requires a static string literal for the first argument")), + ]) + } + + guard let paramsArgument = node.arguments.last?.expression, + let params = paramsArgument.dictionaryLiteral() else { + throw DiagnosticsError(diagnostics: [ + Diagnostic(node: node, + message: MacroExpansionErrorMessage("#URLByExpandingURITemplate requires a Dictionary Literal of string literals for the second argument")), + ]) + } + + let template: URITemplate + do { + template = try URITemplate(string: uriTemplateString) + } catch { + throw DiagnosticsError(diagnostics: [ + Diagnostic(node: templateArgument, + message: MacroExpansionErrorMessage("Invalid URI template: \(error.reason) at \"\(uriTemplateString.suffix(from: error.position).prefix(50))\"")), + ]) + } + + let processedTemplate: String + do { + processedTemplate = try template.process(variables: params) + } catch { + throw DiagnosticsError(diagnostics: [ + Diagnostic(node: node, + message: MacroExpansionErrorMessage("Failed to process template: \(error.reason)")), + ]) + } + + guard URL(string: processedTemplate) != nil else { + throw DiagnosticsError(diagnostics: [ + Diagnostic(node: node, + message: MacroExpansionErrorMessage("Processed template does not form a valid URL\n\(processedTemplate)")), + ]) + } + + return "URL(string: \(processedTemplate.makeLiteralSyntax()))!" + } +} diff --git a/Sources/ScreamURITemplateExample/main.swift b/Sources/ScreamURITemplateExample/main.swift index 097d488..97c86d3 100644 --- a/Sources/ScreamURITemplateExample/main.swift +++ b/Sources/ScreamURITemplateExample/main.swift @@ -31,3 +31,7 @@ print(url.absoluteString) let macroExpansion = #URITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}") print(macroExpansion) + +let urlExpansion = #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", + with: ["owner": "SwiftScream", "repo": "URITemplate", "username": "alexdeem"]) +print(urlExpansion) diff --git a/Sources/ScreamURITemplateMacros/Macros.swift b/Sources/ScreamURITemplateMacros/Macros.swift index 6bedda6..2654e03 100644 --- a/Sources/ScreamURITemplateMacros/Macros.swift +++ b/Sources/ScreamURITemplateMacros/Macros.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation import ScreamURITemplate /// Macro providing compile-time validation of a URITemplate represented by a string literal @@ -24,3 +25,15 @@ import ScreamURITemplate /// - Returns: A `URITemplate` constructed from the string literal @freestanding(expression) public macro URITemplate(_ stringLiteral: StaticString) -> URITemplate = #externalMacro(module: "ScreamURITemplateCompilerPlugin", type: "URITemplateMacro") + +/// Macro providing compile-time validation and processing of a URITemplate and parameters entirely represented by string literals +/// Example: +/// ```swift +/// let template = #URLByExpandingURITemplate("https://api.github.com/repos/{owner}", with: ["owner": "SwiftScream"]) +/// ``` +/// - Parameters: +/// - : A string literal representing the URI Template +/// - with: The parameters to use to process the template, represented by a dictionary literal where the keys and values are all string literals +/// - Returns: A `URL` constructed from the result of processing the template with the parameters +@freestanding(expression) +public macro URLByExpandingURITemplate(_ stringLiteral: StaticString, with: KeyValuePairs) -> URL = #externalMacro(module: "ScreamURITemplateCompilerPlugin", type: "URLByExpandingURITemplateMacro") diff --git a/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift b/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift index d2bb43e..e9941d1 100644 --- a/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift +++ b/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift @@ -13,7 +13,7 @@ // limitations under the License. #if canImport(ScreamURITemplateCompilerPlugin) - import ScreamURITemplateCompilerPlugin + @testable import ScreamURITemplateCompilerPlugin import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport @@ -25,6 +25,11 @@ "URITemplate": URITemplateMacro.self, ] + func testMacroAvailability() { + let plugin = URITemplateCompilerPlugin() + XCTAssert(plugin.providingMacros.contains { $0 == URITemplateMacro.self }) + } + func testValid() throws { assertMacroExpansion( #""" diff --git a/Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift b/Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift new file mode 100644 index 0000000..86f5758 --- /dev/null +++ b/Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift @@ -0,0 +1,172 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(ScreamURITemplateCompilerPlugin) + @testable import ScreamURITemplateCompilerPlugin + + import SwiftSyntaxMacros + import SwiftSyntaxMacrosTestSupport + + import XCTest + + class URLByExpandingURITemplateMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "URLByExpandingURITemplate": URLByExpandingURITemplateMacro.self, + ] + + func testMacroAvailability() { + let plugin = URITemplateCompilerPlugin() + XCTAssert(plugin.providingMacros.contains { $0 == URLByExpandingURITemplateMacro.self }) + } + + func testValid() throws { + assertMacroExpansion( + #""" + #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", [ + "owner": "SwiftScream", + "repo": "URITemplate", + "username": "alexdeem", + ]) + """#, + expandedSource: + #""" + URL(string: "https://api.github.com/repos/SwiftScream/URITemplate/collaborators/alexdeem")! + """#, + diagnostics: [], + macros: testMacros) + } + + func testInvalidTemplate() throws { + assertMacroExpansion( + #""" + #URLByExpandingURITemplate("https://api.github.com/repos/{}/{repo}/collaborators/{username}", [ + "owner": "SwiftScream", + "repo": "URITemplate", + "username": "alexdeem", + ]) + """#, + expandedSource: + #""" + #URLByExpandingURITemplate("https://api.github.com/repos/{}/{repo}/collaborators/{username}", [ + "owner": "SwiftScream", + "repo": "URITemplate", + "username": "alexdeem", + ]) + """#, + diagnostics: [ + DiagnosticSpec(message: "Invalid URI template: Empty Variable Name at \"}/{repo}/collaborators/{username}\"", line: 1, column: 28), + ], + macros: testMacros) + } + + func testInvalidURL() throws { + assertMacroExpansion( + #""" + #URLByExpandingURITemplate("{nope}", ["nope": ""]) + """#, + expandedSource: + #""" + #URLByExpandingURITemplate("{nope}", ["nope": ""]) + """#, + diagnostics: [ + DiagnosticSpec(message: "Processed template does not form a valid URL\n", line: 1, column: 1), + ], + macros: testMacros) + } + + func testMisusedTemplate() throws { + assertMacroExpansion( + #""" + let s: StaticString = "https://api.github.com/repos/{owner}" + #URLByExpandingURITemplate(s, [ + "owner": "SwiftScream", + ]) + """#, + expandedSource: + #""" + let s: StaticString = "https://api.github.com/repos/{owner}" + #URLByExpandingURITemplate(s, [ + "owner": "SwiftScream", + ]) + """#, + diagnostics: [ + DiagnosticSpec(message: "#URLByExpandingURITemplate requires a static string literal for the first argument", line: 2, column: 1), + ], + macros: testMacros) + } + + func testMisusedParams() throws { + assertMacroExpansion( + #""" + let params: KeyValue = ["owner": "SwiftScream"] + #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", params) + """#, + expandedSource: + #""" + let params: KeyValue = ["owner": "SwiftScream"] + #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", params) + """#, + diagnostics: [ + DiagnosticSpec(message: "#URLByExpandingURITemplate requires a Dictionary Literal of string literals for the second argument", line: 2, column: 1), + ], + macros: testMacros) + } + + func testMisusedParamKey() throws { + assertMacroExpansion( + #""" + #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", [ + "owner": "SwiftScream", + 123: "URITemplate", + "username": "alexdeem", + ]) + """#, + expandedSource: + #""" + #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", [ + "owner": "SwiftScream", + 123: "URITemplate", + "username": "alexdeem", + ]) + """#, + diagnostics: [ + DiagnosticSpec(message: "#URLByExpandingURITemplate requires a Dictionary Literal of string literals for the second argument", line: 1, column: 1), + ], + macros: testMacros) + } + + func testMisusedParamValue() throws { + assertMacroExpansion( + #""" + #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", [ + "owner": "SwiftScream", + "repo": 12345, + "username": "alexdeem", + ]) + """#, + expandedSource: + #""" + #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", [ + "owner": "SwiftScream", + "repo": 12345, + "username": "alexdeem", + ]) + """#, + diagnostics: [ + DiagnosticSpec(message: "#URLByExpandingURITemplate requires a Dictionary Literal of string literals for the second argument", line: 1, column: 1), + ], + macros: testMacros) + } + } +#endif From cce1ed020d9b2c2d72d72a362eed57b332c25428 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 30 Dec 2024 16:07:23 +1100 Subject: [PATCH 20/24] Update .swift-version to 6.0 --- .swift-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swift-version b/.swift-version index b883184..5049538 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.9 \ No newline at end of file +6.0 \ No newline at end of file From c5876f21cc306f2938063fd05007ca000331fab1 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Mon, 30 Dec 2024 15:59:00 +1100 Subject: [PATCH 21/24] Add VariableProviderMacro --- .../ProvidedMacro.swift | 26 +++ .../URITemplateCompilerPlugin.swift | 2 + .../VariableProviderMacro.swift | 85 ++++++++ Sources/ScreamURITemplateExample/main.swift | 10 + Sources/ScreamURITemplateMacros/Macros.swift | 20 ++ .../ProvidedMacroTests.swift | 54 +++++ .../VariableProviderMacroTests.swift | 188 ++++++++++++++++++ 7 files changed, 385 insertions(+) create mode 100644 Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift create mode 100644 Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift create mode 100644 Tests/ScreamURITemplateTests/ProvidedMacroTests.swift create mode 100644 Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift diff --git a/Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift new file mode 100644 index 0000000..4b90c21 --- /dev/null +++ b/Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift @@ -0,0 +1,26 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftSyntax +import SwiftSyntaxMacros + +public struct ProvidedMacro: PeerMacro { + public static func expansion( + of _: SwiftSyntax.AttributeSyntax, + providingPeersOf _: some SwiftSyntax.DeclSyntaxProtocol, + in _: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + // This macro does not generate code, it's used as a marker for @VariableProvider + return [] + } +} diff --git a/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift index 09af1e4..38c67f7 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift @@ -20,5 +20,7 @@ struct URITemplateCompilerPlugin: CompilerPlugin { var providingMacros: [Macro.Type] = [ URITemplateMacro.self, URLByExpandingURITemplateMacro.self, + VariableProviderMacro.self, + ProvidedMacro.self, ] } diff --git a/Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift new file mode 100644 index 0000000..61a087c --- /dev/null +++ b/Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift @@ -0,0 +1,85 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +private class ProviderAttributeDiagnosticVisitor: SyntaxVisitor { + var macroExpansionContext: MacroExpansionContext? + + override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind { + let attributeName = node.attributeName.trimmedDescription + if attributeName == "Provided" || attributeName == "ScreamURITemplateMacros.Provided" { + let diagnostic = Diagnostic( + node: node, + message: MacroExpansionErrorMessage("@Provided attribute nested in conditional compilation is not respected; sorry")) + macroExpansionContext?.diagnose(diagnostic) + } + return .visitChildren + } +} + +public struct VariableProviderMacro: ExtensionMacro { + public static func expansion( + of _: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo _: [TypeSyntax], + in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { + let allVariableDeclarations = declaration.memberBlock.members.compactMap { member -> VariableDeclSyntax? in + member.decl.as(VariableDeclSyntax.self) + } + let explicitlyProvidedVariableDeclarations = allVariableDeclarations.filter { declaration in + return declaration.attributes.contains { node in + switch node { + case let .attribute(attribute): + let attributeName = attribute.attributeName.trimmedDescription + return attributeName == "Provided" || attributeName == "ScreamURITemplateMacros.Provided" + case let .ifConfigDecl(node): + let visitor = ProviderAttributeDiagnosticVisitor(viewMode: .all) + visitor.macroExpansionContext = context + visitor.walk(node) + return false + } + } + } + // If no properties are explicitly marked, all proprties are provided + let declarations = explicitlyProvidedVariableDeclarations.count > 0 ? explicitlyProvidedVariableDeclarations : allVariableDeclarations + let variableIdentifiers = declarations.compactMap { declaration -> String? in + declaration.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text + } + + return try [ + ExtensionDeclSyntax("extension \(type.trimmed): VariableProvider") { + """ + subscript(_ v: String) -> VariableValue? { + return switch v { + """ + for variableIdentifiers in variableIdentifiers { + """ + case "\(raw: variableIdentifiers)": \(raw: variableIdentifiers) + """ + } + """ + + default: nil + } + } + """ + }, + ] + } +} diff --git a/Sources/ScreamURITemplateExample/main.swift b/Sources/ScreamURITemplateExample/main.swift index 97c86d3..fc770c9 100644 --- a/Sources/ScreamURITemplateExample/main.swift +++ b/Sources/ScreamURITemplateExample/main.swift @@ -35,3 +35,13 @@ print(macroExpansion) let urlExpansion = #URLByExpandingURITemplate("https://api.github.com/repos/{owner}/{repo}/collaborators/{username}", with: ["owner": "SwiftScream", "repo": "URITemplate", "username": "alexdeem"]) print(urlExpansion) + +@VariableProvider +struct GitHubRepoCollaborator { + let owner: String + let repo: String + let username: String +} + +let expansion = try macroExpansion.process(variables: GitHubRepoCollaborator(owner: "SwiftScream", repo: "URITemplate", username: "alexdeem")) +print(expansion) diff --git a/Sources/ScreamURITemplateMacros/Macros.swift b/Sources/ScreamURITemplateMacros/Macros.swift index 2654e03..dc5e474 100644 --- a/Sources/ScreamURITemplateMacros/Macros.swift +++ b/Sources/ScreamURITemplateMacros/Macros.swift @@ -37,3 +37,23 @@ public macro URITemplate(_ stringLiteral: StaticString) -> URITemplate = #extern /// - Returns: A `URL` constructed from the result of processing the template with the parameters @freestanding(expression) public macro URLByExpandingURITemplate(_ stringLiteral: StaticString, with: KeyValuePairs) -> URL = #externalMacro(module: "ScreamURITemplateCompilerPlugin", type: "URLByExpandingURITemplateMacro") + +/// Macro to provide a default implementation of the `VariableProvider` protocol +/// Example: +/// ```swift +/// @VariableProvider +/// struct GitHubRepo { +/// @Provided let owner: String +/// @Provided let repo: String +/// let transient: Int +/// } +/// ``` +/// The generated implementation provides variables named as per the properties of the struct or class +/// By default all properties are provided. If this is not desirable, tag the properties to be provided with the `@Provided` attribute +@attached(extension, conformances: VariableProvider, names: named(subscript)) +public macro VariableProvider() = #externalMacro(module: "ScreamURITemplateCompilerPlugin", type: "VariableProviderMacro") + +/// Macro used to tag properties that should be provided by the `@VariableProvider` macro +/// This macro generates no code, it's just a marker for use by `@VariableProvider` +@attached(peer) +public macro Provided() = #externalMacro(module: "ScreamURITemplateCompilerPlugin", type: "ProvidedMacro") diff --git a/Tests/ScreamURITemplateTests/ProvidedMacroTests.swift b/Tests/ScreamURITemplateTests/ProvidedMacroTests.swift new file mode 100644 index 0000000..62af57c --- /dev/null +++ b/Tests/ScreamURITemplateTests/ProvidedMacroTests.swift @@ -0,0 +1,54 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(ScreamURITemplateCompilerPlugin) + @testable import ScreamURITemplateCompilerPlugin + + import SwiftSyntaxMacros + import SwiftSyntaxMacrosTestSupport + + import XCTest + + class ProvidedMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "Provided": ProvidedMacro.self, + ] + + func testMacroAvailability() { + let plugin = URITemplateCompilerPlugin() + XCTAssert(plugin.providingMacros.contains { $0 == ProvidedMacro.self }) + } + + func testNoCodeGenerated() throws { + assertMacroExpansion( + #""" + struct A { + @Provided let owner: String + @Provided let repo: String + let username: String + } + """#, + expandedSource: + #""" + struct A { + let owner: String + let repo: String + let username: String + } + """#, + diagnostics: [], + macros: testMacros) + } + } +#endif diff --git a/Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift b/Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift new file mode 100644 index 0000000..9104b85 --- /dev/null +++ b/Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift @@ -0,0 +1,188 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(ScreamURITemplateCompilerPlugin) + @testable import ScreamURITemplateCompilerPlugin + + import SwiftSyntaxMacros + import SwiftSyntaxMacrosTestSupport + + import XCTest + + class VariableProviderMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "VariableProvider": VariableProviderMacro.self, + ] + + func testMacroAvailability() { + let plugin = URITemplateCompilerPlugin() + XCTAssert(plugin.providingMacros.contains { $0 == VariableProviderMacro.self }) + } + + func testEmpty() throws { + assertMacroExpansion( + #""" + @VariableProvider + struct A { + } + """#, + expandedSource: + #""" + struct A { + } + + extension A: VariableProvider { + subscript(_ v: String) -> VariableValue? { + return switch v { + default: nil + } + } + } + """#, + diagnostics: [], + macros: testMacros) + } + + func testDefaultAllProvided() throws { + assertMacroExpansion( + #""" + @VariableProvider + struct A { + let owner: String + let repo: String + let username: String + } + """#, + expandedSource: + #""" + struct A { + let owner: String + let repo: String + let username: String + } + + extension A: VariableProvider { + subscript(_ v: String) -> VariableValue? { + return switch v { + case "owner": owner + case "repo": repo + case "username": username + default: nil + } + } + } + """#, + diagnostics: [], + macros: testMacros) + } + + func testExplicitlyProvided() throws { + assertMacroExpansion( + #""" + @VariableProvider + struct A { + @Provided let owner: String + @Provided let repo: String + let username: String + } + """#, + expandedSource: + #""" + struct A { + @Provided let owner: String + @Provided let repo: String + let username: String + } + + extension A: VariableProvider { + subscript(_ v: String) -> VariableValue? { + return switch v { + case "owner": owner + case "repo": repo + default: nil + } + } + } + """#, + diagnostics: [], + macros: testMacros) + } + + func testNestedInConditionalCompilation() throws { + assertMacroExpansion( + #""" + @VariableProvider + struct A { + #if true + @Provided + #endif + let owner: String + @Provided let repo: String + } + """#, + expandedSource: + #""" + struct A { + let owner: String + @Provided let repo: String + } + + extension A: VariableProvider { + subscript(_ v: String) -> VariableValue? { + return switch v { + case "repo": repo + default: nil + } + } + } + """#, + diagnostics: [ + DiagnosticSpec(message: "@Provided attribute nested in conditional compilation is not respected; sorry", line: 4, column: 5), + ], + macros: testMacros) + } + + func testDisambiguatedExplicitlyProvided() throws { + assertMacroExpansion( + #""" + @VariableProvider + struct A { + @ScreamURITemplateMacros.Provided let owner: String + @ScreamURITemplateMacros.Provided let repo: String + let username: String + } + """#, + expandedSource: + #""" + struct A { + @ScreamURITemplateMacros.Provided let owner: String + @ScreamURITemplateMacros.Provided let repo: String + let username: String + } + + extension A: VariableProvider { + subscript(_ v: String) -> VariableValue? { + return switch v { + case "owner": owner + case "repo": repo + default: nil + } + } + } + """#, + diagnostics: [], + macros: testMacros) + } + } +#endif From 60605b8709a092e9c800e77e6177fe39417dd7a9 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Wed, 1 Jan 2025 23:44:50 +1100 Subject: [PATCH 22/24] Add TypedURITemplate --- .../ScreamURITemplate/TypedURITemplate.swift | 37 +++++++++++++++++++ Sources/ScreamURITemplateExample/main.swift | 4 ++ Tests/ScreamURITemplateTests/Tests.swift | 2 +- .../TypedURITemplateTests.swift | 32 ++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 Sources/ScreamURITemplate/TypedURITemplate.swift create mode 100644 Tests/ScreamURITemplateTests/TypedURITemplateTests.swift diff --git a/Sources/ScreamURITemplate/TypedURITemplate.swift b/Sources/ScreamURITemplate/TypedURITemplate.swift new file mode 100644 index 0000000..e9814e6 --- /dev/null +++ b/Sources/ScreamURITemplate/TypedURITemplate.swift @@ -0,0 +1,37 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +/// A wrapper around a ``URITemplate`` that limits the type of the variables provided when processing +/// This can be used to provide a strongly-typed interface for processing a template +public struct TypedURITemplate { + private let template: URITemplate + + /// Initializes a ``TypedURITemplate`` from a ``URITemplate`` + /// - Parameter template: the URI Template + public init(_ template: URITemplate) { + self.template = template + } + + /// Process a URI Template specifying variables with an instance of the templated type `Variables` + /// - Parameter variables: A ``Variables`` object that can provide values for the template variables + /// + /// - Returns: The result of processing the template + /// + /// - Throws: `URITemplate.Error` with `type = .expansionFailure` if an error occurs processing the template + public func process(variables: Variables) throws(URITemplate.Error) -> String { + try template.process(variables: variables) + } +} diff --git a/Sources/ScreamURITemplateExample/main.swift b/Sources/ScreamURITemplateExample/main.swift index fc770c9..3018951 100644 --- a/Sources/ScreamURITemplateExample/main.swift +++ b/Sources/ScreamURITemplateExample/main.swift @@ -45,3 +45,7 @@ struct GitHubRepoCollaborator { let expansion = try macroExpansion.process(variables: GitHubRepoCollaborator(owner: "SwiftScream", repo: "URITemplate", username: "alexdeem")) print(expansion) + +let typedTemplate = TypedURITemplate(macroExpansion) +let result = try typedTemplate.process(variables: .init(owner: "SwiftScream", repo: "SwiftScream", username: "alexdeem")) +print(result) diff --git a/Tests/ScreamURITemplateTests/Tests.swift b/Tests/ScreamURITemplateTests/Tests.swift index a7d8908..f343392 100644 --- a/Tests/ScreamURITemplateTests/Tests.swift +++ b/Tests/ScreamURITemplateTests/Tests.swift @@ -15,7 +15,7 @@ import ScreamURITemplate import XCTest -struct TestVariableProvider: VariableProvider { +private struct TestVariableProvider: VariableProvider { subscript(_ key: String) -> VariableValue? { switch key { case "missing": diff --git a/Tests/ScreamURITemplateTests/TypedURITemplateTests.swift b/Tests/ScreamURITemplateTests/TypedURITemplateTests.swift new file mode 100644 index 0000000..b403020 --- /dev/null +++ b/Tests/ScreamURITemplateTests/TypedURITemplateTests.swift @@ -0,0 +1,32 @@ +// Copyright 2018-2024 Alex Deem +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest + +import ScreamURITemplate + +private struct TestVariableProvider: VariableProvider { + subscript(_ key: String) -> VariableValue? { + return "_\(key)_" + } +} + +class TypedURITemplateTests: XCTestCase { + func testExpansion() throws { + let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}" + let typedTemplate = TypedURITemplate(template) + let urlString = try typedTemplate.process(variables: .init()) + XCTAssertEqual(urlString, "https://api.github.com/repos/_owner_/_repo_/collaborators/_username_") + } +} From 49245f6f43a9fe03073f6baca133e442e81d051a Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Thu, 2 Jan 2025 00:20:24 +1100 Subject: [PATCH 23/24] Update Copyright Headers --- Sources/ScreamURITemplate/Internal/CharacterSets.swift | 2 +- Sources/ScreamURITemplate/Internal/Components.swift | 2 +- Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift | 2 +- Sources/ScreamURITemplate/Internal/ExpressionOperator.swift | 2 +- Sources/ScreamURITemplate/Internal/Scanner.swift | 2 +- Sources/ScreamURITemplate/Internal/ValueFormatting.swift | 2 +- Sources/ScreamURITemplate/Internal/VariableSpec.swift | 2 +- Sources/ScreamURITemplate/TypedURITemplate.swift | 2 +- Sources/ScreamURITemplate/URITemplate.swift | 2 +- Sources/ScreamURITemplate/VariableProvider.swift | 2 +- Sources/ScreamURITemplate/VariableValue.swift | 2 +- .../ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift | 2 +- Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift | 2 +- .../URITemplateCompilerPlugin.swift | 2 +- Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift | 2 +- .../URLByExpandingURITemplateMacro.swift | 2 +- .../ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift | 2 +- Sources/ScreamURITemplateExample/main.swift | 2 +- Sources/ScreamURITemplateMacros/Macros.swift | 2 +- Tests/ScreamURITemplateTests/JSONValue.swift | 2 +- Tests/ScreamURITemplateTests/ProvidedMacroTests.swift | 2 +- Tests/ScreamURITemplateTests/TestFileTests.swift | 2 +- Tests/ScreamURITemplateTests/TestModels.swift | 2 +- Tests/ScreamURITemplateTests/Tests.swift | 2 +- Tests/ScreamURITemplateTests/TypedURITemplateTests.swift | 2 +- Tests/ScreamURITemplateTests/URITemplateMacroTests.swift | 2 +- .../URLByExpandingURITemplateMacroTests.swift | 2 +- Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift | 2 +- 28 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Sources/ScreamURITemplate/Internal/CharacterSets.swift b/Sources/ScreamURITemplate/Internal/CharacterSets.swift index 40565cd..4c801dc 100644 --- a/Sources/ScreamURITemplate/Internal/CharacterSets.swift +++ b/Sources/ScreamURITemplate/Internal/CharacterSets.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/Internal/Components.swift b/Sources/ScreamURITemplate/Internal/Components.swift index 3f70de1..b17133f 100644 --- a/Sources/ScreamURITemplate/Internal/Components.swift +++ b/Sources/ScreamURITemplate/Internal/Components.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift b/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift index 5e533c7..98bed45 100644 --- a/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift +++ b/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift b/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift index 08d3b53..e5c95bb 100644 --- a/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift +++ b/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/Internal/Scanner.swift b/Sources/ScreamURITemplate/Internal/Scanner.swift index 69ccc01..c42b0e2 100644 --- a/Sources/ScreamURITemplate/Internal/Scanner.swift +++ b/Sources/ScreamURITemplate/Internal/Scanner.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift index 9efc478..4971df5 100644 --- a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift +++ b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/Internal/VariableSpec.swift b/Sources/ScreamURITemplate/Internal/VariableSpec.swift index 370edf1..b2e4e9c 100644 --- a/Sources/ScreamURITemplate/Internal/VariableSpec.swift +++ b/Sources/ScreamURITemplate/Internal/VariableSpec.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/TypedURITemplate.swift b/Sources/ScreamURITemplate/TypedURITemplate.swift index e9814e6..b76d462 100644 --- a/Sources/ScreamURITemplate/TypedURITemplate.swift +++ b/Sources/ScreamURITemplate/TypedURITemplate.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/URITemplate.swift b/Sources/ScreamURITemplate/URITemplate.swift index fc05ec8..e611de1 100644 --- a/Sources/ScreamURITemplate/URITemplate.swift +++ b/Sources/ScreamURITemplate/URITemplate.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/VariableProvider.swift b/Sources/ScreamURITemplate/VariableProvider.swift index d13b4ae..bc7b5a9 100644 --- a/Sources/ScreamURITemplate/VariableProvider.swift +++ b/Sources/ScreamURITemplate/VariableProvider.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplate/VariableValue.swift b/Sources/ScreamURITemplate/VariableValue.swift index a3a66fa..7ffd2e1 100644 --- a/Sources/ScreamURITemplate/VariableValue.swift +++ b/Sources/ScreamURITemplate/VariableValue.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift b/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift index 7d8d198..8b8b736 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/ExprSyntax+Literals.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift index 4b90c21..0cd071c 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/ProvidedMacro.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift index 38c67f7..48ba098 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/URITemplateCompilerPlugin.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift index 1762d9c..dfb2cf9 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/URITemplateMacro.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift index 2df7d5b..bfb4103 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/URLByExpandingURITemplateMacro.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift b/Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift index 61a087c..71de048 100644 --- a/Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift +++ b/Sources/ScreamURITemplateCompilerPlugin/VariableProviderMacro.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateExample/main.swift b/Sources/ScreamURITemplateExample/main.swift index 3018951..301eb49 100644 --- a/Sources/ScreamURITemplateExample/main.swift +++ b/Sources/ScreamURITemplateExample/main.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/ScreamURITemplateMacros/Macros.swift b/Sources/ScreamURITemplateMacros/Macros.swift index dc5e474..e68c022 100644 --- a/Sources/ScreamURITemplateMacros/Macros.swift +++ b/Sources/ScreamURITemplateMacros/Macros.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/JSONValue.swift b/Tests/ScreamURITemplateTests/JSONValue.swift index 13a574b..f46d534 100644 --- a/Tests/ScreamURITemplateTests/JSONValue.swift +++ b/Tests/ScreamURITemplateTests/JSONValue.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/ProvidedMacroTests.swift b/Tests/ScreamURITemplateTests/ProvidedMacroTests.swift index 62af57c..7eb599b 100644 --- a/Tests/ScreamURITemplateTests/ProvidedMacroTests.swift +++ b/Tests/ScreamURITemplateTests/ProvidedMacroTests.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/TestFileTests.swift b/Tests/ScreamURITemplateTests/TestFileTests.swift index b51f1ba..2f2e708 100644 --- a/Tests/ScreamURITemplateTests/TestFileTests.swift +++ b/Tests/ScreamURITemplateTests/TestFileTests.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/TestModels.swift b/Tests/ScreamURITemplateTests/TestModels.swift index 4cf5a2a..3e455ac 100644 --- a/Tests/ScreamURITemplateTests/TestModels.swift +++ b/Tests/ScreamURITemplateTests/TestModels.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/Tests.swift b/Tests/ScreamURITemplateTests/Tests.swift index f343392..4889fd9 100644 --- a/Tests/ScreamURITemplateTests/Tests.swift +++ b/Tests/ScreamURITemplateTests/Tests.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/TypedURITemplateTests.swift b/Tests/ScreamURITemplateTests/TypedURITemplateTests.swift index b403020..0296bd7 100644 --- a/Tests/ScreamURITemplateTests/TypedURITemplateTests.swift +++ b/Tests/ScreamURITemplateTests/TypedURITemplateTests.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift b/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift index e9941d1..5290933 100644 --- a/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift +++ b/Tests/ScreamURITemplateTests/URITemplateMacroTests.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift b/Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift index 86f5758..8830c41 100644 --- a/Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift +++ b/Tests/ScreamURITemplateTests/URLByExpandingURITemplateMacroTests.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift b/Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift index 9104b85..5970a43 100644 --- a/Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift +++ b/Tests/ScreamURITemplateTests/VariableProviderMacroTests.swift @@ -1,4 +1,4 @@ -// Copyright 2018-2024 Alex Deem +// Copyright 2018-2025 Alex Deem // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From a285ce154c9534538f328ffb3614078b40eb29a5 Mon Sep 17 00:00:00 2001 From: Alex Deem Date: Thu, 2 Jan 2025 00:25:21 +1100 Subject: [PATCH 24/24] 5.0.0 Release Notes --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eac767c..48bcd56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. + +# [5.0.0](https://github.com/SwiftScream/URITemplate/compare/4.0.0...5.0.0) (2025-01-02) + +- Move to swift 6.0 as minimum supported version +- Leverage typed-throws +- Add #URITemplate freestanding expression macro +- Add #URLByExpandingURITemplate freestanding expression macro +- Add @VariableProvider attached macro to provide default implementqation of `VariableProvider` +- Add TypedURITemplate enabling a type-safe process interface + # [4.0.0](https://github.com/SwiftScream/URITemplate/compare/3.1.0...4.0.0) (2024-06-13)