From 6697bdc8167ad536422c930b73859969bb2f7c98 Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Fri, 23 Jun 2017 21:49:34 -0600 Subject: [PATCH] products: implement ListProducts, ProductByID Fixes #38. a) Implements ListProducts, which given a place (lat, lon) lists available products b) Implements ProductByID c) Cleans up authentication problems that I noticed while testing out a bunch of client methods. Added "Content-Type": "application/json" headers for PUT and POST. Exhibits: * ProductByID: ```go func Example_client_ProductByID() { client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } product, err := client.ProductByID("bc300c14-c30d-4d3f-afcb-19b240c16a13") if err != nil { log.Fatal(err) } fmt.Printf("The Product information: %#v\n", product) } ``` Giving ```shell The Product information: &uber.Product{UpfrontFareEnabled:true, Capacity:2, ID:"bc300c14-c30d-4d3f-afcb-19b240c16a13", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberpool-2.png", CashEnabled:false, Shared:true, ShortDescription:"uberPOOL", DisplayName:"POOL", Description:"Share the ride, share the cost"} ``` * ListProducts: ```go func Example_client_ListProducts() { client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } products, err := client.ListProducts(&uber.Place{ Latitude: 38.8971, Longitude: -77.0366, }) if err != nil { log.Fatal(err) } for i, product := range products { fmt.Printf("#%d: ID: %q Product: %#v\n", i, product.ID, product) } } ``` Produces ```shell &uber.Product{UpfrontFareEnabled:true, Capacity:2, ID:"bc300c14-c30d-4d3f-afcb-19b240c16a13", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberpool-2.png", CashEnabled:false, Shared:true, ShortDescription:"uberPOOL", DisplayName:"POOL", Description:"Share the ride, share the cost"} &uber.Product{UpfrontFareEnabled:true, Capacity:4, ID:"dee8691c-8b48-4637-b048-300eee72d58d", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", CashEnabled:false, Shared:false, ShortDescription:"uberX", DisplayName:"uberX", Description:"The low-cost Uber"} &uber.Product{UpfrontFareEnabled:true, Capacity:6, ID:"9ffa937e-7d2e-4bcf-bc2b-ffec4ef24380", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberxl2.png", CashEnabled:false, Shared:false, ShortDescription:"uberXL", DisplayName:"uberXL", Description:"Low-cost rides for large groups"} &uber.Product{UpfrontFareEnabled:true, Capacity:4, ID:"bc98a16f-ad72-41a3-8624-809ce654ac57", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-family.png", CashEnabled:false, Shared:false, ShortDescription:"uberX + Car Seat", DisplayName:"uberX + Car Seat", Description:"uberX + Car Seat"} &uber.Product{UpfrontFareEnabled:true, Capacity:4, ID:"a52a9012-d73e-4127-8440-f273cddfd307", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-black.png", CashEnabled:false, Shared:false, ShortDescription:"BLACK CAR", DisplayName:"UberBLACK", Description:"The Original Uber"} &uber.Product{UpfrontFareEnabled:true, Capacity:4, ID:"2a299c73-098d-47cd-b32c-825cb155f82a", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-family.png", CashEnabled:false, Shared:false, ShortDescription:"BLACK CAR + Car Seat", DisplayName:"BLACK CAR + Car Seat", Description:"BLACK CAR + Car Seat"} &uber.Product{UpfrontFareEnabled:true, Capacity:6, ID:"4e6fd14c-3866-40f1-b173-f12aeb8fbbd0", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-suv.png", CashEnabled:false, Shared:false, ShortDescription:"SUV", DisplayName:"UberSUV", Description:"Room for everyone"} &uber.Product{UpfrontFareEnabled:true, Capacity:6, ID:"74766497-b951-4eae-98c9-a67d87e2c0c4", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-family.png", CashEnabled:false, Shared:false, ShortDescription:"SUV + Car Seat", DisplayName:"SUV + Car Seat", Description:"SUV + Car Seat"} &uber.Product{UpfrontFareEnabled:false, Capacity:4, ID:"89f38d7a-d184-4054-9f2e-6b57c94143d6", PriceDetails:(*uber.PriceDetails)(0xc4201d8120), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", CashEnabled:false, Shared:false, ShortDescription:"Wheelchair", DisplayName:"Wheelchair", Description:"TAXI WITHOUT THE HASSLE"} &uber.Product{UpfrontFareEnabled:false, Capacity:4, ID:"f67c83fb-4668-42eb-9aa1-ab32e710c8bf", PriceDetails:(*uber.PriceDetails)(nil), ImageURL:"http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-taxi.png", CashEnabled:false, Shared:false, ShortDescription:"TAXI", DisplayName:"uberTAXI", Description:"Taxi without the hassle"} ``` --- example_test.go | 78 ++++++-- oauth2/oauth2.go | 12 +- v1/client.go | 14 ++ v1/history.go | 2 +- v1/maps.go | 2 +- v1/payment.go | 6 +- v1/places.go | 3 +- v1/prices.go | 5 +- v1/products.go | 174 ++++++++++++++++++ v1/profile.go | 5 +- v1/receipts.go | 2 +- v1/testdata/listProducts.json | 112 +++++++++++ ...-a1111c8c-c720-46c3-8534-2fcdd730040d.json | 27 +++ v1/times.go | 2 +- v1/uber_test.go | 122 ++++++++++++ 15 files changed, 534 insertions(+), 32 deletions(-) create mode 100644 v1/products.go create mode 100644 v1/testdata/listProducts.json create mode 100644 v1/testdata/product-a1111c8c-c720-46c3-8534-2fcdd730040d.json diff --git a/example_test.go b/example_test.go index 4f6d3c4..d2b7229 100644 --- a/example_test.go +++ b/example_test.go @@ -17,12 +17,13 @@ package uber_test import ( "fmt" "log" + "os" "github.com/orijtech/uber/v1" ) func Example_client_ListPaymentMethods() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -41,7 +42,7 @@ func Example_client_ListPaymentMethods() { } func Example_client_ListHistory() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -78,7 +79,7 @@ func Example_client_ListHistory() { } func Example_client_ListAllMyHistory() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -110,7 +111,7 @@ func Example_client_ListAllMyHistory() { } func Example_client_EstimatePrice() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -143,10 +144,12 @@ func Example_client_EstimatePrice() { cancelPaging() } } +// Output: +// WW } func Example_client_EstimateTime() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -181,10 +184,12 @@ func Example_client_EstimateTime() { cancelPaging() } } +// Output: +// WW } func Example_client_RetrieveMyProfile() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -198,7 +203,7 @@ func Example_client_RetrieveMyProfile() { } func Example_client_ApplyPromoCode() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -226,7 +231,7 @@ func Example_client_RequestReceipt() { } func Example_client_RetrieveHomeAddress() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -240,7 +245,7 @@ func Example_client_RetrieveHomeAddress() { } func Example_client_RetrieveWorkAddress() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -254,7 +259,7 @@ func Example_client_RetrieveWorkAddress() { } func Example_client_UpdateHomeAddress() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -271,7 +276,7 @@ func Example_client_UpdateHomeAddress() { } func Example_client_UpdateWorkAddress() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -288,7 +293,7 @@ func Example_client_UpdateWorkAddress() { } func Example_client_RequestMap() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -302,18 +307,18 @@ func Example_client_RequestMap() { } func Example_client_OpenMap() { - client, err := uber.NewClient() + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } - if err := client.OpenMapForTrip("b5512127-a134-4bf4-b1ba-fe9f48f56d9d"); err != nil { + if err := client.OpenMapForTrip("64561dfe-87fa-41d7-807e-f364527b11cb"); err != nil { log.Fatal(err) } } func Example_client_UpfrontFare() { - client, err := uber.NewClientFromOAuth2File("./testdata/.uber/credentials.json") + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -341,7 +346,7 @@ func Example_client_UpfrontFare() { } func Example_client_RequestRide() { - client, err := uber.NewClientFromOAuth2File("./testdata/.uber/credentials.json") + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -366,7 +371,7 @@ func Example_client_RequestRide() { } func Example_client_RequestDelivery() { - client, err := uber.NewClientFromOAuth2File("./testdata/.uber/credentials.json") + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } @@ -421,15 +426,50 @@ func Example_client_RequestDelivery() { } func Example_client_CancelDelivery() { - client, err := uber.NewClientFromOAuth2File("./testdata/.uber/credentials.json") + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) if err != nil { log.Fatal(err) } - err := client.CancelDelivery("71a969ca-5359-4334-a7b7-5a1705869c51") + err = client.CancelDelivery("71a969ca-5359-4334-a7b7-5a1705869c51") if err == nil { log.Printf("Successfully canceled that delivery!") } else { log.Printf("Failed to cancel that delivery, err: %v", err) } } + +func Example_client_ListProducts() { + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) + if err != nil { + log.Fatal(err) + } + + products, err := client.ListProducts(&uber.Place{ + Latitude: 38.8971, + Longitude: -77.0366, + }) + if err != nil { + log.Fatal(err) + } + + for i, product := range products { + fmt.Printf("#%d: ID: %q Product: %#v\n", i, product.ID, product) + } +// Output: +// WW +} + +func Example_client_ProductByID() { + client, err := uber.NewClientFromOAuth2File(os.ExpandEnv("$HOME/.uber/credentials.json")) + if err != nil { + log.Fatal(err) + } + + product, err := client.ProductByID("bc300c14-c30d-4d3f-afcb-19b240c16a13") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("The Product information: %#v\n", product) +} diff --git a/oauth2/oauth2.go b/oauth2/oauth2.go index c1e4008..7b8e79e 100644 --- a/oauth2/oauth2.go +++ b/oauth2/oauth2.go @@ -36,6 +36,7 @@ import ( type OAuth2AppConfig struct { ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` + RedirectURL string `json:"redirect_url"` } var ( @@ -195,9 +196,15 @@ func Authorize(oconfig *OAuth2AppConfig, scopes ...string) (*oauth2.Token, error AuthURL: OAuth2AuthURL, TokenURL: OAuth2TokenURL, }, + RedirectURL: oconfig.RedirectURL, } - state := fmt.Sprintf("%v%s", time.Now().Unix(), rand.Float32()) + srvAddr := ":8889" + if config.RedirectURL == "" { + config.RedirectURL = fmt.Sprintf("http://localhost%s/", srvAddr) + } + + state := fmt.Sprintf("%v%f", time.Now().Unix(), rand.Float32()) urlToVisit := config.AuthCodeURL(state, oauth2.AccessTypeOffline) fmt.Printf("Please visit this URL for the auth dialog: %v\n", urlToVisit) @@ -210,8 +217,7 @@ func Authorize(oconfig *OAuth2AppConfig, scopes ...string) (*oauth2.Token, error }) defer close(callbackURLChan) - addr := ":8889" - if err := http.ListenAndServe(addr, nil); err != nil { + if err := http.ListenAndServe(srvAddr, nil); err != nil { log.Fatal(err) } }() diff --git a/v1/client.go b/v1/client.go index c7421f3..cdd0d85 100644 --- a/v1/client.go +++ b/v1/client.go @@ -42,6 +42,13 @@ type Client struct { sandboxed bool } +func (c *Client) hasServerToken() bool { + c.RLock() + defer c.RUnlock() + + return c.token != "" +} + // Sandboxed if set to true, the client will send requests // to the sandboxed API endpoint. // See: @@ -151,6 +158,13 @@ func (c *Client) doAuthAndHTTPReq(req *http.Request) ([]byte, http.Header, error return c.doHTTPReq(req) } +func (c *Client) doReq(req *http.Request) ([]byte, http.Header, error) { + if c.hasServerToken() { + req.Header.Set("Authorization", c.bearerToken()) + } + return c.doHTTPReq(req) +} + func (c *Client) doHTTPReq(req *http.Request) ([]byte, http.Header, error) { res, err := c.httpClient().Do(req) if err != nil { diff --git a/v1/history.go b/v1/history.go index ab46dc3..4e1be0e 100644 --- a/v1/history.go +++ b/v1/history.go @@ -147,7 +147,7 @@ func (c *Client) ListHistory(threq *Pager) (thChan chan *TripThreadPage, cancelF return } - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { ttp.Err = err historyChan <- ttp diff --git a/v1/maps.go b/v1/maps.go index 6a9eb1d..650f536 100644 --- a/v1/maps.go +++ b/v1/maps.go @@ -45,7 +45,7 @@ func (c *Client) RequestMap(tripID string) (*Map, error) { return nil, err } - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { return nil, err } diff --git a/v1/payment.go b/v1/payment.go index 1d40fba..c342075 100644 --- a/v1/payment.go +++ b/v1/payment.go @@ -101,6 +101,10 @@ func (pm *PaymentMethod) PaymentMethodToString() string { return paymentMethodToString[*pm] } +func (pm PaymentMethod) String() string { + return pm.PaymentMethodToString() +} + func StringToPaymentMethod(str string) PaymentMethod { pm, ok := stringToPaymentMethod[str] if !ok { @@ -138,7 +142,7 @@ func (c *Client) ListPaymentMethods() (*PaymentListing, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept-Language", "en_US") - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { return nil, err } diff --git a/v1/places.go b/v1/places.go index 8eea02b..83f9442 100644 --- a/v1/places.go +++ b/v1/places.go @@ -39,7 +39,7 @@ func (c *Client) Place(placeName PlaceName) (*Place, error) { } func (c *Client) doPlaceReq(req *http.Request) (*Place, error) { - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { return nil, err } @@ -90,5 +90,6 @@ func (c *Client) UpdatePlace(pp *PlaceParams) (*Place, error) { if err != nil { return nil, err } + req.Header.Set("Content-Type", "application/json") return c.doPlaceReq(req) } diff --git a/v1/prices.go b/v1/prices.go index 4f54ce2..2b702d7 100644 --- a/v1/prices.go +++ b/v1/prices.go @@ -148,7 +148,7 @@ func (c *Client) EstimatePrice(ereq *EstimateRequest) (pagesChan chan *PriceEsti return } - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { ep.Err = err estimatesPageChan <- ep @@ -283,7 +283,8 @@ func (c *Client) UpfrontFare(esReq *EstimateRequest) (*UpfrontFare, error) { if err != nil { return nil, err } - slurp, _, err := c.doHTTPReq(req) + req.Header.Set("Content-Type", "application/json") + slurp, _, err := c.doReq(req) if err != nil { return nil, err } diff --git a/v1/products.go b/v1/products.go new file mode 100644 index 0000000..793cd3a --- /dev/null +++ b/v1/products.go @@ -0,0 +1,174 @@ +// Copyright 2017 orijtech. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package uber + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/orijtech/otils" +) + +type Product struct { + UpfrontFareEnabled bool `json:"upfront_fare_enabled,omitempty"` + // Capacity is the number of people that can be + // accomodated by the product for example, 4 people. + Capacity int `json:"capacity,omitempty"` + + // The unique identifier representing a specific + // product for a given latitude and longitude. + // For example, uberX in San Francisco will have + // a different ID than uberX in Los Angeles. + ID string `json:"product_id"` + + // PriceDetails details the basic price + // (not including any surge pricing adjustments). + // This field is nil for products with metered + // fares(taxi) or upfront fares(uberPOOL). + PriceDetails *PriceDetails `json:"price_details"` + + ImageURL string `json:"image,omitempty"` + CashEnabled bool `json:"cash_enabled,omitempty"` + Shared bool `json:"shared"` + + // An abbreviated description of the product. + // It is localized according to `Accept-Language` header. + ShortDescription string `json:"short_description"` + + DisplayName string `json:"display_name"` + + Description string `json:"description"` +} + +type ProductGroup string + +const ( + ProductRideShare ProductGroup = "rideshare" + ProductUberX ProductGroup = "uberx" + ProductUberXL ProductGroup = "uberxl" + ProductUberBlack ProductGroup = "uberblack" + ProductSUV ProductGroup = "suv" + ProductTaxi ProductGroup = "taxi" +) + +// ListProducts is a method that returns information about the +// Uber products offered at a given location. +// Some products such as uberEATS, are not returned by this +// endpoint, at least as of: Fri 23 Jun 2017 18:01:04 MDT. +// The results of this method do not reflect real-time availability +// of the products. Please use the EstimateTime method to determine +// real-time availability and ETAs of products. +// In some markets, the list of products returned from this endpoint +// may vary by the time of day due to time restrictions on +// when that product may be utilized. +func (c *Client) ListProducts(place *Place) ([]*Product, error) { + qv, err := otils.ToURLValues(place) + if err != nil { + return nil, err + } + fullURL := fmt.Sprintf("%s/products?%s", c.baseURL(), qv.Encode()) + req, err := http.NewRequest("GET", fullURL, nil) + if err != nil { + return nil, err + } + slurp, _, err := c.doReq(req) + if err != nil { + return nil, err + } + pWrap := new(productsWrap) + if err := json.Unmarshal(slurp, pWrap); err != nil { + return nil, err + } + return pWrap.Products, nil +} + +var ( + errEmptyProductID = errors.New("expecting a non-empty productID") + errBlankProduct = errors.New("received a blank product back from the server") + + blankProductPtr = new(Product) +) + +func (c *Client) ProductByID(productID string) (*Product, error) { + productID = strings.TrimSpace(productID) + if productID == "" { + return nil, errEmptyProductID + } + fullURL := fmt.Sprintf("%s/products/%s", c.baseURL(), productID) + req, err := http.NewRequest("GET", fullURL, nil) + if err != nil { + return nil, err + } + slurp, _, err := c.doReq(req) + if err != nil { + return nil, err + } + product := new(Product) + if err := json.Unmarshal(slurp, product); err != nil { + return nil, err + } + if reflect.DeepEqual(product, blankProductPtr) { + return nil, errBlankProduct + } + return product, nil +} + +type productsWrap struct { + Products []*Product `json:"products"` +} + +type PriceDetails struct { + // The base price of a trip. + Base otils.NullableFloat64 `json:"base,omitempty"` + + // The minimum price of a trip. + Minimum otils.NullableFloat64 `json:"minimum,omitempty"` + + // CostPerMinute is the charge per minute(if applicable for the product type). + CostPerMinute otils.NullableFloat64 `json:"cost_per_minute,omitempty"` + + // CostPerDistanceUnit is the charge per + // distance unit(if applicable for the product type). + CostPerDistanceUnit otils.NullableFloat64 `json:"cost_per_distance,omitempty"` + + // DistanceUnit is the unit of distance used + // to calculate the fare (either UnitMile or UnitKm) + DistanceUnit Unit `json:"distance_unit,omitempty"` + + // Cancellation fee is what the rider has to pay after + // they cancel the trip after the grace period. + CancellationFee otils.NullableFloat64 `json:"cancellation_fee,omitempty"` + + CurrencyCode CurrencyCode `json:"currency_code,omitempty"` + + ServiceFees []*ServiceFee `json:"service_fees,omitempty"` +} + +type ServiceFee struct { + Name string `json:"name,omitempty"` + + Fee otils.NullableFloat64 `json:"fee,omitempty"` +} + +type Unit otils.NullableString + +const ( + UnitMile otils.NullableString = "mile" + UnitKM otils.NullableString = "km" +) diff --git a/v1/profile.go b/v1/profile.go index bb7fa1c..7cbf9e5 100644 --- a/v1/profile.go +++ b/v1/profile.go @@ -52,7 +52,7 @@ func (c *Client) RetrieveMyProfile() (*Profile, error) { if err != nil { return nil, err } - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { return nil, err } @@ -92,7 +92,8 @@ func (c *Client) ApplyPromoCode(promoCode string) (*PromoCode, error) { if err != nil { return nil, err } - slurp, _, err := c.doAuthAndHTTPReq(req) + req.Header.Set("Content-Type", "application/json") + slurp, _, err := c.doReq(req) if err != nil { return nil, err } diff --git a/v1/receipts.go b/v1/receipts.go index 6676778..db24a19 100644 --- a/v1/receipts.go +++ b/v1/receipts.go @@ -68,7 +68,7 @@ func (c *Client) RequestReceipt(receiptID string) (*Receipt, error) { return nil, err } - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { return nil, err } diff --git a/v1/testdata/listProducts.json b/v1/testdata/listProducts.json new file mode 100644 index 0000000..52f62d1 --- /dev/null +++ b/v1/testdata/listProducts.json @@ -0,0 +1,112 @@ +{ + "products": [ + { + "upfront_fare_enabled": true, + "capacity": 2, + "product_id": "26546650-e557-4a7b-86e7-6a3942445247", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", + "cash_enabled": false, + "shared": true, + "short_description": "POOL", + "display_name": "POOL", + "product_group": "rideshare", + "description": "Share the ride, split the cost." + }, + { + "upfront_fare_enabled": true, + "capacity": 4, + "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", + "cash_enabled": false, + "shared": false, + "short_description": "uberX", + "display_name": "uberX", + "product_group": "uberx", + "description": "THE LOW-COST UBER" + }, + { + "upfront_fare_enabled": true, + "capacity": 6, + "product_id": "821415d8-3bd5-4e27-9604-194e4359a449", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberxl2.png", + "cash_enabled": false, + "shared": false, + "short_description": "uberXL", + "display_name": "uberXL", + "product_group": "uberxl", + "description": "LOW-COST RIDES FOR LARGE GROUPS" + }, + { + "upfront_fare_enabled": true, + "capacity": 4, + "product_id": "57c0ff4e-1493-4ef9-a4df-6b961525cf92", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberselect.png", + "cash_enabled": false, + "shared": false, + "short_description": "SELECT", + "display_name": "SELECT", + "product_group": "uberx", + "description": "A STEP ABOVE THE EVERY DAY" + }, + { + "upfront_fare_enabled": true, + "capacity": 4, + "product_id": "d4abaae7-f4d6-4152-91cc-77523e8165a4", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-black.png", + "cash_enabled": false, + "shared": false, + "short_description": "BLACK", + "display_name": "BLACK", + "product_group": "uberblack", + "description": "THE ORIGINAL UBER" + }, + { + "upfront_fare_enabled": true, + "capacity": 6, + "product_id": "8920cb5e-51a4-4fa4-acdf-dd86c5e18ae0", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-suv.png", + "cash_enabled": false, + "shared": false, + "short_description": "SUV", + "display_name": "SUV", + "product_group": "suv", + "description": "ROOM FOR EVERYONE" + }, + { + "upfront_fare_enabled": true, + "capacity": 4, + "product_id": "ff5ed8fe-6585-4803-be13-3ca541235de3", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", + "cash_enabled": false, + "shared": false, + "short_description": "ASSIST", + "display_name": "ASSIST", + "product_group": "uberx", + "description": "uberX with extra assistance" + }, + { + "upfront_fare_enabled": true, + "capacity": 4, + "product_id": "2832a1f5-cfc0-48bb-ab76-7ea7a62060e7", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-wheelchair.png", + "cash_enabled": false, + "shared": false, + "short_description": "WAV", + "display_name": "WAV", + "product_group": "uberx", + "description": "WHEELCHAIR ACCESSIBLE VEHICLES" + }, + { + "upfront_fare_enabled": false, + "capacity": 4, + "product_id": "3ab64887-4842-4c8e-9780-ccecd3a0391d", + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-taxi.png", + "cash_enabled": false, + "shared": false, + "short_description": "TAXI", + "display_name": "TAXI", + "product_group": "taxi", + "description": "TAXI WITHOUT THE HASSLE" + } + ] +} \ No newline at end of file diff --git a/v1/testdata/product-a1111c8c-c720-46c3-8534-2fcdd730040d.json b/v1/testdata/product-a1111c8c-c720-46c3-8534-2fcdd730040d.json new file mode 100644 index 0000000..cbbed14 --- /dev/null +++ b/v1/testdata/product-a1111c8c-c720-46c3-8534-2fcdd730040d.json @@ -0,0 +1,27 @@ +{ + "upfront_fare_enabled": false, + "capacity": 4, + "product_id": "a1111c8c-c720-46c3-8534-2fcdd730040d", + "price_details": { + "service_fees": [ + { + "fee": 1.55, + "name": "Booking fee" + } + ], + "cost_per_minute": 0.22, + "distance_unit": "mile", + "minimum": 6.55, + "cost_per_distance": 1.15, + "base": 2, + "cancellation_fee": 5, + "currency_code": "USD" + }, + "image": "http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberx.png", + "cash_enabled": false, + "shared": false, + "short_description": "uberX", + "display_name": "uberX", + "product_group": "uberx", + "description": "THE LOW-COST UBER" +} \ No newline at end of file diff --git a/v1/times.go b/v1/times.go index cee6e6c..b7adeca 100644 --- a/v1/times.go +++ b/v1/times.go @@ -109,7 +109,7 @@ func (c *Client) EstimateTime(treq *EstimateRequest) (pagesChan chan *TimeEstima return } - slurp, _, err := c.doAuthAndHTTPReq(req) + slurp, _, err := c.doReq(req) if err != nil { tp.Err = err estimatesPageChan <- tp diff --git a/v1/uber_test.go b/v1/uber_test.go index 8dd1aab..43dd519 100644 --- a/v1/uber_test.go +++ b/v1/uber_test.go @@ -73,6 +73,96 @@ func TestListPaymentMethods(t *testing.T) { } } +func TestListProducts(t *testing.T) { + client, err := uber.NewClient(testToken1) + if err != nil { + t.Fatalf("initializing client; %v", err) + } + + backend := &tRoundTripper{route: listProducts} + client.SetHTTPRoundTripper(backend) + + tests := [...]struct { + place *uber.Place + wantErr bool + }{ + 0: { + place: nil, wantErr: true, + }, + 1: { + place: &uber.Place{}, + }, + 2: { + place: &uber.Place{Latitude: 53.555}, + }, + } + + for i, tt := range tests { + products, err := client.ListProducts(tt.place) + if tt.wantErr { + if err == nil { + t.Errorf("#%d: expected a non-nil error", i) + } + continue + } + + if err != nil { + t.Errorf("#%d: got err: %v want nil error", i, err) + continue + } + + if len(products) == 0 { + t.Errorf("#%d: expecting at least one product", i) + } + } +} + +var blankProductPtr = new(uber.Product) + +func TestProductByID(t *testing.T) { + client, err := uber.NewClient(testToken1) + if err != nil { + t.Fatalf("initializing client; %v", err) + } + + backend := &tRoundTripper{route: productByID} + client.SetHTTPRoundTripper(backend) + + tests := [...]struct { + productID string + wantErr bool + }{ + 0: { + productID: "", wantErr: true, + }, + 1: { + productID: " ", wantErr: true, + }, + 2: { + productID: "a1111c8c-c720-46c3-8534-2fcdd730040d", + }, + } + + for i, tt := range tests { + product, err := client.ProductByID(tt.productID) + if tt.wantErr { + if err == nil { + t.Errorf("#%d: expected a non-nil error", i) + } + continue + } + + if err != nil { + t.Errorf("#%d: got err: %v want nil error", i, err) + continue + } + + if product == nil || reflect.DeepEqual(product, blankProductPtr) { + t.Errorf("#%d: expecting a non-blank product", i) + } + } +} + func TestCancelDelivery(t *testing.T) { client, err := uber.NewClient(testToken1) if err != nil { @@ -925,6 +1015,10 @@ func (trt *tRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { switch trt.route { case listPaymentMethods: return trt.listPaymentMethodRoundTrip(req) + case listProducts: + return trt.listProductsRoundTrip(req) + case productByID: + return trt.productByIDRoundTrip(req) case estimatePriceRoute: return trt.estimatePriceRoundTrip(req) case estimateTimeRoute: @@ -1297,6 +1391,32 @@ func (trt *tRoundTripper) requestReceiptRoundTrip(req *http.Request) (*http.Resp return resp, nil } +func (trt *tRoundTripper) listProductsRoundTrip(req *http.Request) (*http.Response, error) { + if badAuthResp, _, err := prescreenAuthAndMethod(req, "GET"); badAuthResp != nil || err != nil { + return badAuthResp, err + } + resp := responseFromFileContent("./testdata/listProducts.json") + return resp, nil +} + +func (trt *tRoundTripper) productByIDRoundTrip(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, "/") + // Expecting the form: /v1.2/products/ + if len(splits) != 4 || splits[2] != "products" { + resp := makeResp("expecting URL of form /v1.2/products/", http.StatusBadRequest) + return resp, nil + } + + productID := splits[len(splits)-1] + + diskPath := fmt.Sprintf("./testdata/product-%s.json", productID) + resp := responseFromFileContent(diskPath) + return resp, nil +} + func (trt *tRoundTripper) listPaymentMethodRoundTrip(req *http.Request) (*http.Response, error) { if badAuthResp, _, err := prescreenAuthAndMethod(req, "GET"); badAuthResp != nil || err != nil { return badAuthResp, err @@ -1417,6 +1537,8 @@ func unauthorizedToken(token string) bool { const ( listPaymentMethods = "list-payment-methods" + listProducts = "list-products" + productByID = "product-by-id" estimatePriceRoute = "estimate-prices" estimateTimeRoute = "estimate-times" retrieveProfileRoute = "retrieve-profile"