factual-api 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/CHANGELOG.md +9 -0
  2. data/README.md +73 -0
  3. data/lib/factual_api.rb +229 -0
  4. metadata +70 -0
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## v0.1
2
+ * initial version
3
+ ** a table agnostic read api, usage as: api.table(table_alias).rows
4
+ ** normal (but powerful) filters, usage as: api.table(:global).filters(:country => {'$sw' => 'u'}, :region => 'ca')
5
+ ** general mapping of ALL v3 api params, e.g. geo, query, sort, select, limit, offset
6
+ ** places sugers, usage as: api.crosswalk(FACTUAL_ID); api.resolve(:name => 'factual inc.', :region => 'ca')
7
+ ** facets
8
+ ** different format, usage as: json_api = Factual::Api.new( KEY, SECRET, :json)
9
+ ** immutable api objects
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Introduction
2
+
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
+
5
+ # Installation
6
+
7
+ gem 'factual-api'
8
+ require 'factual_api'
9
+ factual = Factual::Api.new(YOUR_KEY, YOUR_SECRET)
10
+
11
+ # Examples
12
+
13
+ ## Quick Sample
14
+
15
+ # Returns Places with names beginning with "Star"
16
+ factual.table("places").filters("name" => {"$bw" => "Star"}).rows
17
+
18
+ ## Read (with all features)
19
+
20
+ # 1. Specify the table Global
21
+ query = factual.table("global")
22
+
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")
25
+
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")
28
+
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})
31
+
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
36
+
37
+ # 6. Page it
38
+ query = query.page(2, :per => 10)
39
+
40
+ # 7. Finally, get response in Factual::Row objects
41
+ query.first # return one row
42
+ query.rows # return many rows
43
+
44
+ # 8. Returns total row counts that matches the criteria
45
+ query.total_count
46
+
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
49
+
50
+ ## Crosswalk
51
+
52
+ # Concordance information of a place
53
+ FACTUAL_ID = "110ace9f-80a7-47d3-9170-e9317624ebd9"
54
+ factual.crosswalk(FACTUAL_ID)
55
+
56
+ ## Resolve
57
+
58
+ # Returns resolved entities as Factual::Row objects
59
+ factual.resolve("name" => "McDonalds",
60
+ "address" => "10451 Santa Monica Blvd",
61
+ "region" => "CA",
62
+ "postcode" => "90025")
63
+
64
+ ## Schema
65
+
66
+ # Returns a Factual::Table object whose fields are an array of Factual::Field objects
67
+ factual.table("global").schema()
68
+
69
+ ## Facets
70
+
71
+ # Returns number of starbucks in regions thoughout the world
72
+ factual.table("global").select("country", "region").search("starbucks").min_count(2).facets()
73
+
@@ -0,0 +1,229 @@
1
+ require 'oauth'
2
+ require 'json'
3
+ require 'uri'
4
+ require 'ostruct'
5
+
6
+ module Factual
7
+ class Api
8
+ API_V3_HOST = "http://api.v3.factual.com"
9
+ DRIVER_VERSION_TAG = "factual-ruby-driver-1.0"
10
+
11
+ DEFAULT_LIMIT = 20
12
+ PARAM_ALIASES = { :search => :q }
13
+
14
+ VALID_PARAMS = {
15
+ :read => [ :filters, :search, :geo, :sort, :select, :limit, :offset ],
16
+ :resolve => [ :values ],
17
+ :crosswalk => [ :factual_id ],
18
+ :facets => [ :filters, :search, :geo, :limit, :select, :min_count ],
19
+ :schema => [ ],
20
+ :any => [ :include_count ]
21
+ }
22
+
23
+ attr_accessor :access_token, :path, :params, :action, :format
24
+
25
+ # initializers
26
+ # ----------------
27
+ def initialize(key, secret, format = :object)
28
+ @access_token = OAuth::AccessToken.new(
29
+ OAuth::Consumer.new(key, secret))
30
+
31
+ @format = format
32
+ @params = Hash.new
33
+ end
34
+
35
+ # helper functions
36
+ # ----------------
37
+ def self.clone(api)
38
+ new_api = self.new(nil, nil)
39
+
40
+ new_api.access_token = api.access_token
41
+ new_api.path = api.path
42
+ new_api.action = api.action
43
+ new_api.format = api.format
44
+ new_api.params = api.params.clone
45
+
46
+ return new_api
47
+ end
48
+
49
+ def set_param(key, value)
50
+ @params[key] = value
51
+ end
52
+
53
+ # attributes, after 'get'
54
+ # ----------------
55
+ def first
56
+ row_data = response["data"].first
57
+
58
+ if (@format == :json) # or :hash ?
59
+ return row_data
60
+ else
61
+ return Row.new(row_data)
62
+ end
63
+ end
64
+
65
+ def schema
66
+ @path += "/schema"
67
+ @action = :schema
68
+
69
+ view = response["view"]
70
+ fields = view["fields"]
71
+
72
+ schema = Table.new(view)
73
+ if schema && fields
74
+ schema.fields = fields.collect do |f|
75
+ Field.new(f)
76
+ end
77
+ end
78
+
79
+ return schema
80
+ end
81
+
82
+ def facets
83
+ @path += "/facets"
84
+ @action = :facets
85
+ columns = response["data"]
86
+
87
+ return Facet.new(columns)
88
+ end
89
+
90
+ def total_count
91
+ response["total_row_count"]
92
+ end
93
+
94
+ def rows
95
+ return response["data"] if (@format == :json)
96
+
97
+ return response["data"].collect do |row_data|
98
+ Row.new(row_data)
99
+ end
100
+ end
101
+
102
+ # query builder, returns immutable ojbects
103
+ # ----------------
104
+ VALID_PARAMS.values.flatten.uniq.each do |param|
105
+ define_method(param) do |*args|
106
+ api = self.class.clone(self)
107
+ val = (args.length == 1) ? args.first : args.join(',')
108
+
109
+ api.set_param(param, val)
110
+
111
+ return api
112
+ end
113
+ end
114
+
115
+ # sugers
116
+ # ----------------
117
+ def sort_desc(*args)
118
+ api = self.class.clone(self)
119
+ columns = args.collect{ |col|"#{col}:desc" }
120
+ api.set_param(:sort, columns.join(','))
121
+
122
+ return api
123
+ end
124
+
125
+ def page(page_num, paging_opts = {})
126
+ limit = (paging_opts[:per] || paging_opts["per"]).to_i
127
+ limit = DEFAULT_LIMIT if limit < 1
128
+
129
+ page_num = page_num.to_i
130
+ page_num = 1 if page_num < 1
131
+ offset = (page_num - 1) * limit
132
+
133
+ api = self.class.clone(self)
134
+ api.set_param(:limit, limit)
135
+ api.set_param(:offset, offset)
136
+
137
+ return api
138
+ end
139
+
140
+ # actions
141
+ # ----------------
142
+ def crosswalk(factual_id)
143
+ api = self
144
+
145
+ api.path = "places/crosswalk"
146
+ api.action = :crosswalk
147
+ api.params = { :factual_id => factual_id }
148
+
149
+ return api
150
+ end
151
+
152
+ def resolve(values)
153
+ api = self
154
+
155
+ api.action = :resolve
156
+ api.path = "places/resolve"
157
+ api.params = { :values => values }
158
+
159
+ return api
160
+ end
161
+
162
+ def table(table_id_or_alias)
163
+ api = self
164
+ if @response
165
+ api = self.class.clone(self)
166
+ end
167
+
168
+ api.path = "t/#{table_id_or_alias}"
169
+ api.action = :read
170
+
171
+ return api
172
+ end
173
+
174
+ private
175
+
176
+ # real requesting
177
+ # ----------------
178
+ def response
179
+ @response ||= {}
180
+ return @response[@action] if @response[@action]
181
+
182
+ # always include count for reads
183
+ @params[:include_count] = true unless @action == :schema
184
+
185
+ res = request()
186
+
187
+ code = res.code
188
+ json = res.body
189
+ payload = JSON.parse(json)
190
+
191
+ if payload["status"] == "ok"
192
+ @response[@action] = payload["response"]
193
+ else
194
+ raise StandardError.new(payload["message"])
195
+ end
196
+
197
+ return @response[@action]
198
+ end
199
+
200
+ def request
201
+ url = "#{API_V3_HOST}/#{@path}?#{query_string}"
202
+ headers = {"X-Factual-Lib" => DRIVER_VERSION_TAG}
203
+
204
+ return @access_token.get(url, headers)
205
+ end
206
+
207
+ def query_string()
208
+ arr = []
209
+ @params.each do |param, v|
210
+ unless (VALID_PARAMS[@action] + VALID_PARAMS[:any]).include?(param)
211
+ raise StandardError.new("InvalidArgument #{param} for #{@action}")
212
+ end
213
+ param_alias = PARAM_ALIASES[param.to_sym] || param.to_sym
214
+
215
+ v = v.to_json if v.class == Hash
216
+ arr << "#{param_alias}=#{URI.escape(v.to_s)}"
217
+ end
218
+ return arr.join("&")
219
+ end
220
+
221
+ end
222
+
223
+ # response classes
224
+ # ----------------
225
+ class Row < OpenStruct; end
226
+ class Facet < OpenStruct; end
227
+ class Table < OpenStruct; end
228
+ class Field < OpenStruct; end
229
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: factual-api
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: "0.1"
6
+ platform: ruby
7
+ authors:
8
+ - Forrest Cao
9
+ - Aaron Crow
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2012-01-12 00:00:00 +08:00
15
+ default_executable:
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: rspec
19
+ prerelease: false
20
+ requirement: &id001 !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: "0"
26
+ type: :development
27
+ version_requirements: *id001
28
+ description: Factual's official Ruby driver for the Factual public API.
29
+ email:
30
+ - aaron@factual.com
31
+ executables: []
32
+
33
+ extensions: []
34
+
35
+ extra_rdoc_files: []
36
+
37
+ files:
38
+ - lib/factual_api.rb
39
+ - README.md
40
+ - CHANGELOG.md
41
+ has_rdoc: true
42
+ homepage: http://github.com/Factual/factual-ruby-driver
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options: []
47
+
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 1.3.6
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.6.2
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Ruby driver for Factual
69
+ test_files: []
70
+