Skip to content

Commit

Permalink
Implement some filters
Browse files Browse the repository at this point in the history
  • Loading branch information
osteele committed Jun 26, 2017
1 parent e3425cc commit 30211ac
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 31 deletions.
91 changes: 85 additions & 6 deletions chunks/render_test.go
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
)

var chunkTests = []struct{ in, expected string }{
var renderTests = []struct{ in, expected string }{
{"{{12}}", "12"},
{"{{x}}", "123"},
{"{{page.title}}", "Introduction"},
Expand Down Expand Up @@ -38,32 +38,111 @@ var chunkTests = []struct{ in, expected string }{

{"{%assign av = 1%}{{av}}", "1"},
{"{%assign av = obj.a%}{{av}}", "1"},
// "{% assign var = obj.a | sort: 'weight' %}"

// {"{%for a in ar%}{{a}} {{%endfor%}", "first second third "},
}

var chunkTestContext = Context{map[string]interface{}{
var filterTests = []struct{ in, expected string }{
// filters
// {{ product_price | default: 2.99 }}

// list filters
// {{ site.pages | map: 'category' | compact | join "," %}
// {% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.first }}
// {`{{"John, Paul, George, Ringo" | split: ", " | join: "and"}}`, "John and Paul and George and Ringo"},
{`{{ animals | sort | join }}`, "Sally Snake, giraffe, octopus, zebra"},
// {`{{ animals | sort | join: "," }}`, "Sally Snake, giraffe, octopus, zebra"},
// join, last, map, slice, sort, sort_natural, reverse, size, uniq

// string filters
// {{ "/my/fancy/url" | append: ".html" }}
// {% assign filename = "/index.html" %}{{ "website.com" | append: filename }}

// {{ "title" | capitalize }}
// {{ "my great title" | capitalize }}

// {{ "Parker Moore" | downcase }}

// {{ "Have you read 'James & the Giant Peach'?" | escape }}
// {{ "1 < 2 & 3" | escape_once }}
// {{ "1 &lt; 2 &amp; 3" | escape_once }}

// lstrip, newline_to_br, prepend, remove, remove_first, replace, replace_first
// rstrip, split, strip, strip_html, strip_newlines, truncate, truncatewords, upcase
// url_decode, url_encode

// number filters
// {{ -17 | abs }}
// {{ 4 | abs }}
// {{ "-19.86" | abs }}

// {{ 1.2 | ceil }}
// {{ 2.0 | ceil }}
// {{ 183.357 | ceil }}
// {{ "3.5" | ceil }}

// {{ 16 | divided_by: 4 }}
// {{ 5 | divided_by: 3 }}
// {{ 20 | divided_by: 7.0 }}

// {{ 1.2 | floor }}
// {{ 2.0 | floor }}
// {{ 183.357 | floor }}
// minus, modulo, plus, round,times

// date filters
// {{ article.published_at | date: "%a, %b %d, %y" }}
// {{ article.published_at | date: "%Y" }}
// {{ "March 14, 2016" | date: "%b %d, %y" }}
// {{ "now" | date: "%Y-%m-%d %H:%M" }
}

var renderTestContext = Context{map[string]interface{}{
"x": 123,
"obj": map[string]interface{}{
"a": 1,
},
"animals": []string{"zebra", "octopus", "giraffe", "Sally Snake"},
"pages": []map[string]interface{}{
{"category": "business"},
{"category": "celebrities"},
{},
{"category": "lifestyle"},
{"category": "sports"},
{},
{"category": "technology"},
},
"ar": []string{"first", "second", "third"},
"page": map[string]interface{}{
"title": "Introduction",
},
},
}

func TestChunkParser(t *testing.T) {
for i, test := range chunkTests {
func TestRender(t *testing.T) {
for i, test := range renderTests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
tokens := Scan(test.in, "")
// fmt.Println(tokens)
ast, err := Parse(tokens)
require.NoErrorf(t, err, test.in)
// fmt.Println(MustYAML(ast))
buf := new(bytes.Buffer)
err = ast.Render(buf, chunkTestContext)
err = ast.Render(buf, renderTestContext)
require.NoErrorf(t, err, test.in)
require.Equalf(t, test.expected, buf.String(), test.in)
})
}
}

func TestFilters(t *testing.T) {
for i, test := range filterTests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
tokens := Scan(test.in, "")
ast, err := Parse(tokens)
require.NoErrorf(t, err, test.in)
buf := new(bytes.Buffer)
err = ast.Render(buf, renderTestContext)
require.NoErrorf(t, err, test.in)
require.Equalf(t, test.expected, buf.String(), test.in)
})
Expand Down
1 change: 1 addition & 0 deletions expressions/expressions.y
Expand Up @@ -51,6 +51,7 @@ expr:
return nil
}
}
| expr '|' IDENTIFIER { $$ = makeFilter($1, $3) }
| expr '[' expr ']' {
e, i := $1, $3
$$ = func(ctx Context) interface{} {
Expand Down
46 changes: 46 additions & 0 deletions expressions/filters.go
@@ -0,0 +1,46 @@
package expressions

import (
"fmt"
"reflect"
"sort"
"strings"
)

type valueFn func(Context) interface{}

func joinFilter(in []interface{}) interface{} {
a := make([]string, len(in))
for i, x := range in {
a[i] = fmt.Sprint(x)
}
return strings.Join(a, ", ")
}

func sortFilter(in []interface{}) []interface{} {
a := make([]interface{}, len(in))
for i, x := range in {
a[i] = x
}
sort.Sort(sortable(a))
return a
}

var filters = map[string]interface{}{
"join": joinFilter,
"sort": sortFilter,
}

func makeFilter(f valueFn, name string) valueFn {
fn, ok := filters[name]
if !ok {
panic(fmt.Errorf("unknown filter: %s", name))
}
fr := reflect.ValueOf(fn)
return func(ctx Context) interface{} {
args := []interface{}{f(ctx)}
in := convertArguments(fr, args)
r := fr.Call(in)[0]
return r.Interface()
}
}
71 changes: 71 additions & 0 deletions expressions/generics.go
Expand Up @@ -5,6 +5,77 @@ import (
"reflect"
)

type sortable []interface{}

// Len is part of sort.Interface.
func (s sortable) Len() int {
return len(s)
}

// Swap is part of sort.Interface.
func (s sortable) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

// Less is part of sort.Interface.
func (s sortable) Less(i, j int) bool {
return genericSameTypeCompare(s[i], s[j]) < 0
}

// Convert val to the type. This is a more aggressive conversion, that will
// recursively create new map and slice values as necessary. It doesn't
// handle circular references.
func convertType(val interface{}, t reflect.Type) reflect.Value {
r := reflect.ValueOf(val)
if r.Type().ConvertibleTo(t) {
return r.Convert(t)
}
switch t.Kind() {
case reflect.Slice:
if r.Kind() != reflect.Array && r.Kind() != reflect.Slice {
break
}
x := reflect.MakeSlice(t, 0, r.Len())
for i := 0; i < r.Len(); i++ {
c := convertType(r.Index(i).Interface(), t.Elem())
x = reflect.Append(x, c)
}
return x
}
panic(fmt.Errorf("convertType: can't convert %v to %v", val, t))
}

// Convert args to match the input types of fr, which should be a function reflection.
func convertArguments(fv reflect.Value, args []interface{}) []reflect.Value {
rt := fv.Type()
rs := make([]reflect.Value, rt.NumIn())
for i, arg := range args {
if i < rt.NumIn() {
rs[i] = convertType(arg, rt.In(i))
}
}
return rs
}

func genericSameTypeCompare(av, bv interface{}) int {
a, b := reflect.ValueOf(av), reflect.ValueOf(bv)
if a.Kind() != b.Kind() {
panic(fmt.Errorf("different types: %v and %v", a, b))
}
if a == b {
return 0
}
switch a.Kind() {
case reflect.String:
if a.String() < b.String() {
return -1
}
default:
panic(fmt.Errorf("unimplemented generic comparison for %s", a.Kind()))
}
return 1
}

func GenericCompare(a, b reflect.Value) int {
if a.Interface() == b.Interface() {
return 0
Expand Down
61 changes: 36 additions & 25 deletions expressions/y.go
Expand Up @@ -41,6 +41,7 @@ var yyToknames = [...]string{
"'>'",
"';'",
"'='",
"'|'",
"'['",
"']'",
}
Expand All @@ -59,45 +60,46 @@ var yyExca = [...]int{

const yyPrivate = 57344

const yyLast = 30
const yyLast = 35

var yyAct = [...]int{

4, 11, 9, 12, 13, 9, 7, 10, 22, 1,
10, 16, 17, 18, 19, 20, 9, 14, 9, 5,
6, 10, 21, 10, 5, 6, 15, 3, 8, 2,
12, 9, 13, 14, 4, 9, 10, 11, 9, 7,
10, 11, 23, 10, 11, 15, 18, 19, 20, 21,
22, 9, 5, 6, 24, 3, 10, 11, 5, 6,
17, 16, 8, 1, 2,
}
var yyPact = [...]int{

20, -1000, -6, 23, -7, -1000, -1000, -1000, 4, 21,
15, 15, 15, 15, 15, -1000, 7, 9, 9, 9,
-4, -1000, -1000,
18, -1000, -3, 27, -8, -1000, -1000, -1000, 2, 26,
25, 24, 24, 24, 24, 24, -1000, -1000, -4, -1,
-1, -1, 12, -1000, -1000,
}
var yyPgo = [...]int{

0, 0, 29, 9,
0, 4, 34, 33,
}
var yyR1 = [...]int{

0, 3, 3, 1, 1, 1, 1, 2, 2, 2,
2,
0, 3, 3, 1, 1, 1, 1, 1, 2, 2,
2, 2,
}
var yyR2 = [...]int{

0, 2, 5, 1, 1, 3, 4, 1, 3, 3,
3,
0, 2, 5, 1, 1, 3, 3, 4, 1, 3,
3, 3,
}
var yyChk = [...]int{

-1000, -3, -2, 7, -1, 4, 5, 12, 5, 9,
14, 8, 10, 11, 13, 5, -1, -1, -1, -1,
-1, 15, 12,
14, 15, 8, 10, 11, 13, 5, 5, -1, -1,
-1, -1, -1, 16, 12,
}
var yyDef = [...]int{

0, -2, 0, 0, 7, 3, 4, 1, 0, 0,
0, 0, 0, 0, 0, 5, 0, 8, 9, 10,
0, 6, 2,
0, -2, 0, 0, 8, 3, 4, 1, 0, 0,
0, 0, 0, 0, 0, 0, 5, 6, 0, 9,
10, 11, 0, 7, 2,
}
var yyTok1 = [...]int{

Expand All @@ -110,7 +112,10 @@ var yyTok1 = [...]int{
10, 13, 11, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 14, 3, 15,
3, 15, 3, 16, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 14,
}
var yyTok2 = [...]int{

Expand Down Expand Up @@ -506,8 +511,14 @@ yydefault:
}
}
case 6:
yyDollar = yyS[yypt-4 : yypt+1]
yyDollar = yyS[yypt-3 : yypt+1]
//line expressions.y:54
{
yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name)
}
case 7:
yyDollar = yyS[yypt-4 : yypt+1]
//line expressions.y:55
{
e, i := yyDollar[1].f, yyDollar[3].f
yyVAL.f = func(ctx Context) interface{} {
Expand All @@ -526,29 +537,29 @@ yydefault:
return nil
}
}
case 8:
case 9:
yyDollar = yyS[yypt-3 : yypt+1]
//line expressions.y:76
//line expressions.y:77
{
a, b := yyDollar[1].f, yyDollar[3].f
yyVAL.f = func(ctx Context) interface{} {
aref, bref := reflect.ValueOf(a(ctx)), reflect.ValueOf(b(ctx))
return GenericCompare(aref, bref) == 0
}
}
case 9:
case 10:
yyDollar = yyS[yypt-3 : yypt+1]
//line expressions.y:83
//line expressions.y:84
{
a, b := yyDollar[1].f, yyDollar[3].f
yyVAL.f = func(ctx Context) interface{} {
aref, bref := reflect.ValueOf(a(ctx)), reflect.ValueOf(b(ctx))
return GenericCompare(aref, bref) < 0
}
}
case 10:
case 11:
yyDollar = yyS[yypt-3 : yypt+1]
//line expressions.y:90
//line expressions.y:91
{
a, b := yyDollar[1].f, yyDollar[3].f
yyVAL.f = func(ctx Context) interface{} {
Expand Down

0 comments on commit 30211ac

Please sign in to comment.