├── .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 | URL-Encoded Form 3 |
4 |
5 | 6 | Documentation 7 | 8 | 9 | Team Chat 10 | 11 | 12 | MIT License 13 | 14 | 15 | Continuous Integration 16 | 17 | 18 | Swift 4.1 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 | --------------------------------------------------------------------------------