Skip to content

Support beta status in navigator #1249

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

Open
wants to merge 6 commits 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,8 @@ extension NavigatorIndex {
platformMask: platformID,
availabilityID: UInt64(availabilityID),
icon: renderNode.icon,
isExternal: external
isExternal: external,
isBeta: renderNode.metadata.isBeta
)
navigationItem.path = identifierPath

Expand Down
67 changes: 63 additions & 4 deletions Sources/SwiftDocC/Indexing/Navigator/NavigatorItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString

var icon: RenderReferenceIdentifier? = nil

/// A value that indicates whether this item is built for a beta platform.
///
/// This value is `false` if the referenced item is not a symbol.
var isBeta: Bool = false

/// Whether the item has originated from an external reference.
///
/// Used for determining whether stray navigation items should remain part of the final navigator.
Expand All @@ -66,7 +71,7 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
- path: The path to load the content.
- icon: A reference to a custom image for this navigator item.
*/
init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, path: String, icon: RenderReferenceIdentifier? = nil, isExternal: Bool = false) {
init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, path: String, icon: RenderReferenceIdentifier? = nil, isExternal: Bool = false, isBeta: Bool = false) {
self.pageType = pageType
self.languageID = languageID
self.title = title
Expand All @@ -75,6 +80,7 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
self.path = path
self.icon = icon
self.isExternal = isExternal
self.isBeta = isBeta
}

/**
Expand All @@ -87,15 +93,18 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
- platformMask: The mask indicating for which platform the page is available.
- availabilityID: The identifier of the availability information of the page.
- icon: A reference to a custom image for this navigator item.
- isExternal: A flag indicating whether the navigator item belongs to an external documentation archive.
- isBeta: A flag indicating whether the navigator item is in beta.
*/
public init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, icon: RenderReferenceIdentifier? = nil, isExternal: Bool = false) {
public init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, icon: RenderReferenceIdentifier? = nil, isExternal: Bool = false, isBeta: Bool = false) {
self.pageType = pageType
self.languageID = languageID
self.title = title
self.platformMask = platformMask
self.availabilityID = availabilityID
self.icon = icon
self.isExternal = isExternal
self.isBeta = isBeta
}

// MARK: - Serialization and Deserialization
Expand Down Expand Up @@ -137,8 +146,27 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString

let pathData = data[cursor..<cursor + Int(pathLength)]
self.path = String(data: pathData, encoding: .utf8)!
cursor += Int(pathLength)

assert(cursor+Int(pathLength) == data.count)
// isBeta and isExternal should be encoded because they are relevant when creating a RenderIndex node.
// Without proper serialization, these indicators would be lost when navigator indexes are loaded from disk.

length = MemoryLayout<UInt8>.stride
// To ensure backwards compatibility, handle both when `isBeta` has been encoded and when it hasn't
if cursor < data.count {
let betaValue: UInt8 = unpackedValueFromData(data[cursor..<cursor + length])
cursor += length
self.isBeta = betaValue != 0
}

// To ensure backwards compatibility, handle both when `isExternal` has been encoded and when it hasn't
if cursor < data.count {
let externalValue: UInt8 = unpackedValueFromData(data[cursor..<cursor + length])
cursor += length
self.isExternal = externalValue != 0
}

assert(cursor == data.count)
}

/// Returns the `Data` representation of the current `NavigatorItem` instance.
Expand All @@ -155,9 +183,38 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
data.append(Data(title.utf8))
data.append(Data(path.utf8))

data.append(packedDataFromValue(isBeta ? UInt8(1) : UInt8(0)))
data.append(packedDataFromValue(isExternal ? UInt8(1) : UInt8(0)))

return data
}

// MARK: - Equatable
// Needed because a Swift class's synthesized Equatable conformance doesn't take into account properties which have default values as part of the designated initializer.

public static func == (lhs: NavigatorItem, rhs: NavigatorItem) -> Bool {
return lhs.pageType == rhs.pageType &&
lhs.languageID == rhs.languageID &&
lhs.title == rhs.title &&
lhs.platformMask == rhs.platformMask &&
lhs.availabilityID == rhs.availabilityID &&
lhs.isBeta == rhs.isBeta &&
lhs.isExternal == rhs.isExternal
}

// MARK: - Hashable
// Needed because a Swift class's synthesized Hashable conformance doesn't take into account properties which have default values as part of the designated initializer.

public func hash(into hasher: inout Hasher) {
hasher.combine(pageType)
hasher.combine(languageID)
hasher.combine(title)
hasher.combine(platformMask)
hasher.combine(availabilityID)
hasher.combine(isBeta)
hasher.combine(isExternal)
}

// MARK: - Description

public var description: String {
Expand All @@ -167,7 +224,9 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
languageID: \(languageID),
title: \(title),
platformMask: \(platformMask),
availabilityID: \(availabilityID)
availabilityID: \(availabilityID),
isBeta: \(isBeta),
isExternal: \(isExternal)
}
"""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ protocol NavigatorIndexableRenderMetadataRepresentation {
var roleHeading: String? { get }
var symbolKind: String? { get }
var platforms: [AvailabilityRenderItem]? { get }
var isBeta: Bool { get }
}

extension NavigatorIndexableRenderNodeRepresentation {
Expand Down Expand Up @@ -122,6 +123,16 @@ struct RenderNodeVariantView: NavigatorIndexableRenderNodeRepresentation {
}
}

extension NavigatorIndexableRenderMetadataRepresentation {
var isBeta: Bool {
guard let platforms, !platforms.isEmpty else {
return false
}

return platforms.allSatisfy { $0.isBeta == true }
}
Comment on lines +127 to +133
Copy link
Contributor

Choose a reason for hiding this comment

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

This code it's repeated in LinkDestinationSummary ResolvedInformation and now here. Might be better to unify these so it does not become a maintenance nightmare

}

private let typesThatShouldNotUseNavigatorTitle: Set<NavigatorIndex.PageType> = [
.framework, .class, .structure, .enumeration, .protocol, .typeAlias, .associatedType, .extension
]
Expand Down
9 changes: 5 additions & 4 deletions Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public struct RenderIndex: Codable, Equatable {
pageType: .framework,
isDeprecated: false,
isExternal: false,
isBeta: false,
children: nodes,
icon: nil
)
Expand Down Expand Up @@ -245,6 +246,7 @@ extension RenderIndex {
pageType: NavigatorIndex.PageType?,
isDeprecated: Bool,
isExternal: Bool,
isBeta: Bool,
children: [Node],
icon: RenderReferenceIdentifier?
) {
Expand All @@ -253,10 +255,8 @@ extension RenderIndex {

self.isDeprecated = isDeprecated
self.isExternal = isExternal

// Currently Swift-DocC doesn't support marking a node as beta in the navigation index
// so we default to `false` here.
self.isBeta = false
self.isBeta = isBeta

self.icon = icon

guard let pageType else {
Expand Down Expand Up @@ -327,6 +327,7 @@ extension RenderIndex.Node {
pageType: NavigatorIndex.PageType(rawValue: node.item.pageType),
isDeprecated: isDeprecated,
isExternal: node.item.isExternal,
isBeta: node.item.isBeta,
children: node.children.map {
RenderIndex.Node.fromNavigatorTreeNode($0, in: navigatorIndex, with: builder)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ package struct ExternalRenderNode {
RenderNode.Variant(traits: [.interfaceLanguage($0.id)], paths: [externalEntity.topicRenderReference.url])
}
}

/// A value that indicates whether this symbol is built for a beta platform
///
/// This value is `false` if the referenced page is not a symbol.
var isBeta: Bool {
externalEntity.topicRenderReference.isBeta
}
}

/// A language specific representation of an external render node value for building a navigator index.
Expand Down Expand Up @@ -117,7 +124,8 @@ struct NavigatorExternalRenderNode: NavigatorIndexableRenderNodeRepresentation {
externalID: renderNode.externalIdentifier.identifier,
role: renderNode.role,
symbolKind: renderNode.symbolKind?.identifier,
images: renderNode.images
images: renderNode.images,
isBeta: renderNode.isBeta
)
}
}
Expand All @@ -130,6 +138,7 @@ struct ExternalRenderNodeMetadataRepresentation: NavigatorIndexableRenderMetadat
var role: String?
var symbolKind: String?
var images: [TopicImage]
var isBeta: Bool

// Values that we have insufficient information to derive.
// These are needed to conform to the navigator indexable metadata protocol.
Expand All @@ -145,4 +154,4 @@ struct ExternalRenderNodeMetadataRepresentation: NavigatorIndexableRenderMetadat
var fragments: [DeclarationRenderSection.Token]? = nil
var roleHeading: String? = nil
var platforms: [AvailabilityRenderItem]? = nil
}
}
74 changes: 68 additions & 6 deletions Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,35 @@ class ExternalRenderNodeTests: XCTestCase {
referencePath: "/path/to/external/swiftArticle",
title: "SwiftArticle",
kind: .article,
language: .swift
language: .swift,
platforms: [.init(name: "iOS", introduced: nil, isBeta: false)]
)
)
externalResolver.entitiesToReturn["/path/to/external/objCArticle"] = .success(
.init(
referencePath: "/path/to/external/objCArticle",
title: "ObjCArticle",
kind: .article,
language: .objectiveC
language: .objectiveC,
platforms: [.init(name: "macOS", introduced: nil, isBeta: true)]
)
)
externalResolver.entitiesToReturn["/path/to/external/swiftSymbol"] = .success(
.init(
referencePath: "/path/to/external/swiftSymbol",
title: "SwiftSymbol",
kind: .class,
language: .swift
language: .swift,
platforms: [.init(name: "iOS", introduced: nil, isBeta: true)]
)
)
externalResolver.entitiesToReturn["/path/to/external/objCSymbol"] = .success(
.init(
referencePath: "/path/to/external/objCSymbol",
title: "ObjCSymbol",
kind: .function,
language: .objectiveC
language: .objectiveC,
platforms: [.init(name: "macOS", introduced: nil, isBeta: false)]
)
)
return externalResolver
Expand Down Expand Up @@ -89,24 +93,28 @@ class ExternalRenderNodeTests: XCTestCase {
XCTAssertEqual(externalRenderNodes[0].symbolKind, nil)
XCTAssertEqual(externalRenderNodes[0].role, "article")
XCTAssertEqual(externalRenderNodes[0].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCArticle")

XCTAssertTrue(externalRenderNodes[0].isBeta)

XCTAssertEqual(externalRenderNodes[1].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/example/path/to/external/objCSymbol")
XCTAssertEqual(externalRenderNodes[1].kind, .symbol)
XCTAssertEqual(externalRenderNodes[1].symbolKind, nil)
XCTAssertEqual(externalRenderNodes[1].role, "symbol")
XCTAssertEqual(externalRenderNodes[1].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCSymbol")
XCTAssertFalse(externalRenderNodes[1].isBeta)

XCTAssertEqual(externalRenderNodes[2].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/example/path/to/external/swiftArticle")
XCTAssertEqual(externalRenderNodes[2].kind, .article)
XCTAssertEqual(externalRenderNodes[2].symbolKind, nil)
XCTAssertEqual(externalRenderNodes[2].role, "article")
XCTAssertEqual(externalRenderNodes[2].externalIdentifier.identifier, "doc://com.test.external/path/to/external/swiftArticle")
XCTAssertFalse(externalRenderNodes[2].isBeta)

XCTAssertEqual(externalRenderNodes[3].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/example/path/to/external/swiftSymbol")
XCTAssertEqual(externalRenderNodes[3].kind, .symbol)
XCTAssertEqual(externalRenderNodes[3].symbolKind, nil)
XCTAssertEqual(externalRenderNodes[3].role, "symbol")
XCTAssertEqual(externalRenderNodes[3].externalIdentifier.identifier, "doc://com.test.external/path/to/external/swiftSymbol")
XCTAssertTrue(externalRenderNodes[3].isBeta)
}

func testExternalRenderNodeVariantRepresentation() throws {
Expand Down Expand Up @@ -146,14 +154,64 @@ class ExternalRenderNodeTests: XCTestCase {
)
XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.title, swiftTitle)
XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.navigatorTitle, navigatorTitle)

XCTAssertFalse(swiftNavigatorExternalRenderNode.metadata.isBeta)

let objcNavigatorExternalRenderNode = try XCTUnwrap(
NavigatorExternalRenderNode(renderNode: externalRenderNode, trait: .interfaceLanguage("objc"))
)
XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.title, occTitle)
XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.navigatorTitle, occNavigatorTitle)
XCTAssertFalse(objcNavigatorExternalRenderNode.metadata.isBeta)
}

func testExternalRenderNodeVariantRepresentationWhenIsBeta() throws {
let renderReferenceIdentifier = RenderReferenceIdentifier(forExternalLink: "doc://com.test.external/path/to/external/symbol")

// Variants for the title
let swiftTitle = "Swift Symbol"
let occTitle = "Occ Symbol"

// Variants for the navigator title
let navigatorTitle: [DeclarationRenderSection.Token] = [.init(text: "symbol", kind: .identifier)]
let occNavigatorTitle: [DeclarationRenderSection.Token] = [.init(text: "occ_symbol", kind: .identifier)]

// Variants for the fragments
let fragments: [DeclarationRenderSection.Token] = [.init(text: "func", kind: .keyword), .init(text: "symbol", kind: .identifier)]
let occFragments: [DeclarationRenderSection.Token] = [.init(text: "func", kind: .keyword), .init(text: "occ_symbol", kind: .identifier)]

let externalEntity = LinkResolver.ExternalEntity(
topicRenderReference: .init(
identifier: renderReferenceIdentifier,
titleVariants: .init(defaultValue: swiftTitle, objectiveCValue: occTitle),
abstractVariants: .init(defaultValue: []),
url: "/example/path/to/external/symbol",
kind: .symbol,
fragmentsVariants: .init(defaultValue: fragments, objectiveCValue: occFragments),
navigatorTitleVariants: .init(defaultValue: navigatorTitle, objectiveCValue: occNavigatorTitle),
isBeta: true
),
renderReferenceDependencies: .init(),
sourceLanguages: [SourceLanguage(name: "swift"), SourceLanguage(name: "objc")])
let externalRenderNode = ExternalRenderNode(
externalEntity: externalEntity,
bundleIdentifier: "com.test.external"
)

let swiftNavigatorExternalRenderNode = try XCTUnwrap(
NavigatorExternalRenderNode(renderNode: externalRenderNode)
)
XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.title, swiftTitle)
XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.navigatorTitle, navigatorTitle)
XCTAssertTrue(swiftNavigatorExternalRenderNode.metadata.isBeta)

let objcNavigatorExternalRenderNode = try XCTUnwrap(
NavigatorExternalRenderNode(renderNode: externalRenderNode, trait: .interfaceLanguage("objc"))
)
XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.title, occTitle)
XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.navigatorTitle, occNavigatorTitle)
XCTAssertTrue(objcNavigatorExternalRenderNode.metadata.isBeta)
}

func testNavigatorWithExternalNodes() throws {
let externalResolver = generateExternalResover()
let (_, bundle, context) = try testBundleAndContext(
Expand Down Expand Up @@ -208,6 +266,10 @@ class ExternalRenderNodeTests: XCTestCase {
XCTAssertEqual(occExternalNodes.map(\.title), ["ObjCArticle", "ObjCSymbol"])
XCTAssert(swiftExternalNodes.allSatisfy(\.isExternal))
XCTAssert(occExternalNodes.allSatisfy(\.isExternal))
XCTAssert(swiftExternalNodes.first { $0.title == "SwiftArticle" }?.isBeta == false)
XCTAssert(swiftExternalNodes.first { $0.title == "SwiftSymbol" }?.isBeta == true)
XCTAssert(occExternalNodes.first { $0.title == "ObjCArticle" }?.isBeta == true)
XCTAssert(occExternalNodes.first { $0.title == "ObjCSymbol" }?.isBeta == false)
}

func testNavigatorWithExternalNodesOnlyAddsCuratedNodesToNavigator() throws {
Expand Down
Loading