eaternet 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![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
|
-
|
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
|