Skip to content

Commit 7b3f733

Browse files
authored
Fix a bug around optional request bodies (#322)
### Motivation As part of #288, we improved handling of nullability. Unfortunately that caused a regression for documents that use a somewhat strange combination of a required request body that has a nullable schema. ### Modifications Ensure that when generating the server and we have to spell out which type is being unwrapped, that we use the non-optional type name. This means that we respect the requestBody's `required` field, and ignore the `nullable` field of the root schema. To me, that seems like the most reasonable interpretation of the potential conflict. ### Result Unblocks one large OpenAPI document. ### Test Plan Added snippet tests.
1 parent 8954706 commit 7b3f733

File tree

2 files changed

+234
-1
lines changed

2 files changed

+234
-1
lines changed

Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,11 @@ extension ServerFileTranslator {
328328
.call([
329329
.init(
330330
label: nil,
331-
expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self")
331+
expression:
332+
.identifier(
333+
contentTypeUsage.fullyQualifiedNonOptionalSwiftName
334+
)
335+
.dot("self")
332336
),
333337
.init(label: "from", expression: .identifier("requestBody")),
334338
.init(

Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,235 @@ final class SnippetBasedReferenceTests: XCTestCase {
15351535
)
15361536
}
15371537

1538+
func testRequestRequiredBodyPrimitiveSchema() throws {
1539+
try self.assertRequestInTypesClientServerTranslation(
1540+
"""
1541+
/foo:
1542+
get:
1543+
requestBody:
1544+
required: true
1545+
content:
1546+
application/json:
1547+
schema:
1548+
type: string
1549+
responses:
1550+
default:
1551+
description: Response
1552+
""",
1553+
types: """
1554+
public struct Input: Sendable, Hashable {
1555+
@frozen public enum Body: Sendable, Hashable { case json(Swift.String) }
1556+
public var body: Operations.get_sol_foo.Input.Body
1557+
public init(body: Operations.get_sol_foo.Input.Body) { self.body = body }
1558+
}
1559+
""",
1560+
client: """
1561+
{ input in let path = try converter.renderedPath(template: "/foo", parameters: [])
1562+
var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get)
1563+
suppressMutabilityWarning(&request)
1564+
let body: OpenAPIRuntime.HTTPBody?
1565+
switch input.body {
1566+
case let .json(value):
1567+
body = try converter.setRequiredRequestBodyAsJSON(
1568+
value,
1569+
headerFields: &request.headerFields,
1570+
contentType: "application/json; charset=utf-8"
1571+
)
1572+
}
1573+
return (request, body)
1574+
}
1575+
""",
1576+
server: """
1577+
{ request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields)
1578+
let body: Operations.get_sol_foo.Input.Body
1579+
if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json")
1580+
{
1581+
body = try await converter.getRequiredRequestBodyAsJSON(
1582+
Swift.String.self,
1583+
from: requestBody,
1584+
transforming: { value in .json(value) }
1585+
)
1586+
} else {
1587+
throw converter.makeUnexpectedContentTypeError(contentType: contentType)
1588+
}
1589+
return Operations.get_sol_foo.Input(body: body)
1590+
}
1591+
"""
1592+
)
1593+
}
1594+
1595+
func testRequestRequiredBodyNullableSchema() throws {
1596+
try self.assertRequestInTypesClientServerTranslation(
1597+
"""
1598+
/foo:
1599+
get:
1600+
requestBody:
1601+
required: true
1602+
content:
1603+
application/json:
1604+
schema:
1605+
type: [string, null]
1606+
responses:
1607+
default:
1608+
description: Response
1609+
""",
1610+
types: """
1611+
public struct Input: Sendable, Hashable {
1612+
@frozen public enum Body: Sendable, Hashable { case json(Swift.String) }
1613+
public var body: Operations.get_sol_foo.Input.Body
1614+
public init(body: Operations.get_sol_foo.Input.Body) { self.body = body }
1615+
}
1616+
""",
1617+
client: """
1618+
{ input in let path = try converter.renderedPath(template: "/foo", parameters: [])
1619+
var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get)
1620+
suppressMutabilityWarning(&request)
1621+
let body: OpenAPIRuntime.HTTPBody?
1622+
switch input.body {
1623+
case let .json(value):
1624+
body = try converter.setRequiredRequestBodyAsJSON(
1625+
value,
1626+
headerFields: &request.headerFields,
1627+
contentType: "application/json; charset=utf-8"
1628+
)
1629+
}
1630+
return (request, body)
1631+
}
1632+
""",
1633+
server: """
1634+
{ request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields)
1635+
let body: Operations.get_sol_foo.Input.Body
1636+
if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json")
1637+
{
1638+
body = try await converter.getRequiredRequestBodyAsJSON(
1639+
Swift.String.self,
1640+
from: requestBody,
1641+
transforming: { value in .json(value) }
1642+
)
1643+
} else {
1644+
throw converter.makeUnexpectedContentTypeError(contentType: contentType)
1645+
}
1646+
return Operations.get_sol_foo.Input(body: body)
1647+
}
1648+
"""
1649+
)
1650+
}
1651+
1652+
func testRequestOptionalBodyPrimitiveSchema() throws {
1653+
try self.assertRequestInTypesClientServerTranslation(
1654+
"""
1655+
/foo:
1656+
get:
1657+
requestBody:
1658+
required: false
1659+
content:
1660+
application/json:
1661+
schema:
1662+
type: string
1663+
responses:
1664+
default:
1665+
description: Response
1666+
""",
1667+
types: """
1668+
public struct Input: Sendable, Hashable {
1669+
@frozen public enum Body: Sendable, Hashable { case json(Swift.String) }
1670+
public var body: Operations.get_sol_foo.Input.Body?
1671+
public init(body: Operations.get_sol_foo.Input.Body? = nil) { self.body = body }
1672+
}
1673+
""",
1674+
client: """
1675+
{ input in let path = try converter.renderedPath(template: "/foo", parameters: [])
1676+
var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get)
1677+
suppressMutabilityWarning(&request)
1678+
let body: OpenAPIRuntime.HTTPBody?
1679+
switch input.body {
1680+
case .none: body = nil
1681+
case let .json(value):
1682+
body = try converter.setOptionalRequestBodyAsJSON(
1683+
value,
1684+
headerFields: &request.headerFields,
1685+
contentType: "application/json; charset=utf-8"
1686+
)
1687+
}
1688+
return (request, body)
1689+
}
1690+
""",
1691+
server: """
1692+
{ request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields)
1693+
let body: Operations.get_sol_foo.Input.Body?
1694+
if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json")
1695+
{
1696+
body = try await converter.getOptionalRequestBodyAsJSON(
1697+
Swift.String.self,
1698+
from: requestBody,
1699+
transforming: { value in .json(value) }
1700+
)
1701+
} else {
1702+
throw converter.makeUnexpectedContentTypeError(contentType: contentType)
1703+
}
1704+
return Operations.get_sol_foo.Input(body: body)
1705+
}
1706+
"""
1707+
)
1708+
}
1709+
1710+
func testRequestOptionalBodyNullableSchema() throws {
1711+
try self.assertRequestInTypesClientServerTranslation(
1712+
"""
1713+
/foo:
1714+
get:
1715+
requestBody:
1716+
required: false
1717+
content:
1718+
application/json:
1719+
schema:
1720+
type: [string, null]
1721+
responses:
1722+
default:
1723+
description: Response
1724+
""",
1725+
types: """
1726+
public struct Input: Sendable, Hashable {
1727+
@frozen public enum Body: Sendable, Hashable { case json(Swift.String) }
1728+
public var body: Operations.get_sol_foo.Input.Body?
1729+
public init(body: Operations.get_sol_foo.Input.Body? = nil) { self.body = body }
1730+
}
1731+
""",
1732+
client: """
1733+
{ input in let path = try converter.renderedPath(template: "/foo", parameters: [])
1734+
var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get)
1735+
suppressMutabilityWarning(&request)
1736+
let body: OpenAPIRuntime.HTTPBody?
1737+
switch input.body {
1738+
case .none: body = nil
1739+
case let .json(value):
1740+
body = try converter.setOptionalRequestBodyAsJSON(
1741+
value,
1742+
headerFields: &request.headerFields,
1743+
contentType: "application/json; charset=utf-8"
1744+
)
1745+
}
1746+
return (request, body)
1747+
}
1748+
""",
1749+
server: """
1750+
{ request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields)
1751+
let body: Operations.get_sol_foo.Input.Body?
1752+
if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json")
1753+
{
1754+
body = try await converter.getOptionalRequestBodyAsJSON(
1755+
Swift.String.self,
1756+
from: requestBody,
1757+
transforming: { value in .json(value) }
1758+
)
1759+
} else {
1760+
throw converter.makeUnexpectedContentTypeError(contentType: contentType)
1761+
}
1762+
return Operations.get_sol_foo.Input(body: body)
1763+
}
1764+
"""
1765+
)
1766+
}
15381767
}
15391768

15401769
extension SnippetBasedReferenceTests {

0 commit comments

Comments
 (0)