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 +4 -4
- data/.gitignore +0 -1
- data/.inch.yml +5 -0
- data/Guardfile +36 -0
- data/README.md +23 -23
- data/Rakefile +1 -1
- data/eaternet.gemspec +13 -11
- data/lib/eaternet.rb +8 -4
- data/lib/eaternet/agencies/nyc.rb +44 -20
- data/lib/eaternet/agencies/snhd.rb +24 -21
- data/lib/eaternet/lives_1_0.rb +240 -0
- data/lib/eaternet/loggable.rb +16 -0
- data/lib/eaternet/prototype.rb +112 -0
- data/lib/eaternet/version.rb +1 -1
- data/test/lives_1_0/business_test.rb +7 -10
- data/test/lives_1_0/feed_info_test.rb +3 -6
- data/test/lives_1_0/inspection_test.rb +10 -13
- data/test/lives_1_0/legend_test.rb +23 -26
- data/test/loggable_test.rb +15 -0
- data/test/nyc_adapter_test.rb +8 -11
- data/test/snhd_adapter_test.rb +39 -7
- data/test/test_helper.rb +3 -0
- metadata +38 -4
- data/lib/eaternet/framework/lives_1_0.rb +0 -232
- data/lib/eaternet/framework/prototype.rb +0 -109
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22ee77b16cad4131bcf6dfa1c970507b39569ba6
|
4
|
+
data.tar.gz: 8649494e45fedc9defa460d1423c2b1c83eee345
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d64e281aab4dada323e6fd72b5facf8b4287c6318b5132fa231b44e71ca0281e5c10c52a221577a35386d2bfc5d5dbcdd294a88c00d07da19fe8006f54d3247d
|
7
|
+
data.tar.gz: 0d1ed8e4684cb36c2af320556e5efca4805fc3647216c34c83770c9c086972c89b4404a23dbe457120ead76d0048e5eea96e439d81e99bba00060300bea9260d
|
data/.gitignore
CHANGED
data/.inch.yml
ADDED
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
|
[](https://travis-ci.org/eaternet/adapters-ruby)
|
2
2
|
[](https://codeclimate.com/github/eaternet/adapters-ruby)
|
3
3
|
[](http://badge.fury.io/rb/eaternet)
|
4
|
+
[](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
|
-
|
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
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
32
|
+
Example: print all the restaurant names in New York City.
|
30
33
|
|
31
34
|
```ruby
|
32
35
|
require 'eaternet'
|
33
36
|
|
34
|
-
|
35
|
-
|
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
|
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
|
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
|
68
|
-
We like good tests, as well. Submit a pull
|
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
|
71
|
-
put into daily production. We're working with
|
72
|
-
|
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
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 =
|
7
|
+
spec.name = 'eaternet'
|
8
8
|
spec.version = Eaternet::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
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 = [
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
spec.required_ruby_version = '>= 2.1.0'
|
21
21
|
|
22
|
-
spec.add_development_dependency
|
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
|
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
|
-
|
6
|
-
#
|
7
|
-
|
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
|
-
|
9
|
+
|
10
|
+
require 'eaternet/lives_1_0'
|
11
|
+
require 'eaternet/loggable'
|
12
|
+
|
10
13
|
|
11
14
|
module Eaternet
|
12
15
|
module Agencies
|
13
|
-
#
|
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
|
17
|
-
class Nyc < Eaternet::
|
18
|
-
include Eaternet::
|
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
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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']
|
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)
|
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/
|
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
|
19
|
-
# The file contains several Mysql tables converted to CSV.
|
20
|
-
# for how to code adapters for other agencies
|
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,
|
23
|
-
# enumerators of Businesses, Inspections, Violations, and
|
24
|
-
# specified in the
|
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
|
27
|
-
# in fields without escaping it. We need to find out what their
|
28
|
-
# In the meantime, the files can be parsed by
|
29
|
-
# that doesn't appear in the
|
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::
|
36
|
+
# @see Eaternet::Prototype Framework module
|
34
37
|
class Snhd
|
35
|
-
include Eaternet::
|
38
|
+
include Eaternet::Prototype::AbstractAdapter
|
36
39
|
|
37
|
-
# (see Eaternet::
|
40
|
+
# (see Eaternet::Prototype::AbstractAdapter#businesses)
|
38
41
|
def businesses
|
39
42
|
lazy_csv_map('restaurant_establishments.csv') do |row|
|
40
|
-
Eaternet::
|
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::
|
53
|
+
# (see Eaternet::Prototype::AbstractAdapter#inspections)
|
51
54
|
def inspections
|
52
55
|
lazy_csv_map('restaurant_inspections.csv') do |row|
|
53
|
-
Eaternet::
|
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::
|
65
|
+
# (see Eaternet::Prototype::AbstractAdapter#violations)
|
63
66
|
def violations
|
64
67
|
lazy_csv_map('restaurant_inspection_violations.csv') do |row|
|
65
|
-
Eaternet::
|
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::
|
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
|