@@ -66,11 +66,24 @@ public enum TopicReferenceResolutionResult: Hashable, CustomStringConvertible {
66
66
}
67
67
}
68
68
69
- /**
70
- A reference to a piece of documentation which has been verified to exist.
71
-
72
- A `ResolvedTopicReference` refers to some piece of documentation, such as an article or symbol. Once an `UnresolvedTopicReference` has been resolved to this type, it should be guaranteed that the content backing the documentation is available (i.e. there is a file on disk or data in memory ready to be recalled at any time).
73
- */
69
+ /// A reference to a piece of documentation which has been verified to exist.
70
+ ///
71
+ /// A `ResolvedTopicReference` refers to some piece of documentation, such as an article or symbol.
72
+ /// Once an `UnresolvedTopicReference` has been resolved to this type, it should be guaranteed
73
+ /// that the content backing the documentation is available
74
+ /// (i.e. there is a file on disk or data in memory ready to be
75
+ /// recalled at any time).
76
+ ///
77
+ /// ## Implementation Details
78
+ ///
79
+ /// `ResolvedTopicReference` is effectively a wrapper around Foundation's `URL` and,
80
+ /// because of this, it exposes an API very similar to `URL` and does not allow direct modification
81
+ /// of its properties. This immutability brings performance benefits and communicates with
82
+ /// user's of the API that doing something like adding a path component
83
+ /// is a potentially expensive operation, just as it is on `URL`.
84
+ ///
85
+ /// > Important: This type has copy-on-write semantics and wraps an underlying class to store
86
+ /// > its data.
74
87
public struct ResolvedTopicReference : Hashable , Codable , Equatable , CustomStringConvertible {
75
88
typealias ReferenceBundleIdentifier = String
76
89
typealias ReferenceKey = String
@@ -92,30 +105,34 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
92
105
return url? . scheme? . lowercased ( ) == ResolvedTopicReference . urlScheme
93
106
}
94
107
108
+ /// The storage for the resolved topic reference's state.
109
+ let _storage : Storage
110
+
95
111
/// The identifier of the bundle that owns this documentation topic.
96
112
public var bundleIdentifier : String {
97
- didSet { updateURL ( ) }
113
+ return _storage . bundleIdentifier
98
114
}
99
115
100
116
/// The absolute path from the bundle to this topic, delimited by `/`.
101
117
public var path : String {
102
- didSet { updateURL ( ) }
118
+ return _storage . path
103
119
}
104
120
105
121
/// A URL fragment referring to a resource in the topic.
106
122
public var fragment : String ? {
107
- didSet { updateURL ( ) }
123
+ return _storage . fragment
108
124
}
109
125
110
126
/// The source language for which this topic is relevant.
111
127
public var sourceLanguage : SourceLanguage {
112
128
// Return Swift by default to maintain backwards-compatibility.
113
- get { sourceLanguages. contains ( . swift) ? . swift : sourceLanguages. first! }
114
- set { sourceLanguages. insert ( newValue) }
129
+ return sourceLanguages. contains ( . swift) ? . swift : sourceLanguages. first!
115
130
}
116
131
117
132
/// The source languages for which this topic is relevant.
118
- public var sourceLanguages : Set < SourceLanguage >
133
+ public var sourceLanguages : Set < SourceLanguage > {
134
+ return _storage. sourceLanguages
135
+ }
119
136
120
137
/// - Note: The `path` parameter is escaped to a path readable string.
121
138
public init ( bundleIdentifier: String , path: String , fragment: String ? = nil , sourceLanguage: SourceLanguage ) {
@@ -132,20 +149,20 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
132
149
return
133
150
}
134
151
135
- // Create a new reference
136
- self . bundleIdentifier = bundleIdentifier
137
- self . path = urlReadablePath ( path)
138
- self . fragment = fragment . map { urlReadableFragment ( $0 ) }
139
- self . sourceLanguages = sourceLanguages
140
- updateURL ( )
152
+ _storage = Storage (
153
+ bundleIdentifier: bundleIdentifier ,
154
+ path: urlReadablePath ,
155
+ fragment : urlReadableFragment ,
156
+ sourceLanguages : sourceLanguages
157
+ )
141
158
142
159
// Cache the reference
143
160
Self . sharedPool. sync { $0 [ bundleIdentifier, default: [ : ] ] [ key] = self }
144
161
}
145
162
146
163
private static func cacheKey(
147
- path: String ,
148
- fragment: String ? ,
164
+ urlReadablePath path: String ,
165
+ urlReadableFragment fragment: String ? ,
149
166
sourceLanguages: Set < SourceLanguage >
150
167
) -> String {
151
168
let sourceLanguagesString = sourceLanguages. map ( \. id) . sorted ( ) . joined ( separator: " - " )
@@ -158,28 +175,19 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
158
175
}
159
176
160
177
/// The topic URL as you would write in a link.
161
- private ( set) public var url : URL ! = nil
162
-
163
- private mutating func updateURL( ) {
164
- var components = URLComponents ( )
165
- components. scheme = ResolvedTopicReference . urlScheme
166
- components. host = bundleIdentifier
167
- components. path = path
168
- components. fragment = fragment
169
- url = components. url!
170
- pathComponents = url. pathComponents
171
- absoluteString = url. absoluteString
178
+ public var url : URL {
179
+ return _storage. url
172
180
}
173
181
174
182
/// A list of the reference path components.
175
- /// > Note: This value is updated inside `updateURL()` to avoid
176
- /// accessing the property on `URL`.
177
- private ( set ) var pathComponents = [ String ] ( )
183
+ var pathComponents : [ String ] {
184
+ return _storage . pathComponents
185
+ }
178
186
179
187
/// A string representation of `url`.
180
- /// > Note: This value is updated inside `updateURL()` to avoid
181
- /// accessing the property on `URL`.
182
- private ( set ) var absoluteString = " "
188
+ var absoluteString : String {
189
+ return _storage . absoluteString
190
+ }
183
191
184
192
enum CodingKeys : CodingKey {
185
193
case url, interfaceLanguage
@@ -284,6 +292,25 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
284
292
return newReference
285
293
}
286
294
295
+ /// Returns a topic reference based on the current one that includes the given source languages.
296
+ ///
297
+ /// If the current topic reference already includes the given source languages, this returns
298
+ /// the original topic reference.
299
+ public func addingSourceLanguages( _ sourceLanguages: Set < SourceLanguage > ) -> ResolvedTopicReference {
300
+ let combinedSourceLanguages = self . sourceLanguages. union ( sourceLanguages)
301
+
302
+ guard combinedSourceLanguages != self . sourceLanguages else {
303
+ return self
304
+ }
305
+
306
+ return ResolvedTopicReference (
307
+ bundleIdentifier: bundleIdentifier,
308
+ urlReadablePath: path,
309
+ urlReadableFragment: fragment,
310
+ sourceLanguages: combinedSourceLanguages
311
+ )
312
+ }
313
+
287
314
/// The last path component of this topic reference.
288
315
public var lastPathComponent : String {
289
316
// There is always at least one component, so we can unwrap `last`.
@@ -322,15 +349,48 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
322
349
// which languages they are available in.
323
350
324
351
public func hash( into hasher: inout Hasher ) {
325
- hasher. combine ( bundleIdentifier)
326
- hasher. combine ( path)
327
- hasher. combine ( fragment)
352
+ hasher. combine ( _storage. identifierPathAndFragment)
328
353
}
329
354
330
355
public static func == ( lhs: ResolvedTopicReference , rhs: ResolvedTopicReference ) -> Bool {
331
- return lhs. bundleIdentifier == rhs. bundleIdentifier
332
- && lhs. path == rhs. path
333
- && lhs. fragment == rhs. fragment
356
+ return lhs. _storage. identifierPathAndFragment == rhs. _storage. identifierPathAndFragment
357
+ }
358
+
359
+ /// Storage for a resolved topic reference's state.
360
+ ///
361
+ /// This is a reference type which allows ``ResolvedTopicReference`` to have copy-on-write behavior.
362
+ class Storage {
363
+ let bundleIdentifier : String
364
+ let path : String
365
+ let fragment : String ?
366
+ let sourceLanguages : Set < SourceLanguage >
367
+ let url : URL
368
+ let pathComponents : [ String ]
369
+ let absoluteString : String
370
+
371
+ let identifierPathAndFragment : String
372
+
373
+ init (
374
+ bundleIdentifier: String ,
375
+ path: String ,
376
+ fragment: String ? = nil ,
377
+ sourceLanguages: Set < SourceLanguage >
378
+ ) {
379
+ self . bundleIdentifier = bundleIdentifier
380
+ self . path = path
381
+ self . fragment = fragment
382
+ self . sourceLanguages = sourceLanguages
383
+ self . identifierPathAndFragment = " \( bundleIdentifier) \( path) \( fragment ?? " " ) "
384
+
385
+ var components = URLComponents ( )
386
+ components. scheme = ResolvedTopicReference . urlScheme
387
+ components. host = bundleIdentifier
388
+ components. path = path
389
+ components. fragment = fragment
390
+ url = components. url!
391
+ pathComponents = url. pathComponents
392
+ absoluteString = url. absoluteString
393
+ }
334
394
}
335
395
}
336
396
0 commit comments