diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index e6ba580..e8eccdf 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -15,3 +15,11 @@ updates:
day: "friday"
timezone: "Australia/Sydney"
target-branch: "develop"
+
+ - package-ecosystem: "swift"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "friday"
+ timezone: "Australia/Sydney"
+ target-branch: "develop"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 87acf80..c9f6dbb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,7 +10,7 @@ jobs:
build:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
@@ -20,21 +20,9 @@ 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@v3
+ uses: codecov/codecov-action@v4
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
fail_ci_if_error: true
verbose: true
-
- build-5_4_2:
- runs-on: macos-11
- steps:
- - uses: actions/checkout@v3
- with:
- submodules: recursive
- - name: Check Swift version
- run: |
- sudo xcode-select -s /Applications/Xcode_12.5.1.app/
- export TOOLCHAINS=swift
- swift --version
- - name: Run tests
- run: swift test
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index f32e050..d77520d 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,17 +8,17 @@ on:
jobs:
SwiftLint:
- runs-on: ubuntu-latest
+ runs-on: macos-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: irgaly/setup-mint@v1
- name: SwiftLint
run: mint run swiftlint --strict
SwiftFormat:
- runs-on: ubuntu-latest
+ runs-on: macos-latest
needs: SwiftLint
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: irgaly/setup-mint@v1
- name: SwiftFormat Lint
run: mint run swiftformat --lint .
diff --git a/.spi.yml b/.spi.yml
new file mode 100644
index 0000000..5ff4ae0
--- /dev/null
+++ b/.spi.yml
@@ -0,0 +1,4 @@
+version: 1
+builder:
+ configs:
+ - documentation_targets: [ScreamURITemplate]
\ No newline at end of file
diff --git a/.swift-version b/.swift-version
new file mode 100644
index 0000000..b883184
--- /dev/null
+++ b/.swift-version
@@ -0,0 +1 @@
+5.9
\ No newline at end of file
diff --git a/.swiftformat b/.swiftformat
index 24a794d..9857c7f 100644
--- a/.swiftformat
+++ b/.swiftformat
@@ -1,4 +1,3 @@
---swiftversion 5.7
--exclude .build
# rules
diff --git a/.swiftlint.yml b/.swiftlint.yml
index ca1a784..63ffd9b 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -1,12 +1,17 @@
disabled_rules:
- line_length
- - trailing_comma
- redundant_optional_initialization
opt_in_rules:
+ - missing_docs
included:
excluded:
.build
+trailing_comma:
+ mandatory_comma: true
+
+vertical_whitespace:
+ max_empty_lines: 2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 276ed26..eac767c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+
+# [4.0.0](https://github.com/SwiftScream/URITemplate/compare/3.1.0...4.0.0) (2024-06-13)
+
+- Refine interface for specifying variables
+- Add docc documentation
+- Move to swift 5.9 as minimum supported version
+
+
# [3.1.0](https://github.com/SwiftScream/URITemplate/compare/3.0.1...3.1.0) (2023-01-20)
diff --git a/Mintfile b/Mintfile
index 8a1487a..fd88089 100644
--- a/Mintfile
+++ b/Mintfile
@@ -1,2 +1,2 @@
-realm/SwiftLint
-nicklockwood/SwiftFormat
+realm/SwiftLint@0.55.1
+nicklockwood/SwiftFormat@0.53.10
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..9ad5a83
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,25 @@
+{
+ "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
+}
diff --git a/Package.swift b/Package.swift
index fd7ed4d..5006247 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.4
+// swift-tools-version: 5.9
import PackageDescription
@@ -10,6 +10,7 @@ let package = Package(
targets: ["ScreamURITemplate"]),
],
dependencies: [
+ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.target(
@@ -18,6 +19,12 @@ let package = Package(
.testTarget(
name: "ScreamURITemplateTests",
dependencies: ["ScreamURITemplate"],
+ exclude: [
+ "data/uritemplate-test/json2xml.xslt",
+ "data/uritemplate-test/LICENSE",
+ "data/uritemplate-test/README.md",
+ "data/uritemplate-test/transform-json-tests.xslt",
+ ],
resources: [
.process("data/tests.json"),
.process("data/uritemplate-test/spec-examples.json"),
@@ -25,13 +32,8 @@ let package = Package(
.process("data/uritemplate-test/extended-tests.json"),
.process("data/uritemplate-test/negative-tests.json"),
]),
- ],
- swiftLanguageVersions: [.v5])
-
-#if swift(>=5.6) || os(macOS) || os(Linux)
- package.targets.append(
.executableTarget(
name: "ScreamURITemplateExample",
- dependencies: ["ScreamURITemplate"])
- )
-#endif
+ dependencies: ["ScreamURITemplate"]),
+ ],
+ swiftLanguageVersions: [.v5])
diff --git a/Sources/ScreamURITemplate/Internal/CharacterSets.swift b/Sources/ScreamURITemplate/Internal/CharacterSets.swift
index ec56760..40565cd 100644
--- a/Sources/ScreamURITemplate/Internal/CharacterSets.swift
+++ b/Sources/ScreamURITemplate/Internal/CharacterSets.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -14,15 +14,15 @@
import Foundation
-internal let unreservedCharacterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~"))
+let unreservedCharacterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~"))
private let genDelimsCharacterSet = CharacterSet(charactersIn: ":/?#[]@")
private let subDelimsCharacterSet = CharacterSet(charactersIn: "!$&'()*+,;=")
-internal let reservedCharacterSet = genDelimsCharacterSet.union(subDelimsCharacterSet)
-internal let reservedAndUnreservedCharacterSet = reservedCharacterSet.union(unreservedCharacterSet)
-internal let invertedLiteralCharacterSet = CharacterSet.illegalCharacters.union(CharacterSet.controlCharacters).union(CharacterSet(charactersIn: " \"%<>\\^`{|}"))
-internal let literalCharacterSet = invertedLiteralCharacterSet.inverted
-internal let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
-internal let varnameCharacterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_%."))
-internal let invertedVarnameCharacterSet = varnameCharacterSet.inverted
-internal let expressionOperatorCharacterSet = CharacterSet(charactersIn: "+#./;?&=,!@|")
-internal let invertedDecimalDigitsCharacterSet = CharacterSet.decimalDigits.inverted
+let reservedCharacterSet = genDelimsCharacterSet.union(subDelimsCharacterSet)
+let reservedAndUnreservedCharacterSet = reservedCharacterSet.union(unreservedCharacterSet)
+let invertedLiteralCharacterSet = CharacterSet.illegalCharacters.union(CharacterSet.controlCharacters).union(CharacterSet(charactersIn: " \"%<>\\^`{|}"))
+let literalCharacterSet = invertedLiteralCharacterSet.inverted
+let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
+let varnameCharacterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_%."))
+let invertedVarnameCharacterSet = varnameCharacterSet.inverted
+let expressionOperatorCharacterSet = CharacterSet(charactersIn: "+#./;?&=,!@|")
+let invertedDecimalDigitsCharacterSet = CharacterSet.decimalDigits.inverted
diff --git a/Sources/ScreamURITemplate/Internal/Components.swift b/Sources/ScreamURITemplate/Internal/Components.swift
index c6d7db0..4280d4f 100644
--- a/Sources/ScreamURITemplate/Internal/Components.swift
+++ b/Sources/ScreamURITemplate/Internal/Components.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -14,14 +14,10 @@
import Foundation
-#if swift(>=5.5)
- internal typealias ComponentBase = Sendable
-#else
- internal protocol ComponentBase {}
-#endif
+typealias ComponentBase = Sendable
-internal protocol Component: ComponentBase {
- func expand(variables: [String: VariableValue]) throws -> String
+protocol Component: ComponentBase {
+ func expand(variables: TypedVariableProvider) throws -> String
var variableNames: [String] { get }
}
@@ -31,13 +27,13 @@ extension Component {
}
}
-internal struct LiteralComponent: Component {
+struct LiteralComponent: Component {
let literal: Substring
init(_ string: Substring) {
literal = string
}
- func expand(variables _: [String: VariableValue]) throws -> String {
+ 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")
@@ -46,18 +42,18 @@ internal struct LiteralComponent: Component {
}
}
-internal struct LiteralPercentEncodedTripletComponent: Component {
+struct LiteralPercentEncodedTripletComponent: Component {
let literal: Substring
init(_ string: Substring) {
literal = string
}
- func expand(variables _: [String: VariableValue]) throws -> String {
+ func expand(variables _: TypedVariableProvider) throws -> String {
return String(literal)
}
}
-internal struct ExpressionComponent: Component {
+struct ExpressionComponent: Component {
let expressionOperator: ExpressionOperator
let variableList: [VariableSpec]
let templatePosition: String.Index
@@ -68,37 +64,14 @@ internal struct ExpressionComponent: Component {
self.templatePosition = templatePosition
}
- // swiftlint:disable:next cyclomatic_complexity
- func expand(variables: [String: VariableValue]) throws -> String {
+ func expand(variables: TypedVariableProvider) throws -> String {
let configuration = expressionOperator.expansionConfiguration()
let expansions = try variableList.compactMap { variableSpec -> String? in
guard let value = variables[String(variableSpec.name)] else {
return nil
}
do {
- if let stringValue = value as? String {
- return try stringValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
- } else if let arrayValue = value as? [String] {
- switch variableSpec.modifier {
- case .prefix:
- throw FormatError.failure(reason: "Prefix operator can only be applied to string")
- case .explode:
- return try arrayValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
- case .none:
- return try arrayValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
- }
- } else if let dictionaryValue = value as? [String: String] {
- switch variableSpec.modifier {
- case .prefix:
- throw FormatError.failure(reason: "Prefix operator can only be applied to string")
- case .explode:
- return try dictionaryValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
- case .none:
- return try dictionaryValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
- }
- } else {
- throw FormatError.failure(reason: "Invalid Value Type")
- }
+ 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)")
}
diff --git a/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift b/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift
index f4c2a49..5e533c7 100644
--- a/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift
+++ b/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -14,11 +14,11 @@
import Foundation
-internal struct ExpansionConfiguration {
+struct ExpansionConfiguration {
let percentEncodingAllowedCharacterSet: CharacterSet
let allowPercentEncodedTriplets: Bool
let prefix: String?
let separator: String
let named: Bool
- let omittOrphanedEquals: Bool
+ let omitOrphanedEquals: Bool
}
diff --git a/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift b/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift
index cfd13d0..08d3b53 100644
--- a/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift
+++ b/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -14,7 +14,7 @@
import Foundation
-internal enum ExpressionOperator: Unicode.Scalar {
+enum ExpressionOperator: Unicode.Scalar {
case simple = "\0"
case reserved = "+"
case fragment = "#"
@@ -33,56 +33,56 @@ internal enum ExpressionOperator: Unicode.Scalar {
prefix: nil,
separator: ",",
named: false,
- omittOrphanedEquals: false)
+ omitOrphanedEquals: false)
case .reserved:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: reservedAndUnreservedCharacterSet,
allowPercentEncodedTriplets: true,
prefix: nil,
separator: ",",
named: false,
- omittOrphanedEquals: false)
+ omitOrphanedEquals: false)
case .fragment:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: reservedAndUnreservedCharacterSet,
allowPercentEncodedTriplets: true,
prefix: "#",
separator: ",",
named: false,
- omittOrphanedEquals: false)
+ omitOrphanedEquals: false)
case .label:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
allowPercentEncodedTriplets: false,
prefix: ".",
separator: ".",
named: false,
- omittOrphanedEquals: false)
+ omitOrphanedEquals: false)
case .pathSegment:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
allowPercentEncodedTriplets: false,
prefix: "/",
separator: "/",
named: false,
- omittOrphanedEquals: false)
+ omitOrphanedEquals: false)
case .pathStyle:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
allowPercentEncodedTriplets: false,
prefix: ";",
separator: ";",
named: true,
- omittOrphanedEquals: true)
+ omitOrphanedEquals: true)
case .query:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
allowPercentEncodedTriplets: false,
prefix: "?",
separator: "&",
named: true,
- omittOrphanedEquals: false)
+ omitOrphanedEquals: false)
case .queryContinuation:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
allowPercentEncodedTriplets: false,
prefix: "&",
separator: "&",
named: true,
- omittOrphanedEquals: false)
+ omitOrphanedEquals: false)
}
}
}
diff --git a/Sources/ScreamURITemplate/Internal/Scanner.swift b/Sources/ScreamURITemplate/Internal/Scanner.swift
index 2ea9bf5..24af5cd 100644
--- a/Sources/ScreamURITemplate/Internal/Scanner.swift
+++ b/Sources/ScreamURITemplate/Internal/Scanner.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -18,22 +18,22 @@ private func ~= (lhs: CharacterSet, rhs: Unicode.Scalar) -> Bool {
return lhs.contains(rhs)
}
-internal struct Scanner {
+struct Scanner {
let string: String
let unicodeScalars: String.UnicodeScalarView
var currentIndex: String.Index
- public init(string: String) {
+ init(string: String) {
self.string = string
unicodeScalars = string.unicodeScalars
currentIndex = string.startIndex
}
- public var isComplete: Bool {
+ var isComplete: Bool {
return currentIndex >= unicodeScalars.endIndex
}
- public mutating func scanComponent() throws -> Component {
+ mutating func scanComponent() throws -> Component {
let nextScalar = unicodeScalars[currentIndex]
switch nextScalar {
diff --git a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift
index 0432976..bf6d7bc 100644
--- a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift
+++ b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -14,11 +14,38 @@
import Foundation
-internal enum FormatError: Error {
+enum FormatError: Error {
case failure(reason: String)
}
-internal func percentEncode(string: String, withAllowedCharacters allowedCharacterSet: CharacterSet, allowPercentEncodedTriplets: Bool) throws -> String {
+extension TypedVariableValue {
+ func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration configuration: ExpansionConfiguration) throws -> String? {
+ switch self {
+ case let .string(plainValue):
+ return try plainValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
+ case let .list(arrayValue):
+ switch variableSpec.modifier {
+ case .prefix:
+ throw FormatError.failure(reason: "Prefix operator can only be applied to string")
+ case .explode:
+ return try arrayValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
+ case .none:
+ return try arrayValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
+ }
+ case let .associativeArray(associativeArrayValue):
+ switch variableSpec.modifier {
+ case .prefix:
+ throw FormatError.failure(reason: "Prefix operator can only be applied to string")
+ case .explode:
+ return try associativeArrayValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
+ case .none:
+ return try associativeArrayValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
+ }
+ }
+ }
+}
+
+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")
}
@@ -45,17 +72,16 @@ internal func percentEncode(string: String, withAllowedCharacters allowedCharact
return encoded
}
-internal extension StringProtocol {
+private extension StringProtocol {
func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String {
- let modifiedValue: String
- if let prefixLength = variableSpec.prefixLength() {
- modifiedValue = String(prefix(prefixLength))
+ let modifiedValue = if let prefixLength = variableSpec.prefixLength() {
+ String(prefix(prefixLength))
} else {
- modifiedValue = String(self)
+ String(self)
}
let encodedExpansion = try percentEncode(string: modifiedValue, withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
if expansionConfiguration.named {
- if encodedExpansion.isEmpty && expansionConfiguration.omittOrphanedEquals {
+ if encodedExpansion.isEmpty && expansionConfiguration.omitOrphanedEquals {
return String(variableSpec.name)
}
return "\(variableSpec.name)=\(encodedExpansion)"
@@ -64,7 +90,7 @@ internal extension StringProtocol {
}
}
-internal extension Array where Element: StringProtocol {
+private extension Array where Element: StringProtocol {
func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? {
let separator = ","
let encodedExpansions = try map { element -> String in
@@ -75,7 +101,7 @@ internal extension Array where Element: StringProtocol {
}
let expansion = encodedExpansions.joined(separator: separator)
if expansionConfiguration.named {
- if expansion.isEmpty && expansionConfiguration.omittOrphanedEquals {
+ if expansion.isEmpty && expansionConfiguration.omitOrphanedEquals {
return String(variableSpec.name)
}
return "\(variableSpec.name)=\(expansion)"
@@ -88,7 +114,7 @@ internal extension Array where Element: StringProtocol {
let encodedExpansions = try map { element -> String in
let encodedElement = try percentEncode(string: String(element), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
if expansionConfiguration.named {
- if encodedElement.isEmpty && expansionConfiguration.omittOrphanedEquals {
+ if encodedElement.isEmpty && expansionConfiguration.omitOrphanedEquals {
return String(variableSpec.name)
}
return "\(variableSpec.name)=\(encodedElement)"
@@ -102,7 +128,7 @@ internal extension Array where Element: StringProtocol {
}
}
-internal extension Dictionary where Key: StringProtocol, Value: StringProtocol {
+private extension [(key: String, value: String)] {
func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? {
let encodedExpansions = try map { key, value -> String in
let encodedKey = try percentEncode(string: String(key), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
@@ -124,7 +150,7 @@ internal extension Dictionary where Key: StringProtocol, Value: StringProtocol {
let encodedExpansions = try map { key, value -> 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.omittOrphanedEquals {
+ if expansionConfiguration.named && encodedValue.isEmpty && expansionConfiguration.omitOrphanedEquals {
return String(variableSpec.name)
}
return "\(encodedKey)=\(encodedValue)"
diff --git a/Sources/ScreamURITemplate/Internal/VariableSpec.swift b/Sources/ScreamURITemplate/Internal/VariableSpec.swift
index 56800cf..370edf1 100644
--- a/Sources/ScreamURITemplate/Internal/VariableSpec.swift
+++ b/Sources/ScreamURITemplate/Internal/VariableSpec.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -14,7 +14,7 @@
import Foundation
-internal struct VariableSpec {
+struct VariableSpec {
enum Modifier {
case prefix(length: Int)
case explode
diff --git a/Sources/ScreamURITemplate/URITemplate.swift b/Sources/ScreamURITemplate/URITemplate.swift
index 8c31bc7..2ec7c13 100644
--- a/Sources/ScreamURITemplate/URITemplate.swift
+++ b/Sources/ScreamURITemplate/URITemplate.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -14,20 +14,23 @@
import Foundation
-public protocol VariableValue {}
-extension String: VariableValue {}
-extension Array: VariableValue where Element: StringProtocol {}
-extension Dictionary: VariableValue where Key: StringProtocol, Value: StringProtocol {}
-
+/// 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)
}
private let string: String
private let components: [Component]
+ /// 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
public init(string: String) throws {
var components: [Component] = []
var scanner = Scanner(string: string)
@@ -38,7 +41,13 @@ public struct URITemplate {
self.components = components
}
- public func process(variables: [String: VariableValue]) throws -> String {
+ /// Process a URI Template specifying variables with a ``TypedVariableProvider``
+ /// - Parameter variables: A ``TypedVariableProvider`` that can provide values for the template variables
+ ///
+ /// - Returns: The result of processing the template
+ ///
+ /// - Throws: `URITemplate.Error.expansionFailure` if an error occurs processing the template
+ public func process(variables: TypedVariableProvider) throws -> String {
var result = ""
for component in components {
result += try component.expand(variables: variables)
@@ -46,6 +55,41 @@ public struct URITemplate {
return result
}
+ /// Process a URI Template specifying variables with a ``VariableProvider``
+ ///
+ /// This method allows for specifying variables in a more ergonomic manner compared to using ``TypedVariableValue`` directly
+ ///
+ /// - Parameter variables: A ``VariableProvider`` that can provide values for the template variables
+ ///
+ /// - Returns: The result of processing the template
+ ///
+ /// - Throws: `URITemplate.Error.expansionFailure` if an error occurs processing the template
+ public func process(variables: VariableProvider) throws -> String {
+ struct TypedVariableProviderWrapper: TypedVariableProvider {
+ let variables: VariableProvider
+
+ subscript(_ key: String) -> TypedVariableValue? {
+ return variables[key]?.asTypedVariableValue()
+ }
+ }
+
+ return try process(variables: TypedVariableProviderWrapper(variables: variables))
+ }
+
+ /// Process a URI Template where the variable values are all of type string
+ ///
+ /// This method is an override allowing for the special case of string-only variables without needing to typecast
+ ///
+ /// - Parameter variables: A [String: String] dictionary representing the variables
+ ///
+ /// - Returns: The result of processing the template
+ ///
+ /// - Throws: `URITemplate.Error.expansionFailure` if an error occurs processing the template
+ public func process(variables: [String: String]) throws -> String {
+ return try process(variables: variables as VariableDictionary)
+ }
+
+ /// An array of all variable names used in the template
public var variableNames: [String] {
return components.flatMap { component in
return component.variableNames
@@ -53,9 +97,7 @@ public struct URITemplate {
}
}
-#if swift(>=5.5)
- extension URITemplate: Sendable {}
-#endif
+extension URITemplate: Sendable {}
extension URITemplate: CustomStringConvertible {
public var description: String {
diff --git a/Sources/ScreamURITemplate/VariableProvider.swift b/Sources/ScreamURITemplate/VariableProvider.swift
new file mode 100644
index 0000000..d969a61
--- /dev/null
+++ b/Sources/ScreamURITemplate/VariableProvider.swift
@@ -0,0 +1,78 @@
+// 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 type that provides variable values to use in template processing
+///
+/// This type provides values using ``VariableValue`` which allows for an ergonomic way to provide values.
+public protocol VariableProvider {
+ /// Get the ``VariableValue`` for a given variable
+ ///
+ /// - Parameters:
+ /// - _: the name of the variable
+ ///
+ /// - Returns: the ``VariableValue`` for the variable, or `nil` if the variable has no value
+ subscript(_: String) -> VariableValue? { get }
+}
+
+/// A type that provides variable values to use in template processing
+///
+/// This type provides values using ``TypedVariableValue``
+///
+/// Consider using ``VariableProvider`` for a more ergonomic way of providing variable values.
+public protocol TypedVariableProvider {
+ /// Get the ``TypedVariableValue`` for a given variable
+ ///
+ /// - Parameters:
+ /// - _: the name of the variable
+ ///
+ /// - Returns: the ``TypedVariableValue`` for the variable, or `nil` if the variable has no value
+ subscript(_: String) -> TypedVariableValue? { get }
+}
+
+/// A typealias for the most simple ``VariableProvider`` implementation: `[String: VariableValue]`
+public typealias VariableDictionary = [String: VariableValue]
+
+extension VariableDictionary: VariableProvider {}
+
+/// A typealias for the most simple ``TypedVariableProvider`` implementation: `[String: TypedVariableValue]`
+public typealias TypedVariableDictionary = [String: TypedVariableValue]
+
+extension TypedVariableDictionary: TypedVariableProvider {}
+
+/// An object that aggregates a `Sequence` of ``VariableProvider`` as a single ``VariableProvider``
+///
+/// This object allows using a prioritised sequence of VariableProvider as a single VariableProvider.
+/// The first VariableProvider in the sequence that provides a value for a given variable name is the value that is returned.
+public struct SequenceVariableProvider: VariableProvider, ExpressibleByArrayLiteral {
+ let sequence: any Sequence
+
+ public init(sequence: any Sequence) {
+ self.sequence = sequence
+ }
+
+ public init(arrayLiteral elements: VariableProvider...) {
+ self.init(sequence: elements)
+ }
+
+ public subscript(_ name: String) -> VariableValue? {
+ for provider in sequence {
+ if let value = provider[name] {
+ return value
+ }
+ }
+ return nil
+ }
+}
diff --git a/Sources/ScreamURITemplate/VariableValue.swift b/Sources/ScreamURITemplate/VariableValue.swift
new file mode 100644
index 0000000..a3a66fa
--- /dev/null
+++ b/Sources/ScreamURITemplate/VariableValue.swift
@@ -0,0 +1,108 @@
+// 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
+
+/// The value of a URITemplate variable to use during processing
+///
+/// This type represents the value of a variable, as defined by [RFC6570](https://tools.ietf.org/html/rfc6570), to be used in
+/// template processing.
+///
+/// Variables can be either a string, a list of strings, or an associative array of string key, value pairs.
+///
+/// While you can process a template by providing variable values using this type (via ``TypedVariableProvider``) you may find it
+/// more ergonomic to provide ``VariableValue`` using ``VariableProvider``, or for simple cases simply `[String: String]`
+public enum TypedVariableValue {
+ /// A simple string value
+ case string(String)
+ /// An ordered list of strings
+ case list([String])
+ /// An associative array of string key, value pairs
+ ///
+ /// Note that the elements are ordered
+ case associativeArray([(key: String, value: String)])
+}
+
+/// A protocol enabling ergonomic expression of variable values
+///
+/// Conforming a type to this protocol will enable it to be directly provided as a variable value via ``VariableProvider``
+public protocol VariableValue {
+ /// Converts this value to a TypedVariableValue to be used for template processing
+ func asTypedVariableValue() -> TypedVariableValue?
+}
+
+/// A protocol enabling ergonomic expression of simple string variable values
+///
+/// Conforming a type to this protocol will enable it to be directly provided as a variable value, or as an element in a list or
+/// associative array value via ``VariableProvider``
+public protocol StringVariableValue: VariableValue {
+ /// Converts this value to a `String` to be used for template processing
+ func asString() -> String
+}
+
+public extension StringVariableValue {
+ /// Converts this value to a TypedVariableValue to be used for template processing
+ func asTypedVariableValue() -> TypedVariableValue? {
+ .string(asString())
+ }
+}
+
+extension [StringVariableValue]: VariableValue {
+ public func asTypedVariableValue() -> TypedVariableValue? {
+ .list(map { $0.asString() })
+ }
+}
+
+extension KeyValuePairs: VariableValue {
+ public func asTypedVariableValue() -> TypedVariableValue? {
+ .associativeArray(map { ($0, $1.asString()) })
+ }
+}
+
+extension [String: StringVariableValue]: VariableValue {
+ public func asTypedVariableValue() -> TypedVariableValue? {
+ .associativeArray(map { ($0, $1.asString()) }.sorted { $0.0 < $1.0 })
+ }
+}
+
+public extension LosslessStringConvertible {
+ /// Converts this value to a `String` to be used for template processing
+ func asString() -> String {
+ description
+ }
+}
+
+extension String: StringVariableValue {}
+extension Bool: StringVariableValue {}
+extension Character: StringVariableValue {}
+extension Double: StringVariableValue {}
+extension Float: StringVariableValue {}
+extension Int: StringVariableValue {}
+extension Int16: StringVariableValue {}
+extension Int32: StringVariableValue {}
+extension Int64: StringVariableValue {}
+extension Int8: StringVariableValue {}
+extension Substring: StringVariableValue {}
+extension UInt: StringVariableValue {}
+extension UInt16: StringVariableValue {}
+extension UInt32: StringVariableValue {}
+extension UInt64: StringVariableValue {}
+extension UInt8: StringVariableValue {}
+extension Unicode.Scalar: StringVariableValue {}
+
+extension UUID: StringVariableValue {
+ public func asString() -> String {
+ uuidString
+ }
+}
diff --git a/Sources/ScreamURITemplateExample/main.swift b/Sources/ScreamURITemplateExample/main.swift
index 2fcd5d4..13d8d9d 100644
--- a/Sources/ScreamURITemplateExample/main.swift
+++ b/Sources/ScreamURITemplateExample/main.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -16,9 +16,11 @@ import Foundation
import ScreamURITemplate
let template = try URITemplate(string: "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}")
-let variables = ["owner": "SwiftScream",
- "repo": "URITemplate",
- "username": "alexdeem"]
+let variables = [
+ "owner": "SwiftScream",
+ "repo": "URITemplate",
+ "username": "alexdeem",
+]
let urlString = try template.process(variables: variables)
diff --git a/Tests/ScreamURITemplateTests/JSONValue.swift b/Tests/ScreamURITemplateTests/JSONValue.swift
index 16e39eb..13a574b 100644
--- a/Tests/ScreamURITemplateTests/JSONValue.swift
+++ b/Tests/ScreamURITemplateTests/JSONValue.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
diff --git a/Tests/ScreamURITemplateTests/TestFileTests.swift b/Tests/ScreamURITemplateTests/TestFileTests.swift
index 22c0685..f96bdff 100644
--- a/Tests/ScreamURITemplateTests/TestFileTests.swift
+++ b/Tests/ScreamURITemplateTests/TestFileTests.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -17,7 +17,7 @@ import XCTest
class TestFileTests: XCTestCase {
private var templateString: String!
- private var variables: [String: VariableValue]!
+ private var variables: VariableDictionary!
private var acceptableExpansions: [String]!
private var failPosition: Int?
private var failReason: String?
@@ -26,14 +26,10 @@ class TestFileTests: XCTestCase {
XCTFail("Test File Parse Failed")
}
- func testSuccessfulProcess() {
- do {
- let template = try URITemplate(string: templateString)
- let result = try template.process(variables: variables)
- XCTAssertTrue(acceptableExpansions.contains(result))
- } catch {
- XCTFail("Unexpected Throw")
- }
+ func testSuccessfulProcess() throws {
+ let template = try URITemplate(string: templateString)
+ let result = try template.process(variables: variables)
+ XCTAssertTrue(acceptableExpansions.contains(result))
}
func testFailedProcess() {
@@ -80,7 +76,10 @@ class TestFileTests: XCTestCase {
return fileTestSuite
}
- let testGroups = parseTestFile(URL: testURL)
+ guard let testGroups = parseTestFile(URL: testURL) else {
+ fileTestSuite.addTest(TestFileTests(selector: #selector(TestFileTests.testFileParseFailed)))
+ return fileTestSuite
+ }
for group in testGroups {
let groupTestSuite = XCTestSuite(name: "Group: \(group.name)")
for test in group.testcases {
diff --git a/Tests/ScreamURITemplateTests/TestModels.swift b/Tests/ScreamURITemplateTests/TestModels.swift
index 37efab9..4cf5a2a 100644
--- a/Tests/ScreamURITemplateTests/TestModels.swift
+++ b/Tests/ScreamURITemplateTests/TestModels.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -23,23 +23,40 @@ private struct TestGroupDecodable: Decodable {
let testcases: [[JSONValue]]
}
-public struct TestGroup {
- public let name: String
- public let level: Int?
- public let variables: [String: VariableValue]
- public let testcases: [TestCase]
+struct TestGroup {
+ let name: String
+ let level: Int?
+ let variables: VariableDictionary
+ let testcases: [TestCase]
}
-public struct TestCase {
- public let template: String
- public let acceptableExpansions: [String]
- public let shouldFail: Bool
- public let failPosition: Int?
- public let failReason: String?
+struct TestCase {
+ let template: String
+ let acceptableExpansions: [String]
+ let shouldFail: Bool
+ let failPosition: Int?
+ let failReason: String?
}
-extension JSONValue {
- func toVariableValue() -> VariableValue? {
+extension JSONValue: VariableValue {
+ public func asTypedVariableValue() -> ScreamURITemplate.TypedVariableValue? {
+ switch self {
+ case let .int(int):
+ return int.asTypedVariableValue()
+ case let .double(double):
+ return double.asTypedVariableValue()
+ case let .string(string):
+ return string.asTypedVariableValue()
+ case let .object(object):
+ return object.compactMapValues { $0.asString() }.asTypedVariableValue()
+ case let .array(array):
+ return array.compactMap { $0.asString() }.asTypedVariableValue()
+ case .null, .bool:
+ return nil
+ }
+ }
+
+ private func asString() -> String? {
switch self {
case let .int(int):
return String(int)
@@ -47,26 +64,7 @@ extension JSONValue {
return String(double)
case let .string(string):
return string
- case let .object(object):
- return object.mapValues { element -> String? in
- switch element {
- case let .string(string):
- return string
- default:
- return nil
- }
- }.filter { $0.value != nil }
- .mapValues { $0! }
- case let .array(array):
- return array.compactMap { element -> String? in
- switch element {
- case let .string(string):
- return string
- default:
- return nil
- }
- }
- default:
+ case .null, .bool, .object, .array:
return nil
}
}
@@ -86,18 +84,7 @@ extension TestCase {
let expansionsData = data[1]
switch expansionsData {
case let .string(string):
- // HACK: ensure the tests support alternate ordering for dictionary explode tests
- // A PR has been raised to add support for the alternate ordering https://github.com/uri-templates/uritemplate-test/pull/58
- switch string {
- case "key1,val1%2F,key2,val2%2F":
- acceptableExpansions = [string, "key2,val2%2F,key1,val1%2F"]
- case "#key1,val1%2F,key2,val2%2F":
- acceptableExpansions = [string, "#key2,val2%2F,key1,val1%2F"]
- case "key1,val1%252F,key2,val2%252F":
- acceptableExpansions = [string, "key2,val2%252F,key1,val1%252F"]
- default:
- acceptableExpansions = [string]
- }
+ acceptableExpansions = [string]
shouldFail = false
case let .array(array):
acceptableExpansions = array.compactMap { value in
@@ -135,23 +122,18 @@ extension TestCase {
}
}
-public func parseTestFile(URL: URL) -> [TestGroup] {
+func parseTestFile(URL: URL) -> [TestGroup]? {
guard let testData = try? Data(contentsOf: URL),
let testCollection = try? JSONDecoder().decode(TestFile.self, from: testData) else {
print("Failed to decode test file \(URL)")
- return []
+ return nil
}
return testCollection.map { testGroupName, testGroupData in
- let variables = testGroupData.variables.mapValues { element in
- return element.toVariableValue()
- }.filter { return $0.value != nil }
- .mapValues { return $0! }
-
let testcases = testGroupData.testcases.compactMap { element in
return TestCase(element)
}
- return TestGroup(name: testGroupName, level: testGroupData.level, variables: variables, testcases: testcases)
+ return TestGroup(name: testGroupName, level: testGroupData.level, variables: testGroupData.variables, testcases: testcases)
}
}
diff --git a/Tests/ScreamURITemplateTests/Tests.swift b/Tests/ScreamURITemplateTests/Tests.swift
index dc447ba..a7d8908 100644
--- a/Tests/ScreamURITemplateTests/Tests.swift
+++ b/Tests/ScreamURITemplateTests/Tests.swift
@@ -1,4 +1,4 @@
-// Copyright 2018-2023 Alex Deem
+// 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.
@@ -15,26 +15,144 @@
import ScreamURITemplate
import XCTest
-class Tests: XCTestCase {
- #if swift(>=5.5)
- func testSendable() {
- let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
- let sendable = template as Sendable
- XCTAssertNotNil(sendable)
+struct TestVariableProvider: VariableProvider {
+ subscript(_ key: String) -> VariableValue? {
+ switch key {
+ case "missing":
+ return nil
+ default:
+ return "_\(key)_"
}
- #endif
+ }
+}
+
+class Tests: XCTestCase {
+ func testVariableProvider() throws {
+ let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
+ let urlString = try template.process(variables: TestVariableProvider())
+ XCTAssertEqual(urlString, "https://api.github.com/repos/_owner_/_repo_/collaborators/_username_")
+ }
+
+ func testSequenceVariableProvider() throws {
+ let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}{missing}"
+ let urlString = try template.process(variables: [
+ ["owner": "SwiftScream"],
+ TestVariableProvider(),
+ ] as SequenceVariableProvider)
+ XCTAssertEqual(urlString, "https://api.github.com/repos/SwiftScream/_repo_/collaborators/_username_")
+ }
+
+ func testStringStringDictionary() throws {
+ let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
+ let variables = [
+ "owner": "SwiftScream",
+ "repo": "URITemplate",
+ "username": "alexdeem",
+ ]
+ let urlString = try template.process(variables: variables)
+ XCTAssertEqual(urlString, "https://api.github.com/repos/SwiftScream/URITemplate/collaborators/alexdeem")
+ }
+
+ func testVariableDictionaryPlain() throws {
+ let template: URITemplate = "https://api.example.com/{string}/{int}/{bool}"
+ let variables: VariableDictionary = [
+ "string": "SwiftScream",
+ "int": 42,
+ "bool": true,
+ ]
+ let urlString = try template.process(variables: variables)
+ XCTAssertEqual(urlString, "https://api.example.com/SwiftScream/42/true")
+ }
+
+ func testVariableDictionaryList() throws {
+ let template: URITemplate = "https://api.example.com/{list}"
+ let variables: VariableDictionary = [
+ "list": ["SwiftScream", 42, true],
+ ]
+ let urlString = try template.process(variables: variables)
+ XCTAssertEqual(urlString, "https://api.example.com/SwiftScream,42,true")
+ }
+
+ func testVariableDictionaryAssocList() throws {
+ let template: URITemplate = "https://api.example.com/path{?unordered*,ordered*}"
+ let variables: VariableDictionary = [
+ "unordered": [
+ "b": 42,
+ "a": "A",
+ "c": true,
+ ],
+ "ordered": [
+ "b2": 42,
+ "a2": "A",
+ "c2": true,
+ ] as KeyValuePairs,
+ ]
+ let urlString = try template.process(variables: variables)
+ XCTAssertEqual("https://api.example.com/path?a=A&b=42&c=true&b2=42&a2=A&c2=true", urlString)
+ }
+
+ func testUUIDVariable() throws {
+ let template: URITemplate = "https://api.example.com/{id}"
+ let variables: VariableDictionary = [
+ "id": UUID(uuidString: "1740A1A9-B3AD-4AE9-954B-918CEDE95285")!,
+ ]
+ let urlString = try template.process(variables: variables)
+ XCTAssertEqual(urlString, "https://api.example.com/1740A1A9-B3AD-4AE9-954B-918CEDE95285")
+ }
+
+ func testVariableDictionaryVariousTypes() throws {
+ let template: URITemplate = "https://api.example.com{/string,int,bool,list}{?unordered*,ordered*}"
+ let variables: VariableDictionary = [
+ "string": "SwiftScream",
+ "int": 42,
+ "bool": true,
+ "list": ["SwiftScream", 42, true],
+ "unordered": [
+ "b": 42,
+ "a": "A",
+ "c": true,
+ ],
+ "ordered": [
+ "b2": 42,
+ "a2": "A",
+ "c2": true,
+ ] as KeyValuePairs,
+ ]
+ let urlString = try template.process(variables: variables)
+ XCTAssertEqual("https://api.example.com/SwiftScream/42/true/SwiftScream,42,true?a=A&b=42&c=true&b2=42&a2=A&c2=true", urlString)
+ }
+
+ func testTypedVariableDictionaryVariousTypes() throws {
+ let template: URITemplate = "https://api.example.com{/string,int,bool,list}{?unordered*,ordered*}"
+ let variables: TypedVariableDictionary = [
+ "string": .string("SwiftScream"),
+ "int": .string("42"),
+ "bool": .string("true"),
+ "list": .list(["SwiftScream", "42", "true"]),
+ "ordered": .associativeArray([
+ ("b", "42"),
+ ("a", "A"),
+ ("c", "true"),
+ ]),
+ ]
+ let urlString = try template.process(variables: variables)
+ XCTAssertEqual("https://api.example.com/SwiftScream/42/true/SwiftScream,42,true?b=42&a=A&c=true", urlString)
+ }
+
+ func testSendable() {
+ let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
+ let sendable = template as Sendable
+ XCTAssertNotNil(sendable)
+ }
func testCustomStringConvertible() {
let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
XCTAssertEqual(template.description, "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}")
}
- func testExpressibleByStringLiteral() {
+ func testExpressibleByStringLiteral() throws {
let templateA: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
- guard let templateB = try? URITemplate(string: "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}") else {
- XCTFail("invalid template")
- return
- }
+ let templateB = try URITemplate(string: "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}")
XCTAssertEqual(templateA, templateB)
}
@@ -73,29 +191,21 @@ class Tests: XCTestCase {
XCTAssertEqual(variableNames, expected)
}
- func testEncoding() {
- do {
- let templateString = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
- let template = try URITemplate(string: templateString)
- let jsonData = try JSONEncoder().encode(["a": template])
- let expectedData = try JSONEncoder().encode(["a": templateString])
- XCTAssertEqual(jsonData, expectedData)
- } catch {
- XCTFail("unexpected throw")
- }
+ func testEncoding() throws {
+ let templateString = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
+ let template = try URITemplate(string: templateString)
+ let jsonData = try JSONEncoder().encode(["a": template])
+ let expectedData = try JSONEncoder().encode(["a": templateString])
+ XCTAssertEqual(jsonData, expectedData)
}
- func testDecoding() {
- do {
- let templateString = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
- let jsonString = "{\"a\":\"\(templateString)\"}"
- let jsonData = jsonString.data(using: .utf8)!
- let object = try JSONDecoder().decode([String: URITemplate].self, from: jsonData)
- let expectedTemplate = try URITemplate(string: templateString)
- XCTAssertEqual(object["a"], expectedTemplate)
- } catch {
- XCTFail("unexpected throw")
- }
+ func testDecoding() throws {
+ let templateString = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
+ let jsonString = "{\"a\":\"\(templateString)\"}"
+ let jsonData = jsonString.data(using: .utf8)!
+ let object = try JSONDecoder().decode([String: URITemplate].self, from: jsonData)
+ let expectedTemplate = try URITemplate(string: templateString)
+ XCTAssertEqual(object["a"], expectedTemplate)
}
func testInitPerformance() {
@@ -108,9 +218,11 @@ class Tests: XCTestCase {
func testProcessPerformance() {
let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
- let variables = ["owner": "SwiftScream",
- "repo": "URITemplate",
- "username": "alexdeem"]
+ let variables = [
+ "owner": "SwiftScream",
+ "repo": "URITemplate",
+ "username": "alexdeem",
+ ]
measure {
for _ in 1...5000 {