From 4bc4c8a71b7a24d0c0cbbada16637097f1292a30 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Tue, 15 Aug 2017 18:49:29 -0400 Subject: [PATCH] Define IterationKeyedMap --- liquid.go | 7 ++++++ liquid_test.go | 43 +++++++++++++++++++++++++++++++++++++ tags/iteration_tags.go | 23 ++++++++++++++++++-- tags/iteration_tags_test.go | 6 ++++-- 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 liquid_test.go diff --git a/liquid.go b/liquid.go index c8843eb..341e11b 100644 --- a/liquid.go +++ b/liquid.go @@ -10,6 +10,7 @@ package liquid import ( "github.com/osteele/liquid/render" + "github.com/osteele/liquid/tags" ) // Bindings is a map of variable names to values. @@ -32,3 +33,9 @@ type SourceError interface { Path() string LineNumber() int } + +// IterationKeyedMap returns a map whose {% for %} tag iteration values are its keys, instead of [key, value] pairs. +// Use this to create a Go map with the semantics of a Ruby struct drop. +func IterationKeyedMap(m map[string]interface{}) tags.IterationKeyedMap { + return m +} diff --git a/liquid_test.go b/liquid_test.go new file mode 100644 index 0000000..0316b6a --- /dev/null +++ b/liquid_test.go @@ -0,0 +1,43 @@ +package liquid + +import ( + "fmt" + "log" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIterationKeyedMap(t *testing.T) { + vars := map[string]interface{}{ + "keyed_map": IterationKeyedMap(map[string]interface{}{"a": 1, "b": 2}), + } + engine := NewEngine() + tpl, err := engine.ParseTemplate([]byte(`{% for k in keyed_map %}{{ k }}={{ keyed_map[k] }}.{% endfor %}`)) + require.NoError(t, err) + out, err := tpl.RenderString(vars) + require.NoError(t, err) + require.Equal(t, "a=1.b=2.", out) +} + +func ExampleIterationKeyedMap() { + vars := map[string]interface{}{ + "map": map[string]interface{}{"a": 1}, + "keyed_map": IterationKeyedMap(map[string]interface{}{"a": 1}), + } + engine := NewEngine() + out, err := engine.ParseAndRenderString( + `{% for k in map %}{{ k[0] }}={{ k[1] }}.{% endfor %}`, vars) + if err != nil { + log.Fatal(err) + } + fmt.Println(out) + out, err = engine.ParseAndRenderString( + `{% for k in keyed_map %}{{ k }}={{ keyed_map[k] }}.{% endfor %}`, vars) + if err != nil { + log.Fatal(err) + } + fmt.Println(out) + // Output: a=1. + // a=1. +} diff --git a/tags/iteration_tags.go b/tags/iteration_tags.go index 6332bee..581ff85 100644 --- a/tags/iteration_tags.go +++ b/tags/iteration_tags.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "reflect" + "sort" yaml "gopkg.in/yaml.v2" @@ -11,6 +12,9 @@ import ( "github.com/osteele/liquid/render" ) +// An IterationKeyedMap is a map that yields its keys, instead of (key, value) pairs, when iterated. +type IterationKeyedMap map[string]interface{} + const forloopVarName = "forloop" var errLoopContinueLoop = fmt.Errorf("continue outside a loop") @@ -174,6 +178,7 @@ func applyLoopModifiers(loop expressions.Loop, iter iterable) iterable { } return iter } + func makeIterator(value interface{}) iterable { if iter, ok := value.(iterable); ok { return iter @@ -181,8 +186,11 @@ func makeIterator(value interface{}) iterable { if value == nil { return nil } - if ms, ok := value.(yaml.MapSlice); ok { - return mapSliceWrapper{ms} + switch value := value.(type) { + case IterationKeyedMap: + return makeIterationKeyedMap(value) + case yaml.MapSlice: + return mapSliceWrapper{value} } switch reflect.TypeOf(value).Kind() { case reflect.Array, reflect.Slice: @@ -200,6 +208,17 @@ func makeIterator(value interface{}) iterable { } } +func makeIterationKeyedMap(m map[string]interface{}) iterable { + // Iteration chooses a random start, so we need a copy of the keys to iterate through them. + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + // Sorting isn't necessary to match Shopify liquid, but it simplifies debugging. + sort.Strings(keys) + return sliceWrapper(reflect.ValueOf(keys)) +} + type sliceWrapper reflect.Value func (w sliceWrapper) Len() int { return reflect.Value(w).Len() } diff --git a/tags/iteration_tags_test.go b/tags/iteration_tags_test.go index ee9bf04..d3acfec 100644 --- a/tags/iteration_tags_test.go +++ b/tags/iteration_tags_test.go @@ -21,8 +21,9 @@ var iterationTests = []struct{ in, expected string }{ {`{% for a in false %}{{ a }}.{% endfor %}`, ""}, {`{% for a in 2 %}{{ a }}.{% endfor %}`, ""}, {`{% for a in "str" %}{{ a }}.{% endfor %}`, ""}, - {`{% for a in hash %}{{ a[0] }}={{ a[1] }}.{% endfor %}`, "a=1."}, + {`{% for a in map %}{{ a[0] }}={{ a[1] }}.{% endfor %}`, "a=1."}, {`{% for a in map_slice %}{{ a[0] }}={{ a[1] }}.{% endfor %}`, "a=1.b=2."}, + {`{% for k in keyed_map %}{{ k }}={{ keyed_map[k] }}.{% endfor %}`, "a=1.b=2."}, // loop modifiers {`{% for a in array reversed %}{{ a }}.{% endfor %}`, "third.second.first."}, @@ -111,7 +112,8 @@ var iterationErrorTests = []struct{ in, expected string }{ var iterationTestBindings = map[string]interface{}{ "array": []string{"first", "second", "third"}, // hash has only one element, since iteration order is non-deterministic - "hash": map[string]interface{}{"a": 1}, + "map": map[string]interface{}{"a": 1}, + "keyed_map": IterationKeyedMap(map[string]interface{}{"a": 1, "b": 2}), "map_slice": yaml.MapSlice{{Key: "a", Value: 1}, {Key: "b", Value: 2}}, "products": []string{ "Cool Shirt", "Alien Poster", "Batman Poster", "Bullseye Shirt", "Another Classic Vinyl", "Awesome Jeans",