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

Populate the "Possible Values" section based on OAS data #857

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions Sources/SwiftDocC/Model/DocumentationNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ public struct DocumentationNode {
returnsSectionVariants: .empty,
parametersSectionVariants: .empty,
dictionaryKeysSectionVariants: .empty,
possibleValuesSectionVariants: .empty,
httpEndpointSectionVariants: endpointVariants,
httpBodySectionVariants: .empty,
httpParametersSectionVariants: .empty,
Expand Down Expand Up @@ -389,6 +390,12 @@ public struct DocumentationNode {
semantic.dictionaryKeysSectionVariants[.fallback] = DictionaryKeysSection(dictionaryKeys:keys)
}

if let values = markupModel.discussionTags?.possibleValues, !values.isEmpty {
// Record the values extracted from the markdown and the complete set defined by the symbol
let section = PossibleValuesSection(documentedValues: values, definedValues: symbol?.allowedValues ?? [])
semantic.possibleValuesSectionVariants[.fallback] = section
}

if let parameters = markupModel.discussionTags?.httpParameters, !parameters.isEmpty {
// Record the parameters extracted from the markdown
semantic.httpParametersSectionVariants[.fallback] = HTTPParametersSection(parameters: parameters)
Expand Down Expand Up @@ -654,6 +661,7 @@ public struct DocumentationNode {
returnsSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.returns.isEmpty ? nil : ReturnsSection(content: $0.returns[0].contents) })),
parametersSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.parameters.isEmpty ? nil : ParametersSection(parameters: $0.parameters) })),
dictionaryKeysSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.dictionaryKeys.isEmpty ? nil : DictionaryKeysSection(dictionaryKeys: $0.dictionaryKeys) })),
possibleValuesSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.possibleValues.isEmpty ? nil : PossibleValuesSection(documentedValues: $0.possibleValues, definedValues: []) })),
httpEndpointSectionVariants: .empty,
httpBodySectionVariants: .empty,
httpParametersSectionVariants: .empty,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
HTTPBodySectionTranslator(),
HTTPResponsesSectionTranslator(),
DictionaryKeysSectionTranslator(),
PossibleValuesSectionTranslator(),
AttributesSectionTranslator(),
ReturnsSectionTranslator(),
MentionsSectionTranslator(referencingSymbol: identifier),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

/// Translates a symbol's possible values into a render node's Possible Values section.
struct PossibleValuesSectionTranslator: RenderSectionTranslator {
func translateSection(
for symbol: Symbol,
renderNode: inout RenderNode,
renderNodeTranslator: inout RenderNodeTranslator
) -> VariantCollection<CodableContentSection?>? {
translateSectionToVariantCollection(
documentationDataVariants: symbol.possibleValuesSectionVariants
) { _, possibleValuesSection in
// Render section only if values were listed in the markdown
// and there are value defined in the symbol graph.
guard !possibleValuesSection.documentedValues.isEmpty else { return nil }
guard !possibleValuesSection.definedValues.isEmpty else { return nil }

// Build a lookup table of the documented values
var documentationLookup = [String: PossibleValue]()
possibleValuesSection.documentedValues.forEach { documentationLookup[$0.value] = $0 }

// Generate list of possible values for rendering from the full list of defined values,
// pulling in any documentation from the documented values list when available.
let renderedValues = possibleValuesSection.definedValues.map {
let valueString = String($0)
let possibleValue = documentationLookup[valueString] ?? PossibleValue(value: valueString, contents: [])
let valueContent = renderNodeTranslator.visitMarkupContainer(
MarkupContainer(possibleValue.contents)
) as! [RenderBlockContent]
return PossibleValuesRenderSection.NamedValue(name: possibleValue.value, content: valueContent)
}

return PossibleValuesRenderSection(
title: PossibleValuesSection.title,
values: renderedValues
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import SymbolKit

/// A section that contains an type's possible values.
public struct PossibleValuesSection {
public static var title: String {
return "Possible Values"
}

/// The list of possible values.
public var documentedValues: [PossibleValue]

/// The list of defined values for the symbol.
public var definedValues: [SymbolGraph.AnyScalar]
Comment on lines +20 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the structure of this type. Is there any relation between the documentedValues and definedValues? It looks to me like each documented values's value may(?) correspond to one of the defined values? If that's correct, would it be better to model this like [SymbolGraph.AnyScalar: [Markup]?] or something along those lines?

It's hard and takes a long time to change a public API after it's been added. If we aren't sure that we have the right API then we shouldn't make it public yet.

}
20 changes: 20 additions & 0 deletions Sources/SwiftDocC/Model/Semantics/PossibleValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2023 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Markdown
import SymbolKit

/// Documentation about the possible values for a symbol.
public struct PossibleValue {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems only relevant within PossibleValuesSection. Is it possible to make it a nested type instead of adding a top-level type with such a general name?

/// The string representation of the value.
public var value: String
/// The content that describe the value.
public var contents: [Markup]
}
7 changes: 7 additions & 0 deletions Sources/SwiftDocC/Semantics/ReferenceResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,12 @@ struct ReferenceResolver: SemanticVisitor {
}
return DictionaryKeysSection(dictionaryKeys: keys)
}
let newPossibleValuesVariants = symbol.possibleValuesSectionVariants.map { possibleValuesSection -> PossibleValuesSection in
let keys = possibleValuesSection.documentedValues.map {
PossibleValue(value: $0.value, contents: $0.contents.map { visitMarkup($0) })
}
return PossibleValuesSection(documentedValues: keys, definedValues: possibleValuesSection.definedValues)
}
let newHTTPEndpointVariants = symbol.httpEndpointSectionVariants.map { httpEndpointSection -> HTTPEndpointSection in
return HTTPEndpointSection(endpoint: httpEndpointSection.endpoint)
}
Expand Down Expand Up @@ -499,6 +505,7 @@ struct ReferenceResolver: SemanticVisitor {
returnsSectionVariants: newReturnsVariants,
parametersSectionVariants: newParametersVariants,
dictionaryKeysSectionVariants: newDictionaryKeysVariants,
possibleValuesSectionVariants: newPossibleValuesVariants,
httpEndpointSectionVariants: newHTTPEndpointVariants,
httpBodySectionVariants: newHTTPBodyVariants,
httpParametersSectionVariants: newHTTPParametersVariants,
Expand Down
8 changes: 7 additions & 1 deletion Sources/SwiftDocC/Semantics/Symbol/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import SymbolKit
/// - ``accessLevelVariants``
/// - ``deprecatedSummaryVariants``
/// - ``declarationVariants``
/// - ``possibleValuesVariants``
/// - ``attributesVariants``
/// - ``locationVariants``
/// - ``constraintsVariants``
Expand Down Expand Up @@ -149,7 +150,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
/// The symbol's alternate declarations in each language variant the symbol is available in.
public var alternateDeclarationVariants = DocumentationDataVariants<[[PlatformName?]: [SymbolGraph.Symbol.DeclarationFragments]]>()

/// The symbol's possible values in each language variant the symbol is available in.
/// The symbol's set of attributes in each language variant the symbol is available in.
public var attributesVariants = DocumentationDataVariants<[RenderAttribute.Kind: Any]>()

public var locationVariants = DocumentationDataVariants<SymbolGraph.Symbol.Location>()
Expand Down Expand Up @@ -206,6 +207,9 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
/// Any dictionary keys of the symbol, if the symbol accepts keys, in each language variant the symbol is available in.
public var dictionaryKeysSectionVariants: DocumentationDataVariants<DictionaryKeysSection>

/// The symbol's possible values in each language variant the symbol is available in.
public var possibleValuesSectionVariants = DocumentationDataVariants<PossibleValuesSection>()

/// The HTTP endpoint of an HTTP request, in each language variant the symbol is available in.
public var httpEndpointSectionVariants: DocumentationDataVariants<HTTPEndpointSection>

Expand Down Expand Up @@ -275,6 +279,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
returnsSectionVariants: DocumentationDataVariants<ReturnsSection>,
parametersSectionVariants: DocumentationDataVariants<ParametersSection>,
dictionaryKeysSectionVariants: DocumentationDataVariants<DictionaryKeysSection>,
possibleValuesSectionVariants: DocumentationDataVariants<PossibleValuesSection>,
httpEndpointSectionVariants: DocumentationDataVariants<HTTPEndpointSection>,
httpBodySectionVariants: DocumentationDataVariants<HTTPBodySection>,
httpParametersSectionVariants: DocumentationDataVariants<HTTPParametersSection>,
Expand Down Expand Up @@ -304,6 +309,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups

self.deprecatedSummaryVariants = deprecatedSummaryVariants
self.declarationVariants = declarationVariants
self.possibleValuesSectionVariants = possibleValuesSectionVariants

self.mixinsVariants = mixinsVariants

Expand Down
95 changes: 91 additions & 4 deletions Sources/SwiftDocC/Utility/MarkupExtensions/ListItemExtractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ let simpleListItemTags = [
]

extension Sequence where Element == InlineMarkup {
private func splitNameAndContent() -> (name: String, nameRange: SourceRange?, content: [Markup], range: SourceRange?)? {
private func splitNameAndContent(skipStartingTag: Bool = false) -> (name: String, nameRange: SourceRange?, content: [Markup], range: SourceRange?)? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why is this parameter needed? If the code scans up to the first colon then does it matter if the line looks like

- one two: Description 

or

- one: Description 

?

var iterator = makeIterator()
guard let initialTextNode = iterator.next() as? Text else {
return nil
Expand All @@ -59,8 +59,16 @@ extension Sequence where Element == InlineMarkup {
guard let colonIndex = initialText.firstIndex(of: ":") else {
return nil
}

let nameStartIndex = initialText[...colonIndex].lastIndex(of: " ").map { initialText.index(after: $0) } ?? initialText.startIndex

let nameStartIndex: String.Index
if skipStartingTag == true, var spaceIndex = initialText[...colonIndex].firstIndex(of: " ") {
while initialText[spaceIndex] == " " {
spaceIndex = initialText.index(spaceIndex, offsetBy: 1)
}
Comment on lines +65 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly manipulating a string index and using it to access the content in a loop like this is a bad way of working with Strings in Swift. It's possibly accidentally quadratic because the characters are not known to be the same number of bytes.

I think this is trying to do something like the below to find the start of the next if there are multiple spaces between the colon and the word

let nameStartIndex = initialText[...colonIndex].drop(while: { $0 != " " }).dropFirst().firstIndex(where: { $0 != " " }) 
    ?? initialText.startIndex

in which case it might be more robust to use $0.isWhitespace to also handle tabs and other whitespace characters.

let nameStartIndex = initialText[...colonIndex].drop(while: { !$0.isWhitespace }).dropFirst().firstIndex(where: { !$0.isWhitespace }) 
    ?? initialText.startIndex

Regardless. It would be good to add a short comment to describe why this is happening.

nameStartIndex = spaceIndex
} else {
nameStartIndex = initialText.startIndex
}
let parameterName = initialText[nameStartIndex..<colonIndex]
guard !parameterName.isEmpty else {
return nil
Expand Down Expand Up @@ -93,7 +101,7 @@ extension Sequence where Element == InlineMarkup {
}

func extractParameter(standalone: Bool) -> Parameter? {
if let (name, nameRange, content, itemRange) = splitNameAndContent() {
if let (name, nameRange, content, itemRange) = splitNameAndContent(skipStartingTag: standalone == true) {
return Parameter(name: name, nameRange: nameRange, contents: content, range: itemRange, isStandalone: standalone)
}
return nil
Expand All @@ -106,6 +114,13 @@ extension Sequence where Element == InlineMarkup {
return nil
}

func extractPossibleValue() -> PossibleValue? {
if let (value, _, content, _) = splitNameAndContent() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New code should capture and use the nameRange and itemRange. Otherwise diagnostics for this content can't be associated with the file where the content comes from.

It would also be good to add a test that verifies that diagnostics in the possible value content are presented with the relevant source and range information.

return PossibleValue(value: value, contents: content)
}
return nil
}

func extractHTTPParameter() -> HTTPParameter? {
if let (name, _, content, _) = splitNameAndContent() {
return HTTPParameter(name: name, source: nil, contents: content)
Expand Down Expand Up @@ -237,6 +252,65 @@ extension ListItem {
return dictionaryKeys
}

/**
Extract a standalone possible value description from this list item.

Expected form:

```markdown
- possibleValue x: The meaning of x
```
*/
func extractStandalonePossibleValue() -> PossibleValue? {
guard let remainder = extractTag(TaggedListItemExtractor.possibleValueTag) else {
return nil
}
return remainder.extractPossibleValue()
}

/**
Extracts an outline of possible values from a sublist underneath this list item.

Expected form:

```markdown
- PossibleValues:
- x: Meaning of x
- y: Meaning of y
```

> Warning: Content underneath `- PossibleValues` that doesn't match this form will be dropped.
*/
func extractPossibleValueOutline() -> [PossibleValue]? {
guard extractTag(TaggedListItemExtractor.possibleValuesTag + ":") != nil else {
return nil
}

var possibleValues = [PossibleValue]()

for child in children {
// The list `- PossibleValues:` should have one child, a list of values.
guard let possibleValuesList = child as? UnorderedList else {
// If it's not, that content is dropped.
continue
}

// Those sublist items are assumed to be a valid `- ___: ...` possible value form or else they are dropped.
for child in possibleValuesList.children {
guard let listItem = child as? ListItem,
let firstParagraph = listItem.child(at: 0) as? Paragraph,
let possibleValue = Array(firstParagraph.inlineChildren).extractPossibleValue() else {
continue
}
// Don't forget the rest of the content under this possible value list item.
let contents = possibleValue.contents + Array(listItem.children.dropFirst(1))

possibleValues.append(PossibleValue(value: possibleValue.value, contents: contents))
}
}
return possibleValues
}

/**
Extract a standalone HTTP parameter description from this list item.

Expand Down Expand Up @@ -530,6 +604,8 @@ struct TaggedListItemExtractor: MarkupRewriter {
static let parametersTag = "parameters"
static let dictionaryKeyTag = "dictionarykey"
static let dictionaryKeysTag = "dictionarykeys"
static let possibleValueTag = "possiblevalue"
static let possibleValuesTag = "possiblevalues"

static let httpBodyTag = "httpbody"
static let httpResponseTag = "httpresponse"
Expand All @@ -540,6 +616,7 @@ struct TaggedListItemExtractor: MarkupRewriter {
static let httpBodyParametersTag = "httpbodyparameters"

var parameters = [Parameter]()
var possibleValues = [PossibleValue]()
var dictionaryKeys = [DictionaryKey]()
var httpResponses = [HTTPResponse]()
var httpParameters = [HTTPParameter]()
Expand Down Expand Up @@ -643,6 +720,16 @@ struct TaggedListItemExtractor: MarkupRewriter {
// - dictionaryKey x: ...
dictionaryKeys.append(dictionaryKeyDescription)
return nil
} else if let possibleValueDescription = listItem.extractPossibleValueOutline() {
// - PossibleValues:
// - x: ...
// - y: ...
possibleValues.append(contentsOf: possibleValueDescription)
return nil
} else if let possibleValueDescription = listItem.extractStandalonePossibleValue() {
// - possibleValue x: ...
possibleValues.append(possibleValueDescription)
return nil
} else if let httpParameterDescription = listItem.extractHTTPParameterOutline() {
// - HTTPParameters:
// - x: ...
Expand Down