chrome_data 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8afb6ab74cdffac02f50225de7260a9ef4e1456a
4
+ data.tar.gz: 33e84a8718d9cfe54b5e255ddf68a474a93138cd
5
+ SHA512:
6
+ metadata.gz: e6cdddc5b6a76f2d4d5e103f17961daa30945f5fb8e8abb9be8c10f5316415061dc4fd8bab1a7026dddfb8da1338d834e822136a0b4512a51a9d7c43fe910d90
7
+ data.tar.gz: c9cec11f40b545fdcb87ab665b0b9b73d1584ba2c4a06a9cb972bfa0a82260ef22be0be34d8a01cab9ce323dee0a0646567d98b3338055315360a22554d3fcae
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chrome_data.gemspec
4
+ gemspec
5
+
6
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Room 118 Solutions, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # ChromeData
2
+
3
+ Provides a simple ruby interface for Chrome Data's API. Read more about it here: http://www.chromedata.com/
4
+
5
+ The wonderful [lolsoap](https://github.com/loco2/lolsoap) gem does most of the heavy lifting.
6
+
7
+ ## Installation
8
+
9
+ Add this gem to your application's Gemfile:
10
+
11
+ gem 'chrome_data'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install chrome_data
20
+
21
+ ## Usage
22
+
23
+ ### Configuration
24
+ Valid options:
25
+
26
+ * account_number (required)
27
+ * account_secret (required)
28
+ * country (default: 'US')
29
+ * language (default: 'en')
30
+ * cache_store
31
+
32
+ Configuration:
33
+
34
+ ChromeData.config.merge! { account_number: 1234, account_secret: '5678' }
35
+
36
+ ### Requests
37
+ #### Data Collection
38
+ #### Makes (Divisions)
39
+ Fields:
40
+
41
+ * id (Integer)
42
+ * name (String)
43
+
44
+ Request a set of divisions:
45
+
46
+ ChromeData::Division.find_all_by_year(1999)
47
+
48
+ Request models for a division (same as ChromeData::Model.find_all_by_year_and_division_id)
49
+
50
+ mazda = ChromeData::Division.new(id: 26, name: "Mazda")
51
+ mazda_models = mazda.models_for_year(1999)
52
+
53
+ #### Models
54
+ Fields:
55
+
56
+ * id (Integer)
57
+ * name (String)
58
+
59
+ Find models for year and division
60
+
61
+ mazda_models = ChromeData::Model.find_all_by_year_and_division_id(1999, 26)
62
+
63
+ Find styles for a specific model (same as ChromeData::Style.find_all_by_model_id)
64
+
65
+ miata = ChromeData::Model.new(id: 4768, name: "MX-5 Miata") # 1999 Mazda MX-5 Miata
66
+ miata_styles = miata.styles
67
+
68
+ #### Styles
69
+ Fields:
70
+
71
+ * id (Integer)
72
+ * name (String)
73
+
74
+ Only loaded through a Vehicle:
75
+
76
+ * trim (String)
77
+ * name_without_trim (String)
78
+
79
+ Find styles for a model by model id
80
+
81
+ miata_styles = ChromeData::Style.find_all_by_model_id(4768)
82
+
83
+ #### Vehicle
84
+ Fields:
85
+
86
+ * division (String)
87
+ * engines (Array of Engine)
88
+ * model (String)
89
+ * model_year (Integer)
90
+ * styles (Array of Style)
91
+
92
+ Find a vehicle by VIN
93
+
94
+ vehicle = ChromeData::Vehicle.find_by_vin('JM1NB3536X0131402')
95
+
96
+ #### Engine
97
+ Engines are only loaded through a find_by_vin request
98
+
99
+ Fields:
100
+
101
+ * type (String)
102
+
103
+ #### Model Years
104
+ Years start at 1981 due to VIN standardization
105
+
106
+ ChromeData::ModelYears.all
107
+
108
+ ## Contributing
109
+
110
+ 1. Fork it
111
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
112
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
113
+ 4. Push to the branch (`git push origin my-new-feature`)
114
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "lib"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chrome_data/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "chrome_data"
8
+ spec.version = ChromeData::VERSION
9
+ spec.authors = ["Jim Ryan"]
10
+ spec.email = ["jim@room118solutions.com"]
11
+ spec.description = %q{Provides a ruby interface for Chrome Data's API. Read more about it here: http://www.chromedata.com/}
12
+ spec.summary = %q{A ruby interface for Chrome Data's API}
13
+ spec.homepage = "http://github.com/room118solutions/chrome_data"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "minitest"
24
+ spec.add_development_dependency "vcr"
25
+ spec.add_development_dependency "webmock", '~> 1.10.0' # Locked at 1.10.x to prevent VCR warnings
26
+ spec.add_development_dependency "mocha"
27
+
28
+ spec.add_dependency "symboltable"
29
+ spec.add_dependency "activesupport", '>= 3.0'
30
+ spec.add_dependency "lolsoap", "~> 0.2.0"
31
+ end
@@ -0,0 +1,88 @@
1
+ require "net/http"
2
+ require "lolsoap"
3
+
4
+ module ChromeData
5
+ class BaseRequest
6
+ def initialize(attrs={})
7
+ attrs.each do |k, v|
8
+ send "#{k}=", v
9
+ end
10
+ end
11
+
12
+ class << self
13
+ # Builds request, sets additional data on request element, makes request,
14
+ # and returns array of child elements wrapped in instances of this class
15
+ def request(data)
16
+ request = build_request
17
+
18
+ request.body do |b|
19
+ # Set configured account info on builder
20
+ b.accountInfo(
21
+ number: ChromeData.config.account_number,
22
+ secret: ChromeData.config.account_secret,
23
+ country: ChromeData.config.country,
24
+ language: ChromeData.config.language
25
+ )
26
+
27
+ # Set additional elements on builder
28
+ data.each do |k, v|
29
+ # Add the key/value pair as an attribute to the request element if that's what it should be,
30
+ # otherwise add it as a sub-element
31
+ # NOTE: This basically mirrors LolSoap::Builder#method_missing
32
+ # because Builder undefines most methods, including #send
33
+ if b.__type__.has_attribute?(k)
34
+ b.__attribute__ k, v
35
+ else
36
+ b.__tag__ k, v
37
+ end
38
+ end
39
+ end
40
+
41
+ # Make the request
42
+ response = make_request(request)
43
+
44
+ parse_response(response)
45
+ end
46
+
47
+ # Makes request, returns LolSoap::Response
48
+ def make_request(request)
49
+ raw_response = Net::HTTP.start(endpoint_uri.host, endpoint_uri.port) do |http|
50
+ http.request_post(endpoint_uri.path, request.content, request.headers)
51
+ end
52
+
53
+ client.response(request, raw_response.body)
54
+ end
55
+
56
+ # Builds request, returns LolSoap::Request
57
+ def build_request
58
+ client.request request_name
59
+ end
60
+
61
+ def client
62
+ @@client ||= LolSoap::Client.new(wsdl_body)
63
+ end
64
+
65
+ def wsdl_body
66
+ @@wsdl_body ||= Net::HTTP.get_response(URI('http://services.chromedata.com/Description/7a?wsdl')).body
67
+ end
68
+
69
+ def endpoint_uri
70
+ @@endpoint_uri ||= URI(client.wsdl.endpoint)
71
+ end
72
+
73
+ # Given an element_name and LolSoap::Response, returns an array of Nokogiri::XML::Elements
74
+ def find_elements(element_name, response)
75
+ response.body.xpath(".//x:#{element_name}", 'x' => response.body.namespace.href)
76
+ end
77
+
78
+ # Internal: Given a LolSoap::Response, returns appropriately parsed response
79
+ def parse_response(response)
80
+ raise NotImplementedError, '.parse_response should be implemented in subclass'
81
+ end
82
+
83
+ def request_name
84
+ raise NotImplementedError, '.request_name should be implemented in subclass'
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ module ChromeData::Caching
2
+ # Creates CacheStore object based on config.cache_store options
3
+ # reverse merges 'chromedata' namespace to options hash
4
+ def _cache_store
5
+ return @@_cache_store if defined? @@_cache_store
6
+
7
+ if ChromeData.config.cache_store
8
+ cache_opts = { namespace: 'chromedata' }
9
+
10
+ if ChromeData.config.cache_store.is_a?(Array)
11
+ if ChromeData.config.cache_store.last.is_a?(Hash)
12
+ ChromeData.config.cache_store.last.reverse_merge! cache_opts
13
+ else
14
+ ChromeData.config.cache_store << cache_opts
15
+ end
16
+ elsif ChromeData.config.cache_store.is_a?(Symbol)
17
+ ChromeData.config.cache_store = [ChromeData.config.cache_store, cache_opts]
18
+ end
19
+
20
+ @@_cache_store = ActiveSupport::Cache.lookup_store(ChromeData.config.cache_store)
21
+ end
22
+ end
23
+
24
+ def cache(key, &blk)
25
+ if ChromeData._cache_store
26
+ ChromeData._cache_store.fetch key, &blk
27
+ else
28
+ blk.call
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ module ChromeData
2
+ class CollectionRequest < BaseRequest
3
+ attr_accessor :id, :name
4
+
5
+ class << self
6
+ def request_name
7
+ # Cheap-o inflector
8
+ "get#{name.split('::').last}s"
9
+ end
10
+
11
+ # Find elements matching class name and instantiate them using their id attribute and text
12
+ def parse_response(response)
13
+ find_elements(name.split('::').last.downcase, response).map do |e|
14
+ new id: e.attr('id').to_i, name: e.text
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module ChromeData
2
+ class Division < CollectionRequest
3
+
4
+ def models_for_year(year)
5
+ Model.find_all_by_year_and_division_id year, id
6
+ end
7
+
8
+ def self.find_all_by_year(year)
9
+ ChromeData.cache "#{request_name.underscore}-model_year-#{year}" do
10
+ request 'modelYear' => year
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module ChromeData
2
+ class Model < CollectionRequest
3
+ def styles
4
+ Style.find_all_by_model_id id
5
+ end
6
+
7
+ def self.find_all_by_year_and_division_id(year, division_id)
8
+ ChromeData.cache "#{request_name.underscore}-model_year-#{year}-division_id-#{division_id}" do
9
+ request 'modelYear' => year, 'divisionId' => division_id
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # NOTE: Chrome Data's API offers a getModelYears method that returns every year from 1981 through to next year.
2
+ # We could hit the API for that data, but I think that's silly, so this class calculates the years
3
+ # that Chrome Data would return.
4
+
5
+ module ChromeData
6
+ class ModelYear
7
+
8
+ # Chrome Data returns years in the opposite order, but that's not typically
9
+ # how selects are built, so they're reversed here,
10
+ # since that's the only purpose that I can see for this method.
11
+ def self.all
12
+ (Time.now.year + 1).downto(1981).to_a
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module ChromeData
2
+ class Style < CollectionRequest
3
+ class BodyType < Struct.new(:id, :name); end
4
+
5
+ # These are only populated when accessing a style through a Vehicle
6
+ attr_accessor :trim, :name_without_trim, :body_types
7
+
8
+ def self.find_all_by_model_id(model_id)
9
+ ChromeData.cache "#{request_name.underscore}-model_id-#{model_id}" do
10
+ request 'modelId' => model_id
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ module ChromeData
2
+ class Vehicle < BaseRequest
3
+ class Engine < Struct.new(:type); end
4
+
5
+ attr_accessor :model_year, :division, :model, :styles, :engines
6
+
7
+ class << self
8
+ def request_name
9
+ "describeVehicle"
10
+ end
11
+
12
+ def find_by_vin(vin)
13
+ request 'vin' => vin
14
+ end
15
+
16
+ def parse_response(response)
17
+ if vin_description = find_elements('vinDescription', response).first
18
+ new.tap do |v|
19
+ v.model_year = vin_description.attr('modelYear').to_i
20
+ v.division = vin_description.attr('division')
21
+ v.model = vin_description.attr('modelName')
22
+
23
+ v.styles = find_elements('style', response).map do |e|
24
+ Style.new(
25
+ id: e.attr('id').to_i,
26
+ name: e.attr('name'),
27
+ trim: e.attr('trim'),
28
+ name_without_trim: e.attr('nameWoTrim'),
29
+ body_types: e.xpath("x:bodyType", 'x' => response.body.namespace.href).map do |bt|
30
+ Style::BodyType.new(bt.attr('id').to_i, bt.text)
31
+ end
32
+ )
33
+ end
34
+
35
+ v.engines = find_elements('engine', response).map do |e|
36
+ Engine.new(e.at_xpath("x:engineType", 'x' => response.body.namespace.href).text)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module ChromeData
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,32 @@
1
+ require "chrome_data/version"
2
+ require "chrome_data/caching"
3
+ require "chrome_data/base_request"
4
+ require "chrome_data/collection_request"
5
+ require "chrome_data/division"
6
+ require "chrome_data/model"
7
+ require "chrome_data/style"
8
+ require "chrome_data/model_year"
9
+ require "chrome_data/vehicle"
10
+ require "active_support/cache"
11
+ require "active_support/core_ext/hash"
12
+
13
+ require "symboltable"
14
+
15
+ module ChromeData
16
+ extend Caching
17
+ extend self
18
+
19
+ def configure
20
+ yield config
21
+ end
22
+
23
+ # Valid options:
24
+ # account_number
25
+ # account_secret
26
+ # country (default: 'US')
27
+ # language (default: 'en')
28
+ # cache_store
29
+ def config
30
+ @@config ||= SymbolTable.new country: 'US', language: 'en'
31
+ end
32
+ end
@@ -0,0 +1,74 @@
1
+ require_relative '../minitest_helper'
2
+
3
+ describe ChromeData::Caching do
4
+ before do
5
+ ChromeData.remove_class_variable :@@config if ChromeData.class_variable_defined? :@@config
6
+ ChromeData::Caching.remove_class_variable :@@_cache_store if ChromeData::Caching.class_variable_defined? :@@_cache_store
7
+ end
8
+
9
+ describe '.cache' do
10
+ it 'caches request when caching is on' do
11
+ ChromeData.config.cache_store = :memory_store
12
+
13
+ foo = stub('foo')
14
+ foo.expects(:bar).once.returns 'bar'
15
+
16
+ 2.times do
17
+ ChromeData.cache 'foo' do
18
+ foo.bar
19
+ end.must_equal 'bar'
20
+ end
21
+ end
22
+
23
+ it 'does not cache request when caching is off' do
24
+ foo = stub('foo')
25
+ foo.expects(:bar).twice.returns 'bar'
26
+
27
+ 2.times do
28
+ ChromeData.cache 'foo' do
29
+ foo.bar
30
+ end.must_equal 'bar'
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '._cache_store' do
36
+ before { ChromeData::Caching.remove_class_variable :@@_cache_store if ChromeData::Caching.class_variable_defined? :@@_cache_store }
37
+
38
+ it 'returns nil with no cache_store config' do
39
+ ChromeData._cache_store.must_equal nil
40
+ end
41
+
42
+ it 'looks up cache with appropriate namespace when cache_store is an array without options hash' do
43
+ ChromeData.config.cache_store = :file_store, '/path/to/cache'
44
+
45
+ ActiveSupport::Cache.expects(:lookup_store).with([:file_store, '/path/to/cache', { namespace: 'chromedata' }])
46
+
47
+ ChromeData._cache_store
48
+ end
49
+
50
+ it 'looks up cache with appropriate namespace when cache_store is an array with an options hash' do
51
+ ChromeData.config.cache_store = :memory_store, { foo: 'bar' }
52
+
53
+ ActiveSupport::Cache.expects(:lookup_store).with([:memory_store, { foo: 'bar', namespace: 'chromedata' }])
54
+
55
+ ChromeData._cache_store
56
+ end
57
+
58
+ it 'does not replace user-defined namespace' do
59
+ ChromeData.config.cache_store = :memory_store, { namespace: 'mynamespace' }
60
+
61
+ ActiveSupport::Cache.expects(:lookup_store).with([:memory_store, { namespace: 'mynamespace' }])
62
+
63
+ ChromeData._cache_store
64
+ end
65
+
66
+ it 'looks up cache with appropriate namespace when cache_store is a symbol' do
67
+ ChromeData.config.cache_store = :memory_store
68
+
69
+ ActiveSupport::Cache.expects(:lookup_store).with([:memory_store, { namespace: 'chromedata' }])
70
+
71
+ ChromeData._cache_store
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,57 @@
1
+ require_relative '../minitest_helper'
2
+
3
+ describe ChromeData::Division do
4
+ it 'returns a proper request name' do
5
+ ChromeData::Division.request_name.must_equal 'getDivisions'
6
+ end
7
+
8
+ describe '.find_all_by_year' do
9
+ before do
10
+ ChromeData.configure do |c|
11
+ c.account_number = '123456'
12
+ c.account_secret = '1111111111111111'
13
+ end
14
+ end
15
+
16
+ def find_divisions
17
+ VCR.use_cassette('wsdl') do
18
+ VCR.use_cassette('2013/divisions') do
19
+ @divisions = ChromeData::Division.find_all_by_year(2013)
20
+ end
21
+ end
22
+ end
23
+
24
+ it 'returns array of Division objects' do
25
+ find_divisions
26
+
27
+ @divisions.first.must_be_instance_of ChromeData::Division
28
+ @divisions.size.must_equal 38
29
+ end
30
+
31
+ it 'sets ID on Division objects' do
32
+ find_divisions
33
+
34
+ @divisions.first.id.must_equal 1
35
+ end
36
+
37
+ it 'sets name on Division objects' do
38
+ find_divisions
39
+
40
+ @divisions.first.name.must_equal 'Acura'
41
+ end
42
+
43
+ it 'caches with proper key' do
44
+ ChromeData.expects(:cache).with('get_divisions-model_year-2013')
45
+
46
+ find_divisions
47
+ end
48
+ end
49
+
50
+ describe '#models_for_year' do
51
+ it 'finds models for given year and Division ID' do
52
+ ChromeData::Model.expects(:find_all_by_year_and_division_id).with(2013, 13)
53
+
54
+ ChromeData::Division.new(id: 13, name: 'Ford').models_for_year 2013
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,57 @@
1
+ require_relative '../minitest_helper'
2
+
3
+ describe ChromeData::Model do
4
+ it 'returns a proper request name' do
5
+ ChromeData::Model.request_name.must_equal 'getModels'
6
+ end
7
+
8
+ describe '.find_all_by_year_and_division_id' do
9
+ before do
10
+ ChromeData.configure do |c|
11
+ c.account_number = '123456'
12
+ c.account_secret = '1111111111111111'
13
+ end
14
+ end
15
+
16
+ def find_models
17
+ VCR.use_cassette('wsdl') do
18
+ VCR.use_cassette('2013/divisions/13/models') do
19
+ @models = ChromeData::Model.find_all_by_year_and_division_id(2013, 13) # 2013 Fords
20
+ end
21
+ end
22
+ end
23
+
24
+ it 'returns array of Model objects' do
25
+ find_models
26
+
27
+ @models.first.must_be_instance_of ChromeData::Model
28
+ @models.size.must_equal 39
29
+ end
30
+
31
+ it 'sets ID on Model objects' do
32
+ find_models
33
+
34
+ @models.first.id.must_equal 25459
35
+ end
36
+
37
+ it 'sets name on Model objects' do
38
+ find_models
39
+
40
+ @models.first.name.must_equal 'C-Max Energi'
41
+ end
42
+
43
+ it 'caches with proper key' do
44
+ ChromeData.expects(:cache).with('get_models-model_year-2013-division_id-13')
45
+
46
+ find_models
47
+ end
48
+ end
49
+
50
+ describe '#styles' do
51
+ it 'finds styles for Model' do
52
+ ChromeData::Style.expects(:find_all_by_model_id).with(24997)
53
+
54
+ ChromeData::Model.new(id: 24997, name: 'Mustang').styles
55
+ end
56
+ end
57
+ end