Skip to content

Commit 7ca772d

Browse files
authored
Generate documentation content based on the symbol graph module names (#52)
rdar://84810134
1 parent 03b711a commit 7ca772d

File tree

4 files changed

+142
-33
lines changed

4 files changed

+142
-33
lines changed

Sources/SwiftDocC/Infrastructure/Workspace/GeneratedDataProvider.swift

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,53 @@
99
*/
1010

1111
import Foundation
12+
import SymbolKit
1213

1314
/// A type that provides documentation bundles that it discovers by traversing the local file system.
1415
public struct GeneratedDataProvider: DocumentationWorkspaceDataProvider {
1516
public var identifier: String = UUID().uuidString
1617

17-
/// Options to configure how the provider generates documentation bundles.
18-
public var options: BundleDiscoveryOptions
19-
20-
private let info: DocumentationBundle.Info
21-
private let topLevelPage: URL
2218
public typealias SymbolGraphDataLoader = (URL) -> Data?
2319
private let symbolGraphDataLoader: SymbolGraphDataLoader
2420

25-
/// Creates a new provider that recursively traverses the content of the given root URL to discover documentation bundles.
21+
/// Creates a new provider that generates documentation bundles from the ``BundleDiscoveryOptions`` it is passed in ``bundles(options:)``.
2622
///
2723
/// - Parameters:
28-
/// - options: Options to configure how the converter discovers documentation bundles.
2924
/// - symbolGraphDataLoader: A closure that loads the raw data for a symbol graph file at a given URL.
30-
public init(options: BundleDiscoveryOptions, symbolGraphDataLoader: @escaping SymbolGraphDataLoader) throws {
31-
self.options = options
25+
public init(symbolGraphDataLoader: @escaping SymbolGraphDataLoader) {
3226
self.symbolGraphDataLoader = symbolGraphDataLoader
27+
}
28+
29+
public func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] {
30+
let info: DocumentationBundle.Info
3331
do {
34-
self.info = try DocumentationBundle.Info(bundleDiscoveryOptions: options)
32+
info = try DocumentationBundle.Info(bundleDiscoveryOptions: options)
3533
} catch {
3634
throw Error.notEnoughDataToGenerateBundle(options: options, underlyingError: error)
3735
}
3836

39-
self.topLevelPage = URL(string: "\(info.displayName.replacingWhitespaceAndPunctuation(with: "-")).md")!
40-
}
41-
42-
public func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] {
4337
guard !options.additionalSymbolGraphFiles.isEmpty else {
4438
return []
4539
}
4640

41+
// Find all the unique module names from the symbol graph files and generate a top level module page for each of them.
42+
var moduleNames = Set<String>()
43+
for url in options.additionalSymbolGraphFiles {
44+
guard let data = symbolGraphDataLoader(url) else {
45+
throw Error.unableToLoadSymbolGraphData(url: url)
46+
}
47+
let container = try JSONDecoder().decode(SymbolGraphModuleContainer.self, from: data)
48+
moduleNames.insert(container.module.name)
49+
}
50+
51+
let topLevelPages = moduleNames.map { URL(string: $0 + ".md")! }
52+
4753
return [
4854
DocumentationBundle(
4955
info: info,
5056
attributedCodeListings: [:],
5157
symbolGraphURLs: options.additionalSymbolGraphFiles,
52-
markupURLs: [topLevelPage],
58+
markupURLs: topLevelPages,
5359
miscResourceURLs: []
5460
)
5561
]
@@ -92,10 +98,11 @@ public struct GeneratedDataProvider: DocumentationWorkspaceDataProvider {
9298
}
9399

94100
public func contentsOfURL(_ url: URL) throws -> Data {
95-
if url == topLevelPage {
96-
let markdown = "# ``\(info.displayName)``"
101+
if DocumentationBundleFileTypes.isMarkupFile(url) {
102+
let moduleName = url.deletingPathExtension().lastPathComponent
103+
let markdown = "# ``\(moduleName)``"
97104
return Data(markdown.utf8)
98-
} else if options.additionalSymbolGraphFiles.contains(url) {
105+
} else if DocumentationBundleFileTypes.isSymbolGraphFile(url) {
99106
guard let data = symbolGraphDataLoader(url) else {
100107
throw Error.unableToLoadSymbolGraphData(url: url)
101108
}
@@ -105,3 +112,17 @@ public struct GeneratedDataProvider: DocumentationWorkspaceDataProvider {
105112
}
106113
}
107114
}
115+
116+
/// A wrapper type that decodes only the module in the symbol graph.
117+
private struct SymbolGraphModuleContainer: Decodable {
118+
/// The decoded symbol graph module.
119+
let module: SymbolGraph.Module
120+
121+
typealias CodingKeys = SymbolGraph.CodingKeys
122+
123+
public init(from decoder: Decoder) throws {
124+
let container = try decoder.container(keyedBy: CodingKeys.self)
125+
126+
self.module = try container.decode(SymbolGraph.Module.self, forKey: .module)
127+
}
128+
}

Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public struct ConvertAction: Action, RecreatingContext {
150150
dataProvider = try LocalFileSystemDataProvider(rootURL: rootURL)
151151
} else {
152152
self.context.externalMetadata.isGeneratedBundle = true
153-
dataProvider = try GeneratedDataProvider(options: bundleDiscoveryOptions, symbolGraphDataLoader: { url in
153+
dataProvider = GeneratedDataProvider(symbolGraphDataLoader: { url in
154154
fileManager.contents(atPath: url.path)
155155
})
156156
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
12+
@testable import SwiftDocC
13+
import SymbolKit
14+
15+
class GeneratedDataProviderTests: XCTestCase {
16+
17+
func testGeneratingBundles() throws {
18+
let firstSymbolGraph = SymbolGraph(
19+
metadata: .init(
20+
formatVersion: .init(major: 0, minor: 0, patch: 1),
21+
generator: "unit-test"
22+
),
23+
module: .init(
24+
name: "FirstModuleName",
25+
platform: .init()
26+
),
27+
symbols: [],
28+
relationships: []
29+
)
30+
var secondSymbolGraph = firstSymbolGraph
31+
secondSymbolGraph.module.name = "SecondModuleName"
32+
33+
let thirdSymbolGraph = firstSymbolGraph // Another symbol graph with the same module name
34+
35+
let dataProvider = GeneratedDataProvider(
36+
symbolGraphDataLoader: {
37+
switch $0.lastPathComponent {
38+
case "first.symbols.json":
39+
return try? JSONEncoder().encode(firstSymbolGraph)
40+
case "second.symbols.json":
41+
return try? JSONEncoder().encode(secondSymbolGraph)
42+
case "third.symbols.json":
43+
return try? JSONEncoder().encode(thirdSymbolGraph)
44+
default:
45+
return nil
46+
}
47+
}
48+
)
49+
50+
let options = BundleDiscoveryOptions(
51+
infoPlistFallbacks: [
52+
"CFBundleDisplayName": "Custom Display Name",
53+
"CFBundleIdentifier": "com.test.example",
54+
],
55+
additionalSymbolGraphFiles: [
56+
URL(fileURLWithPath: "first.symbols.json"),
57+
URL(fileURLWithPath: "second.symbols.json"),
58+
URL(fileURLWithPath: "third.symbols.json"),
59+
]
60+
)
61+
let bundles = try dataProvider.bundles(options: options)
62+
XCTAssertEqual(bundles.count, 1)
63+
let bundle = try XCTUnwrap(bundles.first)
64+
65+
XCTAssertEqual(bundle.displayName, "Custom Display Name")
66+
XCTAssertEqual(bundle.symbolGraphURLs.map { $0.lastPathComponent }.sorted(), [
67+
"first.symbols.json",
68+
"second.symbols.json",
69+
"third.symbols.json",
70+
71+
])
72+
XCTAssertEqual(bundle.markupURLs.map { $0.path }.sorted(), [
73+
"FirstModuleName.md",
74+
"SecondModuleName.md",
75+
// No third file since that symbol graph has the same module name as the first
76+
])
77+
78+
XCTAssertEqual(
79+
try String(data: dataProvider.contentsOfURL(URL(fileURLWithPath: "FirstModuleName.md")), encoding: .utf8),
80+
"# ``FirstModuleName``"
81+
)
82+
XCTAssertEqual(
83+
try String(data: dataProvider.contentsOfURL(URL(fileURLWithPath: "SecondModuleName.md")), encoding: .utf8),
84+
"# ``SecondModuleName``"
85+
)
86+
}
87+
}

Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -345,21 +345,22 @@ class ConvertActionTests: XCTestCase {
345345
var infoPlistFallbacks = [String: Any]()
346346
infoPlistFallbacks["CFBundleIdentifier"] = "com.example.test"
347347

348-
XCTAssertThrowsError(try ConvertAction(
349-
documentationBundleURL: nil,
350-
outOfProcessResolver: nil,
351-
analyze: false,
352-
targetDirectory: outputLocation.absoluteURL,
353-
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
354-
emitDigest: false,
355-
currentPlatforms: nil,
356-
fileManager: testDataProvider,
357-
bundleDiscoveryOptions: BundleDiscoveryOptions(
358-
infoPlistFallbacks: infoPlistFallbacks,
359-
additionalSymbolGraphFiles: [URL(fileURLWithPath: "/Not-a-doc-bundle/MyKit.symbols.json")]
360-
)
348+
var action = try ConvertAction(
349+
documentationBundleURL: nil,
350+
outOfProcessResolver: nil,
351+
analyze: false,
352+
targetDirectory: outputLocation.absoluteURL,
353+
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
354+
emitDigest: false,
355+
currentPlatforms: nil,
356+
fileManager: testDataProvider,
357+
bundleDiscoveryOptions: BundleDiscoveryOptions(
358+
infoPlistFallbacks: infoPlistFallbacks,
359+
additionalSymbolGraphFiles: [URL(fileURLWithPath: "/Not-a-doc-bundle/MyKit.symbols.json")]
361360
)
362-
) { error in
361+
)
362+
let logStorage = LogHandle.LogStorage()
363+
XCTAssertThrowsError(try action.perform(logHandle: .memory(logStorage))) { error in
363364
XCTAssertEqual(error.localizedDescription, """
364365
The information provided as command line arguments is not enough to generate a documentation bundle:
365366

0 commit comments

Comments
 (0)