-
Notifications
You must be signed in to change notification settings - Fork 137
/
Copy pathRenderNode+Coding.swift
250 lines (220 loc) · 10.8 KB
/
RenderNode+Coding.swift
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
*/
public 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
}
}
}