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 +8 -0
- data/README.md +377 -47
- data/lib/factual.rb +10 -8
- data/lib/factual/api.rb +3 -7
- data/lib/factual/query/base.rb +49 -0
- data/lib/factual/query/crosswalk.rb +19 -0
- data/lib/factual/query/resolve.rb +19 -0
- data/lib/factual/query/table.rb +42 -0
- metadata +33 -8
- data/lib/factual/query.rb +0 -105
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,71 +1,401 @@
|
|
1
|
-
#
|
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
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
353
|
+
You can limit the amount of Crosswalk results you get back, like this:
|
14
354
|
|
15
|
-
|
16
|
-
|
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
|
-
|
360
|
+
# Resolve
|
19
361
|
|
20
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
379
|
+
query.first["resolved"] # true or false
|
380
|
+
query.rows # all candidate rows
|
381
|
+
````
|
43
382
|
|
44
|
-
|
45
|
-
query.total_count
|
383
|
+
# Geo Filters
|
46
384
|
|
47
|
-
|
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
|
-
|
387
|
+
````ruby
|
388
|
+
query = query.geo("$circle" => {"$center" => [34.06021, -118.41828], "$meters" => 5000})
|
389
|
+
````
|
51
390
|
|
52
|
-
|
53
|
-
FACTUAL_ID = "110ace9f-80a7-47d3-9170-e9317624ebd9"
|
54
|
-
query = factual.crosswalk(FACTUAL_ID)
|
55
|
-
query.rows
|
391
|
+
# Schema
|
56
392
|
|
57
|
-
|
393
|
+
You can query Factual for the detailed schema of any specific table in Factual. For example:
|
58
394
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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 =
|
9
|
+
@api = API.new(generate_token(key, secret))
|
8
10
|
end
|
9
11
|
|
10
|
-
def
|
11
|
-
Query.new(@api,
|
12
|
+
def table(table_id_or_alias)
|
13
|
+
Query::Table.new(@api, "t/#{table_id_or_alias}")
|
12
14
|
end
|
13
15
|
|
14
|
-
def
|
15
|
-
Query.new(@api, :
|
16
|
+
def crosswalk(factual_id)
|
17
|
+
Query::Crosswalk.new(@api, :factual_id => factual_id)
|
16
18
|
end
|
17
19
|
|
18
|
-
def
|
19
|
-
Query.new(@api, :
|
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
|
-
|
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
|
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:
|
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-
|
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:
|
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: *
|
49
|
+
version_requirements: *id003
|
29
50
|
description: Factual's official Ruby driver for the Factual public API.
|
30
51
|
email:
|
31
|
-
-
|
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:
|
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
|