Skip to content

Commit be8c8e0

Browse files
committed
Initial Commit
0 parents  commit be8c8e0

File tree

6 files changed

+591
-0
lines changed

6 files changed

+591
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

Package.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version:5.5
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "URLQueryEncoder",
8+
platforms: [
9+
.macOS(.v10_13),
10+
.iOS(.v11),
11+
.tvOS(.v11),
12+
.watchOS(.v4)
13+
],
14+
products: [
15+
.library(
16+
name: "URLQueryEncoder",
17+
targets: ["URLQueryEncoder"]),
18+
],
19+
targets: [
20+
.target(
21+
name: "URLQueryEncoder",
22+
dependencies: []),
23+
.testTarget(
24+
name: "URLQueryEncoderTests",
25+
dependencies: ["URLQueryEncoder"]),
26+
]
27+
)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# URLQueryEncoder
2+
3+
A description of this package.
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2021 Alexander Grebenyuk (github.com/kean).
4+
5+
import Foundation
6+
7+
#warning("should this be a struct?")
8+
public class URLQueryEncoder {
9+
public var explode = true
10+
private var _explode = true
11+
12+
public var delimeter = ","
13+
private var _delimeter = ","
14+
15+
public var isDeepObject = false
16+
private var _isDeepObject = false
17+
18+
/// By default, `.iso8601`.
19+
public var dateEncodingStrategy: DateEncodingStrategy = .iso8601
20+
21+
/// The strategy to use for encoding `Date` values.
22+
public enum DateEncodingStrategy {
23+
/// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
24+
case iso8601
25+
26+
/// Encode the `Date` as a UNIX timestamp (as a JSON number).
27+
case secondsSince1970
28+
29+
/// Encode the `Date` as UNIX millisecond timestamp (as a JSON number).
30+
case millisecondsSince1970
31+
32+
/// Encode the `Date` as a string formatted by the given formatter.
33+
case formatted(DateFormatter)
34+
35+
/// Encode the `Date` as a custom value encoded by the given closure.
36+
///
37+
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
38+
case custom((Date) -> String)
39+
}
40+
41+
#warning("this should be private")
42+
public fileprivate(set) var codingPath: [CodingKey] = []
43+
public fileprivate(set) var queryItems: [URLQueryItem] = []
44+
45+
public var items: [(String, String?)] {
46+
queryItems.map { ($0.name, $0.value) }
47+
}
48+
49+
/// Returns the query as a string.
50+
public var query: String? {
51+
urlComponents.query
52+
}
53+
54+
/// Returns the query as a string with percent-encoded values.
55+
public var percentEncodedQuery: String? {
56+
urlComponents.percentEncodedQuery
57+
}
58+
59+
private var urlComponents: URLComponents {
60+
var components = URLComponents()
61+
components.queryItems = queryItems
62+
return components
63+
}
64+
65+
public init() {}
66+
67+
#warning("make throwing?")
68+
#warning("simplify how configuration is passed")
69+
public func encode(_ value: Encodable, explode: Bool? = nil, delimeter: String? = nil, isDeepObject: Bool? = nil) {
70+
_explode = explode ?? self.explode
71+
_delimeter = delimeter ?? self.delimeter
72+
_isDeepObject = isDeepObject ?? self.isDeepObject
73+
try? value.encode(to: self)
74+
}
75+
76+
#warning("do we need this method?")
77+
public func encode<V: Encodable>(_ value: V, forKey key: String, explode: Bool? = nil, delimeter: String? = nil, isDeepObject: Bool? = nil) {
78+
_explode = explode ?? self.explode
79+
_delimeter = delimeter ?? self.delimeter
80+
_isDeepObject = isDeepObject ?? self.isDeepObject
81+
try? [key: value].encode(to: self)
82+
}
83+
84+
public static func data(for queryItems: [URLQueryItem]) -> Data {
85+
var components = URLComponents()
86+
components.queryItems = queryItems
87+
return components.percentEncodedQuery?.data(using: .utf8) ?? Data()
88+
}
89+
}
90+
91+
private extension URLQueryEncoder {
92+
#warning("test encodeNil extensively!")
93+
func encodeNil(forKey codingPath: [CodingKey]) throws {
94+
queryItems.append(URLQueryItem(name: codingPath.last?.stringValue ?? "", value: nil))
95+
}
96+
97+
func encode(_ value: String, forKey codingPath: [CodingKey]) throws {
98+
append(value, forKey: codingPath)
99+
}
100+
101+
func encode(_ value: Bool, forKey codingPath: [CodingKey]) throws {
102+
append(value ? "true" : "false", forKey: codingPath)
103+
}
104+
105+
func encode(_ value: Int, forKey codingPath: [CodingKey]) throws {
106+
append(String(value), forKey: codingPath)
107+
}
108+
109+
func encode(_ value: Int8, forKey codingPath: [CodingKey]) throws {
110+
append(String(value), forKey: codingPath)
111+
}
112+
113+
func encode(_ value: Int16, forKey codingPath: [CodingKey]) throws {
114+
append(String(value), forKey: codingPath)
115+
}
116+
117+
func encode(_ value: Int32, forKey codingPath: [CodingKey]) throws {
118+
append(String(value), forKey: codingPath)
119+
}
120+
121+
func encode(_ value: Int64, forKey codingPath: [CodingKey]) throws {
122+
append(String(value), forKey: codingPath)
123+
}
124+
125+
func encode(_ value: UInt, forKey codingPath: [CodingKey]) throws {
126+
append(String(value), forKey: codingPath)
127+
}
128+
129+
func encode(_ value: UInt8, forKey codingPath: [CodingKey]) throws {
130+
append(String(value), forKey: codingPath)
131+
}
132+
133+
func encode(_ value: UInt16, forKey codingPath: [CodingKey]) throws {
134+
append(String(value), forKey: codingPath)
135+
}
136+
137+
func encode(_ value: UInt32, forKey codingPath: [CodingKey]) throws {
138+
append(String(value), forKey: codingPath)
139+
}
140+
141+
func encode(_ value: UInt64, forKey codingPath: [CodingKey]) throws {
142+
append(String(value), forKey: codingPath)
143+
}
144+
145+
func encode(_ value: Double, forKey codingPath: [CodingKey]) throws {
146+
append(String(value), forKey: codingPath)
147+
}
148+
149+
func encode(_ value: Float, forKey codingPath: [CodingKey]) throws {
150+
append(String(value), forKey: codingPath)
151+
}
152+
153+
func encode(_ value: URL, forKey codingPath: [CodingKey]) throws {
154+
append(value.absoluteString, forKey: codingPath)
155+
}
156+
157+
func encode(_ value: Date, forKey codingPath: [CodingKey]) throws {
158+
let string: String
159+
switch dateEncodingStrategy {
160+
case .iso8601: string = iso8601Formatter.string(from: value)
161+
case .secondsSince1970: string = String(value.timeIntervalSince1970)
162+
case .millisecondsSince1970: string = String(Int(value.timeIntervalSince1970 * 1000))
163+
case .formatted(let formatter): string = formatter.string(from: value)
164+
case .custom(let closure): string = closure(value)
165+
}
166+
append(string, forKey: codingPath)
167+
}
168+
169+
func encodeEncodable<T: Encodable>(_ value: T, forKey codingPath: [CodingKey]) throws {
170+
self.codingPath = codingPath
171+
switch value {
172+
case let value as String: try encode(value, forKey: codingPath)
173+
case let value as Bool: try encode(value, forKey: codingPath)
174+
case let value as Int: try encode(value, forKey: codingPath)
175+
case let value as Int8: try encode(value, forKey: codingPath)
176+
case let value as Int16: try encode(value, forKey: codingPath)
177+
case let value as Int32: try encode(value, forKey: codingPath)
178+
case let value as Int64: try encode(value, forKey: codingPath)
179+
case let value as UInt: try encode(value, forKey: codingPath)
180+
case let value as UInt8: try encode(value, forKey: codingPath)
181+
case let value as UInt16: try encode(value, forKey: codingPath)
182+
case let value as UInt32: try encode(value, forKey: codingPath)
183+
case let value as UInt64: try encode(value, forKey: codingPath)
184+
case let value as Double: try encode(value, forKey: codingPath)
185+
case let value as Float: try encode(value, forKey: codingPath)
186+
case let value as Date: try encode(value, forKey: codingPath)
187+
case let value as URL: try encode(value, forKey: codingPath)
188+
case let value: try value.encode(to: self)
189+
}
190+
}
191+
192+
#warning("refactor")
193+
func append(_ value: String, forKey codingPath: [CodingKey]) {
194+
guard !codingPath.isEmpty else {
195+
return // Should never happen
196+
}
197+
let key = codingPath[0].stringValue
198+
if _explode {
199+
if codingPath.count == 2 { // Encoding an object
200+
if _isDeepObject {
201+
queryItems.append(URLQueryItem(name: "\(key)[\(codingPath[1].stringValue)]", value: value))
202+
} else {
203+
queryItems.append(URLQueryItem(name: codingPath[1].stringValue, value: value))
204+
}
205+
} else {
206+
queryItems.append(URLQueryItem(name: key, value: value))
207+
}
208+
} else {
209+
if codingPath.count == 2 { // Encoding an object
210+
let newValue = "\(codingPath[1].stringValue),\(value)"
211+
if var queryItem = queryItems.last, queryItem.name == key {
212+
queryItem.value = [queryItem.value, newValue].compactMap({ $0 }).joined(separator: ",")
213+
queryItems[queryItems.endIndex - 1] = queryItem
214+
} else {
215+
queryItems.append(URLQueryItem(name: key, value: newValue))
216+
}
217+
} else { // Encoding an array or a primitive value
218+
if var queryItem = queryItems.last, queryItem.name == key {
219+
queryItem.value = [queryItem.value, value].compactMap { $0 }.joined(separator: _delimeter)
220+
queryItems[queryItems.endIndex - 1] = queryItem
221+
} else {
222+
queryItems.append(URLQueryItem(name: key, value: value))
223+
}
224+
}
225+
}
226+
}
227+
}
228+
229+
#warning("TODO: remove from extension")
230+
extension URLQueryEncoder: Encoder {
231+
public var userInfo: [CodingUserInfoKey : Any] { return [:] }
232+
233+
public func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
234+
return KeyedEncodingContainer(KeyedContainer<Key>(encoder: self, codingPath: codingPath))
235+
}
236+
237+
public func unkeyedContainer() -> UnkeyedEncodingContainer {
238+
return UnkeyedContanier(encoder: self, codingPath: codingPath)
239+
}
240+
241+
public func singleValueContainer() -> SingleValueEncodingContainer {
242+
return SingleValueContanier(encoder: self, codingPath: codingPath)
243+
}
244+
}
245+
246+
private struct KeyedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
247+
let encoder: URLQueryEncoder
248+
let codingPath: [CodingKey]
249+
250+
func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
251+
let codingPath = self.codingPath + [key]
252+
encoder.codingPath = codingPath
253+
defer { encoder.codingPath.removeLast() }
254+
try encoder.encodeEncodable(value, forKey: codingPath)
255+
}
256+
257+
func encodeNil(forKey key: Key) throws {
258+
let codingPath = self.codingPath + [key]
259+
encoder.codingPath = codingPath
260+
defer { encoder.codingPath.removeLast() }
261+
try encoder.encodeNil(forKey: codingPath)
262+
}
263+
264+
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
265+
return KeyedEncodingContainer(KeyedContainer<NestedKey>(encoder: encoder, codingPath: codingPath + [key]))
266+
}
267+
268+
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
269+
return UnkeyedContanier(encoder: encoder, codingPath: codingPath + [key])
270+
}
271+
272+
func superEncoder() -> Encoder {
273+
encoder
274+
}
275+
276+
func superEncoder(forKey key: Key) -> Encoder {
277+
encoder
278+
}
279+
}
280+
281+
private final class UnkeyedContanier: UnkeyedEncodingContainer {
282+
var encoder: URLQueryEncoder
283+
var codingPath: [CodingKey]
284+
285+
private(set) var count = 0
286+
287+
init(encoder: URLQueryEncoder, codingPath: [CodingKey]) {
288+
self.encoder = encoder
289+
self.codingPath = codingPath
290+
}
291+
292+
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
293+
KeyedEncodingContainer(KeyedContainer<NestedKey>(encoder: encoder, codingPath: codingPath))
294+
}
295+
296+
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
297+
self
298+
}
299+
300+
func superEncoder() -> Encoder {
301+
encoder
302+
}
303+
304+
func encodeNil() throws {
305+
try encoder.encodeNil(forKey: codingPath)
306+
count += 1
307+
}
308+
309+
func encode<T>(_ value: T) throws where T: Encodable {
310+
try encoder.encodeEncodable(value, forKey: codingPath)
311+
count += 1
312+
}
313+
}
314+
315+
private struct SingleValueContanier: SingleValueEncodingContainer {
316+
let encoder: URLQueryEncoder
317+
var codingPath: [CodingKey]
318+
319+
init(encoder: URLQueryEncoder, codingPath: [CodingKey]) {
320+
self.encoder = encoder
321+
self.codingPath = codingPath
322+
}
323+
324+
mutating func encodeNil() throws {
325+
try encoder.encodeNil(forKey: codingPath)
326+
}
327+
328+
mutating func encode<T>(_ value: T) throws where T : Encodable {
329+
encoder.codingPath = self.codingPath
330+
try encoder.encodeEncodable(value, forKey: codingPath)
331+
}
332+
}
333+
334+
private let iso8601Formatter = ISO8601DateFormatter()

0 commit comments

Comments
 (0)