@@ -66,11 +66,15 @@ public enum TopicReferenceResolutionResult: Hashable, CustomStringConvertible {
6666 }
6767}
6868
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+ /// > Important: This type has copy-on-write semantics.
7478public struct ResolvedTopicReference : Hashable , Codable , Equatable , CustomStringConvertible {
7579 typealias ReferenceBundleIdentifier = String
7680 typealias ReferenceKey = String
@@ -92,30 +96,34 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
9296 return url? . scheme? . lowercased ( ) == ResolvedTopicReference . urlScheme
9397 }
9498
99+ /// The storage for the resolved topic reference's state.
100+ let _storage : Storage
101+
95102 /// The identifier of the bundle that owns this documentation topic.
96103 public var bundleIdentifier : String {
97- didSet { updateURL ( ) }
104+ return _storage . bundleIdentifier
98105 }
99106
100107 /// The absolute path from the bundle to this topic, delimited by `/`.
101108 public var path : String {
102- didSet { updateURL ( ) }
109+ return _storage . path
103110 }
104111
105112 /// A URL fragment referring to a resource in the topic.
106113 public var fragment : String ? {
107- didSet { updateURL ( ) }
114+ return _storage . fragment
108115 }
109116
110117 /// The source language for which this topic is relevant.
111118 public var sourceLanguage : SourceLanguage {
112119 // Return Swift by default to maintain backwards-compatibility.
113- get { sourceLanguages. contains ( . swift) ? . swift : sourceLanguages. first! }
114- set { sourceLanguages. insert ( newValue) }
120+ return sourceLanguages. contains ( . swift) ? . swift : sourceLanguages. first!
115121 }
116122
117123 /// The source languages for which this topic is relevant.
118- public var sourceLanguages : Set < SourceLanguage >
124+ public var sourceLanguages : Set < SourceLanguage > {
125+ return _storage. sourceLanguages
126+ }
119127
120128 /// - Note: The `path` parameter is escaped to a path readable string.
121129 public init ( bundleIdentifier: String , path: String , fragment: String ? = nil , sourceLanguage: SourceLanguage ) {
@@ -132,20 +140,20 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
132140 return
133141 }
134142
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 ( )
143+ _storage = Storage (
144+ bundleIdentifier: bundleIdentifier ,
145+ path: urlReadablePath ,
146+ fragment : urlReadableFragment ,
147+ sourceLanguages : sourceLanguages
148+ )
141149
142150 // Cache the reference
143151 Self . sharedPool. sync { $0 [ bundleIdentifier, default: [ : ] ] [ key] = self }
144152 }
145153
146154 private static func cacheKey(
147- path: String ,
148- fragment: String ? ,
155+ urlReadablePath path: String ,
156+ urlReadableFragment fragment: String ? ,
149157 sourceLanguages: Set < SourceLanguage >
150158 ) -> String {
151159 let sourceLanguagesString = sourceLanguages. map ( \. id) . sorted ( ) . joined ( separator: " - " )
@@ -158,28 +166,19 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
158166 }
159167
160168 /// 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
169+ public var url : URL {
170+ return _storage. url
172171 }
173172
174173 /// 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 ] ( )
174+ var pathComponents : [ String ] {
175+ return _storage . pathComponents
176+ }
178177
179178 /// 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 = " "
179+ var absoluteString : String {
180+ return _storage . absoluteString
181+ }
183182
184183 enum CodingKeys : CodingKey {
185184 case url, interfaceLanguage
@@ -284,6 +283,25 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
284283 return newReference
285284 }
286285
286+ /// Returns a topic reference based on the current one that includes the given source languages.
287+ ///
288+ /// If the current topic reference already includes the given source languages, this returns
289+ /// the original topic reference.
290+ public func addingSourceLanguages( _ sourceLanguages: Set < SourceLanguage > ) -> ResolvedTopicReference {
291+ let combinedSourceLanguages = self . sourceLanguages. union ( sourceLanguages)
292+
293+ guard combinedSourceLanguages != self . sourceLanguages else {
294+ return self
295+ }
296+
297+ return ResolvedTopicReference (
298+ bundleIdentifier: bundleIdentifier,
299+ urlReadablePath: path,
300+ urlReadableFragment: fragment,
301+ sourceLanguages: combinedSourceLanguages
302+ )
303+ }
304+
287305 /// The last path component of this topic reference.
288306 public var lastPathComponent : String {
289307 // There is always at least one component, so we can unwrap `last`.
@@ -322,15 +340,48 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
322340 // which languages they are available in.
323341
324342 public func hash( into hasher: inout Hasher ) {
325- hasher. combine ( bundleIdentifier)
326- hasher. combine ( path)
327- hasher. combine ( fragment)
343+ hasher. combine ( _storage. identifierPathAndFragment)
328344 }
329345
330346 public static func == ( lhs: ResolvedTopicReference , rhs: ResolvedTopicReference ) -> Bool {
331- return lhs. bundleIdentifier == rhs. bundleIdentifier
332- && lhs. path == rhs. path
333- && lhs. fragment == rhs. fragment
347+ return lhs. _storage. identifierPathAndFragment == rhs. _storage. identifierPathAndFragment
348+ }
349+
350+ /// Storage for a resolved topic reference's state.
351+ ///
352+ /// This is a reference type which allows ``ResolvedTopicReference`` to have copy-on-write behavior.
353+ class Storage {
354+ let bundleIdentifier : String
355+ let path : String
356+ let fragment : String ?
357+ let sourceLanguages : Set < SourceLanguage >
358+ let url : URL
359+ let pathComponents : [ String ]
360+ let absoluteString : String
361+
362+ let identifierPathAndFragment : String
363+
364+ init (
365+ bundleIdentifier: String ,
366+ path: String ,
367+ fragment: String ? = nil ,
368+ sourceLanguages: Set < SourceLanguage >
369+ ) {
370+ self . bundleIdentifier = bundleIdentifier
371+ self . path = path
372+ self . fragment = fragment
373+ self . sourceLanguages = sourceLanguages
374+ self . identifierPathAndFragment = " \( bundleIdentifier) \( path) \( fragment ?? " " ) "
375+
376+ var components = URLComponents ( )
377+ components. scheme = ResolvedTopicReference . urlScheme
378+ components. host = bundleIdentifier
379+ components. path = path
380+ components. fragment = fragment
381+ url = components. url!
382+ pathComponents = url. pathComponents
383+ absoluteString = url. absoluteString
384+ }
334385 }
335386}
336387
0 commit comments