ruby-factual 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/lib/factual.rb +153 -40
  2. data/test/unit/adapter.rb +53 -0
  3. data/test/unit/table.rb +51 -0
  4. metadata +11 -24
data/lib/factual.rb CHANGED
@@ -1,26 +1,55 @@
1
- require 'curl'
1
+ # A Ruby Lib for using Facutal API
2
+ #
3
+ # For more information, visit http://github.com/factual/ruby-lib (TODO),
4
+ # and {Factual Developer Tools}[http://www.factual.com/devtools]
5
+ #
6
+ # Author:: Forrest Cao (mailto:forrest@factual.com)
7
+ # Copyright:: Copyright (c) 2010 {Factual Inc}[http://www.factual.com].
8
+ # License:: GPL
9
+
10
+ require 'net/http'
2
11
  require 'json'
3
12
  require 'uri'
4
13
 
5
14
  module Factual
15
+ # The start point of using Factual API
6
16
  class Api
17
+
18
+ # To initialize a Factual::Api, you will have to get an api_key from {Factual Developer Tools}[http://www.factual.com/developers/api_key]
19
+ #
20
+ # Params: opts as a hash
21
+ # * <tt>opts[:api_key]</tt> required
22
+ # * <tt>opts[:debug]</tt> optional, default is false. If you set it as true, it will print the Factual Api Call URLs on the screen
23
+ # * <tt>opts[:version]</tt> optional, default value is 2, just do not change it
24
+ # * <tt>opts[:domain]</tt> optional, default value is www.factual.com (only configurable by Factual employees)
25
+ #
26
+ # Sample:
27
+ # api = Factual::Api.new(:api_key => MY_API_KEY, :debug => true)
7
28
  def initialize(opts)
8
29
  @api_key = opts[:api_key]
9
- @version = opts[:version]
30
+ @version = opts[:version] || 2
10
31
  @domain = opts[:domain] || 'www.factual.com'
11
- @adapter = Adapter.new(@api_key, @version, @domain)
32
+ @debug = opts[:debug]
33
+
34
+ @adapter = Adapter.new(@api_key, @version, @domain, @debug)
12
35
  end
13
36
 
37
+ # Get a Factual::Table object by inputting the table_key
38
+ #
39
+ # Sample:
40
+ # api.get_table('g9R1u2')
14
41
  def get_table(table_key)
15
42
  Table.new(table_key, @adapter)
16
43
  end
17
44
  end
18
45
 
46
+ # This class holds the metadata of a Factual table. The filter and sort methods are to filter and/or sort
47
+ # the table data before calling a find_one or each_row.
19
48
  class Table
20
- attr_accessor :name, :description, :rating, :source, :creator, :total_row_count, :created_at, :updated_at, :fields, :geo_enabled, :downloadable
21
- attr_accessor :key, :adapter
49
+ attr_accessor :name, :key, :description, :rating, :source, :creator, :total_row_count, :created_at, :updated_at, :fields, :geo_enabled, :downloadable
50
+ attr_reader :adapter # :nodoc:
22
51
 
23
- def initialize(table_key, adapter)
52
+ def initialize(table_key, adapter) # :nodoc:
24
53
  @table_key = table_key
25
54
  @adapter = adapter
26
55
  @schema = adapter.schema(@table_key)
@@ -37,54 +66,92 @@ module Factual
37
66
  end
38
67
  end
39
68
 
69
+ # Define table filters, it can be chained before +find_one+ or +each_row+.
70
+ #
71
+ # The params can be:
72
+ # * simple hash for equal filter
73
+ # * nested hash with filter operators
74
+ #
75
+ # Samples:
76
+ # table.filter(:state => 'CA').find_one # hash
77
+ # table.filter(:state => 'CA', :city => 'LA').find_one # multi-key hash
78
+ # table.filter(:state => {"$has" => 'A'}).find_one # nested hash
79
+ # table.filter(:state => {"$has" => 'A'}, :city => {"$ew" => 'A'}).find_one # multi-key nested hashes
80
+ #
81
+ # For more detail inforamtion about filter syntax, please look up at {Server API Doc for Filter}[http://wiki.developer.factual.com/Filter]
40
82
  def filter(filters)
41
83
  @filters = filters
42
84
  return self
43
85
  end
44
86
 
45
- def sort(sorts)
87
+ # Define table sorts, it can be chained before +find_one+ or +each_row+.
88
+ #
89
+ # The params can be:
90
+ # * a hash with single key
91
+ # * single-key hashes, only the first 2 sorts will work. (secondary sort will be supported in next release of Factual API)
92
+ #
93
+ # Samples:
94
+ # table.sort(:state => 1).find_one # hash with single key
95
+ # table.sort({:state => 1}, {:abbr => -1}).find_one # single-key hashes
96
+ # For more detail inforamtion about sort syntax, please look up at {Server API Doc for Sort (TODO)}[http://wiki.developer.factual.com/Sort]
97
+ def sort(*sorts)
46
98
  @sorts = sorts
47
99
  return self
48
100
  end
49
101
 
50
- def each_row
51
- filters_query = "&filters=" + @filters.to_json if @filters
52
- if @sorts
53
- sorts_by = "sort_by=" + @sorts.keys.collect{|k| get_field_id(k).to_s}.join(",")
54
- sorts_dir = "sort_dir=" + @sorts.values.collect{|v| (v==1) ? 'asc' : 'desc' }.join(",")
55
- sorts_query = "&" + sorts_by + "&" + sorts_dir
102
+ # Find the first row (a Factual::Row object) of the table with filters and/or sorts.
103
+ #
104
+ # Samples:
105
+ # * <tt>table.filter(:state => 'CA').find_one</tt>
106
+ # * <tt>table.filter(:state => 'CA').sort(:city => 1).find_one</tt>
107
+ def find_one
108
+ resp = @adapter.read_table(@table_key, @filters, @sorts, 1)
109
+ row_data = resp["data"].first
110
+
111
+ if row_data
112
+ return Row.new(self, row_data)
113
+ else
114
+ return nil
56
115
  end
116
+ end
57
117
 
58
- resp = @adapter.api_call("/tables/#{@table_key}/read.jsaml?limit=999" + filters_query.to_s + sorts_query.to_s)
118
+ # An iterator on each row (a Factual::Row object) of the filtered and/or sorted table data
119
+ #
120
+ # Samples:
121
+ # table.filter(:state => 'CA').sort(:city => 1).each do |row|
122
+ # puts row.inspect
123
+ # end
124
+ def each_row
125
+ resp = @adapter.read_table(@table_key, @filters, @sorts)
59
126
 
60
- @total_rows = resp["response"]["total_rows"]
61
- rows = resp["response"]["data"]
127
+ @total_rows = resp["total_rows"]
128
+ rows = resp["data"]
62
129
 
63
- # TODO iterator
64
130
  rows.each do |row_data|
65
131
  row = Row.new(self, row_data)
66
132
  yield(row) if block_given?
67
133
  end
68
134
  end
69
135
 
70
- private
71
-
72
- def get_field_id(field_ref)
73
- @fields.each do |f|
74
- return f['id'] if f['field_ref'] == field_ref.to_s
75
- end
136
+ # TODO
137
+ def add_row(values)
76
138
  end
77
139
 
140
+ private
141
+
78
142
  def camelize(str)
79
143
  s = str.to_s.split("_").collect{ |w| w.capitalize }.join
80
144
  s[0].chr.downcase + s[1..-1]
81
145
  end
82
146
  end
83
147
 
148
+ # This class holds the subject_key, subject (in array) and facts (Factual::Fact objects) of a Factual Subject.
149
+ #
150
+ # The subject_key and subject array can be accessable directly from attributes, and you can get a fact by <tt>row[field_ref]</tt>.
84
151
  class Row
85
- attr_accessor :subject_key, :subject
152
+ attr_reader :subject_key, :subject
86
153
 
87
- def initialize(table, row_data)
154
+ def initialize(table, row_data) # :nodoc:
88
155
  @subject_key = row_data[0]
89
156
 
90
157
  @table = table
@@ -105,6 +172,11 @@ module Factual
105
172
  end
106
173
  end
107
174
 
175
+ # Get a Factual::Fact object by field_ref
176
+ #
177
+ # Sample:
178
+ # city_info = table.filter(:state => 'CA').find_one
179
+ # city_info['city_name']
108
180
  def [](field_ref)
109
181
  @facts_hash[field_ref]
110
182
  end
@@ -115,10 +187,11 @@ module Factual
115
187
  end
116
188
  end
117
189
 
190
+ # This class holds the subject_key, value, field_ref field (field metadata in hash). The input method is for suggesting a new value for the fact.
118
191
  class Fact
119
- attr_accessor :value, :subject_key, :field, :adapter
192
+ attr_reader :value, :subject_key, :field_ref, :field
120
193
 
121
- def initialize(table, subject_key, field, value)
194
+ def initialize(table, subject_key, field, value) # :nodoc:
122
195
  @value = value
123
196
  @field = field
124
197
  @subject_key = subject_key
@@ -127,64 +200,104 @@ module Factual
127
200
  @adapter = table.adapter
128
201
  end
129
202
 
130
- def field_ref
203
+ def field_ref # :nodoc:
131
204
  @field["field_ref"]
132
205
  end
133
206
 
207
+ # To input a new value to the fact
208
+ #
209
+ # Parameters:
210
+ # * +value+
211
+ # * <tt>opts[:source]</tt> the source of an input, can be a URL or something else
212
+ # * <tt>opts[:comment]</tt> the comment of an input
213
+ #
214
+ # Sample:
215
+ # fact.input('new value', :source => 'http://website.com', :comment => 'because it is new value.'
134
216
  def input(value, opts={})
135
217
  return false if value.nil?
136
218
 
137
219
  hash = opts.merge({
138
220
  :subjectKey => @subject_key,
139
- :fieldId => @field['id'],
140
- :value => value
221
+ :fieldId => @field['id'],
222
+ :value => value
141
223
  })
142
- query_string = hash.to_a.collect{ |k,v| URI.escape(k.to_s) + '=' + URI.escape(v.to_s) }.join('&')
143
224
 
144
- @adapter.api_call("/tables/#{@table_key}/input.js?" + query_string)
225
+ @adapter.input(@table_key, hash)
145
226
  return true
146
227
  end
147
228
 
229
+ # Just return the value
148
230
  def to_s
149
231
  @value
150
232
  end
151
233
 
234
+ # Just return the value
152
235
  def inspect
153
236
  @value
154
237
  end
155
238
  end
156
239
 
157
240
 
158
- class Adapter
241
+ class Adapter # :nodoc:
159
242
  CONNECT_TIMEOUT = 30
160
243
 
161
- def initialize(api_key, version, domain)
162
- @base = "http://#{domain}/api/v#{version}/#{api_key}"
244
+ def initialize(api_key, version, domain, debug=false)
245
+ @domain = domain
246
+ @base = "/api/v#{version}/#{api_key}"
247
+ @debug = debug
163
248
  end
164
249
 
165
250
  def api_call(url)
166
251
  api_url = @base + url
252
+ puts "[Factual API Call] http://#{@domain}#{api_url}" if @debug
167
253
 
254
+ json = "{}"
168
255
  begin
169
- curl = Curl::Easy.new(api_url) do |c|
170
- c.connect_timeout = CONNECT_TIMEOUT
256
+ Net::HTTP.start(@domain, 80) do |http|
257
+ response = http.get(api_url)
258
+ json = response.body
171
259
  end
172
- curl.http_get
173
260
  rescue Exception => e
174
261
  raise ApiError.new(e.to_s + " when getting " + api_url)
175
262
  end
176
263
 
177
- resp = JSON.parse(curl.body_str)
264
+ resp = JSON.parse(json)
178
265
  raise ApiError.new(resp["error"]) if resp["status"] == "error"
179
266
  return resp
180
267
  end
181
268
 
182
269
  def schema(table_key)
183
- resp = api_call("/tables/#{table_key}/schema.json")
270
+ url = "/tables/#{table_key}/schema.json"
271
+ resp = api_call(url)
272
+
184
273
  return resp["schema"]
185
274
  end
275
+
276
+ def read_table(table_key, filters=nil, sorts=nil, limit=999)
277
+ filters_query = "&filters=" + filters.to_json if filters
278
+
279
+ if sorts
280
+ sorts = sorts[0] if sorts.length == 1
281
+ sorts_query = "&sort=" + sorts.to_json
282
+ end
283
+
284
+ url = "/tables/#{table_key}/read.jsaml?limit=#{limit}" + filters_query.to_s + sorts_query.to_s
285
+ resp = api_call(url)
286
+
287
+ return resp["response"]
288
+ end
289
+
290
+ def input(table_key, params)
291
+ query_string = params.to_a.collect{ |k,v| URI.escape(k.to_s) + '=' + URI.escape(v.to_s) }.join('&')
292
+
293
+ url = "/tables/#{table_key}/input.js?" + query_string
294
+ resp = api_call(url)
295
+
296
+ return resp['response']
297
+ end
186
298
  end
187
299
 
300
+ # Exception class for Factual Api Errors
188
301
  class ApiError < Exception
189
302
  end
190
303
  end
@@ -0,0 +1,53 @@
1
+ require 'test/unit/helper'
2
+ require 'lib/factual'
3
+
4
+ class AdapterTest < Factual::TestCase
5
+ def setup
6
+ @adapter = Factual::Adapter.new(API_KEY, API_VERSION, API_DOMAIN, DEBUG_MODE)
7
+ end
8
+
9
+ def test_corret_request
10
+ url = "/tables/#{TABLE_KEY}/schema.json"
11
+
12
+ assert_nothing_raised do
13
+ resp = @adapter.api_call(url)
14
+ end
15
+ end
16
+
17
+ def test_wrong_request
18
+ url = "/tables/#{WRONG_KEY}/schema.json"
19
+
20
+ assert_raise Factual::ApiError do
21
+ resp = @adapter.api_call(url)
22
+ end
23
+ end
24
+
25
+ def test_getting_schema
26
+ schema = @adapter.schema(TABLE_KEY)
27
+
28
+ assert_not_nil schema
29
+ assert_equal schema['name'], TABLE_NAME
30
+ end
31
+
32
+ def test_reading_table
33
+ resp = @adapter.read_table(TABLE_KEY)
34
+ assert_equal resp['total_rows'], TOTAL_ROWS
35
+ end
36
+
37
+ def test_reading_table_with_filter
38
+ resp = @adapter.read_table(TABLE_KEY, {:two_letter_abbrev => 'CA'})
39
+ assert_equal resp['total_rows'], 1
40
+ end
41
+
42
+ def test_inputting
43
+ params = {
44
+ :subjectKey => SUBJECT_KEY,
45
+ :value => 'sample text',
46
+ :fieldId => STATE_FIELD_ID
47
+ }
48
+
49
+ assert_raise Factual::ApiError do
50
+ @adapter.input(TABLE_KEY, params)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ require 'lib/factual'
2
+ require 'test/unit/helper'
3
+
4
+ class TableTest < Factual::TestCase
5
+ def setup
6
+ api = Factual::Api.new(:api_key => API_KEY, :debug => DEBUG_MODE)
7
+
8
+ @table = api.get_table(TABLE_KEY)
9
+ end
10
+
11
+ def test_metadata
12
+ assert_equal @table.name, TABLE_NAME
13
+ assert_equal @table.creator, TABLE_OWNER
14
+ end
15
+
16
+ def test_each_row
17
+ states = []
18
+ @table.each_row do |state_info|
19
+ fact = state_info['state']
20
+ states << fact.value
21
+ end
22
+
23
+ assert_equal states.length, TOTAL_ROWS
24
+ end
25
+
26
+ def test_filtering
27
+ row = @table.filter(:two_letter_abbrev => 'WA').find_one
28
+ assert_equal row["state"].value, "Washington"
29
+
30
+ row = @table.filter(:two_letter_abbrev => { '$has' => 'a' }).sort(:state => 1).find_one
31
+ assert_equal row["state"].value, "California"
32
+ end
33
+
34
+ def test_sorting
35
+ row = @table.sort(:state => 1).find_one
36
+ assert_equal row["state"].value, "California"
37
+
38
+ assert_raise Factual::ApiError do
39
+ # secondary sort will be supported in next release
40
+ row = @table.sort({:state => 1}, {:test_field1 => 1}).find_one
41
+ row["state"].value
42
+ end
43
+ end
44
+
45
+ def test_row
46
+ row = @table.find_one
47
+ end
48
+
49
+ def test_fact
50
+ end
51
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-factual
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 5
10
- version: 0.0.5
9
+ - 6
10
+ version: 0.0.6
11
11
  platform: ruby
12
12
  authors:
13
13
  - Forrest Cao
@@ -15,29 +15,13 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-09-14 00:00:00 +08:00
18
+ date: 2010-10-20 00:00:00 +08:00
19
19
  default_executable:
20
20
  dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: curb
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- hash: 27
30
- segments:
31
- - 0
32
- - 3
33
- - 4
34
- version: 0.3.4
35
- type: :runtime
36
- version_requirements: *id001
37
21
  - !ruby/object:Gem::Dependency
38
22
  name: json
39
23
  prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ requirement: &id001 !ruby/object:Gem::Requirement
41
25
  none: false
42
26
  requirements:
43
27
  - - ">="
@@ -49,7 +33,7 @@ dependencies:
49
33
  - 0
50
34
  version: 1.2.0
51
35
  type: :runtime
52
- version_requirements: *id002
36
+ version_requirements: *id001
53
37
  description: ""
54
38
  email: forrest@factual.com
55
39
  executables: []
@@ -61,6 +45,8 @@ extra_rdoc_files:
61
45
  files:
62
46
  - README.md
63
47
  - lib/factual.rb
48
+ - test/unit/adapter.rb
49
+ - test/unit/table.rb
64
50
  has_rdoc: true
65
51
  homepage: http://github.com/forrestc/ruby-factual
66
52
  licenses: []
@@ -101,5 +87,6 @@ rubygems_version: 1.3.7
101
87
  signing_key:
102
88
  specification_version: 3
103
89
  summary: ""
104
- test_files: []
105
-
90
+ test_files:
91
+ - test/unit/adapter.rb
92
+ - test/unit/table.rb