factual-api 0.5 → 1.0.0
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.
- 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
|