Skip to content

Commit 57c60f3

Browse files
Omit the "doc://" and identifier prefix from V2 link(_:) requests (#1320)
* Omit the "doc://" and identifier prefix from V2 `link(_:)` requests rdar://162694487 * Remove repeated "link" word in code comment Co-authored-by: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> --------- Co-authored-by: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com>
1 parent 44f3997 commit 57c60f3

File tree

3 files changed

+81
-11
lines changed

3 files changed

+81
-11
lines changed

Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+Communication.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ extension OutOfProcessReferenceResolver {
4949
public enum RequestV2: Codable {
5050
/// A request to resolve a link
5151
///
52+
/// DocC omits the "doc:\/\/" and identifier prefix from the link string because it would be the same for every link request.
53+
/// For example: if your resolver registers itself for the `"your.resolver.id"` identifier---by sending it in the ``ResponseV2/identifierAndCapabilities(_:_:)`` handshake message---
54+
/// and DocC encounters a `doc://your.resolver.id/path/to/some-page#some-fragment` link in any documentation content, DocC sends the `"/path/to/some-page#some-fragment"` link to your resolver.
55+
///
5256
/// Your external resolver should respond with either:
5357
/// - a ``ResponseV2/resolved(_:)`` message, with information about the requested link.
5458
/// - a ``ResponseV2/failure(_:)`` message, with human-readable information about the problem that the external link resolver encountered while resolving the link.

Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -406,24 +406,26 @@ extension OutOfProcessReferenceResolver {
406406
private var linkCache: [String /* either a USR or an absolute UnresolvedTopicReference */: LinkDestinationSummary] = [:]
407407

408408
func resolve(unresolvedReference: UnresolvedTopicReference) throws -> TopicReferenceResolutionResult {
409-
let linkString = unresolvedReference.topicURL.absoluteString
410-
if let cachedSummary = linkCache[linkString] {
409+
let unresolvedReferenceString = unresolvedReference.topicURL.absoluteString
410+
if let cachedSummary = linkCache[unresolvedReferenceString] {
411411
return .success( makeReference(for: cachedSummary) )
412412
}
413413

414+
let linkString = unresolvedReference.topicURL.url.withoutHostAndPortAndScheme().standardized.absoluteString
414415
let response: ResponseV2 = try longRunningProcess.sendAndWait(request: RequestV2.link(linkString))
415416

416417
switch response {
417418
case .identifierAndCapabilities:
418419
throw Error.executableSentBundleIdentifierAgain
419420

420421
case .failure(let diagnosticMessage):
422+
let prefixLength = 2 /* for "//" */ + bundleID.rawValue.utf8.count
421423
let solutions: [Solution] = (diagnosticMessage.solutions ?? []).map {
422424
Solution(summary: $0.summary, replacements: $0.replacement.map { replacement in
423425
[Replacement(
424426
// The replacement ranges are relative to the link itself.
425-
// To replace the entire link, we create a range from 0 to the original length, both offset by -4 (the "doc:" length)
426-
range: SourceLocation(line: 0, column: -4, source: nil) ..< SourceLocation(line: 0, column: linkString.utf8.count - 4, source: nil),
427+
// To replace only the path and fragment portion of the link, we create a range from 0 to the relative link string length, both offset by the bundle ID length
428+
range: SourceLocation(line: 0, column: prefixLength, source: nil) ..< SourceLocation(line: 0, column: linkString.utf8.count + prefixLength, source: nil),
427429
replacement: replacement
428430
)]
429431
} ?? [])
@@ -435,7 +437,7 @@ extension OutOfProcessReferenceResolver {
435437

436438
case .resolved(let linkSummary):
437439
// Cache the information for the original authored link
438-
linkCache[linkString] = linkSummary
440+
linkCache[unresolvedReferenceString] = linkSummary
439441
// Cache the information for the resolved reference. That's what's will be used when returning the entity later.
440442
let reference = makeReference(for: linkSummary)
441443
linkCache[reference.absoluteString] = linkSummary

Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ class OutOfProcessReferenceResolverV2Tests: XCTestCase {
424424
let diagnosticInfo = OutOfProcessReferenceResolver.ResponseV2.DiagnosticInformation(
425425
summary: "Some external link issue summary",
426426
solutions: [
427-
.init(summary: "Some external solution", replacement: "some-replacement")
427+
.init(summary: "Some external solution", replacement: "/some-replacement")
428428
]
429429
)
430430
let encodedDiagnostic = try String(decoding: JSONEncoder().encode(diagnosticInfo), as: UTF8.self)
@@ -473,7 +473,7 @@ class OutOfProcessReferenceResolverV2Tests: XCTestCase {
473473
let solution = try XCTUnwrap(problem.possibleSolutions.first)
474474
XCTAssertEqual(solution.summary, "Some external solution")
475475
XCTAssertEqual(solution.replacements.count, 1)
476-
XCTAssertEqual(solution.replacements.first?.range.lowerBound, .init(line: 3, column: 65, source: nil))
476+
XCTAssertEqual(solution.replacements.first?.range.lowerBound, .init(line: 3, column: 87, source: nil))
477477
XCTAssertEqual(solution.replacements.first?.range.upperBound, .init(line: 3, column: 97, source: nil))
478478

479479
// Verify the warning presentation
@@ -493,7 +493,7 @@ class OutOfProcessReferenceResolverV2Tests: XCTestCase {
493493
1 | # My root page
494494
2 |
495495
3 + This page contains an external link that will fail to resolve: <doc:\(highlight)//com.example.test/some-link\(clear)>
496-
| ╰─\(suggestion)suggestion: Some external solution\(clear)
496+
| ╰─\(suggestion)suggestion: Some external solution\(clear)
497497
498498
""")
499499

@@ -504,17 +504,81 @@ class OutOfProcessReferenceResolverV2Tests: XCTestCase {
504504
XCTAssertEqual(try solution.applyTo(original), """
505505
# My root page
506506
507-
This page contains an external link that will fail to resolve: <some-replacement>
507+
This page contains an external link that will fail to resolve: <doc://com.example.test/some-replacement>
508+
""")
509+
}
510+
511+
func testOnlySendsPathAndFragmentInLinkRequest() async throws {
512+
let externalBundleID: DocumentationBundle.Identifier = "com.example.test"
513+
514+
let resolver: OutOfProcessReferenceResolver
515+
let savedRequestsFile: URL
516+
do {
517+
let temporaryFolder = try createTemporaryDirectory()
518+
savedRequestsFile = temporaryFolder.appendingPathComponent("saved-requests.txt")
519+
520+
let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable")
521+
try """
522+
#!/bin/bash
523+
echo '{"identifier":"\(externalBundleID)","capabilities": 0}' # Write this resolver's identifier & capabilities
524+
read # Wait for docc to send a request
525+
echo $REPLY >> \(savedRequestsFile.path) # Save the raw request string
526+
echo '{"failure":"ignored error message"}' # Respond with an error message
527+
# Repeat the same read-save-respond steps 2 more times
528+
read # Wait for 2nd request
529+
echo $REPLY >> \(savedRequestsFile.path) # Save the raw request
530+
echo '{"failure":"ignored error message"}' # Respond
531+
read # Wait for 3rd request
532+
echo $REPLY >> \(savedRequestsFile.path) # Save the raw request
533+
echo '{"failure":"ignored error message"}' # Respond
534+
""".write(to: executableLocation, atomically: true, encoding: .utf8)
535+
536+
// `0o0700` is `-rwx------` (read, write, & execute only for owner)
537+
try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path)
538+
XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path))
539+
540+
resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in })
541+
}
542+
543+
let catalog = Folder(name: "unit-test.docc", content: [
544+
TextFile(name: "Something.md", utf8Content: """
545+
# My root page
546+
547+
This page contains an 3 external links hat will fail to resolve:
548+
- <doc://\(externalBundleID.rawValue)/some-link>
549+
- <doc://\(externalBundleID.rawValue)/path/to/some-link>
550+
- <doc://\(externalBundleID.rawValue)/path/to/some-link#some-fragment>
551+
""")
552+
])
553+
let inputDirectory = Folder(name: "path", content: [Folder(name: "to", content: [catalog])])
554+
555+
var configuration = DocumentationContext.Configuration()
556+
configuration.externalDocumentationConfiguration.sources = [
557+
externalBundleID: resolver
558+
]
559+
// Create the context, just to process all the documentation and make the 3 external link requests
560+
_ = try await loadBundle(catalog: inputDirectory, configuration: configuration)
561+
562+
// The requests can come in any order so we sort the output lines for easier comparison
563+
let readRequests = try String(contentsOf: savedRequestsFile, encoding: .utf8)
564+
.components(separatedBy: .newlines)
565+
.filter { !$0.isEmpty }
566+
.sorted(by: \.count)
567+
.joined(separator: "\n")
568+
XCTAssertEqual(readRequests, """
569+
{"link":"/some-link"}
570+
{"link":"/path/to/some-link"}
571+
{"link":"/path/to/some-link#some-fragment"}
508572
""")
509573
}
510574

511575
func testEncodingAndDecodingRequests() throws {
512576
do {
513-
let request = OutOfProcessReferenceResolver.RequestV2.link("doc://com.example/path/to/something")
577+
let request = OutOfProcessReferenceResolver.RequestV2.link("/path/to/some-page#some-fragment")
514578

515579
let data = try JSONEncoder().encode(request)
516580
if case .link(let link) = try JSONDecoder().decode(OutOfProcessReferenceResolver.RequestV2.self, from: data) {
517-
XCTAssertEqual(link, "doc://com.example/path/to/something")
581+
XCTAssertEqual(link, "/path/to/some-page#some-fragment")
518582
} else {
519583
XCTFail("Decoded the wrong type of request")
520584
}

0 commit comments

Comments
 (0)