Skip to content

Commit c074a16

Browse files
committed
Array variables PoC
1 parent 331db8f commit c074a16

File tree

4 files changed

+121
-17
lines changed

4 files changed

+121
-17
lines changed

arraytemplate/array_template.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package arraytemplate
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
)
7+
8+
type Mapping func(string) (string, bool)
9+
10+
const (
11+
maxArraySize = 100
12+
13+
substitution = "[_a-zA-Z][_a-zA-Z0-9]*"
14+
)
15+
16+
var (
17+
rawPattern = fmt.Sprintf(
18+
"^\\$(?:(%s)\\[\\*]|{(%s)\\[\\*]})$",
19+
substitution,
20+
substitution,
21+
)
22+
23+
ArraySubstitutionPattern = regexp.MustCompile(rawPattern)
24+
)
25+
26+
func Substitute(template string, mapping Mapping) ([]string, error) {
27+
matches := ArraySubstitutionPattern.FindStringSubmatch(template)
28+
prefix, err := findPrefix(matches)
29+
30+
if err != nil {
31+
return nil, fmt.Errorf("invalid array template: %s", template)
32+
}
33+
34+
arr := make([]string, 0, maxArraySize)
35+
for i := 0; i < maxArraySize; i++ {
36+
key := fmt.Sprintf("%s[%d]", prefix, i)
37+
value, ok := mapping(key)
38+
if !ok {
39+
break
40+
}
41+
arr = append(arr, value)
42+
}
43+
44+
return arr, nil
45+
}
46+
47+
func findPrefix(matches []string) (string, error) {
48+
for _, match := range matches[1:] {
49+
if match != "" {
50+
return match, nil
51+
}
52+
}
53+
54+
return "", fmt.Errorf("could not find a match")
55+
}

interpolation/interpolation.go

+47-8
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ package interpolation
1919
import (
2020
"errors"
2121
"fmt"
22+
"github.com/compose-spec/compose-go/v2/arraytemplate"
2223
"os"
24+
"reflect"
2325

2426
"github.com/compose-spec/compose-go/v2/template"
2527
"github.com/compose-spec/compose-go/v2/tree"
@@ -32,7 +34,12 @@ type Options struct {
3234
// TypeCastMapping maps key paths to functions to cast to a type
3335
TypeCastMapping map[tree.Path]Cast
3436
// Substitution function to use
35-
Substitute func(string, template.Mapping) (string, error)
37+
Substitute func(string, LookupValue) (*SubstitutionResult, error)
38+
}
39+
40+
type SubstitutionResult struct {
41+
String string
42+
Array []string
3643
}
3744

3845
// LookupValue is a function which maps from variable names to values.
@@ -53,7 +60,7 @@ func Interpolate(config map[string]interface{}, opts Options) (map[string]interf
5360
opts.TypeCastMapping = make(map[tree.Path]Cast)
5461
}
5562
if opts.Substitute == nil {
56-
opts.Substitute = template.Substitute
63+
opts.Substitute = DefaultSubstitute
5764
}
5865

5966
out := map[string]interface{}{}
@@ -69,18 +76,30 @@ func Interpolate(config map[string]interface{}, opts Options) (map[string]interf
6976
return out, nil
7077
}
7178

79+
func DefaultSubstitute(t string, lookup LookupValue) (*SubstitutionResult, error) {
80+
if (arraytemplate.ArraySubstitutionPattern).MatchString(t) {
81+
arr, err := arraytemplate.Substitute(t, arraytemplate.Mapping(lookup))
82+
return &SubstitutionResult{Array: arr}, err
83+
}
84+
str, err := template.Substitute(t, template.Mapping(lookup))
85+
return &SubstitutionResult{String: str}, err
86+
}
87+
7288
func recursiveInterpolate(value interface{}, path tree.Path, opts Options) (interface{}, error) {
7389
switch value := value.(type) {
7490
case string:
75-
newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue))
91+
result, err := opts.Substitute(value, opts.LookupValue)
7692
if err != nil {
7793
return value, newPathError(path, err)
7894
}
95+
if result.Array != nil {
96+
return result.Array, nil
97+
}
7998
caster, ok := opts.getCasterForPath(path)
8099
if !ok {
81-
return newValue, nil
100+
return result.String, nil
82101
}
83-
casted, err := caster(newValue)
102+
casted, err := caster(result.String)
84103
if err != nil {
85104
return casted, newPathError(path, fmt.Errorf("failed to cast to expected type: %w", err))
86105
}
@@ -98,13 +117,19 @@ func recursiveInterpolate(value interface{}, path tree.Path, opts Options) (inte
98117
return out, nil
99118

100119
case []interface{}:
101-
out := make([]interface{}, len(value))
102-
for i, elem := range value {
120+
out := make([]interface{}, 0, len(value))
121+
for _, elem := range value {
103122
interpolatedElem, err := recursiveInterpolate(elem, path.Next(tree.PathMatchList), opts)
104123
if err != nil {
105124
return nil, err
106125
}
107-
out[i] = interpolatedElem
126+
if isStringSlice(interpolatedElem) {
127+
for _, nestedElem := range interpolatedElem.([]string) {
128+
out = append(out, nestedElem)
129+
}
130+
} else {
131+
out = append(out, interpolatedElem)
132+
}
108133
}
109134
return out, nil
110135

@@ -113,6 +138,20 @@ func recursiveInterpolate(value interface{}, path tree.Path, opts Options) (inte
113138
}
114139
}
115140

141+
func isStringSlice(value interface{}) bool {
142+
if value == nil {
143+
return false
144+
}
145+
t := reflect.TypeOf(value)
146+
if t.Kind() != reflect.Slice {
147+
return false
148+
}
149+
if t.Elem().Kind() != reflect.String {
150+
return false
151+
}
152+
return true
153+
}
154+
116155
func newPathError(path tree.Path, err error) error {
117156
var ite *template.InvalidTemplateError
118157
switch {

interpolation/interpolation_test.go

+18-7
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ import (
2727
)
2828

2929
var defaults = map[string]string{
30-
"USER": "jenny",
31-
"FOO": "bar",
32-
"count": "5",
30+
"USER": "jenny",
31+
"FOO": "bar",
32+
"count": "5",
33+
"ARR[0]": "zero",
34+
"ARR[1]": "one",
35+
"ARR[2]": "two",
3336
}
3437

3538
func defaultMapping(name string) (string, bool) {
@@ -40,8 +43,11 @@ func defaultMapping(name string) (string, bool) {
4043
func TestInterpolate(t *testing.T) {
4144
services := map[string]interface{}{
4245
"servicea": map[string]interface{}{
43-
"image": "example:${USER}",
44-
"volumes": []interface{}{"$FOO:/target"},
46+
"image": "example:${USER}",
47+
"volumes": []interface{}{
48+
"$FOO:/target",
49+
"$ARR[*]",
50+
},
4551
"logging": map[string]interface{}{
4652
"driver": "${FOO}",
4753
"options": map[string]interface{}{
@@ -52,8 +58,13 @@ func TestInterpolate(t *testing.T) {
5258
}
5359
expected := map[string]interface{}{
5460
"servicea": map[string]interface{}{
55-
"image": "example:jenny",
56-
"volumes": []interface{}{"bar:/target"},
61+
"image": "example:jenny",
62+
"volumes": []interface{}{
63+
"bar:/target",
64+
"zero",
65+
"one",
66+
"two",
67+
},
5768
"logging": map[string]interface{}{
5869
"driver": "bar",
5970
"options": map[string]interface{}{

loader/loader.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import (
3535
"github.com/compose-spec/compose-go/v2/override"
3636
"github.com/compose-spec/compose-go/v2/paths"
3737
"github.com/compose-spec/compose-go/v2/schema"
38-
"github.com/compose-spec/compose-go/v2/template"
3938
"github.com/compose-spec/compose-go/v2/transform"
4039
"github.com/compose-spec/compose-go/v2/tree"
4140
"github.com/compose-spec/compose-go/v2/types"
@@ -351,7 +350,7 @@ func loadModelWithContext(ctx context.Context, configDetails *types.ConfigDetail
351350
func toOptions(configDetails *types.ConfigDetails, options []func(*Options)) *Options {
352351
opts := &Options{
353352
Interpolate: &interp.Options{
354-
Substitute: template.Substitute,
353+
Substitute: interp.DefaultSubstitute,
355354
LookupValue: configDetails.LookupEnv,
356355
TypeCastMapping: interpolateTypeCastMapping,
357356
},

0 commit comments

Comments
 (0)