diff --git a/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift b/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift index 975a177d4b..1602b6a86f 100644 --- a/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift +++ b/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift @@ -37,6 +37,8 @@ public class DocumentationContextConverter { /// Whether the documentation converter should include access level information for symbols. let shouldEmitSymbolAccessLevels: Bool + let symbolIdentifiersWithExpandedDocumentation: [String]? + /// The remote source control repository where the documented module's source is hosted. let sourceRepository: SourceRepository? @@ -61,7 +63,8 @@ public class DocumentationContextConverter { renderContext: RenderContext, emitSymbolSourceFileURIs: Bool = false, emitSymbolAccessLevels: Bool = false, - sourceRepository: SourceRepository? = nil + sourceRepository: SourceRepository? = nil, + symbolIdentifiersWithExpandedDocumentation: [String]? = nil ) { self.bundle = bundle self.context = context @@ -69,6 +72,7 @@ public class DocumentationContextConverter { self.shouldEmitSymbolSourceFileURIs = emitSymbolSourceFileURIs self.shouldEmitSymbolAccessLevels = emitSymbolAccessLevels self.sourceRepository = sourceRepository + self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation } /// Converts a documentation node to a render node. @@ -91,7 +95,8 @@ public class DocumentationContextConverter { renderContext: renderContext, emitSymbolSourceFileURIs: shouldEmitSymbolSourceFileURIs, emitSymbolAccessLevels: shouldEmitSymbolAccessLevels, - sourceRepository: sourceRepository + sourceRepository: sourceRepository, + symbolIdentifiersWithExpandedDocumentation: symbolIdentifiersWithExpandedDocumentation ) return translator.visit(node.semantic) as? RenderNode } diff --git a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift index 5b07d7bea4..c21c44621f 100644 --- a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift +++ b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift @@ -185,7 +185,8 @@ public struct ConvertService: DocumentationService { additionalSymbolGraphFiles: [] ), emitSymbolSourceFileURIs: request.emitSymbolSourceFileURIs, - emitSymbolAccessLevels: true + emitSymbolAccessLevels: true, + symbolIdentifiersWithExpandedDocumentation: request.symbolIdentifiersWithExpandedDocumentation ) // Run the conversion. diff --git a/Sources/SwiftDocC/DocumentationService/Models/Services/Convert/ConvertRequest.swift b/Sources/SwiftDocC/DocumentationService/Models/Services/Convert/ConvertRequest.swift index 12f4dfa9a4..96ef7fee38 100644 --- a/Sources/SwiftDocC/DocumentationService/Models/Services/Convert/ConvertRequest.swift +++ b/Sources/SwiftDocC/DocumentationService/Models/Services/Convert/ConvertRequest.swift @@ -8,7 +8,6 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ -import SymbolKit import Foundation /// A request to convert in-memory documentation. @@ -130,6 +129,13 @@ public struct ConvertRequest: Codable { /// - ``DocumentationBundle/miscResourceURLs`` public var miscResourceURLs: [URL] + /// The symbol identifiers that have an expanded documentation page available if they meet the associated access level requirement. + /// + /// DocC sets the ``RenderMetadata/hasExpandedDocumentationForSymbols`` property to `true` + /// for these symbols if they meet the provided requirements, so that renderers can display a "View More" link + /// that navigates the user to the full version of the documentation page. + public var symbolIdentifiersWithExpandedDocumentation: [String: ExpandedDocumentationRequirements]? + /// The default code listing language for the documentation bundle to convert. /// /// ## See Also @@ -177,6 +183,8 @@ public struct ConvertRequest: Codable { version: version, defaultCodeListingLanguage: defaultCodeListingLanguage ) + + self.symbolIdentifiersWithExpandedDocumentation = nil } /// Creates a request to convert in-memory documentation. @@ -195,6 +203,8 @@ public struct ConvertRequest: Codable { /// - markupFiles: The article and documentation extension file data included in the documentation bundle to convert. /// - tutorialFiles: The tutorial file data included in the documentation bundle to convert. /// - miscResourceURLs: The on-disk resources in the documentation bundle to convert. + /// - symbolIdentifiersWithExpandedDocumentation: A dictionary of identifiers to requirements for these symbols to have expanded + /// documentation available. public init( bundleInfo: DocumentationBundle.Info, featureFlags: FeatureFlags = FeatureFlags(), @@ -208,7 +218,8 @@ public struct ConvertRequest: Codable { emitSymbolSourceFileURIs: Bool = true, markupFiles: [Data], tutorialFiles: [Data] = [], - miscResourceURLs: [URL] + miscResourceURLs: [URL], + symbolIdentifiersWithExpandedDocumentation: [String: ExpandedDocumentationRequirements]? = nil ) { self.externalIDsToConvert = externalIDsToConvert self.documentPathsToConvert = documentPathsToConvert @@ -229,6 +240,7 @@ public struct ConvertRequest: Codable { self.miscResourceURLs = miscResourceURLs self.bundleInfo = bundleInfo self.featureFlags = featureFlags + self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation } } @@ -288,4 +300,12 @@ extension ConvertRequest { self.character = character } } + + public struct ExpandedDocumentationRequirements: Codable { + public let accessControlLevel: AccessControlLevel? + + public init(accessControlLevel: AccessControlLevel?) { + self.accessControlLevel = accessControlLevel + } + } } diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift b/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift index 1c2102eed0..312926c1e1 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift @@ -92,6 +92,9 @@ public struct DocumentationConverter: DocumentationConverterProtocol { /// The source repository where the documentation's sources are hosted. var sourceRepository: SourceRepository? + /// The identifiers and access level requirements for symbols that have an expanded version of their documentation page if the requirements are met + var symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil + /// `true` if the conversion is cancelled. private var isCancelled: Synchronized? = nil @@ -118,6 +121,8 @@ public struct DocumentationConverter: DocumentationConverterProtocol { /// Before passing `true` please confirm that your use case doesn't include public /// distribution of any created render nodes as there are filesystem privacy and security /// concerns with distributing this data. + /// - Parameter symbolIdentifiersWithExpandedDocumentation: Identifiers and access level requirements for symbols + /// that have an expanded version of their documentation page if the access level requirement is met. public init( documentationBundleURL: URL?, emitDigest: Bool, @@ -133,7 +138,8 @@ public struct DocumentationConverter: DocumentationConverterProtocol { emitSymbolAccessLevels: Bool = false, sourceRepository: SourceRepository? = nil, isCancelled: Synchronized? = nil, - diagnosticEngine: DiagnosticEngine = .init() + diagnosticEngine: DiagnosticEngine = .init(), + symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil ) { self.rootURL = documentationBundleURL self.emitDigest = emitDigest @@ -149,6 +155,7 @@ public struct DocumentationConverter: DocumentationConverterProtocol { self.sourceRepository = sourceRepository self.isCancelled = isCancelled self.diagnosticEngine = diagnosticEngine + self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation // Inject current platform versions if provided if let currentPlatforms = currentPlatforms { @@ -250,13 +257,22 @@ public struct DocumentationConverter: DocumentationConverterProtocol { // Copy images, sample files, and other static assets. try outputConsumer.consume(assetsInBundle: bundle) + let symbolIdentifiersMeetingRequirementsForExpandedDocumentation: [String]? = symbolIdentifiersWithExpandedDocumentation?.compactMap { (identifier, expandedDocsRequirement) -> String? in + guard let documentationNode = context.nodeWithSymbolIdentifier(identifier) else { + return nil + } + + return documentationNode.meetsExpandedDocumentationRequirements(expandedDocsRequirement) ? identifier : nil + } + let converter = DocumentationContextConverter( bundle: bundle, context: context, renderContext: renderContext, emitSymbolSourceFileURIs: shouldEmitSymbolSourceFileURIs, emitSymbolAccessLevels: shouldEmitSymbolAccessLevels, - sourceRepository: sourceRepository + sourceRepository: sourceRepository, + symbolIdentifiersWithExpandedDocumentation: symbolIdentifiersMeetingRequirementsForExpandedDocumentation ) var indexingRecords = [IndexingRecord]() @@ -455,3 +471,15 @@ public struct DocumentationConverter: DocumentationConverterProtocol { } } } + +extension DocumentationNode { + func meetsExpandedDocumentationRequirements(_ requirements: ConvertRequest.ExpandedDocumentationRequirements) -> Bool { + // If we don't know the required access control level then any level is acceptable. + guard let requiredAccessControlLevel = requirements.accessControlLevel else { return true } + + // If the symbol does not expose it's access control level let's assume it's most restrictive. + guard let symbolAccessLevel = symbol?.accessLevel else { return false } + + return AccessControlLevel(symbolAccessLevel.rawValue) >= requiredAccessControlLevel + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift index 215d56cf71..2ddd5c10f2 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift @@ -10,31 +10,14 @@ import SymbolKit -extension SymbolGraph.Symbol.AccessControl: Comparable { - private var level: Int? { - switch self { - case .private: - return 0 - case .filePrivate: - return 1 - case .internal: - return 2 - case .public: - return 3 - case .open: - return 4 - default: - assertionFailure("Unknown AccessControl case was used in comparison.") - return nil - } +extension AccessControlLevel { + init(from symbolKitAccessControlLevel: SymbolGraph.Symbol.AccessControl) { + self.init(symbolKitAccessControlLevel.rawValue) } - +} + +extension SymbolGraph.Symbol.AccessControl: Comparable { public static func < (lhs: SymbolGraph.Symbol.AccessControl, rhs: SymbolGraph.Symbol.AccessControl) -> Bool { - guard let lhs = lhs.level, - let rhs = rhs.level else { - return false - } - - return lhs < rhs + return AccessControlLevel(from: lhs) < AccessControlLevel(from: rhs) } } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderMetadata.swift b/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderMetadata.swift index 2b21f9c293..fd78269137 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderMetadata.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderMetadata.swift @@ -168,6 +168,15 @@ public struct RenderMetadata: VariantContainer { /// Any tags assigned to the node. public var tags: [RenderNode.Tag]? + + /// Whether there isn't a version of the page with more content that a renderer can link to. + /// + /// This property indicates to renderers that an expanded version of the page does not exist for this render node, + /// which, for example, controls whether a 'View More' link should be displayed or not. + /// + /// It's the renderer's responsibility to fetch the full version of the page, for example using + /// the ``RenderNode/variants`` property. + public var hasNoExpandedDocumentation: Bool = false } extension RenderMetadata: Codable { @@ -238,6 +247,7 @@ extension RenderMetadata: Codable { public static let images = CodingKeys(stringValue: "images") public static let color = CodingKeys(stringValue: "color") public static let customMetadata = CodingKeys(stringValue: "customMetadata") + public static let hasNoExpandedDocumentation = CodingKeys(stringValue: "hasNoExpandedDocumentation") } public init(from decoder: Decoder) throws { @@ -267,6 +277,7 @@ extension RenderMetadata: Codable { sourceFileURIVariants = try container.decodeVariantCollectionIfPresent(ofValueType: String?.self, forKey: .sourceFileURI) remoteSourceVariants = try container.decodeVariantCollectionIfPresent(ofValueType: RemoteSource?.self, forKey: .remoteSource) tags = try container.decodeIfPresent([RenderNode.Tag].self, forKey: .tags) + hasNoExpandedDocumentation = try container.decodeIfPresent(Bool.self, forKey: .hasNoExpandedDocumentation) ?? false let extraKeys = Set(container.allKeys).subtracting( [ @@ -288,7 +299,8 @@ extension RenderMetadata: Codable { .navigatorTitle, .sourceFileURI, .remoteSource, - .tags + .tags, + .hasNoExpandedDocumentation, ] ) for extraKey in extraKeys { @@ -330,5 +342,6 @@ extension RenderMetadata: Codable { try container.encodeIfNotEmpty(images, forKey: .images) try container.encodeIfPresent(color, forKey: .color) try container.encodeIfNotEmpty(customMetadata, forKey: .customMetadata) + try container.encodeIfTrue(hasNoExpandedDocumentation, forKey: .hasNoExpandedDocumentation) } } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderNode+Codable.swift b/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderNode+Codable.swift index c1ef55aaac..7860cf3c67 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderNode+Codable.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNode/RenderNode+Codable.swift @@ -128,4 +128,11 @@ extension KeyedEncodingContainer { try encode(value, forKey: key) } } + + /// Encodes the given boolean if its value is true. + mutating func encodeIfTrue(_ value: Bool, forKey key: Key) throws { + if value { + try encode(value, forKey: key) + } + } } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index a5d9b3ef59..ae734c732e 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -51,6 +51,8 @@ public struct RenderNodeTranslator: SemanticVisitor { /// The source repository where the documentation's sources are hosted. var sourceRepository: SourceRepository? + var symbolIdentifiersWithExpandedDocumentation: [String]? = nil + public mutating func visitCode(_ code: Code) -> RenderTree? { let fileType = NSString(string: code.fileName).pathExtension let fileReference = code.fileReference @@ -1179,8 +1181,7 @@ public struct RenderNodeTranslator: SemanticVisitor { } else if let extendedModule = symbol.extendedModule, extendedModule != moduleName.displayName { node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: [extendedModule])]) } else { - node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: nil)] - ) + node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: nil)]) } node.metadata.extendedModuleVariants = VariantCollection(defaultValue: symbol.extendedModule) @@ -1355,6 +1356,12 @@ public struct RenderNodeTranslator: SemanticVisitor { node.metadata.symbolAccessLevelVariants = VariantCollection(from: symbol.accessLevelVariants) } + if let externalID = symbol.externalID, + let symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation + { + node.metadata.hasNoExpandedDocumentation = !symbolIdentifiersWithExpandedDocumentation.contains(externalID) + } + node.relationshipSectionsVariants = VariantCollection<[RelationshipsRenderSection]>( from: documentationNode.availableVariantTraits, fallbackDefaultValue: [] @@ -1878,7 +1885,8 @@ public struct RenderNodeTranslator: SemanticVisitor { renderContext: RenderContext? = nil, emitSymbolSourceFileURIs: Bool = false, emitSymbolAccessLevels: Bool = false, - sourceRepository: SourceRepository? = nil + sourceRepository: SourceRepository? = nil, + symbolIdentifiersWithExpandedDocumentation: [String]? = nil ) { self.context = context self.bundle = bundle @@ -1889,6 +1897,7 @@ public struct RenderNodeTranslator: SemanticVisitor { self.shouldEmitSymbolSourceFileURIs = emitSymbolSourceFileURIs self.shouldEmitSymbolAccessLevels = emitSymbolAccessLevels self.sourceRepository = sourceRepository + self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation } } diff --git a/Sources/SwiftDocC/Utility/AccessControlLevel.swift b/Sources/SwiftDocC/Utility/AccessControlLevel.swift new file mode 100644 index 0000000000..ed77caf361 --- /dev/null +++ b/Sources/SwiftDocC/Utility/AccessControlLevel.swift @@ -0,0 +1,53 @@ +/* + 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 Foundation + +/// Defines semantic access control levels for symbols +public struct AccessControlLevel: CustomStringConvertible, Codable { + private let rawValue: String + + public var description: String { rawValue } + + public init(_ accessLevel: String) { self.rawValue = accessLevel } + + /// Represents private access level, which normally indicates the symbol is only available in the scope in which it's defined. + public static let `private` = Self.init("private") + /// Represents fileprivate access level, which normally indicates the symbol is only available in the file that defines it. + public static let `fileprivate` = Self.init("fileprivate") + /// Represents internal access level, which normally indicates the symbol is only available in the module that defines it. + public static let `internal` = Self.init("internal") + /// Represents public access level, which normally indicates the symbol is available to clients. + public static let `public` = Self.init("public") + /// Represents open access level, which normally indicates the symbol is extensible and overridable by clients. + public static let `open` = Self.init("open") +} + +extension AccessControlLevel: Comparable { + private var level: UInt? { + switch self { + case .private : return 1 + case .fileprivate: return 2 + case .internal: return 3 + case .public: return 4 + case .open: return 5 + default: + assertionFailure("Unknown access control level was (with representation \(description)) used in comparison") + return nil + } + } + + /// Compares the restrictiveness of access control levels. + public static func < (lhs: Self, rhs: Self) -> Bool { + let lhsLevel = lhs.level ?? .min + let rhsLevel = rhs.level ?? .min + return lhsLevel < rhsLevel + } +} diff --git a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift index ba9cc94713..d3899fa6bd 100644 --- a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift +++ b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift @@ -650,6 +650,144 @@ class ConvertServiceTests: XCTestCase { } } + func testConvertsSymbolPageThatHasExpandedDocumentation() throws { + let symbolGraphFile = Bundle.module.url( + forResource: "mykit-one-symbol", + withExtension: "symbols.json", + subdirectory: "Test Resources" + )! + + let symbolGraph = try Data(contentsOf: symbolGraphFile) + + let request = ConvertRequest( + bundleInfo: testBundleInfo, + externalIDsToConvert: ["s:5MyKit0A5ClassC10myFunctionyyF"], + documentPathsToConvert: [], + symbolGraphs: [symbolGraph], + markupFiles: [], + miscResourceURLs: [], + symbolIdentifiersWithExpandedDocumentation: [ + "s:5MyKit0A5ClassC10myFunctionyyF": + ConvertRequest.ExpandedDocumentationRequirements(accessControlLevel: .public) + ] + ) + + try processAndAssert(request: request) { message in + XCTAssertEqual(message.type, "convert-response") + XCTAssertEqual(message.identifier, "test-identifier-response") + + let renderNodes = try JSONDecoder().decode( + ConvertResponse.self, from: XCTUnwrap(message.payload)).renderNodes + + let data = try XCTUnwrap(renderNodes.first) + let renderNode = try JSONDecoder().decode(RenderNode.self, from: data) + + XCTAssertFalse(renderNode.metadata.hasNoExpandedDocumentation) + } + } + + func testConvertsSymbolPageThatDoesNotMeetAccessLevelRequirementForExpandedDocumentation() throws { + let symbolGraphFile = Bundle.module.url( + forResource: "mykit-one-symbol", + withExtension: "symbols.json", + subdirectory: "Test Resources" + )! + + let symbolGraph = try Data(contentsOf: symbolGraphFile) + + let request = ConvertRequest( + bundleInfo: testBundleInfo, + externalIDsToConvert: ["s:5MyKit0A5ClassC10myFunctionyyF"], + documentPathsToConvert: [], + symbolGraphs: [symbolGraph], + markupFiles: [], + miscResourceURLs: [], + symbolIdentifiersWithExpandedDocumentation: [ + "s:5MyKit0A5ClassC10myFunctionyyF": + ConvertRequest.ExpandedDocumentationRequirements(accessControlLevel: .open) + ] // This symbol is public + ) + + try processAndAssert(request: request) { message in + XCTAssertEqual(message.type, "convert-response") + XCTAssertEqual(message.identifier, "test-identifier-response") + + let renderNodes = try JSONDecoder().decode( + ConvertResponse.self, from: XCTUnwrap(message.payload)).renderNodes + + let data = try XCTUnwrap(renderNodes.first) + let renderNode = try JSONDecoder().decode(RenderNode.self, from: data) + + XCTAssertTrue(renderNode.metadata.hasNoExpandedDocumentation) + } + } + + func testConvertsSymbolPageThatHasDoesNotHaveExpandedDocumentation() throws { + let symbolGraphFile = Bundle.module.url( + forResource: "mykit-one-symbol", + withExtension: "symbols.json", + subdirectory: "Test Resources" + )! + + let symbolGraph = try Data(contentsOf: symbolGraphFile) + + let request = ConvertRequest( + bundleInfo: testBundleInfo, + externalIDsToConvert: ["s:5MyKit0A5ClassC10myFunctionyyF"], + documentPathsToConvert: [], + symbolGraphs: [symbolGraph], + markupFiles: [], + miscResourceURLs: [], + symbolIdentifiersWithExpandedDocumentation: [:] + ) + + try processAndAssert(request: request) { message in + XCTAssertEqual(message.type, "convert-response") + XCTAssertEqual(message.identifier, "test-identifier-response") + + let renderNodes = try JSONDecoder().decode( + ConvertResponse.self, from: XCTUnwrap(message.payload)).renderNodes + + let data = try XCTUnwrap(renderNodes.first) + let renderNode = try JSONDecoder().decode(RenderNode.self, from: data) + + XCTAssert(renderNode.metadata.hasNoExpandedDocumentation) + } + } + + func testConvertsSymbolPageForRequestThatDoesNotSpecifyExpandedDocumentation() throws { + let symbolGraphFile = Bundle.module.url( + forResource: "mykit-one-symbol", + withExtension: "symbols.json", + subdirectory: "Test Resources" + )! + + let symbolGraph = try Data(contentsOf: symbolGraphFile) + + let request = ConvertRequest( + bundleInfo: testBundleInfo, + externalIDsToConvert: ["s:5MyKit0A5ClassC10myFunctionyyF"], + documentPathsToConvert: [], + symbolGraphs: [symbolGraph], + markupFiles: [], + miscResourceURLs: [], + symbolIdentifiersWithExpandedDocumentation: nil + ) + + try processAndAssert(request: request) { message in + XCTAssertEqual(message.type, "convert-response") + XCTAssertEqual(message.identifier, "test-identifier-response") + + let renderNodes = try JSONDecoder().decode( + ConvertResponse.self, from: XCTUnwrap(message.payload)).renderNodes + + let data = try XCTUnwrap(renderNodes.first) + let renderNode = try JSONDecoder().decode(RenderNode.self, from: data) + + XCTAssertFalse(renderNode.metadata.hasNoExpandedDocumentation) + } + } + func testConvertSingleArticlePage() throws { let articleFile = Bundle.module.url( forResource: "StandaloneArticle", diff --git a/Tests/SwiftDocCTests/Rendering/RenderMetadataTests.swift b/Tests/SwiftDocCTests/Rendering/RenderMetadataTests.swift index 3fae04848c..66937e5f55 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderMetadataTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderMetadataTests.swift @@ -202,4 +202,25 @@ class RenderMetadataTests: XCTestCase { metadata.titleVariants = testTitleVariants XCTAssertEqual(metadata.title, "Default title") } + + func testEncodesHasExpandedDocumentation() throws { + var metadata = RenderMetadata() + metadata.hasNoExpandedDocumentation = true + + XCTAssert( + try JSONDecoder().decode( + RenderMetadata.self, + from: JSONEncoder().encode(metadata) + ).hasNoExpandedDocumentation + ) + } + + func testDecodesMissingExpandedDocumentationAsFalse() throws { + XCTAssertFalse( + try JSONDecoder().decode( + RenderMetadata.self, + from: "{}".data(using: .utf8)! + ).hasNoExpandedDocumentation + ) + } } diff --git a/Tests/SwiftDocCTests/Utility/AccessControlLevelTests.swift b/Tests/SwiftDocCTests/Utility/AccessControlLevelTests.swift new file mode 100644 index 0000000000..2eba085ef9 --- /dev/null +++ b/Tests/SwiftDocCTests/Utility/AccessControlLevelTests.swift @@ -0,0 +1,36 @@ +/* + 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 XCTest +@testable import SwiftDocC + +class AccessControlLevelTests: XCTestCase { + private var expectedOrdering: [AccessControlLevel] = [ + .private, + .fileprivate, + .internal, + .public, + .open + ] + + func testOrderingIsRespected() { + for (leftIndex, lhs) in expectedOrdering.enumerated() { + for (rightIndex, rhs) in expectedOrdering.enumerated() { + if leftIndex < rightIndex { + XCTAssertLessThan(lhs, rhs) + } else if leftIndex == rightIndex { + XCTAssertEqual(lhs, rhs) + } else { + XCTAssertGreaterThan(lhs, rhs) + } + } + } + } +}