Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multipart HTTP request bodies #514

Merged
merged 3 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1565,30 +1565,38 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
var keysByTarget = [String: [DictionaryKey]]()
var parametersByTarget = [String: [HTTPParameter]]()
var bodyByTarget = [String: HTTPBody]()
var bodyParametersByTarget = [String: [HTTPParameter]]()
var responsesByTarget = [String: [HTTPResponse]]()

for edge in relationships {
if edge.kind == .memberOf || edge.kind == .optionalMemberOf {
if let source = symbolIndex[edge.source], let _ = symbolIndex[edge.target], let sourceSymbol = source.symbol {
switch source.kind {
case .dictionaryKey:
if let source = symbolIndex[edge.source], let target = symbolIndex[edge.target], let sourceSymbol = source.symbol {
switch (source.kind, target.kind) {
case (.dictionaryKey, .dictionary):
let dictionaryKey = DictionaryKey(name: sourceSymbol.title, contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
if keysByTarget[edge.target] == nil {
keysByTarget[edge.target] = [dictionaryKey]
} else {
keysByTarget[edge.target]?.append(dictionaryKey)
}
case .httpParameter:
case (.httpParameter, .httpRequest):
let parameter = HTTPParameter(name: sourceSymbol.title, source: (sourceSymbol.httpParameterSource ?? "query"), contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
if parametersByTarget[edge.target] == nil {
parametersByTarget[edge.target] = [parameter]
} else {
parametersByTarget[edge.target]?.append(parameter)
}
case .httpBody:
case (.httpBody, .httpRequest):
let body = HTTPBody(mediaType: sourceSymbol.httpMediaType, contents: [], symbol: sourceSymbol)
bodyByTarget[edge.target] = body
case .httpResponse:
case (.httpParameter, .httpBody):
let parameter = HTTPParameter(name: sourceSymbol.title, source: "body", contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
if bodyParametersByTarget[edge.target] == nil {
bodyParametersByTarget[edge.target] = [parameter]
} else {
bodyParametersByTarget[edge.target]?.append(parameter)
}
case (.httpResponse, .httpRequest):
let statusParts = sourceSymbol.title.split(separator: " ", maxSplits: 1)
let statusCode = UInt(statusParts[0]) ?? 0
let reason = statusParts.count > 1 ? String(statusParts[1]) : nil
Expand All @@ -1598,19 +1606,20 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
} else {
responsesByTarget[edge.target]?.append(response)
}
default:
case (_, _):
continue
}
}
}
}

let trait = DocumentationDataVariantsTrait(for: selector)

// Merge in all the dictionary keys for each target into their section variants.
keysByTarget.forEach { targetIdentifier, keys in
let target = symbolIndex[targetIdentifier]
if let semantic = target?.semantic as? Symbol {
let keys = keys.sorted { $0.name < $1.name }
let trait = DocumentationDataVariantsTrait(for: selector)
if semantic.dictionaryKeysSectionVariants[trait] == nil {
semantic.dictionaryKeysSectionVariants[trait] = DictionaryKeysSection(dictionaryKeys: keys)
} else {
Expand All @@ -1624,7 +1633,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
let target = symbolIndex[targetIdentifier]
if let semantic = target?.semantic as? Symbol {
let parameters = parameters.sorted { $0.name < $1.name }
let trait = DocumentationDataVariantsTrait(for: selector)
if semantic.httpParametersSectionVariants[trait] == nil {
semantic.httpParametersSectionVariants[trait] = HTTPParametersSection(parameters: parameters)
} else {
Expand All @@ -1637,11 +1645,15 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
bodyByTarget.forEach { targetIdentifier, body in
let target = symbolIndex[targetIdentifier]
if let semantic = target?.semantic as? Symbol {
let trait = DocumentationDataVariantsTrait(for: selector)
// Add any body parameters to existing body record
var localBody = body
if let identifier = body.symbol?.preciseIdentifier, let bodyParameters = bodyParametersByTarget[identifier] {
localBody.parameters = bodyParameters.sorted { $0.name < $1.name }
}
if semantic.httpBodySectionVariants[trait] == nil {
semantic.httpBodySectionVariants[trait] = HTTPBodySection(body: body)
semantic.httpBodySectionVariants[trait] = HTTPBodySection(body: localBody)
} else {
semantic.httpBodySectionVariants[trait]?.mergeBody(body)
semantic.httpBodySectionVariants[trait]?.mergeBody(localBody)
}
}
}
Expand All @@ -1651,7 +1663,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
let target = symbolIndex[targetIdentifier]
if let semantic = target?.semantic as? Symbol {
let responses = responses.sorted { $0.statusCode < $1.statusCode }
let trait = DocumentationDataVariantsTrait(for: selector)
if semantic.httpResponsesSectionVariants[trait] == nil {
semantic.httpResponsesSectionVariants[trait] = HTTPResponsesSection(responses: responses)
} else {
Expand Down
88 changes: 88 additions & 0 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1741,6 +1741,94 @@ public struct RenderNodeTranslator: SemanticVisitor {
}
}

/// Generate a RenderProperty object from markup content and symbol data.
mutating func createRenderProperty(name: String, contents: [Markup], required: Bool, symbol: SymbolGraph.Symbol?) -> RenderProperty {
let parameterContent = self.visitMarkupContainer(
MarkupContainer(contents)
) as! [RenderBlockContent]

var renderedTokens: [DeclarationRenderSection.Token]? = nil
var attributes: [RenderAttribute] = []
var isReadOnly: Bool? = nil
var deprecated: Bool? = nil
var introducedVersion: String? = nil

if let symbol = symbol {
// Convert the dictionary key's declaration into section tokens
if let fragments = symbol.declarationFragments {
renderedTokens = fragments.map { token -> DeclarationRenderSection.Token in

// Create a reference if one found
var reference: ResolvedTopicReference?
if let preciseIdentifier = token.preciseIdentifier,
let resolved = self.context.symbolIndex[preciseIdentifier]?.reference {
reference = resolved

// Add relationship to render references
self.collectedTopicReferences.append(resolved)
}

// Add the declaration token
return DeclarationRenderSection.Token(fragment: token, identifier: reference?.absoluteString)
}
}

// Populate attributes
if let constraint = symbol.defaultValue {
attributes.append(RenderAttribute.default(String(constraint)))
}
if let constraint = symbol.minimum {
attributes.append(RenderAttribute.minimum(String(constraint)))
}
if let constraint = symbol.maximum {
attributes.append(RenderAttribute.maximum(String(constraint)))
}
if let constraint = symbol.minimumExclusive {
attributes.append(RenderAttribute.minimumExclusive(String(constraint)))
}
if let constraint = symbol.maximumExclusive {
attributes.append(RenderAttribute.maximumExclusive(String(constraint)))
}
if let constraint = symbol.allowedValues {
attributes.append(RenderAttribute.allowedValues(constraint.map{String($0)}))
}
if let constraint = symbol.isReadOnly {
isReadOnly = constraint
}
if let constraint = symbol.minimumLength {
attributes.append(RenderAttribute.minimumLength(String(constraint)))
}
if let constraint = symbol.maximumLength {
attributes.append(RenderAttribute.maximumLength(String(constraint)))
}

// Extract the availability information
if let availabilityItems = symbol.availability, availabilityItems.count > 0 {
availabilityItems.forEach { item in
if deprecated == nil && (item.isUnconditionallyDeprecated || item.deprecatedVersion != nil) {
deprecated = true
}
if let intro = item.introducedVersion, introducedVersion == nil {
introducedVersion = "\(intro)"
}
}
}
}

return RenderProperty(
name: name,
type: renderedTokens ?? [],
typeDetails: nil,
content: parameterContent,
attributes: attributes,
mimeType: symbol?.httpMediaType,
required: required,
deprecated: deprecated,
readOnly: isReadOnly,
introducedVersion: introducedVersion
)
}

init(
context: DocumentationContext,
bundle: DocumentationBundle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,98 +27,8 @@ struct DictionaryKeysSectionTranslator: RenderSectionTranslator {

return PropertiesRenderSection(
title: DictionaryKeysSection.title,
items: filteredKeys.map { translateDictionaryKey($0, &renderNodeTranslator) }
items: filteredKeys.map { renderNodeTranslator.createRenderProperty(name: $0.name, contents: $0.contents, required: $0.required, symbol: $0.symbol) }
)
}
}

func translateDictionaryKey(_ key: DictionaryKey, _ renderNodeTranslator: inout RenderNodeTranslator) -> RenderProperty {
let keyContent = renderNodeTranslator.visitMarkupContainer(
MarkupContainer(key.contents)
) as! [RenderBlockContent]

var required : Bool? = nil
var renderedTokens : [DeclarationRenderSection.Token]? = nil
var attributes : [RenderAttribute] = []
var isReadOnly : Bool? = nil
var deprecated: Bool? = nil
var introducedVersion: String? = nil

if let keySymbol = key.symbol {
required = key.required

// Convert the dictionary key's declaration into section tokens
if let fragments = keySymbol.declarationFragments {
renderedTokens = fragments.map { token -> DeclarationRenderSection.Token in

// Create a reference if one found
var reference: ResolvedTopicReference?
if let preciseIdentifier = token.preciseIdentifier,
let resolved = renderNodeTranslator.context.symbolIndex[preciseIdentifier]?.reference {
reference = resolved

// Add relationship to render references
renderNodeTranslator.collectedTopicReferences.append(resolved)
}

// Add the declaration token
return DeclarationRenderSection.Token(fragment: token, identifier: reference?.absoluteString)
}
}

// Populate attributes
if let constraint = keySymbol.defaultValue {
attributes.append(RenderAttribute.default(String(constraint)))
}
if let constraint = keySymbol.minimum {
attributes.append(RenderAttribute.minimum(String(constraint)))
}
if let constraint = keySymbol.maximum {
attributes.append(RenderAttribute.maximum(String(constraint)))
}
if let constraint = keySymbol.minimumExclusive {
attributes.append(RenderAttribute.minimumExclusive(String(constraint)))
}
if let constraint = keySymbol.maximumExclusive {
attributes.append(RenderAttribute.maximumExclusive(String(constraint)))
}
if let constraint = keySymbol.allowedValues {
attributes.append(RenderAttribute.allowedValues(constraint.map{String($0)}))
}
if let constraint = keySymbol.isReadOnly {
isReadOnly = constraint
}
if let constraint = keySymbol.minimumLength {
attributes.append(RenderAttribute.minimumLength(String(constraint)))
}
if let constraint = keySymbol.maximumLength {
attributes.append(RenderAttribute.maximumLength(String(constraint)))
}

// Extract the availability information
if let availabilityItems = keySymbol.availability, availabilityItems.count > 0 {
availabilityItems.forEach { item in
if deprecated == nil && (item.isUnconditionallyDeprecated || item.deprecatedVersion != nil) {
deprecated = true
}
if let intro = item.introducedVersion, introducedVersion == nil {
introducedVersion = "\(intro)"
}
}
}
}

return RenderProperty(
name: key.name,
type: renderedTokens ?? [],
typeDetails: nil,
content: keyContent,
attributes: attributes,
mimeType: nil,
required: required,
deprecated: deprecated,
readOnly: isReadOnly,
introducedVersion: introducedVersion
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ struct HTTPBodySectionTranslator: RenderSectionTranslator {
) { _, httpBodySection -> RenderSection? in
guard let symbol = httpBodySection.body.symbol, let mediaType = httpBodySection.body.mediaType else { return nil }

let responseContent = renderNodeTranslator.visitMarkupContainer(
// Filter out parameters that aren't backed by a symbol or don't have a "body" source.
let filteredParameters = httpBodySection.body.parameters.filter { $0.symbol != nil && $0.source == "body" }

let bodyContent = renderNodeTranslator.visitMarkupContainer(
MarkupContainer(httpBodySection.body.contents)
) as! [RenderBlockContent]

Expand All @@ -45,8 +48,8 @@ struct HTTPBodySectionTranslator: RenderSectionTranslator {
title: "HTTP Body",
mimeType: mediaType,
bodyContentType: renderedTokens ?? [],
content: responseContent,
parameters: nil // TODO: Support body parameters
content: bodyContent,
parameters: filteredParameters.map { renderNodeTranslator.createRenderProperty(name: $0.name, contents: $0.contents, required: $0.required, symbol: $0.symbol) }
)
}
}
Expand Down
Loading