Skip to content

Commit

Permalink
3.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdeem committed Jan 20, 2023
2 parents 084c1e8 + bb770dc commit 3d8e9dc
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 26 deletions.
18 changes: 15 additions & 3 deletions .github/workflows/build-and-test.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build and Test
name: CI

on:
push:
Expand All @@ -8,9 +8,7 @@ on:

jobs:
build:

runs-on: macos-latest

steps:
- uses: actions/checkout@v3
with:
Expand All @@ -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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented in this file.

<a name="3.1.0"></a>
# [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


<a name="3.0.1"></a>
# [3.0.1](https://github.com/SwiftScream/URITemplate/compare/3.0.0...3.0.1) (2023-01-08)

Expand Down
13 changes: 9 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.7
// swift-tools-version: 5.4

import PackageDescription

Expand All @@ -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
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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.
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.
2 changes: 1 addition & 1 deletion Sources/ScreamURITemplate/Internal/CharacterSets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "_%."))
Expand Down
8 changes: 7 additions & 1 deletion Sources/ScreamURITemplate/Internal/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Foundation

internal struct ExpansionConfiguration {
let percentEncodingAllowedCharacterSet: CharacterSet
let allowPercentEncodedTriplets: Bool
let prefix: String?
let separator: String
let named: Bool
Expand Down
9 changes: 9 additions & 0 deletions Sources/ScreamURITemplate/Internal/ExpressionOperator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 29 additions & 9 deletions Sources/ScreamURITemplate/Internal/ValueFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<encoded.endIndex
while let matchRange = encoded.range(of: "%25", range: searchRange) {
searchRange = matchRange.upperBound..<encoded.endIndex

let firstIndex = matchRange.upperBound
guard firstIndex < encoded.endIndex, encoded[firstIndex].isHexDigit else {
continue
}
let secondIndex = encoded.index(after: firstIndex)
guard secondIndex < encoded.endIndex, encoded[secondIndex].isHexDigit else {
continue
}

let removeRange = encoded.index(after: matchRange.lowerBound)..<matchRange.upperBound
encoded.removeSubrange(removeRange)
searchRange = removeRange.lowerBound..<encoded.endIndex
}
}
return encoded
}

Expand All @@ -33,7 +53,7 @@ internal extension StringProtocol {
} else {
modifiedValue = String(self)
}
let encodedExpansion = try percentEncode(string: modifiedValue, withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet)
let encodedExpansion = try percentEncode(string: modifiedValue, withAllowedCharacters: expansionConfiguration.percentEncodingAllowedCharacterSet, allowPercentEncodedTriplets: expansionConfiguration.allowPercentEncodedTriplets)
if expansionConfiguration.named {
if encodedExpansion.isEmpty && expansionConfiguration.omittOrphanedEquals {
return String(variableSpec.name)
Expand All @@ -48,7 +68,7 @@ internal extension Array where Element: StringProtocol {
func formatForTemplateExpansion(variableSpec: VariableSpec, expansionConfiguration: ExpansionConfiguration) throws -> 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
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/ScreamURITemplate/URITemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public struct URITemplate {
}
}

#if swift(>=5.5)
extension URITemplate: Sendable {}
#endif

extension URITemplate: CustomStringConvertible {
public var description: String {
return string
Expand Down
13 changes: 12 additions & 1 deletion Tests/ScreamURITemplateTests/TestModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions Tests/ScreamURITemplateTests/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down

0 comments on commit 3d8e9dc

Please sign in to comment.