Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 10ddda6

Browse files
authored
Merge pull request #401 from mikeparker/yatee-preserve-order
Add functionality to yatee to preserve order in templated YAML files.
2 parents fd067a7 + f8c6fbf commit 10ddda6

2 files changed

Lines changed: 114 additions & 37 deletions

File tree

pkg/yatee/yatee.go

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/docker/app/internal/yaml"
11+
yml "gopkg.in/yaml.v2"
1112
)
1213

1314
const (
@@ -295,7 +296,7 @@ func recurseList(input []interface{}, settings map[string]interface{}, flattened
295296
var res []interface{}
296297
for _, v := range input {
297298
switch vv := v.(type) {
298-
case map[interface{}]interface{}:
299+
case yml.MapSlice:
299300
newv, err := recurse(vv, settings, flattened, o)
300301
if err != nil {
301302
return nil, err
@@ -337,15 +338,17 @@ func recurseList(input []interface{}, settings map[string]interface{}, flattened
337338

338339
// FIXME complexity on this is 47… get it lower than 16
339340
// nolint: gocyclo
340-
func recurse(input map[interface{}]interface{}, settings map[string]interface{}, flattened map[string]interface{}, o options) (map[interface{}]interface{}, error) {
341-
res := make(map[interface{}]interface{})
342-
for k, v := range input {
341+
func recurse(input yml.MapSlice, settings map[string]interface{}, flattened map[string]interface{}, o options) (yml.MapSlice, error) {
342+
res := yml.MapSlice{}
343+
for _, kvp := range input {
344+
k := kvp.Key
345+
v := kvp.Value
343346
rk := k
344347
kstr, isks := k.(string)
345348
if isks {
346349
trimed := strings.TrimLeft(kstr, " ")
347350
if strings.HasPrefix(trimed, "@switch ") {
348-
mii, ok := v.(map[interface{}]interface{})
351+
mii, ok := v.(yml.MapSlice)
349352
if !ok {
350353
return nil, fmt.Errorf("@switch value must be a mapping")
351354
}
@@ -355,7 +358,9 @@ func recurse(input map[interface{}]interface{}, settings map[string]interface{},
355358
}
356359
var defaultValue interface{}
357360
hit := false
358-
for sk, sv := range mii {
361+
for _, sval := range mii {
362+
sk := sval.Key
363+
sv := sval.Value
359364
ssk, ok := sk.(string)
360365
if !ok {
361366
return nil, fmt.Errorf("@switch entry key must be a string")
@@ -365,28 +370,28 @@ func recurse(input map[interface{}]interface{}, settings map[string]interface{},
365370
}
366371
if ssk == key {
367372
hit = true
368-
svv, ok := sv.(map[interface{}]interface{})
373+
svv, ok := sv.(yml.MapSlice)
369374
if !ok {
370375
return nil, fmt.Errorf("@switch entry must be a mapping")
371376
}
372-
for valk, valv := range svv {
373-
res[valk] = valv
377+
for _, vval := range svv {
378+
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
374379
}
375380
}
376381
}
377382
if !hit && defaultValue != nil {
378-
svv, ok := defaultValue.(map[interface{}]interface{})
383+
svv, ok := defaultValue.(yml.MapSlice)
379384
if !ok {
380385
return nil, fmt.Errorf("@switch entry must be a mapping")
381386
}
382-
for valk, valv := range svv {
383-
res[valk] = valv
387+
for _, vval := range svv {
388+
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
384389
}
385390
}
386391
continue
387392
}
388393
if strings.HasPrefix(trimed, "@for ") {
389-
mii, ok := v.(map[interface{}]interface{})
394+
mii, ok := v.(yml.MapSlice)
390395
if !ok {
391396
return nil, fmt.Errorf("@for value must be a mapping")
392397
}
@@ -416,8 +421,8 @@ func recurse(input map[interface{}]interface{}, settings map[string]interface{},
416421
if err != nil {
417422
return nil, err
418423
}
419-
for valk, valv := range val {
420-
res[valk] = valv
424+
for _, vval := range val {
425+
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
421426
}
422427
}
423428
} else {
@@ -429,8 +434,8 @@ func recurse(input map[interface{}]interface{}, settings map[string]interface{},
429434
if err != nil {
430435
return nil, err
431436
}
432-
for valk, valv := range val {
433-
res[valk] = valv
437+
for _, vval := range val {
438+
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
434439
}
435440
}
436441
}
@@ -441,7 +446,7 @@ func recurse(input map[interface{}]interface{}, settings map[string]interface{},
441446
if err != nil {
442447
return nil, err
443448
}
444-
mii, ok := v.(map[interface{}]interface{})
449+
mii, ok := v.(yml.MapSlice)
445450
if !ok {
446451
return nil, fmt.Errorf("@if value must be a mapping")
447452
}
@@ -450,20 +455,26 @@ func recurse(input map[interface{}]interface{}, settings map[string]interface{},
450455
if err != nil {
451456
return nil, err
452457
}
453-
for valk, valv := range val {
454-
if valk != "@else" {
455-
res[valk] = valv
458+
for _, vval := range val {
459+
if vval.Key != "@else" {
460+
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
456461
}
457462
}
458463
} else {
459-
elseClause, ok := mii["@else"]
460-
if ok {
461-
elseDict, ok := elseClause.(map[interface{}]interface{})
464+
var elseClause interface{}
465+
for _, miiv := range mii {
466+
if miiv.Key == "@else" {
467+
elseClause = miiv.Value
468+
break
469+
}
470+
}
471+
if elseClause != nil {
472+
elseDict, ok := elseClause.(yml.MapSlice)
462473
if !ok {
463474
return nil, fmt.Errorf("@else value must be a mapping")
464475
}
465-
for valk, valv := range elseDict {
466-
res[valk] = valv
476+
for _, vval := range elseDict {
477+
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
467478
}
468479
}
469480
}
@@ -476,26 +487,26 @@ func recurse(input map[interface{}]interface{}, settings map[string]interface{},
476487
rk = rstr
477488
}
478489
switch vv := v.(type) {
479-
case map[interface{}]interface{}:
490+
case yml.MapSlice:
480491
newv, err := recurse(vv, settings, flattened, o)
481492
if err != nil {
482493
return nil, err
483494
}
484-
res[rk] = newv
495+
res = append(res, yml.MapItem{Key: rk, Value: newv})
485496
case []interface{}:
486497
newv, err := recurseList(vv, settings, flattened, o)
487498
if err != nil {
488499
return nil, err
489500
}
490-
res[rk] = newv
501+
res = append(res, yml.MapItem{Key: rk, Value: newv})
491502
case string:
492503
vvv, err := eval(vv, flattened, o)
493504
if err != nil {
494505
return nil, err
495506
}
496-
res[rk] = vvv
507+
res = append(res, yml.MapItem{Key: rk, Value: vvv})
497508
default:
498-
res[rk] = v
509+
res = append(res, yml.MapItem{Key: rk, Value: v})
499510
}
500511
}
501512
return res, nil
@@ -521,23 +532,55 @@ func ProcessStrings(input, settings string) (string, error) {
521532
return string(sres), nil
522533
}
523534

524-
// Process resolves input templated yaml using values given in settings
525-
func Process(inputString string, settings map[string]interface{}, opts ...string) (map[interface{}]interface{}, error) {
535+
// ProcessWithOrder resolves input templated yaml using values given in settings, returning a MapSlice with order preserved
536+
func ProcessWithOrder(inputString string, settings map[string]interface{}, opts ...string) (yml.MapSlice, error) {
526537
var o options
527538
for _, v := range opts {
528539
switch v {
529540
case OptionErrOnMissingKey:
530541
o.errOnMissingKey = true
531542
default:
532-
return nil, fmt.Errorf("unknown option '%s'", v)
543+
return nil, fmt.Errorf("unknown option %q", v)
533544
}
534545
}
535-
input := make(map[interface{}]interface{})
536-
err := yaml.Unmarshal([]byte(inputString), input)
546+
var input yml.MapSlice
547+
err := yaml.Unmarshal([]byte(inputString), &input)
537548
if err != nil {
538549
return nil, err
539550
}
540551
flattened := make(map[string]interface{})
541552
flatten(settings, flattened, "")
542553
return recurse(input, settings, flattened, o)
543554
}
555+
556+
// Process resolves input templated yaml using values given in settings, returning a map
557+
func Process(inputString string, settings map[string]interface{}, opts ...string) (map[interface{}]interface{}, error) {
558+
mapSlice, err := ProcessWithOrder(inputString, settings, opts...)
559+
if err != nil {
560+
return nil, err
561+
}
562+
563+
res, err := convert(mapSlice)
564+
if err != nil {
565+
return nil, err
566+
}
567+
return res, nil
568+
}
569+
570+
func convert(mapSlice yml.MapSlice) (map[interface{}]interface{}, error) {
571+
res := make(map[interface{}]interface{})
572+
for _, kv := range mapSlice {
573+
v := kv.Value
574+
castValue, ok := v.(yml.MapSlice)
575+
if !ok {
576+
res[kv.Key] = kv.Value
577+
} else {
578+
recursed, err := convert(castValue)
579+
if err != nil {
580+
return nil, err
581+
}
582+
res[kv.Key] = recursed
583+
}
584+
}
585+
return res, nil
586+
}

pkg/yatee/yatee_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ func testProcess(t *testing.T, input, output, settings, error string) {
4343
} else {
4444
assert.Equal(t, err.Error(), error)
4545
}
46-
4746
}
4847

4948
func TestProcess(t *testing.T) {
@@ -144,3 +143,38 @@ ab:
144143
settings,
145144
"eval loop detected")
146145
}
146+
147+
func testProcessWithOrder(t *testing.T, input, output, error string) {
148+
settings := make(map[string]interface{})
149+
150+
res, err := ProcessWithOrder(input, settings)
151+
152+
assert.NilError(t, err, "Error processing input: "+input)
153+
sres, err := yaml.Marshal(res)
154+
assert.NilError(t, err)
155+
assert.Equal(t, output, string(sres), "Input was:"+string(sres)+"\nOutput was:"+output)
156+
}
157+
158+
func TestProcessWithOrder(t *testing.T) {
159+
// Test ordering is preserved inside nested structures
160+
testProcessWithOrder(t,
161+
`parent:
162+
bb: true
163+
aa: false
164+
`, `parent:
165+
bb: true
166+
aa: false
167+
`, "")
168+
169+
// Test ordering is preserved at the top level
170+
testProcessWithOrder(t,
171+
`bbb:
172+
nested: true
173+
aaa:
174+
nested: false
175+
`, `bbb:
176+
nested: true
177+
aaa:
178+
nested: false
179+
`, "")
180+
}

0 commit comments

Comments
 (0)