harrisj-nytimes-articles 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ module Nytimes
2
+ module Articles
3
+ class Error < ::RuntimeError
4
+ end
5
+
6
+ class AuthenticationError < Error
7
+ end
8
+
9
+ class BadRequestError < Error
10
+ end
11
+
12
+ class BadResponseError < Error
13
+ end
14
+
15
+ class ServerError < Error
16
+ end
17
+
18
+ class TimeoutError < Error
19
+ end
20
+
21
+ class ConnectionError < Error
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,70 @@
1
+ module Nytimes
2
+ module Articles
3
+ class Facet
4
+ attr_reader :term, :count, :facet_type
5
+
6
+ # Facet name constants
7
+ CLASSIFIERS = 'classifiers_facet'
8
+ COLUMN = 'column_facet'
9
+ DATE = 'date'
10
+ DAY_OF_WEEK = 'day_of_week_facet'
11
+ DESCRIPTION = 'des_facet'
12
+ DESK = 'desk_facet'
13
+ GEO = 'geo_facet'
14
+ MATERIAL_TYPE = 'material_type_facet'
15
+ ORGANIZATION = 'org_facet'
16
+ PAGE = 'page_facet'
17
+ PERSON = 'per_facet'
18
+ PUB_DAY = 'publication_day'
19
+ PUB_MONTH = 'publication_month'
20
+ PUB_YEAR = 'publication_year'
21
+ SECTION_PAGE = 'section_page_facet'
22
+ SOURCE = 'source_facet'
23
+ WORKS_MENTIONED = 'works_mentioned_facet'
24
+
25
+ # Facets of content formatted for nytimes.com
26
+ NYTD_BYLINE = 'nytd_byline'
27
+ NYTD_DESCRIPTION = 'nytd_des_facet'
28
+ NYTD_GEO = 'nytd_geo_facet'
29
+ NYTD_ORGANIZATION = 'nytd_org_facet'
30
+ NYTD_PERSON = 'nytd_per_facet'
31
+ NYTD_SECTION = 'nytd_section_facet'
32
+ NYTD_WORKS_MENTIONED = 'nytd_works_mentioned_facet'
33
+
34
+ # The best 5 facets to return
35
+ DEFAULT_RETURN_FACETS = [NYTD_DESCRIPTION, NYTD_GEO, NYTD_ORGANIZATION, NYTD_PERSON, NYTD_SECTION]
36
+
37
+ ALL_FACETS = [CLASSIFIERS, COLUMN, DATE, DAY_OF_WEEK, DESCRIPTION, DESK, GEO, MATERIAL_TYPE, ORGANIZATION, PAGE, PERSON, PUB_DAY,
38
+ PUB_MONTH, PUB_YEAR, SECTION_PAGE, SOURCE, WORKS_MENTIONED, NYTD_BYLINE, NYTD_DESCRIPTION, NYTD_GEO,
39
+ NYTD_ORGANIZATION, NYTD_PERSON, NYTD_SECTION, NYTD_WORKS_MENTIONED]
40
+
41
+ def initialize(facet_type, term, count)
42
+ @facet_type = facet_type
43
+ @term = term
44
+ @count = count
45
+ end
46
+
47
+ # def self.init_from_api(type, hash)
48
+ # self.new(type, hash['term'], hash['count'].to_i)
49
+ # end
50
+
51
+ def self.init_from_api(api_hash)
52
+ return nil if api_hash.nil?
53
+
54
+ unless api_hash.is_a? Hash
55
+ raise ArgumentError, "expecting a Hash only"
56
+ else
57
+ return nil if api_hash.empty?
58
+ end
59
+
60
+ out = {}
61
+
62
+ api_hash.each_pair do |k,v|
63
+ out[k] = v.map {|f| Facet.new(k, f['term'], f['count'])}
64
+ end
65
+
66
+ out
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'forwardable'
3
+
4
+ module Nytimes
5
+ module Articles
6
+ class ResultSet < Base
7
+ extend Forwardable
8
+ attr_reader :offset, :total_results, :results, :facets
9
+
10
+ BATCH_SIZE = 10
11
+
12
+ def_delegators :@results, :&, :*, :+, :-, :[], :at, :collect, :compact, :each, :each_index, :empty?, :fetch, :first, :include?, :index, :last, :length, :map, :nitems, :reject, :reverse, :reverse_each, :rindex, :select, :size, :slice
13
+
14
+ def initialize(params)
15
+ @offset = params[:offset]
16
+ @total_results = params[:total_results]
17
+ @results = params[:results]
18
+ @facets = params[:facets]
19
+ end
20
+
21
+ def page_number
22
+ return 0 if @total_results == 0
23
+ @offset + 1
24
+ end
25
+
26
+ def total_pages
27
+ return 0 if @total_results == 0
28
+ (@total_results.to_f / BATCH_SIZE).ceil
29
+ end
30
+
31
+ def self.init_from_api(api_hash)
32
+ self.new(:offset => integer_field(api_hash['offset']),
33
+ :total_results => integer_field(api_hash['total']),
34
+ :results => api_hash['results'].map {|r| Article.init_from_api(r)},
35
+ :facets => Facet.init_from_api(api_hash['facets'])
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,527 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper.rb'
2
+
3
+ ARTICLE_API_HASH = {"page_facet"=>"8", "lead_paragraph"=>"", "classifiers_facet"=>["Top/News/Business", "Top/Classifieds/Job Market/Job Categories/Banking, Finance and Insurance", "Top/News/Business/Markets"], "title"=>"Wall St. Treads Water as It Waits on Washington", "nytd_title"=>"Wall St. Treads Water as It Waits on Washington", "byline"=>"By JACK HEALY", "body"=>"Wall Street held its breath on Monday as it awaited details on a banking bailout from Washington. Investors had expected to start the week with an announcement from the Treasury Department outlining its latest plans to stabilize the financial system. But the Obama administration delayed releasing the details until at least Tuesday to keep the focus", "material_type_facet"=>["News"], "url"=>"http://www.nytimes.com/2009/02/10/business/10markets.html", "publication_month"=>"02", "date"=>"20090210", "publication_year"=>"2009", "nytd_section_facet"=>["Business"], "source_facet"=>"The New York Times", "desk_facet"=>"Business", "publication_day"=>"10", "des_facet"=>["STOCKS AND BONDS"], "day_of_week_facet"=>"Tuesday"}
4
+
5
+ ARTICLE_API_HASH2 = {"page_facet"=>"29", "lead_paragraph"=>"", "geo_facet"=>["WALL STREET (NYC)"], "small_image_width"=>"75", "classifiers_facet"=>["Top/News/New York and Region", "Top/Classifieds/Job Market/Job Categories/Education", "Top/Features/Travel/Guides/Destinations/North America", "Top/Classifieds/Job Market/Job Categories/Banking, Finance and Insurance", "Top/Features/Travel/Guides/Destinations/North America/United States/New York", "Top/Features/Travel/Guides/Destinations/North America/United States", "Top/News/Education"], "title"=>"OUR TOWNS; As Pipeline to Wall Street Narrows, Princeton Students Adjust Sights", "nytd_title"=>"As Pipeline to Wall Street Narrows, Princeton Students Adjust Sights", "byline"=>"By PETER APPLEBOME", "body"=>"Princeton, N.J. There must be a screenplay in the fabulous Schoppe twins, Christine and Jennifer, Princeton University juniors from Houston. They had the same G.P.A. and SATs in high school, where they became Gold Award Girl Scouts , sort of the female version of Eagle Scouts. They live together and take all the same courses, wear identical necklac", "material_type_facet"=>["News"], "url"=>"http://www.nytimes.com/2009/02/08/nyregion/08towns.html", "publication_month"=>"02", "small_image_height"=>"75", "date"=>"20090208", "column_facet"=>"Our Towns", "small_image"=>"Y", "publication_year"=>"2009", "nytd_section_facet"=>["New York and Region", "Education"], "source_facet"=>"The New York Times", "org_facet"=>["PRINCETON UNIVERSITY"], "desk_facet"=>"New York Region", "publication_day"=>"08", "small_image_url"=>"http://graphics8.nytimes.com/images/2009/02/08/nyregion/08towns.751.jpg", "des_facet"=>["EDUCATION AND SCHOOLS", "BANKS AND BANKING"], "day_of_week_facet"=>"Sunday"}
6
+
7
+ class TestNytimes::TestArticles::TestArticle < Test::Unit::TestCase
8
+ include Nytimes::Articles
9
+
10
+ def setup
11
+ init_test_key
12
+ Article.stubs(:parse_reply)
13
+ end
14
+
15
+ context "Article.search" do
16
+ should "accept a String for the first argument that is passed through to the query in the API" do
17
+ Article.expects(:invoke).with(has_entry("query", "FOO BAR"))
18
+ Article.search "FOO BAR"
19
+ end
20
+
21
+ should "accept a Hash for the first argument" do
22
+ Article.expects(:invoke).with(has_entry("query", "FOO BAR"))
23
+ Article.search :query => 'FOO BAR', :page => 2
24
+ end
25
+
26
+ context "date ranges" do
27
+ should "pass a string argument to begin_date straight through" do
28
+ date = "20081212"
29
+ Article.expects(:invoke).with(has_entry("begin_date", date))
30
+ Article.search :begin_date => date
31
+ end
32
+
33
+ should "convert begin_date from a Date or Time to YYYYMMDD format" do
34
+ time = Time.now
35
+ Article.expects(:invoke).with(has_entry("begin_date", time.strftime("%Y%m%d")))
36
+ Article.search :begin_date => time
37
+ end
38
+
39
+ should "pass a string argument to end_date straight through" do
40
+ date = "20081212"
41
+ Article.expects(:invoke).with(has_entry("end_date", date))
42
+ Article.search :end_date => date
43
+ end
44
+
45
+ should "convert end_date from a Date or Time to YYYYMMDD format" do
46
+ time = Time.now
47
+ Article.expects(:invoke).with(has_entry("end_date", time.strftime("%Y%m%d")))
48
+ Article.search :end_date => time
49
+ end
50
+
51
+ should "raise an ArgumentError if the begin_date is NOT a string and does not respond_to strftime" do
52
+ assert_raise(ArgumentError) { Article.search :begin_date => 23 }
53
+ end
54
+
55
+ should "raise an ArgumentError if the end_date is NOT a string and does not respond_to strftime" do
56
+ assert_raise(ArgumentError) { Article.search :end_date => 23 }
57
+ end
58
+
59
+ context ":before" do
60
+ should "add a begin_date in 1980 if no :since or :begin_date argument is provided"
61
+ should "not add a begin_date is there is a :since argument"
62
+ should "not add a begin_date if there is a :begin_date argument already"
63
+ end
64
+
65
+ context ":since" do
66
+ should "add an end_date of now if no :before or :end_date argument is provided"
67
+ should "not add an end_date is there is a :before argument"
68
+ should "not add an end_date if there is a :end_date argument already"
69
+ end
70
+ end
71
+
72
+ context "facets" do
73
+ should "accept a single string" do
74
+ Article.expects(:invoke).with(has_entry("facets", Facet::DATE))
75
+ Article.search "FOO BAR", :facets => Facet::DATE
76
+ end
77
+
78
+ should "accept an array of strings" do
79
+ Article.expects(:invoke).with(has_entry("facets", [Facet::DATE, Facet::GEO].join(',')))
80
+ Article.search "FOO BAR", :facets => [Facet::DATE, Facet::GEO]
81
+ end
82
+ end
83
+
84
+ context "search_facets" do
85
+ should "accept a String" do
86
+ Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
87
+ Article.search :search_facets => "#{Facet::GEO}:[CALIFORNIA]"
88
+ end
89
+
90
+ should "accept a single hash value Facet string to a term" do
91
+ Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
92
+ Article.search :search_facets => {Facet::GEO => 'CALIFORNIA'}
93
+ end
94
+
95
+ should "accept an Facet string hashed to an array terms" do
96
+ Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA,GREAT BRITAIN]"))
97
+ Article.search :search_facets => {Facet::GEO => ['CALIFORNIA', 'GREAT BRITAIN']}
98
+ end
99
+
100
+ should "accept a single Facet object" do
101
+ f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
102
+ Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
103
+ Article.search :search_facets => f
104
+ end
105
+
106
+ should "accept an array of Facet objects" do
107
+ f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
108
+ f2 = Facet.new(Facet::NYTD_ORGANIZATION, 'University Of California', 12)
109
+
110
+ Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA] #{Facet::NYTD_ORGANIZATION}:[University Of California]"))
111
+ Article.search :search_facets => [f, f2]
112
+ end
113
+
114
+ should "merge multiple Facets objects in the array of the same type into one array" do
115
+ f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
116
+ f2 = Facet.new(Facet::GEO, 'IOWA', 12)
117
+
118
+ Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA,IOWA]"))
119
+ Article.search :search_facets => [f, f2]
120
+ end
121
+
122
+ should "not stomp on an existing query string" do
123
+ Article.expects(:invoke).with(has_entry("query", "ice cream #{Facet::GEO}:[CALIFORNIA]"))
124
+ Article.search "ice cream", :search_facets => {Facet::GEO => "CALIFORNIA"}
125
+ end
126
+ end
127
+
128
+ context "exclude_facets" do
129
+ should "accept a String" do
130
+ Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
131
+ Article.search :exclude_facets => "-#{Facet::GEO}:[CALIFORNIA]"
132
+ end
133
+
134
+ should "accept a single hash value Facet string to a term" do
135
+ Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
136
+ Article.search :exclude_facets => {Facet::GEO => 'CALIFORNIA'}
137
+ end
138
+
139
+ should "accept an Facet string hashed to an array terms" do
140
+ Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA,GREAT BRITAIN]"))
141
+ Article.search :exclude_facets => {Facet::GEO => ['CALIFORNIA', 'GREAT BRITAIN']}
142
+ end
143
+
144
+ should "accept a single Facet object" do
145
+ f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
146
+ Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
147
+ Article.search :exclude_facets => f
148
+ end
149
+
150
+ should "accept an array of Facet objects" do
151
+ f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
152
+ f2 = Facet.new(Facet::NYTD_ORGANIZATION, 'University Of California', 12)
153
+
154
+ Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA] -#{Facet::NYTD_ORGANIZATION}:[University Of California]"))
155
+ Article.search :exclude_facets => [f, f2]
156
+ end
157
+
158
+ should "merge multiple Facets objects in the array of the same type into one array" do
159
+ f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
160
+ f2 = Facet.new(Facet::GEO, 'IOWA', 12)
161
+
162
+ Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA,IOWA]"))
163
+ Article.search :exclude_facets => [f, f2]
164
+ end
165
+
166
+ should "not stomp on an existing query string" do
167
+ Article.expects(:invoke).with(has_entry("query", "ice cream -#{Facet::GEO}:[CALIFORNIA]"))
168
+ Article.search "ice cream", :exclude_facets => {Facet::GEO => "CALIFORNIA"}
169
+ end
170
+ end
171
+
172
+ context ":fee" do
173
+ should "send through as fee:Y if set to true" do
174
+ Article.expects(:invoke).with(has_entry("query", "ice cream fee:Y"))
175
+ Article.search "ice cream", :fee => true
176
+ end
177
+
178
+ should "send through as -fee:Y if set to false" do
179
+ Article.expects(:invoke).with(has_entry("query", "ice cream -fee:Y"))
180
+ Article.search "ice cream", :fee => false
181
+ end
182
+ end
183
+
184
+ context ":fields" do
185
+ context "for the :all argument" do
186
+ should "pass all fields in a comma-delimited list" do
187
+ Article.expects(:invoke).with(has_entry('fields', Article::ALL_FIELDS.join(',')))
188
+ Article.search "FOO BAR", :fields => :all
189
+ end
190
+ end
191
+
192
+ context "for the :none argument" do
193
+ should "request a blank space for the fields argument"
194
+ should "request the standard :facets if no :facets have been explicitly provided"
195
+ should "request the given :facets field if provided"
196
+ end
197
+
198
+ context ":thumbnail" do
199
+ should "accept the symbol version of the argument"
200
+ should "accept the string version of the argument"
201
+ should "request all the thumbnail image fields from the API"
202
+ end
203
+
204
+ context ":multimedia" do
205
+ should "be implemented"
206
+ end
207
+
208
+ should "accept a single string as an argument" do
209
+ Article.expects(:invoke).with(has_entry('fields', 'body'))
210
+ Article.search "FOO BAR", :fields => 'body'
211
+ end
212
+
213
+ should "accept a single symbol as an argument" do
214
+ Article.expects(:invoke).with(has_entry('fields', 'body'))
215
+ Article.search "FOO BAR", :fields => :body
216
+ end
217
+
218
+ should "accept an array of strings and symbols" do
219
+ Article.expects(:invoke).with(has_entry('fields', 'abstract,body'))
220
+ Article.search "FOO BAR", :fields => [:abstract, 'body']
221
+ end
222
+
223
+ should "raise an ArgumentError otherwise" do
224
+ assert_raise(ArgumentError) { Article.search :fields => 12 }
225
+ end
226
+ end
227
+
228
+ context ":has_multimedia" do
229
+ should "send through as related_multimedia:Y if set to true" do
230
+ Article.expects(:invoke).with(has_entry("query", "ice cream related_multimedia:Y"))
231
+ Article.search "ice cream", :has_multimedia => true
232
+ end
233
+
234
+ should "send through as -related_multimedia:Y if set to false" do
235
+ Article.expects(:invoke).with(has_entry("query", "ice cream -related_multimedia:Y"))
236
+ Article.search "ice cream", :has_multimedia => false
237
+ end
238
+ end
239
+
240
+ context ":has_thumbnail" do
241
+ should "send through as small_image:Y if set to true" do
242
+ Article.expects(:invoke).with(has_entry("query", "ice cream small_image:Y"))
243
+ Article.search "ice cream", :has_thumbnail => true
244
+ end
245
+
246
+ should "send through as -small_image:Y if set to false" do
247
+ Article.expects(:invoke).with(has_entry("query", "ice cream -small_image:Y"))
248
+ Article.search "ice cream", :has_thumbnail => false
249
+ end
250
+ end
251
+
252
+ context ":offset" do
253
+ should "pass through an explicit offset parameter if specified" do
254
+ Article.expects(:invoke).with(has_entry("offset", 10))
255
+ Article.search :offset => 10
256
+ end
257
+
258
+ should "raise an ArgumentError if the offset is not an Integer" do
259
+ assert_raise(ArgumentError) { Article.search :offset => 'apple' }
260
+ end
261
+
262
+ should "pass through an offset of page - 1 if :page is used instead" do
263
+ Article.expects(:invoke).with(has_entry("offset", 2))
264
+ Article.search :page => 3
265
+ end
266
+
267
+ should "not pass through a page parameter to the API" do
268
+ Article.expects(:invoke).with(Not(has_key("page")))
269
+ Article.search :page => 3
270
+ end
271
+
272
+ should "raise an ArgumentError if the page is not an Integer" do
273
+ assert_raise(ArgumentError) { Article.search :page => 'orange' }
274
+ end
275
+
276
+ should "raise an ArgumentError if the page is less than 1" do
277
+ assert_raise(ArgumentError) { Article.search :page => 0 }
278
+ end
279
+
280
+ should "use the :offset argument if both an :offset and :page are provided" do
281
+ Article.expects(:invoke).with(has_entry("offset", 2))
282
+ Article.search :offset => 2, :page => 203
283
+ end
284
+ end
285
+
286
+ context "rank" do
287
+ %w(newest oldest closest).each do |rank|
288
+ should "accept #{rank} as the argument to rank" do
289
+ Article.expects(:invoke).with(has_entry("rank", rank))
290
+ Article.search :rank => rank.to_sym
291
+ end
292
+ end
293
+
294
+ should "raise an ArgumentError if rank is something else" do
295
+ assert_raise(ArgumentError) { Article.search :rank => :clockwise }
296
+ end
297
+ end
298
+
299
+ Article::TEXT_FIELDS.each do |tf|
300
+ context ":#{tf} parameter" do
301
+ should "prefix each non-quoted term with the #{tf}: field identifier in the query to the API" do
302
+ Article.expects(:invoke).with(has_entry("query", "#{tf}:ice #{tf}:cream"))
303
+ Article.search tf.to_sym => 'ice cream'
304
+ end
305
+
306
+ should "prefix -terms (excluded terms) with -#{tf}:" do
307
+ Article.expects(:invoke).with(has_entry("query", "#{tf}:ice -#{tf}:cream"))
308
+ Article.search tf.to_sym => 'ice -cream'
309
+ end
310
+
311
+ should "put quoted terms behind the field spec" do
312
+ Article.expects(:invoke).with(has_entry("query", "#{tf}:\"ice cream\" #{tf}:cone"))
313
+ Article.search tf.to_sym => '"ice cream" cone'
314
+ end
315
+
316
+ should "handle complicated combinations of expressions" do
317
+ Article.expects(:invoke).with(has_entry("query", "#{tf}:\"ice cream\" -#{tf}:cone #{tf}:\"waffle\""))
318
+ Article.search tf.to_sym => '"ice cream" -cone "waffle"'
319
+ end
320
+ end
321
+ end
322
+
323
+ # context "query parameters" do
324
+ # context "abstract" do
325
+ # should "be prefixed with the abstract: field identifier in the query"
326
+ # should "cast the argument to a string (will figure out processing later)"
327
+ # end
328
+ #
329
+ # context "author" do
330
+ # should "be prefixed with the author: field identifier in the query"
331
+ # should "cast the argument to a string (will figure out processing later)"
332
+ # end
333
+ #
334
+ # context "body" do
335
+ # should "be prefixed with the body: field identifier in the query"
336
+ # should "cast the argument to a string (will figure out processing later)"
337
+ # end
338
+ #
339
+ # context "byline" do
340
+ # should "be prefixed with the body: field identifier in the query"
341
+ # should "cast the argument to a string (will figure out processing later)"
342
+ # end
343
+ # end
344
+ end
345
+
346
+ context "Article.init_from_api" do
347
+ setup do
348
+ @article = Article.init_from_api(ARTICLE_API_HASH2)
349
+ end
350
+
351
+ Article::TEXT_FIELDS.each do |tf|
352
+ context "@#{tf}" do
353
+ should "read the value from the hash input" do
354
+ hash = {}
355
+ hash[tf] = "TEST TEXT"
356
+ article = Article.init_from_api(hash)
357
+ assert_equal "TEST TEXT", article.send(tf)
358
+ end
359
+
360
+ should "properly translate HTML entities back into characters" do
361
+ article = Article.init_from_api(tf => '&#8220;Money for Nothing&#8221;')
362
+ assert_equal "“Money for Nothing”", article.send(tf), article.inspect
363
+ end
364
+
365
+ should "only provide read-only access to the field" do
366
+ article = Article.init_from_api(tf => "TEST TEXT")
367
+ assert !article.respond_to?("#{tf}=")
368
+ end
369
+
370
+ should "return nil if the value is not provided in the hash" do
371
+ article = Article.init_from_api({"foo" => "bar"})
372
+ assert_nil article.send(tf)
373
+ end
374
+ end
375
+ end
376
+
377
+ Article::NUMERIC_FIELDS.each do |tf|
378
+ context "@#{tf}" do
379
+ should "read and coerce the string value from the hash input" do
380
+ article = Article.init_from_api(tf => "23")
381
+ assert_equal 23, article.send(tf)
382
+ end
383
+
384
+ should "only provide read-only access to the field" do
385
+ article = Article.init_from_api(tf => "23")
386
+ assert !article.respond_to?("#{tf}=")
387
+ end
388
+
389
+ should "return nil if the value is not provided in the hash" do
390
+ article = Article.init_from_api({"foo" => "bar"})
391
+ assert_nil article.send(tf)
392
+ end
393
+ end
394
+ end
395
+
396
+ # all the rest
397
+ context "@fee" do
398
+ setup do
399
+ @article = Article.init_from_api(ARTICLE_API_HASH)
400
+ end
401
+
402
+ should "be true if returned as true from the API" do
403
+ article = Article.init_from_api('fee' => true)
404
+ assert_equal true, article.fee?
405
+ assert_equal false, article.free?
406
+ end
407
+
408
+ should "default to false if not specified in the hash" do
409
+ assert_equal false, @article.fee?
410
+ assert_equal true, @article.free?
411
+ end
412
+ end
413
+
414
+ context "@url" do
415
+ setup do
416
+ @article = Article.init_from_api(ARTICLE_API_HASH)
417
+ end
418
+
419
+ should "read the value from the hash" do
420
+ assert_equal ARTICLE_API_HASH['url'], @article.url
421
+ end
422
+
423
+ should "return a String" do
424
+ assert_kind_of(String, @article.url)
425
+ end
426
+
427
+ should "only provide read-only access to the field" do
428
+ assert !@article.respond_to?("url=")
429
+ end
430
+
431
+ should "return nil if the value is not provided in the hash" do
432
+ article = Article.init_from_api({"foo" => "bar"})
433
+ assert_nil article.url
434
+ end
435
+ end
436
+
437
+ context "@page" do
438
+ should "read the value from the page_facet field" do
439
+ assert_equal ARTICLE_API_HASH2['page_facet'].to_i, @article.page
440
+ end
441
+
442
+ should "only provide read-only access to the field" do
443
+ article = Article.new
444
+ assert !article.respond_to?("page=")
445
+ end
446
+
447
+ should "return nil if the value is not provided in the hash" do
448
+ article = Article.init_from_api({"foo" => "bar"})
449
+ assert_nil article.page
450
+ end
451
+ end
452
+ end
453
+ end
454
+
455
+
456
+ # abstract String X X A summary of the article, written by Times indexers
457
+ # author String X X An author note, such as an e-mail address or short biography (compare byline)
458
+ # body String X X A portion of the beginning of the article. Note: Only a portion of the article body is included in responses. But when you search against the body field, you search the full text of the article.
459
+ # byline String X X The article byline, including the author's name
460
+ # classifers_facet Array (Strings) X X Taxonomic classifiers that reflect Times content categories, such as Top/News/Sports
461
+ # column_facet String X X A Times column title (if applicable), such as Weddings or Ideas & Trends
462
+ # date Date X X The publication date in YYYYMMDD format
463
+ # day_of_week_facet String X X The day of the week (e.g., Monday, Tuesday) the article was published (compare publication_day, which is the numeric date rather than the day of the week)
464
+ # des_facet Array (Strings) X X Descriptive subject terms assigned by Times indexers
465
+ #
466
+ # When used in a request, values must be UPPERCASE
467
+ # desk_facet
468
+ # desk_facet String X X The Times desk that produced the story (e.g., Business/Financial Desk)
469
+ # fee Boolean X X Indicates whether users must pay a fee to retrieve the full article
470
+ # geo_facet Array (Strings) X X Standardized names of geographic locations, assigned by Times indexers
471
+ #
472
+ # When used in a request, values must be UPPERCASE
473
+ # lead_paragraph String X X The first paragraph of the article (as it appeared in the printed newspaper)
474
+ # material_type_facet Array (Strings) X X The general article type, such as Biography, Editorial or Review
475
+ # multimedia Array X Associated multimedia features, including URLs (see also the related_multimedia field)
476
+ # nytd_byline_facet String X X The article byline, formatted for NYTimes.com
477
+ # nytd_des_facet Array (Strings) X X Descriptive subject terms, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
478
+ #
479
+ # When used in a request, values must be Mixed Case
480
+ # nytd_geo_facet Array (Strings) X X Standardized names of geographic locations, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
481
+ #
482
+ # When used in a request, values must be Mixed Case
483
+ # nytd_lead_paragraph String X X The first paragraph of the article (as it appears on NYTimes.com)
484
+ # nytd_org_facet Array (Strings) X X Standardized names of organizations, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
485
+ #
486
+ # When used in a request, values must be Mixed Case
487
+ # nytd_per_facet Array (Strings) X X Standardized names of people, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
488
+ #
489
+ # When used in a request, values must be Mixed Case
490
+ # nytd_section_facet Array (Strings) X X The section the article appears in (on NYTimes.com)
491
+ # nytd_title String X X The article title on NYTimes.com (this field may or may not match the title field; headlines may be shortened and edited for the Web)
492
+ # nytd_works_mentioned
493
+ # _facet String X X Literary works mentioned (titles formatted for use on NYTimes.com)
494
+ # org_facet Array (Strings) X X Standardized names of organizations, assigned by Times indexers
495
+ #
496
+ # When used in a request, values must be UPPERCASE
497
+ # page_facet String X X The page the article appeared on (in the printed paper)
498
+ # per_facet Array (Strings) X X Standardized names of people, assigned by Times indexers
499
+ #
500
+ # When used in a request, values must be UPPERCASE
501
+ # publication_day
502
+ # publication_month
503
+ # publication_year Date
504
+ # Date
505
+ # Date X
506
+ # X
507
+ # X X
508
+ # x
509
+ # x The day (DD), month (MM) and year (YYYY) segments of date, separated for use as facets
510
+ # related_multimedia Boolean X X Indicates whether multimedia features are associated with this article. Additional metadata for each related multimedia feature appears in the multimedia array.
511
+ # section_page_facet String X X The full page number of the printed article (e.g., D00002)
512
+ # small_image
513
+ # small_image_url
514
+ # small_image_height
515
+ # small_image_width Boolean
516
+ # String
517
+ # Integer
518
+ # Integer X X
519
+ # X
520
+ # X
521
+ # X The small_image field indicates whether a smaller thumbnail image is associated with the article. The small_image_url field provides the URL of the image on NYTimes.com. The small_image_height and small_image_width fields provide the image dimensions.
522
+ # source_facet String X X The originating body (e.g., AP, Dow Jones, The New York Times)
523
+ # text String X The text field consists of title + byline + body (combined in an OR search) and is the default field for keyword searches. For more information, see Constructing a Search Query.
524
+ # title String X X The article title (headline); corresponds to the headline that appeared in the printed newspaper
525
+ # url String X X The URL of the article on NYTimes.com
526
+ # word_count Integer X The full article word count
527
+ # works_mentioned_facet