Skip to content

Commit ff6180b

Browse files
authored
Fix deeply nested comprehensions (#954)
* Fix three-nested comprehensions template evaluation * Support comprehensions of live components
1 parent 2c02901 commit ff6180b

File tree

2 files changed

+279
-12
lines changed

2 files changed

+279
-12
lines changed

Sources/LiveViewNative/Fragment.swift

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ enum Child: Decodable, Equatable {
2424
}
2525
}
2626

27-
fileprivate func buildStringInternal(buffer: inout String, root: Root, templates: Templates?) {
27+
fileprivate func buildStringInternal(buffer: inout String, root: Root, templates: Templates?, componentStatics: ComponentStatics?, childIndex: Int?) {
2828
switch self {
2929
case .fragment(let frag):
30-
frag.buildStringInternal(buffer: &buffer, root: root, templates: templates)
30+
frag.buildStringInternal(buffer: &buffer, root: root, templates: templates, componentStatics: componentStatics, childIndex: childIndex)
3131
case .string(let s):
3232
buffer += s
3333
case .componentID(let cid):
@@ -102,7 +102,7 @@ struct Component: Decodable, Equatable {
102102
buffer += effectiveStatics[0]
103103
for i in children.indices {
104104
// nil because components create their own template contexts
105-
children[i].buildStringInternal(buffer: &buffer, root: root, templates: nil)
105+
children[i].buildStringInternal(buffer: &buffer, root: root, templates: nil, componentStatics: statics, childIndex: i)
106106
buffer += effectiveStatics[i + 1]
107107
}
108108
}
@@ -151,7 +151,7 @@ enum Fragment: Decodable, Equatable {
151151

152152
init(from decoder: Decoder) throws {
153153
let container = try decoder.container(keyedBy: CodingKeys.self)
154-
let statics = try container.decode(Statics.self, forKey: .statics)
154+
let statics = try container.decodeIfPresent(Statics.self, forKey: .statics) ?? .componentRef
155155

156156
if container.contains(.dynamics) {
157157
let dynamics = try container.decode([[Child]].self, forKey: .dynamics)
@@ -170,30 +170,30 @@ enum Fragment: Decodable, Equatable {
170170

171171
func buildString(root: Root) -> String {
172172
var s = ""
173-
buildStringInternal(buffer: &s, root: root, templates: nil)
173+
buildStringInternal(buffer: &s, root: root, templates: nil, componentStatics: nil, childIndex: nil)
174174
return s
175175
}
176176

177177
// todo: track which components are used so we can notify the backend of deleted ones
178-
fileprivate func buildStringInternal(buffer: inout String, root: Root, templates: Templates?) {
179-
let effectiveStatics = statics.effectiveValue(in: templates)
178+
fileprivate func buildStringInternal(buffer: inout String, root: Root, templates: Templates?, componentStatics: ComponentStatics?, childIndex: Int?) {
179+
let effectiveStatics = statics.effectiveValue(in: templates, root: root, componentStatics: componentStatics, childIndex: childIndex)
180180

181181
switch self {
182182
case .regular(let children, statics: _):
183183
assert(effectiveStatics.count == children.count + 1)
184184
buffer += effectiveStatics[0]
185185
for i in children.indices {
186-
children[i].buildStringInternal(buffer: &buffer, root: root, templates: templates)
186+
children[i].buildStringInternal(buffer: &buffer, root: root, templates: templates, componentStatics: componentStatics, childIndex: childIndex)
187187
buffer += effectiveStatics[i + 1]
188188
}
189189

190-
case .comprehension(let dynamics, statics: _, let templates):
190+
case .comprehension(let dynamics, statics: _, let compTemplates):
191191
for dynamicComponents in dynamics {
192192
assert(effectiveStatics.count == dynamicComponents.count + 1)
193193

194194
buffer += effectiveStatics[0]
195195
for i in dynamicComponents.indices {
196-
dynamicComponents[i].buildStringInternal(buffer: &buffer, root: root, templates: templates)
196+
dynamicComponents[i].buildStringInternal(buffer: &buffer, root: root, templates: compTemplates ?? templates, componentStatics: componentStatics, childIndex: childIndex)
197197
buffer += effectiveStatics[i + 1]
198198
}
199199
}
@@ -263,6 +263,7 @@ enum Fragment: Decodable, Equatable {
263263
enum Statics: Decodable, Equatable {
264264
case statics([String])
265265
case templateRef(Int)
266+
case componentRef
266267

267268
init(from decoder: Decoder) throws {
268269
let container = try decoder.singleValueContainer()
@@ -273,7 +274,7 @@ enum Statics: Decodable, Equatable {
273274
}
274275
}
275276

276-
func effectiveValue(in templates: Templates?) -> [String] {
277+
func effectiveValue(in templates: Templates?, root: Root, componentStatics: ComponentStatics?, childIndex: Int?) -> [String] {
277278
switch self {
278279
case .statics(let statics):
279280
return statics
@@ -283,6 +284,15 @@ enum Statics: Decodable, Equatable {
283284
}
284285

285286
return templates
287+
case .componentRef:
288+
guard case let .componentRef(componentId) = componentStatics,
289+
let component = root.components?[componentId],
290+
let childIndex,
291+
case let .fragment(fragment) = component.children[childIndex]
292+
else {
293+
preconditionFailure("Static cannot reference non-existent component")
294+
}
295+
return fragment.statics.effectiveValue(in: templates, root: root, componentStatics: component.statics, childIndex: childIndex)
286296
}
287297
}
288298
}

Tests/LiveViewNativeTests/FragmentTests.swift

Lines changed: 258 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// FragmentTests.swift
3-
//
3+
//
44
//
55
// Created by Shadowfacts on 2/28/22.
66
//
@@ -365,6 +365,161 @@ class FragmentTests: XCTestCase {
365365
)
366366
}
367367

368+
func testDecodeComponentWithDynamicsIterated() {
369+
let data = #"""
370+
{
371+
"0": {
372+
"0": {
373+
"d": [
374+
[
375+
1
376+
],
377+
[
378+
2
379+
],
380+
[
381+
3
382+
]
383+
],
384+
"s": [
385+
"\n ",
386+
"\n"
387+
]
388+
},
389+
"s": [
390+
"",
391+
""
392+
]
393+
},
394+
"c": {
395+
"1": {
396+
"0": {
397+
"d": [
398+
[
399+
"1"
400+
],
401+
[
402+
"2"
403+
],
404+
[
405+
"3"
406+
]
407+
],
408+
"s": [
409+
"\n <Text>Item ",
410+
"</Text>\n "
411+
]
412+
},
413+
"s": [
414+
"<Group>\n ",
415+
"\n</Group>"
416+
]
417+
},
418+
"2": {
419+
"0": {
420+
"d": [
421+
[
422+
"1"
423+
],
424+
[
425+
"2"
426+
],
427+
[
428+
"3"
429+
]
430+
]
431+
},
432+
"s": 1
433+
},
434+
"3": {
435+
"0": {
436+
"d": [
437+
[
438+
"1"
439+
],
440+
[
441+
"2"
442+
],
443+
[
444+
"3"
445+
]
446+
]
447+
},
448+
"s": 1
449+
}
450+
},
451+
"s": [
452+
"",
453+
""
454+
]
455+
}
456+
"""#.data(using: .utf8)!
457+
XCTAssertEqual(
458+
try decoder.decode(Root.self, from: data),
459+
Root(
460+
fragment: .regular(
461+
children: [
462+
.fragment(
463+
.regular(
464+
children: [
465+
.fragment(
466+
.comprehension(
467+
dynamics: [
468+
[.componentID(1)], [.componentID(2)], [.componentID(3)]
469+
],
470+
statics: .statics(["\n ", "\n"]),
471+
templates: nil
472+
)
473+
)
474+
],
475+
statics: .statics(["", ""])
476+
)
477+
)
478+
],
479+
statics: .statics(["", ""])
480+
),
481+
components: [
482+
1: Component(
483+
children: [
484+
.fragment(
485+
.comprehension(
486+
dynamics: [[.string("1")], [.string("2")], [.string("3")]],
487+
statics: .statics(["\n <Text>Item ", "</Text>\n "]),
488+
templates: nil
489+
)
490+
)
491+
],
492+
statics: .statics(["<Group>\n ", "\n</Group>"])
493+
),
494+
2: Component(
495+
children: [
496+
.fragment(
497+
.comprehension(
498+
dynamics: [[.string("1")], [.string("2")], [.string("3")]],
499+
statics: .componentRef,
500+
templates: nil
501+
)
502+
)
503+
],
504+
statics: .componentRef(1)
505+
),
506+
3: Component(
507+
children: [
508+
.fragment(
509+
.comprehension(
510+
dynamics: [[.string("1")], [.string("2")], [.string("3")]],
511+
statics: .componentRef,
512+
templates: nil
513+
)
514+
)
515+
],
516+
statics: .componentRef(1)
517+
)
518+
]
519+
)
520+
)
521+
}
522+
368523
func testBuildStringStatic() {
369524
XCTAssertEqual(
370525
Fragment.regular(children: [], statics: ["test"]).buildString(),
@@ -564,4 +719,106 @@ class FragmentTests: XCTestCase {
564719
"\n \n 0\n \n 1\n \n\n \n 0\n \n 1\n \n"
565720
)
566721
}
722+
723+
func testBuildStringWithThreeNestedComprehensions() throws {
724+
XCTAssertEqual(
725+
Root(
726+
fragment: .comprehension(
727+
dynamics: [
728+
[
729+
.fragment(.comprehension(
730+
dynamics: [
731+
[.fragment(.comprehension(dynamics: [[], []], statics: .templateRef(0), templates: nil))],
732+
[.fragment(.comprehension(dynamics: [[], []], statics: .templateRef(0), templates: nil))]
733+
],
734+
statics: .templateRef(1),
735+
templates: nil
736+
))
737+
],
738+
[
739+
.fragment(.comprehension(
740+
dynamics: [
741+
[.fragment(.comprehension(dynamics: [[], []], statics: .templateRef(0), templates: nil))],
742+
[.fragment(.comprehension(dynamics: [[], []], statics: .templateRef(0), templates: nil))]
743+
],
744+
statics: .templateRef(1),
745+
templates: nil
746+
))
747+
]
748+
],
749+
statics: .statics(["<", ">"]),
750+
templates: Templates(templates: [0: ["0"], 1: ["[", "]"]])
751+
),
752+
components: nil
753+
).buildString(),
754+
"<[00][00]><[00][00]>"
755+
)
756+
}
757+
758+
func testBuildStringWithComponentComprehension() throws {
759+
XCTAssertEqual(
760+
Root(
761+
fragment: .regular(
762+
children: [
763+
.fragment(
764+
.regular(
765+
children: [
766+
.fragment(
767+
.comprehension(
768+
dynamics: [
769+
[.componentID(1)], [.componentID(2)], [.componentID(3)]
770+
],
771+
statics: .statics(["<", ">"]),
772+
templates: nil
773+
)
774+
)
775+
],
776+
statics: .statics(["*", "*"])
777+
)
778+
)
779+
],
780+
statics: .statics(["-", "-"])
781+
),
782+
components: [
783+
1: Component(
784+
children: [
785+
.fragment(
786+
.comprehension(
787+
dynamics: [[.string("1")], [.string("2")], [.string("3")]],
788+
statics: .statics(["{", "}"]),
789+
templates: nil
790+
)
791+
)
792+
],
793+
statics: .statics(["[", "]"])
794+
),
795+
2: Component(
796+
children: [
797+
.fragment(
798+
.comprehension(
799+
dynamics: [[.string("1")], [.string("2")], [.string("3")]],
800+
statics: .componentRef,
801+
templates: nil
802+
)
803+
)
804+
],
805+
statics: .componentRef(1)
806+
),
807+
3: Component(
808+
children: [
809+
.fragment(
810+
.comprehension(
811+
dynamics: [[.string("1")], [.string("2")], [.string("3")]],
812+
statics: .componentRef,
813+
templates: nil
814+
)
815+
)
816+
],
817+
statics: .componentRef(1)
818+
)
819+
]
820+
).buildString(),
821+
"-*<[{1}{2}{3}]><[{1}{2}{3}]><[{1}{2}{3}]>*-"
822+
)
823+
}
567824
}

0 commit comments

Comments
 (0)