ruby-factual 0.0.5 → 0.0.6

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/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