eaternet 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af8af2d45bbf5b308fb47e75f501a35bf436447f
4
- data.tar.gz: 0db0339d37d25f2436b7beeb5b13b07ffc35ee83
3
+ metadata.gz: 202273f23326e002f8e4a03d631b324d71025969
4
+ data.tar.gz: d817710bb5054fe8af26a6550e0e41454e4b0aa7
5
5
  SHA512:
6
- metadata.gz: 88ca6e813d927bcd80cc14b311e6082c378efff9ce359ae28e2af25e0cc57264249704d99c7f31a4ab239967ae6f12fba293acaeb581f4be29ad52bf45d38603
7
- data.tar.gz: 3a95dca9298441e8c3779ac08e27c8b8ecbffc9c9770c939494285ce9a9bd1559f3cce2fc67e4c0627222b4f28f3544087db0c2c2a8b1c441b36591942d03772
6
+ metadata.gz: feac8796ac69bc065bc16385d1a46469aa99d158bd3adbaae8303627af3653278618ac368ae403cc903355c997fa1d7e6721d93f4e594aebfe1efd2e63e40987
7
+ data.tar.gz: 6792b804fa1c37a490fd5ac8b93e88cf6357efec2b481d631d5a6a24440a015ee8269b5dcb95ebc38dfa6ebf675264d12d40a29850890aba8fdb9aed32bf7e64
data/Guardfile CHANGED
@@ -6,31 +6,8 @@
6
6
 
7
7
  ## Uncomment to clear the screen before every task
8
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
9
  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' }
10
+ watch(%r{^test/(.*)_test\.rb$})
11
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
12
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
36
13
  end
data/eaternet.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency 'guard'
24
24
  spec.add_development_dependency 'guard-minitest'
25
25
  spec.add_development_dependency 'minitest'
26
- # spec.add_development_dependency 'minitest-utils'
26
+ spec.add_development_dependency 'minitest-utils'
27
27
  spec.add_development_dependency 'pry'
28
28
  spec.add_development_dependency 'rake', '~> 10'
29
29
  spec.add_development_dependency 'vcr'
@@ -15,17 +15,11 @@ module Eaternet
15
15
  module Agencies
16
16
  # A LIVES 1.0 data source for New York City.
17
17
  #
18
- # Example: print all the restaurant names in New York City.
19
- #
20
- # ```ruby
21
- # require 'eaternet'
22
- #
18
+ # @example Print all the restaurant names in New York City.
23
19
  # nyc = Eaternet::Nyc.new
24
20
  # nyc.businesses.each { |biz| puts biz.name }
25
- # ```
26
21
  #
27
22
  # @see https://data.cityofnewyork.us/Health/DOHMH-New-York-City-Restaurant-Inspection-Results/xx67-kt59
28
- # @todo This is a somewhat long namespace. Can this be improved?
29
23
  class Nyc < Eaternet::Lives_1_0::Adapter
30
24
  include Eaternet::Lives_1_0
31
25
  include Eaternet::Loggable
@@ -34,17 +28,20 @@ module Eaternet
34
28
  DATASET = 'xx67-kt59'
35
29
  CSV_URL = "https://#{DOMAIN}/api/views/#{DATASET}/rows.csv?accessType=DOWNLOAD"
36
30
 
37
- # Create an NYC Adapter.
31
+ # Create an adapter for the NYC data.
38
32
  #
39
- # @param csv_path for unit testing
33
+ # @example
34
+ # nyc = Eaternet::Nyc.new
35
+ #
36
+ # @param [String] csv_path only needed for unit testing
40
37
  def initialize(csv_path: nil)
41
38
  @table_file = csv_path
42
39
  end
43
40
 
44
41
  def businesses
45
42
  map_csv { |row| try_to_create_business(row) }
46
- .compact
47
- .unique
43
+ .compact
44
+ .unique
48
45
  end
49
46
 
50
47
  def inspections
@@ -93,7 +90,7 @@ module Eaternet
93
90
  l.description = 'C'
94
91
  end
95
92
  ]
96
- end
93
+ end.legends
97
94
  end
98
95
 
99
96
 
@@ -180,7 +177,7 @@ module Eaternet
180
177
  end
181
178
 
182
179
  def map_csv(&block)
183
- CSV.new(open(table_file, encoding: 'utf-8'), headers: true)
180
+ CSV.new(File.open(table_file, encoding: 'utf-8'), headers: true)
184
181
  .lazy
185
182
  .map { |row| block.call(row) }
186
183
  end
@@ -192,12 +189,12 @@ module Eaternet
192
189
  def table_file
193
190
  if @table_file.nil?
194
191
  @table_file = Tempfile.new('all.csv.')
195
- Nyc.download(@table_file)
192
+ Nyc.download_to(@table_file)
196
193
  end
197
194
  @table_file
198
195
  end
199
196
 
200
- def self.download(a_file)
197
+ def self.download_to(a_file)
201
198
  download_via_url(a_file)
202
199
  end
203
200
 
@@ -1,6 +1,12 @@
1
- # coding: utf-8
2
- module Eaternet
1
+ require 'eaternet/lives_1_0/adapter'
2
+ require 'eaternet/lives_1_0/business'
3
+ require 'eaternet/lives_1_0/feed_info'
4
+ require 'eaternet/lives_1_0/inspection'
5
+ require 'eaternet/lives_1_0/legend'
6
+ require 'eaternet/lives_1_0/legend_group'
7
+ require 'eaternet/lives_1_0/violation'
3
8
 
9
+ module Eaternet
4
10
  # Framework for creating LIVES 1.0 apps.
5
11
  #
6
12
  # The goal is to make it as easy as possible to create
@@ -12,322 +18,5 @@ module Eaternet
12
18
  # @see http://www.yelp.com/healthscores Local Inspector Value-Entry
13
19
  # Specification (LIVES)
14
20
  module Lives_1_0
15
- require 'active_model'
16
-
17
- # @abstract Subclass and override {#businesses}, {#inspections},
18
- # and optionally {#violations} and {#feed_info} to implement
19
- # a custom Lives 1.0 data source adapter.
20
- class Adapter
21
- # Required.
22
- # @return [Enumerable<Business>]
23
- def businesses
24
- fail 'Override this to return an Enumerable of Business'
25
- end
26
-
27
- # Required.
28
- # @return [Enumerable<Inspection>]
29
- def inspections
30
- fail 'Override this to return an Enumerable of Inspection'
31
- end
32
-
33
- # Optional.
34
- # @return [Enumerable<Violation>]
35
- def violations
36
- fail 'Optionally override this to return an Enumerable of Violation'
37
- end
38
-
39
- # Optional.
40
- # @return [FeedInfo]
41
- def feed_info
42
- fail 'Optionally override this to return a FeedInfo'
43
- end
44
- end
45
-
46
-
47
- # Uses {ActiveModel::Validations} to create self-validating
48
- # Plain Old Ruby objects.
49
- #
50
- # @abstract Subclass and add `attr_accessor` and validations
51
- # to create custom validating objects.
52
- #
53
- # @see http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/
54
- # @see http://www.rubyinside.com/rails-3-0s-activemodel-how-to-give-ruby-classes-some-activerecord-magic-2937.html
55
- class ValidatedObject
56
- include ActiveModel::Validations
57
-
58
- # @raise [ArgumentError] if the object is not valid at the
59
- # end of initialization.
60
- #
61
- # @yieldparam [ValidatedObject] new_object the yielded new object
62
- # for configuration.
63
- #
64
- def initialize(&block)
65
- block.call(self)
66
- check_validations!
67
- end
68
-
69
-
70
- private
71
-
72
- def check_validations!
73
- fail ArgumentError, errors.messages.inspect if invalid?
74
- end
75
-
76
- class TypeValidator < ActiveModel::EachValidator
77
- def validate_each(record, attribute, value)
78
- record.errors.add attribute, (options[:message] || "is not of class #{options[:with]}") unless
79
- value.class == options[:with]
80
- end
81
- end
82
- end
83
-
84
-
85
- # A food service establishment, e.g. a restaurant.
86
- #
87
- # @!attribute [rw] business_id
88
- # Unique identifier for the business. For many cities,
89
- # this may be the license number. Required.
90
- # @return [String]
91
- #
92
- # @!attribute [rw] name
93
- # Common name of the business. Required.
94
- # @return [String]
95
- #
96
- # @!attribute [rw] address
97
- # Street address of the business. For example: 706 Mission St.
98
- # Required.
99
- # @return [String]
100
- #
101
- # @!attribute [rw] city
102
- # City of the business. This field must be included if the
103
- # file contains businesses from multiple cities.
104
- # @return [String]
105
- #
106
- # @!attribute [rw] state
107
- # State or province for the business. In the U.S. this should
108
- # be the two-letter code for the state. Optional.
109
- # @return [String]
110
- #
111
- # @!attribute [rw] postal_code
112
- # Zip code or other postal code. Optional.
113
- # @return [String]
114
- #
115
- # @!attribute [rw] latitude
116
- # Latitude of the business. This field must be a valid WGS 84
117
- # latitude. For example: 37.7859547. Optional.
118
- # @return [Float]
119
- #
120
- # @!attribute [rw] longitude
121
- # Longitude of the business. This field must be a valid WGS 84
122
- # longitude. For example: -122.4024658. Optional.
123
- # @return [Float]
124
- #
125
- # @!attribute [rw] phone_number
126
- # Phone number for a business including country specific dialing
127
- # information. For example: +14159083801
128
- # @return [String]
129
- #
130
- # @see http://www.yelp.com/healthscores#businesses LIVES/Business
131
- class Business < ValidatedObject
132
- attr_accessor :business_id, :name, :address, :city, :state,
133
- :postal_code, :latitude, :longitude, :phone_number
134
-
135
- validates :business_id, :name, :address,
136
- type: String,
137
- presence: true
138
- validates :city, :state, :postal_code, :phone_number,
139
- type: String,
140
- allow_nil: true
141
- validates :latitude,
142
- numericality:
143
- {
144
- greater_than_or_equal_to: -90,
145
- less_than_or_equal_to: 90
146
- },
147
- allow_nil: true
148
- validates :longitude,
149
- numericality:
150
- {
151
- greater_than_or_equal_to: -180,
152
- less_than_or_equal_to: 180
153
- },
154
- allow_nil: true
155
-
156
- def ==(other)
157
- self.business_id == other.business_id
158
- end
159
-
160
- def eql?(other)
161
- self == other
162
- end
163
-
164
- def hash
165
- self.business_id.hash
166
- end
167
-
168
- # @return [String]
169
- def to_s
170
- "Business #{self.business_id}"
171
- end
172
- end
173
-
174
-
175
- # Information about an inspectors’ visit to a businesses.
176
- #
177
- # @!attribute [rw] business_id
178
- # Unique identifier of the business for which this inspection
179
- # was done. Required.
180
- # @return [String]
181
- #
182
- # @!attribute [rw] score
183
- # Inspection score on a 0-100 scale. 100 is the highest score.
184
- # This column must always be present in inspections.csv. However, it
185
- # can be safely left blank for inspection rows that don’t have an
186
- # associated score. (For example, some municipalities don’t associate
187
- # a follow-up inspection with a score.)
188
- # @return [Integer] if it's a scored inspection
189
- # @return [String] if it's an un-scored inspection, then the return value
190
- # will be an empty string.
191
- #
192
- # @!attribute [rw] date
193
- # Date of the inspection.
194
- # @return [Date]
195
- #
196
- # @!attribute [rw] description
197
- # Single line description containing details on the outcome of an
198
- # inspection. Use of this field is only encouraged if no violations
199
- # are provided.
200
- # @return [String]
201
- #
202
- # @!attribute [rw] type
203
- # String representing the type of inspection. Must be (initial,
204
- # routine, followup, complaint).
205
- # @return [String]
206
- #
207
- # @see http://www.yelp.com/healthscores#inspections LIVES / Inspections
208
- class Inspection < ValidatedObject
209
- attr_accessor :business_id, :score, :date, :description, :type
210
-
211
- ZERO_TO_ONE_HUNDRED_AND_BLANK = (0..100).to_a + ['']
212
-
213
- validates :business_id,
214
- type: String,
215
- presence: true
216
- validates :score,
217
- inclusion: { in: ZERO_TO_ONE_HUNDRED_AND_BLANK },
218
- allow_nil: true
219
- validates :date,
220
- type: Date,
221
- presence: true
222
- validates :type,
223
- inclusion: { in: %w(initial routine followup complaint) },
224
- allow_nil: true
225
-
226
- def score
227
- # noinspection RubyResolve
228
- @score.nil? ? '' : @score
229
- end
230
-
231
- # @return [String]
232
- def to_s
233
- "Inspection #{self.business_id}/#{self.date}/#{self.score}"
234
- end
235
- end
236
-
237
-
238
- # @see http://www.yelp.com/healthscores#violations LIVES / Violations
239
- class Violation < ValidatedObject
240
- attr_accessor :business_id, :date, :code, :description
241
-
242
- validates :business_id,
243
- type: String,
244
- presence: true
245
- validates :date,
246
- type: Date,
247
- presence: true
248
- validates :code, :description,
249
- type: String,
250
- allow_nil: true
251
- end
252
-
253
-
254
- # @see http://www.yelp.com/healthscores#feed_info LIVES / Feed Information
255
- class FeedInfo < ValidatedObject
256
- attr_accessor :feed_date, :feed_version, :municipality_name,
257
- :municipality_url, :contact_email
258
-
259
- # See http://railscasts.com/episodes/211-validations-in-rails-3
260
- EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
261
- URL_REGEX = %r{\Ahttps?:/}
262
-
263
- validates :feed_date,
264
- type: Date,
265
- presence: true
266
- validates :feed_version,
267
- type: String,
268
- presence: true
269
- validates :municipality_name,
270
- type: String,
271
- presence: true
272
- validates :municipality_url,
273
- type: String,
274
- format: { with: URL_REGEX },
275
- allow_nil: true
276
- validates :contact_email,
277
- type: String,
278
- format: { with: EMAIL_REGEX },
279
- allow_nil: true
280
- end
281
-
282
-
283
- # @see http://www.yelp.com/healthscores#legend LIVES / Legend
284
- class Legend < ValidatedObject
285
- attr_accessor :minimum_score, :maximum_score, :description
286
-
287
- validates :minimum_score, :maximum_score,
288
- inclusion: { in: (0..100) }
289
- validates :description,
290
- presence: true,
291
- type: String
292
- end
293
-
294
-
295
- # A container for all the Legends in the data set. Performs
296
- # validation on the whole set of Legends ensure they cover
297
- # the entire range of scores and do not overlap.
298
- class LegendGroup < ValidatedObject
299
- attr_accessor :legends
300
-
301
- # Check that all the items in this LegendGroup:
302
- #
303
- # 1. Are of the class, Legend
304
- # 2. Cover the range of scores from 0-100 without overlap
305
- class ComprehensiveValidator < ActiveModel::EachValidator
306
- def validate_each(record, attribute, legends)
307
- scores = (0..100).to_a
308
- legends.each do |legend|
309
- unless legend.class == Legend
310
- record.errors.add attribute, 'must be a Legend'
311
- return
312
- end
313
- range = (legend.minimum_score..legend.maximum_score)
314
- range.each do |score|
315
- if scores.delete(score).nil?
316
- unless score == legend.minimum_score || score == legend.maximum_score
317
- record.errors.add attribute, 'may not overlap'
318
- return
319
- end
320
- end
321
- end
322
- end
323
- unless scores.empty?
324
- record.errors.add attribute, 'do not cover entire span from 0–100'
325
- end
326
- end
327
- end
328
-
329
- validates :legends, comprehensive: true
330
- end
331
-
332
21
  end
333
22
  end
@@ -0,0 +1,38 @@
1
+ module Eaternet
2
+ module Lives_1_0
3
+ # @abstract Subclass and override {#businesses}, {#inspections},
4
+ # and optionally {#violations}, {#feed_info}, and {#legends} to
5
+ # implement a custom Lives 1.0 data source adapter.
6
+ class Adapter
7
+ # Required.
8
+ # @return [Enumerable<Business>]
9
+ def businesses
10
+ fail 'Override this to return an Enumerable of Business'
11
+ end
12
+
13
+ # Required.
14
+ # @return [Enumerable<Inspection>]
15
+ def inspections
16
+ fail 'Override this to return an Enumerable of Inspection'
17
+ end
18
+
19
+ # Optional.
20
+ # @return [Enumerable<Violation>]
21
+ def violations
22
+ fail 'Optionally override this to return an Enumerable of Violation'
23
+ end
24
+
25
+ # Optional.
26
+ # @return [FeedInfo]
27
+ def feed_info
28
+ fail 'Optionally override this to return a FeedInfo'
29
+ end
30
+
31
+ # Optional.
32
+ # @return [Enumerable<Legend>]
33
+ def legends
34
+ fails 'Optionally override this to return an Enumerable of Legend'
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,95 @@
1
+ require 'eaternet/lives_1_0/adapter'
2
+ require 'eaternet/validated_object'
3
+
4
+ module Eaternet
5
+ module Lives_1_0
6
+ # A food service establishment, e.g. a restaurant.
7
+ #
8
+ # @!attribute [rw] business_id
9
+ # Unique identifier for the business. For many cities,
10
+ # this may be the license number. Required.
11
+ # @return [String]
12
+ #
13
+ # @!attribute [rw] name
14
+ # Common name of the business. Required.
15
+ # @return [String]
16
+ #
17
+ # @!attribute [rw] address
18
+ # Street address of the business. For example: 706 Mission St.
19
+ # Required.
20
+ # @return [String]
21
+ #
22
+ # @!attribute [rw] city
23
+ # City of the business. This field must be included if the
24
+ # file contains businesses from multiple cities.
25
+ # @return [String]
26
+ #
27
+ # @!attribute [rw] state
28
+ # State or province for the business. In the U.S. this should
29
+ # be the two-letter code for the state. Optional.
30
+ # @return [String]
31
+ #
32
+ # @!attribute [rw] postal_code
33
+ # Zip code or other postal code. Optional.
34
+ # @return [String]
35
+ #
36
+ # @!attribute [rw] latitude
37
+ # Latitude of the business. This field must be a valid WGS 84
38
+ # latitude. For example: 37.7859547. Optional.
39
+ # @return [Float]
40
+ #
41
+ # @!attribute [rw] longitude
42
+ # Longitude of the business. This field must be a valid WGS 84
43
+ # longitude. For example: -122.4024658. Optional.
44
+ # @return [Float]
45
+ #
46
+ # @!attribute [rw] phone_number
47
+ # Phone number for a business including country specific dialing
48
+ # information. For example: +14159083801
49
+ # @return [String]
50
+ #
51
+ # @see http://www.yelp.com/healthscores#businesses LIVES/Business
52
+ class Business < ValidatedObject
53
+ attr_accessor :business_id, :name, :address, :city, :state,
54
+ :postal_code, :latitude, :longitude, :phone_number
55
+
56
+ validates :business_id, :name, :address,
57
+ type: String,
58
+ presence: true
59
+ validates :city, :state, :postal_code, :phone_number,
60
+ type: String,
61
+ allow_nil: true
62
+ validates :latitude,
63
+ numericality:
64
+ {
65
+ greater_than_or_equal_to: -90,
66
+ less_than_or_equal_to: 90
67
+ },
68
+ allow_nil: true
69
+ validates :longitude,
70
+ numericality:
71
+ {
72
+ greater_than_or_equal_to: -180,
73
+ less_than_or_equal_to: 180
74
+ },
75
+ allow_nil: true
76
+
77
+ def ==(other)
78
+ business_id == other.business_id
79
+ end
80
+
81
+ def eql?(other)
82
+ self == other
83
+ end
84
+
85
+ def hash
86
+ business_id.hash
87
+ end
88
+
89
+ # @return [String]
90
+ def to_s
91
+ "Business #{business_id}"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,33 @@
1
+ require 'eaternet/validated_object'
2
+
3
+ module Eaternet
4
+ module Lives_1_0
5
+ # @see http://www.yelp.com/healthscores#feed_info LIVES / Feed Information
6
+ class FeedInfo < ValidatedObject
7
+ attr_accessor :feed_date, :feed_version, :municipality_name,
8
+ :municipality_url, :contact_email
9
+
10
+ # See http://railscasts.com/episodes/211-validations-in-rails-3
11
+ EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
12
+ URL_REGEX = %r{\Ahttps?:/}
13
+
14
+ validates :feed_date,
15
+ type: Date,
16
+ presence: true
17
+ validates :feed_version,
18
+ type: String,
19
+ presence: true
20
+ validates :municipality_name,
21
+ type: String,
22
+ presence: true
23
+ validates :municipality_url,
24
+ type: String,
25
+ format: { with: URL_REGEX },
26
+ allow_nil: true
27
+ validates :contact_email,
28
+ type: String,
29
+ format: { with: EMAIL_REGEX },
30
+ allow_nil: true
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,69 @@
1
+ # coding: utf-8
2
+ require 'eaternet/validated_object'
3
+
4
+ module Eaternet
5
+ module Lives_1_0
6
+ # Information about an inspectors’ visit to a businesses.
7
+ # @see http://www.yelp.com/healthscores#inspections LIVES / Inspections
8
+ class Inspection < ValidatedObject
9
+ ZERO_TO_ONE_HUNDRED_AND_BLANK = (0..100).to_a + ['']
10
+
11
+ # @!attribute [rw] business_id
12
+ # Unique identifier of the business for which this inspection
13
+ # was done. Required.
14
+ # @return [String]
15
+ attr_accessor :business_id
16
+ validates :business_id,
17
+ type: String,
18
+ presence: true
19
+
20
+ # @!attribute [rw] score
21
+ # Inspection score on a 0-100 scale. 100 is the highest score.
22
+ # This column must always be present in inspections.csv. However, it
23
+ # can be safely left blank for inspection rows that don’t have an
24
+ # associated score. (For example, some municipalities don’t associate
25
+ # a follow-up inspection with a score.)
26
+ # @return [Integer] if it's a scored inspection
27
+ # @return [String] if it's an un-scored inspection, then the return value
28
+ # will be an empty string.
29
+ attr_accessor :score
30
+ validates :score,
31
+ inclusion: { in: ZERO_TO_ONE_HUNDRED_AND_BLANK },
32
+ allow_nil: true
33
+ def score
34
+ # noinspection RubyResolve
35
+ @score.nil? ? '' : @score
36
+ end
37
+
38
+ # @!attribute [rw] date
39
+ # Date of the inspection.
40
+ # @return [Date]
41
+ attr_accessor :date
42
+ validates :date,
43
+ type: Date,
44
+ presence: true
45
+
46
+ # @!attribute [rw] description
47
+ # Single line description containing details on the outcome of an
48
+ # inspection. Use of this field is only encouraged if no violations
49
+ # are provided.
50
+ # @return [String]
51
+ attr_accessor :description
52
+
53
+ validates :type,
54
+ inclusion: { in: %w(initial routine followup complaint) },
55
+ allow_nil: true
56
+
57
+ # @!attribute [rw] type
58
+ # String representing the type of inspection. Must be (initial,
59
+ # routine, followup, complaint).
60
+ # @return [String]
61
+ attr_accessor :type
62
+
63
+ # @return [String]
64
+ def to_s
65
+ "Inspection #{business_id}/#{date}/#{score}"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,15 @@
1
+ require 'eaternet/validated_object'
2
+
3
+ module Eaternet
4
+ module Lives_1_0
5
+ class Legend < ValidatedObject
6
+ attr_accessor :minimum_score, :maximum_score, :description
7
+
8
+ validates :minimum_score, :maximum_score,
9
+ inclusion: { in: (0..100) }
10
+ validates :description,
11
+ presence: true,
12
+ type: String
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ require 'eaternet/validated_object'
3
+
4
+ module Eaternet
5
+ module Lives_1_0
6
+ # A container for all the Legends in the data set. Performs
7
+ # validation on the whole set of Legends to ensure they cover
8
+ # the entire range of scores and do not overlap, as spec'd.
9
+ class LegendGroup < ValidatedObject
10
+ attr_accessor :legends
11
+
12
+ # Check that all the items in this LegendGroup:
13
+ #
14
+ # 1. Are of the class, Legend
15
+ # 2. Cover the range of scores from 0-100 without overlap
16
+ class ComprehensiveValidator < ActiveModel::EachValidator
17
+ def validate_each(record, attribute, legends)
18
+ scores = (0..100).to_a
19
+ legends.each do |legend|
20
+ unless legend.class == Legend
21
+ record.errors.add attribute, 'must be a Legend'
22
+ return
23
+ end
24
+ range = (legend.minimum_score..legend.maximum_score)
25
+ range.each do |score|
26
+ if scores.delete(score).nil?
27
+ unless score == legend.minimum_score || score == legend.maximum_score
28
+ record.errors.add attribute, 'may not overlap'
29
+ return
30
+ end
31
+ end
32
+ end
33
+ end
34
+ unless scores.empty?
35
+ record.errors.add attribute, 'do not cover entire span from 0–100'
36
+ end
37
+ end
38
+ end
39
+
40
+ validates :legends, comprehensive: true
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ require 'eaternet/validated_object'
2
+
3
+ module Eaternet
4
+ module Lives_1_0
5
+ # @see http://www.yelp.com/healthscores#violations LIVES / Violations
6
+ class Violation < ValidatedObject
7
+ attr_accessor :business_id, :date, :code, :description
8
+
9
+ validates :business_id,
10
+ type: String,
11
+ presence: true
12
+ validates :date,
13
+ type: Date,
14
+ presence: true
15
+ validates :code, :description,
16
+ type: String,
17
+ allow_nil: true
18
+ end
19
+ end
20
+ end
@@ -1,19 +1,18 @@
1
1
  module Eaternet
2
+ # A mixin to add logging functionality to a class.
2
3
  module Loggable
3
-
4
4
  # @return [Logger] the configured logger singleton
5
5
  def logger
6
6
  @logger ||= create_logger
7
7
  end
8
8
 
9
9
  private
10
-
10
+
11
11
  def create_logger
12
12
  logger = Logger.new(ENV['EATERNET_LOG_FILE'] || $stderr)
13
13
  logger.datetime_format = '%Y-%m-%d %H:%M:%S'
14
14
  logger.progname = 'Eaternet'
15
15
  logger
16
16
  end
17
-
18
17
  end
19
18
  end
@@ -4,7 +4,6 @@ module Eaternet
4
4
  # LIVES.
5
5
  #:nodoc:
6
6
  module Prototype
7
-
8
7
  module AbstractAdapter
9
8
  # @return [Enumerable<BusinessData>]
10
9
  def businesses
@@ -107,6 +106,5 @@ module Eaternet
107
106
  "ViolationKind #{@orig_key}"
108
107
  end
109
108
  end
110
-
111
109
  end
112
110
  end
data/lib/eaternet/util.rb CHANGED
@@ -3,8 +3,7 @@ require 'zip'
3
3
 
4
4
  module Eaternet
5
5
  module Util
6
- # A utility function to download a zip file,
7
- # extract it into a temp directory.
6
+ # Download a zip file and extract it into a temp directory.
8
7
  #
9
8
  # @return [String] the directory path
10
9
  def self.download_and_extract_zipfile(url)
@@ -23,10 +22,20 @@ module Eaternet
23
22
  dir
24
23
  end
25
24
 
25
+ # Download a file from the network.
26
+ #
27
+ # @param [String] source the URL to retrieve
28
+ # @param [String] dest pathname in which to save the file
26
29
  def self.download(source:, dest:)
27
30
  open(dest, 'wb') { |file| file << open(source).read }
28
31
  end
29
32
 
33
+ # Extract a Zip archive.
34
+ #
35
+ # @param [String] path the Zip file's location
36
+ # @param [String] dest_dir directory in which to perform the
37
+ # extraction
38
+ # @return nil
30
39
  def self.extract_zipfile(path:, dest_dir:)
31
40
  open(path) do |zip_file|
32
41
  Zip::File.open(zip_file, 'rb') do |zip_data|
@@ -0,0 +1,75 @@
1
+ require 'active_model'
2
+
3
+ module Eaternet
4
+ # @abstract Subclass and add `attr_accessor` and validations
5
+ # to create custom validating objects.
6
+ #
7
+ # Uses [ActiveModel::Validations](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validates)
8
+ # to create self-validating Plain Old Ruby objects. This is especially useful for
9
+ # data validation when importing from CSV.
10
+ #
11
+ # @example
12
+ # class Dog < Eaternet::ValidatedObject
13
+ # attr_accessor :name, :birthday
14
+ #
15
+ # validates :name, presence: true
16
+ # validates :birthday, type: Date, allow_nil: true
17
+ # end
18
+ #
19
+ # # The dog1 instance is validated at the end of instantiation. Here, it succeeds
20
+ # # without exception:
21
+ # dog1 = Dog.new do |d|
22
+ # d.name = 'Spot'
23
+ # end
24
+ #
25
+ # puts dog1.valid? # => true
26
+ #
27
+ # dog1.birthday = Date.new(2015, 1, 23)
28
+ # puts dog1.valid? # => true
29
+ #
30
+ # dog1.birthday = '2015-01-23'
31
+ # puts dog1.valid? # => false
32
+ # dog1.check_validations! # => ArgumentError: birthday is not of class Date
33
+ #
34
+ # @see Eaternet::ValidatedObject::TypeValidator
35
+ # @see http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/ ActiveModel: Make Any Ruby Object Feel Like ActiveRecord, Yehuda Katz
36
+ # @see http://www.rubyinside.com/rails-3-0s-activemodel-how-to-give-ruby-classes-some-activerecord-magic-2937.html Rails 3.0′s ActiveModel: How To Give Ruby Classes Some ActiveRecord Magic, Peter Cooper
37
+ class ValidatedObject
38
+ include ActiveModel::Validations
39
+
40
+ # Instantiate and validate a new object.
41
+ #
42
+ # @yieldparam [ValidatedObject] new_object the yielded new object
43
+ # for configuration.
44
+ #
45
+ # @raise [ArgumentError] if the object is not valid at the
46
+ # end of initialization.
47
+ def initialize(&block)
48
+ block.call(self)
49
+ check_validations!
50
+ end
51
+
52
+ # Run any validations and raise an error if invalid.
53
+ # @raise [ArgumentError] if any validations fail.
54
+ def check_validations!
55
+ fail ArgumentError, errors.messages.inspect if invalid?
56
+ end
57
+
58
+ # Ensure an object is a certain class. This is an example of a custom
59
+ # validator. It's here as a nested class for easy access by subclasses.
60
+ #
61
+ # @example
62
+ # class Dog < ValidatedObject
63
+ # attr_accessor :weight
64
+ # validates :weight, type: Float
65
+ # end
66
+ class TypeValidator < ActiveModel::EachValidator
67
+ def validate_each(record, attribute, value)
68
+ return if value.class == options[:with]
69
+
70
+ message = options[:message] || "is not of class #{options[:with]}"
71
+ record.errors.add attribute, message
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,3 +1,3 @@
1
1
  module Eaternet
2
- VERSION = '0.3.3'
2
+ VERSION = '0.3.4'
3
3
  end
@@ -76,7 +76,8 @@ class NycAdapterTest < Minitest::Test
76
76
  end
77
77
 
78
78
  def test_finds_correct_violations
79
- expected_violations = %w(06C 10F 04L 04N 04C 04L 06A 06C 08A 10F 02G 10F 02G 04L 06C 08A 10F)
79
+ expected_violations = %w(06C 10F 04L 04N 04C 04L 06A 06C 08A
80
+ 10F 02G 10F 02G 04L 06C 08A 10F)
80
81
  actual_violations = @@nyc.violations.to_a.map(&:code)
81
82
  assert_equal expected_violations, actual_violations
82
83
  end
@@ -90,6 +91,6 @@ class NycAdapterTest < Minitest::Test
90
91
  end
91
92
 
92
93
  def test_has_legends
93
- assert_instance_of Eaternet::Lives_1_0::LegendGroup, @@nyc.legends
94
+ assert enumerable_of? Eaternet::Lives_1_0::Legend, @@nyc.legends
94
95
  end
95
96
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eaternet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robb Shecter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-13 00:00:00.000000000 Z
11
+ date: 2015-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-utils
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: pry
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -172,20 +186,28 @@ files:
172
186
  - lib/eaternet/agencies/snhd.rb
173
187
  - lib/eaternet/agencies/snhd_config.rb
174
188
  - lib/eaternet/lives_1_0.rb
189
+ - lib/eaternet/lives_1_0/adapter.rb
190
+ - lib/eaternet/lives_1_0/business.rb
191
+ - lib/eaternet/lives_1_0/feed_info.rb
192
+ - lib/eaternet/lives_1_0/inspection.rb
193
+ - lib/eaternet/lives_1_0/legend.rb
194
+ - lib/eaternet/lives_1_0/legend_group.rb
195
+ - lib/eaternet/lives_1_0/violation.rb
175
196
  - lib/eaternet/loggable.rb
176
197
  - lib/eaternet/prototype.rb
177
198
  - lib/eaternet/util.rb
199
+ - lib/eaternet/validated_object.rb
178
200
  - lib/eaternet/version.rb
179
201
  - lib/ext/lazy.rb
180
- - test/eaternet_test.rb
202
+ - test/eaternet/eaternet_test.rb
203
+ - test/eaternet/lives_1_0/business_test.rb
204
+ - test/eaternet/lives_1_0/feed_info_test.rb
205
+ - test/eaternet/lives_1_0/inspection_test.rb
206
+ - test/eaternet/lives_1_0/legend_test.rb
207
+ - test/eaternet/loggable_test.rb
208
+ - test/eaternet/nyc_adapter_test.rb
209
+ - test/eaternet/snhd_adapter_test.rb
181
210
  - test/fixtures/morris-park-bake-shop.csv
182
- - test/lives_1_0/business_test.rb
183
- - test/lives_1_0/feed_info_test.rb
184
- - test/lives_1_0/inspection_test.rb
185
- - test/lives_1_0/legend_test.rb
186
- - test/loggable_test.rb
187
- - test/nyc_adapter_test.rb
188
- - test/snhd_adapter_test.rb
189
211
  - test/test_helper.rb
190
212
  homepage: https://github.com/eaternet/adapters-ruby
191
213
  licenses:
@@ -212,14 +234,14 @@ signing_key:
212
234
  specification_version: 4
213
235
  summary: Regional adapters for restaurant health scores
214
236
  test_files:
215
- - test/eaternet_test.rb
237
+ - test/eaternet/eaternet_test.rb
238
+ - test/eaternet/lives_1_0/business_test.rb
239
+ - test/eaternet/lives_1_0/feed_info_test.rb
240
+ - test/eaternet/lives_1_0/inspection_test.rb
241
+ - test/eaternet/lives_1_0/legend_test.rb
242
+ - test/eaternet/loggable_test.rb
243
+ - test/eaternet/nyc_adapter_test.rb
244
+ - test/eaternet/snhd_adapter_test.rb
216
245
  - test/fixtures/morris-park-bake-shop.csv
217
- - test/lives_1_0/business_test.rb
218
- - test/lives_1_0/feed_info_test.rb
219
- - test/lives_1_0/inspection_test.rb
220
- - test/lives_1_0/legend_test.rb
221
- - test/loggable_test.rb
222
- - test/nyc_adapter_test.rb
223
- - test/snhd_adapter_test.rb
224
246
  - test/test_helper.rb
225
247
  has_rdoc: