Skip to content

Commit

Permalink
WIP improve variable interface
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdeem committed Jun 8, 2024
1 parent 4c2b9fe commit fd86335
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 48 deletions.
25 changes: 12 additions & 13 deletions Sources/ScreamURITemplate/Internal/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Foundation
typealias ComponentBase = Sendable

protocol Component: ComponentBase {
func expand(variables: [String: VariableValue]) throws -> String
func expand(variables: VariableProvider) throws -> String
var variableNames: [String] { get }
}

Expand All @@ -33,7 +33,7 @@ struct LiteralComponent: Component {
literal = string
}

func expand(variables _: [String: VariableValue]) throws -> String {
func expand(variables _: VariableProvider) 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")
Expand All @@ -48,7 +48,7 @@ struct LiteralPercentEncodedTripletComponent: Component {
literal = string
}

func expand(variables _: [String: VariableValue]) throws -> String {
func expand(variables _: VariableProvider) throws -> String {
return String(literal)
}
}
Expand All @@ -65,16 +65,17 @@ struct ExpressionComponent: Component {
}

// swiftlint:disable:next cyclomatic_complexity
func expand(variables: [String: VariableValue]) throws -> String {
func expand(variables: VariableProvider) throws -> String {
let configuration = expressionOperator.expansionConfiguration()
let expansions = try variableList.compactMap { variableSpec -> String? in
guard let value = variables[String(variableSpec.name)] else {
guard let value = variables[String(variableSpec.name)]?.asTypedVariableValue() 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 value {
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")
Expand All @@ -83,17 +84,15 @@ struct ExpressionComponent: Component {
case .none:
return try arrayValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
}
} else if let dictionaryValue = value as? [String: String] {
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 dictionaryValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
return try associativeArrayValue.explodeForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
case .none:
return try dictionaryValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
return try associativeArrayValue.formatForTemplateExpansion(variableSpec: variableSpec, expansionConfiguration: configuration)
}
} else {
throw FormatError.failure(reason: "Invalid Value Type")
}
} catch let FormatError.failure(reason) {
throw URITemplate.Error.expansionFailure(position: templatePosition, reason: "Failed expanding variable \"\(variableSpec.name)\": \(reason)")
Expand Down
2 changes: 1 addition & 1 deletion Sources/ScreamURITemplate/Internal/ValueFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ extension Array where Element: StringProtocol {
}
}

extension Dictionary where Key: StringProtocol, Value: StringProtocol {
extension [TypedVariableValue.AssociativeArrayElement] {
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)
Expand Down
62 changes: 57 additions & 5 deletions Sources/ScreamURITemplate/URITemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,58 @@

import Foundation

public protocol VariableValue {}
extension String: VariableValue {}
extension Array: VariableValue where Element: StringProtocol {}
extension Dictionary: VariableValue where Key: StringProtocol, Value: StringProtocol {}
public protocol VariableProvider {
subscript(_: String) -> VariableValue? { get }
}

public typealias VariableDictionary = [String: VariableValue]
extension VariableDictionary: VariableProvider {}

public enum TypedVariableValue {
public typealias AssociativeArrayElement = (key: String, value: String)

case string(String)
case list([String])
case associativeArray([AssociativeArrayElement])
}

public protocol VariableValue {
func asTypedVariableValue() -> TypedVariableValue?
}

public protocol StringVariableValue: VariableValue {
func asStringVariableValue() -> String
}

public extension StringVariableValue {
func asTypedVariableValue() -> TypedVariableValue? {
.string(asStringVariableValue())
}
}

extension String: StringVariableValue {
public func asStringVariableValue() -> String {
self
}
}

extension Int: StringVariableValue {
public func asStringVariableValue() -> String {
String(self)
}
}

extension [StringVariableValue]: VariableValue {
public func asTypedVariableValue() -> TypedVariableValue? {
.list(map { $0.asStringVariableValue() })
}
}

extension [String: StringVariableValue]: VariableValue {
public func asTypedVariableValue() -> TypedVariableValue? {
.associativeArray(map { ($0, $1.asStringVariableValue()) })
}
}

public struct URITemplate {
public enum Error: Swift.Error {
Expand All @@ -38,14 +86,18 @@ public struct URITemplate {
self.components = components
}

public func process(variables: [String: VariableValue]) throws -> String {
public func process(variables: VariableProvider) throws -> String {
var result = ""
for component in components {
result += try component.expand(variables: variables)
}
return result
}

public func process(variables: [String: String]) throws -> String {
return try process(variables: variables as VariableDictionary)
}

public var variableNames: [String] {
return components.flatMap { component in
return component.variableNames
Expand Down
2 changes: 1 addition & 1 deletion Tests/ScreamURITemplateTests/TestFileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
54 changes: 26 additions & 28 deletions Tests/ScreamURITemplateTests/TestModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private struct TestGroupDecodable: Decodable {
public struct TestGroup {
public let name: String
public let level: Int?
public let variables: [String: VariableValue]
public let variables: VariableDictionary
public let testcases: [TestCase]
}

Expand All @@ -38,35 +38,38 @@ public struct TestCase {
public let failReason: String?
}

extension JSONValue {
func toVariableValue() -> VariableValue? {
extension JSONValue: VariableValue {
public func asTypedVariableValue() -> ScreamURITemplate.TypedVariableValue? {
switch self {
case let .int(int):
return String(int)
return .string(String(int))
case let .double(double):
return String(double)
return .string(String(double))
case let .string(string):
return string
return .string(string)
case let .object(object):
return object.mapValues { element -> String? in
switch element {
case let .string(string):
return string
default:
return .associativeArray(object.compactMap { key, value in
guard let string = value.asString() else {
return nil
}
}.filter { $0.value != nil }
.mapValues { $0! }
return (key, string)
})
case let .array(array):
return array.compactMap { element -> String? in
switch element {
case let .string(string):
return string
default:
return nil
}
}
default:
return .list(array.compactMap { $0.asString() })
case .null, .bool:
return nil
}
}

private func asString() -> String? {
switch self {
case let .int(int):
return String(int)
case let .double(double):
return String(double)
case let .string(string):
return string
case .null, .bool, .object, .array:
return nil
}
}
Expand Down Expand Up @@ -143,15 +146,10 @@ public func parseTestFile(URL: URL) -> [TestGroup]? {
}

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)
}
}
23 changes: 23 additions & 0 deletions Tests/ScreamURITemplateTests/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ import ScreamURITemplate
import XCTest

class Tests: XCTestCase {
func testIntVariable() throws {
let template: URITemplate = "https://api.example.com/{string}/{int}"
let variables: VariableDictionary = [
"string": "SwiftScream",
"int": 42,
]
let urlString = try template.process(variables: variables)
XCTAssertEqual(urlString, "https://api.example.com/SwiftScream/42")
}

func testDictVariable() throws {
let template: URITemplate = "https://api.example.com/{string}{?dict*}"
let variables: VariableDictionary = [
"string": "SwiftScream",
"dict": [
"a": "A",
"int": 42,
],
]
let urlString = try template.process(variables: variables)
XCTAssertTrue(["https://api.example.com/SwiftScream?a=A&int=42", "https://api.example.com/SwiftScream?int=42&a=A"].contains(urlString))
}

func testSendable() {
let template: URITemplate = "https://api.github.com/repos/{owner}/{repo}/collaborators/{username}"
let sendable = template as Sendable
Expand Down

0 comments on commit fd86335

Please sign in to comment.