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 {