diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/ci.yml
similarity index 68%
rename from .github/workflows/build-and-test.yml
rename to .github/workflows/ci.yml
index 3d8bcec..87acf80 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,4 @@
-name: Build and Test
+name: CI
on:
push:
@@ -8,9 +8,7 @@ on:
jobs:
build:
-
runs-on: macos-latest
-
steps:
- uses: actions/checkout@v3
with:
@@ -26,3 +24,17 @@ jobs:
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/CHANGELOG.md b/CHANGELOG.md
index 8d608c0..276ed26 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
+
+# [3.1.0](https://github.com/SwiftScream/URITemplate/compare/3.0.1...3.1.0) (2023-01-20)
+
+- Allow single-quote as literal, refer [RFC errata 6937](https://www.rfc-editor.org/errata/eid6937)
+- Add `Sendable` conformance
+- Do not encode percent-encoded-triplets in reserved or fragment expansion
+- Update test suite
+
+
# [3.0.1](https://github.com/SwiftScream/URITemplate/compare/3.0.0...3.0.1) (2023-01-08)
diff --git a/Package.swift b/Package.swift
index 5793e9f..fd7ed4d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.7
+// swift-tools-version: 5.4
import PackageDescription
@@ -25,8 +25,13 @@ let package = Package(
.process("data/uritemplate-test/extended-tests.json"),
.process("data/uritemplate-test/negative-tests.json"),
]),
- .executableTarget(
- name: "ScreamURITemplateExample",
- dependencies: ["ScreamURITemplate"]),
],
swiftLanguageVersions: [.v5])
+
+#if swift(>=5.6) || os(macOS) || os(Linux)
+ package.targets.append(
+ .executableTarget(
+ name: "ScreamURITemplateExample",
+ dependencies: ["ScreamURITemplate"])
+ )
+#endif
diff --git a/README.md b/README.md
index 43c6ea8..0669356 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,14 @@
-# URITemplate [![license](https://img.shields.io/github/license/SwiftScream/URITemplate.svg)](https://raw.githubusercontent.com/SwiftScream/URITemplate/master/LICENSE) [![GitHub release](https://img.shields.io/github/release/SwiftScream/URITemplate.svg)](https://github.com/SwiftScream/URITemplate/releases/latest)
+# ScreamURITemplate
+A robust and performant Swift 5 implementation of [RFC6570](https://tools.ietf.org/html/rfc6570) URI Template. Full Level 4 support is provided.
-[![Travis](https://api.travis-ci.com/SwiftScream/URITemplate.svg?branch=master)](https://travis-ci.com/SwiftScream/URITemplate)
+[![CI](https://github.com/SwiftScream/URITemplate/actions/workflows/ci.yml/badge.svg)](https://github.com/SwiftScream/URITemplate/actions/workflows/ci.yml)
[![Codecov branch](https://img.shields.io/codecov/c/github/SwiftScream/URITemplate/master.svg)](https://codecov.io/gh/SwiftScream/URITemplate/branch/master)
-![Swift 5](https://img.shields.io/badge/swift-5-4BC51D.svg?style=flat)
-[![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/)
+[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSwiftScream%2FURITemplate%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/SwiftScream/URITemplate)
+[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSwiftScream%2FURITemplate%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/SwiftScream/URITemplate)
-A robust and performant Swift 5 implementation of [RFC6570](https://tools.ietf.org/html/rfc6570) URI Template. Full Level 4 support is provided.
+[![license](https://img.shields.io/github/license/SwiftScream/URITemplate.svg)](https://raw.githubusercontent.com/SwiftScream/URITemplate/master/LICENSE) [![GitHub release](https://img.shields.io/github/release/SwiftScream/URITemplate.svg)](https://github.com/SwiftScream/URITemplate/releases/latest)
## Getting Started
@@ -19,6 +20,8 @@ Add `.package(url: "https://github.com/SwiftScream/URITemplate.git", from: "3.0.
### Template Processing
```swift
+import ScreamURITemplate
+
let template = try URITemplate(string:"https://api.github.com/repos/{owner}/{repository}/traffic/views")
let variables = ["owner":"SwiftScream", "repository":"URITemplate"]
let urlString = try template.process(variables)
@@ -56,4 +59,4 @@ struct HALObject : Codable {
```
## Tests
-The library is tested against the [standard test suite](https://github.com/uri-templates/uritemplate-test), as well as some additional tests for behavior specific to this implementation. It is intended to keep test coverage as high as possible.
\ No newline at end of file
+The library is tested against the [standard test suite](https://github.com/uri-templates/uritemplate-test), as well as some additional tests for behavior specific to this implementation. It is intended to keep test coverage as high as possible.
diff --git a/Sources/ScreamURITemplate/Internal/CharacterSets.swift b/Sources/ScreamURITemplate/Internal/CharacterSets.swift
index 235f47e..ec56760 100644
--- a/Sources/ScreamURITemplate/Internal/CharacterSets.swift
+++ b/Sources/ScreamURITemplate/Internal/CharacterSets.swift
@@ -19,7 +19,7 @@ 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 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: "_%."))
diff --git a/Sources/ScreamURITemplate/Internal/Components.swift b/Sources/ScreamURITemplate/Internal/Components.swift
index 38017dc..c6d7db0 100644
--- a/Sources/ScreamURITemplate/Internal/Components.swift
+++ b/Sources/ScreamURITemplate/Internal/Components.swift
@@ -14,7 +14,13 @@
import Foundation
-internal protocol Component {
+#if swift(>=5.5)
+ internal typealias ComponentBase = Sendable
+#else
+ internal protocol ComponentBase {}
+#endif
+
+internal protocol Component: ComponentBase {
func expand(variables: [String: VariableValue]) throws -> String
var variableNames: [String] { get }
}
diff --git a/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift b/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift
index 68fb182..f4c2a49 100644
--- a/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift
+++ b/Sources/ScreamURITemplate/Internal/ExpansionConfiguration.swift
@@ -16,6 +16,7 @@ import Foundation
internal struct ExpansionConfiguration {
let percentEncodingAllowedCharacterSet: CharacterSet
+ let allowPercentEncodedTriplets: Bool
let prefix: String?
let separator: String
let named: Bool
diff --git a/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift b/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift
index 19d083c..cfd13d0 100644
--- a/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift
+++ b/Sources/ScreamURITemplate/Internal/ExpressionOperator.swift
@@ -24,52 +24,61 @@ internal enum ExpressionOperator: Unicode.Scalar {
case query = "?"
case queryContinuation = "&"
+ // swiftlint:disable:next function_body_length
func expansionConfiguration() -> ExpansionConfiguration {
switch self {
case .simple:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
+ allowPercentEncodedTriplets: false,
prefix: nil,
separator: ",",
named: false,
omittOrphanedEquals: false)
case .reserved:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: reservedAndUnreservedCharacterSet,
+ allowPercentEncodedTriplets: true,
prefix: nil,
separator: ",",
named: false,
omittOrphanedEquals: false)
case .fragment:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: reservedAndUnreservedCharacterSet,
+ allowPercentEncodedTriplets: true,
prefix: "#",
separator: ",",
named: false,
omittOrphanedEquals: false)
case .label:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
+ allowPercentEncodedTriplets: false,
prefix: ".",
separator: ".",
named: false,
omittOrphanedEquals: false)
case .pathSegment:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
+ allowPercentEncodedTriplets: false,
prefix: "/",
separator: "/",
named: false,
omittOrphanedEquals: false)
case .pathStyle:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
+ allowPercentEncodedTriplets: false,
prefix: ";",
separator: ";",
named: true,
omittOrphanedEquals: true)
case .query:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
+ allowPercentEncodedTriplets: false,
prefix: "?",
separator: "&",
named: true,
omittOrphanedEquals: false)
case .queryContinuation:
return ExpansionConfiguration(percentEncodingAllowedCharacterSet: unreservedCharacterSet,
+ allowPercentEncodedTriplets: false,
prefix: "&",
separator: "&",
named: true,
diff --git a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift
index 9b5ddbc..0432976 100644
--- a/Sources/ScreamURITemplate/Internal/ValueFormatting.swift
+++ b/Sources/ScreamURITemplate/Internal/ValueFormatting.swift
@@ -18,10 +18,30 @@ internal enum FormatError: Error {
case failure(reason: String)
}
-internal func percentEncode(string: String, withAllowedCharacters allowedCharacterSet: CharacterSet) throws -> String {
- guard let encoded = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) else {
+internal 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")
}
+ if allowPercentEncodedTriplets {
+ // Revert where any percent-encode-triplets had their % encoded (to %25)
+ var searchRange = encoded.startIndex.. String? {
let separator = ","
let encodedExpansions = try map { element -> String in
- return try percentEncode(string: String(element), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet)
+ return try percentEncode(string: String(element), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
}
if encodedExpansions.count == 0 {
return nil
@@ -66,7 +86,7 @@ internal extension Array where Element: StringProtocol {
func explodeForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? {
let separator = expansionConfiguration.separator
let encodedExpansions = try map { element -> String in
- let encodedElement = try percentEncode(string: String(element), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet)
+ let encodedElement = try percentEncode(string: String(element), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
if expansionConfiguration.named {
if encodedElement.isEmpty && expansionConfiguration.omittOrphanedEquals {
return String(variableSpec.name)
@@ -85,8 +105,8 @@ internal extension Array where Element: StringProtocol {
internal extension Dictionary where Key: StringProtocol, Value: StringProtocol {
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)
- let encodedValue = try percentEncode(string: String(value), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet)
+ let encodedKey = try percentEncode(string: String(key), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
+ let encodedValue = try percentEncode(string: String(value), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
return "\(encodedKey),\(encodedValue)"
}
if encodedExpansions.count == 0 {
@@ -102,8 +122,8 @@ internal extension Dictionary where Key: StringProtocol, Value: StringProtocol {
func explodeForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> String? {
let separator = expansionConfiguration.separator
let encodedExpansions = try map { key, value -> String in
- let encodedKey = try percentEncode(string: String(key), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet)
- let encodedValue = try percentEncode(string: String(value), withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet)
+ 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 {
return String(variableSpec.name)
}
diff --git a/Sources/ScreamURITemplate/URITemplate.swift b/Sources/ScreamURITemplate/URITemplate.swift
index bd36075..8c31bc7 100644
--- a/Sources/ScreamURITemplate/URITemplate.swift
+++ b/Sources/ScreamURITemplate/URITemplate.swift
@@ -53,6 +53,10 @@ public struct URITemplate {
}
}
+#if swift(>=5.5)
+ extension URITemplate: Sendable {}
+#endif
+
extension URITemplate: CustomStringConvertible {
public var description: String {
return string
diff --git a/Tests/ScreamURITemplateTests/TestModels.swift b/Tests/ScreamURITemplateTests/TestModels.swift
index f424952..37efab9 100644
--- a/Tests/ScreamURITemplateTests/TestModels.swift
+++ b/Tests/ScreamURITemplateTests/TestModels.swift
@@ -86,7 +86,18 @@ extension TestCase {
let expansionsData = data[1]
switch expansionsData {
case let .string(string):
- acceptableExpansions = [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]
+ }
shouldFail = false
case let .array(array):
acceptableExpansions = array.compactMap { value in
diff --git a/Tests/ScreamURITemplateTests/Tests.swift b/Tests/ScreamURITemplateTests/Tests.swift
index 185284f..dc447ba 100644
--- a/Tests/ScreamURITemplateTests/Tests.swift
+++ b/Tests/ScreamURITemplateTests/Tests.swift
@@ -16,6 +16,14 @@ 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)
+ }
+ #endif
+
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}")
diff --git a/Tests/ScreamURITemplateTests/data/uritemplate-test b/Tests/ScreamURITemplateTests/data/uritemplate-test
index 520fdd8..1eb27ab 160000
--- a/Tests/ScreamURITemplateTests/data/uritemplate-test
+++ b/Tests/ScreamURITemplateTests/data/uritemplate-test
@@ -1 +1 @@
-Subproject commit 520fdd8b0f78779d12178c357a986e0e727f4bd0
+Subproject commit 1eb27ab4462b9e5819dc47db99044f5fd1fa9bc7