eaternet 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 90ec97b301bb75adbb97b5a7fb23cf99c03ac340
4
- data.tar.gz: 6e2eb97e0e443a47719d0f3f1a761ce6e680ab92
3
+ metadata.gz: 22ee77b16cad4131bcf6dfa1c970507b39569ba6
4
+ data.tar.gz: 8649494e45fedc9defa460d1423c2b1c83eee345
5
5
  SHA512:
6
- metadata.gz: 0937aa8f4e654f9bec8439ea39512cccf98a18067027a8fb0bfec7f21aff096b451e50c0ac33ddb01408050e39eda1e9d1a045c95b2ea2428179910ae5278b09
7
- data.tar.gz: 5fe045bf8704dc53721a94d66bed6e9c4ca638afa0d8c09f324aba00d46855771b9b9662d1d1ad650a5d6ca60185b3b451509bd55b39333a35a6cb5d3d9d3563
6
+ metadata.gz: d64e281aab4dada323e6fd72b5facf8b4287c6318b5132fa231b44e71ca0281e5c10c52a221577a35386d2bfc5d5dbcdd294a88c00d07da19fe8006f54d3247d
7
+ data.tar.gz: 0d1ed8e4684cb36c2af320556e5efca4805fc3647216c34c83770c9c086972c89b4404a23dbe457120ead76d0048e5eea96e439d81e99bba00060300bea9260d
data/.gitignore CHANGED
@@ -22,4 +22,3 @@ mkmf.log
22
22
  *.komodoproject
23
23
  /.idea
24
24
  *.sublime*
25
- >>>>>>> master
data/.inch.yml ADDED
@@ -0,0 +1,5 @@
1
+ files:
2
+ excluded:
3
+ - lib/eaternet/prototype.rb
4
+
5
+
data/Guardfile ADDED
@@ -0,0 +1,36 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features)
6
+
7
+ ## Uncomment to clear the screen before every task
8
+ # clearing :on
9
+
10
+ ## Guard internally checks for changes in the Guardfile and exits.
11
+ ## If you want Guard to automatically start up again, run guard in a
12
+ ## shell loop, e.g.:
13
+ ##
14
+ ## $ while bundle exec guard; do echo "Restarting Guard..."; done
15
+ ##
16
+ ## Note: if you are using the `directories` clause above and you are not
17
+ ## watching the project directory ('.'), then you will want to move
18
+ ## the Guardfile to a watched dir and symlink it back, e.g.
19
+ #
20
+ # $ mkdir config
21
+ # $ mv Guardfile config/
22
+ # $ ln -s config/Guardfile .
23
+ #
24
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
25
+
26
+ guard :minitest do
27
+ # with Minitest::Unit
28
+ watch(%r{^test/(.*)\/?test_(.*)\.rb$})
29
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
30
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
31
+
32
+ # with Minitest::Spec
33
+ # watch(%r{^spec/(.*)_spec\.rb$})
34
+ # watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
35
+ # watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
36
+ end
data/README.md CHANGED
@@ -1,18 +1,21 @@
1
1
  [![Build Status](https://travis-ci.org/eaternet/adapters-ruby.svg?branch=master)](https://travis-ci.org/eaternet/adapters-ruby)
2
2
  [![Code Climate](https://codeclimate.com/github/eaternet/adapters-ruby/badges/gpa.svg)](https://codeclimate.com/github/eaternet/adapters-ruby)
3
3
  [![Gem Version](https://badge.fury.io/rb/eaternet.svg)](http://badge.fury.io/rb/eaternet)
4
+ [![Inline docs](http://inch-ci.org/github/eaternet/adapters-ruby.svg?branch=master)](http://inch-ci.org/github/eaternet/adapters-ruby)
4
5
 
5
6
  # Eaternet Health Score Adapters
6
7
 
7
- **We're bringing the world's restaurant health inspections online,**
8
- into as many sites and apps as possible.
8
+ **We're bringing the world's restaurant health inspections online,** into as
9
+ many sites and apps as possible.
9
10
 
10
11
  Use this repo to publish your local restaurant health scores.
11
12
 
12
- **In a nutshell:** each agency which inspects restaurants can have an "adapter" which is a Ruby class.
13
- Its adapter converts its source format into a simple, standard one. Anybody can then use these adapters to get the
14
- data and do analysis and work with it how they like. [Eaternet](http://eaternet.io/), in particular, will use the production-ready
15
- adapters to pull and publish the information daily on our website and in feeds to partner organizations.
13
+ **In a nutshell:** each agency which inspects restaurants can have an
14
+ "adapter" which is a Ruby class. Its adapter converts its source format into a
15
+ simple, standard one. Anybody can then use these adapters to get the data and
16
+ do analysis and work with it how they like. [Eaternet](http://eaternet.io/),
17
+ in particular, will use the production-ready adapters to pull and publish the
18
+ information daily on our website and in feeds to partner organizations.
16
19
 
17
20
 
18
21
  ## Available Adapters
@@ -26,13 +29,13 @@ adapters to pull and publish the information daily on our website and in feeds t
26
29
 
27
30
  ## Usage
28
31
 
29
- Example: print all the restaurant names in the Southern Nevada Health District
32
+ Example: print all the restaurant names in New York City.
30
33
 
31
34
  ```ruby
32
35
  require 'eaternet'
33
36
 
34
- agency = Snhd.new
35
- agency.businesses.each { |biz| puts biz.name }
37
+ nyc = Eaternet::Nyc.new
38
+ nyc.businesses.each { |biz| puts biz.name }
36
39
  ```
37
40
 
38
41
  Each adapter provides [a small set of enumerators](https://github.com/eaternet/adapters-ruby/blob/master/lib/eaternet/adapters/framework.rb#L27-L39) such as `businesses` and `inspections`. See the [Las Vegas tests](https://github.com/eaternet/adapters-ruby/blob/master/test/snhd_adapter_test.rb) to learn how an application would use this gem. The framework also includes [standardized data transfer objects](https://github.com/eaternet/adapters-ruby/blob/master/lib/eaternet/adapters/framework.rb#L42-L103) which these enumerators return.
@@ -57,22 +60,19 @@ Or install it yourself as:
57
60
 
58
61
  ## Roadmap
59
62
 
60
- See the [eaternet/adapters](https://github.com/eaternet/adapters#roadmap) repo for the project roadmap.
63
+ See the [eaternet/adapters](https://github.com/eaternet/adapters#roadmap) repo
64
+ for the project roadmap.
61
65
 
62
66
  ## Contributing
63
67
 
64
- Write an adapter for your city's restaurant health scores. Or do a small refactor or add
65
- documentation. We're happy to get the help.
68
+ Write an adapter for your city's restaurant health scores. Or do a small
69
+ refactor or add documentation. We're happy to get the help.
66
70
 
67
- To add your local health scores, you just need to create a class which `include`'s `AbstractAdapter`.
68
- We like good tests, as well. Submit a pull requests to start a conversation, and we'll work with you.
71
+ To add your local health scores, you just need to create a class which
72
+ `include`'s `AbstractAdapter`. We like good tests, as well. Submit a pull
73
+ requests to start a conversation, and we'll work with you.
69
74
 
70
- Once a new adapter is production-ready, we'll plug it into the Eaternet system, and your adapter will be
71
- put into daily production. We're working with several organizations to publish the information. Yelp was
72
- the first to work with us, and we have new partnerships in the works which we'll announce soon.
73
-
74
- 1. Fork it ( https://github.com/eaternet/adapters/fork )
75
- 2. Create your feature branch (`git checkout -b my-new-feature`)
76
- 3. Commit your changes (`git commit -am 'Add some feature'`)
77
- 4. Push to the branch (`git push origin my-new-feature`)
78
- 5. Create a new Pull Request
75
+ Once a new adapter is production-ready, we'll plug it into the Eaternet
76
+ system, and your adapter will be put into daily production. We're working with
77
+ several organizations to publish the information. Yelp was the first to work
78
+ with us, and we have new partnerships in the works which we'll announce soon.
data/Rakefile CHANGED
@@ -7,4 +7,4 @@ Rake::TestTask.new do |t|
7
7
  t.verbose = false
8
8
  end
9
9
 
10
- task :default => :test
10
+ task default: :test
data/eaternet.gemspec CHANGED
@@ -4,26 +4,28 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'eaternet/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "eaternet"
7
+ spec.name = 'eaternet'
8
8
  spec.version = Eaternet::VERSION
9
- spec.authors = ["Robb Shecter"]
10
- spec.email = ["robb@eaternet.io"]
11
- spec.summary = %q{Regional adapters for restaurant health scores}
12
- spec.description = %q{A framework for easily retrieving a region's health scores in a standardized way.}
13
- spec.homepage = "https://github.com/eaternet/adapters-ruby"
14
- spec.license = "Apache"
9
+ spec.authors = ['Robb Shecter']
10
+ spec.email = ['robb@eaternet.io']
11
+ spec.summary = 'Regional adapters for restaurant health scores'
12
+ spec.description = "A framework for easily retrieving a region's health scores in a standardized way."
13
+ spec.homepage = 'https://github.com/eaternet/adapters-ruby'
14
+ spec.license = 'Apache'
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0").reject{ |f| f =~ /vcr_cassettes/ }
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f =~ /vcr_cassettes/ }
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
20
20
  spec.required_ruby_version = '>= 2.1.0'
21
21
 
22
- spec.add_development_dependency "bundler", "~> 1"
22
+ spec.add_development_dependency 'bundler', '~> 1'
23
+ spec.add_development_dependency 'guard'
24
+ spec.add_development_dependency 'guard-minitest'
23
25
  spec.add_development_dependency 'minitest'
24
26
  spec.add_development_dependency 'minitest-utils'
25
27
  spec.add_development_dependency 'pry'
26
- spec.add_development_dependency "rake", "~> 10"
28
+ spec.add_development_dependency 'rake', '~> 10'
27
29
  spec.add_development_dependency 'vcr'
28
30
  spec.add_development_dependency 'webmock'
29
31
 
data/lib/eaternet.rb CHANGED
@@ -1,9 +1,13 @@
1
- require 'eaternet/version'
2
1
  require 'eaternet/agencies/snhd'
3
2
  require 'eaternet/agencies/nyc'
4
3
 
5
- # Make the actual adapters easily available
6
- # to the application-level
7
- include Eaternet::Agencies
4
+ module Eaternet
5
+ # Make the actual adapters easily available
6
+ # to the application-level as:
7
+ #
8
+ # * `Eaternet::Nyc`
9
+ # * `Eaternet::Snhd`
10
+ include Eaternet::Agencies
11
+ end
8
12
 
9
13
  Zip.warn_invalid_date = false if ENV['RUBYZIP_NO_WARN_INVALID_DATE']
@@ -6,16 +6,29 @@ require 'set'
6
6
  require 'tempfile'
7
7
 
8
8
  require 'ext/lazy'
9
- require 'eaternet/framework/lives_1_0'
9
+
10
+ require 'eaternet/lives_1_0'
11
+ require 'eaternet/loggable'
12
+
10
13
 
11
14
  module Eaternet
12
15
  module Agencies
13
- # First-level parsing of New York City restaurant inspections
16
+ # A LIVES 1.0 data source for New York City.
17
+ #
18
+ # Example: print all the restaurant names in New York City.
19
+ #
20
+ # ```ruby
21
+ # require 'eaternet'
22
+ #
23
+ # nyc = Eaternet::Nyc.new
24
+ # nyc.businesses.each { |biz| puts biz.name }
25
+ # ```
14
26
  #
15
27
  # @see https://data.cityofnewyork.us/Health/DOHMH-New-York-City-Restaurant-Inspection-Results/xx67-kt59
16
- # @todo This is a very long namespace. Can this be improved?
17
- class Nyc < Eaternet::Framework::Lives_1_0::Adapter
18
- include Eaternet::Framework::Lives_1_0
28
+ # @todo This is a somewhat long namespace. Can this be improved?
29
+ class Nyc < Eaternet::Lives_1_0::Adapter
30
+ include Eaternet::Lives_1_0
31
+ include Eaternet::Loggable
19
32
 
20
33
  DOMAIN = 'data.cityofnewyork.us'
21
34
  DATASET = 'xx67-kt59'
@@ -26,14 +39,12 @@ module Eaternet
26
39
  # @param csv_path for unit testing
27
40
  def initialize(csv_path: nil)
28
41
  @table_file = csv_path
29
- @logger = Logger.new(STDERR)
30
- @logger.datetime_format = '%Y-%m-%d %H:%M:%S'
31
42
  end
32
43
 
33
44
  def businesses
34
45
  map_csv { |row| try_to_create_business(row) }
35
- .compact
36
- .unique
46
+ .compact
47
+ .unique
37
48
  end
38
49
 
39
50
  def inspections
@@ -43,7 +54,7 @@ module Eaternet
43
54
  nil
44
55
  else
45
56
  save_to_inspection_cache(cache, row)
46
- inspection(row)
57
+ try_to_create_inspection(row)
47
58
  end
48
59
  end.compact
49
60
  end
@@ -80,7 +91,7 @@ module Eaternet
80
91
  l.minimum_score = 0
81
92
  l.maximum_score = 72
82
93
  l.description = 'C'
83
- end,
94
+ end
84
95
  ]
85
96
  end
86
97
  end
@@ -89,14 +100,21 @@ module Eaternet
89
100
  private
90
101
 
91
102
  def try_to_create_business(row)
92
- begin
93
- business(row)
94
- rescue ArgumentError => e
95
- @logger.warn('Eaternet') do
96
- "Could not create a NYC Business from \"#{row.to_s.strip}\": #{e}"
97
- end
98
- nil
103
+ business(row)
104
+ rescue ArgumentError => e
105
+ logger.warn('Eaternet') do
106
+ "Could not create a NYC Business from #{row.inspect}: #{e}"
99
107
  end
108
+ nil
109
+ end
110
+
111
+ def try_to_create_inspection(row)
112
+ inspection(row)
113
+ rescue ArgumentError => e
114
+ logger.warn('Eaternet') do
115
+ "Could not create a NYC Inspection from #{row.inspect}: #{e}"
116
+ end
117
+ nil
100
118
  end
101
119
 
102
120
  def business(row)
@@ -154,11 +172,17 @@ module Eaternet
154
172
  end
155
173
 
156
174
  def transfat_inspection?(row)
157
- row['INSPECTION TYPE'].include?('Trans Fat')
175
+ if row['INSPECTION TYPE']
176
+ row['INSPECTION TYPE'].include?('Trans Fat')
177
+ else
178
+ false
179
+ end
158
180
  end
159
181
 
160
182
  def map_csv(&block)
161
- CSV.new(open(table_file), headers: true).lazy.map { |row| block.call(row) }
183
+ CSV.new(open(table_file, encoding: 'utf-8'), headers: true)
184
+ .lazy
185
+ .map { |row| block.call(row) }
162
186
  end
163
187
 
164
188
  def unique(objects)
@@ -1,6 +1,6 @@
1
1
  require 'csv'
2
2
  require 'eaternet/agencies/snhd_config'
3
- require 'eaternet/framework/prototype'
3
+ require 'eaternet/prototype'
4
4
  require 'eaternet/util'
5
5
 
6
6
  module Eaternet
@@ -10,34 +10,37 @@ module Eaternet
10
10
  # @example Print the names of all the Las Vegas businesses
11
11
  # require 'eaternet'
12
12
  #
13
- # agency = Snhd.new
13
+ # agency = Eaternet::Snhd.new
14
14
  # agency.businesses
15
15
  # .select { |biz| biz.city == 'Las Vegas' }
16
16
  # .each { |biz| puts biz.name }
17
17
  #
18
- # The SNHD makes their restaurant inspection data available via a zip file for download.
19
- # The file contains several Mysql tables converted to CSV. And so, this is an example
20
- # for how to code adapters for other agencies which publish their data in a similar way.
18
+ # The SNHD makes their restaurant inspection data available via a zip file
19
+ # for download. The file contains several Mysql tables converted to CSV.
20
+ # And so, this is an example for how to code adapters for other agencies
21
+ # which publish their data in a similar way.
21
22
  #
22
- # This code downloads the latest zip file, extracts it, parses the csv, and provides
23
- # enumerators of Businesses, Inspections, Violations, and ViolationKinds. (These are
24
- # specified in the Eaternet::Framework::Framework module.)
23
+ # This code downloads the latest zip file, extracts it, parses the csv,
24
+ # and provides enumerators of Businesses, Inspections, Violations, and
25
+ # ViolationKinds. (These are specified in the
26
+ # Eaternet::Prototype module.)
25
27
  #
26
- # SNHD's CSV format seems to be non-standard, e.g. using the double-quote character
27
- # in fields without escaping it. We need to find out what their quote character actually is.
28
- # In the meantime, the files can be parsed by setting the quote character to something else
29
- # that doesn't appear in the text, such as a pipe symbol.
28
+ # SNHD's CSV format seems to be non-standard, e.g. using the double-quote
29
+ # character in fields without escaping it. We need to find out what their
30
+ # quote character actually is. In the meantime, the files can be parsed by
31
+ # setting the quote character to something else that doesn't appear in the
32
+ # text, such as a pipe symbol.
30
33
  #
31
34
  # @see http://southernnevadahealthdistrict.org/restaurants/inspect-downloads.php
32
35
  # The SNHD Developer information page
33
- # @see Eaternet::Framework::Prototype Framework module
36
+ # @see Eaternet::Prototype Framework module
34
37
  class Snhd
35
- include Eaternet::Framework::Prototype::AbstractAdapter
38
+ include Eaternet::Prototype::AbstractAdapter
36
39
 
37
- # (see Eaternet::Framework::Framework::AbstractAdapter#businesses)
40
+ # (see Eaternet::Prototype::AbstractAdapter#businesses)
38
41
  def businesses
39
42
  lazy_csv_map('restaurant_establishments.csv') do |row|
40
- Eaternet::Framework::Prototype::BusinessData.new(
43
+ Eaternet::Prototype::BusinessData.new(
41
44
  orig_key: row['permit_number'],
42
45
  name: row['restaurant_name'],
43
46
  address: row['address'],
@@ -47,10 +50,10 @@ module Eaternet
47
50
  end
48
51
  end
49
52
 
50
- # (see Eaternet::Framework::Framework::AbstractAdapter#inspections)
53
+ # (see Eaternet::Prototype::AbstractAdapter#inspections)
51
54
  def inspections
52
55
  lazy_csv_map('restaurant_inspections.csv') do |row|
53
- Eaternet::Framework::Prototype::InspectionData.new(
56
+ Eaternet::Prototype::InspectionData.new(
54
57
  orig_key: row['serial_number'],
55
58
  business_orig_key: row['permit_number'],
56
59
  score: row['inspection_grade'],
@@ -59,10 +62,10 @@ module Eaternet
59
62
  end
60
63
  end
61
64
 
62
- # (see Eaternet::Framework::Framework::AbstractAdapter#violations)
65
+ # (see Eaternet::Prototype::AbstractAdapter#violations)
63
66
  def violations
64
67
  lazy_csv_map('restaurant_inspection_violations.csv') do |row|
65
- Eaternet::Framework::Prototype::ViolationData.new(
68
+ Eaternet::Prototype::ViolationData.new(
66
69
  orig_key: row['inspection_violation_id'],
67
70
  inspection_id: row['inspection_id'],
68
71
  violation_kind_id: row['inspection_violation']
@@ -74,7 +77,7 @@ module Eaternet
74
77
  # @todo Add to AbstractAdapter
75
78
  def violation_kinds
76
79
  lazy_csv_map('restaurant_violations.csv') do |row|
77
- Eaternet::Framework::Prototype::ViolationKindData.new(
80
+ Eaternet::Prototype::ViolationKindData.new(
78
81
  orig_key: row['violation_id'],
79
82
  code: row['violation_code'],
80
83
  demerits: row['violation_demerits'],
@@ -0,0 +1,240 @@
1
+ # coding: utf-8
2
+ module Eaternet
3
+
4
+ # Framework for creating LIVES 1.0 apps.
5
+ #
6
+ # The goal is to make it as easy as possible to create
7
+ # compliant implementations. And so, the Business, Inspection
8
+ # and Violation data objects use validations to ensure that they
9
+ # correctly produce the entities listed in the spec.
10
+ #
11
+ # @see http://www.yelp.com/healthscores
12
+ # @see http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/
13
+ # @see http://www.rubyinside.com/rails-3-0s-activemodel-how-to-give-ruby-classes-some-activerecord-magic-2937.html
14
+ module Lives_1_0
15
+ require 'active_model'
16
+
17
+
18
+ class Adapter
19
+ # Required.
20
+ # @return [Enumerable<Business>]
21
+ def businesses
22
+ fail 'Override this to return an Enumerable of Business'
23
+ end
24
+
25
+ # Required.
26
+ # @return [Enumerable<Inspection>]
27
+ def inspections
28
+ fail 'Override this to return an Enumerable of Inspection'
29
+ end
30
+
31
+ # Optional.
32
+ # @return [Enumerable<Violation>]
33
+ def violations
34
+ fail 'Optionally override this to return an Enumerable of Violation'
35
+ end
36
+
37
+ # Optional.
38
+ # @return [FeedInfo]
39
+ def feed_info
40
+ fail 'Optionally override this to return a FeedInfo'
41
+ end
42
+ end
43
+
44
+
45
+ class ValidatedObject
46
+ include ActiveModel::Validations
47
+
48
+ def initialize(&block)
49
+ block.call(self)
50
+ check_validations!
51
+ end
52
+
53
+
54
+ private
55
+
56
+ def check_validations!
57
+ fail ArgumentError, errors.messages.inspect if invalid?
58
+ end
59
+
60
+ class TypeValidator < ActiveModel::EachValidator
61
+ def validate_each(record, attribute, value)
62
+ record.errors.add attribute, (options[:message] || "is not of class #{options[:with]}") unless
63
+ value.class == options[:with]
64
+ end
65
+ end
66
+ end
67
+
68
+
69
+ # A food service establishment, e.g. a restaurant.
70
+ # @see http://www.yelp.com/healthscores#businesses
71
+ class Business < ValidatedObject
72
+ attr_accessor :business_id, :name, :address, :city, :state,
73
+ :postal_code, :latitude, :longitude, :phone_number
74
+
75
+ validates :business_id, :name, :address,
76
+ type: String,
77
+ presence: true
78
+ validates :city, :state, :postal_code, :phone_number,
79
+ type: String,
80
+ allow_nil: true
81
+ validates :latitude,
82
+ numericality:
83
+ {
84
+ greater_than_or_equal_to: -90,
85
+ less_than_or_equal_to: 90
86
+ },
87
+ allow_nil: true
88
+ validates :longitude,
89
+ numericality:
90
+ {
91
+ greater_than_or_equal_to: -180,
92
+ less_than_or_equal_to: 180
93
+ },
94
+ allow_nil: true
95
+
96
+ def ==(other)
97
+ @business_id == other.business_id
98
+ end
99
+
100
+ def eql?(other)
101
+ self == other
102
+ end
103
+
104
+ def hash
105
+ @business_id.hash
106
+ end
107
+
108
+ # :nodoc:
109
+ # @return [String]
110
+ def to_s
111
+ "Business #{@business_id}"
112
+ end
113
+ end
114
+
115
+
116
+ # @see http://www.yelp.com/healthscores#inspections
117
+ class Inspection < ValidatedObject
118
+ attr_accessor :business_id, :score, :date, :description, :type
119
+
120
+ ZERO_TO_ONE_HUNDRED_AND_BLANK = (0..100).to_a + ['']
121
+
122
+ validates :business_id,
123
+ type: String,
124
+ presence: true
125
+ validates :score,
126
+ inclusion: { in: ZERO_TO_ONE_HUNDRED_AND_BLANK },
127
+ allow_nil: true
128
+ validates :date,
129
+ type: Date,
130
+ presence: true
131
+ validates :type,
132
+ inclusion: { in: %w(initial routine followup complaint) },
133
+ allow_nil: true
134
+
135
+ def score
136
+ @score.nil? ? '' : @score
137
+ end
138
+
139
+ def to_s
140
+ "Inspection #{@business_id}/#{@date}/#{@score}"
141
+ end
142
+ end
143
+
144
+
145
+ # @see http://www.yelp.com/healthscores#violations
146
+ class Violation < ValidatedObject
147
+ attr_accessor :business_id, :date, :code, :description
148
+
149
+ validates :business_id,
150
+ type: String,
151
+ presence: true
152
+ validates :date,
153
+ type: Date,
154
+ presence: true
155
+ validates :code, :description,
156
+ type: String,
157
+ allow_nil: true
158
+ end
159
+
160
+
161
+ # @see http://www.yelp.com/healthscores#feed_info
162
+ class FeedInfo < ValidatedObject
163
+ attr_accessor :feed_date, :feed_version, :municipality_name,
164
+ :municipality_url, :contact_email
165
+
166
+ # See http://railscasts.com/episodes/211-validations-in-rails-3
167
+ EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
168
+ URL_REGEX = %r{\Ahttps?:/}
169
+
170
+ validates :feed_date,
171
+ type: Date,
172
+ presence: true
173
+ validates :feed_version,
174
+ type: String,
175
+ presence: true
176
+ validates :municipality_name,
177
+ type: String,
178
+ presence: true
179
+ validates :municipality_url,
180
+ type: String,
181
+ format: { with: URL_REGEX },
182
+ allow_nil: true
183
+ validates :contact_email,
184
+ type: String,
185
+ format: { with: EMAIL_REGEX },
186
+ allow_nil: true
187
+ end
188
+
189
+
190
+ # @see http://www.yelp.com/healthscores#legend
191
+ class Legend < ValidatedObject
192
+ attr_accessor :minimum_score, :maximum_score, :description
193
+
194
+ validates :minimum_score, :maximum_score,
195
+ inclusion: { in: (0..100) }
196
+ validates :description,
197
+ presence: true,
198
+ type: String
199
+ end
200
+
201
+
202
+ # A container for all the Legends in the data set. Performs
203
+ # validation on the whole set of Legends ensure they cover
204
+ # the entire range of scores and do not overlap.
205
+ class LegendGroup < ValidatedObject
206
+ attr_accessor :legends
207
+
208
+ # Check that all the items in this LegendGroup:
209
+ #
210
+ # 1. Are of the class, Legend
211
+ # 2. Cover the range of scores from 0-100 without overlap
212
+ class ComprehensiveValidator < ActiveModel::EachValidator
213
+ def validate_each(record, attribute, legends)
214
+ scores = (0..100).to_a
215
+ legends.each do |legend|
216
+ unless legend.class == Legend
217
+ record.errors.add attribute, 'must be a Legend'
218
+ return
219
+ end
220
+ range = (legend.minimum_score..legend.maximum_score)
221
+ range.each do |score|
222
+ if scores.delete(score).nil?
223
+ unless score == legend.minimum_score || score == legend.maximum_score
224
+ record.errors.add attribute, 'may not overlap'
225
+ return
226
+ end
227
+ end
228
+ end
229
+ end
230
+ unless scores.empty?
231
+ record.errors.add attribute, 'do not cover entire span from 0–100'
232
+ end
233
+ end
234
+ end
235
+
236
+ validates :legends, comprehensive: true
237
+ end
238
+
239
+ end
240
+ end