diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index f2affe3a8..6f3d1782e 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -319,6 +319,8 @@ enum Constants { /// The form style. static let form = "form" + /// The deepObject style. + static let deepObject = "deepObject" } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index d61957ab4..7ac1f18fc 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -130,7 +130,10 @@ extension FileTranslator { let location = parameter.location switch location { case .query: - guard case .form = style else { + switch style { + case .form: break + case .deepObject where explode: break + default: try diagnostics.emitUnsupported( "Query params of style \(style.rawValue), explode: \(explode)", foundIn: foundIn @@ -243,6 +246,7 @@ extension OpenAPI.Parameter.SchemaContext.Style { var runtimeName: String { switch self { case .form: return Constants.Components.Parameters.Style.form + case .deepObject: return Constants.Components.Parameters.Style.deepObject default: preconditionFailure("Unsupported style") } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index 1d9a5ed56..052cc8eb8 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -69,6 +69,34 @@ paths: schema: format: uuid type: string + - name: sort + in: query + required: false + style: deepObject + explode: true + schema: + type: object + required: + - id + properties: + id: + type: string + name: + type: string + - name: filter + in: query + required: true + style: deepObject + explode: true + schema: + type: object + required: + - name + properties: + name: + type: string + state: + type: string - $ref: '#/components/parameters/query.born-since' responses: '200': diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 25f1f53de..d9c80e62e 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -87,6 +87,20 @@ public struct Client: APIProtocol { name: "My-Request-UUID", value: input.headers.myRequestUUID ) + try converter.setQueryItemAsURI( + in: &request, + style: .deepObject, + explode: true, + name: "sort", + value: input.query.sort + ) + try converter.setQueryItemAsURI( + in: &request, + style: .deepObject, + explode: true, + name: "filter", + value: input.query.filter + ) try converter.setQueryItemAsURI( in: &request, style: .form, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index c7a02fcfe..dae16f3f2 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -187,6 +187,20 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { name: "feeds", as: Operations.ListPets.Input.Query.FeedsPayload.self ), + sort: try converter.getOptionalQueryItemAsURI( + in: request.soar_query, + style: .deepObject, + explode: true, + name: "sort", + as: Operations.ListPets.Input.Query.SortPayload.self + ), + filter: try converter.getRequiredQueryItemAsURI( + in: request.soar_query, + style: .deepObject, + explode: true, + name: "filter", + as: Operations.ListPets.Input.Query.FilterPayload.self + ), since: try converter.getOptionalQueryItemAsURI( in: request.soar_query, style: .form, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index cdcb3785e..43d892254 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -66,7 +66,7 @@ extension APIProtocol { /// - Remark: HTTP `GET /pets`. /// - Remark: Generated from `#/paths//pets/get(listPets)`. public func listPets( - query: Operations.ListPets.Input.Query = .init(), + query: Operations.ListPets.Input.Query, headers: Operations.ListPets.Input.Headers = .init() ) async throws -> Operations.ListPets.Output { try await listPets(Operations.ListPets.Input( @@ -1895,6 +1895,56 @@ public enum Operations { public typealias FeedsPayload = [Operations.ListPets.Input.Query.FeedsPayloadPayload] /// - Remark: Generated from `#/paths/pets/GET/query/feeds`. public var feeds: Operations.ListPets.Input.Query.FeedsPayload? + /// - Remark: Generated from `#/paths/pets/GET/query/sort`. + public struct SortPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/pets/GET/query/sort/id`. + public var id: Swift.String + /// - Remark: Generated from `#/paths/pets/GET/query/sort/name`. + public var name: Swift.String? + /// Creates a new `SortPayload`. + /// + /// - Parameters: + /// - id: + /// - name: + public init( + id: Swift.String, + name: Swift.String? = nil + ) { + self.id = id + self.name = name + } + public enum CodingKeys: String, CodingKey { + case id + case name + } + } + /// - Remark: Generated from `#/paths/pets/GET/query/sort`. + public var sort: Operations.ListPets.Input.Query.SortPayload? + /// - Remark: Generated from `#/paths/pets/GET/query/filter`. + public struct FilterPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/pets/GET/query/filter/name`. + public var name: Swift.String + /// - Remark: Generated from `#/paths/pets/GET/query/filter/state`. + public var state: Swift.String? + /// Creates a new `FilterPayload`. + /// + /// - Parameters: + /// - name: + /// - state: + public init( + name: Swift.String, + state: Swift.String? = nil + ) { + self.name = name + self.state = state + } + public enum CodingKeys: String, CodingKey { + case name + case state + } + } + /// - Remark: Generated from `#/paths/pets/GET/query/filter`. + public var filter: Operations.ListPets.Input.Query.FilterPayload /// Supply this parameter to filter pets born since the provided date. /// /// - Remark: Generated from `#/paths/pets/GET/query/since`. @@ -1905,16 +1955,22 @@ public enum Operations { /// - limit: How many items to return at one time (max 100) /// - habitat: /// - feeds: + /// - sort: + /// - filter: /// - since: Supply this parameter to filter pets born since the provided date. public init( limit: Swift.Int32? = nil, habitat: Operations.ListPets.Input.Query.HabitatPayload? = nil, feeds: Operations.ListPets.Input.Query.FeedsPayload? = nil, + sort: Operations.ListPets.Input.Query.SortPayload? = nil, + filter: Operations.ListPets.Input.Query.FilterPayload, since: Components.Parameters.Query_bornSince? = nil ) { self.limit = limit self.habitat = habitat self.feeds = feeds + self.sort = sort + self.filter = filter self.since = since } } @@ -1946,7 +2002,7 @@ public enum Operations { /// - query: /// - headers: public init( - query: Operations.ListPets.Input.Query = .init(), + query: Operations.ListPets.Input.Query, headers: Operations.ListPets.Input.Headers = .init() ) { self.query = query diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 98cb2f15e..f3a59d0d2 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -2600,6 +2600,32 @@ final class SnippetBasedReferenceTests: XCTestCase { type: array items: type: string + - name: sort + in: query + required: false + style: deepObject + explode: true + schema: + type: object + properties: + option1: + type: string + option2: + type: string + - name: filter + in: query + required: true + style: deepObject + explode: true + schema: + type: object + required: + - option3 + properties: + option3: + type: string + option4: + type: string responses: default: description: Response @@ -2610,18 +2636,54 @@ final class SnippetBasedReferenceTests: XCTestCase { public var single: Swift.String? public var manyExploded: [Swift.String]? public var manyUnexploded: [Swift.String]? + public struct sortPayload: Codable, Hashable, Sendable { + public var option1: Swift.String? + public var option2: Swift.String? + public init( + option1: Swift.String? = nil, + option2: Swift.String? = nil + ) { + self.option1 = option1 + self.option2 = option2 + } + public enum CodingKeys: String, CodingKey { + case option1 + case option2 + } + } + public var sort: Operations.get_sol_foo.Input.Query.sortPayload? + public struct filterPayload: Codable, Hashable, Sendable { + public var option3: Swift.String + public var option4: Swift.String? + public init( + option3: Swift.String, + option4: Swift.String? = nil + ) { + self.option3 = option3 + self.option4 = option4 + } + public enum CodingKeys: String, CodingKey { + case option3 + case option4 + } + } + public var filter: Operations.get_sol_foo.Input.Query.filterPayload public init( single: Swift.String? = nil, manyExploded: [Swift.String]? = nil, - manyUnexploded: [Swift.String]? = nil + manyUnexploded: [Swift.String]? = nil, + sort: Operations.get_sol_foo.Input.Query.sortPayload? = nil, + filter: Operations.get_sol_foo.Input.Query.filterPayload ) { self.single = single self.manyExploded = manyExploded self.manyUnexploded = manyUnexploded + self.sort = sort + self.filter = filter } } public var query: Operations.get_sol_foo.Input.Query - public init(query: Operations.get_sol_foo.Input.Query = .init()) { + public init(query: Operations.get_sol_foo.Input.Query) { self.query = query } } @@ -2658,6 +2720,20 @@ final class SnippetBasedReferenceTests: XCTestCase { name: "manyUnexploded", value: input.query.manyUnexploded ) + try converter.setQueryItemAsURI( + in: &request, + style: .deepObject, + explode: true, + name: "sort", + value: input.query.sort + ) + try converter.setQueryItemAsURI( + in: &request, + style: .deepObject, + explode: true, + name: "filter", + value: input.query.filter + ) return (request, nil) } """, @@ -2684,6 +2760,20 @@ final class SnippetBasedReferenceTests: XCTestCase { explode: false, name: "manyUnexploded", as: [Swift.String].self + ), + sort: try converter.getOptionalQueryItemAsURI( + in: request.soar_query, + style: .deepObject, + explode: true, + name: "sort", + as: Operations.get_sol_foo.Input.Query.sortPayload.self + ), + filter: try converter.getRequiredQueryItemAsURI( + in: request.soar_query, + style: .deepObject, + explode: true, + name: "filter", + as: Operations.get_sol_foo.Input.Query.filterPayload.self ) ) return Operations.get_sol_foo.Input(query: query) diff --git a/Tests/PetstoreConsumerTests/Test_Client.swift b/Tests/PetstoreConsumerTests/Test_Client.swift index 76cb89f92..6b44ae4f1 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -40,7 +40,7 @@ final class Test_Client: XCTestCase { XCTAssertEqual(operationID, "listPets") XCTAssertEqual( request.path, - "/pets?limit=24&habitat=water&feeds=herbivore&feeds=carnivore&since=2023-01-18T10%3A04%3A11Z" + "/pets?limit=24&habitat=water&feeds=herbivore&feeds=carnivore&sort%5Bid%5D=ascending&sort%5Bname%5D=descending&filter%5Bname%5D=whale&since=2023-01-18T10%3A04%3A11Z" ) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .get) @@ -66,7 +66,14 @@ final class Test_Client: XCTestCase { } let response = try await client.listPets( .init( - query: .init(limit: 24, habitat: .water, feeds: [.herbivore, .carnivore], since: .test), + query: .init( + limit: 24, + habitat: .water, + feeds: [.herbivore, .carnivore], + sort: .init(id: "ascending", name: "descending"), + filter: .init(name: "whale"), + since: .test + ), headers: .init(myRequestUUID: "abcd-1234") ) ) @@ -84,7 +91,7 @@ final class Test_Client: XCTestCase { func testListPets_default() async throws { transport = .init { request, body, baseURL, operationID in XCTAssertEqual(operationID, "listPets") - XCTAssertEqual(request.path, "/pets?limit=24") + XCTAssertEqual(request.path, "/pets?limit=24&filter%5Bname%5D=whale") XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .get) XCTAssertEqual(request.headerFields, [.accept: "application/json"]) @@ -100,7 +107,7 @@ final class Test_Client: XCTestCase { """# ) } - let response = try await client.listPets(.init(query: .init(limit: 24))) + let response = try await client.listPets(.init(query: .init(limit: 24, filter: .init(name: "whale")))) guard case let .default(statusCode, value) = response else { XCTFail("Unexpected response: \(response)") return diff --git a/Tests/PetstoreConsumerTests/Test_Server.swift b/Tests/PetstoreConsumerTests/Test_Server.swift index 9a8fa0f93..05a7fb7a8 100644 --- a/Tests/PetstoreConsumerTests/Test_Server.swift +++ b/Tests/PetstoreConsumerTests/Test_Server.swift @@ -33,6 +33,7 @@ final class Test_Server: XCTestCase { XCTAssertEqual(input.query.habitat, .water) XCTAssertEqual(input.query.since, .test) XCTAssertEqual(input.query.feeds, [.carnivore, .herbivore]) + XCTAssertEqual(input.query.sort, .init(id: "ascending", name: "descending")) XCTAssertEqual(input.headers.myRequestUUID, "abcd-1234") return .ok( .init( @@ -43,7 +44,8 @@ final class Test_Server: XCTestCase { }) let (response, responseBody) = try await server.listPets( .init( - soar_path: "/api/pets?limit=24&habitat=water&feeds=carnivore&feeds=herbivore&since=\(Date.testString)", + soar_path: + "/api/pets?limit=24&habitat=water&feeds=carnivore&feeds=herbivore&sort%5Bid%5D=ascending&sort%5Bname%5D=descending&filter%5Bname%5D=whale&since=\(Date.testString)", method: .get, headerFields: [.init("My-Request-UUID")!: "abcd-1234"] ), @@ -82,7 +84,7 @@ final class Test_Server: XCTestCase { .default(statusCode: 400, .init(body: .json(.init(code: 1, me_dollar_sage: "Oh no!")))) }) let (response, responseBody) = try await server.listPets( - .init(soar_path: "/api/pets", method: .get), + .init(soar_path: "/api/pets?filter%5Bname%5D=whale", method: .get), nil, .init() )