File: DocCServer.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (201 lines) | stat: -rw-r--r-- 9,199 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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 the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
@preconcurrency import SwiftDocC

package struct DocCServer {
  private let server: DocumentationServer

  init(peer peerServer: DocumentationServer? = nil, qualityOfService: DispatchQoS) {
    server = DocumentationServer.createDefaultServer(qualityOfService: qualityOfService, peer: peerServer)
  }

  /// Sends a request to SwiftDocC that will convert in-memory documentation.
  ///
  /// - Parameters:
  ///   - externalIDsToConvert: The external IDs of the symbols to convert.
  ///   - documentPathsToConvert: The paths of the documentation nodes to convert.
  ///   - includeRenderReferenceStore: Whether the conversion's render reference store should be included in
  ///     the response.
  ///   - documentationBundleLocation: The file location of the documentation bundle to convert, if any.
  ///   - documentationBundleDisplayName: The name of the documentation bundle to convert.
  ///   - documentationBundleIdentifier: The identifier of the documentation bundle to convert.
  ///   - symbolGraphs: The symbol graph data included in the documentation bundle to convert.
  ///   - overridingDocumentationComments: The mapping of external symbol identifiers to lines of a documentation
  ///     comment that overrides the value in the symbol graph.
  ///   - emitSymbolSourceFileURIs: Whether the conversion's rendered documentation should include source file
  ///     location metadata.
  ///   - 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.
  ///   - convertRequestIdentifier: A unique identifier for the request. Can be used to map additional data alongside
  ///     a request for use later on.
  /// - Throws: A ``DocCServerError`` representing the type of error that occurred.
  func convert(
    externalIDsToConvert: [String]?,
    documentPathsToConvert: [String]?,
    includeRenderReferenceStore: Bool,
    documentationBundleLocation: URL?,
    documentationBundleDisplayName: String,
    documentationBundleIdentifier: String,
    symbolGraphs: [Data],
    overridingDocumentationComments: [String: [String]] = [:],
    emitSymbolSourceFileURIs: Bool,
    markupFiles: [Data],
    tutorialFiles: [Data],
    convertRequestIdentifier: String
  ) async throws(DocCServerError) -> ConvertResponse {
    let request = ConvertRequest(
      bundleInfo: DocumentationBundle.Info(
        displayName: documentationBundleDisplayName,
        id: DocumentationBundle.Identifier(rawValue: documentationBundleIdentifier),
        defaultCodeListingLanguage: nil,
        defaultAvailability: nil,
        defaultModuleKind: nil
      ),
      externalIDsToConvert: externalIDsToConvert,
      documentPathsToConvert: documentPathsToConvert,
      includeRenderReferenceStore: includeRenderReferenceStore,
      bundleLocation: documentationBundleLocation,
      symbolGraphs: symbolGraphs,
      overridingDocumentationComments: overridingDocumentationComments.mapValues {
        $0.map { ConvertRequest.Line(text: $0) }
      },
      knownDisambiguatedSymbolPathComponents: nil,
      emitSymbolSourceFileURIs: emitSymbolSourceFileURIs,
      markupFiles: markupFiles,
      tutorialFiles: tutorialFiles,
      miscResourceURLs: [],
      symbolIdentifiersWithExpandedDocumentation: nil
    )
    let response = try await makeRequest(
      messageType: ConvertService.convertMessageType,
      messageIdentifier: convertRequestIdentifier,
      request: request
    )
    guard let responsePayload = response.payload else {
      throw .unexpectedlyNilPayload(response.type.rawValue)
    }
    // Check for an error response from SwiftDocC
    guard response.type != ConvertService.convertResponseErrorMessageType else {
      let convertServiceError: ConvertServiceError
      do {
        convertServiceError = try JSONDecoder().decode(ConvertServiceError.self, from: responsePayload)
      } catch {
        throw .messagePayloadDecodingFailure(messageType: response.type.rawValue, decodingError: error)
      }
      throw .internalError(convertServiceError)
    }
    guard response.type == ConvertService.convertResponseMessageType else {
      throw .unknownMessageType(response.type.rawValue)
    }
    // Decode the SwiftDocC.ConvertResponse and wrap it in our own Sendable type
    let doccConvertResponse: SwiftDocC.ConvertResponse
    do {
      doccConvertResponse = try JSONDecoder().decode(SwiftDocC.ConvertResponse.self, from: responsePayload)
    } catch {
      throw .decodingFailure(error)
    }
    return ConvertResponse(doccConvertResponse: doccConvertResponse)
  }

  private func makeRequest<Request: Encodable & Sendable>(
    messageType: DocumentationServer.MessageType,
    messageIdentifier: String,
    request: Request
  ) async throws(DocCServerError) -> DocumentationServer.Message {
    let result: Result<DocumentationServer.Message, DocCServerError> = await withCheckedContinuation { continuation in
      // Encode the request in JSON format
      let encodedPayload: Data
      do {
        encodedPayload = try JSONEncoder().encode(request)
      } catch {
        return continuation.resume(returning: .failure(.encodingFailure(error)))
      }
      // Encode the full message in JSON format
      let message = DocumentationServer.Message(
        type: messageType,
        identifier: messageIdentifier,
        payload: encodedPayload
      )
      let encodedMessage: Data
      do {
        encodedMessage = try JSONEncoder().encode(message)
      } catch {
        return continuation.resume(returning: .failure(.encodingFailure(error)))
      }
      // Send the request to the server and decode the response
      server.process(encodedMessage) { response in
        do {
          let decodedMessage = try JSONDecoder().decode(DocumentationServer.Message.self, from: response)
          continuation.resume(returning: .success(decodedMessage))
        } catch {
          continuation.resume(returning: .failure(.decodingFailure(error)))
        }
      }
    }
    return try result.get()
  }
}

/// A Sendable wrapper around ``SwiftDocC.ConvertResponse``
struct ConvertResponse: Sendable, Codable {
  /// The render nodes that were created as part of the conversion, encoded as JSON.
  let renderNodes: [Data]

  /// The render reference store that was created as part of the bundle's conversion, encoded as JSON.
  ///
  /// The ``RenderReferenceStore`` contains compiled information for documentation nodes that were registered as part of
  /// the conversion. This information can be used as a lightweight index of the available documentation content in the bundle that's
  /// been converted.
  let renderReferenceStore: Data?

  /// Creates a conversion response given the render nodes that were created as part of the conversion.
  init(renderNodes: [Data], renderReferenceStore: Data? = nil) {
    self.renderNodes = renderNodes
    self.renderReferenceStore = renderReferenceStore
  }

  /// Creates a conversion response given a SwiftDocC conversion response
  init(doccConvertResponse: SwiftDocC.ConvertResponse) {
    self.renderNodes = doccConvertResponse.renderNodes
    self.renderReferenceStore = doccConvertResponse.renderReferenceStore
  }
}

/// Represents a potential error that the ``DocCServer`` could encounter while processing requests
enum DocCServerError: LocalizedError {
  case encodingFailure(_ encodingError: Error)
  case decodingFailure(_ decodingError: Error)
  case messagePayloadDecodingFailure(messageType: String, decodingError: Error)
  case unknownMessageType(_ messageType: String)
  case unexpectedlyNilPayload(_ messageType: String)
  case internalError(_ underlyingError: LocalizedError)

  var errorDescription: String? {
    switch self {
    case .encodingFailure(let encodingError):
      return "Failed to encode message: \(encodingError.localizedDescription)"
    case .decodingFailure(let decodingError):
      return "Failed to decode a received message: \(decodingError.localizedDescription)"
    case .messagePayloadDecodingFailure(let messageType, let decodingError):
      return
        "Received a message of type '\(messageType)' and failed to decode its payload: \(decodingError.localizedDescription)."
    case .unknownMessageType(let messageType):
      return "Received an unknown message type: '\(messageType)'."
    case .unexpectedlyNilPayload(let messageType):
      return "Received a message of type '\(messageType)' with a 'nil' payload."
    case .internalError(underlyingError: let underlyingError):
      return underlyingError.errorDescription
    }
  }
}