Skip to content

Commit b59b8a3

Browse files
Emit JSON path reference in comments for all generated types (#196)
### Motivation Resolve #13 ### Modifications Applied commentable to the declaration variable in Traslator so that a comment in Types.swift is added indicating that it was generated from the Json path of the openapi document ### Result Some comments in Types.swift is generated, like `Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift`. ### Test Plan Modify Type.swift in the reference source --------- Co-authored-by: Honza Dvorsky <[email protected]>
1 parent 198880f commit b59b8a3

File tree

13 files changed

+320
-52
lines changed

13 files changed

+320
-52
lines changed

Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/CommentExtensions.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ extension TypeName {
9696
suffix: generatedFromCommentText
9797
)
9898
}
99+
100+
/// Returns a documentation comment by appending the "generated from"
101+
/// string to the specified user description.
102+
///
103+
/// The "generated from" string also includes a subpath.
104+
/// - Parameter userDescription: The description specified by the user.
105+
/// - Parameter subPath: A subpath appended to the JSON path of this
106+
/// type name.
107+
func docCommentWithUserDescription(_ userDescription: String?, subPath: String) -> Comment? {
108+
guard let fullyQualifiedJSONPath else {
109+
return Comment.doc(prefix: userDescription, suffix: nil)
110+
}
111+
return Comment.doc(
112+
prefix: userDescription,
113+
suffix: "- Remark: Generated from `\(fullyQualifiedJSONPath)/\(subPath)`."
114+
)
115+
}
99116
}
100117

101118
extension ResponseKind {
@@ -131,6 +148,30 @@ extension ResponseKind {
131148
}
132149
}
133150

151+
extension TypedParameter {
152+
/// Returns a documentation comment for the parameter.
153+
/// - Parameters:
154+
/// - parent: The parent type of the parameter.
155+
func docComment(parent: TypeName) -> Comment? {
156+
parent.docCommentWithUserDescription(
157+
nil,
158+
subPath: "\(parameter.location.rawValue)/\(parameter.name)"
159+
)
160+
}
161+
}
162+
163+
extension ContentType {
164+
/// Returns a documentation comment for the content type.
165+
/// - Parameters:
166+
/// - typeName: The type name of the content.
167+
func docComment(typeName: TypeName) -> Comment? {
168+
typeName.docCommentWithUserDescription(
169+
nil,
170+
subPath: lowercasedTypeAndSubtypeWithEscape
171+
)
172+
}
173+
}
174+
134175
extension Comment {
135176

136177
/// Returns a reference documentation string to attach to the generated function for an operation.

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ struct ContentType: Hashable {
143143
"\(lowercasedType)/\(lowercasedSubtype)"
144144
}
145145

146+
/// Returns the type and subtype as a "<type>\/<subtype>" string.
147+
///
148+
/// Lowercased to ease case-insensitive comparisons, and escaped to show
149+
/// that the slash between type and subtype is not a path separator.
150+
var lowercasedTypeAndSubtypeWithEscape: String {
151+
"\(lowercasedType)\\/\(lowercasedSubtype)"
152+
}
153+
146154
/// The header value used when sending a content-type header.
147155
var headerValueForSending: String {
148156
guard case .json = category else {

Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,7 @@ extension OperationDescription {
137137
var outputTypeName: TypeName {
138138
operationNamespace.appending(
139139
swiftComponent: Constants.Operation.Output.typeName,
140-
141-
// intentionally nil, we'll append the specific params etc
142-
// with their valid JSON key path when nested inside Output
143-
jsonComponent: nil
140+
jsonComponent: "responses"
144141
)
145142
}
146143

Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ extension TypesFileTranslator {
4444
associatedDeclarations = []
4545
}
4646
return .init(
47+
comment: parameter.docComment(parent: parent),
4748
isDeprecated: parameter.parameter.deprecated,
4849
originalName: parameter.name,
4950
typeUsage: parameter.typeUsage,

Sources/_OpenAPIGeneratorCore/Translator/RequestBody/TypedRequestBody.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ extension FileTranslator {
6464
type = try typeAssigner.typeName(for: reference)
6565
case .b:
6666
type = parent.appending(
67-
swiftComponent: Constants.Operation.Body.typeName
67+
swiftComponent: Constants.Operation.Body.typeName,
68+
jsonComponent: "requestBody"
6869
)
6970
}
7071
return try typedRequestBody(

Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ extension TypesFileTranslator {
4444
for requestBody: TypedRequestBody
4545
) throws -> [Declaration] {
4646
var bodyMembers: [Declaration] = []
47+
let typeName = requestBody.typeUsage.typeName
48+
let contentTypeName = typeName.appending(jsonComponent: "content")
4749
let contents = requestBody.contents
4850
for content in contents {
4951
if TypeMatcher.isInlinable(content.content.schema) {
@@ -52,10 +54,12 @@ extension TypesFileTranslator {
5254
)
5355
bodyMembers.append(contentsOf: inlineTypeDecls)
5456
}
55-
let identifier = contentSwiftName(content.content.contentType)
57+
let contentType = content.content.contentType
58+
let identifier = contentSwiftName(contentType)
5659
let associatedType = content.resolvedTypeUsage
57-
let contentCase: Declaration = .enumCase(
58-
.init(
60+
let contentCase: Declaration = .commentable(
61+
contentType.docComment(typeName: contentTypeName),
62+
.enumCase(
5963
name: identifier,
6064
kind: .nameWithAssociatedValues([
6165
.init(type: associatedType.fullyQualifiedNonOptionalSwiftName)
@@ -99,7 +103,8 @@ extension TypesFileTranslator {
99103
} else {
100104
isRequestBodyOptional = true
101105
bodyEnumTypeName = parent.appending(
102-
swiftComponent: Constants.Operation.Body.typeName
106+
swiftComponent: Constants.Operation.Body.typeName,
107+
jsonComponent: "requestBody"
103108
)
104109
extraDecls = [
105110
translateRequestBodyInTypes(
@@ -154,7 +159,11 @@ extension TypesFileTranslator {
154159
conformances: Constants.Operation.Output.conformances,
155160
members: members
156161
)
157-
return bodyEnumDecl
162+
let comment: Comment? = typeName.docCommentWithUserDescription(nil)
163+
return .commentable(
164+
comment,
165+
bodyEnumDecl
166+
)
158167
}
159168
}
160169

Sources/_OpenAPIGeneratorCore/Translator/Responses/ResponseKind.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ enum ResponseKind {
181181
/// Returns a new type name that appends the response's Swift name to
182182
/// the specified parent type name.
183183
func typeName(in parent: TypeName) -> TypeName {
184-
parent.appending(swiftComponent: prettyName.uppercasingFirstLetter)
184+
parent.appending(
185+
swiftComponent: prettyName.uppercasingFirstLetter,
186+
jsonComponent: jsonPathComponent
187+
)
185188
}
186189
}
187190

Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,26 @@ extension TypesFileTranslator {
2323
typeName: TypeName,
2424
response: TypedResponse
2525
) throws -> Declaration {
26-
2726
let response = response.response
28-
2927
let headersTypeName = typeName.appending(
30-
swiftComponent: Constants.Operation.Output.Payload.Headers.typeName
28+
swiftComponent: Constants.Operation.Output.Payload.Headers.typeName,
29+
jsonComponent: "headers"
3130
)
3231
let headers = try typedResponseHeaders(
3332
from: response,
3433
inParent: headersTypeName
3534
)
3635
let headerProperties: [PropertyBlueprint] = try headers.map { header in
37-
try parseResponseHeaderAsProperty(for: header)
36+
try parseResponseHeaderAsProperty(
37+
for: header,
38+
parent: headersTypeName
39+
)
3840
}
41+
let headerStructComment: Comment? =
42+
headersTypeName
43+
.docCommentWithUserDescription(nil)
3944
let headersStructBlueprint: StructBlueprint = .init(
40-
comment: nil,
45+
comment: headerStructComment,
4146
access: config.access,
4247
typeName: headersTypeName,
4348
conformances: Constants.Operation.Output.Payload.Headers.conformances,
@@ -58,15 +63,17 @@ extension TypesFileTranslator {
5863
)
5964

6065
let bodyTypeName = typeName.appending(
61-
swiftComponent: Constants.Operation.Body.typeName
66+
swiftComponent: Constants.Operation.Body.typeName,
67+
jsonComponent: "content"
6268
)
6369
let typedContents = try supportedTypedContents(
6470
response.content,
6571
inParent: bodyTypeName
6672
)
6773
var bodyCases: [Declaration] = []
6874
for typedContent in typedContents {
69-
let identifier = contentSwiftName(typedContent.content.contentType)
75+
let contentType = typedContent.content.contentType
76+
let identifier = contentSwiftName(contentType)
7077
let associatedType = typedContent.resolvedTypeUsage
7178
if TypeMatcher.isInlinable(typedContent.content.schema), let inlineType = typedContent.typeUsage {
7279
let inlineTypeDecls = try translateSchema(
@@ -76,22 +83,30 @@ extension TypesFileTranslator {
7683
)
7784
bodyCases.append(contentsOf: inlineTypeDecls)
7885
}
79-
let bodyCase: Declaration = .enumCase(
80-
name: identifier,
81-
kind: .nameWithAssociatedValues([
82-
.init(type: associatedType.fullyQualifiedSwiftName)
83-
])
86+
87+
let bodyCase: Declaration = .commentable(
88+
contentType.docComment(typeName: bodyTypeName),
89+
.enumCase(
90+
name: identifier,
91+
kind: .nameWithAssociatedValues([
92+
.init(type: associatedType.fullyQualifiedSwiftName)
93+
])
94+
)
8495
)
8596
bodyCases.append(bodyCase)
8697
}
8798
let hasNoContent: Bool = bodyCases.isEmpty
88-
let contentEnumDecl: Declaration = .enum(
89-
isFrozen: true,
90-
accessModifier: config.access,
91-
name: bodyTypeName.shortSwiftName,
92-
conformances: Constants.Operation.Body.conformances,
93-
members: bodyCases
99+
let contentEnumDecl: Declaration = .commentable(
100+
bodyTypeName.docCommentWithUserDescription(nil),
101+
.enum(
102+
isFrozen: true,
103+
accessModifier: config.access,
104+
name: bodyTypeName.shortSwiftName,
105+
conformances: Constants.Operation.Body.conformances,
106+
members: bodyCases
107+
)
94108
)
109+
95110
let contentTypeUsage = bodyTypeName.asUsage.withOptional(hasNoContent)
96111
let contentProperty = PropertyBlueprint(
97112
comment: .doc("Received HTTP response body"),

Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ extension TypesFileTranslator {
2020
///
2121
/// - Parameters:
2222
/// - header: A response parameter.
23+
/// - parent: The type name of the parent struct.
2324
/// - Returns: A property blueprint.
2425
func parseResponseHeaderAsProperty(
25-
for header: TypedResponseHeader
26+
for header: TypedResponseHeader,
27+
parent: TypeName
2628
) throws -> PropertyBlueprint {
29+
let comment = parent.docCommentWithUserDescription(
30+
nil,
31+
subPath: header.name
32+
)
2733
return .init(
28-
comment: nil,
34+
comment: comment,
2935
originalName: header.name,
3036
typeUsage: header.typeUsage,
3137
default: header.header.required ? nil : .nil,

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeName.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ struct TypeName: Equatable {
112112
/// - Parameters:
113113
/// - swiftComponent: The name of the Swift type component.
114114
/// - jsonComponent: The name of the JSON path component.
115+
/// - Precondition: At least one of the components must be non-nil.
115116
/// - Returns: A new type name.
116-
func appending(swiftComponent: String, jsonComponent: String? = nil) -> Self {
117+
func appending(swiftComponent: String? = nil, jsonComponent: String? = nil) -> Self {
118+
precondition(swiftComponent != nil || jsonComponent != nil, "At least the Swift or JSON name must be non-nil.")
117119
let newComponent = Component(swift: swiftComponent, json: jsonComponent)
118120
return .init(components: components + [newComponent])
119121
}

0 commit comments

Comments
 (0)