mirror of
https://github.com/retailcrm/api-client-go.git
synced 2024-11-21 20:36:03 +03:00
Correct v2 (#55)
* refactor library and upgrade version * remove useless environment variable * remove unsupported versions from go.mod * fixes for error handling & data types * different improvements for errors * fixes for types and tests * better coverage, fix error with the unmarshalers
This commit is contained in:
parent
269764175e
commit
97b1abf470
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -9,7 +9,6 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
DEVELOPER_NODE: 1
|
||||
RETAILCRM_URL: https://test.retailcrm.pro
|
||||
RETAILCRM_KEY: key
|
||||
|
||||
@ -41,7 +40,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17']
|
||||
go-version: ['1.13', '1.14', '1.15', '1.16', '1.17']
|
||||
include:
|
||||
- go-version: '1.17'
|
||||
coverage: 1
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
vendor
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
|
@ -146,9 +146,9 @@ linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 25
|
||||
goimports:
|
||||
local-prefixes: github.com/retailcrm/api-client-go
|
||||
local-prefixes: github.com/retailcrm/api-client-go/v2
|
||||
lll:
|
||||
line-length: 120
|
||||
line-length: 160
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
@ -180,6 +180,9 @@ issues:
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- godot
|
||||
- gocritic
|
||||
- gosec
|
||||
- staticcheck
|
||||
exclude-use-default: true
|
||||
exclude-case-sensitive: false
|
||||
max-issues-per-linter: 0
|
||||
|
103
README.md
103
README.md
@ -10,51 +10,51 @@
|
||||
|
||||
This is golang RetailCRM API client.
|
||||
|
||||
## Install
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get -x github.com/retailcrm/api-client-go
|
||||
go get -u github.com/retailcrm/api-client-go/v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```golang
|
||||
Example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"log"
|
||||
|
||||
"github.com/retailcrm/api-client-go/v5"
|
||||
"github.com/retailcrm/api-client-go/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var client = v5.New("https://demo.retailcrm.pro", "09jIJ09j0JKhgyfvyuUIKhiugF")
|
||||
var client = retailcrm.New("https://demo.retailcrm.pro", "09jIJ09j0JKhgyfvyuUIKhiugF")
|
||||
|
||||
data, status, err := client.Orders(v5.OrdersRequest{
|
||||
Filter: v5.OrdersFilter{},
|
||||
data, status, err := client.Orders(retailcrm.OrdersRequest{
|
||||
Filter: retailcrm.OrdersFilter{},
|
||||
Limit: 20,
|
||||
Page: 1,
|
||||
},)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err.Error())
|
||||
if apiErr, ok := retailcrm.AsAPIError(err); ok {
|
||||
log.Fatalf("http status: %d, %s", status, apiErr.String())
|
||||
}
|
||||
|
||||
if status >= http.StatusBadRequest {
|
||||
fmt.Printf("%v", err.ApiError())
|
||||
log.Fatalf("http status: %d, error: %s", status, err)
|
||||
}
|
||||
|
||||
for _, value := range data.Orders {
|
||||
fmt.Printf("%v\n", value.Email)
|
||||
log.Printf("%v\n", value.Email)
|
||||
}
|
||||
|
||||
fmt.Println(data.Orders[1].FirstName)
|
||||
log.Println(data.Orders[1].FirstName)
|
||||
|
||||
idata, status, err := c.InventoriesUpload(
|
||||
[]InventoryUpload{
|
||||
inventories, status, err := client.InventoriesUpload([]retailcrm.InventoryUpload{
|
||||
{
|
||||
XMLID: "pTKIKAeghYzX21HTdzFCe1",
|
||||
Stores: []InventoryUploadStore{
|
||||
Stores: []retailcrm.InventoryUploadStore{
|
||||
{
|
||||
Code: "test-store-v5",
|
||||
Available: 10,
|
||||
@ -74,7 +74,7 @@ func main() {
|
||||
},
|
||||
{
|
||||
XMLID: "JQIvcrCtiSpOV3AAfMiQB3",
|
||||
Stores: []InventoryUploadStore{
|
||||
Stores: []retailcrm.InventoryUploadStore{
|
||||
{
|
||||
Code: "test-store-v5",
|
||||
Available: 45,
|
||||
@ -95,13 +95,70 @@ func main() {
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err.Error())
|
||||
if apiErr, ok := retailcrm.AsAPIError(err); ok {
|
||||
log.Fatalf("http status: %d, %s", status, apiErr.String())
|
||||
}
|
||||
|
||||
if status >= http.StatusBadRequest {
|
||||
fmt.Printf("%v", err.ApiError())
|
||||
log.Fatalf("http status: %d, error: %s", status, err)
|
||||
}
|
||||
|
||||
fmt.Println(idata.processedOffersCount)
|
||||
log.Println(inventories.ProcessedOffersCount)
|
||||
}
|
||||
```
|
||||
|
||||
You can use different error types and `retailcrm.AsAPIError` to process client errors. Example:
|
||||
|
||||
```go
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/retailcrm/api-client-go/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var client = retailcrm.New("https://demo.retailcrm.pro", "09jIJ09j0JKhgyfvyuUIKhiugF")
|
||||
|
||||
resp, status, err := client.APICredentials()
|
||||
if err != nil {
|
||||
apiErr, ok := retailcrm.AsAPIError(err)
|
||||
if !ok {
|
||||
log.Fatalf("http status: %d, error: %s", status, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(apiErr, retailcrm.ErrMissingCredentials):
|
||||
log.Fatalln("No API key provided.")
|
||||
case errors.Is(apiErr, retailcrm.ErrInvalidCredentials):
|
||||
log.Fatalln("Invalid API key.")
|
||||
case errors.Is(apiErr, retailcrm.ErrAccessDenied):
|
||||
log.Fatalln("Access denied. Please check that the provided key has access to the credentials info.")
|
||||
case errors.Is(apiErr, retailcrm.ErrAccountDoesNotExist):
|
||||
log.Fatalln("There is no RetailCRM at the provided URL.")
|
||||
case errors.Is(apiErr, retailcrm.ErrMissingParameter):
|
||||
// retailcrm.APIError in this case will always contain "Name" key in the errors list with the parameter name.
|
||||
log.Fatalln("This parameter should be present:", apiErr.Errors()["Name"])
|
||||
case errors.Is(apiErr, retailcrm.ErrValidation):
|
||||
log.Println("Validation errors from the API:")
|
||||
|
||||
for name, value := range apiErr.Errors() {
|
||||
log.Printf(" - %s: %s\n", name, value)
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
case errors.Is(apiErr, retailcrm.ErrGeneric):
|
||||
log.Fatalf("failure from the API. %s", apiErr.String())
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Available scopes:", strings.Join(resp.Scopes, ", "))
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
Please check the [UPGRADING.md](UPGRADING.md) to learn how to upgrade to the new version.
|
||||
|
70
UPGRADING.md
Normal file
70
UPGRADING.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Upgrading to the v2
|
||||
|
||||
### Install the new version
|
||||
|
||||
```bash
|
||||
go get -u github.com/retailcrm/api-client-go/v2
|
||||
```
|
||||
|
||||
### Update all imports
|
||||
|
||||
Before:
|
||||
```go
|
||||
package main
|
||||
|
||||
import v5 "github.com/retailcrm/api-client-go/v5"
|
||||
```
|
||||
|
||||
After:
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/retailcrm/api-client-go/v2"
|
||||
```
|
||||
|
||||
You can use package alias `v5` to skip the second step.
|
||||
|
||||
### Replace package name for all imported symbols
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import v5 "github.com/retailcrm/api-client-go/v5"
|
||||
|
||||
func main() {
|
||||
client := v5.New("https://test.retailcrm.pro", "key")
|
||||
data, status, err := client.Orders(v5.OrdersRequest{
|
||||
Filter: v5.OrdersFilter{
|
||||
City: "Moscow",
|
||||
},
|
||||
Page: 1,
|
||||
})
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/retailcrm/api-client-go/v2"
|
||||
|
||||
func main() {
|
||||
client := retailcrm.New("https://test.retailcrm.pro", "key")
|
||||
data, status, err := client.Orders(retailcrm.OrdersRequest{
|
||||
Filter: retailcrm.OrdersFilter{
|
||||
City: "Moscow",
|
||||
},
|
||||
Page: 1,
|
||||
})
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Upgrade client usages
|
||||
|
||||
This major release contains some breaking changes regarding field names and fully redesigned error handling. Use the second example from
|
||||
the readme to learn how to process errors correctly.
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
package v5
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -20,15 +20,13 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("DEVELOPER_NODE") == "1" {
|
||||
err := godotenv.Load("../.env")
|
||||
err := godotenv.Load(".env")
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
@ -494,7 +492,7 @@ func TestClient_CustomersUpload_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(460).
|
||||
BodyString(`{"success": false, "errorMsg": "Customers are loaded with errors"}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Customers are loaded with ErrorsList"}`)
|
||||
|
||||
data, _, err := c.CustomersUpload(customers)
|
||||
if err == nil {
|
||||
@ -631,7 +629,7 @@ func TestClient_CustomersFixExternalIds_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"id": "ID must be an integer"}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"id": "ID must be an integer"}}`)
|
||||
|
||||
data, _, err := c.CustomersFixExternalIds(customers)
|
||||
if err == nil {
|
||||
@ -690,7 +688,7 @@ func TestClient_CustomersHistory_Fail(t *testing.T) {
|
||||
Get("/customers/history").
|
||||
MatchParam("filter[startDate]", "2020-13-12").
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[startDate]": "Значение недопустимо."}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[startDate]": "Значение недопустимо."}}`)
|
||||
|
||||
data, _, err := c.CustomersHistory(f)
|
||||
if err == nil {
|
||||
@ -885,7 +883,7 @@ func TestClient_CorporateCustomersFixExternalIds_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"id": "ID must be an integer"}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"id": "ID must be an integer"}}`)
|
||||
|
||||
data, _, err := c.CorporateCustomersFixExternalIds(customers)
|
||||
if err == nil {
|
||||
@ -944,7 +942,7 @@ func TestClient_CorporateCustomersHistory_Fail(t *testing.T) {
|
||||
Get("/customers-corporate/history").
|
||||
MatchParam("filter[startDate]", "2020-13-12").
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[startDate]": "Значение недопустимо."}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[startDate]": "Значение недопустимо."}}`)
|
||||
|
||||
data, _, err := c.CorporateCustomersHistory(f)
|
||||
if err == nil {
|
||||
@ -1130,7 +1128,7 @@ func TestClient_CorporateCustomersUpload_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(460).
|
||||
BodyString(`{"success": false, "errorMsg": "Customers are loaded with errors"}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Customers are loaded with ErrorsList"}`)
|
||||
|
||||
data, _, err := c.CorporateCustomersUpload(customers)
|
||||
if err == nil {
|
||||
@ -1703,7 +1701,7 @@ func TestClient_NotesNotes_Fail(t *testing.T) {
|
||||
Get("/customers/notes").
|
||||
MatchParam("filter[createdAtFrom]", "2020-13-12").
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[createdAtFrom]": "This value is not valid."}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[createdAtFrom]": "This value is not valid."}}`)
|
||||
|
||||
data, _, err := c.CustomerNotes(NotesRequest{
|
||||
Filter: NotesFilter{CreatedAtFrom: "2020-13-12"},
|
||||
@ -1803,7 +1801,7 @@ func TestClient_NotesCreateDelete_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "errors": {"customer": "Set one of the following fields: id, externalId"}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "ErrorsList": {"customer": "Set one of the following fields: id, externalId"}}`)
|
||||
|
||||
data, _, err := c.CustomerNoteCreate(note)
|
||||
if err == nil {
|
||||
@ -1875,7 +1873,7 @@ func TestClient_OrdersOrders_Fail(t *testing.T) {
|
||||
Get("/orders").
|
||||
MatchParam("filter[attachments]", "7").
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[attachments]": "SThis value is not valid."}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[attachments]": "SThis value is not valid."}}`)
|
||||
|
||||
data, _, err := c.Orders(OrdersRequest{Filter: OrdersFilter{Attachments: 7}})
|
||||
if err == nil {
|
||||
@ -2166,7 +2164,7 @@ func TestClient_OrdersUpload_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(460).
|
||||
BodyString(`{"success": false, "errorMsg": "Orders are loaded with errors"}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Orders are loaded with ErrorsList"}`)
|
||||
|
||||
data, _, err := c.OrdersUpload(orders)
|
||||
if err == nil {
|
||||
@ -2395,7 +2393,7 @@ func TestClient_OrdersHistory_Fail(t *testing.T) {
|
||||
Get("/orders/history").
|
||||
MatchParam("filter[startDate]", "2020-13-12").
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[startDate]": "Значение недопустимо."}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[startDate]": "Значение недопустимо."}}`)
|
||||
|
||||
data, _, err := c.OrdersHistory(OrdersHistoryRequest{Filter: OrdersHistoryFilter{StartDate: "2020-13-12"}})
|
||||
if err == nil {
|
||||
@ -2521,7 +2519,7 @@ func TestClient_PaymentCreateEditDelete_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "errors": {"order": "Set one of the following fields: id, externalId, number"}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "ErrorsList": {"order": "Set one of the following fields: id, externalId, number"}}`)
|
||||
|
||||
data, _, err := c.OrderPaymentCreate(f)
|
||||
if err == nil {
|
||||
@ -2743,7 +2741,7 @@ func TestClient_TaskChange_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Task is not loaded", "errors": {"performerId": "This value should not be blank."}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Task is not loaded", "ErrorsList": {"performerId": "This value should not be blank."}}`)
|
||||
|
||||
data, _, err := c.TaskEdit(f)
|
||||
if err == nil {
|
||||
@ -2792,7 +2790,7 @@ func TestClient_UsersUsers_Fail(t *testing.T) {
|
||||
MatchParam("filter[active]", "3").
|
||||
MatchParam("page", "1").
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"active": "he value you selected is not a valid choice."}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"active": "he value you selected is not a valid choice."}}`)
|
||||
|
||||
data, _, err := c.Users(UsersRequest{Filter: UsersFilter{Active: 3}, Page: 1})
|
||||
if err == nil {
|
||||
@ -3550,8 +3548,8 @@ func TestClient_Courier(t *testing.T) {
|
||||
cur := Courier{
|
||||
Active: true,
|
||||
Email: fmt.Sprintf("%s@example.com", RandomString(5)),
|
||||
FirstName: fmt.Sprintf("%s", RandomString(5)),
|
||||
LastName: fmt.Sprintf("%s", RandomString(5)),
|
||||
FirstName: RandomString(5),
|
||||
LastName: RandomString(5),
|
||||
}
|
||||
|
||||
defer gock.Off()
|
||||
@ -3583,7 +3581,7 @@ func TestClient_Courier(t *testing.T) {
|
||||
}
|
||||
|
||||
cur.ID = data.ID
|
||||
cur.Patronymic = fmt.Sprintf("%s", RandomString(5))
|
||||
cur.Patronymic = RandomString(5)
|
||||
|
||||
jr, _ = json.Marshal(&cur)
|
||||
|
||||
@ -3629,7 +3627,7 @@ func TestClient_Courier_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(400).
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "errors": {"firstName": "Specify the first name"}}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "ErrorsList": {"firstName": "Specify the first name"}}`)
|
||||
|
||||
data, st, err := c.CourierCreate(Courier{})
|
||||
if err == nil {
|
||||
@ -3644,7 +3642,7 @@ func TestClient_Courier_Fail(t *testing.T) {
|
||||
t.Error(successFail)
|
||||
}
|
||||
|
||||
cur := Courier{Patronymic: fmt.Sprintf("%s", RandomString(5))}
|
||||
cur := Courier{Patronymic: RandomString(5)}
|
||||
jr, _ = json.Marshal(&cur)
|
||||
|
||||
p = url.Values{
|
||||
@ -5793,7 +5791,7 @@ func TestClient_CostsUpload_Fail(t *testing.T) {
|
||||
MatchType("url").
|
||||
BodyString(p.Encode()).
|
||||
Reply(460).
|
||||
BodyString(`{"success": false, "errorMsg": "Costs are loaded with errors"}`)
|
||||
BodyString(`{"success": false, "errorMsg": "Costs are loaded with ErrorsList"}`)
|
||||
|
||||
data, _, err := c.CostsUpload(costsUpload)
|
||||
if err == nil {
|
192
error.go
Normal file
192
error.go
Normal file
@ -0,0 +1,192 @@
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var missingParameterMatcher = regexp.MustCompile(`^Parameter \'([\w\]\[\_\-]+)\' is missing$`)
|
||||
var (
|
||||
// ErrMissingCredentials will be returned if no API key was provided to the API.
|
||||
ErrMissingCredentials = NewAPIError(`apiKey is missing`)
|
||||
// ErrInvalidCredentials will be returned if provided API key is invalid.
|
||||
ErrInvalidCredentials = NewAPIError(`wrong "apiKey" value`)
|
||||
// ErrAccessDenied will be returned in case of "Access denied" error.
|
||||
ErrAccessDenied = NewAPIError("access denied")
|
||||
// ErrAccountDoesNotExist will be returned if target system does not exist.
|
||||
ErrAccountDoesNotExist = NewAPIError("account does not exist")
|
||||
// ErrValidation will be returned in case of validation errors.
|
||||
ErrValidation = NewAPIError("validation error")
|
||||
// ErrMissingParameter will be returned if parameter is missing.
|
||||
// Underlying error messages list will contain parameter name in the "Name" key.
|
||||
ErrMissingParameter = NewAPIError("missing parameter")
|
||||
// ErrGeneric will be returned if error cannot be classified as one of the errors above.
|
||||
ErrGeneric = NewAPIError("API error")
|
||||
)
|
||||
|
||||
// APIErrorsList struct.
|
||||
type APIErrorsList map[string]string
|
||||
|
||||
// APIError returns when an API error was occurred.
|
||||
type APIError interface {
|
||||
error
|
||||
fmt.Stringer
|
||||
withWrapped(error) APIError
|
||||
withErrors(APIErrorsList) APIError
|
||||
Unwrap() error
|
||||
Errors() APIErrorsList
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
ErrorMsg string `json:"errorMsg,omitempty"`
|
||||
ErrorsList APIErrorsList `json:"errors,omitempty"`
|
||||
wrapped error
|
||||
}
|
||||
|
||||
// CreateAPIError from the provided response data. Different error types will be returned depending on the response,
|
||||
// all of them can be matched using errors.Is. APi errors will always implement APIError interface.
|
||||
func CreateAPIError(dataResponse []byte) error {
|
||||
a := &apiError{}
|
||||
|
||||
if len(dataResponse) > 0 && dataResponse[0] == '<' {
|
||||
return ErrAccountDoesNotExist
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(dataResponse, &a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var found APIError
|
||||
switch a.ErrorMsg {
|
||||
case `"apiKey" is missing.`:
|
||||
found = ErrMissingCredentials
|
||||
case `Wrong "apiKey" value.`:
|
||||
found = ErrInvalidCredentials
|
||||
case "Access denied.":
|
||||
found = ErrAccessDenied
|
||||
case "Account does not exist.":
|
||||
found = ErrAccountDoesNotExist
|
||||
case "Errors in the entity format":
|
||||
fallthrough
|
||||
case "Validation error":
|
||||
found = ErrValidation
|
||||
default:
|
||||
if param, ok := asMissingParameterErr(a.ErrorMsg); ok {
|
||||
return a.withWrapped(ErrMissingParameter).withErrors(APIErrorsList{"Name": param})
|
||||
}
|
||||
found = ErrGeneric
|
||||
}
|
||||
|
||||
result := NewAPIError(a.ErrorMsg).withWrapped(found)
|
||||
if len(a.ErrorsList) > 0 {
|
||||
return result.withErrors(a.ErrorsList)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CreateGenericAPIError for the situations when API response cannot be processed, but response was actually received.
|
||||
func CreateGenericAPIError(message string) APIError {
|
||||
return NewAPIError(message).withWrapped(ErrGeneric)
|
||||
}
|
||||
|
||||
// NewAPIError returns API error with the provided message.
|
||||
func NewAPIError(message string) APIError {
|
||||
return &apiError{ErrorMsg: message}
|
||||
}
|
||||
|
||||
// AsAPIError returns APIError and true if provided error is an APIError or contains wrapped APIError.
|
||||
// Returns (nil, false) otherwise.
|
||||
func AsAPIError(err error) (APIError, bool) {
|
||||
apiErr := unwrapAPIError(err)
|
||||
return apiErr, apiErr != nil
|
||||
}
|
||||
|
||||
func unwrapAPIError(err error) APIError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if apiErr, ok := err.(APIError); ok { // nolint:errorlint
|
||||
return apiErr
|
||||
}
|
||||
|
||||
wrapper, ok := err.(interface { // nolint:errorlint
|
||||
Unwrap() error
|
||||
})
|
||||
if ok {
|
||||
return unwrapAPIError(wrapper.Unwrap())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// asMissingParameterErr returns true if "Parameter 'name' is missing" error message is provided.
|
||||
func asMissingParameterErr(message string) (string, bool) {
|
||||
matches := missingParameterMatcher.FindAllStringSubmatch(message, -1)
|
||||
if len(matches) == 1 && len(matches[0]) == 2 {
|
||||
return matches[0][1], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Error returns errorMsg field from the response.
|
||||
func (e *apiError) Error() string {
|
||||
return e.ErrorMsg
|
||||
}
|
||||
|
||||
// Unwrap returns wrapped error. It is usually one of the predefined types like ErrGeneric or ErrValidation.
|
||||
// It can be used directly, but it's main purpose is to make errors matchable via errors.Is call.
|
||||
func (e *apiError) Unwrap() error {
|
||||
return e.wrapped
|
||||
}
|
||||
|
||||
// Errors returns errors field from the response.
|
||||
func (e *apiError) Errors() APIErrorsList {
|
||||
return e.ErrorsList
|
||||
}
|
||||
|
||||
// String returns string representation of an APIError.
|
||||
func (e *apiError) String() string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(256) // nolint:gomnd
|
||||
sb.WriteString(fmt.Sprintf(`errorMsg: "%s"`, e.Error()))
|
||||
|
||||
if len(e.Errors()) > 0 {
|
||||
i := 0
|
||||
useIndex := true
|
||||
errorList := make([]string, len(e.Errors()))
|
||||
|
||||
for index, errText := range e.Errors() {
|
||||
if i == 0 && index == "0" {
|
||||
useIndex = false
|
||||
}
|
||||
|
||||
if useIndex {
|
||||
errorList[i] = fmt.Sprintf(`%s: "%s"`, index, errText)
|
||||
} else {
|
||||
errorList[i] = errText
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
sb.WriteString(", errors: [" + strings.Join(errorList, ", ") + "]")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// withWrapped is a wrapped setter.
|
||||
func (e *apiError) withWrapped(err error) APIError {
|
||||
e.wrapped = err
|
||||
return e
|
||||
}
|
||||
|
||||
// withErrors is an ErrorsList setter.
|
||||
func (e *apiError) withErrors(m APIErrorsList) APIError {
|
||||
e.ErrorsList = m
|
||||
return e
|
||||
}
|
171
error_test.go
Normal file
171
error_test.go
Normal file
@ -0,0 +1,171 @@
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ErrorTest struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
suite.Run(t, new(ErrorTest))
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_ApiErrorsSlice() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Failed to activate module",
|
||||
"errors": [
|
||||
"Your account has insufficient funds to activate integration module",
|
||||
"Test error"
|
||||
]}`)
|
||||
expected := APIErrorsList{
|
||||
"0": "Your account has insufficient funds to activate integration module",
|
||||
"1": "Test error",
|
||||
}
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().ErrorIs(e, ErrGeneric)
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Assert().Equal(expected, apiErr.Errors())
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_ApiErrorsMap() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Failed to activate module",
|
||||
"errors": {"id": "ID must be an integer", "test": "Test error"}}`,
|
||||
)
|
||||
expected := APIErrorsList{
|
||||
"id": "ID must be an integer",
|
||||
"test": "Test error",
|
||||
}
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().ErrorIs(e, ErrGeneric)
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Assert().Equal(expected, apiErr.Errors())
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_APIKeyMissing() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "\"apiKey\" is missing."}`,
|
||||
)
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Require().ErrorIs(e, ErrMissingCredentials)
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_APIKeyWrong() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Wrong \"apiKey\" value."}`,
|
||||
)
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Require().ErrorIs(e, ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_AccessDenied() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Access denied."}`,
|
||||
)
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Require().ErrorIs(e, ErrAccessDenied)
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_AccountDoesNotExist() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Account does not exist."}`,
|
||||
)
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Require().ErrorIs(e, ErrAccountDoesNotExist)
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_Validation() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Errors in the entity format",
|
||||
"errors": {"name": "name must be provided"}}`,
|
||||
)
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Require().ErrorIs(e, ErrValidation)
|
||||
t.Assert().Equal("name must be provided", apiErr.Errors()["name"])
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_Validation2() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Validation error",
|
||||
"errors": {"name": "name must be provided"}}`,
|
||||
)
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Require().ErrorIs(e, ErrValidation)
|
||||
t.Assert().Equal("name must be provided", apiErr.Errors()["name"])
|
||||
t.Assert().Equal("errorMsg: \"Validation error\", errors: [name: \"name must be provided\"]", apiErr.String())
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_MissingParameter() {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Parameter 'item' is missing"}`,
|
||||
)
|
||||
|
||||
e := CreateAPIError(b)
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Require().ErrorIs(e, ErrMissingParameter)
|
||||
t.Assert().Equal("item", apiErr.Errors()["Name"])
|
||||
}
|
||||
|
||||
func (t *ErrorTest) Test_CreateGenericAPIError() {
|
||||
e := CreateGenericAPIError("generic error message")
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Assert().ErrorIs(apiErr, ErrGeneric)
|
||||
t.Assert().Equal("generic error message", e.Error())
|
||||
}
|
||||
|
||||
func (t *ErrorTest) TestFailure_HTML() {
|
||||
e := CreateAPIError([]byte{'<'})
|
||||
apiErr, ok := AsAPIError(e)
|
||||
|
||||
t.Require().NotNil(apiErr)
|
||||
t.Require().True(ok)
|
||||
t.Assert().ErrorIs(apiErr, ErrAccountDoesNotExist)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package v5
|
||||
package retailcrm
|
||||
|
||||
// CustomersFilter type.
|
||||
type CustomersFilter struct {
|
6
go.mod
6
go.mod
@ -1,12 +1,10 @@
|
||||
module github.com/retailcrm/api-client-go
|
||||
module github.com/retailcrm/api-client-go/v2
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/google/go-querystring v1.1.0
|
||||
github.com/joho/godotenv v1.3.0
|
||||
golang.org/x/net v0.0.0-20191021144547-ec77196f6094 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
|
||||
gopkg.in/h2non/gentleman.v1 v1.0.4 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
)
|
||||
|
18
go.sum
18
go.sum
@ -1,3 +1,5 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
@ -8,14 +10,16 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/h2non/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg=
|
||||
gopkg.in/h2non/gock.v1 v1.0.16 h1:F11k+OafeuFENsjei5t2vMTSTs9L62AdyTe4E1cgdG8=
|
||||
gopkg.in/h2non/gock.v1 v1.0.16/go.mod h1:XVuDAssexPLwgxCLMvDTWNU5eqklsydR6I5phZ9oPB8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
25
log.go
Normal file
25
log.go
Normal file
@ -0,0 +1,25 @@
|
||||
package retailcrm
|
||||
|
||||
// BasicLogger provides basic functionality for logging.
|
||||
type BasicLogger interface {
|
||||
Printf(string, ...interface{})
|
||||
}
|
||||
|
||||
// DebugLogger can be used to easily wrap any logger with Debugf method into the BasicLogger instance.
|
||||
type DebugLogger interface {
|
||||
Debugf(string, ...interface{})
|
||||
}
|
||||
|
||||
type debugLoggerAdapter struct {
|
||||
logger DebugLogger
|
||||
}
|
||||
|
||||
// DebugLoggerAdapter returns BasicLogger that calls underlying DebugLogger.Debugf.
|
||||
func DebugLoggerAdapter(logger DebugLogger) BasicLogger {
|
||||
return &debugLoggerAdapter{logger: logger}
|
||||
}
|
||||
|
||||
// Printf data in the log using DebugLogger.Debugf.
|
||||
func (l *debugLoggerAdapter) Printf(format string, v ...interface{}) {
|
||||
l.logger.Debugf(format, v...)
|
||||
}
|
24
log_test.go
Normal file
24
log_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type wrappedLogger struct {
|
||||
lastMessage string
|
||||
}
|
||||
|
||||
func (w *wrappedLogger) Debugf(msg string, v ...interface{}) {
|
||||
w.lastMessage = fmt.Sprintf(msg, v...)
|
||||
}
|
||||
|
||||
func TestDebugLoggerAdapter_Printf(t *testing.T) {
|
||||
wrapped := &wrappedLogger{}
|
||||
logger := DebugLoggerAdapter(wrapped)
|
||||
logger.Printf("Test message #%d", 1)
|
||||
|
||||
assert.Equal(t, "Test message #1", wrapped.lastMessage)
|
||||
}
|
102
marshaling.go
Normal file
102
marshaling.go
Normal file
@ -0,0 +1,102 @@
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (t Tag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.Name)
|
||||
}
|
||||
|
||||
func (a *APIErrorsList) UnmarshalJSON(data []byte) error {
|
||||
var i interface{}
|
||||
var m APIErrorsList
|
||||
if err := json.Unmarshal(data, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch e := i.(type) {
|
||||
case map[string]interface{}:
|
||||
m = make(APIErrorsList, len(e))
|
||||
for idx, val := range e {
|
||||
m[idx] = fmt.Sprint(val)
|
||||
}
|
||||
case []interface{}:
|
||||
m = make(APIErrorsList, len(e))
|
||||
for idx, val := range e {
|
||||
m[strconv.Itoa(idx)] = fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
*a = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *CustomFieldsList) UnmarshalJSON(data []byte) error {
|
||||
var i interface{}
|
||||
var m CustomFieldsList
|
||||
if err := json.Unmarshal(data, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch e := i.(type) {
|
||||
case map[string]interface{}:
|
||||
m = make(CustomFieldsList, len(e))
|
||||
for idx, val := range e {
|
||||
m[idx] = fmt.Sprint(val)
|
||||
}
|
||||
case []interface{}:
|
||||
m = make(CustomFieldsList, len(e))
|
||||
for idx, val := range e {
|
||||
m[strconv.Itoa(idx)] = fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
*l = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderPayments) UnmarshalJSON(data []byte) error {
|
||||
var i interface{}
|
||||
var m OrderPayments
|
||||
if err := json.Unmarshal(data, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch e := i.(type) {
|
||||
case map[string]interface{}:
|
||||
m = make(OrderPayments, len(e))
|
||||
for idx, val := range e {
|
||||
var res OrderPayment
|
||||
err := unmarshalMap(val.(map[string]interface{}), &res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m[idx] = res
|
||||
}
|
||||
case []interface{}:
|
||||
m = make(OrderPayments, len(e))
|
||||
for idx, val := range e {
|
||||
var res OrderPayment
|
||||
err := unmarshalMap(val.(map[string]interface{}), &res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m[strconv.Itoa(idx)] = res
|
||||
}
|
||||
}
|
||||
|
||||
*p = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalMap(m map[string]interface{}, v interface{}) (err error) {
|
||||
var data []byte
|
||||
data, err = json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
69
marshaling_test.go
Normal file
69
marshaling_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTag_MarshalJSON(t *testing.T) {
|
||||
tags := []Tag{
|
||||
{"first", "#3e89b6", false},
|
||||
{"second", "#ffa654", false},
|
||||
}
|
||||
names := []byte(`["first","second"]`)
|
||||
str, err := json.Marshal(tags)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%v", err.Error())
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(str, names) {
|
||||
t.Errorf("Marshaled: %#v\nExpected: %#v\n", str, names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIErrorsList_UnmarshalJSON(t *testing.T) {
|
||||
var list APIErrorsList
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(`["first", "second"]`), &list))
|
||||
assert.Len(t, list, 2)
|
||||
assert.Equal(t, list["0"], "first")
|
||||
assert.Equal(t, list["1"], "second")
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"a": "first", "b": "second"}`), &list))
|
||||
assert.Len(t, list, 2)
|
||||
assert.Equal(t, list["a"], "first")
|
||||
assert.Equal(t, list["b"], "second")
|
||||
}
|
||||
|
||||
func TestCustomFieldsList_UnmarshalJSON(t *testing.T) {
|
||||
var list CustomFieldsList
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(`["first", "second"]`), &list))
|
||||
assert.Len(t, list, 2)
|
||||
assert.Equal(t, list["0"], "first")
|
||||
assert.Equal(t, list["1"], "second")
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"a": "first", "b": "second"}`), &list))
|
||||
assert.Len(t, list, 2)
|
||||
assert.Equal(t, list["a"], "first")
|
||||
assert.Equal(t, list["b"], "second")
|
||||
}
|
||||
|
||||
func TestOrderPayments_UnmarshalJSON(t *testing.T) {
|
||||
var list OrderPayments
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(`[{"id": 1}, {"id": 2}]`), &list))
|
||||
assert.Len(t, list, 2)
|
||||
assert.Equal(t, list["0"], OrderPayment{ID: 1})
|
||||
assert.Equal(t, list["1"], OrderPayment{ID: 2})
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"a": {"id": 1}, "b": {"id": 2}}`), &list))
|
||||
assert.Len(t, list, 2)
|
||||
assert.Equal(t, list["a"], OrderPayment{ID: 1})
|
||||
assert.Equal(t, list["b"], OrderPayment{ID: 2})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package v5
|
||||
package retailcrm
|
||||
|
||||
// CustomerRequest type.
|
||||
type CustomerRequest struct {
|
||||
@ -177,8 +177,8 @@ type ProductsPropertiesRequest struct {
|
||||
type DeliveryTrackingRequest struct {
|
||||
DeliveryID string `json:"deliveryId,omitempty"`
|
||||
TrackNumber string `json:"trackNumber,omitempty"`
|
||||
History []DeliveryHistoryRecord `json:"history,omitempty,brackets"`
|
||||
ExtraData map[string]string `json:"extraData,omitempty,brackets"`
|
||||
History []DeliveryHistoryRecord `json:"history,omitempty"`
|
||||
ExtraData map[string]string `json:"extraData,omitempty"`
|
||||
}
|
||||
|
||||
// DeliveryShipmentsRequest type.
|
@ -1,4 +1,4 @@
|
||||
package v5
|
||||
package retailcrm
|
||||
|
||||
// SuccessfulResponse type.
|
||||
type SuccessfulResponse struct {
|
||||
@ -20,7 +20,7 @@ type OrderCreateResponse struct {
|
||||
// OperationResponse type.
|
||||
type OperationResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Errors map[string]string `json:"errors,omitempty"`
|
||||
Errors map[string]string `json:"ErrorsList,omitempty"`
|
||||
}
|
||||
|
||||
// VersionResponse return available API versions.
|
||||
@ -409,7 +409,7 @@ type ResponseInfo struct {
|
||||
|
||||
// MgInfo type.
|
||||
type MgInfo struct {
|
||||
EndpointUrl string `json:"endpointUrl"`
|
||||
EndpointURL string `json:"endpointUrl"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package v5
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -19,6 +19,7 @@ type Client struct {
|
||||
Key string
|
||||
Debug bool
|
||||
httpClient *http.Client
|
||||
logger BasicLogger
|
||||
}
|
||||
|
||||
// Pagination type.
|
||||
@ -154,7 +155,7 @@ type Customer struct {
|
||||
BrowserID string `json:"browserId,omitempty"`
|
||||
MgCustomerID string `json:"mgCustomerId,omitempty"`
|
||||
PhotoURL string `json:"photoUrl,omitempty"`
|
||||
CustomFields map[string]string `json:"customFields,omitempty"`
|
||||
CustomFields CustomFieldsList `json:"customFields,omitempty"`
|
||||
Tags []Tag `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
@ -166,7 +167,7 @@ type CorporateCustomer struct {
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
Vip bool `json:"vip,omitempty"`
|
||||
Bad bool `json:"bad,omitempty"`
|
||||
CustomFields map[string]string `json:"customFields,omitempty"`
|
||||
CustomFields CustomFieldsList `json:"customFields,omitempty"`
|
||||
PersonalDiscount float32 `json:"personalDiscount,omitempty"`
|
||||
DiscountCardNumber string `json:"discountCardNumber,omitempty"`
|
||||
ManagerID int `json:"managerId,omitempty"`
|
||||
@ -227,7 +228,7 @@ type Company struct {
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
Contragent *Contragent `json:"contragent,omitempty"`
|
||||
Address *IdentifiersPair `json:"address,omitempty"`
|
||||
CustomFields map[string]string `json:"customFields,omitempty"`
|
||||
CustomFields CustomFieldsList `json:"customFields,omitempty"`
|
||||
}
|
||||
|
||||
// CorporateCustomerNote type.
|
||||
@ -272,6 +273,9 @@ type CorporateCustomerHistoryRecord struct {
|
||||
Order related types
|
||||
*/
|
||||
|
||||
type OrderPayments map[string]OrderPayment
|
||||
type CustomFieldsList map[string]string
|
||||
|
||||
// Order type.
|
||||
type Order struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
@ -321,8 +325,8 @@ type Order struct {
|
||||
Delivery *OrderDelivery `json:"delivery,omitempty"`
|
||||
Marketplace *OrderMarketplace `json:"marketplace,omitempty"`
|
||||
Items []OrderItem `json:"items,omitempty"`
|
||||
CustomFields map[string]string `json:"customFields,omitempty"`
|
||||
Payments map[string]OrderPayment `json:"payments,omitempty"`
|
||||
CustomFields CustomFieldsList `json:"customFields,omitempty"`
|
||||
Payments OrderPayments `json:"payments,omitempty"`
|
||||
}
|
||||
|
||||
// OrdersStatus type.
|
||||
@ -378,8 +382,14 @@ type OrderDeliveryData struct {
|
||||
// UnmarshalJSON method.
|
||||
func (v *OrderDeliveryData) UnmarshalJSON(b []byte) error {
|
||||
var additionalData map[string]interface{}
|
||||
json.Unmarshal(b, &additionalData)
|
||||
json.Unmarshal(b, &v.OrderDeliveryDataBasic)
|
||||
err := json.Unmarshal(b, &additionalData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(b, &v.OrderDeliveryDataBasic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object := reflect.TypeOf(v.OrderDeliveryDataBasic)
|
||||
|
||||
for i := 0; i < object.NumField(); i++ {
|
||||
@ -401,7 +411,10 @@ func (v *OrderDeliveryData) UnmarshalJSON(b []byte) error {
|
||||
func (v OrderDeliveryData) MarshalJSON() ([]byte, error) {
|
||||
result := map[string]interface{}{}
|
||||
data, _ := json.Marshal(v.OrderDeliveryDataBasic)
|
||||
json.Unmarshal(data, &result)
|
||||
err := json.Unmarshal(data, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range v.AdditionalFields {
|
||||
result[key] = value
|
||||
@ -587,7 +600,7 @@ type User struct {
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Groups []UserGroup `json:"groups,omitempty"`
|
||||
MgUserId uint64 `json:"mgUserId,omitempty"`
|
||||
MGUserID uint64 `json:"mgUserId,omitempty"`
|
||||
}
|
||||
|
||||
// UserGroup type.
|
||||
@ -1054,7 +1067,7 @@ type Action struct {
|
||||
|
||||
// MgTransport type.
|
||||
type MgTransport struct {
|
||||
WebhookUrl string `json:"webhookUrl,omitempty"`
|
||||
WebhookURL string `json:"webhookUrl,omitempty"`
|
||||
}
|
||||
|
||||
// MgBot type.
|
||||
@ -1072,7 +1085,7 @@ type CostRecord struct {
|
||||
DateTo string `json:"dateTo,omitempty"`
|
||||
Summ float32 `json:"summ,omitempty"`
|
||||
CostItem string `json:"costItem,omitempty"`
|
||||
UserId int `json:"userId,omitempty"`
|
||||
UserID int `json:"userId,omitempty"`
|
||||
Order *Order `json:"order,omitempty"`
|
||||
Sites []string `json:"sites,omitempty"`
|
||||
}
|
||||
@ -1089,7 +1102,7 @@ type Cost struct {
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
CreatedBy string `json:"createdBy,omitempty"`
|
||||
Order *Order `json:"order,omitempty"`
|
||||
UserId int `json:"userId,omitempty"`
|
||||
UserID int `json:"userId,omitempty"`
|
||||
Sites []string `json:"sites,omitempty"`
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package v5
|
||||
package retailcrm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
32
v5/error.go
32
v5/error.go
@ -1,32 +0,0 @@
|
||||
package v5
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// APIErrorsList struct.
|
||||
type APIErrorsList map[string]string
|
||||
|
||||
// APIError struct.
|
||||
type APIError struct {
|
||||
SuccessfulResponse
|
||||
ErrorMsg string `json:"errorMsg,omitempty"`
|
||||
Errors APIErrorsList `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return e.ErrorMsg
|
||||
}
|
||||
|
||||
func NewAPIError(dataResponse []byte) error {
|
||||
a := &APIError{}
|
||||
|
||||
if len(dataResponse) > 0 && dataResponse[0] == '<' {
|
||||
a.ErrorMsg = "Account does not exist."
|
||||
return a
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(dataResponse, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func TestFailure_ApiErrorsSlice(t *testing.T) {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Failed to activate module",
|
||||
"errors": [
|
||||
"Your account has insufficient funds to activate integration module",
|
||||
"Test error"
|
||||
]}`)
|
||||
expected := APIErrorsList{
|
||||
"0": "Your account has insufficient funds to activate integration module",
|
||||
"1": "Test error",
|
||||
}
|
||||
|
||||
var expEr *APIError
|
||||
e := NewAPIError(b)
|
||||
|
||||
if xerrors.As(e, &expEr) {
|
||||
if eq := reflect.DeepEqual(expEr.Errors, expected); eq != true {
|
||||
t.Errorf("%+v", eq)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Error must be type of APIError: %v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailure_ApiErrorsMap(t *testing.T) {
|
||||
b := []byte(`{"success": false,
|
||||
"errorMsg": "Failed to activate module",
|
||||
"errors": {"id": "ID must be an integer", "test": "Test error"}}`,
|
||||
)
|
||||
expected := APIErrorsList{
|
||||
"id": "ID must be an integer",
|
||||
"test": "Test error",
|
||||
}
|
||||
|
||||
var expEr *APIError
|
||||
e := NewAPIError(b)
|
||||
|
||||
if xerrors.As(e, &expEr) {
|
||||
if eq := reflect.DeepEqual(expEr.Errors, expected); eq != true {
|
||||
t.Errorf("%+v", eq)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Error must be type of APIError: %v", e)
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (t Tag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.Name)
|
||||
}
|
||||
|
||||
func (a *APIErrorsList) UnmarshalJSON(data []byte) error {
|
||||
var i interface{}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(data, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch e := i.(type) {
|
||||
case map[string]interface{}:
|
||||
m = make(map[string]string, len(e))
|
||||
for idx, val := range e {
|
||||
m[idx] = fmt.Sprint(val)
|
||||
}
|
||||
case []interface{}:
|
||||
m = make(map[string]string, len(e))
|
||||
for idx, val := range e {
|
||||
m[strconv.Itoa(idx)] = fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
*a = m
|
||||
return nil
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTag_MarshalJSON(t *testing.T) {
|
||||
tags := []Tag{
|
||||
{"first", "#3e89b6", false},
|
||||
{"second", "#ffa654", false},
|
||||
}
|
||||
names := []byte(`["first","second"]`)
|
||||
str, err := json.Marshal(tags)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%v", err.Error())
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(str, names) {
|
||||
t.Errorf("Marshaled: %#v\nExpected: %#v\n", str, names)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user