1
0
mirror of https://github.com/danog/liquid.git synced 2024-11-26 23:14:39 +01:00

Define IterationKeyedMap

This commit is contained in:
Oliver Steele 2017-08-15 18:49:29 -04:00
parent c49d979750
commit 4bc4c8a71b
4 changed files with 75 additions and 4 deletions

View File

@ -10,6 +10,7 @@ package liquid
import ( import (
"github.com/osteele/liquid/render" "github.com/osteele/liquid/render"
"github.com/osteele/liquid/tags"
) )
// Bindings is a map of variable names to values. // Bindings is a map of variable names to values.
@ -32,3 +33,9 @@ type SourceError interface {
Path() string Path() string
LineNumber() int 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
}

43
liquid_test.go Normal file
View File

@ -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.
}

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"reflect" "reflect"
"sort"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
@ -11,6 +12,9 @@ import (
"github.com/osteele/liquid/render" "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" const forloopVarName = "forloop"
var errLoopContinueLoop = fmt.Errorf("continue outside a loop") var errLoopContinueLoop = fmt.Errorf("continue outside a loop")
@ -174,6 +178,7 @@ func applyLoopModifiers(loop expressions.Loop, iter iterable) iterable {
} }
return iter return iter
} }
func makeIterator(value interface{}) iterable { func makeIterator(value interface{}) iterable {
if iter, ok := value.(iterable); ok { if iter, ok := value.(iterable); ok {
return iter return iter
@ -181,8 +186,11 @@ func makeIterator(value interface{}) iterable {
if value == nil { if value == nil {
return nil return nil
} }
if ms, ok := value.(yaml.MapSlice); ok { switch value := value.(type) {
return mapSliceWrapper{ms} case IterationKeyedMap:
return makeIterationKeyedMap(value)
case yaml.MapSlice:
return mapSliceWrapper{value}
} }
switch reflect.TypeOf(value).Kind() { switch reflect.TypeOf(value).Kind() {
case reflect.Array, reflect.Slice: 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 type sliceWrapper reflect.Value
func (w sliceWrapper) Len() int { return reflect.Value(w).Len() } func (w sliceWrapper) Len() int { return reflect.Value(w).Len() }

View File

@ -21,8 +21,9 @@ var iterationTests = []struct{ in, expected string }{
{`{% for a in false %}{{ a }}.{% endfor %}`, ""}, {`{% for a in false %}{{ a }}.{% endfor %}`, ""},
{`{% for a in 2 %}{{ a }}.{% endfor %}`, ""}, {`{% for a in 2 %}{{ a }}.{% endfor %}`, ""},
{`{% for a in "str" %}{{ 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 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 // loop modifiers
{`{% for a in array reversed %}{{ a }}.{% endfor %}`, "third.second.first."}, {`{% 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{}{ var iterationTestBindings = map[string]interface{}{
"array": []string{"first", "second", "third"}, "array": []string{"first", "second", "third"},
// hash has only one element, since iteration order is non-deterministic // 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}}, "map_slice": yaml.MapSlice{{Key: "a", Value: 1}, {Key: "b", Value: 2}},
"products": []string{ "products": []string{
"Cool Shirt", "Alien Poster", "Batman Poster", "Bullseye Shirt", "Another Classic Vinyl", "Awesome Jeans", "Cool Shirt", "Alien Poster", "Batman Poster", "Bullseye Shirt", "Another Classic Vinyl", "Awesome Jeans",