Skip to content

Commit d2c3760

Browse files
authored
Merge pull request #487 from gardener/feat/manifest-weight-ordering
feat: sort manifest node tree by frontmatter weight
2 parents f2ab337 + d92853a commit d2c3760

6 files changed

Lines changed: 289 additions & 0 deletions

File tree

pkg/manifest/manifest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ func ResolveManifest(url string, r registry.Interface, additionalTransformations
429429
validateTreeAfterManifestToNodeTree,
430430
removeFileTreeNodes,
431431
setDefaultProcessor,
432+
resolveOrder,
432433
)
433434
if err != nil {
434435
return nil, err

pkg/manifest/manifest_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ var _ = Describe("Manifest test", func() {
6363
Entry("covering manifest use cases", "manifest"),
6464
Entry("covering fileTree filtering", "fileTree_filtering"),
6565
Entry("covering fileTree combined with manually added file", "filetree_with_manual_file"),
66+
Entry("covering weight-based ordering", "weight_ordering"),
6667
)
6768

6869
Describe("When there are dirs with frontmatter collision", func() {

pkg/manifest/order.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package manifest
6+
7+
import (
8+
"sort"
9+
10+
"github.com/gardener/docforge/pkg/registry"
11+
"k8s.io/klog/v2"
12+
)
13+
14+
// weightOf extracts the numeric weight from a node's frontmatter.
15+
// Returns (value, true) for int or float64 weight values; (0, false) otherwise.
16+
func weightOf(n *Node) (int, bool) {
17+
if n.Frontmatter == nil {
18+
return 0, false
19+
}
20+
raw, ok := n.Frontmatter["weight"]
21+
if !ok || raw == nil {
22+
return 0, false
23+
}
24+
switch v := raw.(type) {
25+
case int:
26+
return v, true
27+
case float64:
28+
return int(v), true
29+
default:
30+
return 0, false
31+
}
32+
}
33+
34+
// resolveOrder sorts node.Structure so that children with a weight frontmatter
35+
// field come first in ascending order, followed by unweighted children in their
36+
// original manifest order. Equal weights preserve manifest order (stable sort).
37+
func resolveOrder(node *Node, _ *Node, _ registry.Interface) (bool, error) {
38+
if len(node.Structure) < 2 {
39+
return false, nil
40+
}
41+
42+
hasWeight := false
43+
hasNoWeight := false
44+
for _, child := range node.Structure {
45+
if _, ok := weightOf(child); ok {
46+
hasWeight = true
47+
} else {
48+
hasNoWeight = true
49+
}
50+
}
51+
if hasWeight && hasNoWeight {
52+
klog.Warningf("mixed weight ordering at %s: some children have 'weight' frontmatter and some do not", node.NodePath())
53+
}
54+
55+
if !hasWeight {
56+
return false, nil
57+
}
58+
59+
sort.SliceStable(node.Structure, func(i, j int) bool {
60+
wi, iHasWeight := weightOf(node.Structure[i])
61+
wj, jHasWeight := weightOf(node.Structure[j])
62+
if iHasWeight && jHasWeight {
63+
return wi < wj
64+
}
65+
// weighted nodes sort before unweighted
66+
return iHasWeight
67+
})
68+
69+
return false, nil
70+
}

pkg/manifest/order_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package manifest
6+
7+
import (
8+
. "github.com/onsi/ginkgo"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
func fileNames(nodes []*Node) []string {
13+
out := make([]string, len(nodes))
14+
for i, n := range nodes {
15+
out[i] = n.File
16+
}
17+
return out
18+
}
19+
20+
func makeWeightedNode(name string, weight interface{}) *Node {
21+
n := &Node{
22+
FileType: FileType{File: name},
23+
Type: "file",
24+
}
25+
if weight != nil {
26+
n.Frontmatter = map[string]interface{}{"weight": weight}
27+
}
28+
return n
29+
}
30+
31+
var _ = Describe("weightOf", func() {
32+
It("returns 0, false for nil Frontmatter", func() {
33+
n := &Node{}
34+
w, ok := weightOf(n)
35+
Expect(ok).To(BeFalse())
36+
Expect(w).To(Equal(0))
37+
})
38+
39+
It("returns 0, false when weight key is absent", func() {
40+
n := &Node{Frontmatter: map[string]interface{}{"title": "foo"}}
41+
w, ok := weightOf(n)
42+
Expect(ok).To(BeFalse())
43+
Expect(w).To(Equal(0))
44+
})
45+
46+
It("returns int weight", func() {
47+
n := &Node{Frontmatter: map[string]interface{}{"weight": 5}}
48+
w, ok := weightOf(n)
49+
Expect(ok).To(BeTrue())
50+
Expect(w).To(Equal(5))
51+
})
52+
53+
It("returns float64 weight converted to int", func() {
54+
n := &Node{Frontmatter: map[string]interface{}{"weight": float64(7)}}
55+
w, ok := weightOf(n)
56+
Expect(ok).To(BeTrue())
57+
Expect(w).To(Equal(7))
58+
})
59+
60+
It("returns 0, false for string weight", func() {
61+
n := &Node{Frontmatter: map[string]interface{}{"weight": "heavy"}}
62+
w, ok := weightOf(n)
63+
Expect(ok).To(BeFalse())
64+
Expect(w).To(Equal(0))
65+
})
66+
67+
It("returns 0, false for bool weight", func() {
68+
n := &Node{Frontmatter: map[string]interface{}{"weight": true}}
69+
w, ok := weightOf(n)
70+
Expect(ok).To(BeFalse())
71+
Expect(w).To(Equal(0))
72+
})
73+
})
74+
75+
var _ = Describe("resolveOrder", func() {
76+
It("preserves manifest order when no children have weight", func() {
77+
parent := &Node{DirType: DirType{Structure: []*Node{
78+
makeWeightedNode("a", nil),
79+
makeWeightedNode("b", nil),
80+
makeWeightedNode("c", nil),
81+
}}}
82+
_, err := resolveOrder(parent, nil, nil)
83+
Expect(err).ToNot(HaveOccurred())
84+
Expect(fileNames(parent.Structure)).To(Equal([]string{"a", "b", "c"}))
85+
})
86+
87+
It("sorts all-weighted children ascending by weight", func() {
88+
parent := &Node{DirType: DirType{Structure: []*Node{
89+
makeWeightedNode("c", 30),
90+
makeWeightedNode("a", 10),
91+
makeWeightedNode("b", 20),
92+
}}}
93+
_, err := resolveOrder(parent, nil, nil)
94+
Expect(err).ToNot(HaveOccurred())
95+
Expect(fileNames(parent.Structure)).To(Equal([]string{"a", "b", "c"}))
96+
})
97+
98+
It("places weighted children before unweighted, preserving unweighted manifest order", func() {
99+
parent := &Node{DirType: DirType{Structure: []*Node{
100+
makeWeightedNode("u1", nil),
101+
makeWeightedNode("w2", 20),
102+
makeWeightedNode("u2", nil),
103+
makeWeightedNode("w1", 10),
104+
}}}
105+
_, err := resolveOrder(parent, nil, nil)
106+
Expect(err).ToNot(HaveOccurred())
107+
Expect(fileNames(parent.Structure)).To(Equal([]string{"w1", "w2", "u1", "u2"}))
108+
})
109+
110+
It("is stable for equal weights, falling back to manifest order", func() {
111+
parent := &Node{DirType: DirType{Structure: []*Node{
112+
makeWeightedNode("first", 10),
113+
makeWeightedNode("second", 10),
114+
makeWeightedNode("third", 10),
115+
}}}
116+
_, err := resolveOrder(parent, nil, nil)
117+
Expect(err).ToNot(HaveOccurred())
118+
Expect(fileNames(parent.Structure)).To(Equal([]string{"first", "second", "third"}))
119+
})
120+
121+
It("accepts float64 weight as YAML often produces", func() {
122+
parent := &Node{DirType: DirType{Structure: []*Node{
123+
makeWeightedNode("b", float64(20)),
124+
makeWeightedNode("a", float64(10)),
125+
}}}
126+
_, err := resolveOrder(parent, nil, nil)
127+
Expect(err).ToNot(HaveOccurred())
128+
Expect(fileNames(parent.Structure)).To(Equal([]string{"a", "b"}))
129+
})
130+
131+
It("treats string or bool weight as absent without panicking", func() {
132+
parent := &Node{DirType: DirType{Structure: []*Node{
133+
makeWeightedNode("a", "heavy"),
134+
makeWeightedNode("b", true),
135+
makeWeightedNode("c", nil),
136+
}}}
137+
Expect(func() {
138+
_, _ = resolveOrder(parent, nil, nil)
139+
}).ToNot(Panic())
140+
Expect(fileNames(parent.Structure)).To(Equal([]string{"a", "b", "c"}))
141+
})
142+
143+
It("does not panic with nil Frontmatter on children", func() {
144+
parent := &Node{DirType: DirType{Structure: []*Node{
145+
{FileType: FileType{File: "x"}, Type: "file"},
146+
{FileType: FileType{File: "y"}, Type: "file"},
147+
}}}
148+
Expect(func() {
149+
_, _ = resolveOrder(parent, nil, nil)
150+
}).ToNot(Panic())
151+
})
152+
153+
It("exits early and returns false when fewer than 2 children", func() {
154+
single := &Node{DirType: DirType{Structure: []*Node{makeWeightedNode("only", 5)}}}
155+
changed, err := resolveOrder(single, nil, nil)
156+
Expect(err).ToNot(HaveOccurred())
157+
Expect(changed).To(BeFalse())
158+
159+
empty := &Node{}
160+
changed, err = resolveOrder(empty, nil, nil)
161+
Expect(err).ToNot(HaveOccurred())
162+
Expect(changed).To(BeFalse())
163+
})
164+
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
structure:
2+
- dir: third
3+
frontmatter:
4+
weight: 30
5+
structure:
6+
- file: ../contents/README.txt
7+
- dir: first
8+
frontmatter:
9+
weight: 10
10+
structure:
11+
- dir: nested-b
12+
frontmatter:
13+
weight: 20
14+
structure:
15+
- file: ../../contents/README.txt
16+
- dir: nested-a
17+
frontmatter:
18+
weight: 10
19+
structure:
20+
- file: ../../contents/README.txt
21+
- dir: second
22+
frontmatter:
23+
weight: 20
24+
structure:
25+
- file: ../contents/README.txt
26+
- dir: unweighted
27+
structure:
28+
- file: ../contents/README.txt
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
- file: README.txt
2+
processor: downloader
3+
type: file
4+
source: https://github.com/gardener/docforge/blob/master/contents/README.txt
5+
path: first/nested-a
6+
- file: README.txt
7+
processor: downloader
8+
type: file
9+
source: https://github.com/gardener/docforge/blob/master/contents/README.txt
10+
path: first/nested-b
11+
- file: README.txt
12+
processor: downloader
13+
type: file
14+
source: https://github.com/gardener/docforge/blob/master/contents/README.txt
15+
path: second
16+
- file: README.txt
17+
processor: downloader
18+
type: file
19+
source: https://github.com/gardener/docforge/blob/master/contents/README.txt
20+
path: third
21+
- file: README.txt
22+
processor: downloader
23+
type: file
24+
source: https://github.com/gardener/docforge/blob/master/contents/README.txt
25+
path: unweighted

0 commit comments

Comments
 (0)