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 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
|
/*
This source file is part of the Swift.org open source project
Copyright (c) 2021-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 Swift project authors
*/
import Foundation
/// An environmental variable to control the output formatting of the encoded render JSON.
///
/// If this environment variable is set to "YES", DocC will format render node JSON with spacing and indentation,
/// and sort the keys (on supported platforms), to make it deterministic and easy to read.
let jsonFormattingKey = "DOCC_JSON_PRETTYPRINT"
public internal(set) var shouldPrettyPrintOutputJSON = NSString(string: ProcessInfo.processInfo.environment[jsonFormattingKey] ?? "NO").boolValue
extension CodingUserInfoKey {
/// A user info key to indicate that Render JSON references should not be encoded.
static let skipsEncodingReferences = CodingUserInfoKey(rawValue: "skipsEncodingReferences")!
/// A user info key that encapsulates variant overrides.
///
/// This key is used by encoders to accumulate language-specific variants of documentation in a ``VariantOverrides`` value.
static let variantOverrides = CodingUserInfoKey(rawValue: "variantOverrides")!
static let baseEncodingPath = CodingUserInfoKey(rawValue: "baseEncodingPath")!
/// A user info key to indicate a base path for local asset URLs.
static let assetPrefixComponent = CodingUserInfoKey(rawValue: "assetPrefixComponent")!
}
extension Encoder {
/// The variant overrides accumulated as part of the encoding process.
var userInfoVariantOverrides: VariantOverrides? {
userInfo[.variantOverrides] as? VariantOverrides
}
/// The base path to use when creating dynamic JSON pointers
/// with this encoder.
var baseJSONPatchPath: [String]? {
userInfo[.baseEncodingPath] as? [String]
}
/// A Boolean that is true if this encoder skips the encoding of any render references.
///
/// These references will then be encoded at a later stage by `TopicRenderReferenceEncoder`.
var skipsEncodingReferences: Bool {
userInfo[.skipsEncodingReferences] as? Bool ?? false
}
/// A base path to use when creating destination URLs for local assets (images, videos, downloads, etc.)
var assetPrefixComponent: String? {
userInfo[.assetPrefixComponent] as? String
}
}
extension JSONEncoder {
/// The variant overrides accumulated as part of the encoding process.
var userInfoVariantOverrides: VariantOverrides? {
get {
userInfo[.variantOverrides] as? VariantOverrides
}
set {
userInfo[.variantOverrides] = newValue
}
}
/// The base path to use when creating dynamic JSON pointers
/// with this encoder.
var baseJSONPatchPath: [String]? {
get {
userInfo[.baseEncodingPath] as? [String]
}
set {
userInfo[.baseEncodingPath] = newValue
}
}
/// A Boolean that is true if this encoder skips the encoding any render references.
///
/// These references will then be encoded at a later stage by `TopicRenderReferenceEncoder`.
var skipsEncodingReferences: Bool {
get {
userInfo[.skipsEncodingReferences] as? Bool ?? false
}
set {
userInfo[.skipsEncodingReferences] = newValue
}
}
}
/// A namespace for encoders for render node JSON.
public enum RenderJSONEncoder {
/// Creates a new JSON encoder for render node values.
///
/// Returns an encoder that's configured to encode ``RenderNode`` values.
///
/// > Important: Don't reuse encoders returned by this function to encode multiple render nodes, as the encoder accumulates state during the encoding
/// process which should not be shared in other encoding units. Instead, call this API to create a new encoder for each render node you want to encode.
///
/// - Parameters:
/// - prettyPrint: If `true`, the encoder formats its output to make it easy to read; if `false`, the output is compact.
/// - emitVariantOverrides: Whether the encoder should emit the top-level ``RenderNode/variantOverrides`` property that holds language-specific documentation data.
/// - assetPrefixComponent: A path component to include in destination URLs for local assets (images, videos, downloads, etc.)
/// - Returns: The new JSON encoder.
public static func makeEncoder(
prettyPrint: Bool = shouldPrettyPrintOutputJSON,
emitVariantOverrides: Bool = true,
assetPrefixComponent: String? = nil
) -> JSONEncoder {
let encoder = JSONEncoder()
if prettyPrint {
if #available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
} else {
encoder.outputFormatting = [.prettyPrinted]
}
}
if emitVariantOverrides {
encoder.userInfo[.variantOverrides] = VariantOverrides()
}
if let bundleIdentifier = assetPrefixComponent {
encoder.userInfo[.assetPrefixComponent] = bundleIdentifier
}
return encoder
}
}
/// A namespace for decoders for render node JSON.
public enum RenderJSONDecoder {
/// Creates a new JSON decoder for render node values.
///
/// - Returns: The new JSON decoder.
public static func makeDecoder() -> JSONDecoder {
JSONDecoder()
}
}
// This API improves the encoding/decoding to or from JSON with better error messages.
public extension RenderNode {
/// An error that describes failures that may occur while encoding or decoding a render node.
enum CodingError: DescribedError {
/// JSON data could not be decoded as a render node value.
case decoding(description: String, context: DecodingError.Context)
/// A render node value could not be encoded as JSON.
case encoding(description: String, context: EncodingError.Context)
/// A user-facing description of the coding error.
public var errorDescription: String {
switch self {
case .decoding(let description, let context):
let contextMessage = context.codingPath.map { $0.stringValue }.joined(separator: ", ")
if contextMessage.isEmpty { return description }
return "\(description)\nKeypath: \(contextMessage)"
case .encoding(let description, let context):
let contextMessage = context.codingPath.map { $0.stringValue }.joined(separator: ", ")
if contextMessage.isEmpty { return description }
return "\(description)\nKeypath: \(contextMessage)"
}
}
}
/// Decodes a render node value from the given JSON data.
///
/// - Parameters:
/// - data: The JSON data to decode.
/// - decoder: The object that decodes the JSON data.
/// - Throws: A ``CodingError`` in case the decoder is unable to find a key or value in the data, the type of a decoded value is wrong, or the data is corrupted.
/// - Returns: The decoded render node value.
static func decode(fromJSON data: Data, with decoder: JSONDecoder = RenderJSONDecoder.makeDecoder()) throws -> RenderNode {
do {
return try decoder.decode(RenderNode.self, from: data)
} catch {
if let error = error as? DecodingError {
switch error {
case .dataCorrupted(let context):
throw CodingError.decoding(description: "\(error.localizedDescription)\n\(context.debugDescription)", context: context)
case .keyNotFound(let key, let context):
throw CodingError.decoding(description: "\(error.localizedDescription)\nKey: \(key.stringValue).\n\(context.debugDescription)", context: context)
case .valueNotFound(_, let context):
throw CodingError.decoding(description: "\(error.localizedDescription)\n\(context.debugDescription)", context: context)
case .typeMismatch(_, let context):
throw CodingError.decoding(description: "\(error.localizedDescription)\n\(context.debugDescription)", context: context)
@unknown default:
// Re-throws if an unknown decoding error happens.
throw error
}
}
// Re-throws if any other error happens.
throw error
}
}
/// Encodes a render node value as JSON data.
///
/// - Parameters:
/// - encoder: The object that encodes the render node.
/// - renderReferenceCache: A cache for encoded render reference data. When encoding a large number of render nodes, use the same cache instance
/// to avoid encoding the same reference objects repeatedly.
/// - Throws: A ``CodingError`` in case the encoder couldn't encode the render node.
/// - Returns: The data for the encoded render node.
func encodeToJSON(
with encoder: JSONEncoder = RenderJSONEncoder.makeEncoder(),
renderReferenceCache: RenderReferenceCache? = nil
) throws -> Data {
do {
// If there is no topic reference cache, just encode the reference.
// To skim a little off the duration we first do a quick check if the key is present at all.
guard let renderReferenceCache else {
return try encoder.encode(self)
}
// Since we're using a reference cache, skip encoding the references and encode them separately.
encoder.skipsEncodingReferences = true
var renderNodeData = try encoder.encode(self)
// Add render references, using the encoder cache.
try TopicRenderReferenceEncoder.addRenderReferences(
to: &renderNodeData,
references: references,
encodeAccumulatedVariantOverrides: variantOverrides == nil,
encoder: encoder,
renderReferenceCache: renderReferenceCache
)
return renderNodeData
} catch {
if let error = error as? EncodingError {
switch error {
case .invalidValue(_, let context):
throw CodingError.encoding(description: "\(error.localizedDescription)\n\(context.debugDescription)", context: context)
@unknown default:
// Re-throws if an unknown encoding error happens.
throw error
}
}
// Re-throws if any other error happens.
throw error
}
}
}
|