diff --git a/Sources/SwiftDocC/Model/Rendering/References/ExternalLocationReference.swift b/Sources/SwiftDocC/Model/Rendering/References/ExternalLocationReference.swift index 65f4842709..663c6e70a4 100644 --- a/Sources/SwiftDocC/Model/Rendering/References/ExternalLocationReference.swift +++ b/Sources/SwiftDocC/Model/Rendering/References/ExternalLocationReference.swift @@ -24,7 +24,9 @@ public struct ExternalLocationReference: RenderReference, URLReference { public private(set) var type: RenderReferenceType = .externalLocation - public var identifier: RenderReferenceIdentifier + public let identifier: RenderReferenceIdentifier + + let url: String enum CodingKeys: String, CodingKey { case type @@ -34,12 +36,14 @@ public struct ExternalLocationReference: RenderReference, URLReference { public init(identifier: RenderReferenceIdentifier) { self.identifier = identifier + self.url = identifier.identifier } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.identifier = try container.decode(RenderReferenceIdentifier.self, forKey: .identifier) + self.url = try container.decode(String.self, forKey: .url) self.type = try container.decode(RenderReferenceType.self, forKey: .type) } @@ -47,8 +51,6 @@ public struct ExternalLocationReference: RenderReference, URLReference { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type.rawValue, forKey: .type) try container.encode(identifier, forKey: .identifier) - - // Enter the given URL verbatim into the Render JSON - try container.encode(identifier.identifier, forKey: .url) + try container.encode(url, forKey: .url) } } diff --git a/Sources/SwiftDocC/Model/Rendering/Tutorial/References/DownloadReference.swift b/Sources/SwiftDocC/Model/Rendering/Tutorial/References/DownloadReference.swift index 6d27eda596..08229c3f35 100644 --- a/Sources/SwiftDocC/Model/Rendering/Tutorial/References/DownloadReference.swift +++ b/Sources/SwiftDocC/Model/Rendering/Tutorial/References/DownloadReference.swift @@ -26,7 +26,15 @@ public struct DownloadReference: RenderReference, URLReference, Equatable { /// The location of the downloadable resource. public var url: URL - + + /// Indicates whether the ``url`` property was loaded from the regular initializer or from the + /// `Decodable` initializer. + /// + /// This is used during encoding to determine whether to filter ``url`` through the + /// `renderURL(for:)` method. In case the URL was loaded from JSON, we don't want to modify it + /// further after a round-trip. + private var urlWasDecoded = false + /// The SHA512 hash value for the resource. public var checksum: String? @@ -60,7 +68,23 @@ public struct DownloadReference: RenderReference, URLReference, Equatable { public init(identifier: RenderReferenceIdentifier, renderURL url: URL, sha512Checksum: String) { self.init(identifier: identifier, renderURL: url, checksum: sha512Checksum) } - + + enum CodingKeys: CodingKey { + case type + case identifier + case url + case checksum + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.type = try container.decode(RenderReferenceType.self, forKey: .type) + self.identifier = try container.decode(RenderReferenceIdentifier.self, forKey: .identifier) + self.url = try container.decode(URL.self, forKey: .url) + self.urlWasDecoded = true + self.checksum = try container.decodeIfPresent(String.self, forKey: .checksum) + } + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type.rawValue, forKey: .type) @@ -68,7 +92,17 @@ public struct DownloadReference: RenderReference, URLReference, Equatable { try container.encodeIfPresent(checksum, forKey: .checksum) // Render URL - try container.encode(renderURL(for: url), forKey: .url) + if !urlWasDecoded { + try container.encode(renderURL(for: url), forKey: .url) + } else { + try container.encode(url, forKey: .url) + } + } + + static public func ==(lhs: DownloadReference, rhs: DownloadReference) -> Bool { + lhs.identifier == rhs.identifier + && lhs.url == rhs.url + && lhs.checksum == rhs.checksum } } diff --git a/Tests/SwiftDocCTests/Rendering/Rendering Fixtures/external-location-custom-url.json b/Tests/SwiftDocCTests/Rendering/Rendering Fixtures/external-location-custom-url.json new file mode 100644 index 0000000000..b8db85e012 --- /dev/null +++ b/Tests/SwiftDocCTests/Rendering/Rendering Fixtures/external-location-custom-url.json @@ -0,0 +1,110 @@ +{ + "schemaVersion" : { + "major" : 1, + "minor" : 0, + "patch" : 0 + }, + "seeAlsoSections" : [ ], + "metadata" : { + "platforms" : [ + { + "name" : "macOS", + "introducedAt" : "10.15" + } + ], + "modules" : [ + { "name" : "MyKit" } + ], + "title" : "Wifi Access", + "roleHeading" : "Plist Key" + }, + "abstract" : [ + { + "type" : "text", + "text" : "A " + }, + { + "type" : "codeVoice", + "code" : "WiFi access" + }, + { + "type" : "text", + "text" : " abstract description." + } + ], + "sections" : [ + ], + "identifier" : { + "url" : "doc:\/\/org.swift.docc.example\/plist\/wifiaccess", + "interfaceLanguage": "swift" + }, + "hierarchy" : { + "paths" : [["doc:\/\/org.swift.docc.example\/plist\/wifiaccess"]] + }, + "topicSections" : [ + ], + "kind" : "symbol", + "references" : { + "doc:\/\/org.swift.docc.example\/downloads\/sample.zip": { + "identifier": "ExternalLocation.zip", + "url": "https://example.com/ExternalLocation.zip", + "type": "externalLocation" + }, + "doc:\/\/org.swift.docc.example\/plist\/wifiaccess": { + "abstract" : [ + { + "text" : "WiFi access", + "type" : "text" + } + ], + "identifier" : "doc:\/\/org.swift.docc.example\/plist\/wifiaccess", + "kind" : "symbol", + "title" : "WiFi Access", + "type" : "topic", + "url" : "\/documentation\/mykit" + } + }, + "sampleCodeDownload": { + "action": { + "identifier": "doc:\/\/org.swift.docc.example\/downloads\/sample.zip", + "isActive": true, + "overridingTitle": "Download", + "type": "reference" + } + }, + "primaryContentSections" : [ + { + "kind" : "content", + "content" : [ + { + "anchor" : "discussion", + "level" : 2, + "type" : "heading", + "text" : "Discussion" + }, + { + "type" : "paragraph", + "inlineContent" : [ + { + "type" : "text", + "text" : "Use " + }, + { + "type" : "codeVoice", + "code" : "Wifi access" + }, + { + "type" : "text", + "text" : " to secure wifi access for your app." + } + ] + } + ] + } + ], + "variants": [{ + "paths" : ["\/plist\/wifiaccess"], + "traits" : [] + }] +} + diff --git a/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift b/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift index df454447b3..718be55c72 100644 --- a/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift +++ b/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift @@ -240,4 +240,55 @@ class SampleDownloadTests: XCTestCase { let reference = try XCTUnwrap(renderNode.references[identifier.identifier]) XCTAssert(reference is ExternalLocationReference) } + + /// Ensure that a DownloadReference where the URL is different from the reference identifier + /// can still round-trip through an ExternalLocationReference with the URL and reference identifier intact. + func testRoundTripWithDifferentUrl() throws { + let baseReference = DownloadReference(identifier: .init("DownloadReference.zip"), renderURL: .init(string: "https://example.com/DownloadReference.zip")!, checksum: nil) + + let encoder = JSONEncoder() + let decoder = JSONDecoder() + + let encodedReference = try encoder.encode(baseReference) + + let interimReference = try decoder.decode(ExternalLocationReference.self, from: encodedReference) + let interimEncodedReference = try encoder.encode(interimReference) + + let roundTripReference = try decoder.decode(DownloadReference.self, from: interimEncodedReference) + + XCTAssertEqual(baseReference, roundTripReference) + } + + /// Ensure that an ExternalLocationReference loaded from JSON continues to encode the same + /// information after being decoded and re-encoded. + func testRoundTripExternalLocationFromFixture() throws { + let downloadSymbolURL = Bundle.module.url( + forResource: "external-location-custom-url", withExtension: "json", + subdirectory: "Rendering Fixtures")! + + let originalData = try Data(contentsOf: downloadSymbolURL) + let originalRenderNode = try RenderNode.decode(fromJSON: originalData) + + let encodedRenderNode = try JSONEncoder().encode(originalRenderNode) + let symbol = try RenderNode.decode(fromJSON: encodedRenderNode) + + // + // Sample Download Details + // + + guard let section = symbol.sampleDownload else { + XCTFail("Download section not decoded") + return + } + + guard case RenderInlineContent.reference(let identifier, _, _, _) = section.action else { + XCTFail("Could not decode action reference") + return + } + + XCTAssertEqual(identifier.identifier, "doc://org.swift.docc.example/downloads/sample.zip") + + let externalReference = try XCTUnwrap(symbol.references[identifier.identifier] as? ExternalLocationReference) + XCTAssertEqual(externalReference.url, "https://example.com/ExternalLocation.zip") + } }