From 4c622572a158383820d1a83ff17cd97c265c841a Mon Sep 17 00:00:00 2001 From: Phil Kallos Date: Thu, 14 Feb 2013 14:16:04 -0800 Subject: [PATCH 01/13] Upgrade to use the upcoming v2 API. * Replace endpoint calls with new endpoint names * Comment out community features that are being overhauled --- lib/shopsense/api.rb | 70 ++++++++++++++++---------------- lib/shopsense/client.rb | 23 +++++------ lib/shopsense/version.rb | 4 +- spec/shopsense/shopsense_spec.rb | 52 ++++++++++++------------ 4 files changed, 73 insertions(+), 76 deletions(-) diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index db54fc4..45db9e7 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -9,25 +9,25 @@ class API < Client # The start index of results returned. # @param [Integer] num_results # The number of results to be returned. - # @return A list of Product objects. Each Product has an id, name, - # description, price, retailer, brand name, categories, images in small/medium/large, + # @return A list of Product objects. Each Product has an id, name, + # description, price, retailer, brand name, categories, images in small/medium/large, # and a URL that forwards to the retailer's site. def search( search_string = nil, index = 0, num_results = 10) raise "no search string provieded!" if( search_string === nil) fts = "fts=" + search_string.split().join( '+').to_s - min = "min=" + index.to_s - count = "count=" + num_results.to_s + min = "offset=" + index.to_s + count = "limit=" + num_results.to_s args = [fts, min, count].join( '&') return call_api( __method__, args) end - # This method returns a list of categories and product counts that describe the results + # This method returns a list of categories and product counts that describe the results # of a given product query. The query is specified using the product query parameters. # @param [String] search_string The string to be in the query. - # @return [String] A list of Category objects. Each Category has an id, name, and count + # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. def get_category_histogram( search_string = nil) raise "no search string provieded!" if( search_string === nil) @@ -37,26 +37,26 @@ def get_category_histogram( search_string = nil) return call_api( __method__, fts) end - # This method returns a list of categories and product counts that describe the results + # This method returns a list of categories and product counts that describe the results # of a given product query. The query is specified using the product query parameters. # @param [String] filter_type - # The type of filter data to return. Possible values are + # The type of filter data to return. Possible values are # Brand, Retailer, Price, Discount, Size and Color. # @param [String] search_string The string to be in the query. - # @return [String] A list of Category objects. Each Category has an id, name, and count + # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. def get_filter_histogram( filter_type = nil, search_string = nil) - raise "no search string provieded!" if( search_string === nil) + raise "no search string provided!" if( search_string === nil) raise "invalid filter type" if( !self.filter_types.include?( filter_type)) - filterType = "filterType=" + filter_type.to_s + filterType = "filters=" + filter_type.to_s fts = "fts=" + search_string.split().join( '+').to_s args = [filterType, fts].join( '&') return call_api( __method__, args) end - # This method returns a list of brands that have live products. Brands that have + # This method returns a list of brands that have live products. Brands that have # very few products will be omitted. # @return [String] A list of all Brands, with id, name, url, and synonyms of each. def get_brands @@ -64,13 +64,13 @@ def get_brands end # This method returns information about a particular look and its products. - # @param [Integer] look_id - # The ID number of the look. An easy way to get a look's ID is - # to go to the Stylebook page that contains the look at the ShopStyle website and - # right-click on the button that you use to edit the look. From the popup menu, select - # "Copy Link" and paste that into any text editor. The "lookId" query parameter of that + # @param [Integer] look_id + # The ID number of the look. An easy way to get a look's ID is + # to go to the Stylebook page that contains the look at the ShopStyle website and + # right-click on the button that you use to edit the look. From the popup menu, select + # "Copy Link" and paste that into any text editor. The "lookId" query parameter of that # URL is the value to use for this API method. - # @return [String] single look, with title, description, a set of tags, and a list of products. + # @return [String] single look, with title, description, a set of tags, and a list of products. # The products have the fields listed (see #search) def get_look( look_id = nil) raise "no look_id provieded!" if( look_id === nil) @@ -86,7 +86,7 @@ def get_retailers return call_api( __method__) end - # This method returns information about a particular user's Stylebook, the + # This method returns information about a particular user's Stylebook, the # looks within that Stylebook, and the title and description associated with each look. # @param [String] username # The username of the Stylebook owner. @@ -94,12 +94,12 @@ def get_retailers # The start index of results returned. # @param [Integer] num_results # The number of results to be returned. - # @return [String] - # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, + # @return [String] + # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, # and the title and description associated with each look. def get_stylebook( user_name = nil, index = 0, num_results = 10) raise "no user_name provieded!" if( user_name === nil) - + handle = "handle=" + user_name.to_s min = "min=" + index.to_s count = "count=" + num_results.to_s @@ -121,7 +121,7 @@ def get_stylebook( user_name = nil, index = 0, num_results = 10) # The start index of results returned. # @param [Integer] num_results # The number of results to be returned. - # @return [String] + # @return [String] # A list of looks of the given type. def get_looks( look_type = nil, index = 0, num_results = 10) raise "invalid filter type must be one of the following: #{self.look_types}" if( !self.look_types.include?( look_type)) @@ -135,14 +135,14 @@ def get_looks( look_type = nil, index = 0, num_results = 10) end # TODO: - # This method does not return a reponse of XML or JSON data like the other elements of the API. - # Instead, it forwards the user to the retailer's product page for a given product. It is the - # typical behavior to offer when the user clicks on a product. The apiSearch method returns URLs + # This method does not return a reponse of XML or JSON data like the other elements of the API. + # Instead, it forwards the user to the retailer's product page for a given product. It is the + # typical behavior to offer when the user clicks on a product. The apiSearch method returns URLs # that call this method for each of the products it returns. # @param [Integer] - # The ID number of the product. An easy way to get a product's ID is to find the product somewhere - # in the ShopStyle UI and right-click on the product image. From the popup menu, select "Copy Link" - # ("Copy link location" or "Copy shortcut" depending on your browser) and paste that into any text + # The ID number of the product. An easy way to get a product's ID is to find the product somewhere + # in the ShopStyle UI and right-click on the product image. From the popup menu, select "Copy Link" + # ("Copy link location" or "Copy shortcut" depending on your browser) and paste that into any text # editor. The "id" query parameter of that URL is the value to use for this API method. # @return [String] # A web link to the retailer. $1 of the $0 @@ -150,12 +150,12 @@ def visit_retailer( id) end - # This method returns the popular brands for a given category along with a sample product for the + # This method returns the popular brands for a given category along with a sample product for the # brand-category combination. - # @param [String] category - # Category you want to restrict the popularity search for. This is an optional + # @param [String] category + # Category you want to restrict the popularity search for. This is an optional # parameter. If category is not supplied, all the popular brands regardless of category will be returned. - # @return [String] A list of trends in the given category. Each trend has a brand, category, url, and + # @return [String] A list of trends in the given category. Each trend has a brand, category, url, and # optionally the top-ranked product for each brand/category. def get_trends( category = "", products = 0) cat = "cat=" + category.to_s @@ -179,9 +179,9 @@ def call_api( method, args = nil) site = "site=" + self.site if( args === nil) then - uri = URI.parse( method_url.to_s + [pid, format, site].join('&').to_s) + uri = URI.parse( method_url.to_s + [pid, format, site].join('&').to_s) else - uri = URI.parse( method_url.to_s + [pid, format, site, args].join('&').to_s) + uri = URI.parse( method_url.to_s + [pid, format, site, args].join('&').to_s) end return Net::HTTP.get( uri) diff --git a/lib/shopsense/client.rb b/lib/shopsense/client.rb index 41a9a49..a77ce36 100644 --- a/lib/shopsense/client.rb +++ b/lib/shopsense/client.rb @@ -5,23 +5,20 @@ def initialize( args = { }) attr_accessors = { #: - 'format' => 'json', + 'format' => 'json', 'site' => 'us'} attr_accessors.each_key{ |key| (class << self; self; end).send( :attr_accessor, key.to_sym)} attr_readers = { - 'partner_id' => nil, - 'api_url' => 'http://api.shopstyle.com', - 'search_path' => '/action/apiSearch?', - 'get_brands_path' => '/action/apiGetBrands?', - 'get_look_path' => '/action/apiGetLook?', - 'get_looks_path' => '/action/apiGetLooks?', - 'get_retailers_path' => '/action/apiGetRetailers?', - 'get_stylebook_path' => '/action/apiGetStylebook?', + 'partner_id' => nil, + 'api_url' => 'http://api.shopstyle.com/api/v2', + 'search_path' => '/products?', + 'get_brands_path' => '/brands?', + 'get_retailers_path' => '/retailers?', 'visit_retailers_path' => '/action/apiVisitRetailer?', - 'get_trends_path' => '/action/apiGetTrends?', - 'get_category_histogram_path' => '/action/apiGetCategoryHistogram?', - 'get_filter_histogram_path' => '/action/apiGetFilterHistogram?', + 'get_trends_path' => '/trends?', + 'get_category_histogram_path' => '/histogram?filters=Category&', + 'get_filter_histogram_path' => '/histogram?', 'filter_types' => ['Brands', 'Retailer', 'Price', 'Discount', 'Size', 'Color'], 'look_types' => ['New', 'TopRated', 'Celebrities', 'Featured'], 'formats' => ['xml', 'json', 'json2', 'jsonvar', 'jsonvar2', 'jsonp', 'rss'], @@ -32,7 +29,7 @@ def initialize( args = { }) attr_writers.each_key{ |key| (class << self; self; end).send( :attr_writer, key.to_sym)} attrs = attr_accessors.merge( attr_readers).merge( attr_writers) - attrs.each_key do |key| + attrs.each_key do |key| attrs[ key] = args[ key] if( args.has_key?( key)) end diff --git a/lib/shopsense/version.rb b/lib/shopsense/version.rb index 7f327f0..1091f76 100644 --- a/lib/shopsense/version.rb +++ b/lib/shopsense/version.rb @@ -1,8 +1,8 @@ module Shopsense major = 0 - minor = 1 + minor = 2 tiny = 1 #pre = "" VERSION = [major, minor, tiny].compact.join('.') -end \ No newline at end of file +end diff --git a/spec/shopsense/shopsense_spec.rb b/spec/shopsense/shopsense_spec.rb index eaafcb9..b4c10ee 100644 --- a/spec/shopsense/shopsense_spec.rb +++ b/spec/shopsense/shopsense_spec.rb @@ -33,9 +33,9 @@ def rubify_string some_string describe "search" do it "it passes if the proper data is returned" do fts = 'something' - min = 0 - count = 10 - api.search( fts, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.search_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}&min=#{min}&count=#{count}")) + min = 10 + count = 20 + api.search( fts, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.search_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) end end describe "get_category_histogram" do @@ -56,33 +56,33 @@ def rubify_string some_string api.get_brands.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_brands_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) end end - describe "get_look" do - it "it passes if the proper data is returned" do - look_id = 548347 - api.get_look( look_id).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_look_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&look=#{look_id}")) - end - end + # describe "get_look" do + # it "it passes if the proper data is returned" do + # look_id = 548347 + # api.get_look( look_id).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_look_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&look=#{look_id}")) + # end + # end describe "get_retailers" do it "it passes if the proper data is returned" do api.get_retailers.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_retailers_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) end end - describe "get_stylebook" do - it "it passes if the proper data is returned" do - handle = 'KalvinTestone' - min = 0 - count = 10 - api.get_stylebook( handle, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_stylebook_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&handle=#{handle}&min=#{min}&count=#{count}")) - end - end - describe "get_looks" do - it "it passes if the proper data is returned" do - look_type = 'New' - min = 0 - count = 10 - api.get_looks( look_type, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_looks_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&type=#{look_type}&min=#{min}&count=#{count}")) - end - end + # describe "get_stylebook" do + # it "it passes if the proper data is returned" do + # handle = 'KalvinTestone' + # min = 0 + # count = 10 + # api.get_stylebook( handle, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_stylebook_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&handle=#{handle}&min=#{min}&count=#{count}")) + # end + # end + # describe "get_looks" do + # it "it passes if the proper data is returned" do + # look_type = 'New' + # min = 0 + # count = 10 + # api.get_looks( look_type, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_looks_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&type=#{look_type}&min=#{min}&count=#{count}")) + # end + # end describe "get_trends" do it "it passes if the proper data is returned" do category = 0 @@ -91,4 +91,4 @@ def rubify_string some_string end end end -end \ No newline at end of file +end From 947a56bd1fac11d3baf3ddfc23bf7647ab9014a9 Mon Sep 17 00:00:00 2001 From: Phil Kallos Date: Sat, 16 Feb 2013 13:44:59 -0800 Subject: [PATCH 02/13] Update README.md --- README.md | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 88a0468..57058b8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Shopsense-Ruby - Ruby interface for the ShopStyle API +# Shopsense-Ruby - Ruby interface for the POPSUGAR Shopping API -The ShopStyle API, ShopSense! For Ruby! So Great. +The POPSUGAR Shopping API, ShopSense! For Ruby! So Great. ### Description @@ -10,33 +10,15 @@ Shopsense-Ruby includes a set of convent classes and methods designed to make accessing the [ShopStyle API](https://shopsense.shopstyle.com/page/ShopSenseHome) from your Ruby application seamless. -## Installation +## Usage -Install the Ruby Gem: +Install with Bundler - $ gem install shopsense-ruby +Add the folowing to your Gemfile +``` +gem "shopsense-ruby", :git => "git://github.com/PopSugar/ruby-popsugar-shopping-api.git", :branch => "master" +``` Include it in you Ruby application: require 'shopsense' - -## Documentation - -Documentation for this project may be accessed through it's RubyGems Site [here](https://rubygems.org/gems/shopsense-ruby). - -## Bugs - -Please report any bugs found in Shopsense-Ruby [here](https://github.com/RudyIndustries/shopsense-ruby/issues), we appreciate your help improving Shopsnese-Ruby - -## Future Work - -This gem looks to serve not only as a means of elegantly access the -[ShopStyle API](https://shopsense.shopstyle.com/page/ShopSenseHome) -but a means of offering a host of shopping oriented programing features! - -### Suggestions -If you have suggestions please contact ShopSense-Ruby.RubyGems@RudyIndustries.com. The Google group is publically vieable -[here](https://groups.google.com/a/rudyindustries.com/group/ShopSense-Ruby.RubyGems). Thanks! - -### Donate -Click here to lend your support to: Rudy Industries Open Source Projects and make a donation at www.pledgie.com ! \ No newline at end of file From d8b0dea5271d26fe0377130e3d47ee1ea47472c5 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 13:13:26 +0200 Subject: [PATCH 03/13] Cleaned up code formatting and ensured proper cgi encoding --- lib/shopsense.rb | 11 +-- lib/shopsense/api.rb | 146 ++++++++++++++++++---------------------- lib/shopsense/client.rb | 16 ++--- 3 files changed, 81 insertions(+), 92 deletions(-) diff --git a/lib/shopsense.rb b/lib/shopsense.rb index fecc43a..03abe09 100644 --- a/lib/shopsense.rb +++ b/lib/shopsense.rb @@ -1,14 +1,15 @@ require 'net/http' require 'yajl' +require 'cgi' =begin rdoc Shopsense is an easy to use Ruby interface for the {Shopstyle API}[https://shopsense.shopstyle.com/page/ShopSenseHome], -also known a ShopSense. The ShopStyle API is a free service from ShopStyle that pays you for sending traffic +also known a ShopSense. The ShopStyle API is a free service from ShopStyle that pays you for sending traffic to online retailers from your blog, website, or application. -The ShopStyle API allows client applications to retrieve the underlying data for all the basic elements of the -ShopStyle websites, including products, brands, retailers, categories, and looks. For ease of development, -the API is a REST-style web service, composed of simple HTTP GET requests. Data is returned to the client in -either XML or JSON formats. The API is client-language independent and easy to use from PHP, Java, JavaScript, +The ShopStyle API allows client applications to retrieve the underlying data for all the basic elements of the +ShopStyle websites, including products, brands, retailers, categories, and looks. For ease of development, +the API is a REST-style web service, composed of simple HTTP GET requests. Data is returned to the client in +either XML or JSON formats. The API is client-language independent and easy to use from PHP, Java, JavaScript, or any other modern development context. Shopsense provides a number of helpful classes and methods for integrating data aquired from the ShopStyle API into diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index 45db9e7..616f0c5 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -12,15 +12,14 @@ class API < Client # @return A list of Product objects. Each Product has an id, name, # description, price, retailer, brand name, categories, images in small/medium/large, # and a URL that forwards to the retailer's site. - def search( search_string = nil, index = 0, num_results = 10) - raise "no search string provieded!" if( search_string === nil) - - fts = "fts=" + search_string.split().join( '+').to_s - min = "offset=" + index.to_s - count = "limit=" + num_results.to_s - args = [fts, min, count].join( '&') - - return call_api( __method__, args) + def search(search_string, offset = 0, limit = 10) + raise "no search string provieded!" if search_string.nil? + args = { + :fts => search_string, + :offset => offset, + :limit => limit + } + call_api(__method__, args) end @@ -29,12 +28,12 @@ def search( search_string = nil, index = 0, num_results = 10) # @param [String] search_string The string to be in the query. # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. - def get_category_histogram( search_string = nil) - raise "no search string provieded!" if( search_string === nil) - - fts = "fts=" + search_string.split().join( '+').to_s - - return call_api( __method__, fts) + def get_category_histogram(search_string) + raise "no search string provieded!" if search_string.nil? + args = { + :fts => search_string + } + call_api(__method__, args) end # This method returns a list of categories and product counts that describe the results @@ -45,22 +44,21 @@ def get_category_histogram( search_string = nil) # @param [String] search_string The string to be in the query. # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. - def get_filter_histogram( filter_type = nil, search_string = nil) - raise "no search string provided!" if( search_string === nil) - raise "invalid filter type" if( !self.filter_types.include?( filter_type)) - - filterType = "filters=" + filter_type.to_s - fts = "fts=" + search_string.split().join( '+').to_s - args = [filterType, fts].join( '&') - - return call_api( __method__, args) + def get_filter_histogram(filter_type, search_string) + raise "invalid filter type" unless self.filter_types.include?(filter_type) + raise "no search string provided!" if search_string.nil? + args = { + :fts => search_string, + :filters => filter_type + } + call_api(__method__, args) end # This method returns a list of brands that have live products. Brands that have # very few products will be omitted. # @return [String] A list of all Brands, with id, name, url, and synonyms of each. def get_brands - return call_api( __method__) + call_api(__method__) end # This method returns information about a particular look and its products. @@ -72,18 +70,18 @@ def get_brands # URL is the value to use for this API method. # @return [String] single look, with title, description, a set of tags, and a list of products. # The products have the fields listed (see #search) - def get_look( look_id = nil) - raise "no look_id provieded!" if( look_id === nil) - - look = "look=" + look_id.to_s - - return call_api( __method__, look) + def get_look(look_id) + raise "no look_id provieded!" if look_id.nil? + args = { + :look => look_id + } + call_api(__method__, args) end # This method returns a list of retailers that have live products. # @return [Sting] A list of all Retailers, with id, name, and url of each. def get_retailers - return call_api( __method__) + call_api(__method__) end # This method returns information about a particular user's Stylebook, the @@ -97,15 +95,14 @@ def get_retailers # @return [String] # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, # and the title and description associated with each look. - def get_stylebook( user_name = nil, index = 0, num_results = 10) - raise "no user_name provieded!" if( user_name === nil) - - handle = "handle=" + user_name.to_s - min = "min=" + index.to_s - count = "count=" + num_results.to_s - args = [handle, min, count].join( '&') - - return call_api( __method__, args) + def get_stylebook(user_name, offset = 0, limit = 10) + raise "no user_name provieded!" if user_name.nil? + args = { + :handle => user_name, + :offset => offset, + :limit => limit + } + call_api(__method__, args) end # This method returns information about looks that match different kinds of searches. @@ -123,15 +120,15 @@ def get_stylebook( user_name = nil, index = 0, num_results = 10) # The number of results to be returned. # @return [String] # A list of looks of the given type. - def get_looks( look_type = nil, index = 0, num_results = 10) - raise "invalid filter type must be one of the following: #{self.look_types}" if( !self.look_types.include?( look_type)) - - type = "type=" + look_type.to_s - min = "min=" + index.to_s - count = "count=" + num_results.to_s - args = [type, min, count].join( '&') - - return call_api( __method__, args) + def get_looks(look_type, offset = 0, limit = 10) + raise "invalid filter type must be one of the following: #{self.look_types}" unless self.look_types.include?(look_type) + # TODO Are these params correctly named? + args = { + :type => + look_type, + :min => + offset, + :count => + limit + } + call_api(__method__, args) end # TODO: @@ -146,8 +143,8 @@ def get_looks( look_type = nil, index = 0, num_results = 10) # editor. The "id" query parameter of that URL is the value to use for this API method. # @return [String] # A web link to the retailer. $1 of the $0 - def visit_retailer( id) - + def visit_retailer(id) + raise "TODO" end # This method returns the popular brands for a given category along with a sample product for the @@ -157,12 +154,12 @@ def visit_retailer( id) # parameter. If category is not supplied, all the popular brands regardless of category will be returned. # @return [String] A list of trends in the given category. Each trend has a brand, category, url, and # optionally the top-ranked product for each brand/category. - def get_trends( category = "", products = 0) - cat = "cat=" + category.to_s - products = "products=" + products.to_s - args = [cat, products].join( '&') - - return call_api( __method__, args) + def get_trends(category = "", products = 0) + args = { + :cat => category, + :products => products + } + call_api(__method__, args) end private @@ -172,29 +169,20 @@ def get_trends( category = "", products = 0) # @param [String] args # A concatenated group of arguments seperated by a an & symbol and spces substitued with a + symbol. # @return [String] A list of the data returned - def call_api( method, args = nil) - method_url = self.api_url + self.send( "#{method}_path") - pid = "pid=" + self.partner_id - format = "format=" + self.format - site = "site=" + self.site - - if( args === nil) then - uri = URI.parse( method_url.to_s + [pid, format, site].join('&').to_s) + def call_api(method, args = {}) + base_url = self.api_url + self.__send__("#{method}_path") + args[:pid] = self.partner_id + args[:format] = self.format + args[:site] = self.site + if base_url.include?("?") + base_url.chomp!("&") + base_url << "&" else - uri = URI.parse( method_url.to_s + [pid, format, site, args].join('&').to_s) + base_url << "?" end - - return Net::HTTP.get( uri) + base_url << args.map {|(k,v)| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join("&") + uri = URI.parse(base_url) + Net::HTTP.get(uri) end end end - - # @macro [new] api.index - # @param [Integer] index - # The start index of results returned. - # @macro [new] api.num_results - # @param [Integer] num_results - # The number of results to be returned. - # @macro [new] api.search_string - # @param [String] search_string - # The string to be in the query. diff --git a/lib/shopsense/client.rb b/lib/shopsense/client.rb index a77ce36..ad2ed92 100644 --- a/lib/shopsense/client.rb +++ b/lib/shopsense/client.rb @@ -1,13 +1,13 @@ module Shopsense class Client - def initialize( args = { }) - raise "No partner_id" if( !args.has_key?( 'partner_id')) + def initialize(args = {}) + raise "No partner_id" unless args.has_key?('partner_id') attr_accessors = { #: 'format' => 'json', 'site' => 'us'} - attr_accessors.each_key{ |key| (class << self; self; end).send( :attr_accessor, key.to_sym)} + attr_accessors.each_key{ |key| (class << self; self; end).send(:attr_accessor, key.to_sym)} attr_readers = { 'partner_id' => nil, @@ -23,17 +23,17 @@ def initialize( args = { }) 'look_types' => ['New', 'TopRated', 'Celebrities', 'Featured'], 'formats' => ['xml', 'json', 'json2', 'jsonvar', 'jsonvar2', 'jsonp', 'rss'], 'sites' => ['www.shopstyle.com', 'www.shopstyle.co.uk']} - attr_readers.each_key{ |key| (class << self; self; end).send( :attr_reader, key.to_sym)} + attr_readers.each_key{ |key| (class << self; self; end).send(:attr_reader, key.to_sym)} attr_writers = {} - attr_writers.each_key{ |key| (class << self; self; end).send( :attr_writer, key.to_sym)} + attr_writers.each_key{ |key| (class << self; self; end).send(:attr_writer, key.to_sym)} - attrs = attr_accessors.merge( attr_readers).merge( attr_writers) + attrs = attr_accessors.merge(attr_readers).merge(attr_writers) attrs.each_key do |key| - attrs[ key] = args[ key] if( args.has_key?( key)) + attrs[key] = args[key] if args.has_key?(key) end - attrs.each {|key, value| instance_variable_set( "@#{key}", value)} + attrs.each {|key, value| instance_variable_set("@#{key}", value)} end end From e8407c6dd7a6a1132c637baef8ba625ec5d59764 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 13:22:07 +0200 Subject: [PATCH 04/13] Removed ugly url delimiters --- lib/shopsense/client.rb | 14 +++++++------- spec/shopsense/shopsense_spec.rb | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/shopsense/client.rb b/lib/shopsense/client.rb index ad2ed92..71bd883 100644 --- a/lib/shopsense/client.rb +++ b/lib/shopsense/client.rb @@ -12,13 +12,13 @@ def initialize(args = {}) attr_readers = { 'partner_id' => nil, 'api_url' => 'http://api.shopstyle.com/api/v2', - 'search_path' => '/products?', - 'get_brands_path' => '/brands?', - 'get_retailers_path' => '/retailers?', - 'visit_retailers_path' => '/action/apiVisitRetailer?', - 'get_trends_path' => '/trends?', - 'get_category_histogram_path' => '/histogram?filters=Category&', - 'get_filter_histogram_path' => '/histogram?', + 'search_path' => '/products', + 'get_brands_path' => '/brands', + 'get_retailers_path' => '/retailers', + 'visit_retailers_path' => '/action/apiVisitRetailer', + 'get_trends_path' => '/trends', + 'get_category_histogram_path' => '/histogram?filters=Category', + 'get_filter_histogram_path' => '/histogram', 'filter_types' => ['Brands', 'Retailer', 'Price', 'Discount', 'Size', 'Color'], 'look_types' => ['New', 'TopRated', 'Celebrities', 'Featured'], 'formats' => ['xml', 'json', 'json2', 'jsonvar', 'jsonvar2', 'jsonp', 'rss'], diff --git a/spec/shopsense/shopsense_spec.rb b/spec/shopsense/shopsense_spec.rb index b4c10ee..8cf04a3 100644 --- a/spec/shopsense/shopsense_spec.rb +++ b/spec/shopsense/shopsense_spec.rb @@ -35,36 +35,36 @@ def rubify_string some_string fts = 'something' min = 10 count = 20 - api.search( fts, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.search_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) + api.search( fts, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.search_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) end end describe "get_category_histogram" do it "it passes if the proper data is returned" do fts = 'something' - api.get_category_histogram( fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_category_histogram_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}")) + api.get_category_histogram( fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_category_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}")) end end describe "get_filter_histogram" do it "it passes if the proper data is returned" do filter_type = 'Brands' fts = 'something' - api.get_filter_histogram( filter_type, fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_filter_histogram_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&filterType=#{filter_type}&fts=#{fts}")) + api.get_filter_histogram( filter_type, fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_filter_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&filterType=#{filter_type}&fts=#{fts}")) end end describe "get_brands" do it "it passes if the proper data is returned" do - api.get_brands.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_brands_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) + api.get_brands.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_brands_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) end end # describe "get_look" do # it "it passes if the proper data is returned" do # look_id = 548347 - # api.get_look( look_id).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_look_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&look=#{look_id}")) + # api.get_look( look_id).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_look_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&look=#{look_id}")) # end # end describe "get_retailers" do it "it passes if the proper data is returned" do - api.get_retailers.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_retailers_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) + api.get_retailers.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_retailers_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) end end # describe "get_stylebook" do @@ -72,7 +72,7 @@ def rubify_string some_string # handle = 'KalvinTestone' # min = 0 # count = 10 - # api.get_stylebook( handle, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_stylebook_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&handle=#{handle}&min=#{min}&count=#{count}")) + # api.get_stylebook( handle, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_stylebook_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&handle=#{handle}&min=#{min}&count=#{count}")) # end # end # describe "get_looks" do @@ -80,14 +80,14 @@ def rubify_string some_string # look_type = 'New' # min = 0 # count = 10 - # api.get_looks( look_type, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_looks_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&type=#{look_type}&min=#{min}&count=#{count}")) + # api.get_looks( look_type, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_looks_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&type=#{look_type}&min=#{min}&count=#{count}")) # end # end describe "get_trends" do it "it passes if the proper data is returned" do category = 0 products = 0 - api.get_trends( category, products).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_trends_path}pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&cat=#{category}&products=#{products}")) + api.get_trends( category, products).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_trends_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&cat=#{category}&products=#{products}")) end end end From 20ce4cb9c6d953b46778baf80a0581edb34600c0 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 13:43:14 +0200 Subject: [PATCH 05/13] Deprecated use of get_ style accessors --- Rakefile | 2 +- lib/shopsense/api.rb | 53 ++++++++++++++++++++------------ lib/shopsense/client.rb | 1 + spec/shopsense/shopsense_spec.rb | 16 +++++----- test/shopsense_test_config.yml | 1 + 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/Rakefile b/Rakefile index 462dd40..1fa2c0f 100644 --- a/Rakefile +++ b/Rakefile @@ -31,4 +31,4 @@ task :gh_pages do `git push origin gh-pages` end end -=end \ No newline at end of file +=end diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index 616f0c5..e58550c 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -19,21 +19,20 @@ def search(search_string, offset = 0, limit = 10) :offset => offset, :limit => limit } - call_api(__method__, args) + call_api(:search, args) end - # This method returns a list of categories and product counts that describe the results # of a given product query. The query is specified using the product query parameters. # @param [String] search_string The string to be in the query. # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. - def get_category_histogram(search_string) + def category_histogram(search_string) raise "no search string provieded!" if search_string.nil? args = { :fts => search_string } - call_api(__method__, args) + call_api(:get_category_histogram, args) end # This method returns a list of categories and product counts that describe the results @@ -44,21 +43,21 @@ def get_category_histogram(search_string) # @param [String] search_string The string to be in the query. # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. - def get_filter_histogram(filter_type, search_string) + def filter_histogram(filter_type, search_string) raise "invalid filter type" unless self.filter_types.include?(filter_type) raise "no search string provided!" if search_string.nil? args = { :fts => search_string, :filters => filter_type } - call_api(__method__, args) + call_api(:get_filter_histogram, args) end # This method returns a list of brands that have live products. Brands that have # very few products will be omitted. # @return [String] A list of all Brands, with id, name, url, and synonyms of each. - def get_brands - call_api(__method__) + def brands + call_api(:get_brands) end # This method returns information about a particular look and its products. @@ -70,18 +69,18 @@ def get_brands # URL is the value to use for this API method. # @return [String] single look, with title, description, a set of tags, and a list of products. # The products have the fields listed (see #search) - def get_look(look_id) + def look(look_id) raise "no look_id provieded!" if look_id.nil? args = { :look => look_id } - call_api(__method__, args) + call_api(:get_look, args) end # This method returns a list of retailers that have live products. # @return [Sting] A list of all Retailers, with id, name, and url of each. - def get_retailers - call_api(__method__) + def retailers + call_api(:get_retailers) end # This method returns information about a particular user's Stylebook, the @@ -95,14 +94,14 @@ def get_retailers # @return [String] # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, # and the title and description associated with each look. - def get_stylebook(user_name, offset = 0, limit = 10) + def stylebook(user_name, offset = 0, limit = 10) raise "no user_name provieded!" if user_name.nil? args = { :handle => user_name, :offset => offset, :limit => limit } - call_api(__method__, args) + call_api(:get_stylebook, args) end # This method returns information about looks that match different kinds of searches. @@ -120,7 +119,7 @@ def get_stylebook(user_name, offset = 0, limit = 10) # The number of results to be returned. # @return [String] # A list of looks of the given type. - def get_looks(look_type, offset = 0, limit = 10) + def looks(look_type, offset = 0, limit = 10) raise "invalid filter type must be one of the following: #{self.look_types}" unless self.look_types.include?(look_type) # TODO Are these params correctly named? args = { @@ -128,7 +127,7 @@ def get_looks(look_type, offset = 0, limit = 10) :min => + offset, :count => + limit } - call_api(__method__, args) + call_api(:get_looks, args) end # TODO: @@ -154,13 +153,24 @@ def visit_retailer(id) # parameter. If category is not supplied, all the popular brands regardless of category will be returned. # @return [String] A list of trends in the given category. Each trend has a brand, category, url, and # optionally the top-ranked product for each brand/category. - def get_trends(category = "", products = 0) + def trends(category = "", products = 0) args = { :cat => category, :products => products } - call_api(__method__, args) + call_api(:get_trends, args) end + + # Deprecated - Kept here for BC + alias_method :get_category_histogram, :category_histogram + alias_method :get_filter_histogram, :filter_histogram + alias_method :get_brands, :brands + alias_method :get_look, :look + alias_method :get_retailers, :retailers + alias_method :get_stylebook, :stylebook + alias_method :get_looks, :looks + alias_method :get_trends, :trends + private # This method is used for making the http calls building off the DSL of this module. @@ -182,7 +192,12 @@ def call_api(method, args = {}) end base_url << args.map {|(k,v)| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join("&") uri = URI.parse(base_url) - Net::HTTP.get(uri) + data = Net::HTTP.get(uri) + if self.unserialize + Yajl::Parser.new(:symbolize_keys => true).parse(data) + else + data + end end end end diff --git a/lib/shopsense/client.rb b/lib/shopsense/client.rb index 71bd883..34586ea 100644 --- a/lib/shopsense/client.rb +++ b/lib/shopsense/client.rb @@ -6,6 +6,7 @@ def initialize(args = {}) attr_accessors = { #: 'format' => 'json', + 'unserialize' => true, 'site' => 'us'} attr_accessors.each_key{ |key| (class << self; self; end).send(:attr_accessor, key.to_sym)} diff --git a/spec/shopsense/shopsense_spec.rb b/spec/shopsense/shopsense_spec.rb index 8cf04a3..68fcc60 100644 --- a/spec/shopsense/shopsense_spec.rb +++ b/spec/shopsense/shopsense_spec.rb @@ -41,30 +41,30 @@ def rubify_string some_string describe "get_category_histogram" do it "it passes if the proper data is returned" do fts = 'something' - api.get_category_histogram( fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_category_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}")) + api.category_histogram( fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_category_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}")) end end describe "get_filter_histogram" do it "it passes if the proper data is returned" do filter_type = 'Brands' fts = 'something' - api.get_filter_histogram( filter_type, fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_filter_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&filterType=#{filter_type}&fts=#{fts}")) + api.filter_histogram( filter_type, fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_filter_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&filterType=#{filter_type}&fts=#{fts}")) end end describe "get_brands" do it "it passes if the proper data is returned" do - api.get_brands.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_brands_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) + api.brands.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_brands_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) end end # describe "get_look" do # it "it passes if the proper data is returned" do # look_id = 548347 - # api.get_look( look_id).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_look_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&look=#{look_id}")) + # api.look( look_id).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_look_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&look=#{look_id}")) # end # end describe "get_retailers" do it "it passes if the proper data is returned" do - api.get_retailers.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_retailers_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) + api.retailers.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_retailers_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) end end # describe "get_stylebook" do @@ -72,7 +72,7 @@ def rubify_string some_string # handle = 'KalvinTestone' # min = 0 # count = 10 - # api.get_stylebook( handle, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_stylebook_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&handle=#{handle}&min=#{min}&count=#{count}")) + # api.stylebook( handle, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_stylebook_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&handle=#{handle}&min=#{min}&count=#{count}")) # end # end # describe "get_looks" do @@ -80,14 +80,14 @@ def rubify_string some_string # look_type = 'New' # min = 0 # count = 10 - # api.get_looks( look_type, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_looks_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&type=#{look_type}&min=#{min}&count=#{count}")) + # api.looks( look_type, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_looks_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&type=#{look_type}&min=#{min}&count=#{count}")) # end # end describe "get_trends" do it "it passes if the proper data is returned" do category = 0 products = 0 - api.get_trends( category, products).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_trends_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&cat=#{category}&products=#{products}")) + api.trends( category, products).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_trends_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&cat=#{category}&products=#{products}")) end end end diff --git a/test/shopsense_test_config.yml b/test/shopsense_test_config.yml index d2d0646..5eb7714 100644 --- a/test/shopsense_test_config.yml +++ b/test/shopsense_test_config.yml @@ -1,3 +1,4 @@ partner_id: uid7849-6112293-28 format: json +unserialize: false site: www.shopstyle.com \ No newline at end of file From 316566b616597d6b0603a0aea7d9fe8b5e20a330 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 15:13:30 +0200 Subject: [PATCH 06/13] Refactored Configuration away to composite class. Introduced optional support for unserialization --- lib/shopsense.rb | 3 +- lib/shopsense/api.rb | 31 +++++--- lib/shopsense/{client.rb => configuration.rb} | 2 +- spec/shopsense/shopsense_spec.rb | 73 +++++++++++-------- 4 files changed, 63 insertions(+), 46 deletions(-) rename lib/shopsense/{client.rb => configuration.rb} (98%) diff --git a/lib/shopsense.rb b/lib/shopsense.rb index 03abe09..d08e089 100644 --- a/lib/shopsense.rb +++ b/lib/shopsense.rb @@ -1,5 +1,4 @@ require 'net/http' -require 'yajl' require 'cgi' =begin rdoc Shopsense is an easy to use Ruby interface for the {Shopstyle API}[https://shopsense.shopstyle.com/page/ShopSenseHome], @@ -21,5 +20,5 @@ see Shopsense::API =end -require 'shopsense/client' +require 'shopsense/configuration' require 'shopsense/api' diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index e58550c..60bbf5a 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -1,6 +1,12 @@ module Shopsense - class API < Client + class API + + attr_reader :configuration + + def initialize(args = {}) + @configuration = Configuration.new(args) + end # Searches the shopsense API # @param [String] search_string @@ -44,7 +50,7 @@ def category_histogram(search_string) # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. def filter_histogram(filter_type, search_string) - raise "invalid filter type" unless self.filter_types.include?(filter_type) + raise "invalid filter type" unless @configuration.filter_types.include?(filter_type) raise "no search string provided!" if search_string.nil? args = { :fts => search_string, @@ -120,12 +126,12 @@ def stylebook(user_name, offset = 0, limit = 10) # @return [String] # A list of looks of the given type. def looks(look_type, offset = 0, limit = 10) - raise "invalid filter type must be one of the following: #{self.look_types}" unless self.look_types.include?(look_type) + raise "invalid filter type must be one of the following: #{@configuration.look_types}" unless @configuration.look_types.include?(look_type) # TODO Are these params correctly named? args = { - :type => + look_type, - :min => + offset, - :count => + limit + :type => look_type, + :min => offset, + :count => limit } call_api(:get_looks, args) end @@ -180,10 +186,10 @@ def trends(category = "", products = 0) # A concatenated group of arguments seperated by a an & symbol and spces substitued with a + symbol. # @return [String] A list of the data returned def call_api(method, args = {}) - base_url = self.api_url + self.__send__("#{method}_path") - args[:pid] = self.partner_id - args[:format] = self.format - args[:site] = self.site + base_url = @configuration.api_url + @configuration.__send__("#{method}_path") + args[:pid] = @configuration.partner_id + args[:format] = @configuration.format + args[:site] = @configuration.site if base_url.include?("?") base_url.chomp!("&") base_url << "&" @@ -193,8 +199,9 @@ def call_api(method, args = {}) base_url << args.map {|(k,v)| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join("&") uri = URI.parse(base_url) data = Net::HTTP.get(uri) - if self.unserialize - Yajl::Parser.new(:symbolize_keys => true).parse(data) + if @configuration.unserialize + require 'multi_json' + MultiJson.load(data, :symbolize_keys => true) else data end diff --git a/lib/shopsense/client.rb b/lib/shopsense/configuration.rb similarity index 98% rename from lib/shopsense/client.rb rename to lib/shopsense/configuration.rb index 34586ea..46ad5cd 100644 --- a/lib/shopsense/client.rb +++ b/lib/shopsense/configuration.rb @@ -1,5 +1,5 @@ module Shopsense - class Client + class Configuration def initialize(args = {}) raise "No partner_id" unless args.has_key?('partner_id') diff --git a/spec/shopsense/shopsense_spec.rb b/spec/shopsense/shopsense_spec.rb index 68fcc60..2ce37d3 100644 --- a/spec/shopsense/shopsense_spec.rb +++ b/spec/shopsense/shopsense_spec.rb @@ -1,70 +1,66 @@ require './spec/spec_helper' require 'json' -def rubify_string some_string - return some_string.gsub(/[A-Z]+/){ |s| '_' + s.downcase} -end - describe Shopsense do - let( :test_input) {YAML.load_file( 'test/shopsense_test_config.yml')} - describe Shopsense::Client do - let( :client) {Shopsense::Client.new( test_input)} - describe "initialization of 'Client' object" do + let(:test_input) { YAML.load_file('test/shopsense_test_config.yml') } + describe Shopsense::Configuration do + let(:configuration) { Shopsense::Configuration.new(test_input) } + describe "initialization of 'Configuration' object" do it "passes if 'partner_id' is defined properly" do - client.partner_id.should == test_input['partner_id'] + configuration.partner_id.should == test_input['partner_id'] end it "passes if 'format' is defined properly" do - client.format.should == test_input['format'] + configuration.format.should == test_input['format'] end it "passes with a valid 'site'" do - client.site.should == test_input['site'] + configuration.site.should == test_input['site'] end it "passes if 'format' is able to be updated" do new_format = 'xml' - client.format = new_format - client.format.should == new_format + configuration.format = new_format + configuration.format.should == new_format end end end - describe Shopsense::API do - let( :api) {Shopsense::API.new( test_input)} + let(:api) { Shopsense::API.new(test_input) } + let(:configuration) { api.configuration } describe "search" do it "it passes if the proper data is returned" do fts = 'something' min = 10 count = 20 - api.search( fts, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.search_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) + api.search(fts, min, count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.search_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) end end describe "get_category_histogram" do it "it passes if the proper data is returned" do fts = 'something' - api.category_histogram( fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_category_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&fts=#{fts}")) + api.category_histogram(fts).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_category_histogram_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&fts=#{fts}")) end end describe "get_filter_histogram" do it "it passes if the proper data is returned" do filter_type = 'Brands' fts = 'something' - api.filter_histogram( filter_type, fts).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_filter_histogram_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&filterType=#{filter_type}&fts=#{fts}")) + api.filter_histogram(filter_type, fts).should == Net::HTTP.get(URI.parse( "#{configuration.api_url}#{configuration.get_filter_histogram_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&filterType=#{filter_type}&fts=#{fts}")) end end describe "get_brands" do it "it passes if the proper data is returned" do - api.brands.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_brands_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) + api.brands.should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_brands_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}")) end end # describe "get_look" do # it "it passes if the proper data is returned" do # look_id = 548347 - # api.look( look_id).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_look_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&look=#{look_id}")) + # api.look( look_id).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_look_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&look=#{look_id}")) # end # end describe "get_retailers" do it "it passes if the proper data is returned" do - api.retailers.should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_retailers_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}")) + api.retailers.should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_retailers_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}")) end end # describe "get_stylebook" do @@ -72,22 +68,37 @@ def rubify_string some_string # handle = 'KalvinTestone' # min = 0 # count = 10 - # api.stylebook( handle, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_stylebook_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&handle=#{handle}&min=#{min}&count=#{count}")) - # end - # end - # describe "get_looks" do - # it "it passes if the proper data is returned" do - # look_type = 'New' - # min = 0 - # count = 10 - # api.looks( look_type, min, count).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_looks_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&type=#{look_type}&min=#{min}&count=#{count}")) + # api.stylebook(handle, min, count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_stylebook_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&handle=#{handle}&min=#{min}&count=#{count}")) # end # end +=begin + describe "get_looks" do + it "it passes if the proper data is returned" do + look_type = 'New' + min = 0 + count = 10 + api.looks(look_type, min, count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_looks_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&type=#{look_type}&min=#{min}&count=#{count}")) + end + end +=end describe "get_trends" do it "it passes if the proper data is returned" do category = 0 products = 0 - api.trends( category, products).should == Net::HTTP.get( URI.parse( "#{api.api_url}#{api.get_trends_path}?pid=#{api.partner_id}&format=#{api.format}&site=#{api.site}&cat=#{category}&products=#{products}")) + api.trends(category, products).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_trends_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&cat=#{category}&products=#{products}")) + end + end + + describe "With native unserialization" do + let(:api) { Shopsense::API.new(test_input.merge("unserialize" => true)) } + let(:configuration) { api.configuration } + describe "search" do + it "it passes if the proper data is returned" do + fts = 'something' + min = 10 + count = 20 + api.search(fts, min, count).should have_key(:metadata) + end end end end From 025545e8b4a0af6ed722f05d3e346daed2c9f6c1 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 15:14:12 +0200 Subject: [PATCH 07/13] Fixed spelling error --- lib/shopsense/api.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index 60bbf5a..dc2f506 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -19,7 +19,7 @@ def initialize(args = {}) # description, price, retailer, brand name, categories, images in small/medium/large, # and a URL that forwards to the retailer's site. def search(search_string, offset = 0, limit = 10) - raise "no search string provieded!" if search_string.nil? + raise "no search string provided!" if search_string.nil? args = { :fts => search_string, :offset => offset, @@ -34,7 +34,7 @@ def search(search_string, offset = 0, limit = 10) # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. def category_histogram(search_string) - raise "no search string provieded!" if search_string.nil? + raise "no search string provided!" if search_string.nil? args = { :fts => search_string } @@ -76,7 +76,7 @@ def brands # @return [String] single look, with title, description, a set of tags, and a list of products. # The products have the fields listed (see #search) def look(look_id) - raise "no look_id provieded!" if look_id.nil? + raise "no look_id provided!" if look_id.nil? args = { :look => look_id } @@ -101,7 +101,7 @@ def retailers # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, # and the title and description associated with each look. def stylebook(user_name, offset = 0, limit = 10) - raise "no user_name provieded!" if user_name.nil? + raise "no user_name provided!" if user_name.nil? args = { :handle => user_name, :offset => offset, From 0cdc96565585f7d98532a814781ee9af5371f222 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 15:27:38 +0200 Subject: [PATCH 08/13] Changed limit and offset to named parameters --- lib/shopsense/api.rb | 21 ++++++++++++--------- spec/shopsense/shopsense_spec.rb | 9 ++++----- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index dc2f506..67e741a 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -18,12 +18,13 @@ def initialize(args = {}) # @return A list of Product objects. Each Product has an id, name, # description, price, retailer, brand name, categories, images in small/medium/large, # and a URL that forwards to the retailer's site. - def search(search_string, offset = 0, limit = 10) + def search(search_string, opts = {}) + opts = {:offset => 0, :limit => 10}.merge(opts) raise "no search string provided!" if search_string.nil? args = { :fts => search_string, - :offset => offset, - :limit => limit + :offset => opts[:offset], + :limit => opts[:limit] } call_api(:search, args) end @@ -100,12 +101,13 @@ def retailers # @return [String] # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, # and the title and description associated with each look. - def stylebook(user_name, offset = 0, limit = 10) + def stylebook(user_name, opts = {}) + opts = {:offset => 0, :limit => 10}.merge(opts) raise "no user_name provided!" if user_name.nil? args = { :handle => user_name, - :offset => offset, - :limit => limit + :offset => opts[:offset], + :limit => opts[:limit] } call_api(:get_stylebook, args) end @@ -125,13 +127,14 @@ def stylebook(user_name, offset = 0, limit = 10) # The number of results to be returned. # @return [String] # A list of looks of the given type. - def looks(look_type, offset = 0, limit = 10) + def looks(look_type, opts = {}) + opts = {:offset => 0, :limit => 10}.merge(opts) raise "invalid filter type must be one of the following: #{@configuration.look_types}" unless @configuration.look_types.include?(look_type) # TODO Are these params correctly named? args = { :type => look_type, - :min => offset, - :count => limit + :min => opts[:offset], + :count => opts[:limit] } call_api(:get_looks, args) end diff --git a/spec/shopsense/shopsense_spec.rb b/spec/shopsense/shopsense_spec.rb index 2ce37d3..d4a5c91 100644 --- a/spec/shopsense/shopsense_spec.rb +++ b/spec/shopsense/shopsense_spec.rb @@ -1,5 +1,4 @@ require './spec/spec_helper' -require 'json' describe Shopsense do let(:test_input) { YAML.load_file('test/shopsense_test_config.yml') } @@ -31,7 +30,7 @@ fts = 'something' min = 10 count = 20 - api.search(fts, min, count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.search_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) + api.search(fts, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.search_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) end end describe "get_category_histogram" do @@ -68,7 +67,7 @@ # handle = 'KalvinTestone' # min = 0 # count = 10 - # api.stylebook(handle, min, count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_stylebook_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&handle=#{handle}&min=#{min}&count=#{count}")) + # api.stylebook(handle, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_stylebook_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&handle=#{handle}&min=#{min}&count=#{count}")) # end # end =begin @@ -77,7 +76,7 @@ look_type = 'New' min = 0 count = 10 - api.looks(look_type, min, count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_looks_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&type=#{look_type}&min=#{min}&count=#{count}")) + api.looks(look_type, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_looks_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&type=#{look_type}&min=#{min}&count=#{count}")) end end =end @@ -97,7 +96,7 @@ fts = 'something' min = 10 count = 20 - api.search(fts, min, count).should have_key(:metadata) + api.search(fts, :offset => min, :limit => count).should have_key(:metadata) end end end From d9258a37c25447d63e084455645cb74a5941c47a Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 15:57:15 +0200 Subject: [PATCH 09/13] Simplyfied configuration further - Marked unsupported api calls as TODO --- lib/shopsense.rb | 1 - lib/shopsense/api.rb | 62 +++++++++++++------------------- lib/shopsense/configuration.rb | 41 --------------------- spec/shopsense/shopsense_spec.rb | 37 ++++++------------- 4 files changed, 35 insertions(+), 106 deletions(-) delete mode 100644 lib/shopsense/configuration.rb diff --git a/lib/shopsense.rb b/lib/shopsense.rb index d08e089..4b45887 100644 --- a/lib/shopsense.rb +++ b/lib/shopsense.rb @@ -20,5 +20,4 @@ see Shopsense::API =end -require 'shopsense/configuration' require 'shopsense/api' diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index 67e741a..a6d6464 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -1,11 +1,17 @@ module Shopsense + API_ENDPOINT = 'http://api.shopstyle.com/api/v2' + FILTER_TYPES = ['Brands', 'Retailer', 'Price', 'Discount', 'Size', 'Color'].freeze + LOOK_TYPES = ['New', 'TopRated', 'Celebrities', 'Featured'].freeze + class API attr_reader :configuration def initialize(args = {}) - @configuration = Configuration.new(args) + @partner_id = args['partner_id'] + @unserialize = args['unserialize'] + @site = args['site'] || 'www.shopstyle.com' end # Searches the shopsense API @@ -26,7 +32,7 @@ def search(search_string, opts = {}) :offset => opts[:offset], :limit => opts[:limit] } - call_api(:search, args) + call_api('/products', args) end # This method returns a list of categories and product counts that describe the results @@ -37,9 +43,10 @@ def search(search_string, opts = {}) def category_histogram(search_string) raise "no search string provided!" if search_string.nil? args = { - :fts => search_string + :fts => search_string, + :filters => 'Category' } - call_api(:get_category_histogram, args) + call_api('/histogram', args) end # This method returns a list of categories and product counts that describe the results @@ -51,20 +58,20 @@ def category_histogram(search_string) # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. def filter_histogram(filter_type, search_string) - raise "invalid filter type" unless @configuration.filter_types.include?(filter_type) + raise "invalid filter type" unless FILTER_TYPES.include?(filter_type) raise "no search string provided!" if search_string.nil? args = { :fts => search_string, :filters => filter_type } - call_api(:get_filter_histogram, args) + call_api('/histogram', args) end # This method returns a list of brands that have live products. Brands that have # very few products will be omitted. # @return [String] A list of all Brands, with id, name, url, and synonyms of each. def brands - call_api(:get_brands) + call_api('/brands') end # This method returns information about a particular look and its products. @@ -77,17 +84,13 @@ def brands # @return [String] single look, with title, description, a set of tags, and a list of products. # The products have the fields listed (see #search) def look(look_id) - raise "no look_id provided!" if look_id.nil? - args = { - :look => look_id - } - call_api(:get_look, args) + raise "TODO" end # This method returns a list of retailers that have live products. # @return [Sting] A list of all Retailers, with id, name, and url of each. def retailers - call_api(:get_retailers) + call_api('/retailers') end # This method returns information about a particular user's Stylebook, the @@ -102,14 +105,7 @@ def retailers # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, # and the title and description associated with each look. def stylebook(user_name, opts = {}) - opts = {:offset => 0, :limit => 10}.merge(opts) - raise "no user_name provided!" if user_name.nil? - args = { - :handle => user_name, - :offset => opts[:offset], - :limit => opts[:limit] - } - call_api(:get_stylebook, args) + raise "TODO" end # This method returns information about looks that match different kinds of searches. @@ -128,15 +124,7 @@ def stylebook(user_name, opts = {}) # @return [String] # A list of looks of the given type. def looks(look_type, opts = {}) - opts = {:offset => 0, :limit => 10}.merge(opts) - raise "invalid filter type must be one of the following: #{@configuration.look_types}" unless @configuration.look_types.include?(look_type) - # TODO Are these params correctly named? - args = { - :type => look_type, - :min => opts[:offset], - :count => opts[:limit] - } - call_api(:get_looks, args) + raise "TODO" end # TODO: @@ -167,7 +155,7 @@ def trends(category = "", products = 0) :cat => category, :products => products } - call_api(:get_trends, args) + call_api('/trends', args) end # Deprecated - Kept here for BC @@ -188,11 +176,11 @@ def trends(category = "", products = 0) # @param [String] args # A concatenated group of arguments seperated by a an & symbol and spces substitued with a + symbol. # @return [String] A list of the data returned - def call_api(method, args = {}) - base_url = @configuration.api_url + @configuration.__send__("#{method}_path") - args[:pid] = @configuration.partner_id - args[:format] = @configuration.format - args[:site] = @configuration.site + def call_api(relative_url, args = {}) + base_url = API_ENDPOINT + relative_url + args[:pid] = @partner_id + args[:format] = 'json' + args[:site] = @site if base_url.include?("?") base_url.chomp!("&") base_url << "&" @@ -202,7 +190,7 @@ def call_api(method, args = {}) base_url << args.map {|(k,v)| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join("&") uri = URI.parse(base_url) data = Net::HTTP.get(uri) - if @configuration.unserialize + if @unserialize require 'multi_json' MultiJson.load(data, :symbolize_keys => true) else diff --git a/lib/shopsense/configuration.rb b/lib/shopsense/configuration.rb deleted file mode 100644 index 46ad5cd..0000000 --- a/lib/shopsense/configuration.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Shopsense - class Configuration - def initialize(args = {}) - raise "No partner_id" unless args.has_key?('partner_id') - - attr_accessors = { - #: - 'format' => 'json', - 'unserialize' => true, - 'site' => 'us'} - attr_accessors.each_key{ |key| (class << self; self; end).send(:attr_accessor, key.to_sym)} - - attr_readers = { - 'partner_id' => nil, - 'api_url' => 'http://api.shopstyle.com/api/v2', - 'search_path' => '/products', - 'get_brands_path' => '/brands', - 'get_retailers_path' => '/retailers', - 'visit_retailers_path' => '/action/apiVisitRetailer', - 'get_trends_path' => '/trends', - 'get_category_histogram_path' => '/histogram?filters=Category', - 'get_filter_histogram_path' => '/histogram', - 'filter_types' => ['Brands', 'Retailer', 'Price', 'Discount', 'Size', 'Color'], - 'look_types' => ['New', 'TopRated', 'Celebrities', 'Featured'], - 'formats' => ['xml', 'json', 'json2', 'jsonvar', 'jsonvar2', 'jsonp', 'rss'], - 'sites' => ['www.shopstyle.com', 'www.shopstyle.co.uk']} - attr_readers.each_key{ |key| (class << self; self; end).send(:attr_reader, key.to_sym)} - - attr_writers = {} - attr_writers.each_key{ |key| (class << self; self; end).send(:attr_writer, key.to_sym)} - - attrs = attr_accessors.merge(attr_readers).merge(attr_writers) - attrs.each_key do |key| - attrs[key] = args[key] if args.has_key?(key) - end - - attrs.each {|key, value| instance_variable_set("@#{key}", value)} - end - end - -end diff --git a/spec/shopsense/shopsense_spec.rb b/spec/shopsense/shopsense_spec.rb index d4a5c91..d90be3a 100644 --- a/spec/shopsense/shopsense_spec.rb +++ b/spec/shopsense/shopsense_spec.rb @@ -1,54 +1,38 @@ require './spec/spec_helper' +require 'yaml' describe Shopsense do let(:test_input) { YAML.load_file('test/shopsense_test_config.yml') } - describe Shopsense::Configuration do - let(:configuration) { Shopsense::Configuration.new(test_input) } - describe "initialization of 'Configuration' object" do - it "passes if 'partner_id' is defined properly" do - configuration.partner_id.should == test_input['partner_id'] - end - it "passes if 'format' is defined properly" do - configuration.format.should == test_input['format'] - end - it "passes with a valid 'site'" do - configuration.site.should == test_input['site'] - end - it "passes if 'format' is able to be updated" do - new_format = 'xml' - configuration.format = new_format - configuration.format.should == new_format - end - end - end + let(:partner_id) { + test_input['partner_id'] + } describe Shopsense::API do let(:api) { Shopsense::API.new(test_input) } - let(:configuration) { api.configuration } describe "search" do it "it passes if the proper data is returned" do fts = 'something' min = 10 count = 20 - api.search(fts, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.search_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&fts=#{fts}&offset=#{min}&limit=#{count}")) + api.search(fts, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/products?pid=#{partner_id}&format=json&site=us&fts=#{fts}&offset=#{min}&limit=#{count}")) end end describe "get_category_histogram" do it "it passes if the proper data is returned" do fts = 'something' - api.category_histogram(fts).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_category_histogram_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&fts=#{fts}")) + api.category_histogram(fts).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/histogram?pid=#{partner_id}&format=json&site=us&filters=Category&fts=#{fts}")) end end describe "get_filter_histogram" do it "it passes if the proper data is returned" do filter_type = 'Brands' fts = 'something' - api.filter_histogram(filter_type, fts).should == Net::HTTP.get(URI.parse( "#{configuration.api_url}#{configuration.get_filter_histogram_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&filterType=#{filter_type}&fts=#{fts}")) + api.filter_histogram(filter_type, fts).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/histogram?pid=#{partner_id}&format=json&site=us&filterType=#{filter_type}&fts=#{fts}")) end end describe "get_brands" do it "it passes if the proper data is returned" do - api.brands.should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_brands_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}")) + api.brands.should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/brands?pid=#{partner_id}&format=json&site=us")) end end # describe "get_look" do @@ -59,7 +43,7 @@ # end describe "get_retailers" do it "it passes if the proper data is returned" do - api.retailers.should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_retailers_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}")) + api.retailers.should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/retailers?pid=#{partner_id}&format=json&site=us")) end end # describe "get_stylebook" do @@ -84,13 +68,12 @@ it "it passes if the proper data is returned" do category = 0 products = 0 - api.trends(category, products).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_trends_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&cat=#{category}&products=#{products}")) + api.trends(category, products).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/trends?pid=#{partner_id}&format=json&site=us&cat=#{category}&products=#{products}")) end end describe "With native unserialization" do let(:api) { Shopsense::API.new(test_input.merge("unserialize" => true)) } - let(:configuration) { api.configuration } describe "search" do it "it passes if the proper data is returned" do fts = 'something' From 8a35f032f6c619538c9202e81bf843de5e3dcf36 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 21:35:58 +0200 Subject: [PATCH 10/13] Backported old v1 calls and marked them as deprecated --- lib/shopsense/api.rb | 206 +++++++++++++++++++------------ spec/shopsense/shopsense_spec.rb | 46 ++++--- 2 files changed, 150 insertions(+), 102 deletions(-) diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index a6d6464..7fefe88 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -1,16 +1,14 @@ module Shopsense - API_ENDPOINT = 'http://api.shopstyle.com/api/v2' - FILTER_TYPES = ['Brands', 'Retailer', 'Price', 'Discount', 'Size', 'Color'].freeze + API_ENDPOINTS = {1 => 'http://api.shopstyle.com/action', 2 => 'http://api.shopstyle.com/api/v2'} + FILTER_TYPES = ['Category', 'Brand', 'Retailer', 'Price', 'Discount', 'Size', 'Color'].freeze LOOK_TYPES = ['New', 'TopRated', 'Celebrities', 'Featured'].freeze class API - attr_reader :configuration - def initialize(args = {}) @partner_id = args['partner_id'] - @unserialize = args['unserialize'] + @unserialize = args['unserialize'].nil? ? true : args['unserialize'] @site = args['site'] || 'www.shopstyle.com' end @@ -24,15 +22,13 @@ def initialize(args = {}) # @return A list of Product objects. Each Product has an id, name, # description, price, retailer, brand name, categories, images in small/medium/large, # and a URL that forwards to the retailer's site. - def search(search_string, opts = {}) - opts = {:offset => 0, :limit => 10}.merge(opts) - raise "no search string provided!" if search_string.nil? - args = { - :fts => search_string, - :offset => opts[:offset], - :limit => opts[:limit] - } - call_api('/products', args) + def products(opts = {}) + raise "Interface changed. Use as :query => 'search'" if opts.kind_of? String + call_api('/products', build_product_query(opts)) + end + + def product_by_id(product_id) + call_api('/products/' + CGI::escape(product_id.to_s)) end # This method returns a list of categories and product counts that describe the results @@ -40,13 +36,8 @@ def search(search_string, opts = {}) # @param [String] search_string The string to be in the query. # @return [String] A list of Category objects. Each Category has an id, name, and count # of the number of query results in that category. - def category_histogram(search_string) - raise "no search string provided!" if search_string.nil? - args = { - :fts => search_string, - :filters => 'Category' - } - call_api('/histogram', args) + def category_histogram(opts) + filter_histogram('Category', opts) end # This method returns a list of categories and product counts that describe the results @@ -55,38 +46,39 @@ def category_histogram(search_string) # The type of filter data to return. Possible values are # Brand, Retailer, Price, Discount, Size and Color. # @param [String] search_string The string to be in the query. - # @return [String] A list of Category objects. Each Category has an id, name, and count - # of the number of query results in that category. - def filter_histogram(filter_type, search_string) - raise "invalid filter type" unless FILTER_TYPES.include?(filter_type) - raise "no search string provided!" if search_string.nil? - args = { - :fts => search_string, - :filters => filter_type - } - call_api('/histogram', args) + # @return [String] A list of Filter objects of the given type. Each Filter has an id, + # name, and count of the number of results that apply to that filter. + def filter_histogram(filter_type, opts = {}) + raise "Interface changed. Use as :query => 'search'" if opts.kind_of? String + filter_types = [*filter_type] + filter_types.each do |f| + raise "invalid filter type" unless FILTER_TYPES.include?(f) + end + args = build_product_query(opts) + args[:filters] = filter_types.join(",") + args[:floor] = opts[:floor] + call_api('/products/histogram', args) + end + + # This method returns a list of the categories available to the API. + # @return A list of all categories, with id, name, and parent id. + def categories + call_api('/categories') + end + + # This method returns the list of canonical colors available. + # @return A list of all Colors, with id, name, and url of each. + def colors + call_api('/colors') end # This method returns a list of brands that have live products. Brands that have # very few products will be omitted. - # @return [String] A list of all Brands, with id, name, url, and synonyms of each. + # @return A list of all Brands, with id, name, url, and synonyms of each. def brands call_api('/brands') end - # This method returns information about a particular look and its products. - # @param [Integer] look_id - # The ID number of the look. An easy way to get a look's ID is - # to go to the Stylebook page that contains the look at the ShopStyle website and - # right-click on the button that you use to edit the look. From the popup menu, select - # "Copy Link" and paste that into any text editor. The "lookId" query parameter of that - # URL is the value to use for this API method. - # @return [String] single look, with title, description, a set of tags, and a list of products. - # The products have the fields listed (see #search) - def look(look_id) - raise "TODO" - end - # This method returns a list of retailers that have live products. # @return [Sting] A list of all Retailers, with id, name, and url of each. def retailers @@ -104,10 +96,31 @@ def retailers # @return [String] # A look id of the user's Stylebook, the look id of each individual look within that Stylebook, # and the title and description associated with each look. - def stylebook(user_name, opts = {}) - raise "TODO" + def stylebook(opts = {}) + opts = {:offset => 0, :limit => 10}.merge(opts) + args = { + :handle => opts[:username], + :min => opts[:offset], + :count => opts[:limit], + } + call_api('/apiGetStylebook', args, 1) + end + + # @deprecated + # This method returns information about a particular look and its products. + # @param [Integer] look_id + # The ID number of the look. An easy way to get a look's ID is + # to go to the Stylebook page that contains the look at the ShopStyle website and + # right-click on the button that you use to edit the look. From the popup menu, select + # "Copy Link" and paste that into any text editor. The "lookId" query parameter of that + # URL is the value to use for this API method. + # @return [String] single look, with title, description, a set of tags, and a list of products. + # The products have the fields listed (see #search) + def look(look_id) + call_api('/apiGetLook', {:look => look_id}, 1) end + # @deprecated # This method returns information about looks that match different kinds of searches. # @param [Integer] look_type # The type of search to perform. Supported values are: @@ -115,8 +128,6 @@ def stylebook(user_name, opts = {}) # TopRated - Recently created looks that are highly rated. # Celebrities - Looks owned by celebrity users. # Featured - Looks from featured stylebooks. - # @param [String] username - # The username of the Stylebook owner. # @param [Integer] index # The start index of results returned. # @param [Integer] num_results @@ -124,10 +135,19 @@ def stylebook(user_name, opts = {}) # @return [String] # A list of looks of the given type. def looks(look_type, opts = {}) - raise "TODO" + opts = {:offset => 0, :limit => 10}.merge(opts) + args = { + :type => look_type, + :min => opts[:offset], + :count => opts[:limit], + } + call_api('/apiGetLooks', args, 1) end - # TODO: + # @deprecated + # This method provides a link that will redirect to the retailer product page. + # You probably won't need this as the link is also included with the product structs. + # # This method does not return a reponse of XML or JSON data like the other elements of the API. # Instead, it forwards the user to the retailer's product page for a given product. It is the # typical behavior to offer when the user clicks on a product. The apiSearch method returns URLs @@ -139,10 +159,11 @@ def looks(look_type, opts = {}) # editor. The "id" query parameter of that URL is the value to use for this API method. # @return [String] # A web link to the retailer. $1 of the $0 - def visit_retailer(id) - raise "TODO" + def visit_retailer_href(product_id) + API_ENDPOINTS[1] + "/apiVisitRetailer?pid=#{CGI::escape(@partner_id.to_s)}&id=#{CGI::escape(product_id.to_s)}" end + # @deprecated # This method returns the popular brands for a given category along with a sample product for the # brand-category combination. # @param [String] category @@ -150,46 +171,77 @@ def visit_retailer(id) # parameter. If category is not supplied, all the popular brands regardless of category will be returned. # @return [String] A list of trends in the given category. Each trend has a brand, category, url, and # optionally the top-ranked product for each brand/category. - def trends(category = "", products = 0) + def trends(opts = {}) + opts = {:include_products => true}.merge(opts) args = { - :cat => category, - :products => products + :cat => opts[:category], + :products => opts[:include_products] ? nil : "0" } - call_api('/trends', args) + call_api('/apiGetTrends', args, 1) end - # Deprecated - Kept here for BC - alias_method :get_category_histogram, :category_histogram - alias_method :get_filter_histogram, :filter_histogram - alias_method :get_brands, :brands - alias_method :get_look, :look - alias_method :get_retailers, :retailers - alias_method :get_stylebook, :stylebook - alias_method :get_looks, :looks - alias_method :get_trends, :trends - private + def build_product_query(opts = {}) + opts = {:offset => 0, :limit => 10}.merge(opts) + args = { + :fts => opts[:query], + :offset => opts[:offset], + :limit => opts[:limit], + :cat => opts[:category_id] + } + filters = [] + {:brand_id => "b", :retailer_id => "r", :price => "p", :sale => "d", :size => "s", :color => "c"}.each do |key,prefix| + if opts[key] + [*opts[key]].each do |value| + filters << "#{prefix}#{value}" + end + end + end + if filters.any? + args[:fl] = filters + end + if opts[:price_dropped_since] + d = opts[:price_dropped_since] + args[:pdd] = d.kind_of?(DateTime) ? d.to_time.to_i : d.to_i + end + args[:sort] = case opts[:sort_by] + when :price_lo_hi + 'PriceLoHi' + when :price_hi_lo + 'PriceHiLo' + when :recency + 'Recency' + when :popular + 'Popular' + end + args + end + # This method is used for making the http calls building off the DSL of this module. # @param [String] method # The method which is to be used in the call to Shopsense # @param [String] args # A concatenated group of arguments seperated by a an & symbol and spces substitued with a + symbol. # @return [String] A list of the data returned - def call_api(relative_url, args = {}) - base_url = API_ENDPOINT + relative_url + def call_api(relative_url, args = {}, api_version = 2) args[:pid] = @partner_id args[:format] = 'json' args[:site] = @site - if base_url.include?("?") - base_url.chomp!("&") - base_url << "&" - else - base_url << "?" + compiled_args = args.delete_if {|k,v| v.nil? }.map {|(k,v)| + if v.kind_of? Array + v.map {|vv| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join("&") + else + "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" + end + }.join("&") + href = API_ENDPOINTS[api_version] + relative_url + "?" + compiled_args + uri = URI.parse(href) + response = Net::HTTP.get_response(uri) + data = response.body + if response.code.to_i > 299 || response.code.to_i < 200 + raise data end - base_url << args.map {|(k,v)| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join("&") - uri = URI.parse(base_url) - data = Net::HTTP.get(uri) if @unserialize require 'multi_json' MultiJson.load(data, :symbolize_keys => true) diff --git a/spec/shopsense/shopsense_spec.rb b/spec/shopsense/shopsense_spec.rb index d90be3a..9097888 100644 --- a/spec/shopsense/shopsense_spec.rb +++ b/spec/shopsense/shopsense_spec.rb @@ -14,20 +14,20 @@ fts = 'something' min = 10 count = 20 - api.search(fts, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/products?pid=#{partner_id}&format=json&site=us&fts=#{fts}&offset=#{min}&limit=#{count}")) + api.products(:query => fts, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/products?pid=#{partner_id}&format=json&site=us&fts=#{fts}&offset=#{min}&limit=#{count}")) end end describe "get_category_histogram" do it "it passes if the proper data is returned" do fts = 'something' - api.category_histogram(fts).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/histogram?pid=#{partner_id}&format=json&site=us&filters=Category&fts=#{fts}")) + api.category_histogram(:query => fts).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/products/histogram?pid=#{partner_id}&format=json&site=us&filters=Category&fts=#{fts}")) end end describe "get_filter_histogram" do it "it passes if the proper data is returned" do - filter_type = 'Brands' + filter_type = 'Brand' fts = 'something' - api.filter_histogram(filter_type, fts).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/histogram?pid=#{partner_id}&format=json&site=us&filterType=#{filter_type}&fts=#{fts}")) + api.filter_histogram(filter_type, :query => fts).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/products/histogram?pid=#{partner_id}&format=json&site=us&filters=#{filter_type}&fts=#{fts}")) end end describe "get_brands" do @@ -35,40 +35,36 @@ api.brands.should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/brands?pid=#{partner_id}&format=json&site=us")) end end - # describe "get_look" do - # it "it passes if the proper data is returned" do - # look_id = 548347 - # api.look( look_id).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_look_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&look=#{look_id}")) - # end - # end + describe "get_look" do + it "it passes if the proper data is returned" do + look_id = 548347 + api.look(look_id).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/action/apiGetLook?look=#{look_id}&pid=#{partner_id}&format=json&site=us")) + end + end describe "get_retailers" do it "it passes if the proper data is returned" do api.retailers.should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/retailers?pid=#{partner_id}&format=json&site=us")) end end - # describe "get_stylebook" do - # it "it passes if the proper data is returned" do - # handle = 'KalvinTestone' - # min = 0 - # count = 10 - # api.stylebook(handle, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_stylebook_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&handle=#{handle}&min=#{min}&count=#{count}")) - # end - # end -=begin + describe "get_stylebook" do + it "it passes if the proper data is returned" do + handle = 'KalvinTestone' + min = 0 + count = 10 + api.stylebook(:username => handle, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/action/apiGetStylebook?pid=#{partner_id}&format=json&site=us&handle=#{handle}&min=#{min}&count=#{count}&handle=KalvinTestone")) + end + end describe "get_looks" do it "it passes if the proper data is returned" do look_type = 'New' min = 0 count = 10 - api.looks(look_type, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("#{configuration.api_url}#{configuration.get_looks_path}?pid=#{configuration.partner_id}&format=#{configuration.format}&site=#{configuration.site}&type=#{look_type}&min=#{min}&count=#{count}")) + api.looks(look_type, :offset => min, :limit => count).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/action/apiGetLooks?type=#{look_type}&min=#{min}&count=#{count}&pid=#{partner_id}&format=json&site=us")) end end -=end describe "get_trends" do it "it passes if the proper data is returned" do - category = 0 - products = 0 - api.trends(category, products).should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/api/v2/trends?pid=#{partner_id}&format=json&site=us&cat=#{category}&products=#{products}")) + api.trends.should == Net::HTTP.get(URI.parse("http://api.shopstyle.com/action/apiGetTrends?pid=#{partner_id}&format=json&site=us")) end end @@ -79,7 +75,7 @@ fts = 'something' min = 10 count = 20 - api.search(fts, :offset => min, :limit => count).should have_key(:metadata) + api.search(:query => fts, :offset => min, :limit => count).should have_key(:metadata) end end end From 3ea3c9df13b151e441f6806d73e06db0486720ed Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Tue, 21 May 2013 22:23:32 +0200 Subject: [PATCH 11/13] Utilize http keep-alive --- lib/shopsense/api.rb | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index 7fefe88..efe5fa3 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -4,12 +4,20 @@ module Shopsense FILTER_TYPES = ['Category', 'Brand', 'Retailer', 'Price', 'Discount', 'Size', 'Color'].freeze LOOK_TYPES = ['New', 'TopRated', 'Celebrities', 'Featured'].freeze + class HTTP < Net::HTTP + def socket_closed? + @socket && @socket.closed? + end + end + class API def initialize(args = {}) @partner_id = args['partner_id'] @unserialize = args['unserialize'].nil? ? true : args['unserialize'] + @http_keep_alive = args['http_keep_alive'].nil? ? true : args['http_keep_alive'] @site = args['site'] || 'www.shopstyle.com' + @http_session = {} end # Searches the shopsense API @@ -237,7 +245,7 @@ def call_api(relative_url, args = {}, api_version = 2) }.join("&") href = API_ENDPOINTS[api_version] + relative_url + "?" + compiled_args uri = URI.parse(href) - response = Net::HTTP.get_response(uri) + response = http_session_for(uri).request(Net::HTTP::Get.new(uri.request_uri)) data = response.body if response.code.to_i > 299 || response.code.to_i < 200 raise data @@ -249,5 +257,22 @@ def call_api(relative_url, args = {}, api_version = 2) data end end + + def http_session_for(uri) + key = "#{uri.scheme}://#{uri.host}:#{uri.port}" + unless @http_session[key] && !@http_session[key].socket_closed? + http = Shopsense::HTTP.new(uri.host, uri.port) + http.use_ssl = true if uri.scheme == "https" + http.open_timeout = 2 + http.read_timeout = 10 + if @http_keep_alive + http.start + @http_session[key] = http + end + return http + end + @http_session[key] + end + end end From 88fc13383dc92f597743004a1724f8a595cc4c36 Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Thu, 23 May 2013 14:59:43 +0200 Subject: [PATCH 12/13] Added singleton management --- lib/shopsense/api.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/shopsense/api.rb b/lib/shopsense/api.rb index efe5fa3..1655936 100644 --- a/lib/shopsense/api.rb +++ b/lib/shopsense/api.rb @@ -10,6 +10,19 @@ def socket_closed? end end + def self.configuration + @configuration ||= {} + end + + # Shopsense.configuration = YAML.load_file('config/shopsense.yml') + def self.configuration=(conf) + @configuration = conf + end + + def self.api + @api ||= Shopsense::API.new(Shopsense.configuration) + end + class API def initialize(args = {}) From b5c07f5e5bb1d334136d593fd04b63bfdd5175fe Mon Sep 17 00:00:00 2001 From: Troels Knak-Nielsen Date: Thu, 10 Jul 2014 09:24:54 +0200 Subject: [PATCH 13/13] Removed reference to popsugar branch of code --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 60a6427..f64459b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Install with Bundler Add the folowing to your Gemfile ``` -gem "shopsense-ruby", :git => "git://github.com/PopSugar/ruby-popsugar-shopping-api.git", :branch => "master" +gem "shopsense-ruby" ``` Include it in you Ruby application: