diff --git a/_example/draft-query.go b/_example/draft-query.go new file mode 100644 index 0000000..2db94b2 --- /dev/null +++ b/_example/draft-query.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + + "github.com/goccy/go-json" +) + +type User struct { + ID int64 + Name string + Age int + Address UserAddressResolver +} + +type UserAddress struct { + UserID int64 + PostCode string + City string + Address1 string + Address2 string +} + +type UserAddressResolver func() (*UserAddress, error) + +func (resolver UserAddressResolver) MarshalJSON() ([]byte, error) { + address, err := resolver() + if err != nil { + return nil, err + } + return json.Marshal(address) +} + +func (resolver UserAddressResolver) ResolveQueryJSON(q *json.Query) (interface{}, error) { + // validate or rewrite json.Query + // + address, err := resolver() + if err != nil { + return nil, err + } + return address, nil +} + +type UserRepository struct{} + +func (r UserRepository) FindByID(id int64) (*User, error) { + v := User{ID: id, Name: "Ken", Age: 20} + // resolve relation from User to UserAddress + uaRepo := new(UserAddressRepository) + v.Address = func() (*UserAddress, error) { + return uaRepo.FindByUserID(v.ID) + } + return v, nil +} + +type UserAddressRepository struct{} + +func (r UserAddressRepository) FindByUserID(id int64) (*UserAddress, error) { + return &UserAddress{UserID: id, City: "A", Address1: "hoge", Address2: "fuga"}, nil +} + +func main() { + user, err := new(UserRepository).FindByID(1) + if err != nil { + panic(err) + } + { + b, err := json.Marshal(user) + if err != nil { + panic(err) + } + fmt.Println(string(b)) + } + { + //json.QueryFromJSON(`["Name", "Age", { "Address": [ "City" ] }]`) + q := json.NewQuery().Fields( + "Name", + "Age", + json.NewQuery("Address").Fields( + "City", + ), + ) + b, err := json.MarshalWithQuery(user, q) + if err != nil { + panic(err) + } + fmt.Println(string(b)) + } +} diff --git a/encode.go b/encode.go index 4dfdd2c..7e1afd0 100644 --- a/encode.go +++ b/encode.go @@ -148,6 +148,27 @@ func marshal(v interface{}, opt EncodeOption) ([]byte, error) { return copied, nil } +func marshalQuery(v interface{}, q *Query) ([]byte, error) { + ctx := takeEncodeRuntimeContext() + + buf, err := encode(ctx, v, EncodeOptionHTMLEscape) + if err != nil { + releaseEncodeRuntimeContext(ctx) + return nil, err + } + + // this line exists to escape call of `runtime.makeslicecopy` . + // if use `make([]byte, len(buf)-1)` and `copy(copied, buf)`, + // dst buffer size and src buffer size are differrent. + // in this case, compiler uses `runtime.makeslicecopy`, but it is slow. + buf = buf[:len(buf)-1] + copied := make([]byte, len(buf)) + copy(copied, buf) + + releaseEncodeRuntimeContext(ctx) + return copied, nil +} + func marshalNoEscape(v interface{}, opt EncodeOption) ([]byte, error) { ctx := takeEncodeRuntimeContext() diff --git a/json.go b/json.go index 3d879d0..c753500 100644 --- a/json.go +++ b/json.go @@ -13,6 +13,10 @@ type Marshaler interface { MarshalJSON() ([]byte, error) } +type QueryResolver interface { + ResolveQueryJSON(*Query) (interface{}, error) +} + // Unmarshaler is the interface implemented by types // that can unmarshal a JSON description of themselves. // The input can be assumed to be a valid encoding of @@ -172,6 +176,10 @@ func MarshalWithOption(v interface{}, optFuncs ...EncodeOptionFunc) ([]byte, err return marshal(v, opt) } +func MarshalWithQuery(v interface{}, q *Query) ([]byte, error) { + return marshalQuery(v, q) +} + // MarshalIndent is like Marshal but applies Indent to format the output. // Each JSON element in the output will begin on a new line beginning with prefix // followed by one or more copies of indent according to the indentation nesting. diff --git a/query.go b/query.go new file mode 100644 index 0000000..fd80bfd --- /dev/null +++ b/query.go @@ -0,0 +1,71 @@ +package json + +import "fmt" + +type Query struct { + query *subQuery + err error +} + +func (q *Query) String() string { + if q.err != nil { + return "" + } + if q.query == nil { + return "" + } + b, err := Marshal(q.query.dump()) + if err != nil { + return "" + } + return string(b) +} + +func (q *Query) Error() error { + return q.err +} + +func (q *Query) Fields(fieldNameOrQueryList ...interface{}) *Query { + for _, fieldNameOrQuery := range fieldNameOrQueryList { + switch v := fieldNameOrQuery.(type) { + case string: + q.fields = append(q.fields, &subQuery{name: v}) + case *Query: + q.fields = append(q.fields, v.query) + q.err = v.err + default: + q.err = fmt.Errorf("children types must be string or *Query but found %T", fieldNameOrQuery) + } + if q.err != nil { + break + } + } + return q +} + +func NewQuery(name ...string) *Query { + if len(name) > 1 { + return &Query{err: fmt.Errorf( + "NewQuery's argument allow empty or single name only, but passed %v", name, + )} + } + return &Query{query: &subQuery{name: name}} +} + +type subQuery struct { + name string + fields []*subQuery +} + +func (q *subQuery) dump() interface{} { + fields := []interface{}{} + for _, field := range q.fields { + fields = append(fields, field.dump()) + } + if q.name != "" { + return map[string][]interface{}{ + q.name: fields, + } + } + return interface{}(fields) +}