├── .gitignore ├── leancloud ├── role.go ├── roles.go ├── relation.go ├── user.go ├── authdata.go ├── fileref.go ├── roleref.go ├── class.go ├── auth_options.go ├── operator.go ├── geopoint.go ├── error.go ├── userref.go ├── acl.go ├── operator_test.go ├── client.go ├── server_test.go ├── object.go ├── files_test.go ├── client_test.go ├── request.go ├── query_test.go ├── files.go ├── engine_test.go ├── objectref_test.go ├── users.go ├── hook.go ├── file.go ├── engine.go ├── query.go ├── objectref.go ├── server.go └── encoding.go ├── go.mod ├── go.sum ├── README.md ├── .github └── workflows │ └── go.yml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .envrc -------------------------------------------------------------------------------- /leancloud/role.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type Role struct { 4 | Object 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leancloud/go-sdk 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/go-querystring v1.0.0 // indirect 7 | github.com/levigross/grequests v0.0.0-20180715163950-d0df86deffcb 8 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /leancloud/roles.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type Roles struct { 4 | c *Client 5 | } 6 | 7 | func (ref *Roles) NewQuery() *Query { 8 | return &Query{ 9 | class: &Class{ 10 | Name: "_Role", 11 | c: ref.c, 12 | }, 13 | c: ref.c, 14 | where: make(map[string]interface{}), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /leancloud/relation.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type Relation struct { 4 | key string 5 | parentClass string 6 | } 7 | 8 | func NewRelation(key, parentClass string) *Relation { 9 | return &Relation{ 10 | key: key, 11 | parentClass: parentClass, 12 | } 13 | } 14 | 15 | func (relation *Relation) Add(objects ...interface{}) { 16 | 17 | } 18 | 19 | func (relation *Relation) Remove(objects ...interface{}) { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /leancloud/user.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | // User is a local representation of a user persisted to the LeanCloud server. 4 | type User struct { 5 | Object 6 | SessionToken string `json:"sessionToken"` 7 | Username string `json:"username"` 8 | Email string `json:"email"` 9 | EmailVerified bool `json:"emailVerified"` 10 | MobilePhoneNumber string `json:"mobilePhoneNumber"` 11 | MobilePhoneVerified bool `json:"mobilePhoneVerified"` 12 | } 13 | -------------------------------------------------------------------------------- /leancloud/authdata.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type AuthData struct { 4 | data map[string]map[string]interface{} 5 | } 6 | 7 | func NewAuthData() *AuthData { 8 | auth := new(AuthData) 9 | auth.data = make(map[string]map[string]interface{}) 10 | return auth 11 | } 12 | 13 | func (auth *AuthData) Set(provider string, data map[string]interface{}) { 14 | auth.data[provider] = data 15 | } 16 | 17 | func (auth *AuthData) SetAnonymous(data map[string]interface{}) { 18 | auth.data["anonymous"] = data 19 | } 20 | 21 | func (auth *AuthData) Get(provider string) map[string]interface{} { 22 | return auth.data[provider] 23 | } 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 2 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 3 | github.com/levigross/grequests v0.0.0-20180715163950-d0df86deffcb h1:ah0KveUT2AATyy8fmPGbZoAfsEtLt5HA60innQYZnyM= 4 | github.com/levigross/grequests v0.0.0-20180715163950-d0df86deffcb/go.mod h1:uCZIhROSrVmuF/BPYFPwDeiiQ6juSLp0kikFoEcNcEs= 5 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM= 6 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 7 | -------------------------------------------------------------------------------- /leancloud/fileref.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | // FileRef refer to a File object in _File class 4 | type FileRef struct { 5 | c *Client 6 | class string 7 | ID string 8 | } 9 | 10 | // Get fetch the referred _File object 11 | func (ref *FileRef) Get(file *File, authOptions ...AuthOption) error { 12 | err := objectGet(ref, file, authOptions...) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | return nil 18 | } 19 | 20 | // Destroy delete the referred _File object 21 | func (ref *FileRef) Destroy(authOptions ...AuthOption) error { 22 | if err := objectDestroy(ref, authOptions...); err != nil { 23 | return err 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /leancloud/roleref.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type RoleRef struct { 4 | c *Client 5 | class string 6 | ID string 7 | } 8 | 9 | func (client *Client) Role(id string) *RoleRef { 10 | return &RoleRef{ 11 | c: client, 12 | class: "roles", 13 | ID: id, 14 | } 15 | } 16 | 17 | func (ref *RoleRef) Get(authOption ...AuthOption) (*Role, error) { 18 | return nil, nil 19 | } 20 | 21 | func (ref *RoleRef) Set(field string, value interface{}, authOptions ...AuthOption) error { 22 | return nil 23 | } 24 | 25 | func (ref *RoleRef) Update(data map[string]interface{}, authOptions ...AuthOption) error { 26 | return nil 27 | } 28 | 29 | func (ref *RoleRef) UpdateWithQuery(data map[string]interface{}, authOptions ...AuthOption) error { 30 | // TODO 31 | return nil 32 | } 33 | 34 | func (ref *RoleRef) Destroy(authOptions ...AuthOption) error { 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LeanCloud Go SDK 2 | Golang SDK for LeanCloud Storage and LeanEngine. 3 | 4 | ```go 5 | import "github.com/leancloud/go-sdk/leancloud" 6 | ``` 7 | 8 | ## Examples 9 | 10 | - [LeanEngine Getting Started](https://github.com/leancloud/golang-getting-started) 11 | 12 | ## Documentation 13 | 14 | - [Go SDK Setup](https://leancloud.cn/docs/sdk_setup-go.html) 15 | - [API Reference](https://pkg.go.dev/github.com/leancloud/go-sdk/leancloud) 16 | 17 | ## Development 18 | 19 | Release: 20 | 21 | - Update `Version` in `leancloud/client.go` 22 | - `git tag v..` 23 | - Update pkg.go.dev via `GOPROXY=https://proxy.golang.org GO111MODULE=on go get github.com/leancloud/go-sdk@v..` 24 | - Write changelog on [GitHub Releases](https://github.com/leancloud/go-sdk/releases) 25 | - Upgrade [golang-getting-started](https://github.com/leancloud/golang-getting-started) to latest SDK 26 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | testing: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.15 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | env: 24 | LEANCLOUD_REGION: us-w1 25 | LEANCLOUD_APP_ID: ShYpcmyUphz1iPDan8vWHToT-MdYXbMMI 26 | LEANCLOUD_APP_KEY: ${{ secrets.LEANCLOUD_APP_KEY }} 27 | LEANCLOUD_APP_MASTER_KEY: ${{ secrets.LEANCLOUD_APP_MASTER_KEY }} 28 | LEANCLOUD_API_SERVER: https://shypcmyu.api.lncldglobal.com 29 | TEST_USER_ID: 6045e1abd0ba635b64a16152 30 | TEST_USERNAME: ${{ secrets.TEST_USERNAME }} 31 | TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} 32 | -------------------------------------------------------------------------------- /leancloud/class.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type Class struct { 4 | c *Client 5 | Name string 6 | } 7 | 8 | // ID constructs reference with objectId and className 9 | func (ref *Class) ID(id string) *ObjectRef { 10 | return &ObjectRef{ 11 | c: ref.c, 12 | class: ref.Name, 13 | ID: id, 14 | } 15 | } 16 | 17 | // Create write the Object to the Storage from the custom structure/bare Object/map. 18 | func (ref *Class) Create(object interface{}, authOptions ...AuthOption) (*ObjectRef, error) { 19 | newRef, err := objectCreate(ref, object, authOptions...) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | objectRef, _ := newRef.(*ObjectRef) 25 | 26 | return objectRef, nil 27 | } 28 | 29 | // NewQuery constructs a new Query for general Class 30 | func (ref *Class) NewQuery() *Query { 31 | return &Query{ 32 | c: ref.c, 33 | class: ref, 34 | where: make(map[string]interface{}), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /leancloud/auth_options.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/levigross/grequests" 7 | ) 8 | 9 | type AuthOption interface { 10 | apply(*Client, *grequests.RequestOptions) 11 | } 12 | 13 | type authOption struct { 14 | useMasterKey bool 15 | sessionToken string 16 | } 17 | 18 | func (option *authOption) apply(client *Client, request *grequests.RequestOptions) { 19 | if option.useMasterKey { 20 | request.Headers["X-LC-Key"] = fmt.Sprint(client.masterKey, ",master") 21 | } 22 | 23 | if option.sessionToken != "" { 24 | request.Headers["X-LC-Session"] = option.sessionToken 25 | } 26 | } 27 | 28 | func UseMasterKey(useMasterKey bool) AuthOption { 29 | return &authOption{ 30 | useMasterKey: useMasterKey, 31 | } 32 | } 33 | 34 | func UseSessionToken(sessionToken string) AuthOption { 35 | return &authOption{ 36 | sessionToken: sessionToken, 37 | } 38 | } 39 | 40 | func UseUser(user *User) AuthOption { 41 | return &authOption{ 42 | sessionToken: user.SessionToken, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /leancloud/operator.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type Op struct { 4 | name string 5 | objects interface{} 6 | } 7 | 8 | func OpIncrement(amount interface{}) Op { 9 | return Op{ 10 | name: "Increment", 11 | objects: amount, 12 | } 13 | } 14 | 15 | func OpDecrement(amount interface{}) Op { 16 | return Op{ 17 | name: "Decrement", 18 | objects: amount, 19 | } 20 | } 21 | func OpAdd(objects interface{}) Op { 22 | return Op{ 23 | name: "Add", 24 | objects: objects, 25 | } 26 | } 27 | 28 | func OpAddUnique(objects interface{}) Op { 29 | return Op{ 30 | name: "AddUnique", 31 | objects: objects, 32 | } 33 | } 34 | func OpRemove(objects interface{}) Op { 35 | return Op{ 36 | name: "Remove", 37 | objects: objects, 38 | } 39 | } 40 | 41 | func OpDelete() Op { 42 | return Op{ 43 | name: "Delete", 44 | } 45 | } 46 | 47 | func OpAddRelation(objects interface{}) Op { 48 | // TODO 49 | return Op{} 50 | } 51 | 52 | func OpRemoveRelation(objects interface{}) Op { 53 | // TODO 54 | return Op{} 55 | } 56 | 57 | func OpBitAnd(value interface{}) Op { 58 | return Op{ 59 | name: "BitAnd", 60 | objects: value, 61 | } 62 | } 63 | 64 | func OpBitOr(value interface{}) Op { 65 | return Op{ 66 | name: "BitAnd", 67 | objects: value, 68 | } 69 | } 70 | 71 | func OpBitXor(value interface{}) Op { 72 | return Op{ 73 | name: "BitAnd", 74 | objects: value, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /leancloud/geopoint.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import "math" 4 | 5 | // GeoPoint contains location's latitude and longitude 6 | type GeoPoint struct { 7 | Latitude float64 `json:"latitude"` 8 | Longitude float64 `json:"longitude"` 9 | } 10 | 11 | // RadiansTo return the distance from this GeoPoint to another in radians 12 | func (point *GeoPoint) RadiansTo(target *GeoPoint) float64 { 13 | radius := math.Pi / 180.0 14 | startLatRadius := point.Latitude * radius 15 | startLongRadius := point.Longitude * radius 16 | 17 | endLatRadius := target.Latitude * radius 18 | endLongRadius := target.Longitude * radius 19 | 20 | deltaLat := startLatRadius - endLatRadius 21 | deltaLong := startLongRadius - endLongRadius 22 | 23 | latSinDelta := math.Sin(deltaLat / 2.0) 24 | longSinDelta := math.Sin(deltaLong / 2.0) 25 | 26 | a := (latSinDelta * longSinDelta) + (math.Cos(startLatRadius) * math.Cos(endLatRadius) * longSinDelta * longSinDelta) 27 | 28 | a = math.Min(1.0, a) 29 | 30 | return (2 * math.Asin(math.Sqrt(a))) 31 | } 32 | 33 | // KilometersTo return the distance from this GeoPoint to another in kilometers 34 | func (point *GeoPoint) KilometersTo(target *GeoPoint) float64 { 35 | return (point.RadiansTo(target) * 6371.0) 36 | } 37 | 38 | // MilesTo return the distance from this GeoPoint to another in miles 39 | func (point *GeoPoint) MilesTo(target *GeoPoint) float64 { 40 | return (point.RadiansTo(target) * 3958.8) 41 | } 42 | -------------------------------------------------------------------------------- /leancloud/error.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // CloudError contains user-defined error 12 | type CloudError struct { 13 | Code int `json:"code"` 14 | Message string `json:"error"` 15 | StatusCode int `json:"-"` 16 | callStack []byte 17 | } 18 | 19 | func (err CloudError) Error() string { 20 | return fmt.Sprintf("CloudError: code: %d, Message: %s\n", err.Code, err.Message) 21 | } 22 | 23 | func writeCloudError(w http.ResponseWriter, r *http.Request, err error) { 24 | cloudErr, ok := err.(CloudError) 25 | if !ok { 26 | writeCloudError(w, r, CloudError{ 27 | Code: 1, 28 | Message: err.Error(), 29 | StatusCode: http.StatusBadRequest, 30 | }) 31 | return 32 | } 33 | 34 | cloudErrJSON, err := json.Marshal(cloudErr) 35 | if err != nil { 36 | w.WriteHeader(http.StatusInternalServerError) 37 | w.Write([]byte(fmt.Sprintf("%s: %s\n", err.Error(), cloudErr.Error()))) 38 | return 39 | } 40 | 41 | if len(cloudErr.callStack) != 0 { 42 | builder := new(strings.Builder) // for better performance when converting []byte to string. see https://pkg.go.dev/strings#Builder 43 | builder.Write(cloudErr.callStack) 44 | fmt.Fprintln(os.Stderr, builder.String()) 45 | } 46 | 47 | if cloudErr.StatusCode == 0 { 48 | cloudErr.StatusCode = http.StatusBadRequest 49 | } 50 | w.WriteHeader(cloudErr.StatusCode) 51 | w.Write(cloudErrJSON) 52 | } 53 | -------------------------------------------------------------------------------- /leancloud/userref.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | type UserRef struct { 4 | c *Client 5 | class string 6 | ID string 7 | } 8 | 9 | func (client *Client) User(user interface{}) *UserRef { 10 | if meta := extractUserMeta(user); meta != nil { 11 | return &UserRef{ 12 | c: client, 13 | class: "users", 14 | ID: meta.ID, 15 | } 16 | } 17 | return nil 18 | } 19 | 20 | func (ref *Users) ID(id string) *UserRef { 21 | return &UserRef{ 22 | c: ref.c, 23 | class: "users", 24 | ID: id, 25 | } 26 | } 27 | 28 | func (ref *UserRef) Get(user interface{}, authOptions ...AuthOption) error { 29 | if ref == nil || ref.ID == "" || ref.class == "" { 30 | return nil 31 | } 32 | if err := objectGet(ref, user, authOptions...); err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func (ref *UserRef) Set(key string, value interface{}, authOptions ...AuthOption) error { 40 | if ref == nil || ref.ID == "" || ref.class == "" { 41 | return nil 42 | } 43 | if err := objectSet(ref, key, value, authOptions...); err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (ref *UserRef) Update(diff interface{}, authOptions ...AuthOption) error { 51 | if ref == nil || ref.ID == "" || ref.class == "" { 52 | return nil 53 | } 54 | if err := objectUpdate(ref, diff, authOptions...); err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (ref *UserRef) UpdateWithQuery(diff interface{}, query *Query, authOptions ...AuthOption) error { 62 | if ref == nil || ref.ID == "" || ref.class == "" { 63 | return nil 64 | } 65 | if err := objectUpdateWithQuery(ref, diff, query, authOptions...); err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (ref *UserRef) Destroy(authOptions ...AuthOption) error { 73 | if ref == nil || ref.ID == "" || ref.class == "" { 74 | return nil 75 | } 76 | if err := objectDestroy(ref, authOptions...); err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /leancloud/acl.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import "fmt" 4 | 5 | // ACL include permission group of object 6 | type ACL struct { 7 | content map[string]map[string]bool 8 | } 9 | 10 | // NewACL constructs a new ACL 11 | func NewACL() *ACL { 12 | acl := new(ACL) 13 | acl.content = make(map[string]map[string]bool) 14 | return acl 15 | } 16 | 17 | func NewACLWithUser(user *User) *ACL { 18 | acl := NewACL() 19 | acl.set(user.ID, "read", true) 20 | acl.set(user.ID, "write", true) 21 | return acl 22 | } 23 | 24 | func (acl *ACL) SetPublicReadAccess(allowed bool) { 25 | acl.set("*", "read", allowed) 26 | } 27 | 28 | func (acl *ACL) SetPublicWriteAccess(allowed bool) { 29 | acl.set("*", "write", allowed) 30 | } 31 | 32 | func (acl *ACL) SetWriteAccess(user *User, allowed bool) { 33 | acl.set(user.ID, "write", allowed) 34 | } 35 | 36 | func (acl *ACL) SetReadAccess(user *User, allowed bool) { 37 | acl.set(user.ID, "read", allowed) 38 | } 39 | 40 | func (acl *ACL) SetRoleReadAccess(role *Role, allowed bool) { 41 | acl.set(fmt.Sprint("role:", role.Name), "read", allowed) 42 | } 43 | 44 | func (acl *ACL) SetRoleWriteAccess(role *Role, allowed bool) { 45 | acl.set(fmt.Sprint("role:", role.Name), "write", allowed) 46 | } 47 | 48 | func (acl *ACL) GetPublicReadAccess() bool { 49 | return acl.get("*", "read") 50 | } 51 | 52 | func (acl *ACL) GetPublicWriteAccess() bool { 53 | return acl.get("*", "write") 54 | } 55 | 56 | func (acl *ACL) GetReadAccess(user *User) bool { 57 | return acl.get(user.ID, "read") 58 | } 59 | 60 | func (acl *ACL) GetWriteAccess(user *User) bool { 61 | return acl.get(user.ID, "write") 62 | } 63 | 64 | func (acl *ACL) GetRoleReadAccess(role *Role) bool { 65 | return acl.get(fmt.Sprint("role:", role.Name), "read") 66 | } 67 | 68 | func (acl *ACL) GetRoleWriteAccess(role *Role) bool { 69 | return acl.get(fmt.Sprint("role:", role.Name), "write") 70 | } 71 | 72 | func (acl *ACL) set(key, perm string, allowed bool) { 73 | acl.content[key][perm] = allowed 74 | } 75 | 76 | func (acl *ACL) get(key, perm string) bool { 77 | return acl.content[key][perm] 78 | } 79 | -------------------------------------------------------------------------------- /leancloud/operator_test.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestOperatorEncode(t *testing.T) { 10 | t.Run("Increment/Decrement", func(t *testing.T) { 11 | ret := encode(OpIncrement(1), false) 12 | if !reflect.DeepEqual(ret, map[string]interface{}{ 13 | "__op": "Increment", 14 | "amount": 1, 15 | }) { 16 | t.FailNow() 17 | } 18 | }) 19 | 20 | t.Run("Add/AddUnique/Remove", func(t *testing.T) { 21 | ret := encode(OpAdd([]string{"Hello", "World"}), false) 22 | if !reflect.DeepEqual(ret, map[string]interface{}{ 23 | "__op": "Add", 24 | "objects": []string{"Hello", "World"}, 25 | }) { 26 | t.FailNow() 27 | } 28 | }) 29 | 30 | t.Run("Delete", func(t *testing.T) { 31 | ret := encode(OpDelete(), false) 32 | if !reflect.DeepEqual(ret, map[string]interface{}{ 33 | "__op": "Delete", 34 | }) { 35 | t.FailNow() 36 | } 37 | }) 38 | } 39 | 40 | func TestOperatorDecode(t *testing.T) { 41 | t.Run("Increment/Decrement", func(t *testing.T) { 42 | decodedOp, err := decode(map[string]interface{}{ 43 | "__op": "Increment", 44 | "amount": 1, 45 | }) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | op, ok := decodedOp.(*Op) 50 | if !ok { 51 | t.Fatal(fmt.Errorf("bad Op type: %v", reflect.TypeOf(decodedOp))) 52 | } 53 | 54 | if op.name != "Increment" && op.objects != 1 { 55 | t.FailNow() 56 | } 57 | }) 58 | 59 | t.Run("Add/AddUnique/Remove", func(t *testing.T) { 60 | decodedOp, err := decode(map[string]interface{}{ 61 | "__op": "Increment", 62 | "objects": []string{"Hello", "World"}, 63 | }) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | op, ok := decodedOp.(*Op) 68 | if !ok { 69 | t.Fatal(fmt.Errorf("bad Op type: %v", reflect.TypeOf(decodedOp))) 70 | } 71 | 72 | if op.name != "Add" && reflect.DeepEqual(op.objects, []string{"Hello", "World"}) { 73 | t.FailNow() 74 | } 75 | }) 76 | 77 | t.Run("Delete", func(t *testing.T) { 78 | decodedOp, err := decode(map[string]interface{}{ 79 | "__op": "Delete", 80 | }) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | op, ok := decodedOp.(*Op) 85 | if !ok { 86 | t.Fatal(fmt.Errorf("bad Op type: %v", reflect.TypeOf(decodedOp))) 87 | } 88 | 89 | if op.name != "Delete" { 90 | t.FailNow() 91 | } 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /leancloud/client.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | const Version = "0.3.2" 11 | 12 | type Client struct { 13 | serverURL string 14 | appID string 15 | appKey string 16 | masterKey string 17 | production string 18 | requestLogger *log.Logger 19 | Users Users 20 | Files Files 21 | Roles Roles 22 | } 23 | 24 | type ClientOptions struct { 25 | AppID string 26 | AppKey string 27 | MasterKey string 28 | ServerURL string 29 | Production string 30 | } 31 | 32 | // NewClient constructs a client from parameters 33 | func NewClient(options *ClientOptions) *Client { 34 | client := &Client{ 35 | appID: options.AppID, 36 | appKey: options.AppKey, 37 | masterKey: options.MasterKey, 38 | serverURL: options.ServerURL, 39 | production: options.Production, 40 | } 41 | 42 | if !strings.HasSuffix(options.AppID, "MdYXbMMI") { 43 | if client.serverURL == "" { 44 | panic(fmt.Errorf("please set API's serverURL")) 45 | } 46 | } 47 | 48 | _, debugEnabled := os.LookupEnv("LEANCLOUD_DEBUG") 49 | 50 | if debugEnabled { 51 | client.requestLogger = log.New(os.Stdout, "", log.LstdFlags) 52 | } 53 | 54 | client.Users.c = client 55 | client.Files.c = client 56 | client.Roles.c = client 57 | return client 58 | } 59 | 60 | // NewEnvClient constructs a client from environment variables 61 | func NewEnvClient() *Client { 62 | options := &ClientOptions{ 63 | AppID: os.Getenv("LEANCLOUD_APP_ID"), 64 | AppKey: os.Getenv("LEANCLOUD_APP_KEY"), 65 | MasterKey: os.Getenv("LEANCLOUD_APP_MASTER_KEY"), 66 | ServerURL: os.Getenv("LEANCLOUD_API_SERVER"), 67 | } 68 | 69 | if appEnv := os.Getenv("LEANCLOUD_APP_ENV"); appEnv == "production" { 70 | options.Production = "1" 71 | } else if appEnv == "stage" { 72 | options.Production = "0" 73 | } else { // probably on local machine 74 | if os.Getenv("LEAN_CLI_HAVE_STAGING") == "true" { 75 | options.Production = "0" 76 | } else { // free trial instance only 77 | options.Production = "1" 78 | } 79 | } 80 | 81 | return NewClient(options) 82 | } 83 | 84 | // SetProduction sets the production environment 85 | func (client *Client) SetProduction(production bool) { 86 | if production { 87 | client.production = "1" 88 | } else { 89 | client.production = "0" 90 | } 91 | } 92 | 93 | // Class constructs a reference of Class 94 | func (client *Client) Class(name string) *Class { 95 | return &Class{ 96 | c: client, 97 | Name: name, 98 | } 99 | } 100 | 101 | // File construct a new reference to a _File object by given objectId 102 | func (client *Client) File(id string) *FileRef { 103 | return &FileRef{ 104 | c: client, 105 | class: "files", 106 | ID: id, 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /leancloud/server_test.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "testing" 10 | 11 | "github.com/levigross/grequests" 12 | ) 13 | 14 | var cloudEndpoint = "http://localhost:3000" 15 | 16 | func TestMain(m *testing.M) { 17 | go http.ListenAndServe(":3000", Engine.Handler()) 18 | 19 | os.Exit(m.Run()) 20 | } 21 | func TestMetadataResponse(t *testing.T) { 22 | resp, err := grequests.Get(cloudEndpoint+"/1.1/functions/_ops/metadatas", &grequests.RequestOptions{}) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | metadata := new(metadataResponse) 28 | if err := json.NewDecoder(resp.RawResponse.Body).Decode(metadata); err != nil { 29 | if err != io.EOF { 30 | t.Fatal(err) 31 | } 32 | } 33 | 34 | for _, v := range metadata.Result { 35 | if Engine.(*engine).functions[v] == nil { 36 | t.Fatal(fmt.Errorf("cannot found cloud function")) 37 | } 38 | } 39 | } 40 | 41 | func TestHandler(t *testing.T) { 42 | t.Run("function call", func(t *testing.T) { 43 | resp, err := grequests.Get(cloudEndpoint+"/1.1/functions/hello", &grequests.RequestOptions{ 44 | Headers: map[string]string{ 45 | "X-LC-Id": os.Getenv("LEANCLOUD_APP_ID"), 46 | "X-LC-Key": os.Getenv("LEANCLOUD_APP_KEY"), 47 | }, 48 | }) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | ret := new(functionResponse) 54 | if err := json.Unmarshal(resp.Bytes(), ret); err != nil { 55 | t.Log(string(resp.Bytes())) 56 | t.Fatal(err) 57 | } 58 | 59 | respBody, ok := ret.Result.(map[string]interface{}) 60 | if !ok { 61 | t.Fatal("unexpected response format") 62 | } 63 | 64 | if respBody["Hello"] != "World" { 65 | t.Fatal("unexpected response format") 66 | } 67 | }) 68 | 69 | t.Run("function call with sessionToken", func(t *testing.T) { 70 | user := new(User) 71 | if err := client.Users.ID(testUserID).Get(user, UseMasterKey(true)); err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | options := grequests.RequestOptions{ 76 | Headers: map[string]string{ 77 | "X-LC-Id": os.Getenv("LEANCLOUD_APP_ID"), 78 | "X-LC-Key": os.Getenv("LEANCLOUD_APP_KEY"), 79 | "X-LC-Session": user.SessionToken, 80 | }, 81 | } 82 | 83 | resp, err := grequests.Get(cloudEndpoint+"/1.1/functions/hello_with_option_fetch_user", &options) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | ret := new(functionResponse) 89 | if err := json.Unmarshal(resp.Bytes(), ret); err != nil { 90 | t.Log(string(resp.Bytes())) 91 | t.Fatal(err) 92 | } 93 | 94 | respBody, ok := ret.Result.(map[string]interface{}) 95 | if !ok { 96 | t.Fatal("unexpected response format") 97 | } 98 | 99 | if respBody["sessionToken"] != user.SessionToken { 100 | t.Fatal("unexpected response format") 101 | } 102 | }) 103 | 104 | t.Run("function call should not found", func(t *testing.T) { 105 | resp, err := grequests.Get(cloudEndpoint+"/1.1/functions/not_found", nil) 106 | if err != nil { 107 | if resp.StatusCode != http.StatusNotFound { 108 | t.Fatal(err) 109 | } 110 | } 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /leancloud/object.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "reflect" 5 | "time" 6 | ) 7 | 8 | // Object contains full data of Object. 9 | // Object could also be metadata for custom structure 10 | type Object struct { 11 | ID string `json:"objectId"` 12 | CreatedAt time.Time `json:"createdAt"` 13 | UpdatedAt time.Time `json:"updatedAt"` 14 | fields map[string]interface{} 15 | isPointer bool 16 | isIncluded bool 17 | ref interface{} 18 | } 19 | 20 | // Clone binds Object to user-defined type 21 | func (object *Object) Clone(dst interface{}) error { 22 | return bind(reflect.Indirect(reflect.ValueOf(object)), reflect.Indirect(reflect.ValueOf(dst))) 23 | } 24 | 25 | // Raw returns raw data of Object in form of map 26 | func (object *Object) Raw() map[string]interface{} { 27 | return object.fields 28 | } 29 | 30 | // Get returns value of the key 31 | func (object *Object) Get(key string) interface{} { 32 | return object.fields[key] 33 | } 34 | 35 | // Int returns int64 value of the key 36 | func (object *Object) Int(key string) int64 { 37 | return reflect.ValueOf(object.fields[key]).Int() 38 | } 39 | 40 | // String returns string value of the key 41 | func (object *Object) String(key string) string { 42 | return reflect.ValueOf(object.fields[key]).String() 43 | } 44 | 45 | // Float returns float64 value of the key 46 | func (object *Object) Float(key string) float64 { 47 | return reflect.ValueOf(object.fields[key]).Float() 48 | } 49 | 50 | // Bool returns boolean value of the key 51 | func (object *Object) Bool(key string) bool { 52 | return reflect.ValueOf(object.fields[key]).Bool() 53 | } 54 | 55 | // GeoPoint returns GeoPoint value of the key 56 | func (object *Object) GeoPoint(key string) *GeoPoint { 57 | pointPtr, ok := object.fields[key].(*GeoPoint) 58 | if !ok { 59 | point, ok := object.fields[key].(GeoPoint) 60 | if !ok { 61 | return nil 62 | } 63 | return &point 64 | } 65 | return pointPtr 66 | } 67 | 68 | // Date returns time.Time value of the key 69 | func (object *Object) Date(key string) *time.Time { 70 | datePtr, ok := object.fields[key].(*time.Time) 71 | if !ok { 72 | date, ok := object.fields[key].(time.Time) 73 | if !ok { 74 | return nil 75 | } 76 | return &date 77 | } 78 | return datePtr 79 | } 80 | 81 | // File returns File value of the key 82 | func (object *Object) File(key string) *File { 83 | filePtr, ok := object.fields[key].(*File) 84 | if !ok { 85 | file, ok := object.fields[key].(File) 86 | if !ok { 87 | return nil 88 | } 89 | return &file 90 | } 91 | return filePtr 92 | } 93 | 94 | // Bytes returns []byte value of the key 95 | func (object *Object) Bytes(key string) []byte { 96 | bytes, ok := object.fields[key].([]byte) 97 | if !ok { 98 | return nil 99 | } 100 | return bytes 101 | } 102 | 103 | // ACL returns ACL value 104 | func (object *Object) ACL() *ACL { 105 | aclPtr, ok := object.fields["ACL"].(*ACL) 106 | if !ok { 107 | acl, ok := object.fields["ACL"].(ACL) 108 | if !ok { 109 | return nil 110 | } 111 | return &acl 112 | } 113 | return aclPtr 114 | } 115 | 116 | // IsPointer shows whether the Object is a Pointer 117 | func (object *Object) IsPointer() bool { 118 | return object.isPointer 119 | } 120 | 121 | // Included shows whether the Object is a included Pointer 122 | func (object *Object) Included() bool { 123 | return object.isIncluded 124 | } 125 | -------------------------------------------------------------------------------- /leancloud/files_test.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "mime" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/levigross/grequests" 12 | ) 13 | 14 | var c *Client 15 | 16 | var testUsername string 17 | var testPassword string 18 | 19 | func init() { 20 | c = NewEnvClient() 21 | testUsername = os.Getenv("TEST_USERNAME") 22 | testPassword = os.Getenv("TEST_PASSWORD") 23 | } 24 | 25 | func generateTempFile(pattern string) (string, error) { 26 | content := []byte("temporary file's content") 27 | tmpfile, err := ioutil.TempFile("", "go-sdk-file-upload-*.txt") 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | if _, err := tmpfile.Write(content); err != nil { 33 | return "", err 34 | } 35 | 36 | name := tmpfile.Name() 37 | 38 | if err := tmpfile.Close(); err != nil { 39 | return "", err 40 | } 41 | 42 | return name, nil 43 | } 44 | 45 | func checkUpload(url string) error { 46 | resp, err := grequests.Get(url, nil) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if resp.StatusCode != 200 { 52 | return fmt.Errorf("unable to get file with url: %v", url) 53 | } 54 | 55 | return nil 56 | } 57 | func TestFilesUpload(t *testing.T) { 58 | filename, err := generateTempFile("go-sdk-file-upload-*.txt") 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | defer os.Remove(filename) 64 | 65 | fd, err := os.Open(filename) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | _, name := filepath.Split(filename) 71 | file := &File{ 72 | Name: name, 73 | MIME: mime.TypeByExtension(filepath.Ext(name)), 74 | } 75 | 76 | if err := c.Files.Upload(file, fd); err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | if err := checkUpload(file.URL); err != nil { 81 | t.Fatal(err) 82 | } 83 | } 84 | 85 | func TestFilesUploadWithOwner(t *testing.T) { 86 | filename, err := generateTempFile("go-sdk-file-upload-*.txt") 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | defer os.Remove(filename) 92 | 93 | fd, err := os.Open(filename) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | _, name := filepath.Split(filename) 99 | file := &File{ 100 | Name: name, 101 | MIME: mime.TypeByExtension(filepath.Ext(name)), 102 | } 103 | 104 | user, err := client.Users.LogIn(testUsername, testPassword) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | if err := c.Files.Upload(file, fd, UseUser(user)); err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | if err := checkUpload(file.URL); err != nil { 114 | t.Fatal(err) 115 | } 116 | } 117 | func TestFilesUploadFromURL(t *testing.T) { 118 | file := &File{ 119 | Name: "go-sdk-file-upload.txt", 120 | MIME: "text/plain", 121 | URL: "https://example.com/assets/go-sdk-file-upload.txt", 122 | } 123 | 124 | if err := c.Files.UploadFromURL(file); err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | if file.ID == "" { 129 | t.Fatal("unable to create _File object") 130 | } 131 | } 132 | 133 | func TestFilesUploadFromFile(t *testing.T) { 134 | filename, err := generateTempFile("go-sdk-file-upload-*.txt") 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | 139 | defer os.Remove(filename) 140 | 141 | file := &File{ 142 | MetaData: map[string]interface{}{ 143 | "comment": "This is a comment of Metadata", 144 | }, 145 | } 146 | 147 | if err := c.Files.UploadFromLocalFile(file, filename); err != nil { 148 | t.Fatal(err) 149 | } 150 | 151 | if err := checkUpload(file.URL); err != nil { 152 | t.Fatal(err) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /leancloud/client_test.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | var client *Client 10 | 11 | func init() { 12 | client = NewEnvClient() 13 | Engine.Init(client) 14 | } 15 | 16 | func TestNewClient(t *testing.T) { 17 | appID, appKey, masterKey, serverURL := os.Getenv("LEANCLOUD_APP_ID"), os.Getenv("LEANCLOUD_APP_KEY"), os.Getenv("LEANCLOUD_APP_MASTER_KEY"), os.Getenv("LEANCLOUD_API_SERVER") 18 | options := &ClientOptions{ 19 | AppID: appID, 20 | AppKey: appKey, 21 | MasterKey: masterKey, 22 | ServerURL: serverURL, 23 | } 24 | t.Run("Production", func(t *testing.T) { 25 | client := NewClient(options) 26 | if client == nil { 27 | t.Fatal(errors.New("unable to create a client")) 28 | } 29 | if client.appID != appID { 30 | t.Fatal(errors.New("LEANCLOUD_APP_ID unmatch")) 31 | } 32 | if client.appKey != appKey { 33 | t.Fatal(errors.New("LEANCLOUD_APP_KEY unmatch")) 34 | } 35 | if client.masterKey != masterKey { 36 | t.Fatal(errors.New("LEANCLOUD_APP_MASTER_KEY unmatch")) 37 | } 38 | }) 39 | 40 | t.Run("Debug", func(t *testing.T) { 41 | if err := os.Setenv("LEANCLOUD_DEBUG", "true"); err != nil { 42 | t.Fatal("unable to set debugging flag") 43 | } 44 | client := NewClient(options) 45 | if client == nil { 46 | t.Fatal(errors.New("unable to create a client")) 47 | } 48 | if client.appID != appID { 49 | t.Fatal(errors.New("LEANCLOUD_APP_ID unmatch")) 50 | } 51 | if client.appKey != appKey { 52 | t.Fatal(errors.New("LEANCLOUD_APP_KEY unmatch")) 53 | } 54 | if client.masterKey != masterKey { 55 | t.Fatal(errors.New("LEANCLOUD_APP_MASTER_KEY unmatch")) 56 | } 57 | if client.requestLogger == nil { 58 | t.Fatal(errors.New("unable to set logger")) 59 | } 60 | }) 61 | } 62 | 63 | func TestNewEnvClient(t *testing.T) { 64 | appID, appKey, masterKey, serverURL := os.Getenv("LEANCLOUD_APP_ID"), os.Getenv("LEANCLOUD_APP_KEY"), os.Getenv("LEANCLOUD_APP_MASTER_KEY"), os.Getenv("LEANCLOUD_API_SERVER") 65 | t.Run("Production", func(t *testing.T) { 66 | client := NewEnvClient() 67 | if client == nil { 68 | t.Fatal(errors.New("unable to create a client")) 69 | } 70 | if client.appID != appID { 71 | t.Fatal(errors.New("LEANCLOUD_APP_ID unmatch")) 72 | } 73 | if client.appKey != appKey { 74 | t.Fatal(errors.New("LEANCLOUD_APP_KEY unmatch")) 75 | } 76 | if client.masterKey != masterKey { 77 | t.Fatal(errors.New("LEANCLOUD_APP_MASTER_KEY unmatch")) 78 | } 79 | if client.serverURL != serverURL { 80 | t.Fatal(errors.New("LEANCLOUD_API_SERVER unmatch")) 81 | } 82 | }) 83 | 84 | t.Run("Debug", func(t *testing.T) { 85 | if err := os.Setenv("LEANCLOUD_DEBUG", "true"); err != nil { 86 | t.Fatal("unable to set debugging flag") 87 | } 88 | client := NewEnvClient() 89 | if client == nil { 90 | t.Fatal(errors.New("unable to create a client")) 91 | } 92 | if client.appID != appID { 93 | t.Fatal(errors.New("LEANCLOUD_APP_ID unmatch")) 94 | } 95 | if client.appKey != appKey { 96 | t.Fatal(errors.New("LEANCLOUD_APP_KEY unmatch")) 97 | } 98 | if client.masterKey != masterKey { 99 | t.Fatal(errors.New("LEANCLOUD_APP_MASTER_KEY unmatch")) 100 | } 101 | if client.requestLogger == nil { 102 | t.Fatal(errors.New("unable to set logger")) 103 | } 104 | }) 105 | } 106 | 107 | func TestClientClass(t *testing.T) { 108 | client := &Client{} 109 | class := client.Class("class") 110 | if class.c != client { 111 | t.Fatal(errors.New("client unmatch")) 112 | } 113 | if class.Name != "class" { 114 | t.Fatal(errors.New("name of class unmatch")) 115 | } 116 | } 117 | 118 | func TestClientObject(t *testing.T) { 119 | client := &Client{} 120 | ref := client.Class("class").ID("f47ac10b58cc4372a5670e02b2c3d479") 121 | if ref.c != client { 122 | t.Fatal(errors.New("client unmatch")) 123 | } 124 | if ref.class != "class" { 125 | t.Fatal(errors.New("name of class unmatch")) 126 | } 127 | if ref.ID != "f47ac10b58cc4372a5670e02b2c3d479" { 128 | t.Fatal(errors.New("ID unmatch")) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /leancloud/request.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "runtime" 7 | "strings" 8 | "time" 9 | 10 | "github.com/levigross/grequests" 11 | ) 12 | 13 | type requestMethod string 14 | 15 | var requestCount int 16 | 17 | const ( 18 | methodGet requestMethod = "GET" 19 | methodPost requestMethod = "POST" 20 | methodPut requestMethod = "PUT" 21 | methodPatch requestMethod = "PATCH" 22 | methodDelete requestMethod = "DELETE" 23 | ) 24 | 25 | type objectResponse map[string]interface{} 26 | 27 | type objectsResponse struct { 28 | Results []objectResponse `json:"results"` 29 | } 30 | 31 | type createObjectResponse struct { 32 | ObjectID string `json:"objectId"` 33 | CreatedAt time.Time `json:"createdAt"` 34 | } 35 | 36 | type cqlResponse struct { 37 | objectsResponse 38 | 39 | ClassName string `json:"className"` 40 | } 41 | 42 | type ParseResponseError struct { 43 | ParseError error 44 | ResponseHeader http.Header 45 | ResponseText string 46 | StatusCode int 47 | URL string 48 | } 49 | 50 | type ServerResponseError struct { 51 | Code int `json:"code"` 52 | Err string `json:"error"` 53 | StatusCode int 54 | URL string 55 | } 56 | 57 | func (err *ParseResponseError) Error() string { 58 | return fmt.Sprintf("parse response failed(%d): %s [%s (%d)]", err.StatusCode, err.ResponseText, err.URL, err.StatusCode) 59 | } 60 | 61 | func (err *ServerResponseError) Error() string { 62 | return fmt.Sprintf("%d %s [%s (%d)]", err.Code, err.Err, err.URL, err.StatusCode) 63 | } 64 | 65 | func (client *Client) getServerURL() string { 66 | if client.serverURL != "" { 67 | return client.serverURL 68 | } 69 | 70 | return fmt.Sprint("https://", strings.ToLower(client.appID[:8]), ".api.lncldglobal.com") 71 | } 72 | 73 | func (client *Client) getRequestOptions() *grequests.RequestOptions { 74 | return &grequests.RequestOptions{ 75 | UserAgent: getUserAgent(), 76 | Headers: map[string]string{ 77 | "X-LC-Id": client.appID, 78 | "X-LC-Key": client.appKey, 79 | "X-LC-Prod": client.production, 80 | }, 81 | } 82 | } 83 | 84 | func (client *Client) request(method requestMethod, path string, options *grequests.RequestOptions, authOptions ...AuthOption) (*grequests.Response, error) { 85 | if options == nil { 86 | options = client.getRequestOptions() 87 | } 88 | 89 | for _, authOption := range authOptions { 90 | authOption.apply(client, options) 91 | } 92 | 93 | URL := fmt.Sprint(client.getServerURL(), path) 94 | 95 | requestID := requestCount 96 | requestCount++ 97 | 98 | if client.requestLogger != nil { 99 | client.requestLogger.Printf("[REQUEST] request(%d) %s %s %#v\n", requestID, method, URL, options) 100 | } 101 | 102 | resp, err := getRequestAgentByMethod(method)(URL, options) 103 | 104 | if err != nil { 105 | return resp, err 106 | } 107 | 108 | if !resp.Ok { 109 | errResp := &ServerResponseError{} 110 | err = resp.JSON(errResp) 111 | 112 | if err != nil { 113 | return resp, &ParseResponseError{ 114 | ParseError: err, 115 | ResponseHeader: resp.Header, 116 | ResponseText: string(resp.Bytes()), 117 | StatusCode: resp.StatusCode, 118 | URL: URL, 119 | } 120 | } 121 | 122 | errResp.StatusCode = resp.StatusCode 123 | errResp.URL = URL 124 | 125 | return resp, errResp 126 | } 127 | 128 | if client.requestLogger != nil { 129 | client.requestLogger.Printf("[REQUEST] response(%d) %d %s\n", requestID, resp.StatusCode, string(resp.Bytes())) 130 | } 131 | 132 | return resp, err 133 | } 134 | 135 | func getRequestAgentByMethod(method requestMethod) func(string, *grequests.RequestOptions) (*grequests.Response, error) { 136 | switch method { 137 | case methodGet: 138 | return grequests.Get 139 | case methodPost: 140 | return grequests.Post 141 | case methodPut: 142 | return grequests.Put 143 | case methodPatch: 144 | return grequests.Patch 145 | case methodDelete: 146 | return grequests.Delete 147 | default: 148 | panic(fmt.Sprint("invalid method: ", method)) 149 | } 150 | } 151 | 152 | func getUserAgent() string { 153 | return fmt.Sprint("LeanCloud-Golang-SDK/", Version, " ", runtime.GOOS, "/"+runtime.Version()) 154 | } 155 | -------------------------------------------------------------------------------- /leancloud/query_test.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestQueryFind(t *testing.T) { 9 | jake := Staff{ 10 | Name: "Jake", 11 | Age: 20, 12 | } 13 | 14 | if _, err := client.Class("Staff").Create(&jake); err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | meeting := Meeting{ 19 | Title: "Team Meeting", 20 | Number: 1, 21 | Progress: 12.5, 22 | Host: jake, 23 | Participants: []Staff{jake, jake, jake}, 24 | Date: time.Now(), 25 | Attachment: []byte("There is nothing attachable."), 26 | Location: &GeoPoint{1, 2}, 27 | } 28 | 29 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | ret := make([]Meeting, 10) 34 | if err := client.Class("Meeting").NewQuery().EqualTo("title", "Team Meeting").Include("host").Find(&ret); err != nil { 35 | t.Fatal(err) 36 | } 37 | } 38 | 39 | func TestQueryFirst(t *testing.T) { 40 | meeting := Meeting{ 41 | Title: "Team Meeting", 42 | Number: 1, 43 | Progress: 12.5, 44 | Date: time.Now(), 45 | Attachment: []byte("There is nothing attachable."), 46 | Location: &GeoPoint{1, 2}, 47 | } 48 | 49 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | ret := new(Meeting) 54 | if err := client.Class("Meeting").NewQuery().EqualTo("title", "Team Meeting").First(ret); err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | if ret.Title != meeting.Title { 59 | t.Fatal("dismatch title") 60 | } 61 | } 62 | 63 | func TestQueryCount(t *testing.T) { 64 | meeting := Meeting{ 65 | Title: "Team Meeting", 66 | Number: 1, 67 | Progress: 12.5, 68 | Date: time.Now(), 69 | Attachment: []byte("There is nothing attachable."), 70 | Location: &GeoPoint{1, 2}, 71 | } 72 | 73 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | if count, err := client.Class("Meeting").NewQuery().EqualTo("title", "Team Meeting").Count(); err != nil { 78 | t.Fatal(err) 79 | } else { 80 | if count < 1 { 81 | t.Fatal("unexpected count of results") 82 | } 83 | } 84 | } 85 | 86 | func TestQueryExists(t *testing.T) { 87 | meeting := Meeting{ 88 | Title: "Team Meeting", 89 | Number: 1, 90 | Progress: 12.5, 91 | Date: time.Now(), 92 | Attachment: []byte("There is nothing attachable."), 93 | Location: &GeoPoint{1, 2}, 94 | } 95 | 96 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if count, err := client.Class("Meeting").NewQuery().Exists("progress").Count(); err != nil { 101 | t.Fatal(err) 102 | } else { 103 | if count < 1 { 104 | t.Fatal("unexpected count of results") 105 | } 106 | } 107 | } 108 | 109 | func TestQueryNotExists(t *testing.T) { 110 | meeting := Meeting{ 111 | Title: "Team Meeting", 112 | Number: 1, 113 | Date: time.Now(), 114 | Attachment: []byte("There is nothing attachable."), 115 | Location: &GeoPoint{1, 2}, 116 | } 117 | 118 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 119 | t.Fatal(err) 120 | } 121 | 122 | if count, err := client.Class("Meeting").NewQuery().NotExists("progress").Count(); err != nil { 123 | t.Fatal(err) 124 | } else { 125 | if count < 1 { 126 | t.Fatal("unexpected count of results") 127 | } 128 | } 129 | } 130 | 131 | type room struct { 132 | Object 133 | Name string `json:"name"` 134 | Meeting interface{} 135 | } 136 | 137 | func TestQueryMatchesQuery(t *testing.T) { 138 | res := []room{} 139 | innerQuery := client.Class("Meeting").NewQuery().EqualTo("title", "meeting1") 140 | client.Class("room").NewQuery().MatchesQuery("meeting", innerQuery).Find(&res) 141 | if len(res) < 1 || res[0].Name != "会议室1" { 142 | t.Fatal("unexpected count of results or wrong results") 143 | } 144 | } 145 | func TestQueryNotMatchesQuery(t *testing.T) { 146 | res := []room{} 147 | innerQuery := client.Class("Meeting").NewQuery().EqualTo("title", "meeting1") 148 | client.Class("room").NewQuery().NotMatchesQuery("meeting", innerQuery).Find(&res) 149 | if len(res) < 1 { 150 | t.Fatal("unexpected count of results") 151 | } 152 | for _, v := range res { 153 | if v.Name == "会议室1" { 154 | t.Fatal("wrong results") 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /leancloud/files.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "mime" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "time" 12 | ) 13 | 14 | type Files struct { 15 | c *Client 16 | } 17 | 18 | func (ref *Files) NewQuery() *Query { 19 | return &Query{ 20 | class: &Class{ 21 | Name: "_File", 22 | c: ref.c, 23 | }, 24 | c: ref.c, 25 | where: make(map[string]interface{}), 26 | } 27 | } 28 | 29 | // Upload transfer the file to cloud storage and create a File object in _File class 30 | func (ref *Files) Upload(file *File, reader io.ReadSeeker, authOptions ...AuthOption) error { 31 | size, err := getSeekerSize(reader) 32 | if err != nil { 33 | return fmt.Errorf("unexpected error when get length of file: %v", err) 34 | } 35 | 36 | owner, err := file.fetchOwner(ref.c, authOptions...) 37 | if err != nil { 38 | return fmt.Errorf("unexpected error when fetch owner: %v", err) 39 | } 40 | 41 | if file.Size == 0 { 42 | file.Size = size 43 | } 44 | 45 | if reflect.ValueOf(file.MetaData).IsNil() { 46 | file.MetaData = make(map[string]interface{}) 47 | } 48 | file.MetaData["size"] = file.Size 49 | if owner != nil { 50 | file.MetaData["owner"] = owner.ID 51 | } else { 52 | file.MetaData["owner"] = "unknown" 53 | } 54 | 55 | token, uploadURL, err := file.fetchToken(ref.c, authOptions...) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | switch file.Provider { 61 | case "qiniu": 62 | if err := file.uploadQiniu(token, "https://up.qbox.me/", reader); err != nil { 63 | if err := file.fileCallback(false, token, ref.c, authOptions...); err != nil { 64 | return err 65 | } 66 | return err 67 | } 68 | case "s3": 69 | if err := file.uploadS3(token, uploadURL, reader); err != nil { 70 | if err := file.fileCallback(false, token, ref.c, authOptions...); err != nil { 71 | return err 72 | } 73 | return err 74 | } 75 | case "qcloud": 76 | if err := file.uploadCOS(token, uploadURL, reader); err != nil { 77 | if err := file.fileCallback(false, token, ref.c, authOptions...); err != nil { 78 | return err 79 | } 80 | return err 81 | } 82 | } 83 | 84 | if err := file.fileCallback(true, token, ref.c, authOptions...); err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // UploadFromURL create an object of file in _File class with given file's url 92 | func (ref *Files) UploadFromURL(file *File, authOptions ...AuthOption) error { 93 | if reflect.ValueOf(file.MetaData).IsNil() { 94 | file.MetaData = make(map[string]interface{}) 95 | } 96 | owner, err := file.fetchOwner(ref.c, authOptions...) 97 | if err != nil { 98 | return fmt.Errorf("unexpected error when fetch owner: %v", err) 99 | } 100 | file.MetaData["__source"] = "external" 101 | if owner != nil { 102 | file.MetaData["owner"] = owner.ID 103 | } else { 104 | file.MetaData["owner"] = "unknown" 105 | } 106 | 107 | path := "/1.1/files" 108 | options := ref.c.getRequestOptions() 109 | options.JSON = encodeFile(file, false) 110 | 111 | resp, err := ref.c.request(methodPost, path, options, authOptions...) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | respJSON := make(map[string]interface{}) 117 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 118 | return err 119 | } 120 | 121 | objectID, ok := respJSON["objectId"].(string) 122 | if !ok { 123 | return fmt.Errorf("unexpected error when fetch objectId: want type string but %v", reflect.TypeOf(respJSON["objectId"])) 124 | } 125 | file.ID = objectID 126 | 127 | createdAt, ok := respJSON["createdAt"].(string) 128 | if !ok { 129 | return fmt.Errorf("unexpected error when fetch createdAt: want type string but %v", reflect.TypeOf(respJSON["createdAt"])) 130 | } 131 | decodedCreatedAt, err := time.Parse(time.RFC3339, createdAt) 132 | if err != nil { 133 | return fmt.Errorf("unexpected error when parse createdAt: %v", err) 134 | } 135 | file.CreatedAt = decodedCreatedAt 136 | 137 | return nil 138 | } 139 | 140 | // UploadFromFile transfer the file given by path to cloud storage and create an object in _File class 141 | func (ref *Files) UploadFromLocalFile(file *File, path string, authOptions ...AuthOption) error { 142 | if file.Name == "" { 143 | _, name := filepath.Split(path) 144 | file.Name = name 145 | } 146 | 147 | if file.MIME == "" { 148 | mime := mime.TypeByExtension(filepath.Ext(path)) 149 | if mime == "" { 150 | mime = "application/octet-stream" 151 | } 152 | file.MIME = mime 153 | } 154 | 155 | f, err := os.Open(path) 156 | if err != nil { 157 | return fmt.Errorf("unexpected error when open %s: %v", path, err) 158 | } 159 | 160 | if err := ref.c.Files.Upload(file, f, authOptions...); err != nil { 161 | return err 162 | } 163 | 164 | return nil 165 | } 166 | -------------------------------------------------------------------------------- /leancloud/engine_test.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var testUserID string 11 | 12 | func init() { 13 | testUserID = os.Getenv("TEST_USER_ID") 14 | Engine.Define("hello", func(r *FunctionRequest) (interface{}, error) { 15 | return map[string]string{ 16 | "Hello": "World", 17 | }, nil 18 | }) 19 | 20 | Engine.Define("hello_with_option_internal", func(r *FunctionRequest) (interface{}, error) { 21 | return map[string]string{ 22 | "Hello": "World", 23 | }, nil 24 | }, WithInternal(), WithoutFetchUser()) 25 | 26 | Engine.Define("hello_with_option_fetch_user", func(r *FunctionRequest) (interface{}, error) { 27 | return map[string]string{ 28 | "sessionToken": r.SessionToken, 29 | }, nil 30 | }) 31 | 32 | Engine.Define("hello_with_option_not_fetch_user", func(r *FunctionRequest) (interface{}, error) { 33 | return map[string]interface{}{ 34 | "sessionToken": r.SessionToken, 35 | }, nil 36 | }, WithoutFetchUser()) 37 | 38 | Engine.Define("hello_with_object", func(r *FunctionRequest) (interface{}, error) { 39 | return r.CurrentUser, nil 40 | }) 41 | } 42 | 43 | func TestRun(t *testing.T) { 44 | t.Run("local", func(t *testing.T) { 45 | resp, err := Engine.Run("hello", nil) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | respMap, ok := resp.(map[string]string) 51 | if !ok { 52 | t.Fatal(fmt.Errorf("unmatch response")) 53 | } 54 | 55 | if respMap["Hello"] != "World" { 56 | t.Fatal(fmt.Errorf("unmatch response")) 57 | } 58 | 59 | }) 60 | 61 | t.Run("hello_with_option_internal", func(t *testing.T) { 62 | t.Run("remote", func(t *testing.T) { 63 | _, err := client.Run("hello_with_option_internal", nil) 64 | 65 | if err != nil { 66 | if !strings.Contains(err.Error(), "401 Internal cloud function") { 67 | t.Fatal(err) 68 | } 69 | } 70 | }) 71 | 72 | t.Run("local", func(t *testing.T) { 73 | resp, err := Engine.Run("hello_with_option_internal", nil) 74 | 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | respMap, ok := resp.(map[string]string) 80 | if !ok { 81 | t.Fatal(fmt.Errorf("unmatch response")) 82 | } 83 | 84 | if respMap["Hello"] != "World" { 85 | t.Fatal(fmt.Errorf("unmatch response")) 86 | } 87 | }) 88 | }) 89 | 90 | t.Run("hello_with_option_fetch_user", func(t *testing.T) { 91 | user := new(User) 92 | if err := client.Users.ID(testUserID).Get(user, UseMasterKey(true)); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | t.Run("remote", func(t *testing.T) { 97 | resp, err := client.Run("hello_with_option_fetch_user", nil, WithSessionToken(user.SessionToken)) 98 | 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | 103 | respMap, ok := resp.(map[string]interface{}) 104 | if !ok { 105 | t.Fatal("unexpected response format") 106 | } 107 | 108 | sessionToken, ok := respMap["sessionToken"].(string) 109 | 110 | if !ok { 111 | t.Fatal("unexpected response format") 112 | } 113 | 114 | if sessionToken != user.SessionToken { 115 | t.Fatal("unexpected response format") 116 | } 117 | 118 | }) 119 | 120 | t.Run("local", func(t *testing.T) { 121 | resp, err := Engine.Run("hello_with_option_fetch_user", nil, WithSessionToken(user.SessionToken)) 122 | 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | respMap, ok := resp.(map[string]string) 128 | if !ok { 129 | t.Fatal("unexpected response format") 130 | } 131 | 132 | if respMap["sessionToken"] != user.SessionToken { 133 | t.Fatal("unexpected response format") 134 | } 135 | }) 136 | }) 137 | 138 | t.Run("don't fetch user", func(t *testing.T) { 139 | t.Run("remote", func(t *testing.T) { 140 | resp, err := client.Run("hello_with_option_not_fetch_user", nil) 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | 145 | respMap, ok := resp.(map[string]interface{}) 146 | if !ok { 147 | t.Fatal("unexpected response format") 148 | } 149 | 150 | if len(respMap) != 0 { 151 | t.Fatal("unexpected response format") 152 | } 153 | }) 154 | 155 | t.Run("local", func(t *testing.T) { 156 | resp, err := Engine.Run("hello_with_option_not_fetch_user", nil) 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | respMap, ok := resp.(map[string]interface{}) 162 | if !ok { 163 | t.Fatal("unexpected response format") 164 | } 165 | 166 | if respMap["currentUser"] != nil { 167 | t.Fatal("unexpected response format") 168 | } 169 | }) 170 | }) 171 | 172 | t.Run("not_found", func(t *testing.T) { 173 | t.Run("remote", func(t *testing.T) { 174 | _, err := client.Run("not_found", nil) 175 | 176 | if err != nil { 177 | if !strings.Contains(err.Error(), "No such cloud function") { 178 | t.Fatal(err) 179 | } 180 | } 181 | }) 182 | 183 | t.Run("local", func(t *testing.T) { 184 | _, err := Engine.Run("not_found", nil) 185 | 186 | if err != nil { 187 | if !strings.Contains(err.Error(), "no such cloud function") { 188 | t.Fatal(err) 189 | } 190 | } 191 | }) 192 | }) 193 | } 194 | 195 | func TestRPC(t *testing.T) { 196 | t.Run("local", func(t *testing.T) { 197 | user := new(User) 198 | if err := client.Users.ID(testUserID).Get(user, UseMasterKey(true)); err != nil { 199 | t.Fatal(err) 200 | } 201 | 202 | retUser := new(User) 203 | err := Engine.RPC("hello_with_object", nil, retUser, WithUser(user)) 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | if retUser.SessionToken != user.SessionToken { 209 | t.Fatal(fmt.Errorf("dismatch sessionToken")) 210 | } 211 | }) 212 | 213 | t.Run("remote", func(t *testing.T) { 214 | user := new(User) 215 | if err := client.Users.ID(testUserID).Get(user, UseMasterKey(true)); err != nil { 216 | t.Fatal(err) 217 | } 218 | 219 | retUser := new(User) 220 | err := client.RPC("hello_with_object", nil, retUser, WithUser(user)) 221 | if err != nil { 222 | t.Fatal(err) 223 | } 224 | 225 | if retUser.ID != user.ID { 226 | t.Fatal(fmt.Errorf("dismatch sessionToken")) 227 | } 228 | }) 229 | } 230 | -------------------------------------------------------------------------------- /leancloud/objectref_test.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | type Staff struct { 9 | Object 10 | Name string `json:"name"` 11 | Age int `json:"age"` 12 | } 13 | 14 | type Meeting struct { 15 | Object 16 | Title string `json:"title"` 17 | Number int `json:"number"` 18 | Progress float64 `json:"progress"` 19 | Date time.Time `json:"date"` 20 | Attachment []byte `json:"attachment"` 21 | Host Staff `json:"host"` 22 | Participants []Staff `json:"participants"` 23 | Location *GeoPoint `json:"location"` 24 | } 25 | 26 | func TestObjectCreate(t *testing.T) { 27 | t.Run("Struct", func(t *testing.T) { 28 | meeting := Meeting{ 29 | Title: "Team Meeting", 30 | Number: 1, 31 | Progress: 12.5, 32 | Date: time.Now(), 33 | Attachment: []byte("There is nothing attachable."), 34 | Location: &GeoPoint{1, 2}, 35 | } 36 | 37 | if ref, err := client.Class("Meeting").Create(&meeting); err != nil { 38 | t.Fatal(err) 39 | } else { 40 | if ref.class == "" || ref.ID == "" { 41 | t.FailNow() 42 | } 43 | if meeting.CreatedAt.IsZero() || meeting.UpdatedAt.IsZero() { 44 | t.FailNow() 45 | } 46 | if !meeting.CreatedAt.Equal(meeting.UpdatedAt) { 47 | t.FailNow() 48 | } 49 | } 50 | }) 51 | 52 | t.Run("Map", func(t *testing.T) { 53 | meeting := map[string]interface{}{ 54 | "title": "Team Meeting", 55 | "number": 1, 56 | "progress": 12.5, 57 | "date": time.Now(), 58 | "attachment": []byte("There is nothing attachable."), 59 | "location": &GeoPoint{1, 2}, 60 | } 61 | 62 | if ref, err := client.Class("Meeting").Create(meeting); err != nil { 63 | t.Fatal(err) 64 | } else { 65 | if ref.class == "" || ref.ID == "" { 66 | t.FailNow() 67 | } 68 | } 69 | }) 70 | } 71 | 72 | func TestObjectGet(t *testing.T) { 73 | t.Run("Custom", func(t *testing.T) { 74 | jake := Staff{ 75 | Name: "Jake", 76 | Age: 20, 77 | } 78 | 79 | _, err := client.Class("Staff").Create(&jake) 80 | if err != nil { 81 | t.Fatal() 82 | } 83 | 84 | meeting := Meeting{ 85 | Title: "Team Meeting", 86 | Number: 1, 87 | Progress: 12.5, 88 | Host: jake, 89 | Participants: []Staff{jake, jake, jake}, 90 | Date: time.Now(), 91 | Attachment: []byte("There is nothing attachable."), 92 | Location: &GeoPoint{1, 2}, 93 | } 94 | 95 | _, err = client.Class("Meeting").Create(&meeting) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | newMeeting := new(Meeting) 101 | if err := client.Class("Meeting").ID(meeting.ID).Get(newMeeting); err != nil { 102 | t.Fatal(err) 103 | } 104 | }) 105 | 106 | t.Run("Bare", func(t *testing.T) { 107 | meeting := map[string]interface{}{ 108 | "title": "Team Meeting", 109 | "number": 1, 110 | "progress": 12.5, 111 | "date": time.Now(), 112 | "attachment": []byte("There is nothing attachable."), 113 | "location": &GeoPoint{1, 2}, 114 | } 115 | 116 | ref, err := client.Class("Meeting").Create(meeting) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | object := new(Object) 122 | if err := client.Class("Meeting").ID(ref.ID).Get(object); err != nil { 123 | t.Fatal(err) 124 | } 125 | }) 126 | } 127 | 128 | func TestObjectSet(t *testing.T) { 129 | meeting := Meeting{ 130 | Title: "Team Meeting", 131 | Number: 1, 132 | Progress: 12.5, 133 | Date: time.Now(), 134 | Attachment: []byte("There is nothing attachable."), 135 | Location: &GeoPoint{1, 2}, 136 | } 137 | 138 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | if err := client.Object(meeting).Set("title", "Another Team Meeting"); err != nil { 143 | t.Fatal(err) 144 | } 145 | 146 | newMeeting := new(Meeting) 147 | if err := client.Object(meeting).Get(newMeeting); err != nil { 148 | t.Fatal(err) 149 | } 150 | } 151 | 152 | func TestObjectUpdate(t *testing.T) { 153 | t.Run("Struct", func(t *testing.T) { 154 | meeting := Meeting{ 155 | Title: "Team Meeting", 156 | Number: 1, 157 | Progress: 12.5, 158 | Date: time.Now(), 159 | Attachment: []byte("There is nothing attachable."), 160 | Location: &GeoPoint{1, 2}, 161 | } 162 | 163 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 164 | t.Fatal(err) 165 | } 166 | 167 | diff := &Meeting{ 168 | Title: "Another Team Meeting", 169 | Number: 2, 170 | Progress: 13.5, 171 | Date: time.Now(), 172 | } 173 | 174 | if err := client.Object(meeting).Update(diff); err != nil { 175 | t.Fatal(err) 176 | } 177 | 178 | newMeeting := new(Meeting) 179 | if err := client.Object(meeting).Get(newMeeting); err != nil { 180 | t.Fatal(err) 181 | } 182 | }) 183 | 184 | t.Run("Map", func(t *testing.T) { 185 | meeting := map[string]interface{}{ 186 | "title": "Team Meeting", 187 | "number": 1, 188 | "progress": 12.5, 189 | "date": time.Now(), 190 | "attachment": []byte("There is nothing attachable."), 191 | "location": &GeoPoint{1, 2}, 192 | } 193 | 194 | ref, err := client.Class("Meeting").Create(meeting) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | 199 | if err := client.Class("Meeting").ID(ref.ID).Update(map[string]interface{}{ 200 | "title": "Another Team Meeting", 201 | "number": 2, 202 | "progress": 13.5, 203 | "date": time.Now(), 204 | }); err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | newMeeting := new(Meeting) 209 | if err := client.Class("Meeting").ID(ref.ID).Get(newMeeting); err != nil { 210 | t.Fatal(err) 211 | } 212 | }) 213 | } 214 | 215 | func TestObjectUpdateWithQuery(t *testing.T) { 216 | meeting := Meeting{ 217 | Title: "Team Meeting", 218 | Number: 1, 219 | Progress: 13.5, 220 | Date: time.Now(), 221 | Attachment: []byte("There is nothing attachable."), 222 | Location: &GeoPoint{1, 2}, 223 | } 224 | 225 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 226 | t.Fatal(err) 227 | } 228 | 229 | diff := &Meeting{ 230 | Title: "Another Team Meeting", 231 | Number: 2, 232 | Progress: 14.5, 233 | Date: time.Now(), 234 | } 235 | 236 | if err := client.Object(meeting).UpdateWithQuery(diff, client.Class("Meeting").NewQuery().EqualTo("progress", 13.5)); err != nil { 237 | t.Fatal(err) 238 | } 239 | 240 | newMeeting := new(Meeting) 241 | if err := client.Object(meeting).Get(newMeeting); err != nil { 242 | t.Fatal(err) 243 | } 244 | } 245 | 246 | func TestObjectDestroy(t *testing.T) { 247 | meeting := Meeting{ 248 | Title: "Team Meeting", 249 | Number: 1, 250 | Progress: 12.5, 251 | Date: time.Now(), 252 | Attachment: []byte("There is nothing attachable."), 253 | Location: &GeoPoint{1, 2}, 254 | } 255 | 256 | if _, err := client.Class("Meeting").Create(&meeting); err != nil { 257 | t.Fatal(err) 258 | } 259 | 260 | if err := client.Object(meeting).Destroy(UseMasterKey(true)); err != nil { 261 | t.Fatal(err) 262 | } 263 | 264 | newMeeting := new(Meeting) 265 | if err := client.Object(meeting).Get(newMeeting); err == nil { 266 | if newMeeting.ID != "" { 267 | t.FailNow() 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /leancloud/users.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | ) 9 | 10 | type Users struct { 11 | c *Client 12 | } 13 | 14 | func (ref *Users) NewQuery() *Query { 15 | return &Query{ 16 | class: &Class{ 17 | Name: "_User", 18 | c: ref.c, 19 | }, 20 | c: ref.c, 21 | where: make(map[string]interface{}), 22 | } 23 | } 24 | 25 | func (ref *Users) LogIn(username, password string) (*User, error) { 26 | path := fmt.Sprint("/1.1/login") 27 | options := ref.c.getRequestOptions() 28 | options.JSON = map[string]string{ 29 | "username": username, 30 | "password": password, 31 | } 32 | 33 | resp, err := ref.c.request(methodPost, path, options) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | respJSON := make(map[string]interface{}) 39 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 40 | return nil, err 41 | } 42 | 43 | return decodeUser(respJSON) 44 | } 45 | 46 | func (ref *Users) LogInByMobilePhoneNumber(number, smsCode string) (*User, error) { 47 | path := "/1.1/login" 48 | options := ref.c.getRequestOptions() 49 | options.JSON = map[string]string{ 50 | "mobilePhoneNumber": number, 51 | "smsCode": smsCode, 52 | } 53 | 54 | resp, err := ref.c.request(methodPost, path, options) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | respJSON := make(map[string]interface{}) 60 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 61 | return nil, err 62 | } 63 | 64 | return decodeUser(respJSON) 65 | } 66 | 67 | func (ref *Users) LogInByEmail(email, password string) (*User, error) { 68 | path := "/1.1/login" 69 | options := ref.c.getRequestOptions() 70 | options.JSON = map[string]string{ 71 | "email": email, 72 | "password": password, 73 | } 74 | 75 | resp, err := ref.c.request(methodPost, path, options) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | respJSON := make(map[string]interface{}) 81 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 82 | return nil, err 83 | } 84 | 85 | return decodeUser(respJSON) 86 | } 87 | 88 | func (ref *Users) SignUp(username, password string) (*User, error) { 89 | body := map[string]string{ 90 | "username": username, 91 | "password": password, 92 | } 93 | decodedUser, err := objectCreate(ref, body) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | user, ok := decodedUser.(*User) 99 | if !ok { 100 | return nil, fmt.Errorf("unexpected error when parse User from response: want type *User but %v", reflect.TypeOf(decodedUser)) 101 | } 102 | return user, nil 103 | } 104 | 105 | func (ref *Users) SignUpByMobilePhone(number, smsCode string) (*User, error) { 106 | path := "/1.1/usersByMobilePhone" 107 | options := ref.c.getRequestOptions() 108 | options.JSON = map[string]string{ 109 | "mobilePhoneNumber": number, 110 | "smsCode": smsCode, 111 | } 112 | 113 | resp, err := ref.c.request(methodPost, path, options) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | respJSON := make(map[string]interface{}) 119 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 120 | return nil, err 121 | } 122 | 123 | decodedUser, err := decodeUser(respJSON) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | return decodedUser, nil 129 | } 130 | 131 | func (ref *Users) SignUpByEmail(email, password string) (*User, error) { 132 | body := map[string]string{ 133 | "email": email, 134 | "password": password, 135 | } 136 | decodedUser, err := objectCreate(ref, body) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | user, ok := decodedUser.(*User) 142 | if !ok { 143 | return nil, fmt.Errorf("unexpected error when parse User from response: want type *User but %v", reflect.TypeOf(decodedUser)) 144 | } 145 | 146 | return user, nil 147 | } 148 | 149 | func (ref *Users) ResetPasswordBySMSCode(number, smsCode, password string, authOptions ...AuthOption) error { 150 | path := "/1.1/resetPasswordBySmsCode/" 151 | options := ref.c.getRequestOptions() 152 | options.JSON = map[string]string{ 153 | "password": password, 154 | "mobilePhoneNumber": number, 155 | } 156 | 157 | _, err := ref.c.request(methodPost, fmt.Sprint(path, smsCode), options, authOptions...) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | return nil 163 | } 164 | 165 | func (ref *Users) Become(sessionToken string) (*User, error) { 166 | resp, err := ref.c.request(methodGet, "/1.1/users/me", ref.c.getRequestOptions(), UseSessionToken(sessionToken)) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | respJSON := make(map[string]interface{}) 172 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 173 | return nil, err 174 | } 175 | 176 | return decodeUser(respJSON) 177 | } 178 | 179 | func (ref *Users) RequestEmailVerify(email string, authOptions ...AuthOption) error { 180 | path := "/1.1/requestEmailVerify" 181 | options := ref.c.getRequestOptions() 182 | options.JSON = map[string]string{ 183 | "email": email, 184 | } 185 | 186 | _, err := ref.c.request(methodPost, path, options, authOptions...) 187 | if err != nil { 188 | return err 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func (ref *Users) RequestMobilePhoneVerify(number string, authOptions ...AuthOption) error { 195 | path := "/1.1/requestMobilePhoneVerify" 196 | options := ref.c.getRequestOptions() 197 | options.JSON = map[string]string{ 198 | "mobilePhoneNumber": number, 199 | } 200 | 201 | resp, err := ref.c.request(methodPost, path, options, authOptions...) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | if resp.StatusCode != http.StatusOK { 207 | return fmt.Errorf("%s", string(resp.Bytes())) 208 | } 209 | 210 | return nil 211 | } 212 | 213 | func (ref *Users) RequestPasswordReset(email string, authOptions ...AuthOption) error { 214 | path := "/1.1/requestPasswordReset" 215 | options := ref.c.getRequestOptions() 216 | options.JSON = map[string]string{ 217 | "email": email, 218 | } 219 | 220 | resp, err := ref.c.request(methodPost, path, options, authOptions...) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | if resp.StatusCode != http.StatusOK { 226 | return fmt.Errorf("%s", string(resp.Bytes())) 227 | } 228 | 229 | return nil 230 | } 231 | 232 | func (ref *Users) RequestPasswordResetBySMSCode(number string, authOptions ...AuthOption) error { 233 | path := "/1.1/requestPasswordResetBySmsCode" 234 | options := ref.c.getRequestOptions() 235 | options.JSON = map[string]string{ 236 | "mobilePhoneNumber": number, 237 | } 238 | 239 | resp, err := ref.c.request(methodPost, path, options, authOptions...) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | if resp.StatusCode != http.StatusOK { 245 | return fmt.Errorf("%s", string(resp.Bytes())) 246 | } 247 | 248 | return nil 249 | } 250 | 251 | func (ref *Users) RequestLoginSMSCode(number string, authOptions ...AuthOption) error { 252 | path := "/1.1/requestLoginSmsCode" 253 | options := ref.c.getRequestOptions() 254 | options.JSON = map[string]string{ 255 | "mobilePhoneNumber": number, 256 | } 257 | 258 | resp, err := ref.c.request(methodPost, path, options, authOptions...) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | if resp.StatusCode != http.StatusOK { 264 | return fmt.Errorf("%s", string(resp.Bytes())) 265 | } 266 | 267 | return nil 268 | } 269 | -------------------------------------------------------------------------------- /leancloud/hook.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ClassHookRequest contains object and user passed by Class hook calling 8 | type ClassHookRequest struct { 9 | Object *Object 10 | User *User 11 | Meta map[string]string 12 | } 13 | 14 | // UpdatedKeys return keys which would be updated, only valid in beforeUpdate hook 15 | func (r *ClassHookRequest) UpdatedKeys() []string { 16 | return r.Object.fields["_updatedKeys"].([]string) 17 | } 18 | 19 | // RealtimeHookRequest contains parameters passed by RTM hook calling 20 | type RealtimeHookRequest struct { 21 | Params map[string]interface{} 22 | Meta map[string]string 23 | } 24 | 25 | var classHookmap = map[string]string{ 26 | "beforeSave": "__before_save_for_", 27 | "afterSave": "__after_save_for_", 28 | "beforeUpdate": "__before_update_for_", 29 | "afterUpdate": "__after_update_for_", 30 | "beforeDelete": "__before_delete_for_", 31 | "afterDelete": "__after_delete_for_", 32 | "onVerified": "__on_verified_", 33 | "onLogin": "__on_login_", 34 | } 35 | 36 | func (engine *engine) defineClassHook(class, hook string, fn func(*ClassHookRequest) (interface{}, error)) { 37 | name := fmt.Sprint(hook, class) 38 | if engine.functions[name] != nil { 39 | panic(fmt.Errorf("LeanEngine: %s of %s already defined", hook, class)) 40 | } 41 | 42 | engine.functions[name] = new(functionType) 43 | engine.functions[name].defineOption = map[string]interface{}{ 44 | "fetchUser": true, 45 | "internal": false, 46 | "hook": true, 47 | } 48 | engine.functions[name].call = func(r *FunctionRequest) (interface{}, error) { 49 | if r.Params != nil { 50 | req := new(ClassHookRequest) 51 | params, ok := r.Params.(map[string]interface{}) 52 | if !ok { 53 | return nil, fmt.Errorf("invalid request body") 54 | } 55 | object, err := decodeObject(params["object"]) 56 | if err != nil { 57 | return nil, err 58 | } 59 | req.Object = object 60 | if params["user"] != nil { 61 | user, err := decodeUser(params["user"]) 62 | if err != nil { 63 | return nil, err 64 | } 65 | req.User = user 66 | } 67 | return fn(req) 68 | } 69 | 70 | return nil, nil 71 | } 72 | } 73 | 74 | // BeforeSave will be called before saving an Object 75 | func (engine *engine) BeforeSave(class string, fn func(*ClassHookRequest) (interface{}, error)) { 76 | engine.defineClassHook(class, "__before_save_for_", fn) 77 | } 78 | 79 | // AfterSave will be called after Object saved 80 | func (engine *engine) AfterSave(class string, fn func(*ClassHookRequest) error) { 81 | engine.defineClassHook(class, "__after_save_for_", func(r *ClassHookRequest) (interface{}, error) { 82 | return nil, fn(r) 83 | }) 84 | } 85 | 86 | // BeforeUpdate will be called before updating an Object 87 | func (engine *engine) BeforeUpdate(class string, fn func(*ClassHookRequest) (interface{}, error)) { 88 | engine.defineClassHook(class, "__before_update_for_", fn) 89 | } 90 | 91 | // AfterUpdate will be called after Object updated 92 | func (engine *engine) AfterUpdate(class string, fn func(*ClassHookRequest) error) { 93 | engine.defineClassHook(class, "__after_update_for_", func(r *ClassHookRequest) (interface{}, error) { 94 | return nil, fn(r) 95 | }) 96 | } 97 | 98 | // BeforeDelete will be called before deleting an Object 99 | func (engine *engine) BeforeDelete(class string, fn func(*ClassHookRequest) (interface{}, error)) { 100 | engine.defineClassHook(class, "__before_delete_for_", fn) 101 | } 102 | 103 | // AfterDelete will be called after Object deleted 104 | func (engine *engine) AfterDelete(class string, fn func(*ClassHookRequest) error) { 105 | engine.defineClassHook(class, "__after_delete_for_", func(r *ClassHookRequest) (interface{}, error) { 106 | return nil, fn(r) 107 | }) 108 | } 109 | 110 | // OnVerified will be called when user was online 111 | func (engine *engine) OnVerified(verifyType string, fn func(*ClassHookRequest) error) { 112 | engine.Define(fmt.Sprint("__on_verified_", verifyType), func(r *FunctionRequest) (interface{}, error) { 113 | params, ok := r.Params.(map[string]interface{}) 114 | if !ok { 115 | return nil, fmt.Errorf("invalid request body") 116 | } 117 | user, err := decodeUser(params["object"]) 118 | if err != nil { 119 | return nil, err 120 | } 121 | req := ClassHookRequest{ 122 | User: user, 123 | Meta: r.Meta, 124 | } 125 | return nil, fn(&req) 126 | }) 127 | } 128 | 129 | // OnLogin will be called when user logged in 130 | func (engine *engine) OnLogin(fn func(*ClassHookRequest) error) { 131 | engine.Define("__on_login__User", func(r *FunctionRequest) (interface{}, error) { 132 | params, ok := r.Params.(map[string]interface{}) 133 | if !ok { 134 | return nil, fmt.Errorf("invalid request body") 135 | } 136 | user, err := decodeUser(params["object"]) 137 | if err != nil { 138 | return nil, err 139 | } 140 | req := ClassHookRequest{ 141 | User: user, 142 | Meta: r.Meta, 143 | } 144 | return nil, fn(&req) 145 | }) 146 | } 147 | 148 | func (engine *engine) defineRealtimeHook(name string, fn func(*RealtimeHookRequest) (interface{}, error)) { 149 | engine.Define(name, func(r *FunctionRequest) (interface{}, error) { 150 | params, ok := r.Params.(map[string]interface{}) 151 | if !ok { 152 | return nil, fmt.Errorf("invalid request body") 153 | } 154 | req := RealtimeHookRequest{ 155 | Params: params, 156 | Meta: r.Meta, 157 | } 158 | return fn(&req) 159 | }) 160 | engine.functions[name].defineOption["hook"] = true 161 | } 162 | 163 | func (engine *engine) OnIMMessageReceived(fn func(*RealtimeHookRequest) (interface{}, error)) { 164 | engine.defineRealtimeHook("_messageReceived", fn) 165 | } 166 | 167 | func (engine *engine) OnIMReceiversOffline(fn func(*RealtimeHookRequest) (interface{}, error)) { 168 | engine.defineRealtimeHook("_receiverOffline", fn) 169 | } 170 | 171 | func (engine *engine) OnIMMessageSent(fn func(*RealtimeHookRequest) error) { 172 | engine.defineRealtimeHook("_messageSent", func(r *RealtimeHookRequest) (interface{}, error) { 173 | return nil, fn(r) 174 | }) 175 | } 176 | 177 | func (engine *engine) OnIMMessageUpdate(fn func(*RealtimeHookRequest) (interface{}, error)) { 178 | engine.defineRealtimeHook("_messageUpdate", fn) 179 | } 180 | 181 | func (engine *engine) OnIMConversationStart(fn func(*RealtimeHookRequest) (interface{}, error)) { 182 | engine.defineRealtimeHook("_conversationStart", fn) 183 | } 184 | 185 | func (engine *engine) OnIMConversationStarted(fn func(*RealtimeHookRequest) error) { 186 | engine.defineRealtimeHook("_conversationStarted", func(r *RealtimeHookRequest) (interface{}, error) { 187 | return nil, fn(r) 188 | }) 189 | } 190 | 191 | func (engine *engine) OnIMConversationAdd(fn func(*RealtimeHookRequest) (interface{}, error)) { 192 | engine.defineRealtimeHook("_conversationStarted", fn) 193 | } 194 | 195 | func (engine *engine) OnIMConversationRemove(fn func(*RealtimeHookRequest) (interface{}, error)) { 196 | engine.defineRealtimeHook("_conversationRemove", fn) 197 | } 198 | 199 | func (engine *engine) OnIMConversationAdded(fn func(*RealtimeHookRequest) error) { 200 | engine.defineRealtimeHook("_conversationAdded", func(r *RealtimeHookRequest) (interface{}, error) { 201 | return nil, fn(r) 202 | }) 203 | } 204 | 205 | func (engine *engine) OnIMConversationRemoved(fn func(*RealtimeHookRequest) error) { 206 | engine.defineRealtimeHook("_conversationRemoved", func(r *RealtimeHookRequest) (interface{}, error) { 207 | return nil, fn(r) 208 | }) 209 | } 210 | 211 | func (engine *engine) OnIMConversationUpdate(fn func(*RealtimeHookRequest) (interface{}, error)) { 212 | engine.defineRealtimeHook("_conversationUpdate", fn) 213 | } 214 | 215 | func (engine *engine) OnIMClientOnline(fn func(*RealtimeHookRequest) error) { 216 | engine.defineRealtimeHook("_clientOnline", func(r *RealtimeHookRequest) (interface{}, error) { 217 | return nil, fn(r) 218 | }) 219 | } 220 | 221 | func (engine *engine) OnIMClientOffline(fn func(*RealtimeHookRequest) error) { 222 | engine.defineRealtimeHook("_clientOffline", func(r *RealtimeHookRequest) (interface{}, error) { 223 | return nil, fn(r) 224 | }) 225 | } 226 | -------------------------------------------------------------------------------- /leancloud/file.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "net/url" 12 | "reflect" 13 | "time" 14 | ) 15 | 16 | type File struct { 17 | Object 18 | Key string `json:"key"` 19 | Name string `json:"name"` 20 | Provider string `json:"provider"` 21 | Bucket string `json:"bucket"` 22 | MIME string `json:"mime_type"` 23 | URL string `json:"url"` 24 | Size int64 `json:"size"` 25 | MetaData map[string]interface{} `json:"metadata"` 26 | } 27 | 28 | func (file *File) fetchOwner(client *Client, authOptions ...AuthOption) (*User, error) { 29 | options := client.getRequestOptions() 30 | for _, authOption := range authOptions { 31 | authOption.apply(client, options) 32 | } 33 | 34 | if options.Headers["X-LC-Session"] == "" { 35 | return nil, nil 36 | } 37 | 38 | user, err := client.Users.Become(options.Headers["X-LC-Session"]) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return user, nil 44 | } 45 | 46 | func (file *File) fetchToken(client *Client, authOptions ...AuthOption) (string, string, error) { 47 | reqJSON := encodeFile(file, false) 48 | 49 | path := "/1.1/fileTokens" 50 | options := client.getRequestOptions() 51 | options.JSON = reqJSON 52 | 53 | resp, err := client.request(methodPost, path, options, authOptions...) 54 | if err != nil { 55 | return "", "", err 56 | } 57 | 58 | respJSON := make(map[string]interface{}) 59 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 60 | return "", "", err 61 | } 62 | 63 | objectID, ok := respJSON["objectId"].(string) 64 | if !ok { 65 | return "", "", fmt.Errorf("unexpected error when parse objectId from response: want type string but %v", reflect.TypeOf(respJSON["objectId"])) 66 | } 67 | file.ID = objectID 68 | 69 | createdAt, ok := respJSON["createdAt"].(string) 70 | if !ok { 71 | return "", "", fmt.Errorf("unexpected error when parse createdAt from response: want type string but %v", reflect.TypeOf(respJSON["createdAt"])) 72 | } 73 | decodedCreatedAt, err := time.Parse(time.RFC3339, createdAt) 74 | if err != nil { 75 | return "", "", fmt.Errorf("unexpected error when parse createdAt from response: %v", err) 76 | } 77 | file.CreatedAt = decodedCreatedAt 78 | 79 | key, ok := respJSON["key"].(string) 80 | if !ok { 81 | return "", "", fmt.Errorf("unexpected error when parse key from response: want type string but %v", reflect.TypeOf(respJSON["key"])) 82 | } 83 | file.Key = key 84 | 85 | url, ok := respJSON["url"].(string) 86 | if !ok { 87 | return "", "", fmt.Errorf("unexpected error when parse url from response: want type string but %v", reflect.TypeOf(respJSON["url"])) 88 | } 89 | file.URL = url 90 | 91 | token, ok := respJSON["token"].(string) 92 | if !ok { 93 | return "", "", fmt.Errorf("unexpected error when parse token from response: want type string but %v", reflect.TypeOf(respJSON["token"])) 94 | } 95 | 96 | bucket, ok := respJSON["bucket"].(string) 97 | if !ok { 98 | return "", "", fmt.Errorf("unexpected error when parse bucket from response: want type string but %v", reflect.TypeOf(respJSON["bucket"])) 99 | } 100 | file.Bucket = bucket 101 | 102 | uploadURL, ok := respJSON["upload_url"].(string) 103 | if !ok { 104 | return "", "", fmt.Errorf("unexpected error when parse upload_url from response: want type string but %v", reflect.TypeOf(respJSON["upload_url"])) 105 | } 106 | 107 | provider, ok := respJSON["provider"].(string) 108 | if !ok { 109 | return "", "", fmt.Errorf("unexpected error when parse provider from response: want type string but %v", reflect.TypeOf(respJSON["provider"])) 110 | } 111 | file.Provider = provider 112 | 113 | return token, uploadURL, nil 114 | } 115 | 116 | func (file *File) fileCallback(result bool, token string, client *Client, authOptions ...AuthOption) error { 117 | path := "/1.1/fileCallback" 118 | options := client.getRequestOptions() 119 | options.JSON = map[string]interface{}{ 120 | "result": result, 121 | "token": token, 122 | } 123 | 124 | _, err := client.request(methodPost, path, options, authOptions...) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func (file *File) uploadQiniu(token, uploadURL string, reader io.ReadSeeker) error { 133 | out, in := io.Pipe() 134 | part := multipart.NewWriter(in) 135 | done := make(chan error) 136 | 137 | go func() { 138 | if err := part.WriteField("key", file.Key); err != nil { 139 | in.Close() 140 | done <- err 141 | return 142 | } 143 | if err := part.WriteField("token", token); err != nil { 144 | in.Close() 145 | done <- err 146 | return 147 | } 148 | writer, err := part.CreateFormFile("file", file.Name) 149 | if err != nil { 150 | in.Close() 151 | done <- err 152 | return 153 | } 154 | _, err = io.Copy(writer, reader) 155 | if err != nil { 156 | in.Close() 157 | done <- err 158 | return 159 | } 160 | if err := part.Close(); err != nil { 161 | in.Close() 162 | done <- err 163 | return 164 | } 165 | in.Close() 166 | done <- nil 167 | }() 168 | 169 | req, err := http.NewRequest("POST", "https://up.qbox.me/", out) 170 | req.Header.Set("Content-Type", part.FormDataContentType()) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | resp, err := http.DefaultClient.Do(req) 176 | if err != nil { 177 | return fmt.Errorf("unexpected error when upload file to Qiniu: %v", err) 178 | } 179 | defer resp.Body.Close() 180 | 181 | err = <-done 182 | if err != nil { 183 | return fmt.Errorf("unexpected error when upload file to Qiniu: %v", err) 184 | } 185 | 186 | content, err := ioutil.ReadAll(resp.Body) 187 | if err != nil { 188 | return fmt.Errorf("unexpected error when upload file to Qiniu: %v", err) 189 | } 190 | if resp.StatusCode != 200 { 191 | return fmt.Errorf("unexpected error when upload file to Qiniu: %v", string(content)) 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func (file *File) uploadS3(token, uploadURL string, reader io.ReadSeeker) error { 198 | req, err := http.NewRequest("PUT", uploadURL, reader) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | req.Header.Set("Content-Type", file.MIME) 204 | req.Header.Set("Cache-Control", "public, max-age=31536000") 205 | req.ContentLength = file.Size 206 | 207 | response, err := http.DefaultClient.Do(req) 208 | if err != nil { 209 | return fmt.Errorf("unexpected error when upload file to AWS S3: %v", err) 210 | } 211 | defer response.Body.Close() 212 | 213 | body, err := ioutil.ReadAll(response.Body) 214 | if err != nil { 215 | return fmt.Errorf("unexpected error when upload file to AWS S3: %v", err) 216 | } 217 | if response.StatusCode != 200 { 218 | return fmt.Errorf("unexpected error when upload file to AWS S3: %v", string(body)) 219 | } 220 | 221 | return nil 222 | } 223 | 224 | func (file *File) uploadCOS(token, uploadURL string, reader io.ReadSeeker) error { 225 | out, in := io.Pipe() 226 | part := multipart.NewWriter(in) 227 | done := make(chan error) 228 | 229 | go func() { 230 | if err := part.WriteField("op", "upload"); err != nil { 231 | in.Close() 232 | done <- err 233 | return 234 | } 235 | writer, err := part.CreateFormFile("fileContent", file.Name) 236 | if err != nil { 237 | in.Close() 238 | done <- err 239 | return 240 | } 241 | _, err = io.Copy(writer, reader) 242 | if err != nil { 243 | in.Close() 244 | done <- err 245 | return 246 | } 247 | if err := part.Close(); err != nil { 248 | in.Close() 249 | done <- err 250 | return 251 | } 252 | in.Close() 253 | done <- nil 254 | }() 255 | 256 | body, err := ioutil.ReadAll(out) 257 | if err != nil { 258 | return fmt.Errorf("unexpected error when upload file to COS: %v", err) 259 | } 260 | 261 | req, err := http.NewRequest("POST", uploadURL+"?sign="+url.QueryEscape(token), bytes.NewBuffer(body)) 262 | if err != nil { 263 | return fmt.Errorf("unexpected error when upload file to COS: %v", err) 264 | } 265 | 266 | req.Header.Set("Content-Type", part.FormDataContentType()) 267 | req.ContentLength = int64(len(body)) 268 | 269 | resp, err := http.DefaultClient.Do(req) 270 | if err != nil { 271 | return fmt.Errorf("unexpected error when upload file to COS: %v", err) 272 | } 273 | defer resp.Body.Close() 274 | 275 | err = <-done 276 | if err != nil { 277 | return fmt.Errorf("unexpected error when upload file to COS: %v", err) 278 | } 279 | 280 | content, err := ioutil.ReadAll(resp.Body) 281 | if err != nil { 282 | return fmt.Errorf("unexpected error when upload file to COS: %v", err) 283 | } 284 | if resp.StatusCode != 200 { 285 | return fmt.Errorf("unexpected error when upload file to COS: %v", string(content)) 286 | } 287 | 288 | return nil 289 | } 290 | 291 | func getSeekerSize(seeker io.Seeker) (int64, error) { 292 | size, err := seeker.Seek(0, io.SeekEnd) 293 | if err != nil { 294 | return 0, err 295 | } 296 | 297 | _, err = seeker.Seek(0, io.SeekStart) 298 | if err != nil { 299 | return 0, err 300 | } 301 | 302 | return size, nil 303 | } 304 | -------------------------------------------------------------------------------- /leancloud/engine.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "reflect" 10 | 11 | "github.com/levigross/grequests" 12 | ) 13 | 14 | type CloudEngine interface { 15 | Init(client *Client) 16 | Handler() http.Handler 17 | 18 | Run(name string, params interface{}, runOptions ...RunOption) (interface{}, error) 19 | RPC(name string, params interface{}, results interface{}, runOptions ...RunOption) error 20 | 21 | Define(name string, fn func(*FunctionRequest) (interface{}, error), defineOptions ...DefineOption) 22 | 23 | BeforeSave(class string, fn func(*ClassHookRequest) (interface{}, error)) 24 | AfterSave(class string, fn func(*ClassHookRequest) error) 25 | BeforeUpdate(class string, fn func(*ClassHookRequest) (interface{}, error)) 26 | AfterUpdate(class string, fn func(*ClassHookRequest) error) 27 | BeforeDelete(class string, fn func(*ClassHookRequest) (interface{}, error)) 28 | AfterDelete(class string, fn func(*ClassHookRequest) error) 29 | OnVerified(verifyType string, fn func(*ClassHookRequest) error) 30 | OnLogin(fn func(*ClassHookRequest) error) 31 | 32 | OnIMMessageReceived(fn func(*RealtimeHookRequest) (interface{}, error)) 33 | OnIMReceiversOffline(fn func(*RealtimeHookRequest) (interface{}, error)) 34 | OnIMMessageSent(fn func(*RealtimeHookRequest) error) 35 | OnIMMessageUpdate(fn func(*RealtimeHookRequest) (interface{}, error)) 36 | OnIMConversationStart(fn func(*RealtimeHookRequest) (interface{}, error)) 37 | OnIMConversationStarted(fn func(*RealtimeHookRequest) error) 38 | OnIMConversationAdd(fn func(*RealtimeHookRequest) (interface{}, error)) 39 | OnIMConversationRemove(fn func(*RealtimeHookRequest) (interface{}, error)) 40 | OnIMConversationAdded(fn func(*RealtimeHookRequest) error) 41 | OnIMConversationRemoved(fn func(*RealtimeHookRequest) error) 42 | OnIMConversationUpdate(fn func(*RealtimeHookRequest) (interface{}, error)) 43 | OnIMClientOnline(fn func(*RealtimeHookRequest) error) 44 | OnIMClientOffline(fn func(*RealtimeHookRequest) error) 45 | 46 | client() *Client 47 | } 48 | 49 | type engine struct { 50 | c *Client 51 | functions map[string]*functionType 52 | } 53 | 54 | var Engine CloudEngine 55 | 56 | // FunctionRequest contains request information of Cloud Function 57 | type FunctionRequest struct { 58 | Params interface{} 59 | CurrentUser *User 60 | SessionToken string 61 | Meta map[string]string 62 | } 63 | 64 | // DefineOption apply options for definition of Cloud Function 65 | type DefineOption interface { 66 | apply(*functionType) 67 | } 68 | 69 | type defineOption struct { 70 | fetchUser bool 71 | internal bool 72 | } 73 | 74 | func (option *defineOption) apply(fn *functionType) { 75 | if option.fetchUser == false { 76 | fn.defineOption["fetchUser"] = false 77 | } 78 | 79 | if option.internal == true { 80 | fn.defineOption["internal"] = true 81 | } 82 | } 83 | 84 | // WithoutFetchUser don't fetch current user originated the request 85 | func WithoutFetchUser() DefineOption { 86 | return &defineOption{ 87 | fetchUser: false, 88 | } 89 | } 90 | 91 | // WithInternal restricts that the Cloud Function can only be executed in LeanEngine 92 | func WithInternal() DefineOption { 93 | return &defineOption{ 94 | internal: true, 95 | } 96 | } 97 | 98 | // RunOption apply options for execution of Cloud Function 99 | type RunOption interface { 100 | apply(*map[string]interface{}) 101 | } 102 | 103 | type runOption struct { 104 | remote bool 105 | rpc bool 106 | engine *engine 107 | user *User 108 | sessionToken string 109 | } 110 | 111 | func (option *runOption) apply(runOption *map[string]interface{}) { 112 | if option.remote { 113 | (*runOption)["remote"] = true 114 | } 115 | 116 | if option.user != nil { 117 | (*runOption)["user"] = option.user 118 | } 119 | 120 | if option.sessionToken != "" { 121 | (*runOption)["sessionToken"] = option.sessionToken 122 | } 123 | 124 | if option.engine != nil { 125 | (*runOption)["engine"] = option.engine 126 | } 127 | 128 | if option.rpc { 129 | (*runOption)["rpc"] = option.rpc 130 | } 131 | } 132 | 133 | // WithUser specifics the user of the calling 134 | func WithUser(user *User) RunOption { 135 | return &runOption{ 136 | user: user, 137 | } 138 | } 139 | 140 | // WithSessionToken specifics the sessionToken of the calling 141 | func WithSessionToken(token string) RunOption { 142 | return &runOption{ 143 | sessionToken: token, 144 | } 145 | } 146 | 147 | type functionType struct { 148 | call func(*FunctionRequest) (interface{}, error) 149 | defineOption map[string]interface{} 150 | } 151 | 152 | func init() { 153 | Engine = &engine{ 154 | functions: make(map[string]*functionType), 155 | } 156 | } 157 | 158 | // Init the LeanEngine part of Go SDK 159 | func (engine *engine) Init(client *Client) { 160 | engine.c = client 161 | } 162 | 163 | func (engine *engine) client() *Client { 164 | if engine.c == nil { 165 | err := errors.New("not initialized (call leancloud.Engine.Init before use LeanEngine features)") 166 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 167 | panic(err) 168 | } 169 | 170 | return engine.c 171 | } 172 | 173 | // Define declares a Cloud Function with name & options of definition 174 | func (engine *engine) Define(name string, fn func(*FunctionRequest) (interface{}, error), defineOptions ...DefineOption) { 175 | if engine.functions[name] != nil { 176 | panic(fmt.Errorf("%s alreay defined", name)) 177 | } 178 | 179 | engine.functions[name] = new(functionType) 180 | engine.functions[name].defineOption = map[string]interface{}{ 181 | "fetchUser": true, 182 | "internal": false, 183 | } 184 | 185 | for _, v := range defineOptions { 186 | v.apply(engine.functions[name]) 187 | } 188 | 189 | engine.functions[name].call = fn 190 | } 191 | 192 | // Call cloud function locally 193 | func (engine *engine) Run(name string, params interface{}, runOptions ...RunOption) (interface{}, error) { 194 | return callCloudFunction(engine.client(), name, params, (append(runOptions, &runOption{ 195 | engine: engine, 196 | }))...) 197 | } 198 | 199 | // Call cloud function locally, bind response into `results` 200 | func (engine *engine) RPC(name string, params interface{}, results interface{}, runOptions ...RunOption) error { 201 | response, err := callCloudFunction(engine.client(), name, params, (append(runOptions, &runOption{ 202 | rpc: true, 203 | engine: engine, 204 | }))...) 205 | 206 | if err != nil { 207 | return err 208 | } 209 | 210 | return bind(reflect.Indirect(reflect.ValueOf(response)), reflect.Indirect(reflect.ValueOf(results))) 211 | } 212 | 213 | // Call cloud funcion remotely 214 | func (client *Client) Run(name string, params interface{}, runOptions ...RunOption) (interface{}, error) { 215 | return callCloudFunction(client, name, params, (append(runOptions, &runOption{ 216 | remote: true, 217 | }))...) 218 | } 219 | 220 | // Call cloud function remotely, bind response into `results` 221 | func (client *Client) RPC(name string, params interface{}, results interface{}, runOptions ...RunOption) error { 222 | response, err := callCloudFunction(client, name, params, (append(runOptions, &runOption{ 223 | rpc: true, 224 | remote: true, 225 | }))...) 226 | 227 | if err != nil { 228 | return err 229 | } 230 | 231 | return bind(reflect.Indirect(reflect.ValueOf(response)), reflect.Indirect(reflect.ValueOf(results))) 232 | } 233 | 234 | func callCloudFunction(client *Client, name string, params interface{}, runOptions ...RunOption) (interface{}, error) { 235 | options := make(map[string]interface{}) 236 | sessionToken := "" 237 | var currentUser *User 238 | 239 | for _, v := range runOptions { 240 | v.apply(&options) 241 | } 242 | 243 | isRpc := options["rpc"] != nil && options["rpc"] != false 244 | 245 | if options["sessionToken"] != nil && options["user"] != nil { 246 | return nil, fmt.Errorf("unable to set both of sessionToken & User") 247 | } 248 | 249 | if options["sessionToken"] != nil { 250 | sessionToken = options["sessionToken"].(string) 251 | } 252 | 253 | if options["user"] != nil { 254 | currentUser = options["user"].(*User) 255 | } 256 | 257 | if options["remote"] == true { 258 | var err error 259 | var resp *grequests.Response 260 | var path string 261 | 262 | reqOptions := client.getRequestOptions() 263 | 264 | if isRpc { 265 | path = fmt.Sprint("/1.1/call/", name) 266 | reqOptions.JSON = encode(params, true) 267 | } else { 268 | path = fmt.Sprint("/1.1/functions/", name) 269 | reqOptions.JSON = params 270 | } 271 | 272 | if sessionToken != "" { 273 | resp, err = client.request(methodPost, path, reqOptions, UseSessionToken(sessionToken)) 274 | } else if currentUser != nil { 275 | resp, err = client.request(methodPost, path, reqOptions, UseUser(currentUser)) 276 | } else { 277 | resp, err = client.request(methodPost, path, reqOptions) 278 | } 279 | 280 | if err != nil { 281 | return nil, err 282 | } 283 | 284 | respJSON := &functionResponse{} 285 | 286 | if err := json.Unmarshal(resp.Bytes(), respJSON); err != nil { 287 | return nil, err 288 | } 289 | 290 | if isRpc { 291 | return decode(respJSON.Result) 292 | } else { 293 | return respJSON.Result, err 294 | } 295 | } 296 | 297 | if options["engine"].(*engine).functions[name] == nil { 298 | return nil, fmt.Errorf("no such cloud function %s", name) 299 | } 300 | 301 | request := FunctionRequest{ 302 | Params: params, 303 | Meta: map[string]string{ 304 | "remoteAddr": "", 305 | }, 306 | } 307 | 308 | if sessionToken != "" { 309 | request.SessionToken = sessionToken 310 | user, err := client.Users.Become(sessionToken) 311 | if err != nil { 312 | return nil, err 313 | } 314 | request.CurrentUser = user 315 | } else if currentUser != nil { 316 | request.CurrentUser = currentUser 317 | request.SessionToken = currentUser.SessionToken 318 | } 319 | 320 | return options["engine"].(*engine).functions[name].call(&request) 321 | } 322 | -------------------------------------------------------------------------------- /leancloud/query.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | "time" 10 | 11 | "github.com/levigross/grequests" 12 | ) 13 | 14 | // Query contain parameters of queries 15 | type Query struct { 16 | c *Client 17 | class *Class 18 | where map[string]interface{} 19 | include []string 20 | keys []string 21 | order []string 22 | limit int 23 | skip int 24 | includeACL bool 25 | } 26 | 27 | // Find fetch results of the Query 28 | func (q *Query) Find(objects interface{}, authOptions ...AuthOption) error { 29 | _, err := objectQuery(q, objects, false, false, authOptions...) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // First fetch the first result of the Query 38 | func (q *Query) First(object interface{}, authOptions ...AuthOption) error { 39 | _, err := objectQuery(q, object, false, true, authOptions...) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // Count returns the count of results of the Query 48 | func (q *Query) Count(authOptions ...AuthOption) (int, error) { 49 | resp, err := objectQuery(q, nil, true, false, authOptions...) 50 | if err != nil { 51 | return 0, err 52 | } 53 | 54 | count, ok := resp.(float64) 55 | if !ok { 56 | return 0, fmt.Errorf("unable to parse count from response") 57 | } 58 | 59 | return int(count), nil 60 | } 61 | 62 | func (q *Query) Skip(count int) *Query { 63 | q.skip = count 64 | return q 65 | } 66 | 67 | func (q *Query) Limit(limit int) *Query { 68 | q.limit = limit 69 | return q 70 | } 71 | 72 | func (q *Query) Order(keys ...string) *Query { 73 | q.order = keys 74 | return q 75 | } 76 | 77 | func (q *Query) Or(queries ...*Query) *Query { 78 | qArray := []map[string]interface{}{} 79 | for _, v := range queries { 80 | qArray = append(qArray, v.where) 81 | } 82 | q.where["$or"] = qArray 83 | return q 84 | } 85 | 86 | func (q *Query) And(queries ...*Query) *Query { 87 | qArray := []map[string]interface{}{} 88 | for _, v := range queries { 89 | qArray = append(qArray, v.where) 90 | } 91 | q.where["$and"] = qArray 92 | return q 93 | } 94 | 95 | func (q *Query) Near(key string, point *GeoPoint) *Query { 96 | q.where[key] = wrapCondition("$nearSphere", point, "") 97 | return q 98 | } 99 | 100 | func (q *Query) WithinGeoBox(key string, southwest *GeoPoint, northeast *GeoPoint) *Query { 101 | q.where[key] = wrapCondition("$withinBox", []GeoPoint{*southwest, *northeast}, "") 102 | return q 103 | } 104 | 105 | func (q *Query) WithinKilometers(key string, point *GeoPoint) *Query { 106 | q.where[key] = wrapCondition("$maxDistanceInKilometers", point, "") 107 | return q 108 | } 109 | 110 | func (q *Query) WithinMiles(key string, point *GeoPoint) *Query { 111 | q.where[key] = wrapCondition("$maxDistanceInMiles", point, "") 112 | return q 113 | } 114 | 115 | func (q *Query) WithinRadians(key string, point *GeoPoint) *Query { 116 | q.where[key] = wrapCondition("$maxDistanceInRadians", point, "") 117 | return q 118 | } 119 | 120 | func (q *Query) Include(keys ...string) *Query { 121 | q.include = append(q.include, keys...) 122 | return q 123 | } 124 | 125 | func (q *Query) Select(keys ...string) *Query { 126 | q.keys = append(q.keys, keys...) 127 | return q 128 | } 129 | 130 | func (q *Query) MatchesQuery(key string, query *Query) *Query { 131 | q.where[key] = wrapCondition("$inQuery", query, "") 132 | return q 133 | } 134 | 135 | func (q *Query) NotMatchesQuery(key string, query *Query) *Query { 136 | q.where[key] = wrapCondition("$notInQuery", query, "") 137 | return q 138 | } 139 | 140 | func (q *Query) MatchesKeyQuery(key, queryKey string, query *Query) *Query { 141 | q.where[key] = map[string]interface{}{ 142 | "$select": map[string]interface{}{ 143 | "query": map[string]interface{}{ 144 | "className": query.class, 145 | "where": query.where, 146 | }, 147 | "key": queryKey, 148 | }, 149 | } 150 | return q 151 | } 152 | 153 | func (q *Query) EqualTo(key string, value interface{}) *Query { 154 | q.where[key] = wrapCondition("", value, "") 155 | return q 156 | } 157 | 158 | func (q *Query) NotEqualTo(key string, value interface{}) *Query { 159 | q.where[key] = wrapCondition("$ne", value, "") 160 | return q 161 | } 162 | func (q *Query) Exists(key string) *Query { 163 | q.where[key] = wrapCondition("$exists", "", "") 164 | return q 165 | } 166 | func (q *Query) NotExists(key string) *Query { 167 | q.where[key] = wrapCondition("$notexists", "", "") 168 | return q 169 | } 170 | 171 | func (q *Query) GreaterThan(key string, value interface{}) *Query { 172 | q.where[key] = wrapCondition("$gt", value, "") 173 | return q 174 | } 175 | 176 | func (q *Query) GreaterThanOrEqualTo(key string, value interface{}) *Query { 177 | q.where[key] = wrapCondition("$gte", value, "") 178 | return q 179 | } 180 | 181 | func (q *Query) LessThan(key string, value interface{}) *Query { 182 | q.where[key] = wrapCondition("$lt", value, "") 183 | return q 184 | } 185 | 186 | func (q *Query) LessThanOrEqualTo(key string, value interface{}) *Query { 187 | q.where[key] = wrapCondition("$lte", value, "") 188 | return q 189 | } 190 | 191 | func (q *Query) In(key string, data interface{}) *Query { 192 | q.where[key] = wrapCondition("$in", data, "") 193 | return q 194 | } 195 | 196 | func (q *Query) NotIn(key string, data interface{}) *Query { 197 | q.where[key] = wrapCondition("$nin", data, "") 198 | return q 199 | } 200 | 201 | func (q *Query) Regexp(key, expr, options string) *Query { 202 | q.where[key] = wrapCondition("$regex", expr, options) 203 | return q 204 | } 205 | 206 | func (q *Query) Contains(key, substring string) *Query { 207 | q.Regexp(key, substring, "") 208 | return q 209 | } 210 | 211 | func (q *Query) ContainsAll(key string, objects interface{}) *Query { 212 | q.where[key] = wrapCondition("$all", objects, "") 213 | return q 214 | } 215 | 216 | func (q *Query) StartsWith(key, prefix string) *Query { 217 | q.Regexp(key, fmt.Sprint("^", prefix), "") 218 | return q 219 | } 220 | 221 | func (q *Query) IncludeACL() *Query { 222 | q.includeACL = true 223 | return q 224 | } 225 | 226 | func wrapCondition(verb string, value interface{}, options string) interface{} { 227 | switch verb { 228 | case "$ne", "$lt", "$lte", "$gt", "$gte", "$in", "$nin", "$all", "nearShpere": 229 | return map[string]interface{}{ 230 | verb: encode(value, false), 231 | } 232 | case "$withinBox": 233 | return encode(map[string]interface{}{ 234 | "$box": value, 235 | }, true) 236 | case "$regex": 237 | return map[string]interface{}{ 238 | "$regex": value, 239 | "$options": options, 240 | } 241 | case "$exists": 242 | return map[string]interface{}{ 243 | "$exists": true, 244 | } 245 | case "$notexists": 246 | return map[string]interface{}{ 247 | "$exists": false, 248 | } 249 | case "$inQuery": 250 | queryMap, err := formatQuery(value, false, false) 251 | if err != nil { 252 | return nil 253 | } 254 | queryMap["className"] = value.(*Query).class.Name 255 | return map[string]interface{}{ 256 | "$inQuery": queryMap, 257 | } 258 | case "$notInQuery": 259 | queryMap, err := formatQuery(value, false, false) 260 | if err != nil { 261 | return nil 262 | } 263 | queryMap["className"] = value.(*Query).class.Name 264 | return map[string]interface{}{ 265 | "$notInQuery": queryMap, 266 | } 267 | default: 268 | switch v := value.(type) { 269 | case time.Time: 270 | return encodeDate(&v) 271 | default: 272 | return encode(value, false) 273 | } 274 | } 275 | } 276 | 277 | func objectQuery(query interface{}, objects interface{}, count bool, first bool, authOptions ...AuthOption) (interface{}, error) { 278 | path := fmt.Sprint("/1.1/") 279 | var client *Client 280 | var options *grequests.RequestOptions 281 | params, err := wrapParams(query, count, first) 282 | if err != nil { 283 | return nil, err 284 | } 285 | 286 | switch v := query.(type) { 287 | case *Query: 288 | if v.class.Name == "_User" { 289 | path = fmt.Sprint(path, "users") 290 | } else if v.class.Name == "_File" { 291 | path = fmt.Sprint(path, "classes/files") 292 | } else if v.class.Name == "_Role" { 293 | path = fmt.Sprint(path, "roles") 294 | } else { 295 | path = fmt.Sprint(path, "classes/", v.class.Name) 296 | } 297 | options = v.c.getRequestOptions() 298 | client = v.c 299 | } 300 | 301 | options.Params = params 302 | 303 | resp, err := client.request(methodGet, path, options, authOptions...) 304 | if err != nil { 305 | return nil, err 306 | } 307 | 308 | respJSON := make(map[string]interface{}) 309 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 310 | return nil, fmt.Errorf("unable to parse response %w", err) 311 | } 312 | if count { 313 | return respJSON["count"], nil 314 | } 315 | 316 | results := respJSON["results"].([]interface{}) 317 | switch query.(type) { 318 | case *Query: 319 | decodedObjects, err := decodeArray(results, true) 320 | 321 | if err != nil { 322 | return nil, err 323 | } 324 | 325 | rDecodedObjects := reflect.ValueOf(decodedObjects) 326 | 327 | if !first { 328 | if err := bind(rDecodedObjects, reflect.ValueOf(objects).Elem()); err != nil { 329 | return nil, err 330 | } 331 | } else if rDecodedObjects.Len() == 0 { 332 | return nil, errors.New("no matched object found") 333 | } else { 334 | if err := bind(rDecodedObjects.Index(0), reflect.ValueOf(objects).Elem()); err != nil { 335 | return nil, err 336 | } 337 | } 338 | } 339 | 340 | return nil, nil 341 | } 342 | 343 | func wrapParams(query interface{}, count, first bool) (map[string]string, error) { 344 | var where map[string]interface{} 345 | var order string 346 | var include string 347 | var keys string 348 | var skip, limit int 349 | 350 | switch v := query.(type) { 351 | case *Query: 352 | where = v.where 353 | order = strings.Join(v.order, ",") 354 | include = strings.Join(v.include, ",") 355 | keys = strings.Join(v.keys, ",") 356 | skip, limit = v.skip, v.limit 357 | } 358 | 359 | whereString, err := json.Marshal(where) 360 | if err != nil { 361 | return nil, fmt.Errorf("unable to wrap params %w", err) 362 | } 363 | 364 | params := map[string]string{ 365 | "where": string(whereString), 366 | } 367 | 368 | if skip != 0 { 369 | params["skip"] = fmt.Sprintf("%d", skip) 370 | } 371 | 372 | if limit != 0 { 373 | params["limit"] = fmt.Sprintf("%d", limit) 374 | } 375 | 376 | if len(order) != 0 { 377 | params["order"] = order 378 | } 379 | 380 | if len(include) != 0 { 381 | params["include"] = include 382 | } 383 | 384 | if len(keys) != 0 { 385 | params["keys"] = keys 386 | } 387 | 388 | if count { 389 | params["count"] = "1" 390 | } 391 | 392 | if first { 393 | params["limit"] = "1" 394 | } 395 | 396 | return params, nil 397 | } 398 | 399 | func formatQuery(query interface{}, count, first bool) (map[string]interface{}, error) { 400 | paramsInString, err := wrapParams(query, count, first) 401 | if err != nil { 402 | return nil, err 403 | } 404 | paramsInInterface := make(map[string]interface{}) 405 | for k, v := range paramsInString { 406 | paramsInInterface[k] = interface{}(v) 407 | } 408 | paramsInInterface["where"] = query.(*Query).where 409 | 410 | return paramsInInterface, nil 411 | } 412 | -------------------------------------------------------------------------------- /leancloud/objectref.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "time" 8 | 9 | "github.com/levigross/grequests" 10 | ) 11 | 12 | type ObjectRef struct { 13 | c *Client 14 | class string 15 | ID string 16 | } 17 | 18 | func (client *Client) Object(object interface{}) *ObjectRef { 19 | if meta := extractObjectMeta(object); meta != nil { 20 | return meta.ref.(*ObjectRef) 21 | } 22 | 23 | return nil 24 | } 25 | 26 | // Get fetchs object from backend 27 | func (ref *ObjectRef) Get(object interface{}, authOptions ...AuthOption) error { 28 | if ref == nil || ref.ID == "" || ref.class == "" { 29 | return nil 30 | } 31 | 32 | if err := objectGet(ref, object, authOptions...); err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // Set manipulate 40 | func (ref *ObjectRef) Set(key string, value interface{}, authOptions ...AuthOption) error { 41 | if ref == nil || ref.ID == "" || ref.class == "" { 42 | return nil 43 | } 44 | 45 | if err := objectSet(ref, key, value, authOptions...); err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (ref *ObjectRef) Update(diff interface{}, authOptions ...AuthOption) error { 53 | if ref == nil || ref.ID == "" || ref.class == "" { 54 | return nil 55 | } 56 | 57 | if err := objectUpdate(ref, diff, authOptions...); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (ref *ObjectRef) UpdateWithQuery(diff interface{}, query *Query, authOptions ...AuthOption) error { 65 | if ref == nil || ref.ID == "" || ref.class == "" { 66 | return nil 67 | } 68 | 69 | if err := objectUpdateWithQuery(ref, diff, query, authOptions...); err != nil { 70 | return err 71 | } 72 | return nil 73 | } 74 | 75 | func (ref *ObjectRef) Destroy(authOptions ...AuthOption) error { 76 | if ref == nil || ref.ID == "" || ref.class == "" { 77 | return nil 78 | } 79 | 80 | if err := objectDestroy(ref, authOptions...); err != nil { 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func objectCreate(class interface{}, object interface{}, authOptions ...AuthOption) (interface{}, error) { 88 | path := "/1.1/" 89 | var c *Client 90 | var options *grequests.RequestOptions 91 | 92 | switch v := class.(type) { 93 | case *Class: 94 | path = fmt.Sprint(path, "classes/", v.Name) 95 | c = v.c 96 | options = c.getRequestOptions() 97 | switch reflect.Indirect(reflect.ValueOf(object)).Kind() { 98 | case reflect.Map: 99 | options.JSON = encodeMap(object, false) 100 | case reflect.Struct: 101 | options.JSON = encodeObject(object, false, false) 102 | default: 103 | return nil, fmt.Errorf("object should be struct or map Class") 104 | } 105 | break 106 | case *Users: 107 | path = fmt.Sprint(path, "users") 108 | c = v.c 109 | options = c.getRequestOptions() 110 | switch reflect.Indirect(reflect.ValueOf(object)).Kind() { 111 | case reflect.Map: 112 | options.JSON = encodeMap(object, false) 113 | case reflect.Struct: 114 | options.JSON = encodeUser(object, false, false) 115 | default: 116 | return nil, fmt.Errorf("object should be struct or map") 117 | } 118 | break 119 | } 120 | 121 | resp, err := c.request(methodPost, path, options, authOptions...) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | respJSON := make(map[string]interface{}) 127 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 128 | return nil, err 129 | } 130 | switch v := class.(type) { 131 | case *Class: 132 | objectID, ok := respJSON["objectId"].(string) 133 | if !ok { 134 | return nil, fmt.Errorf("unexpected error when parse objectId from response: want type string but %v", reflect.TypeOf(respJSON["objectId"])) 135 | } 136 | if rv := reflect.Indirect(reflect.ValueOf(object)); rv.CanSet() { 137 | createdAt, ok := respJSON["createdAt"].(string) 138 | if !ok { 139 | return nil, fmt.Errorf("unexpected error when parse createdAt from response: want type string but %v", reflect.TypeOf(respJSON["createdAt"])) 140 | } 141 | decodedCreatedAt, err := time.Parse(time.RFC3339, createdAt) 142 | if err != nil { 143 | return nil, err 144 | } 145 | if rv.Type() == reflect.TypeOf(Object{}) { 146 | objectPtr, _ := object.(*Object) 147 | objectPtr.ID = objectID 148 | objectPtr.CreatedAt = decodedCreatedAt 149 | objectPtr.UpdatedAt = decodedCreatedAt 150 | objectPtr.ref = v 151 | } else if meta := extractObjectMeta(rv.Interface()); meta != nil { 152 | objectPtr := &Object{ 153 | ID: objectID, 154 | CreatedAt: decodedCreatedAt, 155 | UpdatedAt: decodedCreatedAt, 156 | ref: &ObjectRef{ 157 | ID: objectID, 158 | class: v.Name, 159 | c: c, 160 | }, 161 | } 162 | rv.FieldByName("Object").Set(reflect.ValueOf(*objectPtr)) 163 | } 164 | } 165 | 166 | return &ObjectRef{ 167 | ID: objectID, 168 | class: v.Name, 169 | c: c, 170 | }, nil 171 | case *Users: 172 | return decodeUser(respJSON) 173 | } 174 | 175 | return nil, nil 176 | 177 | } 178 | 179 | func objectGet(ref interface{}, object interface{}, authOptions ...AuthOption) error { 180 | path := "/1.1/" 181 | var c *Client 182 | 183 | switch v := ref.(type) { 184 | case *ObjectRef: 185 | path = fmt.Sprint(path, "classes/", v.class, "/", v.ID) 186 | c = v.c 187 | break 188 | case *UserRef: 189 | path = fmt.Sprint(path, "users/", v.ID) 190 | c = v.c 191 | break 192 | case *FileRef: 193 | path = fmt.Sprint(path, "files/", v.ID) 194 | c = v.c 195 | } 196 | 197 | resp, err := c.request(methodGet, path, c.getRequestOptions(), authOptions...) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | respJSON := make(map[string]interface{}) 203 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 204 | return err 205 | } 206 | 207 | switch v := ref.(type) { 208 | case *ObjectRef: 209 | decodedObject, err := decodeObject(respJSON) 210 | if err != nil { 211 | return err 212 | } 213 | decodedObject.ref = v 214 | if reflect.Indirect(reflect.ValueOf(object)).Type() == reflect.TypeOf(Object{}) { 215 | reflect.Indirect(reflect.ValueOf(object)).Set(reflect.ValueOf(*decodedObject)) 216 | } else if meta := extractObjectMeta(reflect.Indirect(reflect.ValueOf(object)).Interface()); meta != nil { 217 | if err := bind(reflect.ValueOf(decodedObject.fields), reflect.Indirect(reflect.ValueOf(object))); err != nil { 218 | return err 219 | } 220 | reflect.ValueOf(object).Elem().FieldByName("Object").Set(reflect.ValueOf(*decodedObject)) 221 | } 222 | case *UserRef: 223 | decodedUser, err := decodeUser(respJSON) 224 | if err != nil { 225 | return err 226 | } 227 | decodedUser.ref = v 228 | if reflect.Indirect(reflect.ValueOf(object)).Type() == reflect.TypeOf(User{}) { 229 | reflect.Indirect(reflect.ValueOf(object)).Set(reflect.ValueOf(*decodedUser)) 230 | } else if meta := extractUserMeta(reflect.Indirect(reflect.ValueOf(object)).Interface()); meta != nil { 231 | if err := bind(reflect.ValueOf(decodedUser.fields), reflect.Indirect(reflect.ValueOf(object))); err != nil { 232 | return err 233 | } 234 | reflect.ValueOf(object).Elem().FieldByName("User").Set(reflect.Indirect(reflect.ValueOf(decodedUser))) 235 | } 236 | case *FileRef: 237 | decodedFile, err := decodeFile(respJSON) 238 | if err != nil { 239 | return err 240 | } 241 | decodedFile.ref = v 242 | object = decodedFile 243 | } 244 | 245 | return nil 246 | } 247 | 248 | func objectSet(ref interface{}, key string, value interface{}, authOptions ...AuthOption) error { 249 | path := "/1.1/" 250 | var c *Client 251 | 252 | switch v := ref.(type) { 253 | case *ObjectRef: 254 | path = fmt.Sprint(path, "classes/", v.class, "/", v.ID) 255 | c = v.c 256 | break 257 | case *UserRef: 258 | path = fmt.Sprint(path, "users/", v.ID) 259 | c = v.c 260 | break 261 | } 262 | 263 | options := c.getRequestOptions() 264 | options.JSON = encode(map[string]interface{}{key: value}, true) 265 | 266 | _, err := c.request(methodPut, path, options, authOptions...) 267 | if err != nil { 268 | return err 269 | } 270 | 271 | return nil 272 | } 273 | 274 | func objectUpdate(ref interface{}, diff interface{}, authOptions ...AuthOption) error { 275 | path := "/1.1/" 276 | var c *Client 277 | var options *grequests.RequestOptions 278 | 279 | switch v := ref.(type) { 280 | case *ObjectRef: 281 | path = fmt.Sprint(path, "classes/", v.class, "/", v.ID) 282 | c = v.c 283 | options = c.getRequestOptions() 284 | switch reflect.Indirect(reflect.ValueOf(diff)).Kind() { 285 | case reflect.Map: 286 | options.JSON = encodeMap(diff, true) 287 | case reflect.Struct: 288 | options.JSON = encodeObject(diff, false, true) 289 | default: 290 | return fmt.Errorf("object should be strcut or map") 291 | } 292 | break 293 | case *UserRef: 294 | path = fmt.Sprint(path, "users/", v.ID) 295 | c = v.c 296 | options = c.getRequestOptions() 297 | switch reflect.ValueOf(diff).Kind() { 298 | case reflect.Map: 299 | options.JSON = encodeMap(diff, true) 300 | case reflect.Struct: 301 | options.JSON = encodeUser(diff, false, true) 302 | default: 303 | return fmt.Errorf("object should be struct or map") 304 | } 305 | break 306 | } 307 | 308 | _, err := c.request(methodPut, path, options, authOptions...) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | return nil 314 | } 315 | 316 | func objectUpdateWithQuery(ref interface{}, diff interface{}, query interface{}, authOptions ...AuthOption) error { 317 | path := "/1.1/" 318 | var c *Client 319 | var options *grequests.RequestOptions 320 | 321 | switch v := ref.(type) { 322 | case *ObjectRef: 323 | path = fmt.Sprint(path, "classes/", v.class, "/", v.ID) 324 | c = v.c 325 | options = c.getRequestOptions() 326 | switch reflect.Indirect(reflect.ValueOf(diff)).Kind() { 327 | case reflect.Map: 328 | options.JSON = encodeMap(diff, true) 329 | case reflect.Struct: 330 | options.JSON = encodeObject(diff, false, true) 331 | default: 332 | return fmt.Errorf("object should be strcut or map") 333 | } 334 | break 335 | case *UserRef: 336 | path = fmt.Sprint(path, "users/", v.ID) 337 | c = v.c 338 | options = c.getRequestOptions() 339 | switch reflect.ValueOf(diff).Kind() { 340 | case reflect.Map: 341 | options.JSON = encodeMap(diff, true) 342 | case reflect.Struct: 343 | options.JSON = encodeUser(diff, false, true) 344 | default: 345 | return fmt.Errorf("object should be struct or map") 346 | } 347 | break 348 | } 349 | 350 | params, err := wrapParams(query, false, false) 351 | if err != nil { 352 | return err 353 | } 354 | options.Params = params 355 | 356 | _, err = c.request(methodPut, path, options, authOptions...) 357 | if err != nil { 358 | return err 359 | } 360 | 361 | return nil 362 | } 363 | 364 | func objectDestroy(ref interface{}, authOptions ...AuthOption) error { 365 | path := "/1.1/" 366 | var c *Client 367 | 368 | switch v := ref.(type) { 369 | case *ObjectRef: 370 | path = fmt.Sprint(path, "classes/", v.class, "/", v.ID) 371 | c = v.c 372 | case *UserRef: 373 | path = fmt.Sprint(path, "users/", v.ID) 374 | c = v.c 375 | case *FileRef: 376 | path = fmt.Sprint(path, "files/", v.ID) 377 | c = v.c 378 | } 379 | 380 | resp, err := c.request(methodDelete, path, c.getRequestOptions(), authOptions...) 381 | if err != nil { 382 | return err 383 | } 384 | 385 | respJSON := make(map[string]interface{}) 386 | if err := json.Unmarshal(resp.Bytes(), &respJSON); err != nil { 387 | return err 388 | } 389 | 390 | return nil 391 | } 392 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 LeanCloud 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /leancloud/server.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "context" 5 | "crypto/md5" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "runtime" 12 | "runtime/debug" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | const cloudFunctionTimeout = time.Second * 15 18 | 19 | type metadataResponse struct { 20 | Result []string `json:"result"` 21 | } 22 | 23 | type functionResponse struct { 24 | Result interface{} `json:"result"` 25 | } 26 | 27 | // Handler takes all requests related to LeanEngine 28 | func (engine *engine) Handler() http.Handler { 29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | uri := strings.Split(r.RequestURI, "/") 31 | engine.corsHandler(w, r) 32 | if r.Method == "OPTIONS" { 33 | w.WriteHeader(http.StatusOK) 34 | return 35 | } 36 | if strings.HasPrefix(r.RequestURI, "/1.1/functions/") || strings.HasPrefix(r.RequestURI, "/1/functions/") { 37 | if strings.Compare(r.RequestURI, "/1.1/functions/_ops/metadatas") == 0 || strings.Compare(r.RequestURI, "/1/functions/_ops/metadatas") == 0 { 38 | engine.metadataHandler(w, r) 39 | } else { 40 | if uri[3] != "" { 41 | if len(uri) == 5 { 42 | engine.classHookHandler(w, r, uri[3], uri[4]) 43 | } else { 44 | engine.functionHandler(w, r, uri[3], false) 45 | } 46 | } else { 47 | w.WriteHeader(http.StatusNotFound) 48 | } 49 | } 50 | } else if strings.HasPrefix(r.RequestURI, "/1.1/call/") || strings.HasPrefix(r.RequestURI, "/1/call/") { 51 | if engine.functions[uri[3]] != nil { 52 | engine.functionHandler(w, r, uri[3], true) 53 | } else { 54 | w.WriteHeader(http.StatusNotFound) 55 | } 56 | } else if r.RequestURI == "/__engine/1/ping" || r.RequestURI == "/__engine/1.1/ping" { 57 | engine.healthCheckHandler(w, r) 58 | } 59 | }) 60 | } 61 | 62 | func (engine *engine) corsHandler(w http.ResponseWriter, r *http.Request) { 63 | if r.Header.Get("origin") != "" { 64 | w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("origin")) 65 | } 66 | 67 | if r.Method == "OPTIONS" { 68 | w.Header().Add("Access-Control-Max-Age", "86400") 69 | w.Header().Add("Access-Control-Allow-Methods", "HEAD, GET, POST, PUT, DELETE, OPTIONS") 70 | w.Header().Add("Access-Control-Allow-Headers", `Content-Type,X-AVOSCloud-Application-Id,X-AVOSCloud-Application-Key,X-AVOSCloud-Application-Production,X-AVOSCloud-Client-Version,X-AVOSCloud-Request-Sign,X-AVOSCloud-Session-Token,X-AVOSCloud-Super-Key,X-LC-Hook-Key,X-LC-Id,X-LC-Key,X-LC-Prod,X-LC-Session,X-LC-Sign,X-LC-UA,X-Requested-With,X-Uluru-Application-Id,X-Uluru-Application-Key,X-Uluru-Application-Production,X-Uluru-Client-Version,X-Uluru-Session-Token`) 71 | } 72 | } 73 | 74 | func (engine *engine) metadataHandler(w http.ResponseWriter, r *http.Request) { 75 | if !engine.validateMasterKey(r) { 76 | writeCloudError(w, r, CloudError{ 77 | Code: http.StatusUnauthorized, 78 | Message: fmt.Sprintf("Master Key check failed, request from %s", r.RemoteAddr), 79 | StatusCode: http.StatusUnauthorized, 80 | }) 81 | return 82 | } 83 | 84 | meta, err := engine.generateMetadata() 85 | if err != nil { 86 | writeCloudError(w, r, CloudError{ 87 | Code: 1, 88 | Message: err.Error(), 89 | StatusCode: http.StatusInternalServerError, 90 | callStack: debug.Stack(), 91 | }) 92 | return 93 | } 94 | 95 | w.Write(meta) 96 | } 97 | 98 | func (engine *engine) healthCheckHandler(w http.ResponseWriter, r *http.Request) { 99 | resp, err := json.Marshal(map[string]string{ 100 | "runtime": runtime.Version(), 101 | "version": Version, 102 | }) 103 | if err != nil { 104 | writeCloudError(w, r, CloudError{ 105 | Code: 1, 106 | Message: err.Error(), 107 | StatusCode: http.StatusInternalServerError, 108 | callStack: debug.Stack(), 109 | }) 110 | return 111 | } 112 | 113 | w.Write(resp) 114 | } 115 | 116 | func (engine *engine) functionHandler(w http.ResponseWriter, r *http.Request, name string, rpc bool) { 117 | if engine.functions[name] == nil { 118 | writeCloudError(w, r, CloudError{ 119 | Code: 1, 120 | Message: fmt.Sprintf("No such cloud function %s", name), 121 | StatusCode: http.StatusNotFound, 122 | }) 123 | return 124 | } 125 | 126 | if engine.functions[name].defineOption["hook"] == true { 127 | if !engine.validateHookKey(r) { 128 | writeCloudError(w, r, CloudError{ 129 | Code: http.StatusUnauthorized, 130 | Message: fmt.Sprintf("Hook key check failed, request from %s", r.RemoteAddr), 131 | StatusCode: http.StatusUnauthorized, 132 | }) 133 | return 134 | } 135 | } 136 | 137 | if engine.functions[name].defineOption["internal"] == true { 138 | if !engine.validateMasterKey(r) { 139 | if !engine.validateHookKey(r) { 140 | master, pass := engine.validateSignature(r) 141 | if !master || !pass { 142 | writeCloudError(w, r, CloudError{ 143 | Code: http.StatusUnauthorized, 144 | Message: fmt.Sprintf("Internal cloud function, request from %s", r.RemoteAddr), 145 | StatusCode: http.StatusUnauthorized, 146 | }) 147 | return 148 | } 149 | } 150 | } 151 | } 152 | 153 | if !engine.validateAppKey(r) { 154 | if !engine.validateMasterKey(r) { 155 | _, pass := engine.validateSignature(r) 156 | if !pass { 157 | writeCloudError(w, r, CloudError{ 158 | Code: http.StatusUnauthorized, 159 | Message: fmt.Sprintf("App key check failed, request from %s", r.RemoteAddr), 160 | StatusCode: http.StatusUnauthorized, 161 | }) 162 | return 163 | } 164 | } 165 | } 166 | 167 | request, err := engine.constructRequest(r, name, rpc) 168 | if err != nil { 169 | writeCloudError(w, r, CloudError{ 170 | Code: 1, 171 | Message: err.Error(), 172 | StatusCode: http.StatusInternalServerError, 173 | }) 174 | return 175 | } 176 | 177 | ret, err := engine.executeTimeout(request, name, cloudFunctionTimeout) 178 | if err != nil { 179 | writeCloudError(w, r, err) 180 | return 181 | } 182 | var resp functionResponse 183 | if rpc { 184 | resp.Result = encode(ret, true) 185 | } else { 186 | resp.Result = ret 187 | } 188 | 189 | respJSON, err := json.Marshal(resp) 190 | if err != nil { 191 | writeCloudError(w, r, CloudError{ 192 | Code: 1, 193 | Message: err.Error(), 194 | StatusCode: http.StatusInternalServerError, 195 | callStack: debug.Stack(), 196 | }) 197 | return 198 | } 199 | 200 | w.Write(respJSON) 201 | } 202 | 203 | func (engine *engine) classHookHandler(w http.ResponseWriter, r *http.Request, class, hook string) { 204 | if !engine.validateHookKey(r) { 205 | writeCloudError(w, r, CloudError{ 206 | Code: http.StatusUnauthorized, 207 | Message: fmt.Sprintf("Hook key check failed, request from %s", r.RemoteAddr), 208 | StatusCode: http.StatusUnauthorized, 209 | }) 210 | return 211 | } 212 | 213 | name := fmt.Sprint(classHookmap[hook], class) 214 | 215 | request, err := engine.constructRequest(r, name, false) 216 | if err != nil { 217 | writeCloudError(w, r, CloudError{ 218 | Code: 1, 219 | Message: err.Error(), 220 | StatusCode: http.StatusInternalServerError, 221 | }) 222 | return 223 | } 224 | 225 | ret, err := engine.executeTimeout(request, name, cloudFunctionTimeout) 226 | 227 | if err != nil { 228 | writeCloudError(w, r, err) 229 | return 230 | } 231 | 232 | var resp map[string]interface{} 233 | if hook == "beforeSave" { 234 | resp = encodeObject(ret, false, false) 235 | } else { 236 | resp = map[string]interface{}{ 237 | "result": "ok", 238 | } 239 | } 240 | 241 | respJSON, err := json.Marshal(resp) 242 | if err != nil { 243 | writeCloudError(w, r, CloudError{ 244 | Code: 1, 245 | Message: err.Error(), 246 | StatusCode: http.StatusInternalServerError, 247 | callStack: debug.Stack(), 248 | }) 249 | return 250 | } 251 | 252 | w.Write(respJSON) 253 | } 254 | 255 | func (engine *engine) executeTimeout(r *FunctionRequest, name string, timeout time.Duration) (interface{}, error) { 256 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 257 | defer cancel() 258 | 259 | var ret interface{} 260 | var err error 261 | ch := make(chan bool, 0) 262 | go func() { 263 | defer func() { 264 | if ierr := recover(); ierr != nil { 265 | err = CloudError{ 266 | Code: 1, 267 | Message: fmt.Sprint(ierr), 268 | StatusCode: http.StatusInternalServerError, 269 | callStack: debug.Stack(), 270 | } 271 | ch <- true 272 | } 273 | }() 274 | ret, err = engine.functions[name].call(r) 275 | ch <- true 276 | }() 277 | 278 | select { 279 | case <-ch: 280 | return ret, err 281 | case <-ctx.Done(): 282 | return nil, CloudError{ 283 | Code: 124, 284 | Message: fmt.Sprintf("LeanEngine: /1.1/functions/%s : function timeout (15000ms)", name), 285 | StatusCode: http.StatusServiceUnavailable, 286 | } 287 | } 288 | } 289 | 290 | func (engine *engine) unmarshalBody(r *http.Request) (interface{}, error) { 291 | defer r.Body.Close() 292 | 293 | body := make(map[string]interface{}) 294 | err := json.NewDecoder(r.Body).Decode(&body) 295 | if err == io.EOF { 296 | return nil, nil 297 | } 298 | 299 | if err != nil { 300 | return nil, err 301 | } 302 | 303 | return body, nil 304 | } 305 | 306 | func printErrWithStack(errMsg string) { 307 | // 输出错误 308 | builder := new(strings.Builder) 309 | fmt.Fprintf(builder, "%s\n", errMsg) 310 | // 输出调用栈信息 311 | pc := make([]uintptr, 50) // 最多获取 50 层调用栈信息 312 | n := runtime.Callers(1, pc) 313 | frames := runtime.CallersFrames(pc[:n-1]) 314 | // 跳过当前层级的信息 315 | frames.Next() 316 | // 打印剩余的调用栈信息 317 | for { 318 | frame, more := frames.Next() 319 | if !more { 320 | break 321 | } 322 | fmt.Fprintf(builder, "%s()\n\t%s:%d\n", frame.Function, frame.File, frame.Line) 323 | } 324 | fmt.Fprintf(os.Stderr, builder.String()) 325 | } 326 | 327 | func (engine *engine) constructRequest(r *http.Request, name string, rpc bool) (*FunctionRequest, error) { 328 | request := new(FunctionRequest) 329 | request.Meta = map[string]string{ 330 | "remoteAddr": r.RemoteAddr, 331 | } 332 | var sessionToken string 333 | if r.Header.Get("X-LC-Session") != "" { 334 | sessionToken = r.Header.Get("X-LC-Session") 335 | } else if r.Header.Get("x-uluru-session-token") != "" { 336 | sessionToken = r.Header.Get("x-uluru-session-token") 337 | } else if r.Header.Get("x-avoscloud-session-token") != "" { 338 | sessionToken = r.Header.Get("x-avoscloud-session-token") 339 | } 340 | 341 | if engine.functions[name].defineOption["fetchUser"] == true && sessionToken != "" { 342 | user, err := Engine.client().Users.Become(sessionToken) 343 | if err != nil { 344 | printErrWithStack(fmt.Sprintf("Users.Become() failed. req=%s, err=%v", r.RequestURI, err)) 345 | return nil, err 346 | } 347 | request.CurrentUser = user 348 | request.SessionToken = sessionToken 349 | } 350 | 351 | if r.Body == nil { 352 | request.Params = nil 353 | return request, nil 354 | } 355 | 356 | params, err := engine.unmarshalBody(r) 357 | if err != nil { 358 | printErrWithStack(fmt.Sprintf("engine.unmarshalBody() failed. req=%s err=%v", r.RequestURI, err)) 359 | return nil, err 360 | } 361 | 362 | if rpc { 363 | decodedParams, err := decode(params) 364 | if err != nil { 365 | printErrWithStack(fmt.Sprintf("decode() failed. req=%s err=%v", r.RequestURI, err)) 366 | return nil, err 367 | } 368 | 369 | request.Params = decodedParams 370 | } else { 371 | request.Params = params 372 | } 373 | 374 | return request, nil 375 | } 376 | 377 | func (engine *engine) generateMetadata() ([]byte, error) { 378 | meta := metadataResponse{ 379 | Result: []string{}, 380 | } 381 | 382 | for k := range engine.functions { 383 | meta.Result = append(meta.Result, k) 384 | } 385 | return json.Marshal(meta) 386 | } 387 | 388 | func (engine *engine) validateAppID(r *http.Request) bool { 389 | if r.Header.Get("X-LC-Id") != "" { 390 | if engine.client().appID != r.Header.Get("X-LC-Id") { 391 | return false 392 | } 393 | } else if r.Header.Get("x-avoscloud-application-id") != "" { 394 | if engine.client().appID != r.Header.Get("x-avoscloud-application-id") { 395 | return false 396 | } 397 | } else if r.Header.Get("x-uluru-application-id") != "" { 398 | if engine.client().appID != r.Header.Get("x-uluru-application-id") { 399 | return false 400 | } 401 | } else { 402 | return false 403 | } 404 | 405 | return true 406 | } 407 | 408 | func (engine *engine) validateAppKey(r *http.Request) bool { 409 | if !engine.validateAppID(r) { 410 | return false 411 | } 412 | 413 | if r.Header.Get("X-LC-Key") != "" { 414 | if engine.client().appKey != r.Header.Get("X-LC-Key") { 415 | return false 416 | } 417 | } else if r.Header.Get("x-avoscloud-application-key") != "" { 418 | if engine.client().appKey != r.Header.Get("x-avoscloud-application-key") { 419 | return false 420 | } 421 | } else if r.Header.Get("x-uluru-application-key") != "" { 422 | if engine.client().appKey != r.Header.Get("x-uluru-application-key") { 423 | return false 424 | } 425 | } else { 426 | return false 427 | } 428 | 429 | return true 430 | } 431 | 432 | func (engine *engine) validateMasterKey(r *http.Request) bool { 433 | if !engine.validateAppID(r) { 434 | return false 435 | } 436 | 437 | if r.Header.Get("X-LC-Key") != "" { 438 | if strings.TrimSuffix(r.Header.Get("X-LC-Key"), ",master") != engine.client().masterKey { 439 | return false 440 | } 441 | } else if r.Header.Get("x-avoscloud-master-key") != "" { 442 | if r.Header.Get("x-avoscloud-master-key") != engine.client().masterKey { 443 | return false 444 | } 445 | } else if r.Header.Get("x-uluru-master-key") != "" { 446 | if r.Header.Get("x-uluru-master-key") != engine.client().masterKey { 447 | return false 448 | } 449 | } else { 450 | return false 451 | } 452 | 453 | return true 454 | } 455 | 456 | func (engine *engine) validateHookKey(r *http.Request) bool { 457 | if !engine.validateAppID(r) { 458 | return false 459 | } 460 | 461 | if os.Getenv("LEANCLOUD_APP_HOOK_KEY") != r.Header.Get("X-LC-Hook-Key") { 462 | return false 463 | } 464 | 465 | return true 466 | } 467 | 468 | func (engine *engine) validateSignature(r *http.Request) (bool, bool) { 469 | var master, pass bool 470 | if !engine.validateAppID(r) { 471 | return master, pass 472 | } 473 | 474 | var sign string 475 | if r.Header.Get("X-LC-Sign") != "" { 476 | sign = r.Header.Get("X-LC-Sign") 477 | } else if r.Header.Get("x-avoscloud-request-sign") != "" { 478 | sign = r.Header.Get("x-avoscloud-request-sign") 479 | } 480 | 481 | if sign == "" { 482 | return master, pass 483 | } 484 | signSlice := strings.Split(sign, ",") 485 | var hash [16]byte 486 | if len(signSlice) == 3 && signSlice[2] == "master" { 487 | hash = md5.Sum([]byte(fmt.Sprint(signSlice[1], engine.client().masterKey))) 488 | master = true 489 | } else { 490 | hash = md5.Sum([]byte(fmt.Sprint(signSlice[1], engine.client().appKey))) 491 | } 492 | if signSlice[0] == fmt.Sprintf("%x", hash) { 493 | pass = true 494 | } 495 | return master, pass 496 | } 497 | -------------------------------------------------------------------------------- /leancloud/encoding.go: -------------------------------------------------------------------------------- 1 | package leancloud 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func isBare(object interface{}) bool { 12 | v := reflect.Indirect(reflect.ValueOf(object)) 13 | 14 | if v.Kind() == reflect.Struct { 15 | if v.Type() == reflect.TypeOf(Object{}) { 16 | return true 17 | } else if v.Type() == reflect.TypeOf(User{}) { 18 | return true 19 | } else if v.Type() == reflect.TypeOf(Role{}) { 20 | return true 21 | } 22 | } 23 | 24 | return false 25 | } 26 | 27 | func extractObjectMeta(object interface{}) *Object { 28 | if object == nil { 29 | return nil 30 | } 31 | 32 | v := reflect.Indirect(reflect.ValueOf(object)) 33 | if v.Kind() == reflect.Struct { 34 | if v.Type() == reflect.TypeOf(Object{}) { 35 | meta, ok := v.Interface().(Object) 36 | if !ok { 37 | return nil 38 | } 39 | return &meta 40 | } 41 | metaField, ok := v.Type().FieldByName("Object") 42 | if !ok { 43 | return nil 44 | } 45 | if metaField.Type == reflect.TypeOf(Object{}) { 46 | meta, ok := v.FieldByName("Object").Interface().(Object) 47 | if !ok { 48 | return nil 49 | } 50 | return &meta 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func extractUserMeta(user interface{}) *User { 57 | if user == nil { 58 | return nil 59 | } 60 | 61 | v := reflect.Indirect(reflect.ValueOf(user)) 62 | 63 | if v.Kind() == reflect.Struct { 64 | if v.Type() == reflect.TypeOf(User{}) { 65 | meta, ok := v.Interface().(User) 66 | if !ok { 67 | return nil 68 | } 69 | return &meta 70 | } 71 | 72 | metaField, ok := v.Type().FieldByName("User") 73 | if !ok { 74 | return nil 75 | } 76 | if metaField.Type == reflect.TypeOf(User{}) { 77 | meta, ok := v.FieldByName("User").Interface().(User) 78 | if !ok { 79 | return nil 80 | } 81 | return &meta 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | func encode(object interface{}, ignoreZero bool) interface{} { 88 | switch o := object.(type) { 89 | case Op: 90 | return encodeOp(&o) 91 | case GeoPoint: 92 | return encodeGeoPoint(&o) 93 | case time.Time: 94 | return encodeDate(&o) 95 | case File: 96 | return encodeFile(&o, true) 97 | case Relation: 98 | return encodeRelation(&o) 99 | case ACL: 100 | return encodeACL(&o) 101 | case *Op: 102 | return encodeOp(o) 103 | case *GeoPoint: 104 | return encodeGeoPoint(o) 105 | case *time.Time: 106 | return encodeDate(o) 107 | case []byte: 108 | return encodeBytes(o) 109 | case *File: 110 | return encodeFile(o, true) 111 | case *Relation: 112 | return encodeRelation(o) 113 | case *ACL: 114 | return encodeACL(o) 115 | default: 116 | switch reflect.ValueOf(object).Kind() { 117 | case reflect.Slice, reflect.Array: 118 | return encodeArray(object, ignoreZero) 119 | case reflect.Map: 120 | return encodeMap(o, ignoreZero) 121 | case reflect.Struct: 122 | if meta := extractUserMeta(o); meta != nil { 123 | return encodeUser(o, true, ignoreZero) 124 | } else if meta := extractObjectMeta(o); meta != nil { 125 | return encodeObject(o, true, ignoreZero) 126 | } else { 127 | return encodeStruct(o, ignoreZero) 128 | } 129 | case reflect.Interface, reflect.Ptr: 130 | return encode(reflect.Indirect(reflect.ValueOf(o)).Interface(), ignoreZero) 131 | default: 132 | return object 133 | } 134 | } 135 | } 136 | 137 | func encodeObject(object interface{}, embedded bool, ignoreZero bool) map[string]interface{} { 138 | v := reflect.Indirect(reflect.ValueOf(object)) 139 | t := v.Type() 140 | 141 | meta := extractObjectMeta(object) 142 | if meta == nil { 143 | return nil 144 | } 145 | 146 | if embedded { 147 | ref, ok := meta.ref.(*ObjectRef) 148 | if !ok { 149 | return nil 150 | } 151 | return map[string]interface{}{ 152 | "__type": "Pointer", 153 | "objectId": ref.ID, 154 | "className": ref.class, 155 | } 156 | } 157 | 158 | encodedObject := make(map[string]interface{}) 159 | 160 | if isBare(object) { 161 | return encodeMap(meta.fields, ignoreZero) 162 | } 163 | 164 | for i := 0; i < v.NumField(); i++ { 165 | tag, option := parseTag(t.Field(i).Tag.Get("json")) 166 | if option == "omitempty" && v.Field(i).IsZero() { 167 | continue 168 | } 169 | if tag == "" { 170 | tag = t.Field(i).Name 171 | } 172 | if v.Field(i).Kind() == reflect.Ptr || v.Field(i).Kind() == reflect.Interface { 173 | if v.Field(i).IsNil() { 174 | continue 175 | } 176 | } else { 177 | if ignoreZero { 178 | if v.Field(i).IsZero() { 179 | continue 180 | } 181 | } 182 | } 183 | 184 | if isBare(v.Field(i).Interface()) && tag == "Object" { 185 | continue 186 | } 187 | 188 | encoded := encode(v.Field(i).Interface(), ignoreZero) 189 | if !reflect.ValueOf(encoded).IsZero() { 190 | encodedObject[tag] = encoded 191 | } 192 | 193 | } 194 | return encodedObject 195 | } 196 | 197 | func encodeUser(user interface{}, embedded bool, ignoreZero bool) map[string]interface{} { 198 | v := reflect.Indirect(reflect.ValueOf(user)) 199 | t := v.Type() 200 | 201 | meta := extractUserMeta(user) 202 | if meta == nil { 203 | return nil 204 | } 205 | 206 | if embedded { 207 | if meta.ID == "" { 208 | return nil 209 | } 210 | 211 | return map[string]interface{}{ 212 | "__type": "Pointer", 213 | "objectId": meta.ID, 214 | "className": "_User", 215 | } 216 | } 217 | encodedUser := make(map[string]interface{}) 218 | 219 | if isBare(user) { 220 | return encodeMap(meta.fields, ignoreZero) 221 | } 222 | 223 | for i := 0; i < v.NumField(); i++ { 224 | tag, option := parseTag(t.Field(i).Tag.Get("json")) 225 | if option == "omitempty" && v.Field(i).IsZero() { 226 | continue 227 | } 228 | if tag == "" { 229 | tag = t.Field(i).Name 230 | } 231 | if v.Field(i).Kind() == reflect.Ptr || v.Field(i).Kind() == reflect.Interface { 232 | if v.Field(i).IsNil() { 233 | continue 234 | } 235 | } else { 236 | if ignoreZero { 237 | if v.Field(i).IsZero() { 238 | continue 239 | } 240 | } 241 | } 242 | if isBare(v.Field(i).Interface()) && tag == "User" { 243 | continue 244 | } 245 | encoded := encode(v.Field(i).Interface(), ignoreZero) 246 | if !reflect.ValueOf(encoded).IsZero() { 247 | encodedUser[tag] = encoded 248 | } 249 | } 250 | return encodedUser 251 | } 252 | 253 | func encodeMap(fields interface{}, ignoreZero bool) map[string]interface{} { 254 | encodedMap := make(map[string]interface{}) 255 | v := reflect.ValueOf(fields) 256 | for iter := v.MapRange(); iter.Next(); { 257 | encodedMap[iter.Key().String()] = encode(iter.Value().Interface(), ignoreZero) 258 | if encodedMap[iter.Key().String()] == nil { 259 | delete(encodedMap, iter.Key().String()) 260 | } 261 | } 262 | return encodedMap 263 | } 264 | 265 | func encodeStruct(object interface{}, ignoreZero bool) map[string]interface{} { 266 | v := reflect.Indirect(reflect.ValueOf(object)) 267 | t := v.Type() 268 | 269 | if v.IsValid() && v.Kind() == reflect.Struct { 270 | encodedMap := make(map[string]interface{}) 271 | for i := 0; i < v.NumField(); i++ { 272 | encodedMap[t.Field(i).Name] = encode(v.Field(i).Interface(), ignoreZero) 273 | if encodedMap[t.Field(i).Name] == nil { 274 | delete(encodedMap, t.Field(i).Name) 275 | } 276 | } 277 | 278 | return encodedMap 279 | } 280 | 281 | return nil 282 | } 283 | 284 | func encodeArray(array interface{}, ignoreZero bool) []interface{} { 285 | var encodedArray []interface{} 286 | v := reflect.ValueOf(array) 287 | for i := 0; i < v.Len(); i++ { 288 | encodedArray = append(encodedArray, encode(v.Index(i).Interface(), ignoreZero)) 289 | } 290 | 291 | return encodedArray 292 | } 293 | 294 | func encodeDate(date *time.Time) map[string]interface{} { 295 | return map[string]interface{}{ 296 | "__type": "Date", 297 | "iso": fmt.Sprint(date.In(time.FixedZone("UTC", 0)).Format("2006-01-02T15:04:05.000Z")), 298 | } 299 | } 300 | 301 | func encodeGeoPoint(point *GeoPoint) map[string]interface{} { 302 | return map[string]interface{}{ 303 | "__type": "GeoPoint", 304 | "latitude": point.Latitude, 305 | "longitude": point.Longitude, 306 | } 307 | } 308 | 309 | func encodeBytes(bytes []byte) map[string]interface{} { 310 | if len(bytes) == 0 { 311 | return nil 312 | } 313 | 314 | return map[string]interface{}{ 315 | "__type": "Bytes", 316 | "base64": base64.StdEncoding.EncodeToString([]byte(strings.TrimSpace(string(bytes)))), 317 | } 318 | } 319 | 320 | func encodeFile(file *File, embedded bool) map[string]interface{} { 321 | if embedded { 322 | if file.ID == "" { 323 | return nil 324 | } 325 | 326 | return map[string]interface{}{ 327 | "__type": "File", 328 | "id": file.ID, 329 | } 330 | } 331 | 332 | return map[string]interface{}{ 333 | "__type": "File", 334 | "name": file.Name, 335 | "mime_type": file.MIME, 336 | "metaData": file.MetaData, 337 | } 338 | } 339 | 340 | func encodeACL(acl *ACL) map[string]interface{} { 341 | return map[string]interface{}{ 342 | "ACL": acl.content, 343 | } 344 | } 345 | 346 | func encodeAuthData(data *AuthData) interface{} { 347 | return data.data 348 | } 349 | 350 | func encodeOp(op *Op) map[string]interface{} { 351 | ret := make(map[string]interface{}) 352 | ret["__op"] = op.name 353 | switch op.name { 354 | case "Increment", "Decrement": 355 | ret["amount"] = op.objects 356 | case "Add", "AddUnique", "Remove": 357 | ret["objects"] = op.objects 358 | case "Delete": 359 | 360 | case "BitAnd", "BitOr", "BitXor": 361 | ret["value"] = op.objects 362 | default: 363 | return nil 364 | } 365 | 366 | return ret 367 | } 368 | 369 | func encodeRelation(relation *Relation) map[string]interface{} { 370 | return nil 371 | } 372 | 373 | func bind(src reflect.Value, dst reflect.Value) error { 374 | tdst := dst.Type() 375 | switch dst.Kind() { 376 | case reflect.Struct: 377 | if src.Kind() == reflect.Map { 378 | for i := 0; i < tdst.NumField(); i++ { 379 | tag, ok := tdst.Field(i).Tag.Lookup("json") 380 | if !ok || tag == "" { 381 | tag = tdst.Field(i).Name 382 | } 383 | mapIndex := src.MapIndex(reflect.ValueOf(tag)) 384 | if mapIndex.Kind() == reflect.Ptr && mapIndex.IsNil() { 385 | continue 386 | } 387 | if mapIndex.IsValid() { 388 | if dst.Field(i).Kind() == reflect.Ptr && dst.Field(i).IsNil() { 389 | pv := reflect.New(dst.Field(i).Type().Elem()) 390 | if err := bind(src.MapIndex(reflect.ValueOf(tag)), pv); err != nil { 391 | return err 392 | } 393 | dst.Field(i).Set(pv) 394 | } else { 395 | if err := bind(src.MapIndex(reflect.ValueOf(tag)), dst.Field(i)); err != nil { 396 | return err 397 | } 398 | } 399 | } 400 | } 401 | } else { 402 | if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr { 403 | if src.IsValid() { 404 | if src.Type() == reflect.TypeOf(Object{}) && dst.Type() != reflect.TypeOf(Object{}) { 405 | srcObject, _ := src.Interface().(Object) 406 | if err := bind(reflect.ValueOf(srcObject.fields), dst); err != nil { 407 | return err 408 | } 409 | dst.FieldByName("Object").Set(reflect.ValueOf(srcObject)) 410 | } else if src.Type() == reflect.TypeOf(User{}) && dst.Type() != reflect.TypeOf(User{}) { 411 | srcUser, _ := src.Interface().(User) 412 | if err := bind(reflect.ValueOf(srcUser.fields), dst); err != nil { 413 | return err 414 | } 415 | dst.FieldByName("User").Set(reflect.ValueOf(srcUser)) 416 | } else { 417 | dst.Set(src) 418 | } 419 | } 420 | } else { 421 | if err := bind(src.Elem(), dst); err != nil { 422 | return err 423 | } 424 | } 425 | } 426 | case reflect.Array, reflect.Slice: 427 | var isrc reflect.Value 428 | if src.Kind() != reflect.Slice { 429 | isrc = src.Elem() 430 | } else { 431 | isrc = src 432 | } 433 | if isrc.IsValid() { 434 | slice := reflect.MakeSlice(dst.Type(), isrc.Len(), isrc.Len()) 435 | for i := 0; i < isrc.Len(); i++ { 436 | var isrcIndex reflect.Value 437 | if isrc.Index(i).Kind() != reflect.Interface { 438 | isrcIndex = isrc.Index(i) 439 | } else { 440 | isrcIndex = reflect.Indirect(isrc.Index(i)) 441 | } 442 | if slice.Index(i).Kind() == reflect.Ptr && slice.Index(i).IsNil() { 443 | pv := reflect.New(slice.Index(i).Type()) 444 | if err := bind(isrcIndex, pv); err != nil { 445 | return err 446 | } 447 | slice.Index(i).Set(reflect.Indirect(pv)) 448 | } else { 449 | if err := bind(isrcIndex, slice.Index(i)); err != nil { 450 | return err 451 | } 452 | } 453 | } 454 | dst.Set(slice) 455 | } 456 | case reflect.String: 457 | dst.Set(reflect.Indirect(reflect.ValueOf(src.Interface()))) 458 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 459 | if src.Kind() != reflect.Interface { 460 | dst.Set(src.Convert(dst.Type())) 461 | } else { 462 | dst.Set(src.Elem().Convert(dst.Type())) 463 | } 464 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 465 | if src.Kind() != reflect.Interface { 466 | dst.Set(src.Convert(dst.Type())) 467 | } else { 468 | dst.Set(src.Elem().Convert(dst.Type())) 469 | } 470 | case reflect.Float32, reflect.Float64: 471 | if src.Kind() != reflect.Interface { 472 | dst.Set(src.Convert(dst.Type())) 473 | } else { 474 | dst.Set(src.Elem().Convert(dst.Type())) 475 | } 476 | case reflect.Bool: 477 | dst.SetBool(src.Elem().Bool()) 478 | case reflect.Ptr: 479 | if !dst.IsNil() { 480 | if dst.Elem().Kind() != reflect.Interface && dst.Elem().Kind() != reflect.Ptr { 481 | if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr { 482 | if src.Kind() == reflect.Array || src.Kind() == reflect.Slice { 483 | if err := bind(src, dst.Elem()); err != nil { 484 | return err 485 | } 486 | } else { 487 | if src.Type() == reflect.TypeOf(Object{}) && dst.Elem().Type() != reflect.TypeOf(Object{}) { 488 | srcObject, _ := src.Interface().(Object) 489 | if err := bind(reflect.ValueOf(srcObject.fields), dst); err != nil { 490 | return err 491 | } 492 | } else if src.Type() == reflect.TypeOf(User{}) && dst.Elem().Type() != reflect.TypeOf(User{}) { 493 | srcUser, _ := src.Interface().(User) 494 | if err := bind(reflect.ValueOf(srcUser.fields), dst); err != nil { 495 | return err 496 | } 497 | } else { 498 | dst.Elem().Set(src.Convert(dst.Type().Elem())) 499 | } 500 | } 501 | } else { 502 | if err := bind(src.Elem(), dst); err != nil { 503 | return err 504 | } 505 | } 506 | } else { 507 | if err := bind(src, dst.Elem()); err != nil { 508 | return err 509 | } 510 | } 511 | } else { 512 | pv := reflect.New(dst.Type().Elem()) 513 | if dst.Elem().Kind() != reflect.Interface && dst.Elem().Kind() != reflect.Ptr { 514 | if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr { 515 | pv.Elem().Set(src.Convert(dst.Type().Elem())) 516 | } else { 517 | if err := bind(src.Elem(), pv); err != nil { 518 | return err 519 | } 520 | } 521 | } else { 522 | if err := bind(src, dst.Elem()); err != nil { 523 | return err 524 | } 525 | } 526 | dst.Set(pv) 527 | } 528 | default: 529 | if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr { 530 | dst.Set(src) 531 | } else { 532 | if err := bind(src.Elem(), dst); err != nil { 533 | return err 534 | } 535 | } 536 | } 537 | 538 | return nil 539 | } 540 | 541 | func decode(fields interface{}) (interface{}, error) { 542 | mapFields, ok := fields.(map[string]interface{}) 543 | if !ok { 544 | switch reflect.ValueOf(fields).Kind() { 545 | case reflect.Array, reflect.Slice: 546 | return decodeArray(fields, false) 547 | case reflect.Interface, reflect.Ptr: 548 | return decode(reflect.Indirect(reflect.ValueOf(fields)).Interface()) 549 | default: 550 | return fields, nil 551 | } 552 | } 553 | if mapFields["__type"] != nil { 554 | fieldType, ok := mapFields["__type"].(string) 555 | if !ok { 556 | return nil, fmt.Errorf("unexpected error when parse __type: want string but %v", reflect.TypeOf(mapFields["__type"])) 557 | } 558 | switch fieldType { 559 | case "Pointer": 560 | return decodePointer(fields) 561 | case "Object": 562 | return decodeObject(fields) 563 | case "Date": 564 | iso, ok := mapFields["iso"].(string) 565 | if !ok { 566 | return nil, fmt.Errorf("unexpected error when parse Date: iso expected string but %v", reflect.TypeOf(mapFields["iso"])) 567 | } 568 | return decodeDate(iso) 569 | case "Bytes": 570 | base64String, ok := mapFields["base64"].(string) 571 | if !ok { 572 | return nil, fmt.Errorf("unexpected error when parse Byte: base64 string expected string but %v", reflect.TypeOf(mapFields["base64"])) 573 | } 574 | return decodeBytes(base64String) 575 | case "GeoPoint": 576 | return decodeGeoPoint(mapFields) 577 | case "File": 578 | return decodeFile(mapFields) 579 | case "Relation": 580 | return nil, nil 581 | default: 582 | return fields, nil 583 | } 584 | } else { 585 | if mapFields["__op"] != nil { 586 | return decodeOp(mapFields) 587 | } 588 | return decodeMap(fields) 589 | } 590 | } 591 | 592 | func decodeObject(fields interface{}) (*Object, error) { 593 | decodedFields, err := decodeMap(fields) 594 | if err != nil { 595 | return nil, err 596 | } 597 | var objectID, createdAt, updatedAt string 598 | var decodedCreatedAt, decodedUpdatedAt time.Time 599 | var ok bool 600 | 601 | if decodedFields["objectId"] != "" && decodedFields["objectId"] != nil { 602 | objectID, ok = decodedFields["objectId"].(string) 603 | if !ok { 604 | return nil, fmt.Errorf("unexpected error when parse objectId: want type string but %v", reflect.TypeOf(decodedFields["objectId"])) 605 | } 606 | } 607 | 608 | if decodedFields["createdAt"] != "" && decodedFields["createdAt"] != nil { 609 | createdAt, ok = decodedFields["createdAt"].(string) 610 | if !ok { 611 | return nil, fmt.Errorf("unexpected error when parse createdAt: want type string but %v", reflect.TypeOf(decodedFields["createdAt"])) 612 | } 613 | decodedCreatedAt, err = time.Parse(time.RFC3339, createdAt) 614 | if err != nil { 615 | return nil, fmt.Errorf("unexpected error when parse createdAt: %v", err) 616 | } 617 | decodedFields["createdAt"] = decodedCreatedAt 618 | } 619 | 620 | if decodedFields["updatedAt"] != "" && decodedFields["updatedAt"] != nil { 621 | updatedAt, ok = decodedFields["updatedAt"].(string) 622 | if !ok { 623 | if decodedFields["updatedAt"] == nil { 624 | updatedAt = "" 625 | } else { 626 | return nil, fmt.Errorf("unexpected error when parse updatedAt: want type string but %v", reflect.TypeOf(decodedFields["updatedAt"])) 627 | } 628 | } 629 | decodedUpdatedAt, err = time.Parse(time.RFC3339, updatedAt) 630 | if err != nil { 631 | return nil, fmt.Errorf("unexpected error when parse updatedAt: %v", err) 632 | } 633 | decodedFields["updatedAt"] = decodedUpdatedAt 634 | } 635 | 636 | return &Object{ 637 | ID: objectID, 638 | CreatedAt: decodedCreatedAt, 639 | UpdatedAt: decodedUpdatedAt, 640 | fields: decodedFields, 641 | isPointer: false, 642 | }, nil 643 | } 644 | 645 | func decodeUser(fields interface{}) (*User, error) { 646 | object, err := decodeObject(fields) 647 | if err != nil { 648 | return nil, err 649 | } 650 | 651 | sessionToken, ok := object.fields["sessionToken"].(string) 652 | 653 | if !ok && object.fields["sessionToken"] != nil { 654 | return nil, fmt.Errorf("unexpected error when parse sessionToken: want type string but %v", reflect.TypeOf(object.fields["sessionToken"])) 655 | } 656 | 657 | return &User{ 658 | Object: *object, 659 | SessionToken: sessionToken, 660 | }, nil 661 | } 662 | 663 | func decodePointer(pointer interface{}) (*Object, error) { 664 | decodedFields, err := decodeMap(pointer) 665 | if err != nil { 666 | return nil, err 667 | } 668 | 669 | objectID, ok := decodedFields["objectId"].(string) 670 | if !ok { 671 | return nil, fmt.Errorf("unexpected error when parse objectId: want type string but %v", reflect.TypeOf(decodedFields["objectId"])) 672 | } 673 | 674 | if len(decodedFields) > 2 { 675 | createdAt, ok := decodedFields["createdAt"].(string) 676 | if !ok { 677 | return nil, fmt.Errorf("unexpected error when parse createdAt: want type string but %v", reflect.TypeOf(decodedFields["createdAt"])) 678 | } 679 | decodedCreatedAt, err := time.Parse(time.RFC3339, createdAt) 680 | if err != nil { 681 | return nil, fmt.Errorf("unexpected error when parse createdAt: %v", err) 682 | } 683 | decodedFields["createdAt"] = decodedCreatedAt 684 | 685 | updatedAt, ok := decodedFields["updatedAt"].(string) 686 | if !ok { 687 | if decodedFields["updatedAt"] == nil { 688 | updatedAt = "" 689 | } else { 690 | return nil, fmt.Errorf("unexpected error when parse updatedAt: want type string but %v", reflect.TypeOf(decodedFields["updatedAt"])) 691 | } 692 | } 693 | decodedUpdatedAt, err := time.Parse(time.RFC3339, updatedAt) 694 | if err != nil { 695 | return nil, fmt.Errorf("unexpected error when parse updatedAt: %v", err) 696 | } 697 | decodedFields["updatedAt"] = decodedUpdatedAt 698 | return &Object{ 699 | ID: objectID, 700 | CreatedAt: decodedCreatedAt, 701 | UpdatedAt: decodedUpdatedAt, 702 | isPointer: true, 703 | isIncluded: true, 704 | fields: decodedFields, 705 | }, nil 706 | } 707 | 708 | return &Object{ 709 | ID: objectID, 710 | isPointer: true, 711 | isIncluded: false, 712 | fields: decodedFields, 713 | }, nil 714 | } 715 | 716 | func decodeArray(array interface{}, topQuery bool) ([]interface{}, error) { 717 | var decodedArray []interface{} 718 | v := reflect.ValueOf(array) 719 | for i := 0; i < v.Len(); i++ { 720 | if !topQuery { 721 | r, err := decode(v.Index(i).Interface()) 722 | if err != nil { 723 | return nil, err 724 | } 725 | decodedArray = append(decodedArray, r) 726 | } else { 727 | r, err := decodeObject(v.Index(i).Interface()) 728 | if err != nil { 729 | return nil, err 730 | } 731 | decodedArray = append(decodedArray, r) 732 | } 733 | } 734 | 735 | return decodedArray, nil 736 | } 737 | 738 | func decodeMap(fields interface{}) (map[string]interface{}, error) { 739 | decodedMap := make(map[string]interface{}) 740 | iter := reflect.ValueOf(fields).MapRange() 741 | for iter.Next() { 742 | if iter.Key().String() != "__type" { 743 | r, err := decode(iter.Value().Interface()) 744 | if err != nil { 745 | return nil, err 746 | } 747 | decodedMap[iter.Key().String()] = r 748 | } 749 | } 750 | 751 | return decodedMap, nil 752 | } 753 | 754 | func decodeBytes(bytesStr string) ([]byte, error) { 755 | bytes, err := base64.StdEncoding.DecodeString(bytesStr) 756 | if err != nil { 757 | return nil, fmt.Errorf("unexpected error when parse Byte %v", err) 758 | } 759 | return bytes, nil 760 | } 761 | 762 | func decodeDate(dateStr string) (*time.Time, error) { 763 | date, err := time.Parse(time.RFC3339, dateStr) 764 | if err != nil { 765 | return &time.Time{}, err 766 | } 767 | return &date, nil 768 | } 769 | 770 | func decodeGeoPoint(v map[string]interface{}) (*GeoPoint, error) { 771 | latitude, ok := v["latitude"].(float64) 772 | if !ok { 773 | return nil, fmt.Errorf("latitude want type float64 but %v", reflect.TypeOf(v["latitude"])) 774 | } 775 | longitude, ok := v["longitude"].(float64) 776 | if !ok { 777 | return nil, fmt.Errorf("longitude want type float64 but %v", reflect.TypeOf(v["longitude"])) 778 | } 779 | return &GeoPoint{ 780 | Latitude: latitude, 781 | Longitude: longitude, 782 | }, nil 783 | } 784 | 785 | func decodeFile(fields map[string]interface{}) (*File, error) { 786 | file := new(File) 787 | 788 | decodedFields, err := decodeMap(fields) 789 | if err != nil { 790 | return nil, err 791 | } 792 | file.fields = decodedFields 793 | 794 | objectID, ok := decodedFields["objectId"].(string) 795 | if !ok { 796 | if decodedFields["objectId"] == nil { 797 | return nil, nil 798 | } 799 | return nil, fmt.Errorf("unexpected error when parse objectId: want type string but %v", reflect.TypeOf(decodedFields["objectId"])) 800 | } 801 | file.ID = objectID 802 | 803 | createdAt, ok := decodedFields["createdAt"].(string) 804 | if !ok { 805 | return nil, fmt.Errorf("unexpected error when parse createdAt: want type string but %v", reflect.TypeOf(decodedFields["createdAt"])) 806 | } 807 | decodedCreatedAt, err := time.Parse(time.RFC3339, createdAt) 808 | if err != nil { 809 | return nil, fmt.Errorf("unexpected error when parse createdAt: %v", err) 810 | } 811 | file.CreatedAt = decodedCreatedAt 812 | 813 | updatedAt, ok := decodedFields["updatedAt"].(string) 814 | if !ok { 815 | return nil, fmt.Errorf("unexpected error when parse updatedAt: want type string but %v", reflect.TypeOf(decodedFields["updatedAt"])) 816 | } 817 | decodedUpdatedAt, err := time.Parse(time.RFC3339, updatedAt) 818 | if err != nil { 819 | return nil, fmt.Errorf("unexpected error when parse updatedAt: %v", err) 820 | } 821 | file.UpdatedAt = decodedUpdatedAt 822 | 823 | key, ok := decodedFields["key"].(string) 824 | if !ok && decodedFields["key"] != nil { 825 | return nil, fmt.Errorf("unexpected error when parse key from response: want type string but %v", reflect.TypeOf(decodedFields["key"])) 826 | } 827 | file.Key = key 828 | 829 | url, ok := decodedFields["url"].(string) 830 | if !ok && decodedFields["url"] != nil { 831 | return nil, fmt.Errorf("unexpected error when parse url from response: want type string but %v", reflect.TypeOf(decodedFields["url"])) 832 | } 833 | file.URL = url 834 | 835 | bucket, ok := decodedFields["bucket"].(string) 836 | if !ok && decodedFields["bucket"] != nil { 837 | return nil, fmt.Errorf("unexpected error when parse bucket from response: want type string but %v", reflect.TypeOf(decodedFields["bucket"])) 838 | } 839 | file.Bucket = bucket 840 | 841 | provider, ok := decodedFields["provider"].(string) 842 | if !ok && decodedFields["provider"] != nil { 843 | return nil, fmt.Errorf("unexpected error when parse provider from response: want type string but %v", reflect.TypeOf(decodedFields["provider"])) 844 | } 845 | file.Provider = provider 846 | 847 | return file, nil 848 | } 849 | 850 | func decodeACL(fields map[string]map[string]bool) (*ACL, error) { 851 | return nil, nil 852 | } 853 | 854 | func decodeOp(fields map[string]interface{}) (*Op, error) { 855 | op := new(Op) 856 | switch fields["__op"].(string) { 857 | case "Increment", "Decrement": 858 | op.name = fields["__op"].(string) 859 | op.objects = fields["amount"] 860 | case "Add", "AddUnique", "Remove": 861 | op.name = fields["__op"].(string) 862 | op.objects = fields["amount"] 863 | case "Delete": 864 | op.name = "Delete" 865 | case "BitAnd", "BitOr", "BitXor": 866 | op.name = fields["__op"].(string) 867 | op.objects = fields["value"] 868 | default: 869 | return nil, nil 870 | } 871 | 872 | return op, nil 873 | } 874 | 875 | func parseTag(tag string) (name string, option string) { 876 | parts := strings.Split(tag, ",") 877 | 878 | if len(parts) > 1 { 879 | return parts[0], parts[1] 880 | } 881 | 882 | return parts[0], "" 883 | } 884 | --------------------------------------------------------------------------------