Skip to content

Commit 828277f

Browse files
author
Dean Karn
authored
Custom coercions (#15)
1 parent 9ef4e91 commit 828277f

File tree

7 files changed

+192
-27
lines changed

7 files changed

+192
-27
lines changed

.github/workflows/workflow.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
test:
99
strategy:
1010
matrix:
11-
go-version: [1.19.x]
11+
go-version: [1.20.x,1.19.x]
1212
os: [ubuntu-latest, macos-latest, windows-latest]
1313
runs-on: ${{ matrix.os }}
1414
steps:
@@ -32,7 +32,7 @@ jobs:
3232
run: go test -race -covermode=atomic -coverprofile="profile.cov" ./...
3333

3434
- name: Send Coverage
35-
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.19.x'
35+
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.20.x'
3636
uses: shogo82148/actions-goveralls@v1
3737
with:
3838
path-to-profile: profile.cov
@@ -43,6 +43,6 @@ jobs:
4343
steps:
4444
- uses: actions/checkout@v3
4545
- name: golangci-lint
46-
uses: golangci/golangci-lint-action@v2
46+
uses: golangci/golangci-lint-action@v3
4747
with:
4848
version: latest

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.8.0] - 2023-06-10
10+
### Added
11+
- Ability to register new, remove existing and replace existing COERCE types.
12+
913
## [0.7.0] - 2023-05-31
1014
### Added
1115
- _string_ & _number_ COERCE types.
@@ -66,8 +70,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6670
### Added
6771
- Initial conversion from https://github.com/rust-playground/ksql.
6872

69-
[Unreleased]: https://github.com/go-playground/ksql/compare/v0.7.0...HEAD
70-
[0.6.1]: https://github.com/go-playground/ksql/compare/v0.7.0...v0.7.0
73+
[Unreleased]: https://github.com/go-playground/ksql/compare/v0.8.0...HEAD
74+
[0.8.0]: https://github.com/go-playground/ksql/compare/v0.7.0...v0.8.0
75+
[0.7.0]: https://github.com/go-playground/ksql/compare/v0.6.1...v0.7.0
7176
[0.6.1]: https://github.com/go-playground/ksql/compare/v0.6.0...v0.6.1
7277
[0.6.0]: https://github.com/go-playground/ksql/compare/v0.5.1...v0.6.0
7378
[0.5.1]: https://github.com/go-playground/ksql/compare/v0.5.0...v0.5.1

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
ksql
22
=====
3-
![Project status](https://img.shields.io/badge/version-0.7.0-green.svg)
3+
![Project status](https://img.shields.io/badge/version-0.8.0-green.svg)
44
[![GoDoc](https://godoc.org/github.com/go-playground/ksql?status.svg)](https://pkg.go.dev/github.com/go-playground/ksql)
55
![License](https://img.shields.io/dub/l/vibe-d.svg)
66

_examples/custom-coercion/main.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/go-playground/ksql"
8+
)
9+
10+
type Star struct {
11+
expression ksql.Expression
12+
}
13+
14+
func (s *Star) Calculate(json []byte) (interface{}, error) {
15+
inner, err := s.expression.Calculate(json)
16+
if err != nil {
17+
return nil, err
18+
}
19+
20+
switch t := inner.(type) {
21+
case string:
22+
return strings.Repeat("*", len(t)), nil
23+
default:
24+
return nil, fmt.Errorf("cannot star value %v", inner)
25+
}
26+
}
27+
28+
func main() {
29+
// Add custom coercion to the parser.
30+
// REMEMBER: coercions start and end with an _(underscore).
31+
guard := ksql.Coercions.Lock()
32+
guard.T["_star_"] = func(constEligible bool, expression ksql.Expression) (stillConstEligible bool, e ksql.Expression, err error) {
33+
return constEligible, &Star{expression}, nil
34+
}
35+
guard.Unlock()
36+
37+
expression := []byte(`COERCE "My Name" _star_`)
38+
input := []byte(`{}`)
39+
ex, err := ksql.Parse(expression)
40+
if err != nil {
41+
panic(err)
42+
}
43+
44+
result, err := ex.Calculate(input)
45+
if err != nil {
46+
panic(err)
47+
}
48+
fmt.Printf("%v\n", result)
49+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ go 1.18
55
require (
66
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
77
github.com/go-playground/itertools v0.1.0
8-
github.com/go-playground/pkg/v5 v5.18.0
98
github.com/stretchr/testify v1.8.1
9+
github.com/go-playground/pkg/v5 v5.18.0
1010
github.com/tidwall/gjson v1.14.4
1111
)
1212

parser.go

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"github.com/go-playground/itertools"
7+
syncext "github.com/go-playground/pkg/v5/sync"
78
resultext "github.com/go-playground/pkg/v5/values/result"
89
"io"
910
"reflect"
@@ -17,6 +18,86 @@ import (
1718
"github.com/tidwall/gjson"
1819
)
1920

21+
var (
22+
// Coercions is a `map` of all coercions guarded by a Mutex for use allowing registration,
23+
// removal or even replacing of existing coercions.
24+
Coercions = syncext.NewRWMutex2(map[string]func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error){
25+
"_datetime_": func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error) {
26+
expression = coerceDateTime{value: expression}
27+
if constEligible {
28+
value, err := expression.Calculate([]byte{})
29+
if err != nil {
30+
return false, nil, err
31+
}
32+
return constEligible, coercedConstant{value: value}, nil
33+
} else {
34+
return false, expression, nil
35+
}
36+
},
37+
"_lowercase_": func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error) {
38+
expression = coerceLowercase{value: expression}
39+
if constEligible {
40+
value, err := expression.Calculate([]byte{})
41+
if err != nil {
42+
return false, nil, err
43+
}
44+
return constEligible, coercedConstant{value: value}, nil
45+
} else {
46+
return false, expression, nil
47+
}
48+
},
49+
"_string_": func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error) {
50+
expression = coerceString{value: expression}
51+
if constEligible {
52+
value, err := expression.Calculate([]byte{})
53+
if err != nil {
54+
return false, nil, err
55+
}
56+
return constEligible, coercedConstant{value: value}, nil
57+
} else {
58+
return false, expression, nil
59+
}
60+
},
61+
"_number_": func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error) {
62+
expression = coerceNumber{value: expression}
63+
if constEligible {
64+
value, err := expression.Calculate([]byte{})
65+
if err != nil {
66+
return false, nil, err
67+
}
68+
return constEligible, coercedConstant{value: value}, nil
69+
} else {
70+
return false, expression, nil
71+
}
72+
},
73+
"_uppercase_": func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error) {
74+
expression = coerceUppercase{value: expression}
75+
if constEligible {
76+
value, err := expression.Calculate([]byte{})
77+
if err != nil {
78+
return false, nil, err
79+
}
80+
return constEligible, coercedConstant{value: value}, nil
81+
} else {
82+
return false, expression, nil
83+
}
84+
},
85+
"_title_": func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error) {
86+
expression = coerceTitle{value: expression}
87+
if constEligible {
88+
value, err := expression.Calculate([]byte{})
89+
if err != nil {
90+
return false, nil, err
91+
}
92+
return constEligible, coercedConstant{value: value}, nil
93+
} else {
94+
return false, expression, nil
95+
}
96+
},
97+
})
98+
)
99+
100+
// Expression Represents a stateless parsed expression that can be applied to JSON data.
20101
type Expression interface {
21102

22103
// Calculate will execute the parsed expression and apply it against the supplied data.
@@ -196,29 +277,19 @@ func (p *parser) parseValue(token Token) (Expression, error) {
196277
return nil, fmt.Errorf("COERCE missing data type identifier, found instead: %s", identifier)
197278
}
198279

199-
switch identifier {
200-
case "_datetime_":
201-
expression = coerceDateTime{value: expression}
202-
if constEligible {
203-
value, err := expression.Calculate([]byte{})
204-
if err != nil {
205-
return nil, err
206-
}
207-
expression = coercedConstant{value: value}
280+
guard := Coercions.RLock()
281+
fn, found := guard.T[identifier]
282+
guard.RUnlock()
283+
284+
if found {
285+
constEligible, expression, err = fn(constEligible, expression)
286+
if err != nil {
287+
return nil, err
208288
}
209-
case "_lowercase_":
210-
expression = coerceLowercase{value: expression}
211-
case "_string_":
212-
expression = coerceString{value: expression}
213-
case "_number_":
214-
expression = coerceNumber{value: expression}
215-
case "_uppercase_":
216-
expression = coerceUppercase{value: expression}
217-
case "_title_":
218-
expression = coerceTitle{value: expression}
219-
default:
289+
} else {
220290
return nil, fmt.Errorf("invalid COERCE data type '%s'", identifier)
221291
}
292+
222293
nextPeeked := p.tokenizer.Peek()
223294
if nextPeeked.IsSome() && nextPeeked.Unwrap().IsOk() && nextPeeked.Unwrap().Unwrap().kind == Comma {
224295
_ = p.tokenizer.Next() // consume peeked comma

parser_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package ksql
22

33
import (
4+
"fmt"
45
"github.com/stretchr/testify/require"
6+
"strings"
57
"testing"
68
"time"
79
)
@@ -727,3 +729,41 @@ func TestParser(t *testing.T) {
727729
})
728730
}
729731
}
732+
733+
type Star struct {
734+
expression Expression
735+
}
736+
737+
func (s *Star) Calculate(json []byte) (interface{}, error) {
738+
inner, err := s.expression.Calculate(json)
739+
if err != nil {
740+
return nil, err
741+
}
742+
743+
switch t := inner.(type) {
744+
case string:
745+
return strings.Repeat("*", len(t)), nil
746+
default:
747+
return nil, fmt.Errorf("cannot star value %v", inner)
748+
}
749+
}
750+
751+
func TestParserCustomCoercion(t *testing.T) {
752+
assert := require.New(t)
753+
754+
guard := Coercions.Lock()
755+
guard.T["_star_"] = func(constEligible bool, expression Expression) (stillConstEligible bool, e Expression, err error) {
756+
return constEligible, &Star{expression}, nil
757+
}
758+
guard.Unlock()
759+
760+
expression := []byte(`COERCE "My Name" _star_`)
761+
input := []byte(`{}`)
762+
ex, err := Parse(expression)
763+
assert.NoError(err)
764+
765+
result, err := ex.Calculate(input)
766+
assert.NoError(err)
767+
768+
assert.Equal("*******", result)
769+
}

0 commit comments

Comments
 (0)