Skip to content

Commit e86acf9

Browse files
committed
Step 3: Change the generator and update tests to reflect the new logic
1 parent f680a32 commit e86acf9

File tree

16 files changed

+732
-1211
lines changed

16 files changed

+732
-1211
lines changed

Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,38 @@ struct SwitchDescription: Equatable, Codable {
664664
var cases: [SwitchCaseDescription]
665665
}
666666

667+
/// A description of an if condition and the corresponding code block.
668+
///
669+
/// For example: in `if foo { bar }`, the condition pair represents
670+
/// `foo` + `bar`.
671+
struct IfConditionPair: Equatable, Codable {
672+
673+
/// The expressions evaluated by the if statement and their corresponding
674+
/// body blocks. If more than one is provided, an `else if` branch is added.
675+
///
676+
/// For example, in `if foo { bar }`, `condition` is `foo`.
677+
var condition: Expression
678+
679+
/// The body executed if the `condition` evaluates to true.
680+
///
681+
/// For example, in `if foo { bar }`, `body` is `bar`.
682+
var body: [CodeBlock]
683+
}
684+
685+
/// A description of an if[[/elseif]/else] statement expression.
686+
///
687+
/// For example: `if foo { } else if bar { } else { }`.
688+
struct IfStatementDescription: Equatable, Codable {
689+
690+
/// The conditional branches.
691+
var branches: [IfConditionPair]
692+
693+
/// The body of an else block.
694+
///
695+
/// No `else` statement is added when `elseBody` is nil.
696+
var elseBody: [CodeBlock]?
697+
}
698+
667699
/// A description of a do statement.
668700
///
669701
/// For example: `do { try foo() } catch { return bar }`.
@@ -709,6 +741,9 @@ enum KeywordKind: Equatable, Codable {
709741

710742
/// The await keyword.
711743
case `await`
744+
745+
/// The throw keyword.
746+
case `throw`
712747
}
713748

714749
/// A description of an expression that places a keyword before an expression.
@@ -751,8 +786,14 @@ enum BinaryOperator: String, Equatable, Codable {
751786
/// The += operator, adds and then assigns another value.
752787
case plusEquals = "+="
753788

789+
/// The == operator, checks equality between two values.
790+
case equals = "=="
791+
754792
/// The ... operator, creates an end-inclusive range between two numbers.
755793
case rangeInclusive = "..."
794+
795+
/// The || operator, used between two Boolean values.
796+
case booleanOr = "||"
756797
}
757798

758799
/// A description of a binary operation expression.
@@ -832,6 +873,11 @@ indirect enum Expression: Equatable, Codable {
832873
/// For example: `switch foo {`.
833874
case `switch`(SwitchDescription)
834875

876+
/// An if statement, with optional else if's and an else statement attached.
877+
///
878+
/// For example: `if foo { bar } else if baz { boo } else { bam }`.
879+
case ifStatement(IfStatementDescription)
880+
835881
/// A do statement.
836882
///
837883
/// For example: `do { try foo() } catch { return bar }`.
@@ -1202,6 +1248,23 @@ extension Expression {
12021248
)
12031249
}
12041250

1251+
/// Returns an if statement, with optional else if's and an else
1252+
/// statement attached.
1253+
/// - Parameters:
1254+
/// - branches: The conditional branches.
1255+
/// - elseBody: The body of an else block.
1256+
static func ifStatement(
1257+
branches: [IfConditionPair],
1258+
elseBody: [CodeBlock]? = nil
1259+
) -> Self {
1260+
.ifStatement(
1261+
.init(
1262+
branches: branches,
1263+
elseBody: elseBody
1264+
)
1265+
)
1266+
}
1267+
12051268
/// Returns a new function call expression.
12061269
///
12071270
/// For example `foo(bar: 42)`.

Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,28 @@ struct TextBasedRenderer: RendererProtocol {
173173
return lines.joinedLines()
174174
}
175175

176+
/// Renders the specified if statement.
177+
func renderedIf(_ ifDesc: IfStatementDescription) -> String {
178+
var branches = ifDesc.branches
179+
precondition(!branches.isEmpty, "Cannot have an if statement with no branches.")
180+
var lines: [String] = []
181+
let firstCondition = branches.removeFirst()
182+
lines.append("if \(renderedExpression(firstCondition.condition)) {")
183+
lines.append(renderedCodeBlocks(firstCondition.body))
184+
lines.append("}")
185+
for branch in branches {
186+
lines.append("else if \(renderedExpression(branch.condition)) {")
187+
lines.append(renderedCodeBlocks(branch.body))
188+
lines.append("}")
189+
}
190+
if let elseBody = ifDesc.elseBody {
191+
lines.append("else {")
192+
lines.append(renderedCodeBlocks(elseBody))
193+
lines.append("}")
194+
}
195+
return lines.joinedLines()
196+
}
197+
176198
/// Renders the specified switch expression.
177199
func renderedDoStatement(_ description: DoStatementDescription) -> String {
178200
var lines: [String] = ["do {"]
@@ -199,6 +221,8 @@ struct TextBasedRenderer: RendererProtocol {
199221
return "try\(hasPostfixQuestionMark ? "?" : "")"
200222
case .await:
201223
return "await"
224+
case .throw:
225+
return "throw"
202226
}
203227
}
204228

@@ -266,6 +290,8 @@ struct TextBasedRenderer: RendererProtocol {
266290
return renderedAssignment(assignment)
267291
case .switch(let switchDesc):
268292
return renderedSwitch(switchDesc)
293+
case .ifStatement(let ifDesc):
294+
return renderedIf(ifDesc)
269295
case .doStatement(let doStmt):
270296
return renderedDoStatement(doStmt)
271297
case .valueBinding(let valueBinding):

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,78 @@ extension FileTranslator {
6161
return .init(content: content, typeUsage: associatedType)
6262
}
6363

64+
/// Extract the supported content types.
65+
/// - Parameters:
66+
/// - map: The content map from the OpenAPI document.
67+
/// - excludeBinary: A Boolean value controlling whether binary content
68+
/// type should be skipped, for example used when encoding headers.
69+
/// - parent: The parent type of the chosen typed schema.
70+
/// - Returns: The supported content type + schema + type names.
71+
func supportedTypedContents(
72+
_ map: OpenAPI.Content.Map,
73+
excludeBinary: Bool = false,
74+
inParent parent: TypeName
75+
) throws -> [TypedSchemaContent] {
76+
let contents = supportedContents(
77+
map,
78+
excludeBinary: excludeBinary,
79+
foundIn: parent.description
80+
)
81+
return try contents.compactMap { content in
82+
guard
83+
try validateSchemaIsSupported(
84+
content.schema,
85+
foundIn: parent.description
86+
)
87+
else {
88+
return nil
89+
}
90+
let identifier = contentSwiftName(content.contentType)
91+
let associatedType = try typeAssigner.typeUsage(
92+
usingNamingHint: identifier,
93+
withSchema: content.schema,
94+
inParent: parent
95+
)
96+
return .init(content: content, typeUsage: associatedType)
97+
}
98+
}
99+
100+
/// Extract the supported content types.
101+
/// - Parameters:
102+
/// - contents: The content map from the OpenAPI document.
103+
/// - excludeBinary: A Boolean value controlling whether binary content
104+
/// type should be skipped, for example used when encoding headers.
105+
/// - foundIn: The location where this content is parsed.
106+
/// - Returns: the detected content type + schema, nil if no supported
107+
/// schema found or if empty.
108+
func supportedContents(
109+
_ contents: OpenAPI.Content.Map,
110+
excludeBinary: Bool = false,
111+
foundIn: String
112+
) -> [SchemaContent] {
113+
guard !contents.isEmpty else {
114+
return []
115+
}
116+
guard config.featureFlags.contains(.multipleContentTypes) else {
117+
return bestSingleContent(
118+
contents,
119+
excludeBinary: excludeBinary,
120+
foundIn: foundIn
121+
)
122+
.flatMap { [$0] } ?? []
123+
}
124+
return
125+
contents
126+
.compactMap { key, value in
127+
parseContentIfSupported(
128+
contentKey: key,
129+
contentValue: value,
130+
excludeBinary: excludeBinary,
131+
foundIn: foundIn + "/\(key.rawValue)"
132+
)
133+
}
134+
}
135+
64136
/// While we only support a single content at a time, choose the best one.
65137
///
66138
/// Priority:
@@ -72,6 +144,7 @@ extension FileTranslator {
72144
/// - map: The content map from the OpenAPI document.
73145
/// - excludeBinary: A Boolean value controlling whether binary content
74146
/// type should be skipped, for example used when encoding headers.
147+
/// - foundIn: The location where this content is parsed.
75148
/// - Returns: the detected content type + schema, nil if no supported
76149
/// schema found or if empty.
77150
func bestSingleContent(
@@ -123,4 +196,62 @@ extension FileTranslator {
123196
return nil
124197
}
125198
}
199+
200+
/// Returns a wrapped version of the provided content if supported, returns
201+
/// nil otherwise.
202+
///
203+
/// Priority of checking for known MIME types:
204+
/// 1. JSON
205+
/// 2. text
206+
/// 3. binary
207+
///
208+
/// - Parameters:
209+
/// - contentKey: The content key from the OpenAPI document.
210+
/// - contentValue: The content value from the OpenAPI document.
211+
/// - excludeBinary: A Boolean value controlling whether binary content
212+
/// type should be skipped, for example used when encoding headers.
213+
/// - foundIn: The location where this content is parsed.
214+
/// - Returns: The detected content type + schema, nil if unsupported.
215+
func parseContentIfSupported(
216+
contentKey: OpenAPI.ContentType,
217+
contentValue: OpenAPI.Content,
218+
excludeBinary: Bool = false,
219+
foundIn: String
220+
) -> SchemaContent? {
221+
if contentKey.isJSON,
222+
let contentType = ContentType(contentKey.typeAndSubtype)
223+
{
224+
diagnostics.emitUnsupportedIfNotNil(
225+
contentValue.encoding,
226+
"Custom encoding for JSON content",
227+
foundIn: "\(foundIn), content \(contentKey.rawValue)"
228+
)
229+
return .init(
230+
contentType: contentType,
231+
schema: contentValue.schema
232+
)
233+
}
234+
if contentKey.isText,
235+
let contentType = ContentType(contentKey.typeAndSubtype)
236+
{
237+
return .init(
238+
contentType: contentType,
239+
schema: .b(.string)
240+
)
241+
}
242+
if !excludeBinary,
243+
contentKey.isBinary,
244+
let contentType = ContentType(contentKey.typeAndSubtype)
245+
{
246+
return .init(
247+
contentType: contentType,
248+
schema: .b(.string(format: .binary))
249+
)
250+
}
251+
diagnostics.emitUnsupported(
252+
"Unsupported content",
253+
foundIn: foundIn
254+
)
255+
return nil
256+
}
126257
}

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,25 @@ extension FileTranslator {
2121
/// - Parameter contentType: The content type for which to compute the name.
2222
func contentSwiftName(_ contentType: ContentType) -> String {
2323
if config.featureFlags.contains(.multipleContentTypes) {
24-
return "unsupported"
24+
let rawMIMEType = contentType.rawMIMEType
25+
switch rawMIMEType {
26+
case "application/json":
27+
return "json"
28+
case "application/x-www-form-urlencoded":
29+
return "form"
30+
case "multipart/form-data":
31+
return "multipart"
32+
case "text/plain":
33+
return "text"
34+
case "*/*":
35+
return "any"
36+
case "application/xml":
37+
return "xml"
38+
case "application/octet-stream":
39+
return "binary"
40+
default:
41+
return swiftSafeName(for: rawMIMEType)
42+
}
2543
} else {
2644
switch contentType {
2745
case .json:

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ enum ContentType: Hashable {
4242
self = .binary(rawValue)
4343
}
4444

45+
/// Returns the original raw MIME type.
46+
var rawMIMEType: String {
47+
switch self {
48+
case .json(let string), .text(let string), .binary(let string):
49+
return string
50+
}
51+
}
52+
4553
/// The header value used when sending a content-type header.
4654
var headerValueForSending: String {
4755
switch self {
@@ -107,23 +115,10 @@ enum ContentType: Hashable {
107115
}
108116
return false
109117
}
110-
111-
/// Returns a new content type representing an octet stream.
112-
static var octetStream: Self {
113-
.binary("application/octet-stream")
114-
}
115-
116-
/// Returns a new content type representing JSON.
117-
static var applicationJSON: Self {
118-
.json("application/json")
119-
}
120118
}
121119

122120
extension OpenAPI.ContentType {
123121

124-
/// Returns a new content type representing an octet stream.
125-
static let octetStream: Self = .other(ContentType.octetStream.headerValueForValidation)
126-
127122
/// A Boolean value that indicates whether the content type
128123
/// is a type of JSON.
129124
var isJSON: Bool {

0 commit comments

Comments
 (0)