factual-api 0.1

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.
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
+