From 4b7e5a67c78cba0aaf3cacfe5040924640c0cf3b Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Pinalie <2850825+jybp@users.noreply.github.com> Date: Wed, 15 May 2019 16:25:41 +0200 Subject: [PATCH] Setup client --- README.md | 4 +- browse.go | 38 +++++++++++++ browse_test.go | 15 ++++++ ebay.go | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ ebay_test.go | 68 +++++++++++++++++++++++ 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 browse.go create mode 100644 browse_test.go create mode 100644 ebay.go create mode 100644 ebay_test.go diff --git a/README.md b/README.md index ee4197e..e2546fc 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# ebay \ No newline at end of file +# ebay + +ebay is a Go client library for accessing the [eBay API](https://developer.ebay.com/). \ No newline at end of file diff --git a/browse.go b/browse.go new file mode 100644 index 0000000..bafd18c --- /dev/null +++ b/browse.go @@ -0,0 +1,38 @@ +package ebay + +import ( + "context" + "fmt" + "net/http" +) + +// BrowseService handles communication with the Browse API +// +// eBay API docs: https://developer.ebay.com/api-docs/buy/browse/overview.html +type BrowseService service + +// OptContextualLocation adds the header containing contextualLocation. +// It is strongly recommended that you use it when submitting Browse API methods. +// +// eBay API docs: https://developer.ebay.com/api-docs/buy/static/api-browse.html#Headers +func OptContextualLocation(country, zip string) func(*http.Request) { + return func(req *http.Request) { + // X-EBAY-C-ENDUSERCTX: contextualLocation=country=US,zip=19406 + } +} + +// Item represents a eBay item. +type Item struct{} + +// GetItem retrieves the details of a specific item. +// +// eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItem +func (s *BrowseService) GetItem(ctx context.Context, itemID string, opts ...Opt) (Item, error) { + u := fmt.Sprintf("item/%s", itemID) + req, err := s.client.NewRequest(http.MethodGet, u, opts...) + if err != nil { + return Item{}, err + } + var it Item + return it, s.client.Do(ctx, req, &it) +} diff --git a/browse_test.go b/browse_test.go new file mode 100644 index 0000000..9a5aa5b --- /dev/null +++ b/browse_test.go @@ -0,0 +1,15 @@ +package ebay_test + +import ( + "net/http" + "testing" + + "github.com/jybp/ebay" + "github.com/stretchr/testify/assert" +) + +func TestOptContextualLocation(t *testing.T) { + r, _ := http.NewRequest("", "", nil) + ebay.OptContextualLocation("US", "19406")(r) + assert.Equal(t, "country%3DUS%2Czip%3D19406", r.Header.Get("X-EBAY-C-ENDUSERCTX")) +} diff --git a/ebay.go b/ebay.go new file mode 100644 index 0000000..66a83d1 --- /dev/null +++ b/ebay.go @@ -0,0 +1,143 @@ +package ebay + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +const ( + baseURL = "https://api.ebay.com/" + sandboxBaseURL = "https://api.sandbox.ebay.com/" + + headerEndUserCtx = "X-EBAY-C-ENDUSERCTX" +) + +// BuyAPI regroups the eBay Buy APIs. +// +// eBay API docs: https://developer.ebay.com/api-docs/buy/static/buy-landing.html +type BuyAPI struct { + Browse *BrowseService +} + +// Client manages communication with the eBay API. +type Client struct { + client *http.Client // Used to make actual API requests. + baseURL *url.URL // Base URL for API requests. + + // eBay APIs. + Buy BuyAPI +} + +// NewClient returns a new eBay API client. +// If a nil httpClient is provided, http.DefaultClient will be used. +func NewClient(httpclient *http.Client) *Client { + return newClient(httpclient, baseURL) +} + +// NewSandboxClient returns a new eBay sandbox API client. +// If a nil httpClient is provided, http.DefaultClient will be used. +func NewSandboxClient(httpclient *http.Client) *Client { + return newClient(httpclient, sandboxBaseURL) +} + +// NewCustomClient returns a new custom eBay API client. +// BaseURL should have a trailing slash. +// If a nil httpClient is provided, http.DefaultClient will be used. +func NewCustomClient(httpclient *http.Client, baseURL string) (*Client, error) { + if !strings.HasSuffix(baseURL, "/") { + return nil, fmt.Errorf("BaseURL %s must have a trailing slash", baseURL) + } + return newClient(httpclient, baseURL), nil +} + +func newClient(httpclient *http.Client, baseURL string) *Client { + if httpclient == nil { + httpclient = http.DefaultClient + } + url, _ := url.Parse(baseURL) + c := &Client{client: httpclient, baseURL: url} + c.Buy = BuyAPI{ + Browse: (*BrowseService)(&service{c}), + } + return c +} + +type service struct { + client *Client +} + +// Opt describes functional options for the eBay API. +type Opt func(*http.Request) + +// NewRequest creates an API request. +// url should always be specified without a preceding slash. +func (c *Client) NewRequest(method, url string, opts ...Opt) (*http.Request, error) { + u, err := c.baseURL.Parse(url) + if err != nil { + return nil, err + } + req, err := http.NewRequest(method, u.String(), nil) + if err != nil { + return nil, err + } + for _, opt := range opts { + opt(req) + } + return req, nil +} + +// Do sends an API reauest and stores the JSON decoded value into v. +func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) error { + resp, err := c.client.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + if err := CheckResponse(resp); err != nil { + return err + } + if v == nil { + return nil + } + return json.NewDecoder(resp.Body).Decode(v) +} + +// An ErrorData reports one or more errors caused by an API request. +// +// eBay API docs: https://developer.ebay.com/api-docs/static/handling-error-messages.html +type ErrorData struct { + Errors []struct { + ErrorID int `json:"errorId,omitempty"` + Domain string `json:"domain,omitempty"` + SubDomain string `json:"subDomain,omitempty"` + Category string `json:"category,omitempty"` + Message string `json:"message,omitempty"` + LongMessage string `json:"longMessage,omitempty"` + InputRefIds []string `json:"inputRefIds,omitempty"` + OuputRefIds []string `json:"outputRefIds,omitempty"` + Parameters []struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + } `json:"parameters,omitempty"` + } `json:"errors,omitempty"` + Response *http.Response +} + +func (e *ErrorData) Error() string { + return fmt.Sprintf("%s %s: %d %+v", e.Response.Request.Method, e.Response.Request.URL, + e.Response.StatusCode, e.Errors) +} + +// CheckResponse checks the API response for errors, and returns them if present. +func CheckResponse(resp *http.Response) error { + if s := resp.StatusCode; 200 <= s && s < 300 { + return nil + } + errorData := &ErrorData{Response: resp} + _ = json.NewDecoder(resp.Body).Decode(errorData) + return errorData +} diff --git a/ebay_test.go b/ebay_test.go new file mode 100644 index 0000000..2228700 --- /dev/null +++ b/ebay_test.go @@ -0,0 +1,68 @@ +package ebay_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/jybp/ebay" + "github.com/stretchr/testify/assert" +) + +func TestNewRequest(t *testing.T) { + testOpt := func(r *http.Request) { + r.URL.RawQuery = "q=1" + } + client, _ := ebay.NewCustomClient(nil, "https://api.ebay.com/") + r, _ := client.NewRequest(http.MethodPost, "test", testOpt) + assert.Equal(t, "https://api.ebay.com/test?q=1", fmt.Sprint(r.URL)) + assert.Equal(t, http.MethodPost, r.Method) +} + +func TestCheckResponseNoError(t *testing.T) { + resp := &http.Response{StatusCode: 200} + assert.Nil(t, ebay.CheckResponse(resp)) +} + +func TestCheckResponse(t *testing.T) { + body := ` { + "errors": [ + { + "errorId": 15008, + "domain": "API_ORDER", + "subDomain": "subdomain", + "category": "REQUEST", + "message": "Invalid Field : itemId.", + "longMessage": "longMessage", + "inputRefIds": [ + "$.lineItemInputs[0].itemId" + ], + "outputRefIds": [ + "outputRefId" + ], + "parameters": [ + { + "name": "itemId", + "value": "2200077988|0" + } + ] + } + ] + }` + resp := &http.Response{StatusCode: 400, Body: ioutil.NopCloser(bytes.NewBufferString(body))} + err, ok := ebay.CheckResponse(resp).(*ebay.ErrorData) + assert.True(t, ok) + assert.Equal(t, 1, len(err.Errors)) + assert.Equal(t, 15008, err.Errors[0].ErrorID) + assert.Equal(t, "API_ORDER", err.Errors[0].Domain) + assert.Equal(t, "subdomain", err.Errors[0].SubDomain) + assert.Equal(t, "REQUEST", err.Errors[0].Category) + assert.Equal(t, "Invalid Field : itemId.", err.Errors[0].Message) + assert.Equal(t, "longMessage", err.Errors[0].LongMessage) + assert.Equal(t, []string{"$.lineItemInputs[0].itemId"}, err.Errors[0].InputRefIds) + assert.Equal(t, []string{"outputRefId"}, err.Errors[0].OuputRefIds) + assert.Equal(t, "itemId", err.Errors[0].Parameters[0].Name) + assert.Equal(t, "2200077988|0", err.Errors[0].Parameters[0].Value) +}