From 18b827c46bccbc39e9426ba644ae7c90a62cf3fa Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Wed, 5 Jul 2017 00:15:09 -0600 Subject: [PATCH] delivery: implemented Client.ListDeliveries Updates #32 * Implemented Client.ListDeliveries. * Introduced scope oauth2.ScopeDelivery. NB: * ListDeliveries hits the /v1 API instead of /v1.2 Sample usage: ```go func main() { client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } delivRes, err := client.ListDeliveries(&uber.DeliveryListRequest{ Status: uber.StatusCompleted, StartOffset: 20, }) if err != nil { log.Fatal(err) } itemCount := uint64(0) for page := range delivRes.Pages { if page.Err != nil { fmt.Printf("Page #%d err: %v", page.PageNumber, page.Err) } for i, delivery := range page.Deliveries { fmt.Printf("\t(%d): %#v\n", i, delivery) itemCount += 1 } if itemCount >= 10 { delivRes.Cancel() } } } ``` --- cmd/uber/main.go | 2 +- example_test.go | 35 +++++++++-- oauth2/oauth2.go | 4 ++ v1/client.go | 16 +++++ v1/deliveries.go | 154 ++++++++++++++++++++++++++++++++++++++++++++++- v1/uber_test.go | 103 ++++++++++++++++++++++++++++++- 6 files changed, 301 insertions(+), 13 deletions(-) diff --git a/cmd/uber/main.go b/cmd/uber/main.go index 1f16450..8180366 100644 --- a/cmd/uber/main.go +++ b/cmd/uber/main.go @@ -47,7 +47,7 @@ func authorize() { scopes := []string{ oauth2.ScopeProfile, oauth2.ScopeRequest, oauth2.ScopeHistory, oauth2.ScopePlaces, - oauth2.ScopeRequestReceipt, + oauth2.ScopeRequestReceipt, oauth2.ScopeDelivery, } token, err := oauth2.AuthorizeByEnvApp(scopes...) diff --git a/example_test.go b/example_test.go index d2b7229..b335675 100644 --- a/example_test.go +++ b/example_test.go @@ -144,8 +144,6 @@ func Example_client_EstimatePrice() { cancelPaging() } } -// Output: -// WW } func Example_client_EstimateTime() { @@ -184,8 +182,6 @@ func Example_client_EstimateTime() { cancelPaging() } } -// Output: -// WW } func Example_client_RetrieveMyProfile() { @@ -456,8 +452,6 @@ func Example_client_ListProducts() { for i, product := range products { fmt.Printf("#%d: ID: %q Product: %#v\n", i, product.ID, product) } -// Output: -// WW } func Example_client_ProductByID() { @@ -473,3 +467,32 @@ func Example_client_ProductByID() { fmt.Printf("The Product information: %#v\n", product) } + +func Example_client_ListDeliveries() { + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) + if err != nil { + log.Fatal(err) + } + + delivRes, err := client.ListDeliveries(&uber.DeliveryListRequest{ + Status: uber.StatusCompleted, + StartOffset: 20, + }) + if err != nil { + log.Fatal(err) + } + + itemCount := uint64(0) + for page := range delivRes.Pages { + if page.Err != nil { + fmt.Printf("Page #%d err: %v", page.PageNumber, page.Err) + } + for i, delivery := range page.Deliveries { + fmt.Printf("\t(%d): %#v\n", i, delivery) + itemCount += 1 + } + if itemCount >= 10 { + delivRes.Cancel() + } + } +} diff --git a/oauth2/oauth2.go b/oauth2/oauth2.go index 7b8e79e..9bbf141 100644 --- a/oauth2/oauth2.go +++ b/oauth2/oauth2.go @@ -177,6 +177,10 @@ const ( // including pickup, destination and real-time // location for all of your future rides. ScopeAllTrips = "all_trips" + + // ScopeDelivery is a privileged scope that gives + // access to the authenticated user's deliveries. + ScopeDelivery = "delivery" ) func AuthorizeByEnvApp(scopes ...string) (*oauth2.Token, error) { diff --git a/v1/client.go b/v1/client.go index cdd0d85..b296ffa 100644 --- a/v1/client.go +++ b/v1/client.go @@ -80,6 +80,22 @@ func (c *Client) baseURL() string { } } +// Some endpoints require us to hit /v1 instead of /v1.2 as in Client.baseURL. +// These endpoints include: +// + ListDeliveries --> /v1/deliveries at least as of "Tue 4 Jul 2017 23:17:14 MDT" +func (c *Client) legacyV1BaseURL() string { + // Setting the baseURLs in here to ensure that no-one mistakenly + // directly invokes baseURL or sandboxBaseURL. + c.RLock() + defer c.RUnlock() + + if c.sandboxed { + return "https://sandbox-api.uber.com/v1" + } else { // Invoking the production endpoint + return "https://api.uber.com/v1" + } +} + func NewClient(tokens ...string) (*Client, error) { if token := otils.FirstNonEmptyString(tokens...); token != "" { return &Client{token: token}, nil diff --git a/v1/deliveries.go b/v1/deliveries.go index 019588c..e761538 100644 --- a/v1/deliveries.go +++ b/v1/deliveries.go @@ -20,7 +20,9 @@ import ( "errors" "fmt" "net/http" + "net/url" "strings" + "time" "github.com/orijtech/otils" ) @@ -98,7 +100,7 @@ type Phone struct { type CurrencyCode string -type DeliveryResponse struct { +type Delivery struct { ID string `json:"delivery_id"` Fee float32 `json:"fee"` QuoteID string `json:"quote_id"` @@ -196,7 +198,7 @@ func (e *Endpoint) Validate() error { return nil } -func (c *Client) RequestDelivery(req *DeliveryRequest) (*DeliveryResponse, error) { +func (c *Client) RequestDelivery(req *DeliveryRequest) (*Delivery, error) { if err := req.Validate(); err != nil { return nil, err } @@ -215,7 +217,7 @@ func (c *Client) RequestDelivery(req *DeliveryRequest) (*DeliveryResponse, error if err != nil { return nil, err } - dRes := new(DeliveryResponse) + dRes := new(Delivery) if err := json.Unmarshal(blob, dRes); err != nil { return nil, err } @@ -240,3 +242,149 @@ func (c *Client) CancelDelivery(deliveryID string) error { _, _, err = c.doHTTPReq(httpReq) return err } + +type DeliveryListRequest struct { + Status Status `json:"status,omitempty"` + LimitPerPage int64 `json:"limit"` + MaxPageNumber int64 `json:"max_page,omitempty"` + StartOffset int64 `json:"offset"` + + ThrottleDurationMs int64 `json:"throttle_duration_ms"` +} + +type DeliveryThread struct { + Pages chan *DeliveryPage `json:"-"` + Cancel func() +} + +type DeliveryPage struct { + Err error `json:"error"` + PageNumber int64 `json:"page_number,omitempty"` + Deliveries []*Delivery `json:"deliveries,omitempty"` +} + +type recvDelivery struct { + Count int64 `json:"count"` + NextPageQuery string `json:"next_page"` + PreviousPageQuery string `json:"previous_page"` + Deliveries []*Delivery `json:"deliveries"` +} + +type deliveryPager struct { + Offset int64 `json:"offset"` + Limit int64 `json:"limit"` + Status Status `json:"status"` +} + +const ( + NoThrottle = -1 + + defaultThrottleDurationMs = 150 * time.Millisecond +) + +// ListDeliveries requires authorization with OAuth2.0 with +// the delivery scope set. +func (c *Client) ListDeliveries(dReq *DeliveryListRequest) (*DeliveryThread, error) { + if dReq == nil { + dReq = &DeliveryListRequest{Status: StatusReceiptReady} + } + + baseURL := c.legacyV1BaseURL() + fullURL := fmt.Sprintf("%s/deliveries", baseURL) + qv, err := otils.ToURLValues(&deliveryPager{ + Limit: dReq.LimitPerPage, + Status: dReq.Status, + Offset: dReq.StartOffset, + }) + if err != nil { + return nil, err + } + + if len(qv) > 0 { + fullURL = fmt.Sprintf("%s/deliveries?%s", baseURL, qv.Encode()) + } + + parsedURL, err := url.Parse(fullURL) + if err != nil { + return nil, err + } + parsedBaseURL, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + var errsList []string + if want, got := parsedBaseURL.Scheme, parsedURL.Scheme; got != want { + errsList = append(errsList, fmt.Sprintf("gotScheme=%q wantBaseScheme=%q", got, want)) + } + if want, got := parsedBaseURL.Host, parsedURL.Host; got != want { + errsList = append(errsList, fmt.Sprintf("gotHost=%q wantBaseHost=%q", got, want)) + } + if len(errsList) > 0 { + return nil, errors.New(strings.Join(errsList, "\n")) + } + + maxPage := dReq.MaxPageNumber + pageExceeded := func(pageNumber int64) bool { + return maxPage > 0 && pageNumber >= maxPage + } + + fullDeliveriesBaseURL := fmt.Sprintf("%s/deliveries", baseURL) + resChan := make(chan *DeliveryPage) + cancelChan, cancelFn := makeCancelParadigm() + + go func() { + defer close(resChan) + + pageNumber := int64(0) + throttleDurationMs := defaultThrottleDurationMs + if dReq.ThrottleDurationMs == NoThrottle { + throttleDurationMs = 0 + } else { + throttleDurationMs = time.Duration(dReq.ThrottleDurationMs) * time.Millisecond + } + + for { + page := &DeliveryPage{PageNumber: pageNumber} + + req, err := http.NewRequest("GET", fullURL, nil) + if err != nil { + page.Err = err + resChan <- page + return + } + + slurp, _, err := c.doReq(req) + if err != nil { + page.Err = err + resChan <- page + return + } + + recv := new(recvDelivery) + if err := json.Unmarshal(slurp, recv); err != nil { + page.Err = err + resChan <- page + return + } + + page.Deliveries = recv.Deliveries + resChan <- page + pageNumber += 1 + pageToken := recv.NextPageQuery + if pageExceeded(pageNumber) || pageToken == "" || len(recv.Deliveries) == 0 { + return + } + + fullURL = fmt.Sprintf("%s?%s", fullDeliveriesBaseURL, pageToken) + + select { + case <-cancelChan: + return + case <-time.After(throttleDurationMs): + } + } + }() + + return &DeliveryThread{Cancel: cancelFn, Pages: resChan}, nil +} diff --git a/v1/uber_test.go b/v1/uber_test.go index 43dd519..643ac56 100644 --- a/v1/uber_test.go +++ b/v1/uber_test.go @@ -23,6 +23,7 @@ import ( "net/http" "os" "reflect" + "strconv" "strings" "testing" "time" @@ -203,7 +204,7 @@ func TestRequestDelivery(t *testing.T) { tests := [...]struct { req *uber.DeliveryRequest - want *uber.DeliveryResponse + want *uber.Delivery wantErr bool }{ 0: { @@ -291,6 +292,73 @@ func TestRequestDelivery(t *testing.T) { } } +func TestListDeliveries(t *testing.T) { + t.Skipf("Need to get ListDelivery samples from Uber") + + client, err := uber.NewClient(testToken1) + if err != nil { + t.Fatalf("initializing client; %v", err) + } + + backend := &tRoundTripper{route: listDeliveriesRoute} + transport := uberOAuth2.TransportWithBase(testOAuth2Token1, backend) + client.SetHTTPRoundTripper(transport) + + tests := [...]struct { + req *uber.DeliveryListRequest + wantAtLeast int + wantErr bool + }{ + 0: { + req: &uber.DeliveryListRequest{}, + }, + 1: { + req: nil, + }, + 2: { + req: &uber.DeliveryListRequest{ + LimitPerPage: 4, + MaxPageNumber: 2, + StartOffset: 10, + }, + wantAtLeast: 8, + }, + } + + for i, tt := range tests { + dres, err := client.ListDeliveries(tt.req) + if tt.wantErr { + if err == nil { + t.Errorf("#%d: want non-nil error", i) + } + continue + } + + if err != nil { + t.Errorf("#%d: unexpected err: %v", i, err) + continue + } + + if dres == nil { + t.Errorf("#%d: expecting non-nil delivery response", i) + continue + } + + itemCount := 0 + for page := range dres.Pages { + if page.Err != nil { + t.Errorf("#%d: err: %v", i, page.Err) + continue + } + + itemCount += len(page.Deliveries) + } + if itemCount < tt.wantAtLeast { + t.Errorf("#%d: got=%d wantAtLeast=%d", i, itemCount, tt.wantAtLeast) + } + } +} + type sandboxState string const ( @@ -1043,6 +1111,8 @@ func (trt *tRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return trt.deliveryRoundTrip(req) case cancelDeliveryRoute: return trt.cancelDeliveryRoundTrip(req) + case listDeliveriesRoute: + return trt.listDeliveriesRoundTrip(req) default: return makeResp("Not Found", http.StatusNotFound), nil } @@ -1238,6 +1308,32 @@ func knownDeliveryID(deliveryID string) bool { } } +func deliveryListResponsePath(offset int64) string { + return fmt.Sprintf("./testdata/delivery-list-%d.json", offset) +} + +func (trt *tRoundTripper) listDeliveriesRoundTrip(req *http.Request) (*http.Response, error) { + if badAuthResp, _, err := prescreenAuthAndMethod(req, "GET"); badAuthResp != nil || err != nil { + return badAuthResp, err + } + splits := strings.Split(req.URL.Path, "/") + if len(splits) < 2 || splits[len(splits)-1] != "deliveries" { + resp := makeResp("expecting a path of form: /v1/deliveries", http.StatusBadRequest) + return resp, nil + } + query := req.URL.Query() + offset := int64(0) + if offsetStr := query.Get("offset"); offsetStr != "" { + var err error + offset, err = strconv.ParseInt(offsetStr, 10, 32) + if err != nil { + return makeResp(err.Error(), http.StatusBadRequest), nil + } + } + path := deliveryListResponsePath(offset) + return responseFromFileContent(path), nil +} + func (trt *tRoundTripper) cancelDeliveryRoundTrip(req *http.Request) (*http.Response, error) { if badAuthResp, _, err := prescreenAuthAndMethod(req, "POST"); badAuthResp != nil || err != nil { return badAuthResp, err @@ -1470,8 +1566,8 @@ func priceEstimateFromFile(path string) []*uber.PriceEstimate { return save.Estimates } -func deliveryResponseFromFile(path string) *uber.DeliveryResponse { - save := new(uber.DeliveryResponse) +func deliveryResponseFromFile(path string) *uber.Delivery { + save := new(uber.Delivery) if err := readFromFileAndDeserialize(path, save); err != nil { return nil } @@ -1552,4 +1648,5 @@ const ( deliveryRoute = "delivery" sandboxTesterRoute = "sandbox-test" cancelDeliveryRoute = "cancel-delivery" + listDeliveriesRoute = "list-deliveries" )