eaternet 0.3.3 → 0.3.4

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: 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: