├── .gitignore
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── URLEncodedForm
│ ├── Codable
│ ├── URLEncodedFormDecoder.swift
│ └── URLEncodedFormEncoder.swift
│ ├── Data
│ ├── URLEncodedFormData.swift
│ ├── URLEncodedFormDataConvertible.swift
│ ├── URLEncodedFormParser.swift
│ └── URLEncodedFormSerializer.swift
│ └── Utilities
│ ├── Exports.swift
│ └── URLEncodedFormError.swift
├── Tests
├── LinuxMain.swift
└── URLEncodedFormTests
│ ├── URLEncodedFormCodableTests.swift
│ ├── URLEncodedFormParserTests.swift
│ └── URLEncodedFormSerializerTests.swift
└── circle.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | Package.resolved
6 | DerivedData
7 | .swiftpm
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Qutheory, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.1
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "URLEncodedForm",
6 | products: [
7 | .library(name: "URLEncodedForm", targets: ["URLEncodedForm"]),
8 | ],
9 | dependencies: [
10 | // 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging.
11 | .package(url: "https://github.com/vapor/core.git", from: "3.0.0"),
12 | ],
13 | targets: [
14 | .target(name: "URLEncodedForm", dependencies: ["Core"]),
15 | .testTarget(name: "URLEncodedFormTests", dependencies: ["URLEncodedForm"]),
16 | ]
17 | )
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Codable/URLEncodedFormDecoder.swift:
--------------------------------------------------------------------------------
1 | /// Decodes instances of `Decodable` types from `application/x-www-form-urlencoded` `Data`.
2 | ///
3 | /// print(data) // "name=Vapor&age=3"
4 | /// let user = try URLEncodedFormDecoder().decode(User.self, from: data)
5 | /// print(user) // User
6 | ///
7 | /// URL-encoded forms are commonly used by websites to send form data via POST requests. This encoding is relatively
8 | /// efficient for small amounts of data but must be percent-encoded. `multipart/form-data` is more efficient for sending
9 | /// large data blobs like files.
10 | ///
11 | /// See [Mozilla's](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) docs for more information about
12 | /// url-encoded forms.
13 | public final class URLEncodedFormDecoder: DataDecoder {
14 | /// The underlying `URLEncodedFormEncodedParser`
15 | private let parser: URLEncodedFormParser
16 |
17 | /// If `true`, empty values will be omitted. Empty values are URL-Encoded keys with no value following the `=` sign.
18 | ///
19 | /// name=Vapor&age=
20 | ///
21 | /// In the above example, `age` is an empty value.
22 | public var omitEmptyValues: Bool
23 |
24 | /// If `true`, flags will be omitted. Flags are URL-encoded keys with no following `=` sign.
25 | ///
26 | /// name=Vapor&isAdmin&age=3
27 | ///
28 | /// In the above example, `isAdmin` is a flag.
29 | public var omitFlags: Bool
30 |
31 | /// Create a new `URLEncodedFormDecoder`.
32 | ///
33 | /// - parameters:
34 | /// - omitEmptyValues: If `true`, empty values will be omitted.
35 | /// Empty values are URL-Encoded keys with no value following the `=` sign.
36 | /// - omitFlags: If `true`, flags will be omitted.
37 | /// Flags are URL-encoded keys with no following `=` sign.
38 | public init(omitEmptyValues: Bool = false, omitFlags: Bool = false) {
39 | self.parser = URLEncodedFormParser()
40 | self.omitFlags = omitFlags
41 | self.omitEmptyValues = omitEmptyValues
42 | }
43 |
44 | /// Decodes an instance of the supplied `Decodable` type from `Data`.
45 | ///
46 | /// print(data) // "name=Vapor&age=3"
47 | /// let user = try URLEncodedFormDecoder().decode(User.self, from: data)
48 | /// print(user) // User
49 | ///
50 | /// - parameters:
51 | /// - decodable: Generic `Decodable` type (`D`) to decode.
52 | /// - from: `Data` to decode a `D` from.
53 | /// - returns: An instance of the `Decodable` type (`D`).
54 | /// - throws: Any error that may occur while attempting to decode the specified type.
55 | public func decode(_ decodable: D.Type, from data: Data) throws -> D where D : Decodable {
56 | let urlEncodedFormData = try self.parser.parse(percentEncoded: String(data: data, encoding: .utf8) ?? "", omitEmptyValues: self.omitEmptyValues, omitFlags: self.omitFlags)
57 | let decoder = _URLEncodedFormDecoder(context: .init(.dict(urlEncodedFormData)), codingPath: [])
58 | return try D(from: decoder)
59 | }
60 | }
61 |
62 | // MARK: Private
63 |
64 | /// Private `Decoder`. See `URLEncodedFormDecoder` for public decoder.
65 | private final class _URLEncodedFormDecoder: Decoder {
66 | /// See `Decoder`
67 | let codingPath: [CodingKey]
68 |
69 | /// See `Decoder`
70 | var userInfo: [CodingUserInfoKey: Any] {
71 | return [:]
72 | }
73 |
74 | /// The data being decoded
75 | let context: URLEncodedFormDataContext
76 |
77 | /// Creates a new `_URLEncodedFormDecoder`.
78 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
79 | self.context = context
80 | self.codingPath = codingPath
81 | }
82 |
83 | /// See `Decoder`
84 | func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer
85 | where Key: CodingKey
86 | {
87 | return .init(_URLEncodedFormKeyedDecoder(context: context, codingPath: codingPath))
88 | }
89 |
90 | /// See `Decoder`
91 | func unkeyedContainer() throws -> UnkeyedDecodingContainer {
92 | return _URLEncodedFormUnkeyedDecoder(context: context, codingPath: codingPath)
93 | }
94 |
95 | /// See `Decoder`
96 | func singleValueContainer() throws -> SingleValueDecodingContainer {
97 | return _URLEncodedFormSingleValueDecoder(context: context, codingPath: codingPath)
98 | }
99 | }
100 |
101 | /// Private `SingleValueDecodingContainer`.
102 | private final class _URLEncodedFormSingleValueDecoder: SingleValueDecodingContainer {
103 | /// The data being decoded
104 | let context: URLEncodedFormDataContext
105 |
106 | /// See `SingleValueDecodingContainer`
107 | var codingPath: [CodingKey]
108 |
109 | /// Creates a new `_URLEncodedFormSingleValueDecoder`.
110 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
111 | self.context = context
112 | self.codingPath = codingPath
113 | }
114 |
115 | /// See `SingleValueDecodingContainer`
116 | func decodeNil() -> Bool {
117 | return context.data.get(at: codingPath) == nil
118 | }
119 |
120 | /// See `SingleValueDecodingContainer`
121 | func decode(_ type: T.Type) throws -> T where T: Decodable {
122 | guard let data = context.data.get(at: codingPath) else {
123 | throw DecodingError.valueNotFound(T.self, at: codingPath)
124 | }
125 | if let convertible = T.self as? URLEncodedFormDataConvertible.Type {
126 | return try convertible.convertFromURLEncodedFormData(data) as! T
127 | } else {
128 | let decoder = _URLEncodedFormDecoder(context: context, codingPath: codingPath)
129 | return try T.init(from: decoder)
130 | }
131 | }
132 | }
133 |
134 | /// Private `KeyedDecodingContainerProtocol`.
135 | private final class _URLEncodedFormKeyedDecoder: KeyedDecodingContainerProtocol where K: CodingKey {
136 | /// See `KeyedDecodingContainerProtocol.`
137 | typealias Key = K
138 |
139 | /// The data being decoded
140 | let context: URLEncodedFormDataContext
141 |
142 | /// See `KeyedDecodingContainerProtocol.`
143 | var codingPath: [CodingKey]
144 |
145 | /// See `KeyedDecodingContainerProtocol.`
146 | var allKeys: [K] {
147 | guard let dictionary = context.data.get(at: codingPath)?.dictionary else {
148 | return []
149 | }
150 | return dictionary.keys.compactMap { K(stringValue: $0) }
151 | }
152 |
153 | /// Create a new `_URLEncodedFormKeyedDecoder`
154 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
155 | self.context = context
156 | self.codingPath = codingPath
157 | }
158 |
159 | /// See `KeyedDecodingContainerProtocol.`
160 | func contains(_ key: K) -> Bool {
161 | return context.data.get(at: codingPath)?.dictionary?[key.stringValue] != nil
162 | }
163 |
164 | /// See `KeyedDecodingContainerProtocol.`
165 | func decodeNil(forKey key: K) throws -> Bool {
166 | return context.data.get(at: codingPath + [key]) == nil
167 | }
168 |
169 | /// See `KeyedDecodingContainerProtocol.`
170 | func decode(_ type: T.Type, forKey key: K) throws -> T where T: Decodable {
171 | if let convertible = T.self as? URLEncodedFormDataConvertible.Type {
172 | guard let data = context.data.get(at: codingPath + [key]) else {
173 | throw DecodingError.valueNotFound(T.self, at: codingPath + [key])
174 | }
175 | return try convertible.convertFromURLEncodedFormData(data) as! T
176 | } else {
177 | let decoder = _URLEncodedFormDecoder(context: context, codingPath: codingPath + [key])
178 | return try T(from: decoder)
179 | }
180 | }
181 |
182 | /// See `KeyedDecodingContainerProtocol.`
183 | func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer
184 | where NestedKey: CodingKey
185 | {
186 | return .init(_URLEncodedFormKeyedDecoder(context: context, codingPath: codingPath + [key]))
187 | }
188 |
189 | /// See `KeyedDecodingContainerProtocol.`
190 | func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer {
191 | return _URLEncodedFormUnkeyedDecoder(context: context, codingPath: codingPath + [key])
192 | }
193 |
194 | /// See `KeyedDecodingContainerProtocol.`
195 | func superDecoder() throws -> Decoder {
196 | return _URLEncodedFormDecoder(context: context, codingPath: codingPath)
197 | }
198 |
199 | /// See `KeyedDecodingContainerProtocol.`
200 | func superDecoder(forKey key: K) throws -> Decoder {
201 | return _URLEncodedFormDecoder(context: context, codingPath: codingPath + [key])
202 | }
203 | }
204 |
205 | /// Private `UnkeyedDecodingContainer`.
206 | private final class _URLEncodedFormUnkeyedDecoder: UnkeyedDecodingContainer {
207 | /// The data being decoded
208 | let context: URLEncodedFormDataContext
209 |
210 | /// See `UnkeyedDecodingContainer`.
211 | var codingPath: [CodingKey]
212 |
213 | /// See `UnkeyedDecodingContainer`.
214 | var count: Int? {
215 | guard let array = context.data.get(at: codingPath)?.array else {
216 | return nil
217 | }
218 | return array.count
219 | }
220 |
221 | /// See `UnkeyedDecodingContainer`.
222 | var isAtEnd: Bool {
223 | guard let count = self.count else {
224 | return true
225 | }
226 | return currentIndex >= count
227 | }
228 |
229 | /// See `UnkeyedDecodingContainer`.
230 | var currentIndex: Int
231 |
232 | /// Converts the current index to a coding key
233 | var index: CodingKey {
234 | return BasicKey(currentIndex)
235 | }
236 |
237 | /// Create a new `_URLEncodedFormUnkeyedDecoder`
238 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
239 | self.context = context
240 | self.codingPath = codingPath
241 | currentIndex = 0
242 | }
243 |
244 | /// See `UnkeyedDecodingContainer`.
245 | func decodeNil() throws -> Bool {
246 | return context.data.get(at: codingPath + [index]) == nil
247 | }
248 |
249 | /// See `UnkeyedDecodingContainer`.
250 | func decode(_ type: T.Type) throws -> T where T: Decodable {
251 | defer { currentIndex += 1 }
252 | if let convertible = T.self as? URLEncodedFormDataConvertible.Type {
253 | guard let data = context.data.get(at: codingPath + [index]) else {
254 | throw DecodingError.valueNotFound(T.self, at: codingPath + [index])
255 | }
256 | return try convertible.convertFromURLEncodedFormData(data) as! T
257 | } else {
258 | let decoder = _URLEncodedFormDecoder(context: context, codingPath: codingPath + [index])
259 | return try T(from: decoder)
260 | }
261 | }
262 |
263 | /// See `UnkeyedDecodingContainer`.
264 | func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer
265 | where NestedKey: CodingKey
266 | {
267 | return .init(_URLEncodedFormKeyedDecoder(context: context, codingPath: codingPath + [index]))
268 | }
269 |
270 | /// See `UnkeyedDecodingContainer`.
271 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
272 | return _URLEncodedFormUnkeyedDecoder(context: context, codingPath: codingPath + [index])
273 | }
274 |
275 | /// See `UnkeyedDecodingContainer`.
276 | func superDecoder() throws -> Decoder {
277 | defer { currentIndex += 1 }
278 | return _URLEncodedFormDecoder(context: context, codingPath: codingPath + [index])
279 | }
280 |
281 | }
282 |
283 |
284 | // MARK: Utils
285 |
286 | private extension DecodingError {
287 | static func typeMismatch(_ type: Any.Type, at path: [CodingKey]) -> DecodingError {
288 | let pathString = path.map { $0.stringValue }.joined(separator: ".")
289 | let context = DecodingError.Context(
290 | codingPath: path,
291 | debugDescription: "No \(type) was found at path \(pathString)"
292 | )
293 | return Swift.DecodingError.typeMismatch(type, context)
294 | }
295 |
296 | static func valueNotFound(_ type: Any.Type, at path: [CodingKey]) -> DecodingError {
297 | let pathString = path.map { $0.stringValue }.joined(separator: ".")
298 | let context = DecodingError.Context(
299 | codingPath: path,
300 | debugDescription: "No \(type) was found at path \(pathString)"
301 | )
302 | return Swift.DecodingError.valueNotFound(type, context)
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Codable/URLEncodedFormEncoder.swift:
--------------------------------------------------------------------------------
1 | /// Encodes `Encodable` instances to `application/x-www-form-urlencoded` data.
2 | ///
3 | /// print(user) /// User
4 | /// let data = try URLEncodedFormEncoder().encode(user)
5 | /// print(data) /// Data
6 | ///
7 | /// URL-encoded forms are commonly used by websites to send form data via POST requests. This encoding is relatively
8 | /// efficient for small amounts of data but must be percent-encoded. `multipart/form-data` is more efficient for sending
9 | /// large data blobs like files.
10 | ///
11 | /// See [Mozilla's](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) docs for more information about
12 | /// url-encoded forms.
13 | public final class URLEncodedFormEncoder: DataEncoder {
14 | /// Create a new `URLEncodedFormEncoder`.
15 | public init() {}
16 |
17 | /// Encodes the supplied `Encodable` object to `Data`.
18 | ///
19 | /// print(user) // User
20 | /// let data = try URLEncodedFormEncoder().encode(user)
21 | /// print(data) // "name=Vapor&age=3"
22 | ///
23 | /// - parameters:
24 | /// - encodable: Generic `Encodable` object (`E`) to encode.
25 | /// - returns: Encoded `Data`
26 | /// - throws: Any error that may occur while attempting to encode the specified type.
27 | public func encode(_ encodable: E) throws -> Data where E: Encodable {
28 | let context = URLEncodedFormDataContext(.dict([:]))
29 | let encoder = _URLEncodedFormEncoder(context: context, codingPath: [])
30 | try encodable.encode(to: encoder)
31 | let serializer = URLEncodedFormSerializer()
32 | guard case .dict(let dict) = context.data else {
33 | throw URLEncodedFormError(
34 | identifier: "invalidTopLevel",
35 | reason: "form-urlencoded requires a top level dictionary"
36 | )
37 | }
38 | return try serializer.serialize(dict)
39 | }
40 | }
41 |
42 | /// MARK: Private
43 |
44 | /// Private `Encoder`.
45 | private final class _URLEncodedFormEncoder: Encoder {
46 | /// See `Encoder`
47 | var userInfo: [CodingUserInfoKey: Any] {
48 | return [:]
49 | }
50 |
51 | /// See `Encoder`
52 | let codingPath: [CodingKey]
53 |
54 | /// The data being decoded
55 | var context: URLEncodedFormDataContext
56 |
57 | /// Creates a new form url-encoded encoder
58 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
59 | self.context = context
60 | self.codingPath = codingPath
61 | }
62 |
63 | /// See `Encoder`
64 | func container(keyedBy type: Key.Type) -> KeyedEncodingContainer
65 | where Key: CodingKey
66 | {
67 | let container = _URLEncodedFormKeyedEncoder(context: context, codingPath: codingPath)
68 | return .init(container)
69 | }
70 |
71 | /// See `Encoder`
72 | func unkeyedContainer() -> UnkeyedEncodingContainer {
73 | return _URLEncodedFormUnkeyedEncoder(context: context, codingPath: codingPath)
74 | }
75 |
76 | /// See `Encoder`
77 | func singleValueContainer() -> SingleValueEncodingContainer {
78 | return _URLEncodedFormSingleValueEncoder(context: context, codingPath: codingPath)
79 | }
80 | }
81 |
82 | /// Private `SingleValueEncodingContainer`.
83 | private final class _URLEncodedFormSingleValueEncoder: SingleValueEncodingContainer {
84 | /// See `SingleValueEncodingContainer`
85 | var codingPath: [CodingKey]
86 |
87 | /// The data being encoded
88 | let context: URLEncodedFormDataContext
89 |
90 | /// Creates a new single value encoder
91 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
92 | self.context = context
93 | self.codingPath = codingPath
94 | }
95 |
96 | /// See `SingleValueEncodingContainer`
97 | func encodeNil() throws {
98 | // skip
99 | }
100 |
101 | /// See `SingleValueEncodingContainer`
102 | func encode(_ value: T) throws where T: Encodable {
103 | if let convertible = value as? URLEncodedFormDataConvertible {
104 | try context.data.set(to: convertible.convertToURLEncodedFormData(), at: codingPath)
105 | } else {
106 | let encoder = _URLEncodedFormEncoder(context: context, codingPath: codingPath)
107 | try value.encode(to: encoder)
108 | }
109 | }
110 | }
111 |
112 |
113 | /// Private `KeyedEncodingContainerProtocol`.
114 | private final class _URLEncodedFormKeyedEncoder: KeyedEncodingContainerProtocol where K: CodingKey {
115 | /// See `KeyedEncodingContainerProtocol`
116 | typealias Key = K
117 |
118 | /// See `KeyedEncodingContainerProtocol`
119 | var codingPath: [CodingKey]
120 |
121 | /// The data being encoded
122 | let context: URLEncodedFormDataContext
123 |
124 | /// Creates a new `_URLEncodedFormKeyedEncoder`.
125 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
126 | self.context = context
127 | self.codingPath = codingPath
128 | }
129 |
130 | /// See `KeyedEncodingContainerProtocol`
131 | func encodeNil(forKey key: K) throws {
132 | // skip
133 | }
134 |
135 | /// See `KeyedEncodingContainerProtocol`
136 | func encode(_ value: T, forKey key: K) throws where T : Encodable {
137 | if let convertible = value as? URLEncodedFormDataConvertible {
138 | try context.data.set(to: convertible.convertToURLEncodedFormData(), at: codingPath + [key])
139 | } else {
140 | let encoder = _URLEncodedFormEncoder(context: context, codingPath: codingPath + [key])
141 | try value.encode(to: encoder)
142 | }
143 | }
144 |
145 | /// See `KeyedEncodingContainerProtocol`
146 | func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer
147 | where NestedKey: CodingKey
148 | {
149 | return .init(_URLEncodedFormKeyedEncoder(context: context, codingPath: codingPath + [key]))
150 | }
151 |
152 | /// See `KeyedEncodingContainerProtocol`
153 | func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer {
154 | return _URLEncodedFormUnkeyedEncoder(context: context, codingPath: codingPath + [key])
155 | }
156 |
157 | /// See `KeyedEncodingContainerProtocol`
158 | func superEncoder() -> Encoder {
159 | return _URLEncodedFormEncoder(context: context, codingPath: codingPath)
160 | }
161 |
162 | /// See `KeyedEncodingContainerProtocol`
163 | func superEncoder(forKey key: K) -> Encoder {
164 | return _URLEncodedFormEncoder(context: context, codingPath: codingPath + [key])
165 | }
166 |
167 | }
168 |
169 | /// Private `UnkeyedEncodingContainer`.
170 | private final class _URLEncodedFormUnkeyedEncoder: UnkeyedEncodingContainer {
171 | /// See `UnkeyedEncodingContainer`.
172 | var codingPath: [CodingKey]
173 |
174 | /// See `UnkeyedEncodingContainer`.
175 | var count: Int
176 |
177 | /// The data being encoded
178 | let context: URLEncodedFormDataContext
179 |
180 | /// Converts the current count to a coding key
181 | var index: CodingKey {
182 | return BasicKey(count)
183 | }
184 |
185 | /// Creates a new `_URLEncodedFormUnkeyedEncoder`.
186 | init(context: URLEncodedFormDataContext, codingPath: [CodingKey]) {
187 | self.context = context
188 | self.codingPath = codingPath
189 | self.count = 0
190 | }
191 |
192 | /// See `UnkeyedEncodingContainer`.
193 | func encodeNil() throws {
194 | // skip
195 | }
196 |
197 | /// See UnkeyedEncodingContainer.encode
198 | func encode(_ value: T) throws where T: Encodable {
199 | defer { count += 1 }
200 | if let convertible = value as? URLEncodedFormDataConvertible {
201 | try context.data.set(to: convertible.convertToURLEncodedFormData(), at: codingPath + [index])
202 | } else {
203 | let encoder = _URLEncodedFormEncoder(context: context, codingPath: codingPath + [index])
204 | try value.encode(to: encoder)
205 | }
206 | }
207 |
208 | /// See UnkeyedEncodingContainer.nestedContainer
209 | func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer
210 | where NestedKey: CodingKey
211 | {
212 | defer { count += 1 }
213 | return .init(_URLEncodedFormKeyedEncoder(context: context, codingPath: codingPath + [index]))
214 | }
215 |
216 | /// See UnkeyedEncodingContainer.nestedUnkeyedContainer
217 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
218 | defer { count += 1 }
219 | return _URLEncodedFormUnkeyedEncoder(context: context, codingPath: codingPath + [index])
220 | }
221 |
222 | /// See UnkeyedEncodingContainer.superEncoder
223 | func superEncoder() -> Encoder {
224 | defer { count += 1 }
225 | return _URLEncodedFormEncoder(context: context, codingPath: codingPath + [index])
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Data/URLEncodedFormData.swift:
--------------------------------------------------------------------------------
1 | import Bits
2 |
3 | /// Represents application/x-www-form-urlencoded encoded data.
4 | enum URLEncodedFormData: NestedData, ExpressibleByArrayLiteral, ExpressibleByStringLiteral, ExpressibleByDictionaryLiteral, Equatable {
5 | /// See `NestedData`.
6 | static func dictionary(_ value: [String : URLEncodedFormData]) -> URLEncodedFormData {
7 | return .dict(value)
8 | }
9 |
10 | /// See `NestedData`.
11 | static func array(_ value: [URLEncodedFormData]) -> URLEncodedFormData {
12 | return .arr(value)
13 | }
14 |
15 | /// Stores a string, this is the root storage.
16 | case str(String)
17 |
18 | /// Stores a dictionary of self.
19 | case dict([String: URLEncodedFormData])
20 |
21 | /// Stores an array of self.
22 | case arr([URLEncodedFormData])
23 |
24 | // MARK: Polymorphic
25 |
26 | /// Converts self to an `String` or returns `nil` if not convertible.
27 | var string: String? {
28 | switch self {
29 | case .str(let s): return s
30 | default: return nil
31 | }
32 | }
33 |
34 | /// Converts self to an `URL` or returns `nil` if not convertible.
35 | var url: URL? {
36 | switch self {
37 | case .str(let s): return URL(string: s)
38 | default: return nil
39 | }
40 | }
41 |
42 | /// Converts self to an `[URLEncodedFormData]` or returns `nil` if not convertible.
43 | var array: [URLEncodedFormData]? {
44 | switch self {
45 | case .arr(let arr): return arr
46 | default: return nil
47 | }
48 | }
49 |
50 | /// Converts self to an `[String: URLEncodedFormData]` or returns `nil` if not convertible.
51 | var dictionary: [String: URLEncodedFormData]? {
52 | switch self {
53 | case .dict(let dict): return dict
54 | default: return nil
55 | }
56 | }
57 |
58 | // MARK: Literal
59 |
60 | /// See `ExpressibleByArrayLiteral`.
61 | init(arrayLiteral elements: URLEncodedFormData...) {
62 | self = .arr(elements)
63 | }
64 |
65 | /// See `ExpressibleByStringLiteral`.
66 | init(stringLiteral value: String) {
67 | self = .str(value)
68 | }
69 |
70 | /// See `ExpressibleByDictionaryLiteral`.
71 | init(dictionaryLiteral elements: (String, URLEncodedFormData)...) {
72 | var dict: [String: URLEncodedFormData] = [:]
73 | elements.forEach { dict[$0.0] = $0.1 }
74 | self = .dict(dict)
75 | }
76 | }
77 |
78 | /// Reference type wrapper around `URLEncodedFormData`.
79 | final class URLEncodedFormDataContext {
80 | /// The wrapped data.
81 | var data: URLEncodedFormData
82 |
83 | /// Creates a new `URLEncodedFormDataContext`.
84 | init(_ data: URLEncodedFormData) {
85 | self.data = data
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Data/URLEncodedFormDataConvertible.swift:
--------------------------------------------------------------------------------
1 | /// Capable of converting to / from `URLEncodedFormData`.
2 | protocol URLEncodedFormDataConvertible {
3 | /// Converts self to `URLEncodedFormData`.
4 | func convertToURLEncodedFormData() throws -> URLEncodedFormData
5 |
6 | /// Converts `URLEncodedFormData` to self.
7 | static func convertFromURLEncodedFormData(_ data: URLEncodedFormData) throws -> Self
8 | }
9 |
10 | extension String: URLEncodedFormDataConvertible {
11 | /// See `URLEncodedFormDataConvertible`.
12 | func convertToURLEncodedFormData() throws -> URLEncodedFormData {
13 | return .str(self)
14 | }
15 |
16 | /// See `URLEncodedFormDataConvertible`.
17 | static func convertFromURLEncodedFormData(_ data: URLEncodedFormData) throws -> String {
18 | guard let string = data.string else {
19 | throw URLEncodedFormError(identifier: "string", reason: "Could not convert to `String`: \(data)")
20 | }
21 |
22 | return string
23 | }
24 | }
25 |
26 | extension URL: URLEncodedFormDataConvertible {
27 | /// See `URLEncodedFormDataConvertible`.
28 | func convertToURLEncodedFormData() throws -> URLEncodedFormData {
29 | return .str(self.absoluteString)
30 | }
31 |
32 | /// See `URLEncodedFormDataConvertible`.
33 | static func convertFromURLEncodedFormData(_ data: URLEncodedFormData) throws -> URL {
34 | guard let url = data.url else {
35 | throw URLEncodedFormError(identifier: "url", reason: "Could not convert to `URL`: \(data)")
36 | }
37 |
38 | return url
39 | }
40 | }
41 |
42 | extension FixedWidthInteger {
43 | /// See `URLEncodedFormDataConvertible`.
44 | func convertToURLEncodedFormData() throws -> URLEncodedFormData {
45 | return .str(description)
46 | }
47 |
48 | /// See `URLEncodedFormDataConvertible`.
49 | static func convertFromURLEncodedFormData(_ data: URLEncodedFormData) throws -> Self {
50 | guard let fwi = data.string.flatMap(Self.init) else {
51 | throw URLEncodedFormError(identifier: "fwi", reason: "Could not convert to `\(Self.self)`: \(data)")
52 | }
53 |
54 | return fwi
55 | }
56 | }
57 |
58 | extension Int: URLEncodedFormDataConvertible { }
59 | extension Int8: URLEncodedFormDataConvertible { }
60 | extension Int16: URLEncodedFormDataConvertible { }
61 | extension Int32: URLEncodedFormDataConvertible { }
62 | extension Int64: URLEncodedFormDataConvertible { }
63 | extension UInt: URLEncodedFormDataConvertible { }
64 | extension UInt8: URLEncodedFormDataConvertible { }
65 | extension UInt16: URLEncodedFormDataConvertible { }
66 | extension UInt32: URLEncodedFormDataConvertible { }
67 | extension UInt64: URLEncodedFormDataConvertible { }
68 |
69 | extension BinaryFloatingPoint {
70 | /// See `URLEncodedFormDataConvertible`.
71 | func convertToURLEncodedFormData() throws -> URLEncodedFormData {
72 | return .str("\(self)")
73 | }
74 |
75 | /// See `URLEncodedFormDataConvertible`.
76 | static func convertFromURLEncodedFormData(_ data: URLEncodedFormData) throws -> Self {
77 | guard let bfp = data.string.flatMap(Double.init).flatMap(Self.init) else {
78 | throw URLEncodedFormError(identifier: "bfp", reason: "Could not convert to `\(Self.self)`: \(data)")
79 | }
80 |
81 | return bfp
82 | }
83 | }
84 |
85 | extension Float: URLEncodedFormDataConvertible { }
86 | extension Double: URLEncodedFormDataConvertible { }
87 |
88 | extension Bool: URLEncodedFormDataConvertible {
89 | /// See `URLEncodedFormDataConvertible`.
90 | func convertToURLEncodedFormData() throws -> URLEncodedFormData {
91 | return .str(description)
92 | }
93 |
94 | /// See `URLEncodedFormDataConvertible`.
95 | static func convertFromURLEncodedFormData(_ data: URLEncodedFormData) throws -> Bool {
96 | guard let bool = data.string?.bool else {
97 | throw URLEncodedFormError(identifier: "bool", reason: "Could not convert to Bool: \(data)")
98 | }
99 | return bool
100 | }
101 | }
102 |
103 | extension Decimal: URLEncodedFormDataConvertible {
104 | /// See `URLEncodedFormDataConvertible`.
105 | func convertToURLEncodedFormData() throws -> URLEncodedFormData {
106 | return .str(description)
107 | }
108 |
109 | /// See `URLEncodedFormDataConvertible`.
110 | static func convertFromURLEncodedFormData(_ data: URLEncodedFormData) throws -> Decimal {
111 | guard let string = data.string, let d = Decimal(string: string) else {
112 | throw URLEncodedFormError(identifier: "decimal", reason: "Could not convert to Decimal: \(data)")
113 | }
114 |
115 | return d
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Data/URLEncodedFormParser.swift:
--------------------------------------------------------------------------------
1 | import Core
2 |
3 | /// Converts `Data` to `[String: URLEncodedFormData]`.
4 | final class URLEncodedFormParser {
5 | /// Default form url encoded parser.
6 | static let `default` = URLEncodedFormParser()
7 |
8 | /// Create a new form-urlencoded data parser.
9 | init() {}
10 |
11 | /// Parses the data.
12 | /// If empty values is false, `foo=` will resolve as `foo: true`
13 | /// instead of `foo: ""`
14 | func parse(percentEncoded: String, omitEmptyValues: Bool = false, omitFlags: Bool = false) throws -> [String: URLEncodedFormData] {
15 | let partiallyDecoded = percentEncoded.replacingOccurrences(of: "+", with: " ")
16 | return try parse(data: partiallyDecoded, omitEmptyValues: omitEmptyValues, omitFlags: omitFlags)
17 | }
18 |
19 | /// Parses the data.
20 | /// If empty values is false, `foo=` will resolve as `foo: true`
21 | /// instead of `foo: ""`
22 | func parse(data: LosslessDataConvertible, omitEmptyValues: Bool = false, omitFlags: Bool = false) throws -> [String: URLEncodedFormData] {
23 | var encoded: [String: URLEncodedFormData] = [:]
24 | let data = data.convertToData()
25 |
26 | for pair in data.split(separator: .ampersand) {
27 | let data: URLEncodedFormData
28 | let key: URLEncodedFormEncodedKey
29 |
30 | /// Allow empty subsequences
31 | /// value= => "value": ""
32 | /// value => "value": true
33 | let token = pair.split(
34 | separator: .equals,
35 | maxSplits: 1, // max 1, `foo=a=b` should be `"foo": "a=b"`
36 | omittingEmptySubsequences: false
37 | )
38 |
39 | guard let decodedKey = try token.first?.utf8DecodedString().removingPercentEncoding else {
40 | throw URLEncodedFormError(
41 | identifier: "percentDecoding",
42 | reason: "Could not percent decode string key: \(token[0])"
43 | )
44 | }
45 | let decodedValue = try token.last?.utf8DecodedString().removingPercentEncoding
46 |
47 | if token.count == 2 {
48 | if omitEmptyValues && token[1].count == 0 {
49 | continue
50 | }
51 | guard let decodedValue = decodedValue else {
52 | throw URLEncodedFormError(identifier: "percentDecoding", reason: "Could not percent decode string value: \(token[1])")
53 | }
54 | key = try parseKey(data: decodedKey)
55 | data = .str(decodedValue)
56 | } else if token.count == 1 {
57 | if omitFlags {
58 | continue
59 | }
60 | key = try parseKey(data: decodedKey)
61 | data = "true"
62 | } else {
63 | throw URLEncodedFormError(
64 | identifier: "malformedData",
65 | reason: "Malformed form-urlencoded data encountered"
66 | )
67 | }
68 |
69 | let resolved: URLEncodedFormData
70 |
71 | if !key.subKeys.isEmpty {
72 | var current = encoded[key.string] ?? .dictionary([:])
73 | self.set(¤t, to: data, at: key.subKeys)
74 | resolved = current
75 | } else {
76 | resolved = data
77 | }
78 |
79 | encoded[key.string] = resolved
80 | }
81 |
82 | return encoded
83 | }
84 |
85 | /// Parses a `URLEncodedFormEncodedKey` from `Data`.
86 | private func parseKey(data dataConvertible: LosslessDataConvertible) throws -> URLEncodedFormEncodedKey {
87 | let data = dataConvertible.convertToData()
88 | let stringData: Data
89 | let subKeys: [URLEncodedFormEncodedSubKey]
90 |
91 | // check if the key has `key[]` or `key[5]`
92 | if data.contains(.rightSquareBracket) && data.contains(.leftSquareBracket) {
93 | // split on the `[`
94 | // a[b][c][d][hello] => a, b], c], d], hello]
95 | let slices = data.split(separator: .leftSquareBracket)
96 |
97 | guard slices.count > 0 else {
98 | throw URLEncodedFormError(identifier: "malformedKey", reason: "Malformed form-urlencoded key encountered.")
99 | }
100 | stringData = Data(slices[0])
101 | subKeys = try slices[1...]
102 | .map { Data($0) }
103 | .map { data -> URLEncodedFormEncodedSubKey in
104 | if data[0] == .rightSquareBracket {
105 | return .array
106 | } else {
107 | return try .dictionary(data.dropLast().utf8DecodedString())
108 | }
109 | }
110 | } else {
111 | stringData = data
112 | subKeys = []
113 | }
114 |
115 | return try URLEncodedFormEncodedKey(
116 | string: stringData.utf8DecodedString(),
117 | subKeys: subKeys
118 | )
119 | }
120 |
121 | /// Sets mutable form-urlencoded input to a value at the given `[URLEncodedFormEncodedSubKey]` path.
122 | private func set(_ base: inout URLEncodedFormData, to data: URLEncodedFormData, at path: [URLEncodedFormEncodedSubKey]) {
123 | guard path.count >= 1 else {
124 | base = data
125 | return
126 | }
127 |
128 | let first = path[0]
129 |
130 | var child: URLEncodedFormData
131 | switch path.count {
132 | case 1:
133 | child = data
134 | case 2...:
135 | switch first {
136 | case .array:
137 | /// always append to the last element of the array
138 | child = base.array?.last ?? .array([])
139 | set(&child, to: data, at: Array(path[1...]))
140 | case .dictionary(let key):
141 | child = base.dictionary?[key] ?? .dictionary([:])
142 | set(&child, to: data, at: Array(path[1...]))
143 | }
144 | default: fatalError()
145 | }
146 |
147 | switch first {
148 | case .array:
149 | if case .arr(var arr) = base {
150 | /// always append
151 | arr.append(child)
152 | base = .array(arr)
153 | } else {
154 | base = .array([child])
155 | }
156 | case .dictionary(let key):
157 | if case .dict(var dict) = base {
158 | dict[key] = child
159 | base = .dictionary(dict)
160 | } else {
161 | base = .dictionary([key: child])
162 | }
163 | }
164 | }
165 | }
166 |
167 | // MARK: Key
168 |
169 | /// Represents a key in a URLEncodedForm.
170 | private struct URLEncodedFormEncodedKey {
171 | let string: String
172 | let subKeys: [URLEncodedFormEncodedSubKey]
173 | }
174 |
175 | /// Available subkeys.
176 | private enum URLEncodedFormEncodedSubKey {
177 | case array
178 | case dictionary(String)
179 | }
180 |
181 | // MARK: Utilities
182 |
183 | private extension Data {
184 | /// UTF8 decodes a Stirng or throws an error.
185 | func utf8DecodedString() throws -> String {
186 | guard let string = String(data: self, encoding: .utf8) else {
187 | throw URLEncodedFormError(identifier: "utf8Decoding", reason: "Failed to utf8 decode string: \(self)")
188 | }
189 |
190 | return string
191 | }
192 | }
193 |
194 | private extension Data {
195 | /// Percent decodes a String or throws an error.
196 | func percentDecodedString() throws -> String {
197 | let utf8 = try utf8DecodedString()
198 |
199 | guard let decoded = utf8.replacingOccurrences(of: "+", with: " ").removingPercentEncoding else {
200 | throw URLEncodedFormError(
201 | identifier: "percentDecoding",
202 | reason: "Failed to percent decode string: \(self)"
203 | )
204 | }
205 |
206 | return decoded
207 | }
208 | }
209 |
210 | fileprivate extension Array {
211 | /// Accesses an array index or returns `nil` if the array isn't long enough.
212 | subscript(safe index: Int) -> Element? {
213 | guard index < count else {
214 | return nil
215 | }
216 | return self[index]
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Data/URLEncodedFormSerializer.swift:
--------------------------------------------------------------------------------
1 | import Bits
2 |
3 | /// Converts `[String: URLEncodedFormData]` structs to `Data`.
4 | final class URLEncodedFormSerializer {
5 | /// Default form url encoded serializer.
6 | static let `default` = URLEncodedFormSerializer()
7 |
8 | /// Create a new form-urlencoded data serializer.
9 | init() {}
10 |
11 | /// Serializes the data.
12 | func serialize(_ URLEncodedFormEncoded: [String: URLEncodedFormData]) throws -> Data {
13 | var data: [Data] = []
14 | for (key, val) in URLEncodedFormEncoded {
15 | let key = try key.urlEncodedFormEncoded()
16 | let subdata = try serialize(val, forKey: key)
17 | data.append(subdata)
18 | }
19 | return data.joinedWithAmpersands()
20 | }
21 |
22 | /// Serializes a `URLEncodedFormData` at a given key.
23 | private func serialize(_ data: URLEncodedFormData, forKey key: Data) throws -> Data {
24 | let encoded: Data
25 | switch data {
26 | case .arr(let subArray): encoded = try serialize(subArray, forKey: key)
27 | case .dict(let subDict): encoded = try serialize(subDict, forKey: key)
28 | case .str(let string): encoded = try key + [.equals] + string.urlEncodedFormEncoded()
29 | }
30 | return encoded
31 | }
32 |
33 | /// Serializes a `[String: URLEncodedFormData]` at a given key.
34 | private func serialize(_ dictionary: [String: URLEncodedFormData], forKey key: Data) throws -> Data {
35 | let values = try dictionary.map { subKey, value -> Data in
36 | let keyPath = try [.leftSquareBracket] + subKey.urlEncodedFormEncoded() + [.rightSquareBracket]
37 | return try serialize(value, forKey: key + keyPath)
38 | }
39 | return values.joinedWithAmpersands()
40 | }
41 |
42 | /// Serializes a `[URLEncodedFormData]` at a given key.
43 | private func serialize(_ array: [URLEncodedFormData], forKey key: Data) throws -> Data {
44 | let collection = try array.map { value -> Data in
45 | let keyPath = key + [.leftSquareBracket, .rightSquareBracket]
46 | return try serialize(value, forKey: keyPath)
47 | }
48 |
49 | return collection.joinedWithAmpersands()
50 | }
51 | }
52 |
53 | // MARK: Utilties
54 |
55 | private extension Array where Element == Data {
56 | /// Joins an array of `Data` with ampersands.
57 | func joinedWithAmpersands() -> Data {
58 | return Data(self.joined(separator: [.ampersand]))
59 | }
60 | }
61 |
62 | private extension String {
63 | /// Prepares a `String` for inclusion in form-urlencoded data.
64 | func urlEncodedFormEncoded() throws -> Data {
65 | guard let string = self.addingPercentEncoding(withAllowedCharacters: _allowedCharacters) else {
66 | throw URLEncodedFormError(identifier: "percentEncoding", reason: "Failed to percent encode string: \(self)")
67 | }
68 |
69 | guard let encoded = string.data(using: .utf8) else {
70 | throw URLEncodedFormError(identifier: "utf8Encoding", reason: "Failed to utf8 encode string: \(self)")
71 | }
72 |
73 | return encoded
74 | }
75 | }
76 |
77 | /// Characters allowed in form-urlencoded data.
78 | private var _allowedCharacters: CharacterSet = {
79 | var allowed = CharacterSet.urlQueryAllowed
80 | allowed.remove(charactersIn: "?&=[];+")
81 | return allowed
82 | }()
83 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Utilities/Exports.swift:
--------------------------------------------------------------------------------
1 | @_exported import Core
2 |
--------------------------------------------------------------------------------
/Sources/URLEncodedForm/Utilities/URLEncodedFormError.swift:
--------------------------------------------------------------------------------
1 | import Debugging
2 |
3 | /// Errors thrown while encoding/decoding `application/x-www-form-urlencoded` data.
4 | public struct URLEncodedFormError: Error, Debuggable {
5 | /// See Debuggable.identifier
6 | public let identifier: String
7 |
8 | /// See Debuggable.reason
9 | public let reason: String
10 |
11 | /// Creates a new `URLEncodedFormError`.
12 | public init(identifier: String, reason: String) {
13 | self.identifier = identifier
14 | self.reason = reason
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | #if os(Linux)
2 |
3 | import XCTest
4 | @testable import URLEncodedFormTests
5 | XCTMain([
6 | testCase(URLEncodedFormCodableTests.allTests),
7 | testCase(URLEncodedFormParserTests.allTests),
8 | testCase(URLEncodedFormSerializerTests.allTests),
9 | ])
10 |
11 | #endif
--------------------------------------------------------------------------------
/Tests/URLEncodedFormTests/URLEncodedFormCodableTests.swift:
--------------------------------------------------------------------------------
1 | import URLEncodedForm
2 | import XCTest
3 |
4 | class URLEncodedFormCodableTests: XCTestCase {
5 | func testDecode() throws {
6 | let data = """
7 | name=Tanner&age=23&pets[]=Zizek&pets[]=Foo&dict[a]=1&dict[b]=2&foos[]=baz&nums[]=3.14&url=https%3A%2F%2Fvapor.codes
8 | """.data(using: .utf8)!
9 |
10 | let user = try URLEncodedFormDecoder().decode(User.self, from: data)
11 | XCTAssertEqual(user.name, "Tanner")
12 | XCTAssertEqual(user.age, 23)
13 | XCTAssertEqual(user.pets.count, 2)
14 | XCTAssertEqual(user.pets.first, "Zizek")
15 | XCTAssertEqual(user.pets.last, "Foo")
16 | XCTAssertEqual(user.dict["a"], 1)
17 | XCTAssertEqual(user.dict["b"], 2)
18 | XCTAssertEqual(user.foos[0], .baz)
19 | XCTAssertEqual(user.nums[0], 3.14)
20 | XCTAssertEqual(user.url, URL(string: "https://vapor.codes"))
21 | }
22 |
23 | func testEncode() throws {
24 | let user = User(name: "Tanner", age: 23, pets: ["Zizek", "Foo"], dict: ["a": 1, "b": 2], foos: [.baz], nums: [3.14], url: URL(string: "https://vapor.codes")!)
25 | let data = try URLEncodedFormEncoder().encode(user)
26 | let result = String(data: data, encoding: .utf8)!
27 | XCTAssert(result.contains("pets[]=Zizek"))
28 | XCTAssert(result.contains("pets[]=Foo"))
29 | XCTAssert(result.contains("age=23"))
30 | XCTAssert(result.contains("name=Tanner"))
31 | XCTAssert(result.contains("dict[a]=1"))
32 | XCTAssert(result.contains("dict[b]=2"))
33 | XCTAssert(result.contains("foos[]=baz"))
34 | XCTAssert(result.contains("nums[]=3.14"))
35 | XCTAssert(result.contains("url=https://vapor.codes"))
36 | }
37 |
38 | func testCodable() throws {
39 | let a = User(name: "Tanner", age: 23, pets: ["Zizek", "Foo"], dict: ["a": 1, "b": 2], foos: [], nums: [], url: URL(string: "https://vapor.codes")!)
40 | let body = try URLEncodedFormEncoder().encode(a)
41 | print(String(data: body, encoding: .utf8)!)
42 | let b = try URLEncodedFormDecoder().decode(User.self, from: body)
43 | XCTAssertEqual(a, b)
44 | }
45 |
46 | func testDecodeIntArray() throws {
47 | let data = """
48 | array[]=1&array[]=2&array[]=3
49 | """.data(using: .utf8)!
50 |
51 | let content = try URLEncodedFormDecoder().decode([String: [Int]].self, from: data)
52 | XCTAssertEqual(content["array"], [1, 2, 3])
53 | }
54 |
55 | func testRawEnum() throws {
56 | enum PetType: String, Codable {
57 | case cat, dog
58 | }
59 | struct Pet: Codable {
60 | var name: String
61 | var type: PetType
62 | }
63 | let ziz = try URLEncodedFormDecoder().decode(Pet.self, from: "name=Ziz&type=cat")
64 | XCTAssertEqual(ziz.name, "Ziz")
65 | XCTAssertEqual(ziz.type, .cat)
66 | let data = try URLEncodedFormEncoder().encode(ziz)
67 | let string = String(data: data, encoding: .ascii)
68 | XCTAssertEqual(string?.contains("name=Ziz"), true)
69 | XCTAssertEqual(string?.contains("type=cat"), true)
70 | }
71 |
72 | /// https://github.com/vapor/url-encoded-form/issues/3
73 | func testGH3() throws {
74 | struct Foo: Codable {
75 | var flag: Bool
76 | }
77 | let foo = try URLEncodedFormDecoder().decode(Foo.self, from: "flag=1")
78 | XCTAssertEqual(foo.flag, true)
79 | }
80 |
81 | /// https://github.com/vapor/url-encoded-form/issues/3
82 | func testEncodeReserved() throws {
83 | struct Foo: Codable {
84 | var reserved: String
85 | }
86 | let foo = Foo(reserved: "?&=[];+")
87 | let data = try URLEncodedFormEncoder().encode(foo)
88 | XCTAssertEqual(String(decoding: data, as: UTF8.self), "reserved=%3F%26%3D%5B%5D%3B%2B")
89 | }
90 |
91 | static let allTests = [
92 | ("testDecode", testDecode),
93 | ("testEncode", testEncode),
94 | ("testCodable", testCodable),
95 | ("testDecodeIntArray", testDecodeIntArray),
96 | ("testRawEnum", testRawEnum),
97 | ("testGH3", testGH3),
98 | ("testEncodeReserved", testEncodeReserved),
99 | ]
100 | }
101 |
102 | struct User: Codable, Equatable {
103 | static func ==(lhs: User, rhs: User) -> Bool {
104 | return lhs.name == rhs.name
105 | && lhs.age == rhs.age
106 | && lhs.pets == rhs.pets
107 | && lhs.dict == rhs.dict
108 | }
109 |
110 | var name: String
111 | var age: Int
112 | var pets: [String]
113 | var dict: [String: Int]
114 | var foos: [Foo]
115 | var nums: [Decimal]
116 | var url: URL
117 | }
118 |
119 | enum Foo: String, Codable {
120 | case foo, bar, baz
121 | }
122 |
--------------------------------------------------------------------------------
/Tests/URLEncodedFormTests/URLEncodedFormParserTests.swift:
--------------------------------------------------------------------------------
1 | @testable import URLEncodedForm
2 | import XCTest
3 |
4 | class URLEncodedFormParserTests: XCTestCase {
5 | func testBasic() throws {
6 | let data = "hello=world&foo=bar".data(using: .utf8)!
7 | let form = try URLEncodedFormParser.default.parse(data: data)
8 | XCTAssertEqual(form, ["hello": "world", "foo": "bar"])
9 | }
10 |
11 | func testBasicWithAmpersand() throws {
12 | let data = "hello=world&foo=bar%26bar".data(using: .utf8)!
13 | let form = try URLEncodedFormParser.default.parse(data: data)
14 | XCTAssertEqual(form, ["hello": "world", "foo": "bar&bar"])
15 | }
16 |
17 | func testDictionary() throws {
18 | let data = "greeting[en]=hello&greeting[es]=hola".data(using: .utf8)!
19 | let form = try URLEncodedFormParser.default.parse(data: data)
20 | XCTAssertEqual(form, ["greeting": ["es": "hola", "en": "hello"]])
21 | }
22 |
23 | func testArray() throws {
24 | let data = "greetings[]=hello&greetings[]=hola".data(using: .utf8)!
25 | let form = try URLEncodedFormParser.default.parse(data: data)
26 | XCTAssertEqual(form, ["greetings": ["hello", "hola"]])
27 | }
28 |
29 | func testOptions() throws {
30 | let data = "hello=&foo".data(using: .utf8)!
31 | let normal = try! URLEncodedFormParser.default.parse(data: data)
32 | let noEmpty = try! URLEncodedFormParser.default.parse(data: data, omitEmptyValues: true)
33 | let noFlags = try! URLEncodedFormParser.default.parse(data: data, omitFlags: true)
34 |
35 | XCTAssertEqual(normal, ["hello": "", "foo": "true"])
36 | XCTAssertEqual(noEmpty, ["foo": "true"])
37 | XCTAssertEqual(noFlags, ["hello": ""])
38 | }
39 |
40 | func testPercentDecoding() throws {
41 | let data = "aaa%5D=%2Bbbb%20+ccc&d%5B%5D=1&d%5B%5D=2"
42 | let form = try URLEncodedFormParser.default.parse(percentEncoded: data)
43 | XCTAssertEqual(form, ["aaa]": "+bbb ccc", "d": ["1","2"]])
44 | }
45 |
46 | func testNestedParsing() throws {
47 | // a[][b]=c&a[][b]=c
48 | // [a:[[b:c],[b:c]]
49 | let data = "a[b][c][d][hello]=world".data(using: .utf8)!
50 | let form = try URLEncodedFormParser.default.parse(data: data)
51 | XCTAssertEqual(form, ["a": ["b": ["c": ["d": ["hello": "world"]]]]])
52 | }
53 |
54 | static let allTests = [
55 | ("testBasic", testBasic),
56 | ("testBasicWithAmpersand", testBasicWithAmpersand),
57 | ("testDictionary", testDictionary),
58 | ("testArray", testArray),
59 | ("testOptions", testOptions),
60 | ("testPercentDecoding", testPercentDecoding),
61 | ("testNestedParsing", testNestedParsing),
62 | ]
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/URLEncodedFormTests/URLEncodedFormSerializerTests.swift:
--------------------------------------------------------------------------------
1 | @testable import URLEncodedForm
2 | import XCTest
3 |
4 | class URLEncodedFormSerializerTests: XCTestCase {
5 | func testPercentEncoding() throws {
6 | let form: [String: URLEncodedFormData] = ["aaa]": "+bbb ccc"]
7 | let data = try URLEncodedFormSerializer.default.serialize(form)
8 | XCTAssertEqual(String(data: data, encoding: .utf8)!, "aaa%5D=%2Bbbb%20%20ccc")
9 | }
10 |
11 | func testPercentEncodingWithAmpersand() throws {
12 | let form: [String: URLEncodedFormData] = ["aaa": "b%26&b"]
13 | let data = try URLEncodedFormSerializer.default.serialize(form)
14 | XCTAssertEqual(String(data: data, encoding: .utf8)!, "aaa=b%2526%26b")
15 | }
16 |
17 | func testNested() throws {
18 | let form: [String: URLEncodedFormData] = ["a": ["b": ["c": ["d": ["hello": "world"]]]]]
19 | let data = try URLEncodedFormSerializer.default.serialize(form)
20 | XCTAssertEqual(String(data: data, encoding: .utf8)!, "a[b][c][d][hello]=world")
21 | }
22 |
23 | static let allTests = [
24 | ("testPercentEncoding", testPercentEncoding),
25 | ("testPercentEncodingWithAmpersand", testPercentEncodingWithAmpersand),
26 | ("testNested", testNested),
27 | ]
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | macos:
5 | macos:
6 | xcode: "9.2"
7 | steps:
8 | - checkout
9 | - run: swift build
10 | - run: swift test
11 |
12 | linux:
13 | docker:
14 | - image: codevapor/swift:4.1
15 | steps:
16 | - checkout
17 | - run:
18 | name: Compile code
19 | command: swift build
20 | - run:
21 | name: Run unit tests
22 | command: swift test
23 | - run:
24 | name: Compile code with optimizations
25 | command: swift build -c release
26 |
27 |
28 | linux-vapor:
29 | docker:
30 | - image: codevapor/swift:4.1
31 | steps:
32 | - run:
33 | name: Clone Vapor
34 | command: git clone -b 3 https://github.com/vapor/vapor.git
35 | working_directory: ~/
36 | - run:
37 | name: Switch Vapor to this URLEncodedForm revision
38 | command: swift package edit URLEncodedForm --revision $CIRCLE_SHA1
39 | working_directory: ~/vapor
40 | - run:
41 | name: Run Vapor unit tests
42 | command: swift test
43 | working_directory: ~/vapor
44 |
45 |
46 | workflows:
47 | version: 2
48 | tests:
49 | jobs:
50 | - linux
51 | - linux-vapor
52 | # - macos
53 |
54 | nightly:
55 | triggers:
56 | - schedule:
57 | cron: "0 0 * * *"
58 | filters:
59 | branches:
60 | only:
61 | - master
62 | jobs:
63 | - linux
64 | # - macos
65 |
66 |
--------------------------------------------------------------------------------