diff --git a/example/apis/org/org__list.go b/example/apis/org/org__list.go index 3d3db70..26c8b58 100644 --- a/example/apis/org/org__list.go +++ b/example/apis/org/org__list.go @@ -2,6 +2,7 @@ package org import ( "context" + "github.com/octohelm/courier/pkg/filter" "net/http" "github.com/octohelm/courier/pkg/courierhttp" @@ -10,6 +11,8 @@ import ( // 拉取组织列表 type ListOrg struct { courierhttp.MethodGet `path:"/orgs"` + + Name *filter.Filter[string] `name:"org~name,omitempty" in:"query"` } func (r *ListOrg) Output(ctx context.Context) (any, error) { diff --git a/example/apis/org/zz_generated.runtimedoc.go b/example/apis/org/zz_generated.runtimedoc.go index 5c22059..c89f4b4 100644 --- a/example/apis/org/zz_generated.runtimedoc.go +++ b/example/apis/org/zz_generated.runtimedoc.go @@ -147,6 +147,8 @@ func (v Info) RuntimeDoc(names ...string) ([]string, bool) { func (v ListOrg) RuntimeDoc(names ...string) ([]string, bool) { if len(names) > 0 { switch names[0] { + case "Name": + return []string{}, true } diff --git a/pkg/courierhttp/transport/incoming_transport.go b/pkg/courierhttp/transport/incoming_transport.go index 9b2cb28..eb5bbde 100644 --- a/pkg/courierhttp/transport/incoming_transport.go +++ b/pkg/courierhttp/transport/incoming_transport.go @@ -159,8 +159,13 @@ func (t *incomingTransport) decodeFromRequestInfo(ctx context.Context, info cour } if len(values) > 0 { + paramValue := param.FieldValue(rv) + if paramValue.Kind() != reflect.Ptr { + paramValue = paramValue.Addr() + } + err := core.Wrap(param.Transformer, ¶m.TransformerOption.CommonOption). - DecodeFrom(ctx, core.NewStringReaders(values), param.FieldValue(rv).Addr()) + DecodeFrom(ctx, core.NewStringReaders(values), paramValue) if err != nil { errSet.AddErr(err, validator.Location(param.In), param.Name) diff --git a/pkg/filter/composed.go b/pkg/filter/composed.go new file mode 100644 index 0000000..32c6d0c --- /dev/null +++ b/pkg/filter/composed.go @@ -0,0 +1,268 @@ +package filter + +import ( + "bytes" + "encoding/json" + "go/ast" + "reflect" + "strings" + + "github.com/octohelm/courier/pkg/filter/internal/directive" + slicesx "github.com/octohelm/x/slices" +) + +func Compose(filters ...any) *Composed { + c := &Composed{} + + for _, filter := range filters { + rv := reflect.ValueOf(filter) + + if rv.Kind() == reflect.Struct { + t := rv.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + for i := 0; i < rv.NumField(); i++ { + f := t.Field(i) + + if !ast.IsExported(f.Name) { + continue + } + + fv := rv.Field(i) + + if ruleExpr, ok := fv.Interface().(RuleExpr); ok { + name := f.Name + + if tagName, ok := f.Tag.Lookup("name"); ok { + n := strings.SplitN(tagName, ",", 2)[0] + if n != "" { + name = n + } + } + + c.register(name, &fieldRuler{ + name: name, + tpe: t, + ruleExprIdx: i, + }) + + if fv.Kind() == reflect.Ptr && fv.IsNil() { + continue + } + + if !ruleExpr.IsZero() { + c.rules = append(c.rules, ruleExpr.WhereOf(name)) + } + + break + } + } + } + } + + return c +} + +type fieldRuler struct { + tpe reflect.Type + name string + ruleExprIdx int +} + +func (t *fieldRuler) Name() string { + return t.name +} + +func (t *fieldRuler) New() *ruleWrapper { + rv := reflect.New(t.tpe) + + f := rv.Elem().Field(t.ruleExprIdx) + if f.Kind() == reflect.Ptr { + f.Set(reflect.New(f.Type().Elem())) + } + + return &ruleWrapper{ + obj: rv.Interface(), + rule: f.Interface().(Rule), + } +} + +type ruleWrapper struct { + obj any + rule Rule +} + +func (r *ruleWrapper) Obj() any { + return r.obj +} + +func (r *ruleWrapper) Rule() Rule { + return r.rule +} + +func (r *ruleWrapper) UnmarshalDirective(dec *directive.Decoder) error { + return r.rule.UnmarshalDirective(dec) +} + +type Composed struct { + Filters []any + + fieldRulers map[string]*fieldRuler + rules []Arg +} + +func (c *Composed) register(fieldName string, fr *fieldRuler) { + if c.fieldRulers == nil { + c.fieldRulers = map[string]*fieldRuler{} + } + c.fieldRulers[fieldName] = fr +} + +func (c Composed) IsZero() bool { + return len(c.rules) == 0 +} + +func (c *Composed) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + d := directive.NewDecoder(bytes.NewBuffer(data)) + + d.RegisterDirectiveNewer("or", func() directive.Unmarshaler { + return directive.UnmarshalerFunc(func(dec *directive.Decoder) error { + _, err := dec.DirectiveName() + if err != nil { + return err + } + + for { + k, text := dec.Next() + if k == directive.EOF || k == directive.KindFuncEnd { + break + } + + switch k { + case directive.KindFuncStart: + sub, err := dec.Unmarshaler(string(text)) + if err != nil { + return err + } + if err := sub.UnmarshalDirective(dec); err != nil { + return err + } + if arg, ok := sub.(Arg); ok { + c.rules = append(c.rules, arg) + } + default: + + } + } + + return nil + }) + }) + + d.RegisterDirectiveNewer("where", func() directive.Unmarshaler { + return directive.UnmarshalerFunc(func(dec *directive.Decoder) error { + _, err := dec.DirectiveName() + if err != nil { + return err + } + + argIdx := 0 + var fr *fieldRuler + + for { + k, text := dec.Next() + if k == directive.EOF || k == directive.KindFuncEnd { + break + } + + switch k { + case directive.KindValue: + if argIdx == 0 { + name := "" + if err := json.Unmarshal(text, &name); err != nil { + return err + } + + n, ok := c.fieldRulers[name] + if ok { + fr = n + continue + } + + return &ErrUnsupportedQLField{ + FieldName: name, + } + } + argIdx++ + case directive.KindFuncStart: + if fr == nil { + return &directive.ErrInvalidDirective{} + } + + wrapper := fr.New() + + if err := wrapper.UnmarshalDirective(dec); err != nil { + return err + } + + if ruleExpr, ok := wrapper.rule.(RuleExpr); ok { + c.rules = append(c.rules, ruleExpr.WhereOf(fr.Name())) + } + + c.Filters = append(c.Filters, wrapper.obj) + + argIdx++ + default: + + } + } + + return nil + }) + }) + + _, err := d.DirectiveName() + if err != nil { + return err + } + + for { + k, text := d.Next() + if k == directive.EOF || k == directive.KindFuncEnd { + break + } + + switch k { + case directive.KindFuncStart: + sub, err := d.Unmarshaler(string(text)) + if err != nil { + return err + } + if err := sub.UnmarshalDirective(d); err != nil { + return err + } + default: + + } + } + + return nil +} + +func (c Composed) MarshalText() ([]byte, error) { + switch len(c.rules) { + case 0: + return nil, nil + case 1: + return c.rules[0].MarshalText() + } + return directive.MarshalDirective("or", slicesx.Map(c.rules, func(e Arg) any { + return e + })...) +} diff --git a/pkg/filter/errors.go b/pkg/filter/errors.go new file mode 100644 index 0000000..2edc545 --- /dev/null +++ b/pkg/filter/errors.go @@ -0,0 +1,37 @@ +package filter + +import ( + "fmt" + + "github.com/octohelm/courier/pkg/statuserror" +) + +type ErrInvalidFilterOp struct { + statuserror.BadRequest + + Op string +} + +func (e *ErrInvalidFilterOp) Error() string { + return fmt.Sprintf("invalid filter op `%s`", e.Op) +} + +type ErrInvalidFilter struct { + statuserror.BadRequest + + Filter string +} + +func (e *ErrInvalidFilter) Error() string { + return fmt.Sprintf("invalid filter `%s`", e.Filter) +} + +type ErrUnsupportedQLField struct { + statuserror.BadRequest + + FieldName string +} + +func (e *ErrUnsupportedQLField) Error() string { + return fmt.Sprintf("unsupported ql field `%s`", e.FieldName) +} diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go new file mode 100644 index 0000000..b559a35 --- /dev/null +++ b/pkg/filter/filter.go @@ -0,0 +1,137 @@ +package filter + +import ( + "bytes" + "strings" + + "github.com/octohelm/courier/pkg/filter/internal/directive" + slicesx "github.com/octohelm/x/slices" +) + +type Filter[T comparable] struct { + op Op + args []Arg +} + +func (v Filter[T]) New() *T { + return new(T) +} + +func (f Filter[T]) WhereOf(name string) Rule { + return Where[T](name, &f) +} + +func (f Filter[T]) MarshalDirective() ([]byte, error) { + if f.IsZero() { + return nil, nil + } + + return directive.MarshalDirective(strings.ToLower(f.op.String()), slicesx.Map(f.args, func(e Arg) any { + return e + })...) +} + +func (Filter[T]) OneOf() []any { + return []any{new(T)} +} + +func (f Filter[T]) Op() Op { + return f.op +} + +func (v Filter[T]) Args() []Arg { + return v.args +} + +func (f Filter[T]) IsZero() bool { + return f.op == OP_UNKNOWN || len(f.args) == 0 +} + +func (f *Filter[T]) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + ff := Filter[T]{} + + dec := directive.NewDecoder(bytes.NewBuffer(data)) + + dec.RegisterDirectiveNewer(directive.DefaultDirectiveNewer, func() directive.Unmarshaler { + return &Filter[T]{} + }) + + kind, _ := dec.Next() + if kind != directive.KindFuncStart { + v := lit[T]{} + + if err := v.UmarshalText(data); err != nil { + return err + } + + ff.op = OP__EQ + ff.args = append(ff.args, v) + } else { + err := ff.UnmarshalDirective(dec) + if err != nil { + return err + } + } + + *f = ff + + return nil +} + +func (f *Filter[T]) UnmarshalDirective(dec *directive.Decoder) error { + name, err := dec.DirectiveName() + if err != nil { + return err + } + + ff := Filter[T]{} + if err := ff.op.UnmarshalText([]byte(strings.ToUpper(name))); err != nil { + return err + } + + for { + k, text := dec.Next() + if k == directive.EOF || k == directive.KindFuncEnd { + break + } + + switch k { + case directive.KindValue: + l := &lit[T]{} + if err := l.UnmarshalJSON(text); err != nil { + return err + } + ff.args = append(ff.args, l) + case directive.KindFuncStart: + sub, err := dec.Unmarshaler(string(text)) + if err != nil { + return err + } + if err := sub.UnmarshalDirective(dec); err != nil { + return err + } + if arg, ok := sub.(Arg); ok { + ff.args = append(ff.args, arg) + } + default: + + } + } + + *f = ff + + return nil +} + +func (f Filter[T]) MarshalText() ([]byte, error) { + return f.MarshalDirective() +} + +func (f Filter[T]) String() string { + txt, _ := f.MarshalText() + return string(txt) +} diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go new file mode 100644 index 0000000..07ae091 --- /dev/null +++ b/pkg/filter/filter_test.go @@ -0,0 +1,169 @@ +package filter + +import ( + "context" + "encoding" + "reflect" + "testing" + + "github.com/octohelm/courier/pkg/courier" + "github.com/octohelm/courier/pkg/courierhttp" + "github.com/octohelm/courier/pkg/courierhttp/operatortest" + testingx "github.com/octohelm/x/testing" +) + +func TestFilterMarshalAndUnmarshal(t *testing.T) { + cases := []struct { + filter Rule + query string + }{ + { + Eq(1), + "eq(1)", + }, + { + And(Gte(1), Lte(10)), + "and(gte(1),lte(10))", + }, + { + And(Gte(1), Lte(10)).WhereOf("item.id"), + `where("item.id",and(gte(1),lte(10)))`, + }, + { + In([]string{"a", "b", "c"}).WhereOf("item.name"), + `where("item.name",in("a","b","c"))`, + }, + } + + for _, c := range cases { + t.Run(c.query, func(t *testing.T) { + data, err := c.filter.MarshalText() + testingx.Expect(t, err, testingx.BeNil[error]()) + testingx.Expect(t, string(data), testingx.Be(c.query)) + + tt := reflect.TypeOf(c.filter) + if tt.Kind() == reflect.Ptr { + tt = tt.Elem() + } + + f := reflect.New(tt).Interface().(Rule) + + err = f.(encoding.TextUnmarshaler).UnmarshalText([]byte(c.query)) + testingx.Expect(t, err, testingx.BeNil[error]()) + + raw, err := f.MarshalText() + testingx.Expect(t, err, testingx.BeNil[error]()) + testingx.Expect(t, string(raw), testingx.Equal(string(data))) + }) + } + + t.Run("unmarshal single value", func(t *testing.T) { + f := Filter[int]{} + err := f.UnmarshalText([]byte("1")) + testingx.Expect(t, err, testingx.BeNil[error]()) + testingx.Expect(t, f.String(), testingx.Be("eq(1)")) + }) + + t.Run("composed", func(t *testing.T) { + c := Compose( + ItemListFilterByName{ + ItemName: Eq("x"), + }, + ItemListFilterByID{ + ItemID: In([]int{1, 2, 3, 4}), + }, + ) + + rule, err := c.MarshalText() + testingx.Expect(t, err, testingx.BeNil[error]()) + testingx.Expect(t, string(rule), testingx.Be(`or(where("item.name",eq("x")),where("item.id",in(1,2,3,4)))`)) + + cc := Compose( + ItemListFilterByName{}, + ItemListFilterByID{}, + ) + + err = cc.UnmarshalText(rule) + testingx.Expect(t, err, testingx.BeNil[error]()) + + rule2, err := cc.MarshalText() + testingx.Expect(t, err, testingx.BeNil[error]()) + testingx.Expect(t, string(rule2), testingx.Be(`or(where("item.name",eq("x")),where("item.id",in(1,2,3,4)))`)) + }) +} + +type ItemListFilterByID struct { + ItemID *Filter[int] `name:"item.id,omitempty" in:"query"` +} + +type ItemListFilterByName struct { + ItemName *Filter[string] `name:"item.name,omitempty" in:"query"` +} + +type Operator struct { + courierhttp.MethodGet `path:"/x"` + + ItemListFilterByID + ItemListFilterByName + + Composed Composed `name:"ql,omitempty" in:"query"` +} + +func (Operator) New() courier.Operator { + o := &Operator{} + o.Composed = *Compose( + o.ItemListFilterByID, + o.ItemListFilterByName, + ) + + return o +} + +func (x Operator) Output(ctx context.Context) (any, error) { + return x, nil +} + +func TestFilterForHTTP(t *testing.T) { + ctx := context.Background() + + c := operatortest.Serve(context.Background(), &Operator{}) + defer c.Close() + + t.Run("request single values", func(t *testing.T) { + req := &Operator{} + req.ItemName = Eq("name") + + resp := &ItemListFilterByName{} + _, err := c.Do(ctx, req).Into(resp) + testingx.Expect(t, err, testingx.BeNil[error]()) + testingx.Expect(t, resp.ItemName.String(), testingx.Be(`eq("name")`)) + }) + + t.Run("request multiple values", func(t *testing.T) { + req := &Operator{} + req.ItemID = In([]int{1, 2, 3, 4}) + + resp := &ItemListFilterByID{} + _, err := c.Do(ctx, req).Into(resp) + testingx.Expect(t, err, testingx.BeNil[error]()) + testingx.Expect(t, resp.ItemID.String(), testingx.Be("in(1,2,3,4)")) + }) + + t.Run("request composed values", func(t *testing.T) { + req := &Operator{} + req.Composed = *Compose( + ItemListFilterByID{ + ItemID: In([]int{1, 2, 3, 4}), + }, + ItemListFilterByName{ + ItemName: Eq("name"), + }, + ) + + resp := &struct { + Composed string + }{} + _, err := c.Do(ctx, req).Into(resp) + testingx.Expect(t, err, testingx.BeNil[error]()) + }) +} diff --git a/pkg/filter/internal/directive/decoder.go b/pkg/filter/internal/directive/decoder.go new file mode 100644 index 0000000..65fbfc3 --- /dev/null +++ b/pkg/filter/internal/directive/decoder.go @@ -0,0 +1,125 @@ +package directive + +import ( + "bytes" + "io" + "text/scanner" +) + +type Marshaler interface { + MarshalDirective() ([]byte, error) +} + +type Unmarshaler interface { + UnmarshalDirective(*Decoder) error +} + +type UnmarshalerFunc func(*Decoder) error + +func (fn UnmarshalerFunc) UnmarshalDirective(d *Decoder) error { + return fn(d) +} + +func NewDecoder(r io.Reader) *Decoder { + d := &Decoder{} + d.unmarshalers = map[string]Newer{} + d.Reset(r) + return d +} + +type Decoder struct { + unmarshalers map[string]Newer + s scanner.Scanner + + directiveName string + + kind Kind + text []byte + + tmp bytes.Buffer +} + +type Newer func() Unmarshaler + +const DefaultDirectiveNewer = "_default" + +func (d *Decoder) RegisterDirectiveNewer(directiveName string, fn Newer) { + d.unmarshalers[directiveName] = fn +} + +func (d *Decoder) Unmarshaler(name string) (Unmarshaler, error) { + d.directiveName = name + + if u, ok := d.unmarshalers[name]; ok { + return u(), nil + } + + if u, ok := d.unmarshalers[DefaultDirectiveNewer]; ok { + return u(), nil + } + + return nil, &ErrUnsupportedDirective{ + DirectiveName: name, + } +} + +func (d *Decoder) Reset(r io.Reader) { + d.s.Init(r) + d.tmp.Reset() +} + +func (d *Decoder) DirectiveName() (string, error) { + if d.directiveName == "" { + k, _ := d.Next() + if k != KindFuncStart { + return "", &ErrInvalidDirective{} + } + } + return d.directiveName, nil +} + +func (d *Decoder) textToken() []byte { + textToken := d.tmp.Bytes() + d.tmp.Reset() + return textToken +} + +func (d *Decoder) Next() (Kind, []byte) { + tok := d.s.Scan() + switch tok { + case scanner.EOF: + return EOF, nil + case ')': + return KindFuncEnd, d.textToken() + case ',': + return d.Next() + default: + tokenText := d.s.TokenText() + + switch d.s.Peek() { + case '(': + d.directiveName = tokenText + + return KindFuncStart, []byte(tokenText) + case ',', ')': + return KindValue, []byte(tokenText) + } + } + return d.Next() +} + +type Kind int + +const ( + KindInvalid Kind = iota + KindFuncStart + KindFuncEnd + KindValue + EOF +) + +type RawValue []byte + +func (v RawValue) MarshalDirective() ([]byte, error) { + return v, nil +} diff --git a/pkg/filter/internal/directive/decoder_test.go b/pkg/filter/internal/directive/decoder_test.go new file mode 100644 index 0000000..6904efa --- /dev/null +++ b/pkg/filter/internal/directive/decoder_test.go @@ -0,0 +1,21 @@ +package directive + +import ( + "bytes" + testingx "github.com/octohelm/x/testing" + "testing" +) + +func TestNewDecoder(t *testing.T) { + d := NewDecoder(bytes.NewBufferString(`fn("1",eq(1))`)) + d.RegisterDirectiveNewer(DefaultDirectiveNewer, func() Unmarshaler { + return &Directive{} + }) + + fn := &Directive{} + err := fn.UnmarshalDirective(d) + testingx.Expect(t, err, testingx.BeNil[error]()) + + v, _ := fn.MarshalDirective() + testingx.Expect(t, string(v), testingx.Be(`fn("1",eq(1))`)) +} diff --git a/pkg/filter/internal/directive/directive.go b/pkg/filter/internal/directive/directive.go new file mode 100644 index 0000000..6cf339d --- /dev/null +++ b/pkg/filter/internal/directive/directive.go @@ -0,0 +1,47 @@ +package directive + +type Directive struct { + Args []any + Name string +} + +func (r *Directive) UnmarshalDirective(dec *Decoder) error { + dd := Directive{} + + name, err := dec.DirectiveName() + if err != nil { + return err + } + dd.Name = name + + for { + k, text := dec.Next() + if k == EOF || k == KindFuncEnd { + break + } + + switch k { + case KindValue: + dd.Args = append(dd.Args, RawValue(text)) + case KindFuncStart: + sub, err := dec.Unmarshaler(string(text)) + if err != nil { + return err + } + if err := sub.UnmarshalDirective(dec); err != nil { + return err + } + dd.Args = append(dd.Args, sub) + default: + + } + } + + *r = dd + + return nil +} + +func (f Directive) MarshalDirective() ([]byte, error) { + return MarshalDirective(f.Name, f.Args...) +} diff --git a/pkg/filter/internal/directive/encoder.go b/pkg/filter/internal/directive/encoder.go new file mode 100644 index 0000000..97e88d5 --- /dev/null +++ b/pkg/filter/internal/directive/encoder.go @@ -0,0 +1,40 @@ +package directive + +import ( + "bytes" + "encoding/json" + "strings" +) + +func MarshalDirective(funcName string, args ...any) ([]byte, error) { + b := bytes.NewBuffer(nil) + + b.WriteString(strings.ToLower(funcName)) + + b.WriteByte('(') + + for i, arg := range args { + if i > 0 { + b.WriteByte(',') + } + + switch x := arg.(type) { + case Marshaler: + directive, err := x.MarshalDirective() + if err != nil { + return nil, err + } + b.Write(directive) + default: + value, err := json.Marshal(arg) + if err != nil { + return nil, err + } + b.Write(value) + } + } + + b.WriteByte(')') + + return b.Bytes(), nil +} diff --git a/pkg/filter/internal/directive/encoder_test.go b/pkg/filter/internal/directive/encoder_test.go new file mode 100644 index 0000000..ae6e288 --- /dev/null +++ b/pkg/filter/internal/directive/encoder_test.go @@ -0,0 +1,25 @@ +package directive + +import ( + "github.com/davecgh/go-spew/spew" + testingx "github.com/octohelm/x/testing" + "testing" +) + +func Eq[T comparable](v T) Directive { + return Directive{ + Name: "eq", + Args: []any{v}, + } +} + +func TestEncoder(t *testing.T) { + x, _ := MarshalDirective("fn", 1, 2, 3, 4) + testingx.Expect(t, string(x), testingx.Be(`fn(1,2,3,4)`)) + + x2, err := MarshalDirective("or", Eq(1), Eq(2)) + if err != nil { + spew.Dump(err) + } + testingx.Expect(t, string(x2), testingx.Be(`or(eq(1),eq(2))`)) +} diff --git a/pkg/filter/internal/directive/type.go b/pkg/filter/internal/directive/type.go new file mode 100644 index 0000000..6a8fc2a --- /dev/null +++ b/pkg/filter/internal/directive/type.go @@ -0,0 +1,30 @@ +package directive + +import ( + "fmt" + "github.com/octohelm/courier/pkg/statuserror" +) + +type ErrInvalidDirective struct { + statuserror.BadRequest + + DirectiveName string +} + +func (e *ErrInvalidDirective) Error() string { + if e.DirectiveName == "" { + return fmt.Sprintf("invalid directive") + } + + return fmt.Sprintf("invalid directive: %s", e.DirectiveName) +} + +type ErrUnsupportedDirective struct { + statuserror.BadRequest + + DirectiveName string +} + +func (e *ErrUnsupportedDirective) Error() string { + return fmt.Sprintf("unsupported directive: %s", e.DirectiveName) +} diff --git a/pkg/filter/internal/directive/zz_generated.runtimedoc.go b/pkg/filter/internal/directive/zz_generated.runtimedoc.go new file mode 100644 index 0000000..5f37097 --- /dev/null +++ b/pkg/filter/internal/directive/zz_generated.runtimedoc.go @@ -0,0 +1,69 @@ +/* +Package directive GENERATED BY gengo:runtimedoc +DON'T EDIT THIS FILE +*/ +package directive + +// nolint:deadcode,unused +func runtimeDoc(v any, names ...string) ([]string, bool) { + if c, ok := v.(interface { + RuntimeDoc(names ...string) ([]string, bool) + }); ok { + return c.RuntimeDoc(names...) + } + return nil, false +} + +func (v Directive) RuntimeDoc(names ...string) ([]string, bool) { + if len(names) > 0 { + switch names[0] { + case "Args": + return []string{}, true + case "Name": + return []string{}, true + + } + + return nil, false + } + return []string{}, true +} + +func (v ErrInvalidDirective) RuntimeDoc(names ...string) ([]string, bool) { + if len(names) > 0 { + switch names[0] { + case "DirectiveName": + return []string{}, true + + } + + return nil, false + } + return []string{}, true +} + +func (v ErrUnsupportedDirective) RuntimeDoc(names ...string) ([]string, bool) { + if len(names) > 0 { + switch names[0] { + case "DirectiveName": + return []string{}, true + + } + + return nil, false + } + return []string{}, true +} + +func (Kind) RuntimeDoc(names ...string) ([]string, bool) { + return []string{}, true +} +func (Newer) RuntimeDoc(names ...string) ([]string, bool) { + return []string{}, true +} +func (RawValue) RuntimeDoc(names ...string) ([]string, bool) { + return []string{}, true +} +func (UnmarshalerFunc) RuntimeDoc(names ...string) ([]string, bool) { + return []string{}, true +} diff --git a/pkg/filter/lit.go b/pkg/filter/lit.go new file mode 100644 index 0000000..4f6f44f --- /dev/null +++ b/pkg/filter/lit.go @@ -0,0 +1,61 @@ +package filter + +import ( + "encoding/json" + encodingx "github.com/octohelm/x/encoding" + "strconv" +) + +func Lit[T comparable](v T) Value[T] { + return &lit[T]{ + value: v, + } +} + +type lit[T comparable] struct { + value T +} + +func (v lit[T]) Value() T { + return v.value +} + +func (l *lit[T]) PtrValue() *T { + return &l.value +} + +func (l *lit[T]) UnmarshalJSON(b []byte) error { + v := new(T) + if err := json.Unmarshal(b, v); err != nil { + return err + } + *l = lit[T]{value: *v} + return nil +} + +func (l *lit[T]) UmarshalText(b []byte) (err error) { + if len(b) == 0 { + return nil + } + if b[0] == '"' { + raw, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + b = []byte(raw) + } + v := new(T) + if err := encodingx.UnmarshalText(v, b); err != nil { + return err + } + *l = lit[T]{value: *v} + return nil +} + +func (v lit[T]) MarshalText() (text []byte, err error) { + return json.Marshal(v.value) +} + +func (l lit[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(l.value) +} diff --git a/pkg/filter/op.go b/pkg/filter/op.go new file mode 100644 index 0000000..180beaa --- /dev/null +++ b/pkg/filter/op.go @@ -0,0 +1,134 @@ +package filter + +import ( + slicesx "github.com/octohelm/x/slices" +) + +// +gengo:enum +type Op uint8 + +const ( + OP_UNKNOWN Op = iota + + OP__EQ + OP__NEQ + OP__IN + OP__NOTIN + + OP__GTE + OP__GT + + OP__LTE + OP__LT + + OP__WHERE + OP__AND + OP__OR +) + +// Eq == v +func Eq[T comparable](v T) *Filter[T] { + return &Filter[T]{ + op: OP__EQ, + args: []Arg{ + Lit(v), + }, + } +} + +// Neq != v +func Neq[T comparable](v T) *Filter[T] { + return &Filter[T]{ + op: OP__NEQ, + args: []Arg{ + Lit(v), + }, + } +} + +// Lt < v +func Lt[T comparable](v T) *Filter[T] { + return &Filter[T]{ + op: OP__LT, + args: []Arg{ + Lit(v), + }, + } +} + +// Lte <= v +func Lte[T comparable](v T) *Filter[T] { + return &Filter[T]{ + op: OP__LTE, + args: []Arg{ + Lit(v), + }, + } +} + +// In values +func In[T comparable](values []T) *Filter[T] { + return &Filter[T]{ + op: OP__IN, + args: slicesx.Map(values, func(e T) Arg { + return Lit(e) + }), + } +} + +// Notin values +func Notin[T comparable](values []T) *Filter[T] { + return &Filter[T]{ + op: OP__NOTIN, + args: slicesx.Map(values, func(e T) Arg { + return Lit(e) + }), + } +} + +// Gt > v +func Gt[T comparable](v T) *Filter[T] { + return &Filter[T]{ + op: OP__GT, + args: []Arg{ + Lit(v), + }, + } +} + +// Gte >= v +func Gte[T comparable](v T) *Filter[T] { + return &Filter[T]{ + op: OP__GTE, + args: []Arg{ + Lit(v), + }, + } +} + +func And[T comparable](filters ...TypedRule[T]) *Filter[T] { + return &Filter[T]{ + op: OP__AND, + args: slicesx.Map(filters, func(f TypedRule[T]) Arg { + return f + }), + } +} + +func Or[T comparable](filters ...TypedRule[T]) *Filter[T] { + return &Filter[T]{ + op: OP__OR, + args: slicesx.Map(filters, func(f TypedRule[T]) Arg { + return f + }), + } +} + +func OrRules(rules ...Rule) Rule { + return &Filter[any]{ + op: OP__OR, + args: slicesx.Map(rules, func(e Rule) Arg { + return e + }), + } +} diff --git a/pkg/filter/rule.go b/pkg/filter/rule.go new file mode 100644 index 0000000..7508e3a --- /dev/null +++ b/pkg/filter/rule.go @@ -0,0 +1,36 @@ +package filter + +import ( + "encoding" + + "github.com/octohelm/courier/pkg/filter/internal/directive" +) + +type Arg interface { + encoding.TextMarshaler +} + +type Value[T comparable] interface { + Arg + Value() T +} + +type TypedRule[T comparable] interface { + Rule + + New() *T +} + +type Rule interface { + Arg + Op() Op + Args() []Arg + IsZero() bool + + directive.Unmarshaler +} + +type RuleExpr interface { + IsZero() bool + WhereOf(name string) Rule +} diff --git a/pkg/filter/util.go b/pkg/filter/util.go new file mode 100644 index 0000000..0b61c90 --- /dev/null +++ b/pkg/filter/util.go @@ -0,0 +1,24 @@ +package filter + +func MapFilter[A Arg, O any](args []A, where func(a Arg) (O, bool)) []O { + ret := make([]O, 0, len(args)) + + for _, x := range args { + v, ok := where(x) + if ok { + ret = append(ret, v) + } + } + + return ret +} + +func First[A Arg, O any](args []A, where func(a Arg) (O, bool)) (O, bool) { + for _, x := range args { + v, ok := where(x) + if ok { + return v, true + } + } + return *new(O), false +} diff --git a/pkg/filter/where.go b/pkg/filter/where.go new file mode 100644 index 0000000..391a97d --- /dev/null +++ b/pkg/filter/where.go @@ -0,0 +1,116 @@ +package filter + +import ( + "bytes" + "encoding/json" + "slices" + + "github.com/octohelm/courier/pkg/filter/internal/directive" + slicesx "github.com/octohelm/x/slices" +) + +func Where[T comparable](name string, rules ...TypedRule[T]) TypedRule[T] { + return &where[T]{ + name: name, + args: slicesx.Map(rules, func(e TypedRule[T]) Arg { + return e + }), + } +} + +type where[T comparable] struct { + name string + args []Arg +} + +func (w where[T]) Args() []Arg { + return w.args +} + +func (w where[T]) IsZero() bool { + return len(w.args) == 0 +} + +func (where[T]) Op() Op { + return OP__WHERE +} + +func (where[T]) New() *T { + return new(T) +} + +func (w *where[T]) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + d := directive.NewDecoder(bytes.NewBuffer(data)) + + d.RegisterDirectiveNewer(directive.DefaultDirectiveNewer, func() directive.Unmarshaler { + return &Filter[T]{} + }) + + return w.UnmarshalDirective(d) +} + +func (w *where[T]) UnmarshalDirective(dec *directive.Decoder) error { + name, err := dec.DirectiveName() + if err != nil { + return err + } + if name != "where" { + return &directive.ErrInvalidDirective{} + } + + dd := &where[T]{} + + argIdx := 0 + + for { + k, text := dec.Next() + if k == directive.EOF || k == directive.KindFuncEnd { + break + } + + switch k { + case directive.KindValue: + if argIdx == 0 { + if err := json.Unmarshal(text, &dd.name); err != nil { + return err + } + } + argIdx++ + case directive.KindFuncStart: + sub, err := dec.Unmarshaler(string(text)) + if err != nil { + return err + } + if err := sub.UnmarshalDirective(dec); err != nil { + return err + } + if arg, ok := sub.(Arg); ok { + dd.args = append(dd.args, arg) + } + argIdx++ + default: + + } + } + + *w = *dd + + return nil +} + +func (w where[T]) MarshalText() (text []byte, err error) { + return w.MarshalDirective() +} + +func (w where[T]) MarshalDirective() (text []byte, err error) { + return directive.MarshalDirective("where", slices.Concat( + []any{w.name}, + slicesx.Map(w.args, func(e Arg) any { + return e + }), + )...) +} diff --git a/pkg/filter/zz_generated.enum.go b/pkg/filter/zz_generated.enum.go new file mode 100644 index 0000000..80be7ab --- /dev/null +++ b/pkg/filter/zz_generated.enum.go @@ -0,0 +1,179 @@ +/* +Package filter GENERATED BY gengo:enum +DON'T EDIT THIS FILE +*/ +package filter + +import ( + bytes "bytes" + database_sql_driver "database/sql/driver" + + github_com_octohelm_storage_pkg_enumeration "github.com/octohelm/storage/pkg/enumeration" + github_com_pkg_errors "github.com/pkg/errors" +) + +var InvalidOp = github_com_pkg_errors.New("invalid Op") + +func (Op) EnumValues() []any { + return []any{ + OP__EQ, OP__AND, OP__OR, OP__NEQ, OP__IN, OP__NOTIN, OP__GTE, OP__GT, OP__LTE, OP__LT, OP__WHERE, + } +} +func (v Op) MarshalText() ([]byte, error) { + str := v.String() + if str == "UNKNOWN" { + return nil, InvalidOp + } + return []byte(str), nil +} + +func (v *Op) UnmarshalText(data []byte) error { + vv, err := ParseOpFromString(string(bytes.ToUpper(data))) + if err != nil { + return err + } + *v = vv + return nil +} + +func ParseOpFromString(s string) (Op, error) { + switch s { + case "EQ": + return OP__EQ, nil + case "AND": + return OP__AND, nil + case "OR": + return OP__OR, nil + case "NEQ": + return OP__NEQ, nil + case "IN": + return OP__IN, nil + case "NOTIN": + return OP__NOTIN, nil + case "GTE": + return OP__GTE, nil + case "GT": + return OP__GT, nil + case "LTE": + return OP__LTE, nil + case "LT": + return OP__LT, nil + case "WHERE": + return OP__WHERE, nil + + default: + return OP_UNKNOWN, InvalidOp + } +} + +func (v Op) String() string { + switch v { + case OP__EQ: + return "EQ" + case OP__AND: + return "AND" + case OP__OR: + return "OR" + case OP__NEQ: + return "NEQ" + case OP__IN: + return "IN" + case OP__NOTIN: + return "NOTIN" + case OP__GTE: + return "GTE" + case OP__GT: + return "GT" + case OP__LTE: + return "LTE" + case OP__LT: + return "LT" + case OP__WHERE: + return "WHERE" + + default: + return "UNKNOWN" + } +} + +func ParseOpLabelString(label string) (Op, error) { + switch label { + case "EQ": + return OP__EQ, nil + case "AND": + return OP__AND, nil + case "OR": + return OP__OR, nil + case "NEQ": + return OP__NEQ, nil + case "IN": + return OP__IN, nil + case "NOTIN": + return OP__NOTIN, nil + case "GTE": + return OP__GTE, nil + case "GT": + return OP__GT, nil + case "LTE": + return OP__LTE, nil + case "LT": + return OP__LT, nil + case "WHERE": + return OP__WHERE, nil + + default: + return OP_UNKNOWN, InvalidOp + } +} + +func (v Op) Label() string { + switch v { + case OP__EQ: + return "EQ" + case OP__AND: + return "AND" + case OP__OR: + return "OR" + case OP__NEQ: + return "NEQ" + case OP__IN: + return "IN" + case OP__NOTIN: + return "NOTIN" + case OP__GTE: + return "GTE" + case OP__GT: + return "GT" + case OP__LTE: + return "LTE" + case OP__LT: + return "LT" + case OP__WHERE: + return "WHERE" + + default: + return "UNKNOWN" + } +} + +func (v Op) Value() (database_sql_driver.Value, error) { + offset := 0 + if o, ok := any(v).(github_com_octohelm_storage_pkg_enumeration.DriverValueOffset); ok { + offset = o.Offset() + } + return int64(v) + int64(offset), nil +} + +func (v *Op) Scan(src any) error { + offset := 0 + if o, ok := any(v).(github_com_octohelm_storage_pkg_enumeration.DriverValueOffset); ok { + offset = o.Offset() + } + + i, err := github_com_octohelm_storage_pkg_enumeration.ScanIntEnumStringer(src, offset) + if err != nil { + return err + } + *v = Op(i) + return nil +} diff --git a/pkg/filter/zz_generated.runtimedoc.go b/pkg/filter/zz_generated.runtimedoc.go new file mode 100644 index 0000000..7f6263f --- /dev/null +++ b/pkg/filter/zz_generated.runtimedoc.go @@ -0,0 +1,71 @@ +/* +Package filter GENERATED BY gengo:runtimedoc +DON'T EDIT THIS FILE +*/ +package filter + +// nolint:deadcode,unused +func runtimeDoc(v any, names ...string) ([]string, bool) { + if c, ok := v.(interface { + RuntimeDoc(names ...string) ([]string, bool) + }); ok { + return c.RuntimeDoc(names...) + } + return nil, false +} + +func (v Composed) RuntimeDoc(names ...string) ([]string, bool) { + if len(names) > 0 { + switch names[0] { + case "Filters": + return []string{}, true + + } + + return nil, false + } + return []string{}, true +} + +func (v ErrInvalidFilter) RuntimeDoc(names ...string) ([]string, bool) { + if len(names) > 0 { + switch names[0] { + case "Filter": + return []string{}, true + + } + + return nil, false + } + return []string{}, true +} + +func (v ErrInvalidFilterOp) RuntimeDoc(names ...string) ([]string, bool) { + if len(names) > 0 { + switch names[0] { + case "Op": + return []string{}, true + + } + + return nil, false + } + return []string{}, true +} + +func (v ErrUnsupportedQLField) RuntimeDoc(names ...string) ([]string, bool) { + if len(names) > 0 { + switch names[0] { + case "FieldName": + return []string{}, true + + } + + return nil, false + } + return []string{}, true +} + +func (Op) RuntimeDoc(names ...string) ([]string, bool) { + return []string{}, true +} diff --git a/pkg/transformer/core/utils_parameters.go b/pkg/transformer/core/utils_parameters.go index eac4852..e28b84a 100644 --- a/pkg/transformer/core/utils_parameters.go +++ b/pkg/transformer/core/utils_parameters.go @@ -25,27 +25,29 @@ type Parameter struct { func (p *Parameter) FieldValue(structReflectValue reflect.Value) reflect.Value { structReflectValue = reflectx.Indirect(structReflectValue) - n := len(p.Loc) - - fieldValue := structReflectValue + f := structReflectValue + n := len(p.Loc) for i := 0; i < n; i++ { loc := p.Loc[i] - fieldValue = fieldValue.Field(loc) + + f = f.Field(loc) + + if f.Kind() == reflect.Ptr && f.IsNil() { + if f.CanSet() { + f.Set(reflect.New(f.Type().Elem())) + } + } // last loc should keep ptr value if i < n-1 { - for fieldValue.Kind() == reflect.Ptr { - // notice the ptr struct ensure only for Ptr Anonymous Field - if fieldValue.IsNil() { - fieldValue.Set(reflectx.New(fieldValue.Type())) - } - fieldValue = fieldValue.Elem() + if f.Kind() == reflect.Ptr { + f = f.Elem() } } } - return fieldValue + return f } type Tag string