Skip to content

Commit 8c447b0

Browse files
Support diffing for RenderNode (#696)
These changes implement support to diff any two RenderNode objects, across all their properties, and generate a list of JSON Patches to go from the first RenderNode to the second one. To achieve this: * A new RenderJSONDiffable protocol is defined to ensure similar object types can be diffed. RenderJSONDiffable conformance requires the object to implement difference(from:at:) and optionally isSimilar(to:) methods. This is done for all RenderNode object types and for common built-in types used with JSON. * DifferenceBuilder is defined to help abstracting away the different difference methods. It collects differences between two objects and handles the JSON paths for those differences using addDifferences(atKeyPath:forKey:) method. * The following content is supported for diffing yet: * Language-specific overrides * Tutorials rdar://105186012 Co-authored-by: Maya Epps <[email protected]>
1 parent 7f881ee commit 8c447b0

File tree

55 files changed

+2136
-77
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2136
-77
lines changed

Sources/SwiftDocC/Infrastructure/Communication/Foundation/JSON.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,34 @@ extension JSON {
138138
}
139139

140140
}
141+
142+
extension JSON {
143+
/// An integer coding key.
144+
struct IntegerKey: CodingKey {
145+
var intValue: Int?
146+
var stringValue: String
147+
148+
init(_ value: Int) {
149+
self.intValue = value
150+
self.stringValue = value.description
151+
}
152+
153+
init(_ value: String) {
154+
self.intValue = nil
155+
self.stringValue = value
156+
}
157+
158+
init?(intValue: Int) {
159+
self.init(intValue)
160+
}
161+
162+
init?(stringValue: String) {
163+
guard let intValue = Int(stringValue) else {
164+
return nil
165+
}
166+
167+
self.intValue = intValue
168+
self.stringValue = stringValue
169+
}
170+
}
171+
}

Sources/SwiftDocC/Model/Identifier.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,21 @@ extension ResolvedTopicReference {
514514
}
515515
}
516516

517+
extension ResolvedTopicReference: RenderJSONDiffable {
518+
/// Returns the differences between this ResolvedTopicReference and the given one.
519+
func difference(from other: ResolvedTopicReference, at path: CodablePath) -> JSONPatchDifferences {
520+
var diffBuilder = DifferenceBuilder(current: self, other: other, basePath: path)
521+
522+
// The only part of the URL that is encoded to RenderJSON is the absolute string.
523+
diffBuilder.addDifferences(atKeyPath: \.url.absoluteString, forKey: CodingKeys.url)
524+
525+
// The only part of the source language that is encoded to RenderJSON is the id.
526+
diffBuilder.addDifferences(atKeyPath: \.sourceLanguage.id, forKey: CodingKeys.interfaceLanguage)
527+
528+
return diffBuilder.differences
529+
}
530+
}
531+
517532
/// An unresolved reference to a documentation node.
518533
///
519534
/// You can create unresolved references from partial information if that information can be derived from the enclosing context when the

Sources/SwiftDocC/Model/Rendering/Content/RenderInlineContent.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,16 @@ extension Sequence where Element == RenderInlineContent {
217217
return map { $0.plainText }.joined()
218218
}
219219
}
220+
221+
// Diffable conformance
222+
extension RenderInlineContent: RenderJSONDiffable {
223+
/// Returns the differences between this RenderInlineContent and the given one.
224+
func difference(from other: RenderInlineContent, at path: CodablePath) -> JSONPatchDifferences {
225+
var diffBuilder = DifferenceBuilder(current: self, other: other, basePath: path)
226+
227+
diffBuilder.addDifferences(atKeyPath: \.type, forKey: CodingKeys.type)
228+
diffBuilder.addDifferences(atKeyPath: \.plainText, forKey: CodingKeys.text)
229+
230+
return diffBuilder.differences
231+
}
232+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
/// A RenderReference value that can be diffed.
12+
///
13+
/// An `AnyRenderReference` value forwards difference operations to the underlying base type, which implement the difference differently.
14+
struct AnyRenderReference: Encodable, Equatable, RenderJSONDiffable {
15+
var value: RenderReference & Codable
16+
17+
init(_ value: RenderReference & Codable) {
18+
self.value = value
19+
}
20+
21+
func encode(to encoder: Encoder) throws {
22+
try value.encode(to: encoder)
23+
}
24+
25+
/// Forwards the difference methods on to the correct concrete type.
26+
func difference(from other: AnyRenderReference, at path: CodablePath) -> JSONPatchDifferences {
27+
switch (self.value.type, other.value.type) {
28+
29+
// MARK: References
30+
31+
case (.file, .file):
32+
return (value as! FileReference).difference(from: (other.value as! FileReference), at: path)
33+
case (.fileType, .fileType):
34+
return (value as! FileTypeReference).difference(from: (other.value as! FileTypeReference), at: path)
35+
case (.image, .image):
36+
return (value as! ImageReference).difference(from: (other.value as! ImageReference), at: path)
37+
case (.link, .link):
38+
return (value as! LinkReference).difference(from: (other.value as! LinkReference), at: path)
39+
case (.section, .section), (.topic, .topic):
40+
return (value as! TopicRenderReference).difference(from: (other.value as! TopicRenderReference), at: path)
41+
case (.unresolvable, .unresolvable):
42+
return (value as! UnresolvedRenderReference).difference(from: (other.value as! UnresolvedRenderReference), at: path)
43+
case (.video, .video):
44+
return (value as! VideoReference).difference(from: (other.value as! VideoReference), at: path)
45+
46+
// MARK: Tutorial References
47+
48+
case (.download, .download):
49+
return (value as! DownloadReference).difference(from: (other.value as! DownloadReference), at: path)
50+
case (.xcodeRequirement, .xcodeRequirement):
51+
return (value as! XcodeRequirementReference).difference(from: (other.value as! XcodeRequirementReference), at: path)
52+
53+
default:
54+
assertionFailure("Case diffing \(value) with \(other.value) is not implemented.")
55+
return []
56+
}
57+
}
58+
59+
static func == (lhs: AnyRenderReference, rhs: AnyRenderReference) -> Bool {
60+
switch (lhs.value.type, rhs.value.type) {
61+
62+
// MARK: References
63+
64+
case (.file, .file):
65+
return (lhs.value as! FileReference) == (rhs.value as! FileReference)
66+
case (.fileType, .fileType):
67+
return (lhs.value as! FileTypeReference) == (rhs.value as! FileTypeReference)
68+
case (.image, .image):
69+
return (lhs.value as! ImageReference) == (rhs.value as! ImageReference)
70+
case (.link, .link):
71+
return (lhs.value as! LinkReference) == (rhs.value as! LinkReference)
72+
case (.section, .section), (.topic, .topic):
73+
return (lhs.value as! TopicRenderReference) == (rhs.value as! TopicRenderReference)
74+
case (.unresolvable, .unresolvable):
75+
return (lhs.value as! UnresolvedRenderReference) == (rhs.value as! UnresolvedRenderReference)
76+
case (.video, .video):
77+
return (lhs.value as! VideoReference) == (rhs.value as! VideoReference)
78+
79+
// MARK: Tutorial References
80+
81+
case (.download, .download):
82+
return (lhs.value as! DownloadReference) == (rhs.value as! DownloadReference)
83+
case (.xcodeRequirement, .xcodeRequirement):
84+
return (lhs.value as! XcodeRequirementReference) == (rhs.value as! XcodeRequirementReference)
85+
86+
default:
87+
assertionFailure("Case diffing \(lhs.value) with \(rhs.value) is not implemented.")
88+
return false
89+
}
90+
}
91+
92+
func isSimilar(to other: AnyRenderReference) -> Bool {
93+
return self.value.identifier == other.value.identifier
94+
}
95+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
/// A RenderSection value that can be diffed.
12+
///
13+
/// An `AnyRenderSection` value forwards difference operations to the underlying base type, each of which determine the difference differently.
14+
struct AnyRenderSection: Equatable, Encodable, RenderJSONDiffable {
15+
var value: RenderSection
16+
17+
init(_ value: RenderSection) {
18+
self.value = value
19+
}
20+
21+
func encode(to encoder: Encoder) throws {
22+
try value.encode(to: encoder)
23+
}
24+
25+
/// Forwards the difference methods on to the correct concrete type.
26+
func difference(from other: AnyRenderSection, at path: CodablePath) -> JSONPatchDifferences {
27+
switch (self.value.kind, other.value.kind) {
28+
29+
// MARK: Symbol Sections
30+
31+
case (.attributes, .attributes):
32+
return (value as! AttributesRenderSection).difference(from: (other.value as! AttributesRenderSection), at: path)
33+
case (.discussion, .discussion), (.content, .content):
34+
return (value as! ContentRenderSection).difference(from: (other.value as! ContentRenderSection), at: path)
35+
case (.declarations, .declarations):
36+
return (value as! DeclarationsRenderSection).difference(from: (other.value as! DeclarationsRenderSection), at: path)
37+
case (.parameters, .parameters):
38+
return (value as! ParametersRenderSection).difference(from: (other.value as! ParametersRenderSection), at: path)
39+
case (.plistDetails, .plistDetails):
40+
return (value as! PlistDetailsRenderSection).difference(from: (other.value as! PlistDetailsRenderSection), at: path)
41+
case (.possibleValues, .possibleValues):
42+
return (value as! PossibleValuesRenderSection).difference(from: (other.value as! PossibleValuesRenderSection), at: path)
43+
case (.relationships, .relationships):
44+
return (value as! RelationshipsRenderSection).difference(from: (other.value as! RelationshipsRenderSection), at: path)
45+
case (.restBody, .restBody):
46+
return (value as! RESTBodyRenderSection).difference(from: (other.value as! RESTBodyRenderSection), at: path)
47+
case (.restEndpoint, .restEndpoint):
48+
return (value as! RESTEndpointRenderSection).difference(from: (other.value as! RESTEndpointRenderSection), at: path)
49+
case (.restParameters, .restParameters):
50+
return (value as! RESTParametersRenderSection).difference(from: (other.value as! RESTParametersRenderSection), at: path)
51+
case (.restResponses, .restResponses):
52+
return (value as! RESTResponseRenderSection).difference(from: (other.value as! RESTResponseRenderSection), at: path)
53+
case (.sampleDownload, .sampleDownload):
54+
return (value as! SampleDownloadSection).difference(from: (other.value as! SampleDownloadSection), at: path)
55+
case (.taskGroup, .taskGroup):
56+
return (value as! TaskGroupRenderSection).difference(from: (other.value as! TaskGroupRenderSection), at: path)
57+
58+
// MARK: Tutorial Sections
59+
60+
case (.intro, .intro), (.hero, .hero):
61+
return (value as! IntroRenderSection).difference(from: (other.value as! IntroRenderSection), at: path)
62+
case (.assessments, .assessments):
63+
return (value as! TutorialAssessmentsRenderSection).difference(from: (other.value as! TutorialAssessmentsRenderSection), at: path)
64+
case (.tasks, .tasks):
65+
return (value as! TutorialSectionsRenderSection).difference(from: (other.value as! TutorialSectionsRenderSection), at: path)
66+
67+
// MARK: Tutorial Article Sections
68+
69+
case (.articleBody, .articleBody):
70+
return (value as! TutorialArticleSection).difference(from: (other.value as! TutorialArticleSection), at: path)
71+
72+
// MARK: Tutorials Overview Sections
73+
74+
case (.callToAction, .callToAction):
75+
return (value as! CallToActionSection).difference(from: (other.value as! CallToActionSection), at: path)
76+
case (.contentAndMediaGroup, .contentAndMediaGroup):
77+
return (value as! ContentAndMediaGroupSection).difference(from: (other.value as! ContentAndMediaGroupSection), at: path)
78+
case (.contentAndMedia, .contentAndMedia):
79+
return (value as! ContentAndMediaSection).difference(from: (other.value as! ContentAndMediaSection), at: path)
80+
case (.resources, .resources):
81+
return (value as! ResourcesRenderSection).difference(from: (other.value as! ResourcesRenderSection), at: path)
82+
case (.volume, .volume):
83+
return (value as! VolumeRenderSection).difference(from: (other.value as! VolumeRenderSection), at: path)
84+
85+
default:
86+
assertionFailure("Case diffing \(value) with \(other.value) is not implemented.")
87+
return []
88+
}
89+
}
90+
91+
static func == (lhs: AnyRenderSection, rhs: AnyRenderSection) -> Bool {
92+
switch (lhs.value.kind, rhs.value.kind) {
93+
94+
// MARK: Symbol Sections
95+
96+
case (.attributes, .attributes):
97+
return (lhs.value as! AttributesRenderSection) == (rhs.value as! AttributesRenderSection)
98+
case (.discussion, .discussion), (.content, .content):
99+
return (lhs.value as! ContentRenderSection) == (rhs.value as! ContentRenderSection)
100+
case (.declarations, .declarations):
101+
return (lhs.value as! DeclarationsRenderSection) == (rhs.value as! DeclarationsRenderSection)
102+
case (.parameters, .parameters):
103+
return (lhs.value as! ParametersRenderSection) == (rhs.value as! ParametersRenderSection)
104+
case (.plistDetails, .plistDetails):
105+
return (lhs.value as! PlistDetailsRenderSection) == (rhs.value as! PlistDetailsRenderSection)
106+
case (.possibleValues, .possibleValues):
107+
return (lhs.value as! PossibleValuesRenderSection) == (rhs.value as! PossibleValuesRenderSection)
108+
case (.relationships, .relationships):
109+
return (lhs.value as! RelationshipsRenderSection) == (rhs.value as! RelationshipsRenderSection)
110+
case (.restBody, .restBody):
111+
return (lhs.value as! RESTBodyRenderSection) == (rhs.value as! RESTBodyRenderSection)
112+
case (.restEndpoint, .restEndpoint):
113+
return (lhs.value as! RESTEndpointRenderSection) == (rhs.value as! RESTEndpointRenderSection)
114+
case (.restParameters, .restParameters):
115+
return (lhs.value as! RESTParametersRenderSection) == (rhs.value as! RESTParametersRenderSection)
116+
case (.restResponses, .restResponses):
117+
return (lhs.value as! RESTResponseRenderSection) == (rhs.value as! RESTResponseRenderSection)
118+
case (.sampleDownload, .sampleDownload):
119+
return (lhs.value as! SampleDownloadSection) == (rhs.value as! SampleDownloadSection)
120+
case (.taskGroup, .taskGroup):
121+
return (lhs.value as! TaskGroupRenderSection) == (rhs.value as! TaskGroupRenderSection)
122+
123+
// MARK: Tutorial Sections
124+
125+
case (.intro, .intro), (.hero, .hero):
126+
return (lhs.value as! IntroRenderSection) == (rhs.value as! IntroRenderSection)
127+
case (.assessments, .assessments):
128+
return (lhs.value as! TutorialAssessmentsRenderSection) == (rhs.value as! TutorialAssessmentsRenderSection)
129+
case (.tasks, .tasks):
130+
return (lhs.value as! TutorialSectionsRenderSection) == (rhs.value as! TutorialSectionsRenderSection)
131+
132+
// MARK: Tutorial Article Sections
133+
134+
case (.articleBody, .articleBody):
135+
return (lhs.value as! TutorialArticleSection) == (rhs.value as! TutorialArticleSection)
136+
137+
// MARK: Tutorials Overview Sections
138+
139+
case (.callToAction, .callToAction):
140+
return (lhs.value as! CallToActionSection) == (rhs.value as! CallToActionSection)
141+
case (.contentAndMediaGroup, .contentAndMediaGroup):
142+
return (lhs.value as! ContentAndMediaGroupSection) == (rhs.value as! ContentAndMediaGroupSection)
143+
case (.contentAndMedia, .contentAndMedia):
144+
return (lhs.value as! ContentAndMediaSection) == (rhs.value as! ContentAndMediaSection)
145+
case (.resources, .resources):
146+
return (lhs.value as! ResourcesRenderSection) == (rhs.value as! ResourcesRenderSection)
147+
case (.volume, .volume):
148+
return (lhs.value as! VolumeRenderSection) == (rhs.value as! VolumeRenderSection)
149+
150+
default:
151+
assertionFailure("Case diffing \(lhs.value) with \(rhs.value) is not implemented.")
152+
return false
153+
}
154+
}
155+
156+
func isSimilar(to other: AnyRenderSection) -> Bool {
157+
return self.value.kind == other.value.kind
158+
}
159+
}

0 commit comments

Comments
 (0)