diff --git a/Sources/SwiftAPIClient/Utils/Coders/URLQuery/URLQueryEncoder.swift b/Sources/SwiftAPIClient/Utils/Coders/URLQuery/URLQueryEncoder.swift index fffb102..84af651 100644 --- a/Sources/SwiftAPIClient/Utils/Coders/URLQuery/URLQueryEncoder.swift +++ b/Sources/SwiftAPIClient/Utils/Coders/URLQuery/URLQueryEncoder.swift @@ -258,7 +258,10 @@ final class _URLQueryEncoder: Encoder { @discardableResult func encode(_ value: Encodable) throws -> QueryValue { - if case let .json(jsonEncoder) = context.nestedEncodingStrategy, !codingPath.isEmpty { + let isArrayEncoder = IsArrayEncoder(codingPath: codingPath) + try? value.encode(to: isArrayEncoder) + let isArray = isArrayEncoder.isArray ?? false + if case let .json(jsonEncoder) = context.nestedEncodingStrategy, !codingPath.isEmpty, !(codingPath.count < 2 && isArray) { let jsonEncoder = jsonEncoder ?? { let encoder = JSONEncoder() encoder.dateEncodingStrategy = context.dateEncodingStrategy @@ -740,3 +743,113 @@ private let _iso8601Formatter: ISO8601DateFormatter = { formatter.formatOptions = .withInternetDateTime return formatter }() + +private final class IsArrayEncoder: Encoder { + + var isArray: Bool? + var codingPath: [CodingKey] = [] + var userInfo: [CodingUserInfoKey: Any] = [:] + + init(codingPath: [CodingKey] = []) { + self.codingPath = codingPath + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + if isArray == nil { + isArray = false + } + return KeyedEncodingContainer(MockKeyed()) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + if isArray == nil { + isArray = true + } + return MockUnkeyed() + } + + func singleValueContainer() -> SingleValueEncodingContainer { + return MockSingle(encoder: self, codingPath: codingPath) + } + + private struct MockKeyed: KeyedEncodingContainerProtocol { + var codingPath: [CodingKey] = [] + mutating func encodeNil(forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Bool, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: String, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Double, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Float, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Int, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Int8, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Int16, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Int32, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: Int64, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: UInt, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: UInt8, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: UInt16, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: UInt32, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: UInt64, forKey key: Key) throws { throw MockError() } + mutating func encode(_ value: T, forKey key: Key) throws where T : Encodable { throw MockError() } + mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { return KeyedEncodingContainer(MockKeyed()) } + mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { return MockUnkeyed() } + mutating func superEncoder() -> Encoder { return IsArrayEncoder() } + mutating func superEncoder(forKey key: Key) -> Encoder { return IsArrayEncoder() } + } + + private struct MockUnkeyed: UnkeyedEncodingContainer { + + var codingPath: [CodingKey] = [] + var count: Int = 0 + + mutating func encodeNil() throws { throw MockError() } + mutating func encode(_ value: Bool) throws { throw MockError() } + mutating func encode(_ value: String) throws { throw MockError() } + mutating func encode(_ value: Double) throws { throw MockError() } + mutating func encode(_ value: Float) throws { throw MockError() } + mutating func encode(_ value: Int) throws { throw MockError() } + mutating func encode(_ value: Int8) throws { throw MockError() } + mutating func encode(_ value: Int16) throws { throw MockError() } + mutating func encode(_ value: Int32) throws { throw MockError() } + mutating func encode(_ value: Int64) throws { throw MockError() } + mutating func encode(_ value: UInt) throws { throw MockError() } + mutating func encode(_ value: UInt8) throws { throw MockError() } + mutating func encode(_ value: UInt16) throws { throw MockError() } + mutating func encode(_ value: UInt32) throws { throw MockError() } + mutating func encode(_ value: UInt64) throws { throw MockError() } + mutating func encode(_ value: T) throws where T : Encodable { throw MockError() } + mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { return KeyedEncodingContainer(MockKeyed()) } + mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { return MockUnkeyed() } + mutating func superEncoder() -> Encoder { return IsArrayEncoder() } + } + + private struct MockSingle: SingleValueEncodingContainer { + + let encoder: IsArrayEncoder + var codingPath: [CodingKey] = [] + + mutating func encodeNil() throws { try throwError() } + mutating func encode(_ value: Bool) throws { try throwError() } + mutating func encode(_ value: String) throws { try throwError() } + mutating func encode(_ value: Double) throws { try throwError() } + mutating func encode(_ value: Float) throws { try throwError() } + mutating func encode(_ value: Int) throws { try throwError() } + mutating func encode(_ value: Int8) throws { try throwError() } + mutating func encode(_ value: Int16) throws { try throwError() } + mutating func encode(_ value: Int32) throws { try throwError() } + mutating func encode(_ value: Int64) throws { try throwError() } + mutating func encode(_ value: UInt) throws { try throwError() } + mutating func encode(_ value: UInt8) throws { try throwError() } + mutating func encode(_ value: UInt16) throws { try throwError() } + mutating func encode(_ value: UInt32) throws { try throwError() } + mutating func encode(_ value: UInt64) throws { try throwError() } + mutating func encode(_ value: T) throws where T : Encodable { try value.encode(to: encoder) } + private func throwError() throws { + if encoder.isArray == nil { + encoder.isArray = false + } + throw MockError() + } + } +} + +private struct MockError: Error {} diff --git a/Tests/SwiftAPIClientTests/URLQueryEncoderTests.swift b/Tests/SwiftAPIClientTests/URLQueryEncoderTests.swift index 2c51c00..770eebd 100644 --- a/Tests/SwiftAPIClientTests/URLQueryEncoderTests.swift +++ b/Tests/SwiftAPIClientTests/URLQueryEncoderTests.swift @@ -406,6 +406,19 @@ final class FormURLEncoderTests: XCTestCase { XCTAssertEqual(result, expected) } + func testThatEncodableStructCanBeEncodedWithCommaAndJSON() { + // Given + let encoder = FormURLEncoder(arrayEncodingStrategy: .commaSeparator, nestedEncodingStrategy: .json) + let parameters = EncodableStruct() + + // When + let result = try? String(data: encoder.encode(parameters), encoding: .utf8) + + // Then + let expected = "one=one&two=2&three=true&four=1%2C2%2C3&five=%7B%22a%22%3A%22a%22%7D&six=%7B%22a%22%3A%7B%22b%22%3A%22b%22%7D%7D&seven=%7B%22a%22%3A%22a%22%7D" + XCTAssertEqual(result, expected) + } + func testThatManuallyEncodableStructCanBeEncodedWithIndexInBrackets() { // Given let encoder = FormURLEncoder(arrayEncodingStrategy: .brackets(indexed: true))