10
10
11
11
import Foundation
12
12
13
+ /// A thread-safe cache for encoded render references.
14
+ public typealias RenderReferenceCache = Synchronized < [ String : ( reference: Data , overrides: [ VariantOverride ] ) ] >
15
+
13
16
enum TopicRenderReferenceEncoder {
14
17
/// Inserts an encoded list of render references to an already encoded as data render node.
15
18
/// - Parameters:
@@ -21,12 +24,17 @@ enum TopicRenderReferenceEncoder {
21
24
static func addRenderReferences(
22
25
to renderNodeData: inout Data ,
23
26
references: [ String : RenderReference ] ,
27
+ encodeAccumulatedVariantOverrides: Bool = false ,
24
28
encoder: JSONEncoder ,
25
- renderReferenceCache referenceCache: Synchronized < [ String : Data ] >
26
- ) {
27
-
29
+ renderReferenceCache referenceCache: RenderReferenceCache
30
+ ) throws {
28
31
guard !references. isEmpty else { return }
29
32
33
+ // Because we'll be clearing the encoder's variant overrides field before
34
+ // encoding each reference, we need to store any existing values now so that
35
+ // when we finally encode the variant overrides we have all relevant values.
36
+ var variantOverrides = encoder. userInfoVariantOverrides? . values ?? [ ]
37
+
30
38
let fragments : Fragments = encoder. outputFormatting. contains ( . prettyPrinted) ? . prettyPrinted : . compact
31
39
32
40
// Remove the final "}"
@@ -49,6 +57,55 @@ enum TopicRenderReferenceEncoder {
49
57
let key = reference. identifier. identifier
50
58
let value : Data
51
59
60
+ // Declare a helper function that we'll use to encode any non-cached references
61
+ // we encounter
62
+
63
+ func encodeRenderReference( cacheKey: String ? = nil ) throws -> Data {
64
+ // Because we're encoding these reference ad-hoc and not as part of a full render
65
+ // node, the `encodingPath` on the encoder will be incorrect. This means that the
66
+ // logic in `VariantEncoder.addVariantsToEncoder(_:pointer:isDefaultValueEncoded:)`
67
+ // will incorrectly set the path and the produced JSON patch we use to switch
68
+ // between language variants will be incorrect.
69
+ //
70
+ // To work around this, we set a `baseJSONPatchPath` property in the encoder's
71
+ // user info dictionary. Then when `addVariantsToEncoder` is called, it prepends
72
+ // this value to the coding path. This way the produced JSON patch will be accurate.
73
+ encoder. baseJSONPatchPath = [
74
+ " references " ,
75
+ reference. identifier. identifier,
76
+ ]
77
+
78
+ // Because we want to cache each render reference with the specific
79
+ // variant overrides it produces, we first clear the encoder's user info
80
+ // fields before encoding.
81
+ //
82
+ // This ensures that the whatever override the user info field holds
83
+ // _after_ we encode, are the ones for this particular reference.
84
+ encoder. userInfoVariantOverrides? . values. removeAll ( )
85
+
86
+ // Encode the reference.
87
+ let encodedReference = try encoder. encode ( CodableRenderReference . init ( reference) )
88
+
89
+ // Add the collected variant overrides to the collection of overrides
90
+ // we're currently tracking.
91
+ if let encodedVariantOverrides = encoder. userInfoVariantOverrides {
92
+ variantOverrides. append ( contentsOf: encodedVariantOverrides. values)
93
+ }
94
+
95
+ // If a cache key was provided, update the cache with the reference and it's
96
+ // overrides.
97
+ if let cacheKey = cacheKey {
98
+ referenceCache. sync { cache in
99
+ cache [ cacheKey] = (
100
+ encodedReference,
101
+ encoder. userInfoVariantOverrides? . values ?? [ ]
102
+ )
103
+ }
104
+ }
105
+
106
+ return encodedReference
107
+ }
108
+
52
109
if let topicReference = reference as? TopicRenderReference {
53
110
if let conformance = topicReference. conformance {
54
111
// In case there is a conformance section, adds conformance hash to the cache key.
@@ -60,20 +117,19 @@ enum TopicRenderReferenceEncoder {
60
117
61
118
let conformanceHash = Checksum . md5 ( of: Data ( conformance. constraints. map ( { $0. plainText } ) . joined ( ) . utf8) )
62
119
let cacheKeyWithConformance = " \( key) : \( conformanceHash) "
63
- if let cached = referenceCache. sync ( { $0 [ cacheKeyWithConformance] } ) {
64
- value = cached
120
+ if let ( reference, overrides) = referenceCache. sync ( { $0 [ cacheKeyWithConformance] } ) {
121
+ value = reference
122
+ variantOverrides. append ( contentsOf: overrides)
65
123
} else {
66
- value = try ! encoder. encode ( CodableRenderReference . init ( reference) )
67
- referenceCache. sync ( { $0 [ cacheKeyWithConformance] = value } )
124
+ value = try encodeRenderReference ( cacheKey: cacheKeyWithConformance)
68
125
}
69
126
70
- } else if let cached = referenceCache. sync ( { $0 [ key] } ) {
127
+ } else if let ( reference , overrides ) = referenceCache. sync ( { $0 [ key] } ) {
71
128
// Use a cached copy if the reference is already encoded.
72
- value = cached
129
+ value = reference
130
+ variantOverrides. append ( contentsOf: overrides)
73
131
} else {
74
- // Encode the reference and add it to the cache.
75
- value = try ! encoder. encode ( CodableRenderReference . init ( reference) )
76
- referenceCache. sync ( { $0 [ key] = value } )
132
+ value = try encodeRenderReference ( cacheKey: key)
77
133
}
78
134
}
79
135
else {
@@ -82,7 +138,7 @@ enum TopicRenderReferenceEncoder {
82
138
// For example:  and 
83
139
// have the same identifier when encoded in the render node where they are used but the reference
84
140
// abstract is not unique within the project.
85
- value = try ! encoder . encode ( CodableRenderReference . init ( reference ) )
141
+ value = try encodeRenderReference ( )
86
142
}
87
143
88
144
renderNodeData. append ( fragments. quote)
@@ -96,32 +152,45 @@ enum TopicRenderReferenceEncoder {
96
152
// Remove the last comma from the list
97
153
renderNodeData. removeLast ( fragments. listDelimiter. count)
98
154
99
- // Append closing "}}"
100
- renderNodeData. append ( fragments. closingBrackets)
155
+ // Append closing "}"
156
+ renderNodeData. append ( fragments. closingBrace)
157
+
158
+ if encodeAccumulatedVariantOverrides, !variantOverrides. isEmpty {
159
+ // Insert the "variantOverrides" key
160
+ renderNodeData. append ( fragments. variantOverridesKey)
161
+ let variantOverrideData = try encoder. encode ( VariantOverrides ( values: variantOverrides) )
162
+ renderNodeData. append ( variantOverrideData)
163
+ }
164
+
165
+ // Append closing "}"
166
+ renderNodeData. append ( fragments. closingBrace)
101
167
}
102
168
103
169
/// Data fragments to use to build a reference list.
104
170
private struct Fragments {
105
171
172
+ let variantOverridesKey : Data
106
173
let referencesKey : Data
107
- let closingBrackets : Data
174
+ let closingBrace : Data
108
175
let listDelimiter : Data
109
176
let quote : Data
110
177
let colon : Data
111
178
112
179
// Compact fragments
113
180
static let compact = Fragments (
181
+ variantOverridesKey: Data ( " , \" variantOverrides \" : " . utf8) ,
114
182
referencesKey: Data ( " , \" references \" :{ " . utf8) ,
115
- closingBrackets : Data ( " } }" . utf8) ,
183
+ closingBrace : Data ( " } " . utf8) ,
116
184
listDelimiter: Data ( " , " . utf8) ,
117
185
quote: Data ( " \" " . utf8) ,
118
186
colon: Data ( " : " . utf8)
119
187
)
120
188
121
189
// Pretty printed fragments
122
190
static let prettyPrinted = Fragments (
191
+ variantOverridesKey: Data ( " , \n \" variantOverrides \" : " . utf8) ,
123
192
referencesKey: Data ( " , \n \" references \" : { \n " . utf8) ,
124
- closingBrackets : Data ( " \n } \n } " . utf8) ,
193
+ closingBrace : Data ( " \n } " . utf8) ,
125
194
listDelimiter: Data ( " , \n " . utf8) ,
126
195
quote: Data ( " \" " . utf8) ,
127
196
colon: Data ( " : " . utf8)
0 commit comments