Skip to content

Commit

Permalink
[Generator] Support unexploded query items (#171)
Browse files Browse the repository at this point in the history
[Generator] Support unexploded query items

### Motivation

Depends on apple/swift-openapi-runtime#35.

Fixes #52.

By default, query items are encoded as exploded (`key=value1&key=value2`), but OpenAPI allows explicitly requesting them unexploded (`key=value1,value2`). This feature missing has shown up in a few OpenAPI documents recently.

### Modifications

Adapt the generator to provide the two new `style` and `explode` parameters to query item encoding/decoding functions.

### Result

We now support unexploded query items.

### Test Plan

Expanded snippet-based tests to allow generating not just types, but also parts of the client/server. This has allowed us to not have to expand file-based reference tests here.


Reviewed by: glbrntt

Builds:
     ✔︎ pull request validation (5.8) - Build finished. 
     ✔︎ pull request validation (5.9) - Build finished. 
     ✔︎ pull request validation (docc test) - Build finished. 
     ✔︎ pull request validation (integration test) - Build finished. 
     ✔︎ pull request validation (nightly) - Build finished. 
     ✔︎ pull request validation (soundness) - Build finished. 

#171
  • Loading branch information
czechboy0 authored Aug 8, 2023
1 parent fd64063 commit 0ae57b9
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ let package = Package(
),

// Tests-only: Runtime library linked by generated code
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.7")),
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.8")),

// Build and preview docs
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ enum LiteralDescription: Equatable, Codable {
/// For example `42`.
case int(Int)

/// A Boolean literal.
///
/// For example `true`.
case bool(Bool)

/// The nil literal: `nil`.
case `nil`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ struct TextBasedRenderer: RendererProtocol {
return "\"\(string)\""
case let .int(int):
return "\(int)"
case let .bool(bool):
return bool ? "true" : "false"
case .nil:
return "nil"
case .array(let items):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,13 @@ enum Constants {

/// The name of the namespace.
static let namespace: String = "Parameters"

/// Maps to `OpenAPIRuntime.ParameterStyle`.
enum Style {

/// The form style.
static let form = "form"
}
}

/// Constants related to the Headers namespace.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ struct TypedParameter {
/// The underlying schema.
var schema: UnresolvedSchema

/// The parameter serialization style.
var style: OpenAPI.Parameter.SchemaContext.Style

/// The parameter explode value.
var explode: Bool

/// The computed type usage.
var typeUsage: TypeUsage

Expand Down Expand Up @@ -134,9 +140,13 @@ extension FileTranslator {

let schema: UnresolvedSchema
let codingStrategy: CodingStrategy
let style: OpenAPI.Parameter.SchemaContext.Style
let explode: Bool
switch parameter.schemaOrContent {
case let .a(schemaContext):
schema = schemaContext.schema
style = schemaContext.style
explode = schemaContext.explode
codingStrategy = .text

// Check supported exploded/style types
Expand All @@ -150,13 +160,6 @@ extension FileTranslator {
)
return nil
}
guard schemaContext.explode else {
diagnostics.emitUnsupported(
"Unexploded query params",
foundIn: foundIn
)
return nil
}
case .header, .path:
guard case .simple = schemaContext.style else {
diagnostics.emitUnsupported(
Expand Down Expand Up @@ -189,6 +192,17 @@ extension FileTranslator {
.content
.contentType
.codingStrategy

// Defaults are defined by the OpenAPI specification:
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-10
switch parameter.location {
case .query, .cookie:
style = .form
explode = true
case .path, .header:
style = .simple
explode = false
}
}

// Check if the underlying schema is supported
Expand Down Expand Up @@ -221,6 +235,8 @@ extension FileTranslator {
return .init(
parameter: parameter,
schema: schema,
style: style,
explode: explode,
typeUsage: usage,
codingStrategy: codingStrategy,
asSwiftSafeName: swiftSafeName
Expand Down Expand Up @@ -271,3 +287,16 @@ extension OpenAPI.Parameter.Context.Location {
}
}
}

extension OpenAPI.Parameter.SchemaContext.Style {

/// The runtime name for the style.
var runtimeName: String {
switch self {
case .form:
return Constants.Components.Parameters.Style.form
default:
preconditionFailure("Unsupported style")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,20 +116,32 @@ extension ClientFileTranslator {
) throws -> Expression? {
let methodPrefix: String
let containerExpr: Expression
let supportsStyleAndExplode: Bool
switch parameter.location {
case .header:
methodPrefix = "HeaderField"
containerExpr = .identifier(requestVariableName).dot("headerFields")
supportsStyleAndExplode = false
case .query:
methodPrefix = "QueryItem"
containerExpr = .identifier(requestVariableName)
supportsStyleAndExplode = true
default:
diagnostics.emitUnsupported(
"Parameter of type \(parameter.location.rawValue)",
foundIn: parameter.description
)
return nil
}
let styleAndExplodeArgs: [FunctionArgumentDescription]
if supportsStyleAndExplode {
styleAndExplodeArgs = [
.init(label: "style", expression: .dot(parameter.style.runtimeName)),
.init(label: "explode", expression: .literal(.bool(parameter.explode))),
]
} else {
styleAndExplodeArgs = []
}
return .try(
.identifier("converter")
.dot("set\(methodPrefix)As\(parameter.codingStrategy.runtimeName)")
Expand All @@ -138,7 +150,8 @@ extension ClientFileTranslator {
.init(
label: "in",
expression: .inOut(containerExpr)
),
)
] + styleAndExplodeArgs + [
.init(label: "name", expression: .literal(parameter.name)),
.init(
label: "value",
Expand Down Expand Up @@ -194,6 +207,8 @@ extension ServerFileTranslator {
.identifier("converter").dot(methodName("QueryItem"))
.call([
.init(label: "in", expression: .identifier("metadata").dot("queryParameters")),
.init(label: "style", expression: .dot(typedParameter.style.runtimeName)),
.init(label: "explode", expression: .literal(.bool(typedParameter.explode))),
.init(label: "name", expression: .literal(parameter.name)),
.init(
label: "as",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct TypesFileTranslator: FileTranslator {
let components = try translateComponents(doc.components)

let operationDescriptions = try OperationDescription.all(
from: parsedOpenAPI.paths,
from: doc.paths,
in: doc.components,
asSwiftSafeName: swiftSafeName
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Supported features are always provided on _both_ client and server.
- [x] requestBody
- [x] responses
- [ ] callbacks
- [ ] deprecated
- [x] deprecated
- [ ] security
- [ ] servers

Expand Down Expand Up @@ -162,7 +162,7 @@ Supported features are always provided on _both_ client and server.
- [ ] xml
- [ ] externalDocs
- [ ] example
- [ ] deprecated
- [x] deprecated

#### External Documentation Object

Expand Down Expand Up @@ -196,15 +196,15 @@ Supported features are always provided on _both_ client and server.
- [x] in
- [x] description
- [x] required
- [ ] deprecated
- [x] deprecated
- [ ] allowEmptyValue
- [x] style (not all)
- [x] explode (only explode: `true`)
- [x] style (only defaults)
- [x] explode (non default only for query items)
- [ ] allowReserved
- [x] schema
- [ ] example
- [ ] examples
- [ ] content
- [x] content (chooses one from the map)

#### Style Values

Expand All @@ -223,7 +223,7 @@ Supported features are always provided on _both_ client and server.

Supported location + styles + exploded combinations:
- path + simple + false
- query + form + true
- query + form + true/false
- header + simple + false

#### Reference Object
Expand Down
8 changes: 5 additions & 3 deletions Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,15 @@ extension Declaration {
extension LiteralDescription {
var name: String {
switch self {
case .string(_):
case .string:
return "string"
case .int(_):
case .int:
return "int"
case .bool:
return "bool"
case .nil:
return "nil"
case .array(_):
case .array:
return "array"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,22 @@ public struct Client: APIProtocol {
suppressMutabilityWarning(&request)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "limit",
value: input.query.limit
)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "habitat",
value: input.query.habitat
)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "feeds",
value: input.query.feeds
)
Expand All @@ -72,6 +78,8 @@ public struct Client: APIProtocol {
)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "since",
value: input.query.since
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,29 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
let query: Operations.listPets.Input.Query = .init(
limit: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "limit",
as: Swift.Int32.self
),
habitat: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "habitat",
as: Operations.listPets.Input.Query.habitatPayload.self
),
feeds: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "feeds",
as: Operations.listPets.Input.Query.feedsPayload.self
),
since: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "since",
as: Components.Parameters.query_born_since.self
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,22 @@ public struct Client: APIProtocol {
suppressMutabilityWarning(&request)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "limit",
value: input.query.limit
)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "habitat",
value: input.query.habitat
)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "feeds",
value: input.query.feeds
)
Expand All @@ -72,6 +78,8 @@ public struct Client: APIProtocol {
)
try converter.setQueryItemAsText(
in: &request,
style: .form,
explode: true,
name: "since",
value: input.query.since
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,29 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
let query: Operations.listPets.Input.Query = .init(
limit: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "limit",
as: Swift.Int32.self
),
habitat: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "habitat",
as: Operations.listPets.Input.Query.habitatPayload.self
),
feeds: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "feeds",
as: Operations.listPets.Input.Query.feedsPayload.self
),
since: try converter.getOptionalQueryItemAsText(
in: metadata.queryParameters,
style: .form,
explode: true,
name: "since",
as: Components.Parameters.query_born_since.self
)
Expand Down
Loading

0 comments on commit 0ae57b9

Please sign in to comment.