factual-api 0.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## v1.0.0
2
+ * fully documented
3
+ * crosswalk(<factual_id>).only('yelp')
4
+
5
+ ## v0.5
6
+ * big refactoring from Rudy
7
+ * ready for releasing
8
+
1
9
  ## v0.3
2
10
  * return hash instead of objects
3
11
  * removing facets
data/README.md CHANGED
@@ -1,71 +1,401 @@
1
- # Introduction
1
+ # About
2
2
 
3
3
  This is the Factual supported Ruby driver for [Factual's public API](http://developer.factual.com/display/docs/Factual+Developer+APIs+Version+3).
4
4
 
5
- # Installation
5
+ This API supports queries to Factual's Read, Schema, Crosswalk, and Resolve APIs. Full documentation is available on the Factual website:
6
6
 
7
- gem 'factual-api'
8
- require 'factual'
9
- factual = Factual.new(YOUR_KEY, YOUR_SECRET)
7
+ * [Read](http://developer.factual.com/display/docs/Factual+Developer+APIs+Version+3): Search the data
8
+ * [Schema](http://developer.factual.com/display/docs/Core+API+-+Schema): Get table metadata
9
+ * [Crosswalk](http://developer.factual.com/display/docs/Places+API+-+Crosswalk): Get third-party IDs
10
+ * [Resolve](http://developer.factual.com/display/docs/Places+API+-+Resolve): Enrich your data and match it against Factual's
11
+
12
+ This driver is supported via the [Factual Developer Group](https://groups.google.com/group/factual_developers)
13
+
14
+ # Overview
15
+
16
+ ## Basic Design
17
+
18
+ The driver allows you to create an authenticated handle to Factual. With a Factual handle, you can send queries and get results back.
19
+
20
+ Queries are created using the Factual handle, which provides a fluent interface to constructing your queries.
21
+
22
+ ````ruby
23
+ # You can chain the query methods, like this:
24
+ factual.table("places").filters("region" => "CA").search("sushi", "sashimi")
25
+ .geo("$circle" => {"$center" => [34.06021, -118.41828], "$meters" => 5000})
26
+ .sort("name").page(2, :per => 10)
27
+ ````
28
+
29
+ Results are returned as Ruby Arrays of Hashes, where each Hash is a result record.
30
+
31
+ ## Setup
32
+
33
+ The driver's gems are hosted at [Rubygems.org](http://rubygems.org). You can install the factual-api gem as follows:
34
+
35
+ `````bash
36
+ $ gem install factual-api
37
+ `````
38
+
39
+ Once the gem is installed, you can use it in your Ruby project like:
40
+
41
+ ````ruby
42
+ require 'factual'
43
+ factual = Factual.new("YOUR_KEY", "YOUR_SECRET")
44
+ ````
10
45
 
11
- # Examples
46
+ ## Simple Read Examples
47
+
48
+ `````ruby
49
+ # Return entities from the Places dataset with names beginning with "starbucks"
50
+ factual.table("places").filters("name" => {"$bw" => "starbucks"}).rows
51
+ ````
52
+
53
+ `````ruby
54
+ # Return entity names and non-blank websites from the Global dataset, for entities located in Thailand
55
+ factual.table("global").select(:name, :website)
56
+ .filters({"country" => "TH", "website" => {"$blank" => false}})
57
+ ````
58
+
59
+ `````ruby
60
+ # Return highly rated U.S. restaurants in Los Angeles with WiFi
61
+ factual.table("restaurants-us")
62
+ .filters({"locality" => "los angeles", "rating" => {"$gte" => 4}, "wifi" => true}).rows
63
+ ````
64
+
65
+ ## Simple Crosswalk Example
66
+
67
+ ````ruby
68
+ # Concordance information of a place
69
+ FACTUAL_ID = "110ace9f-80a7-47d3-9170-e9317624ebd9"
70
+ query = factual.crosswalk(FACTUAL_ID)
71
+ query.rows
72
+ ````
73
+
74
+ ## Simple Resolve Example
75
+
76
+ ````ruby
77
+ # Returns resolved entities as an array of hashes
78
+ query = factual.resolve("name" => "McDonalds",
79
+ "address" => "10451 Santa Monica Blvd",
80
+ "region" => "CA",
81
+ "postcode" => "90025")
82
+
83
+ query.first["resolved"] # true or false
84
+ query.rows # all candidate rows
85
+ ````
86
+
87
+ ## More Read Examples
88
+
89
+ ````ruby
90
+ # 1. Specify the table Global
91
+ query = factual.table("global")
92
+ ````
93
+
94
+ ````ruby
95
+ # 2. Filter results in country US
96
+ query = query.filters("country" => "US")
97
+ ````
98
+
99
+ ````ruby
100
+ # 3. Search for "sushi" or "sashimi"
101
+ query = query.search("sushi", "sashimi")
102
+ ````
103
+
104
+ ````ruby
105
+ # 4. Filter by geolocation
106
+ query = query.geo("$circle" => {"$center" => [34.06021, -118.41828], "$meters" => 5000})
107
+ ````
108
+
109
+ ````ruby
110
+ # 5. Sort it
111
+ query = query.sort("name") # ascending
112
+ query = query.sort_desc("name") # descending
113
+ query = query.sort("address", "name") # sort by multiple columns
114
+ ````
115
+
116
+ ````ruby
117
+ # 6. Page it
118
+ query = query.page(2, :per => 10)
119
+ ````
120
+
121
+ ````ruby
122
+ # 7. Finally, get response in a hash or array of hashes
123
+ query.first # return one row
124
+ query.rows # return many rows
125
+ ````
126
+
127
+ ````ruby
128
+ # 8. Returns total row counts that matches the criteria
129
+ query.total_count
130
+ ````
131
+
132
+ # Read API
133
+
134
+ ## All Top Level Query Parameters
135
+
136
+ <table>
137
+ <col width="33%"/>
138
+ <col width="33%"/>
139
+ <col width="33%"/>
140
+ <tr>
141
+ <th>Parameter</th>
142
+ <th>Description</th>
143
+ <th>Example</th>
144
+ </tr>
145
+ <tr>
146
+ <td>filters</td>
147
+ <td>Restrict the data returned to conform to specific conditions.</td>
148
+ <td><tt>query = query.filters("name" => {"$bw" => "starbucks"})</tt></td>
149
+ </tr>
150
+ <tr>
151
+ <td>get total row count</td>
152
+ <td>returns the total count of the number of rows in the dataset that conform to the query.</td>
153
+ <td><tt>query.total_count</tt></td>
154
+ </tr>
155
+ <tr>
156
+ <td>geo</td>
157
+ <td>Restrict data to be returned to be within a geographical range based.</td>
158
+ <td>(See the section on Geo Filters)</td>
159
+ </tr>
160
+ <tr>
161
+ <td>limit</td>
162
+ <td>Limit the results</td>
163
+ <td><tt>query = query.limit(12)</tt></td>
164
+ </tr>
165
+ <tr>
166
+ <td>page</td>
167
+ <td>Limit the results to a specific "page".</td>
168
+ <td><tt>query = query.page(2, :per => 10)</tt></td>
169
+ </tr>
170
+ <tr>
171
+ <td>search (across entity)</td>
172
+ <td>Full text search across entity</td>
173
+ <td>
174
+ Find "sushi":<br><tt>query = query.search("sushi")</tt><p>
175
+ Find "sushi" or "sashimi":<br><tt>query = query.search("sushi", "sashimi")</tt><p>
176
+ Find "sushi" and "santa" and "monica":<br><tt>query.search("sushi santa monica")</tt>
177
+ </td>
178
+ </tr>
179
+ <tr>
180
+ <td>search (across field)</td>
181
+ <td>Full text search on specific field</td>
182
+ <td><tt>query = query.filters({"name" => {"$search" => "cafe"}})</tt></td>
183
+ </tr>
184
+ <tr>
185
+ <td>select</td>
186
+ <td>Specifiy which fields to include in the query results. Note that the order of fields will not necessarily be preserved in the resulting response due to the nature Hashes.</td>
187
+ <td><tt>query = query.select(:name, :address, :locality, :region)</tt></td>
188
+ </tr>
189
+ <tr>
190
+ <td>sort</td>
191
+ <td>The field (or fields) to sort data on, as well as the direction of sort.<p>
192
+ Sorts ascending by default, but supports both explicitly sorting ascending and descending, by using <tt>sort_asc</tt> or <tt>sort_desc</tt>.
193
+ Supports $distance as a sort option if a geo-filter is specified.<p>
194
+ Supports $relevance as a sort option if a full text search is specified either using the q parameter or using the $search operator in the filter parameter.<p>
195
+ By default, any query with a full text search will be sorted by relevance.<p>
196
+ Any query with a geo filter will be sorted by distance from the reference point. If both a geo filter and full text search are present, the default will be relevance followed by distance.</td>
197
+ <td><tt>query = query.sort("name")</tt><br>
198
+ <tt>query = query.sort_desc("$distance")</tt>
199
+ <tt>query = query.sort_asc("name").sort_desc("rating")</tt></td>
200
+ </tr>
201
+ </table>
202
+
203
+ ## Row Filters
204
+
205
+ The driver supports various row filter logic. For example:
206
+
207
+ `````ruby
208
+ # Returns records from the Places dataset with names beginning with "starbucks"
209
+ factual.table("places").filters("name" => {"$bw" => "starbucks"}).rows
210
+ ````
211
+
212
+ ### Supported row filter logic
213
+
214
+ <table>
215
+ <tr>
216
+ <th>Predicate</th>
217
+ <th width="25%">Description</th>
218
+ <th>Example</th>
219
+ </tr>
220
+ <tr>
221
+ <td>$eq</td>
222
+ <td>equal to</td>
223
+ <td><tt>query = query.filters("region" => {"$eq" => "CA"})</tt></td>
224
+ </tr>
225
+ <tr>
226
+ <td>$neq</td>
227
+ <td>not equal to</td>
228
+ <td><tt>query = query.filters("region" => {"$neq" => "CA"})</tt></td>
229
+ </tr>
230
+ <tr>
231
+ <td>search</td>
232
+ <td>full text search</td>
233
+ <td><tt>query = query.search("sushi")</tt></td>
234
+ </tr>
235
+ <tr>
236
+ <td>$in</td>
237
+ <td>equals any of</td>
238
+ <td><tt>query = query.filters("region" => {"$in" => ["CA", "NM", "NY"]})</tt></td>
239
+ </tr>
240
+ <tr>
241
+ <td>$nin</td>
242
+ <td>does not equal any of</td>
243
+ <td><tt>query = query.filters("region" => {"$nin" => ["CA", "NM", "NY"]})</tt></td>
244
+ </tr>
245
+ <tr>
246
+ <td>$bw</td>
247
+ <td>begins with</td>
248
+ <td><tt>query = query.filters("name" => {"$bw" => "starbucks"})</tt></td>
249
+ </tr>
250
+ <tr>
251
+ <td>$nbw</td>
252
+ <td>does not begin with</td>
253
+ <td><tt>query = query.filters("name" => {"$nbw" => "starbucks"})</tt></td>
254
+ </tr>
255
+ <tr>
256
+ <td>$bwin</td>
257
+ <td>begins with any of</td>
258
+ <td><tt>query = query.filters("name" => {"$bwin" => ["starbucks", "coffee", "tea"]})</tt></td>
259
+ </tr>
260
+ <tr>
261
+ <td>$nbwin</td>
262
+ <td>does not begin with any of</td>
263
+ <td><tt>query = query.filters("name" => {"$nbwin" => ["starbucks", "coffee", "tea"]})</tt></td>
264
+ </tr>
265
+ <tr>
266
+ <td>$blank</td>
267
+ <td>test to see if a value is (or is not) blank or null</td>
268
+ <td><tt>query = query.filters("tel" => {"$blank" => true})</tt><br>
269
+ <tt>query = query.filters("website" => {"$blank" => false})</tt></td>
270
+ </tr>
271
+ <tr>
272
+ <td>$gt</td>
273
+ <td>greater than</td>
274
+ <td><tt>query = query.filters("rating" => {"$gt" => 7.5})</tt></td>
275
+ </tr>
276
+ <tr>
277
+ <td>$gte</td>
278
+ <td>greater than or equal</td>
279
+ <td><tt>query = query.filters("rating" => {"$gte" => 7.5})</tt></td>
280
+ </tr>
281
+ <tr>
282
+ <td>$lt</td>
283
+ <td>less than</td>
284
+ <td><tt>query = query.filters("rating" => {"$lt" => 7.5})</tt></td>
285
+ </tr>
286
+ <tr>
287
+ <td>$lte</td>
288
+ <td>less than or equal</td>
289
+ <td><tt>query = query.filters("rating" => {"$lte" => 7.5})</tt></td>
290
+ </tr>
291
+ </table>
292
+
293
+ ### AND
294
+
295
+ Filters can be logically AND'd together. For example:
296
+
297
+ ````ruby
298
+ # name begins with "coffee" AND tel is not blank
299
+ query = query.filters({ "$and" => [{"name" => {"$bw" => "coffee"}}, {"tel" => {"$blank" => false}}] })
300
+ ````
301
+
302
+ ### OR
303
+
304
+ Filters can be logically OR'd. For example:
305
+
306
+ ````ruby
307
+ # name begins with "coffee" OR tel is not blank
308
+ query = query.filters({ "$or" => [{"name" => {"$bw" => "coffee"}}, {"tel" => {"$blank" => false}}] })
309
+ ````
310
+
311
+ ### Combined ANDs and ORs
312
+
313
+ You can nest AND and OR logic to whatever level of complexity you need. For example:
314
+
315
+ ````ruby
316
+ # (name begins with "Starbucks") OR (name begins with "Coffee")
317
+ # OR
318
+ # (name full text search matches on "tea" AND tel is not blank)
319
+ query = query.filters({ "$or" => [ {"$or" => [ {"name" => {"$bw" => "starbucks"}},
320
+ {"name" => {"$bw" => "coffee"}}]},
321
+ {"$and" => [ {"name" => {"$search" => "tea"}},
322
+ {"tel" => {"$blank" => false}} ]} ]})
323
+ ````
324
+
325
+ # Crosswalk
326
+
327
+ The driver fully supports Factual's Crosswalk feature, which lets you "crosswalk" the web and relate entities between Factual's data and that of other web authorities.
328
+
329
+ (See [the Crosswalk Blog](http://blog.factual.com/crosswalk-api) for more background.)
330
+
331
+ ## Simple Crosswalk Example
332
+
333
+ ````ruby
334
+ # Get all Crosswalk data for a Place with a specific FactualID
335
+ factual.crosswalk("110ace9f-80a7-47d3-9170-e9317624ebd9").rows
336
+ ````
337
+
338
+ ## Supported Crosswalk Options
339
+
340
+ ### only
341
+
342
+ You can specify which namespaces you want, like this:
343
+
344
+ ````ruby
345
+ # Get Crosswalk data for only yelp and facebook for a Place with a specific FactualID
346
+ factual.crosswalk("110ace9f-80a7-47d3-9170-e9317624ebd9").only(:yelp, :facebook).rows
347
+ ````
348
+
349
+ This will generally return 1 record per requested namespace.
350
+
351
+ ### limit
12
352
 
13
- ## Quick Sample
353
+ You can limit the amount of Crosswalk results you get back, like this:
14
354
 
15
- # Returns Places with names beginning with "Star"
16
- factual.table("places").filters("name" => {"$bw" => "Star"}).rows
355
+ ````ruby
356
+ # Get only 3 Crosswalk results for a Place with a specific FactualID
357
+ factual.crosswalk("110ace9f-80a7-47d3-9170-e9317624ebd9").limit(3).rows
358
+ ````
17
359
 
18
- ## Read (with all features)
360
+ # Resolve
19
361
 
20
- # 1. Specify the table Global
21
- query = factual.table("global")
362
+ The driver fully supports Factual's Resolve feature, which lets you start with incomplete data you may have for an entity, and get potential entity matches back from Factual.
22
363
 
23
- # 2. Filter results in country US (For more filters syntax, refer to [Core API - Row Filters](http://developer.factual.com/display/docs/Core+API+-+Row+Filters))
24
- query = query.filters("country" => "US")
364
+ Each result record will include a confidence score (<tt>"similarity"</tt>), and a flag indicating whether Factual decided the entity is the correct resolved match with a high degree of accuracy (<tt>"resolved"</tt>).
25
365
 
26
- # 3. Search for "sushi" or "sashimi" (For more search syntax, refer to [Core API - Search Filters](http://developer.factual.com/display/docs/Core+API+-+Search+Filters))
27
- query = query.search("sushi", "sashimi")
366
+ For any Resolve query, there will be 0 or 1 entities returned with <tt>"resolved"=true</tt>. If there was a full match, it is guaranteed to be the first record in the response Array.
28
367
 
29
- # 4. Filter by Geo (For more geo syntax, refer to [Core API - Geo Filters](http://developer.factual.com/display/docs/Core+API+-+Geo+Filters))
30
- query = query.geo("$circle" => {"$center" => [34.06021, -118.41828], "$meters" => 5000})
368
+ (See [the Resolve Blog](http://blog.factual.com/factual-resolve) for more background.)
31
369
 
32
- # 5. Sort it
33
- query = query.sort("name") # ascending
34
- query = query.sort_desc("name") # descending
35
- query = query.sort("address", "name") # sort by multiple columns
370
+ ## Simple Resolve Examples
36
371
 
37
- # 6. Page it
38
- query = query.page(2, :per => 10)
372
+ ````ruby
373
+ # Returns resolved entities as an array of hashes
374
+ query = factual.resolve("name" => "McDonalds",
375
+ "address" => "10451 Santa Monica Blvd",
376
+ "region" => "CA",
377
+ "postcode" => "90025")
39
378
 
40
- # 7. Finally, get response in a hash or array of hashes
41
- query.first # return one row
42
- query.rows # return many rows
379
+ query.first["resolved"] # true or false
380
+ query.rows # all candidate rows
381
+ ````
43
382
 
44
- # 8. Returns total row counts that matches the criteria
45
- query.total_count
383
+ # Geo Filters
46
384
 
47
- # You can chain the query methods, like this
48
- factual.table("places").filters("region" => "CA").search("sushi", "sashimi").geo("$circle" => {"$center" => [34.06021, -118.41828], "$meters" => 5000}).sort("name").page(2, :per => 10).rows
385
+ You can query Factual for entities located within a geographic area. For example:
49
386
 
50
- ## Crosswalk
387
+ ````ruby
388
+ query = query.geo("$circle" => {"$center" => [34.06021, -118.41828], "$meters" => 5000})
389
+ ````
51
390
 
52
- # Concordance information of a place
53
- FACTUAL_ID = "110ace9f-80a7-47d3-9170-e9317624ebd9"
54
- query = factual.crosswalk(FACTUAL_ID)
55
- query.rows
391
+ # Schema
56
392
 
57
- ## Resolve
393
+ You can query Factual for the detailed schema of any specific table in Factual. For example:
58
394
 
59
- # Returns resolved entities as an array of hashes
60
- query = factual.resolve("name" => "McDonalds",
61
- "address" => "10451 Santa Monica Blvd",
62
- "region" => "CA",
63
- "postcode" => "90025")
395
+ ````ruby
396
+ # Returns a hash of metadata for the table named "global", including an array of fields
397
+ factual.table("global").schema
398
+ ````
64
399
 
65
- query.first.resolved # true or false
66
- query.rows # resolved rows
67
400
 
68
- ## Schema
69
401
 
70
- # Returns a hash of table metadata, including an array of fields
71
- factual.table("global").schema
data/lib/factual.rb CHANGED
@@ -1,22 +1,24 @@
1
1
  require 'oauth'
2
2
  require 'factual/api'
3
- require 'factual/query'
3
+ require 'factual/query/table'
4
+ require 'factual/query/resolve'
5
+ require 'factual/query/crosswalk'
4
6
 
5
7
  class Factual
6
8
  def initialize(key, secret)
7
- @api = Factual::API.new(generate_token(key, secret))
9
+ @api = API.new(generate_token(key, secret))
8
10
  end
9
11
 
10
- def crosswalk(factual_id)
11
- Query.new(@api, :crosswalk, "places/crosswalk", :factual_id => factual_id)
12
+ def table(table_id_or_alias)
13
+ Query::Table.new(@api, "t/#{table_id_or_alias}")
12
14
  end
13
15
 
14
- def resolve(values)
15
- Query.new(@api, :resolve, "places/resolve", :values => values)
16
+ def crosswalk(factual_id)
17
+ Query::Crosswalk.new(@api, :factual_id => factual_id)
16
18
  end
17
19
 
18
- def table(table_id_or_alias)
19
- Query.new(@api, :read, "t/#{table_id_or_alias}")
20
+ def resolve(values)
21
+ Query::Resolve.new(@api, :values => values)
20
22
  end
21
23
 
22
24
  private
data/lib/factual/api.rb CHANGED
@@ -5,7 +5,7 @@ class Factual
5
5
  class API
6
6
  API_V3_HOST = "http://api.v3.factual.com"
7
7
  DRIVER_VERSION_TAG = "factual-ruby-driver-1.0"
8
- PARAM_ALIASES = { :search => :q }
8
+ PARAM_ALIASES = { :search => :q, :sort_asc => :sort }
9
9
 
10
10
  def initialize(access_token)
11
11
  @access_token = access_token
@@ -23,17 +23,13 @@ class Factual
23
23
  private
24
24
 
25
25
  def handle_request(action, path, params)
26
- response = make_request(path, params)
27
- json = response.body
28
- payload = JSON.parse(json)
29
-
26
+ payload = JSON.parse(make_request(path, params).body)
30
27
  raise StandardError.new(payload["message"]) unless payload["status"] == "ok"
31
-
32
28
  payload["response"]
33
29
  end
34
30
 
35
31
  def make_request(path, params)
36
- url = "#{API_V3_HOST}/#{path}?#{query_string(params)}"
32
+ url = "#{API_V3_HOST}/#{path}?#{query_string(params)}"
37
33
  headers = { "X-Factual-Lib" => DRIVER_VERSION_TAG }
38
34
 
39
35
  @access_token.get(url, headers)
@@ -0,0 +1,49 @@
1
+ class Factual
2
+ module Query
3
+ class Base
4
+ include Enumerable
5
+
6
+ def initialize(api, params)
7
+ @api = api
8
+ @params = params
9
+ end
10
+
11
+ attr_reader :action, :path, :params
12
+
13
+ def each(&block)
14
+ rows.each { |row| block.call(row) }
15
+ end
16
+
17
+ def last
18
+ rows.last
19
+ end
20
+
21
+ def [](index)
22
+ rows[index]
23
+ end
24
+
25
+ def rows
26
+ response["data"]
27
+ end
28
+
29
+ def total_rows
30
+ response["total_row_count"]
31
+ end
32
+
33
+ def schema
34
+ @schema ||= @api.schema(self)
35
+ end
36
+
37
+ private
38
+
39
+ def form_value(args)
40
+ args = args.map { |arg| arg.is_a?(String) ? arg.strip : arg }
41
+ args.length == 1 ? args.first : args.join(',')
42
+ end
43
+
44
+ def response
45
+ @response ||= @api.execute(self)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,19 @@
1
+ require 'factual/query/base'
2
+
3
+ class Factual
4
+ module Query
5
+ class Crosswalk < Base
6
+ def initialize(api, params = {})
7
+ @path = "places/crosswalk"
8
+ @action = :crosswalk
9
+ super(api, params)
10
+ end
11
+
12
+ [:factual_id, :only, :limit, :include_count].each do |param|
13
+ define_method(param) do |*args|
14
+ self.class.new(@api, @params.merge(param => form_value(args)))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'factual/query/base'
2
+
3
+ class Factual
4
+ module Query
5
+ class Resolve < Base
6
+ def initialize(api, params = {})
7
+ @path = "places/resolve"
8
+ @action = :resolve
9
+ super(api, params)
10
+ end
11
+
12
+ [:values, :include_count].each do |param|
13
+ define_method(param) do |*args|
14
+ self.class.new(@api, @params.merge(param => form_value(args)))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ require 'factual/query/base'
2
+
3
+ class Factual
4
+ module Query
5
+ class Table < Base
6
+ DEFAULT_LIMIT = 20
7
+
8
+ def initialize(api, path, params = {})
9
+ @path = path
10
+ @action = :read
11
+ super(api, params)
12
+ end
13
+
14
+ [:filters, :search, :geo, :sort, :select, :limit, :offset, :include_count].each do |param|
15
+ define_method(param) do |*args|
16
+ Table.new(@api, @path, @params.merge(param => form_value(args)))
17
+ end
18
+ end
19
+
20
+ def sort_asc(*args)
21
+ columns = args.map { |column| "#{column}" }
22
+ Table.new(@api, @path, @params.merge(:sort => columns.join(',')))
23
+ end
24
+
25
+ def sort_desc(*args)
26
+ columns = args.map { |column| "#{column}:desc" }
27
+ Table.new(@api, @path, @params.merge(:sort => columns.join(',')))
28
+ end
29
+
30
+ def page(page_number, paging_options = {})
31
+ limit = (paging_options[:per] || paging_options["per"] || DEFAULT_LIMIT).to_i
32
+ limit = DEFAULT_LIMIT if limit < 1
33
+
34
+ page_number = page_number.to_i
35
+ page_number = 1 if page_number < 1
36
+
37
+ offset = (page_number - 1) * limit
38
+ Table.new(@api, @path, @params.merge(:limit => limit, :offset => offset))
39
+ end
40
+ end
41
+ end
42
+ end
metadata CHANGED
@@ -2,33 +2,55 @@
2
2
  name: factual-api
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: "0.5"
5
+ version: 1.0.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Rudiger Lippert
9
9
  - Forrest Cao
10
- - Aaron Crow
11
10
  autorequire:
12
11
  bindir: bin
13
12
  cert_chain: []
14
13
 
15
- date: 2012-01-19 00:00:00 +08:00
14
+ date: 2012-01-25 00:00:00 +08:00
16
15
  default_executable:
17
16
  dependencies:
18
17
  - !ruby/object:Gem::Dependency
19
- name: rspec
18
+ name: oauth
20
19
  prerelease: false
21
20
  requirement: &id001 !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.4.4
26
+ type: :runtime
27
+ version_requirements: *id001
28
+ - !ruby/object:Gem::Dependency
29
+ name: json
30
+ prerelease: false
31
+ requirement: &id002 !ruby/object:Gem::Requirement
32
+ none: false
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 1.2.0
37
+ type: :runtime
38
+ version_requirements: *id002
39
+ - !ruby/object:Gem::Dependency
40
+ name: rspec
41
+ prerelease: false
42
+ requirement: &id003 !ruby/object:Gem::Requirement
22
43
  none: false
23
44
  requirements:
24
45
  - - ">="
25
46
  - !ruby/object:Gem::Version
26
47
  version: "0"
27
48
  type: :development
28
- version_requirements: *id001
49
+ version_requirements: *id003
29
50
  description: Factual's official Ruby driver for the Factual public API.
30
51
  email:
31
- - aaron@factual.com
52
+ - rudy@factual.com
53
+ - forrest@factual.com
32
54
  executables: []
33
55
 
34
56
  extensions: []
@@ -37,7 +59,10 @@ extra_rdoc_files: []
37
59
 
38
60
  files:
39
61
  - lib/factual/api.rb
40
- - lib/factual/query.rb
62
+ - lib/factual/query/base.rb
63
+ - lib/factual/query/crosswalk.rb
64
+ - lib/factual/query/resolve.rb
65
+ - lib/factual/query/table.rb
41
66
  - lib/factual.rb
42
67
  - README.md
43
68
  - CHANGELOG.md
@@ -55,7 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
80
  requirements:
56
81
  - - ">="
57
82
  - !ruby/object:Gem::Version
58
- version: "0"
83
+ version: 1.8.6
59
84
  required_rubygems_version: !ruby/object:Gem::Requirement
60
85
  none: false
61
86
  requirements:
data/lib/factual/query.rb DELETED
@@ -1,105 +0,0 @@
1
- class Factual
2
- class Query
3
- include Enumerable
4
-
5
- DEFAULT_LIMIT = 20
6
- VALID_PARAMS = {
7
- :read => [ :filters, :search, :geo, :sort, :select, :limit, :offset ],
8
- :resolve => [ :values ],
9
- :crosswalk => [ :factual_id ],
10
- :schema => [ ],
11
- :any => [ :include_count ]
12
- }
13
-
14
- def initialize(api, action, path, params = {})
15
- @api = api
16
- @action = action
17
- @path = path
18
- @params = params
19
- @response = nil
20
- @schema = nil
21
- validate_params
22
- end
23
-
24
- # Attribute Readers
25
- attr_reader :action
26
-
27
- [:path, :params].each do |attribute|
28
- define_method(attribute) do
29
- instance_variable_get("@#{attribute}").clone
30
- end
31
- end
32
-
33
- # Response Methods
34
- def each(&block)
35
- rows.each { |row| block.call(row) }
36
- end
37
-
38
- def last
39
- rows.last
40
- end
41
-
42
- def [](index)
43
- rows[index]
44
- end
45
-
46
- def rows
47
- (@response ||= @api.execute(self))["data"].clone
48
- end
49
-
50
- def total_rows
51
- (@response ||= @api.execute(self))["total_row_count"]
52
- end
53
-
54
- def schema
55
- (@schema ||= @api.schema(self)).clone
56
- end
57
-
58
- # Query Modifiers
59
- VALID_PARAMS.values.flatten.uniq.each do |param|
60
- define_method(param) do |*args|
61
- args = args.collect{|arg| arg.is_a?(String) ? arg.strip : arg }
62
- value = (args.length == 1) ? args.first : args.join(',')
63
-
64
- new_params = @params.clone
65
- new_params[param] = value
66
-
67
- Query.new(@api, @action, @path, new_params)
68
- end
69
- end
70
-
71
- def sort_desc(*args)
72
- columns = args.map { |column| "#{column}:desc" }
73
-
74
- new_params = @params.clone
75
- new_params[:sort] = columns.join(',')
76
-
77
- Query.new(@api, @action, @path, new_params)
78
- end
79
-
80
- def page(page_number, paging_options = {})
81
- limit = (paging_options[:per] || paging_options["per"] || DEFAULT_LIMIT).to_i
82
- limit = DEFAULT_LIMIT if limit < 1
83
-
84
- page_number = page_number.to_i
85
- page_number = 1 if page_number < 1
86
-
87
- new_params = @params.clone
88
- new_params[:limit] = limit
89
- new_params[:offset] = (page_number - 1) * limit
90
-
91
- Query.new(@api, @action, @path, new_params)
92
- end
93
-
94
- private
95
-
96
- # Validations
97
- def validate_params
98
- @params.each do |param, val|
99
- unless (VALID_PARAMS[@action] + VALID_PARAMS[:any]).include?(param)
100
- raise StandardError.new("InvalidArgument #{param} for #{@action}")
101
- end
102
- end
103
- end
104
- end
105
- end