Skip to content

Commit

Permalink
[Generator] Integrate the new URI and String coders (#226)
Browse files Browse the repository at this point in the history
[Generator] Integrate the new URI and String coders

### Motivation

Depends on runtime changes from apple/swift-openapi-runtime#45.

Up until now, we relied on a series of marker and helper protocols `_StringConvertible` and `_AutoLosslessStringConvertible` to handle converting between various types and their string representation.

This has been very manual and required a non-trivial amount of work to support any extra type, especially `Date` and generated string enums.

Well, turns out this was an unnecessarily difficult way to approach the problem - we had a better solution available for a long time - `Codable`.

Since all the generated types and all the built-in types we reference are already `Codable`, there is no need to reinvent a way to serialize and deserialize types, and we should just embrace it.

While a JSON encoder and decoder already exists in Foundation, we didn't have one handy for encoding to and from URIs (used by headers, query and path parameters), and raw string representation (using `LosslessStringConvertible`). We created those in the runtime library in PRs apple/swift-openapi-runtime#44 and apple/swift-openapi-runtime#41, and integrated them into our helper functions (which got significantly simplified this way) in apple/swift-openapi-runtime#45.

Out of scope of this PR, but this also opens the door to supporting URL form encoded bodies (#182), multipart (#36), and base64 (#11).

While this should be mostly invisible to our adopters, this refactoring creates space for implementing more complex features and overall simplifies our serialization story.

### Modifications

- Updated the generator to use the new helper functions.
- Updated the article about serialization, shows how we reduced the number of helper functions by moving to `Codable`.
- Set the `lineLength` to 120 on the formatter configuration, it was inconsistent with our `.swift-format` file, and lead to the soundness script trying to update the reference files, but then the reference tests were failing. Since we're planning to sync these in #40, this is a step closer to it, but it means that it's probably best to review this PR's diff with whitespace ignored.

### Result

Now the generated code uses the new helper functions, allowing us to delete all the deprecated helpers in 0.2.0.

### Test Plan

Updated file-based reference, snippet, and unit tests.


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. 

#226
  • Loading branch information
czechboy0 authored Aug 30, 2023
1 parent cf8e437 commit 7ddef95
Show file tree
Hide file tree
Showing 22 changed files with 360 additions and 784 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ let package = Package(
// Tests-only: Runtime library linked by generated code, and also
// helps keep the runtime library new enough to work with the generated
// code.
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.10")),
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.11")),

// Build and preview docs
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
Expand Down
1 change: 1 addition & 0 deletions Sources/_OpenAPIGeneratorCore/Extensions/SwiftFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ extension String {
configuration.lineBreakAroundMultilineExpressionChainComponents = true
configuration.indentConditionalCompilationBlocks = false
configuration.maximumBlankLines = 0
configuration.lineLength = 120
let formatter = SwiftFormatter(configuration: configuration)
try formatter.format(
source: self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension ClientFileTranslator {
left: "path",
right: .try(
.identifier("converter")
.dot("renderedRequestPath")
.dot("renderedPath")
.call([
.init(label: "template", expression: .literal(pathTemplate)),
.init(label: "parameters", expression: pathParamsArrayExpr),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ extension FileTranslator {
let generateUnknownCases = shouldGenerateUndocumentedCaseForEnumsAndOneOfs
let baseConformance =
generateUnknownCases ? Constants.StringEnum.baseConformanceOpen : Constants.StringEnum.baseConformanceClosed
let conformances =
generateUnknownCases ? Constants.StringEnum.conformancesOpen : Constants.StringEnum.conformancesClosed
let unknownCaseName = generateUnknownCases ? Constants.StringEnum.undocumentedCaseName : nil
return try translateRawRepresentableEnum(
typeName: typeName,
conformances: [baseConformance] + Constants.StringEnum.conformances,
conformances: [baseConformance] + conformances,
userDescription: userDescription,
cases: cases,
unknownCaseName: unknownCaseName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,19 @@ enum Constants {
static let baseConformanceClosed: String = "String"

/// The types that every enum conforms to.
static let conformances: [String] = [
static let conformancesOpen: [String] = [
"Codable",
"Hashable",
"Sendable",
"_AutoLosslessStringConvertible",
"CaseIterable",
]

/// The types that every enum conforms to.
static let conformancesClosed: [String] = [
"Codable",
"Hashable",
"Sendable",
]
}

/// Constants related to generated oneOf enums.
Expand Down Expand Up @@ -370,8 +376,11 @@ enum Constants {
/// The substring used in method names for the JSON coding strategy.
static let json: String = "JSON"

/// The substring used in method names for the text coding strategy.
static let text: String = "Text"
/// The substring used in method names for the URI coding strategy.
static let uri: String = "URI"

/// The substring used in method names for the string coding strategy.
static let string: String = "String"

/// The substring used in method names for the binary coding strategy.
static let binary: String = "Binary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ enum CodingStrategy: String, Hashable, Sendable {
/// A strategy using JSONEncoder/JSONDecoder.
case json

/// A strategy using LosslessStringConvertible.
case text
/// A strategy using URIEncoder/URIDecoder.
case uri

/// A strategy using StringEncoder/StringDecoder.
case string

/// A strategy that passes through the data unmodified.
case binary
Expand All @@ -29,8 +32,10 @@ enum CodingStrategy: String, Hashable, Sendable {
switch self {
case .json:
return Constants.CodingStrategy.json
case .text:
return Constants.CodingStrategy.text
case .uri:
return Constants.CodingStrategy.uri
case .string:
return Constants.CodingStrategy.string
case .binary:
return Constants.CodingStrategy.binary
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ extension FileTranslator {
{
diagnostics.emitUnsupportedIfNotNil(
chosenContent.1.encoding,
"Custom encoding for JSON content",
"Custom encoding for multipart/formEncoded content",
foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)"
)
}
Expand Down Expand Up @@ -234,11 +234,15 @@ extension FileTranslator {
) -> SchemaContent? {
if contentKey.isJSON {
let contentType = ContentType(contentKey.typeAndSubtype)
diagnostics.emitUnsupportedIfNotNil(
contentValue.encoding,
"Custom encoding for JSON content",
foundIn: "\(foundIn), content \(contentKey.rawValue)"
)
if contentType.lowercasedType == "multipart"
|| contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded")
{
diagnostics.emitUnsupportedIfNotNil(
contentValue.encoding,
"Custom encoding for multipart/formEncoded content",
foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)"
)
}
return .init(
contentType: contentType,
schema: contentValue.schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ struct ContentType: Hashable {
case .json:
return .json
case .text:
return .text
return .string
case .binary:
return .binary
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,23 @@ extension FileTranslator {
schema = schemaContext.schema
style = schemaContext.style
explode = schemaContext.explode
codingStrategy = .text
codingStrategy = .uri

// Check supported exploded/style types
let location = parameter.location
switch location {
case .query:
guard case .form = schemaContext.style else {
guard case .form = style else {
diagnostics.emitUnsupported(
"Non-form style query params",
"Query params of style \(style.rawValue), explode: \(explode)",
foundIn: foundIn
)
return nil
}
case .header, .path:
guard case .simple = schemaContext.style else {
guard case .simple = style else {
diagnostics.emitUnsupported(
"Non-simple style \(location.rawValue) params",
"\(location.rawValue) params of style \(style.rawValue), explode: \(explode)",
foundIn: foundIn
)
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ extension ServerFileTranslator {
convertExpr = .try(
.identifier("converter").dot(methodName("QueryItem"))
.call([
.init(label: "in", expression: .identifier("metadata").dot("queryParameters")),
.init(label: "in", expression: .identifier("request").dot("query")),
.init(label: "style", expression: .dot(typedParameter.style.runtimeName)),
.init(label: "explode", expression: .literal(.bool(typedParameter.explode))),
.init(label: "name", expression: .literal(parameter.name)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ extension FileTranslator {
switch header.schemaOrContent {
case let .a(schemaContext):
schema = schemaContext.schema
codingStrategy = .text
codingStrategy = .uri
case let .b(contentMap):
guard
let typedContent = try bestSingleTypedContent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,21 +210,22 @@ Supported features are always provided on _both_ client and server.

- [ ] matrix (in path)
- [ ] label (in path)
- [ ] form (in query)
- [x] primitive
- [x] array
- [ ] object
- [x] form (in query)
- [ ] form (in cookie)
- [x] simple (in path)
- [x] simple (in header)
- [ ] spaceDelimited (in query)
- [ ] pipeDelimited (in query)
- [ ] deepObject (in query)

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

| Location | Style | Explode |
| -------- | ----- | ------- |
| path | `simple` | `false` |
| query | `form` | `true` |
| query | `form` | `false` |
| header | `simple` | `false` |

#### Reference Object

Expand Down
Loading

0 comments on commit 7ddef95

Please sign in to comment.