airrecord 0.2.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1224190a9be6b7349af9ab26640516b13961d88bc3b42d188c5879777e90fe04
4
- data.tar.gz: 119beef65efdc6d110cc4e5c1865e92308c3e2a043b0e2e853484480488307d2
3
+ metadata.gz: 52a1efc847fab74c1bd0ff89814a274b1eb789359efda52a71675b6e7602f362
4
+ data.tar.gz: cb766b78e31e88ed253c2f7d7f2f7a1166fb5fec61bdbf46602b20b76a43cf9c
5
5
  SHA512:
6
- metadata.gz: 577ae326c6cb761f7557c9b75a6a3e1fc46373aba67b6300430d9d0efac54e21eea30885237d40c233ef1c33f2d4bd3766984f0697211ebbff8bd6d7af160bcf
7
- data.tar.gz: 11c1ce89e39a1f0e66f8c3b34cbf53eb21f69a2e91f98ae5460af85f1df2b62e37a1c01940263da8dba7c0499e822f8a3280ef55345cc93f76401c2d36579252
6
+ metadata.gz: ee40920b905e08644f8de12241b3bd842bac654b396c71e68677d8eec53b6f67f2fec17841754c9841e61542ac9f5de83c420141efc198cc2309d7d4fbbf0629
7
+ data.tar.gz: 64de96b6d7b81a8b9e147aa6adbf81402a5a6c1c5e23b681807df3cd408e420dfe065beb15135ad63706bf5899539cd979792afac9c8971aa21281fb55cce9d5
@@ -1,6 +1,22 @@
1
- # 1.0.0 (unreleased)
2
-
3
- * 1.0.0 will introduce breaking changes, including removing support for symbols. To update, change snake-case symbols to their correct column names (for example, `record["First Name"]` instead of `record[:first_name]`)
1
+ # 1.0.0
2
+
3
+ * 1.0.0 introduces breaking changes, including removing support for symbols and
4
+ implementing associations as instance methods. To upgrade:
5
+ 1. Change snake-case symbols to their correct column names:
6
+ `record["First Name"]` instead of `record[:first_name]`)
7
+ 2. Change your association calls to use instance methods instead of `[]`:
8
+ ```ruby
9
+ class Tea < Airrecord::Table
10
+ has_many :brews, class: "Brew", column: "Brews"
11
+ end
12
+ tea[:brews] #=> Error, no longer supported
13
+ tea.brews #=> [<Brew>, <Brew>] returns associated Brew instances
14
+ tea["Brews"] #=> ["rec456", "rec789"] returns a raw Airtable field
15
+ ```
16
+ 3. Dates that are formed `\d{4}-\d{2}-\d{2}` are no longer auto-parsed. Define a helper instead.
17
+ * Automatically throttle client calls to Airtable's API limit of 5 requests per second.
18
+ * Fix sorting by multiple fields
19
+ * Report `User-Agent` as `Airrecord`.
4
20
 
5
21
  # 0.2.5
6
22
 
data/README.md CHANGED
@@ -20,7 +20,7 @@ class Tea < Airrecord::Table
20
20
  self.base_key = "app1"
21
21
  self.table_name = "Teas"
22
22
 
23
- has_many "Brews", class: 'Brew', column: "Brews"
23
+ has_many :brews, class: "Brew", column: "Brews"
24
24
 
25
25
  def self.chinese
26
26
  all(filter: '{Country} = "China"')
@@ -43,14 +43,14 @@ class Brew < Airrecord::Table
43
43
  self.base_key = "app1"
44
44
  self.table_name = "Brews"
45
45
 
46
- belongs_to "Tea", class: 'Tea', column: 'Tea'
46
+ belongs_to :tea, class: "Tea", column: "Tea"
47
47
 
48
48
  def self.hot
49
49
  all(filter: "{Temperature} > 90")
50
50
  end
51
51
 
52
52
  def done_brewing?
53
- self["Created At"] + self["Duration"] > Time.now
53
+ Time.parse(self["Created At"]) + self["Duration"] > Time.now
54
54
  end
55
55
  end
56
56
 
@@ -58,7 +58,7 @@ teas = Tea.all
58
58
  tea = teas.first
59
59
  tea["Country"] # access atribute
60
60
  tea.location # instance methods
61
- tea["Brews"] # associated brews
61
+ tea.brews # associated brews
62
62
  ```
63
63
 
64
64
  A short-hand API for definitions and more ad-hoc querying is also available:
@@ -115,10 +115,17 @@ end
115
115
  This gives us a class that maps to records in a table. Class methods are
116
116
  available to fetch records on the table.
117
117
 
118
+ ### Reading a Single Record
119
+
120
+ Retrieve a single record via `#find`:
121
+ ```ruby
122
+ tea = Tea.find("someid")
123
+ ```
124
+
118
125
  ### Listing Records
119
126
 
120
- Retrieval of multiple records is done through `#all`. To get all records in a
121
- table:
127
+ Retrieval of multiple records is usually done through `#all`. To get all records
128
+ in a table:
122
129
 
123
130
  ```ruby
124
131
  Tea.all # array of Tea instances
@@ -183,17 +190,33 @@ Tea.all(paginate: false)
183
190
  Tea.all(sort: { "Created At" => "desc" }, paginate: false)
184
191
  ```
185
192
 
193
+ When you know the IDs of the records you want, and you want them in an ad-hoc
194
+ order, use `#find_many` instead of `#all`:
195
+
196
+ ```ruby
197
+ teas = Tea.find_many(["someid", "anotherid", "yetanotherid"])
198
+ #=> [<Tea @id="someid">,<Tea @id="anotherid">, <Tea @id="yetanotherid">]
199
+ ```
200
+
186
201
  ### Creating
187
202
 
188
- Creating a new record is done through `#create`.
203
+ Creating a new record is done through `Table.create`.
189
204
 
190
205
  ```ruby
191
- tea = Tea.new("Name" => "Feng Gang", "Type" => "Green", "Country" => "China")
192
- tea.create # creates the record
206
+ tea = Tea.create("Name" => "Feng Gang", "Type" => "Green", "Country" => "China")
193
207
  tea.id # id of the new record
194
208
  tea["Name"] # "Feng Gang"
195
209
  ```
196
210
 
211
+ If you need to manipulate a record before saving it, you can use `Table.new`
212
+ instead of `create`, then call `#save` when you're ready.
213
+
214
+ ```ruby
215
+ tea = Tea.new("Type" => "Green", "Country" => "China")
216
+ tea["Name"] = "Feng Gang"
217
+ tea.save
218
+ ```
219
+
197
220
  Note that column names need to match the exact column names in Airtable,
198
221
  otherwise Airrecord will throw an error that no column matches it.
199
222
 
@@ -268,14 +291,22 @@ class Tea < Airrecord::Table
268
291
  self.base_key = "app1"
269
292
  self.table_name = "Teas"
270
293
 
271
- has_many "Brews", class: 'Brew', column: "Brews"
294
+ has_many :brews, class: "Brew", column: "Brews"
295
+ has_one :teapot, class: "Teapot", column: "Teapot"
272
296
  end
273
297
 
274
298
  class Brew < Airrecord::Table
275
299
  self.base_key = "app1"
276
300
  self.table_name = "Brews"
277
301
 
278
- belongs_to "Tea", class: 'Tea', column: 'Tea'
302
+ belongs_to :tea, class: "Tea", column: "Tea"
303
+ end
304
+
305
+ class Teapot < Airrecord::Table
306
+ self.base_key = "app1"
307
+ self.table_name = "Teapot"
308
+
309
+ belongs_to :tea, class: "Tea", column: "Tea"
279
310
  end
280
311
  ```
281
312
 
@@ -289,15 +320,23 @@ _not_ support associations across Bases.
289
320
  To retrieve records from associations to a record:
290
321
 
291
322
  ```ruby
292
- tea = Tea.find('rec84')
293
- tea["Brews"] # brews associated with tea
323
+ tea = Tea.find("rec123")
324
+
325
+ # record.association returns Airrecord instances
326
+ tea.brews #=> [<Brew @id="rec456">, <Brew @id="rec789">]
327
+ tea.teapot #=> <Teapot @id="rec012">
328
+
329
+ # record["Associated Column"] returns the raw Airtable field, an array of IDs
330
+ tea["Brews"] #=> ["rec789", "rec456"]
331
+ tea["Teapot"] #=> ["rec012"]
294
332
  ```
295
333
 
296
334
  This in turn works the other way too:
297
335
 
298
336
  ```ruby
299
- brew = Brew.find('rec849')
300
- brew["Tea"] # the associated tea instance
337
+ brew = Brew.find("rec456")
338
+ brew.tea #=> <Tea @id="rec123"> the associated tea instance
339
+ brew["Tea"] #=> the raw Airtable field, a single-item array ["rec123"]
301
340
  ```
302
341
 
303
342
  ### Creating associated records
@@ -305,9 +344,18 @@ brew["Tea"] # the associated tea instance
305
344
  You can easily associate records with each other:
306
345
 
307
346
  ```ruby
308
- tea = Tea.find('rec849829')
347
+ tea = Tea.find("rec123")
309
348
  # This will create a brew associated with the specific tea
310
- brew = Brew.new("Tea" => tea, "Temperature" => "80", "Time" => "4m", "Rating" => "5")
349
+ brew = Brew.new("Temperature" => "80", "Time" => "4m", "Rating" => "5")
350
+ brew.tea = tea
351
+ brew.create
352
+ ```
353
+
354
+ Alternatively, you can specify association ids directly:
355
+
356
+ ```ruby
357
+ tea = Tea.find("rec123")
358
+ brew = Brew.new("Tea" => [tea.id], "Temperature" => "80", "Time" => "4m", "Rating" => "5")
311
359
  brew.create
312
360
  ```
313
361
 
@@ -328,6 +376,15 @@ end
328
376
  Tea.find("rec3838")
329
377
  ```
330
378
 
379
+ ### Throttling
380
+
381
+ Airtable's API enforces a 5 requests per second limit per client. In most cases,
382
+ you won't reach this limit in a single thread but it's possible for some fast
383
+ calls. Airrecord automatically enforces this limit. It doesn't use a naive
384
+ throttling algorithm that does a `sleep(0.2)` between each call, but rather
385
+ keeps a sliding window of when each call was made and will sleep at the end of
386
+ the window if requires. This means that bursting is supported.
387
+
331
388
  ### Production Middlewares
332
389
 
333
390
  For production use-cases, it's worth considering adding retries and circuit
@@ -9,4 +9,10 @@ module Airrecord
9
9
  extend self
10
10
  Error = Class.new(StandardError)
11
11
  attr_accessor :api_key
12
+ attr_accessor :throttle
13
+
14
+ def throttle?
15
+ return true if @throttle.nil?
16
+ @throttle
17
+ end
12
18
  end
@@ -1,25 +1,39 @@
1
1
  require 'uri'
2
+ require_relative 'query_string'
3
+ require_relative 'faraday_rate_limiter'
2
4
 
3
5
  module Airrecord
4
6
  class Client
5
7
  attr_reader :api_key
6
8
  attr_writer :connection
7
9
 
10
+ # Per Airtable's documentation you will get throttled for 30 seconds if you
11
+ # issue more than 5 requests per second. Airrecord is a good citizen.
12
+ AIRTABLE_RPS_LIMIT = 5
13
+
8
14
  def initialize(api_key)
9
15
  @api_key = api_key
10
16
  end
11
17
 
12
18
  def connection
13
- @connection ||= Faraday.new(url: "https://api.airtable.com", headers: {
14
- "Authorization" => "Bearer #{api_key}",
15
- "X-API-VERSION" => "0.1.0",
16
- }) { |conn|
19
+ @connection ||= Faraday.new(
20
+ url: "https://api.airtable.com",
21
+ headers: {
22
+ "Authorization" => "Bearer #{api_key}",
23
+ "User-Agent" => "Airrecord/#{Airrecord::VERSION}",
24
+ "X-API-VERSION" => "0.1.0",
25
+ },
26
+ request: { params_encoder: Airrecord::QueryString },
27
+ ) { |conn|
28
+ if Airrecord.throttle?
29
+ conn.request :airrecord_rate_limiter, requests_per_second: AIRTABLE_RPS_LIMIT
30
+ end
17
31
  conn.adapter :net_http_persistent
18
32
  }
19
33
  end
20
34
 
21
35
  def escape(*args)
22
- URI.escape(*args)
36
+ QueryString.escape(*args)
23
37
  end
24
38
 
25
39
  def parse(body)
@@ -0,0 +1,60 @@
1
+ require 'thread'
2
+
3
+ module Airrecord
4
+ class FaradayRateLimiter < Faraday::Middleware
5
+ class << self
6
+ attr_accessor :requests
7
+ end
8
+
9
+ def initialize(app, requests_per_second: nil, sleeper: nil)
10
+ super(app)
11
+ @rps = requests_per_second
12
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
13
+ @mutex = Mutex.new
14
+ clear
15
+ end
16
+
17
+ def call(env)
18
+ @mutex.synchronize do
19
+ wait if too_many_requests_in_last_second?
20
+ @app.call(env).on_complete do |_response_env|
21
+ requests << Process.clock_gettime(Process::CLOCK_MONOTONIC)
22
+ requests.shift if requests.size > @rps
23
+ end
24
+ end
25
+ end
26
+
27
+ def clear
28
+ self.class.requests = []
29
+ end
30
+
31
+ private
32
+
33
+ def requests
34
+ self.class.requests
35
+ end
36
+
37
+ def too_many_requests_in_last_second?
38
+ return false unless @rps
39
+ return false unless requests.size >= @rps
40
+ window_span < 1.0
41
+ end
42
+
43
+ def wait
44
+ # Time to wait until making the next request to stay within limits.
45
+ # [1.1, 1.2, 1.3, 1.4, 1.5] => 1 - 0.4 => 0.6
46
+ wait_time = 1.0 - window_span
47
+ @sleeper.call(wait_time)
48
+ end
49
+
50
+ # [1.1, 1.2, 1.3, 1.4, 1.5] => 1.5 - 1.1 => 0.4
51
+ def window_span
52
+ requests.last - requests.first
53
+ end
54
+ end
55
+ end
56
+
57
+ Faraday::Request.register_middleware(
58
+ # Avoid polluting the global middleware namespace with a prefix.
59
+ :airrecord_rate_limiter => Airrecord::FaradayRateLimiter
60
+ )
@@ -0,0 +1,47 @@
1
+ require 'erb'
2
+
3
+ module Airrecord
4
+ # Airtable expects that arrays in query strings be encoded with indices.
5
+ # Faraday follows Rack conventions and encodes arrays _without_ indices.
6
+ #
7
+ # Airrecord::QueryString is a Faraday-compliant params_encoder that follows
8
+ # the Airtable spec.
9
+ module QueryString
10
+ def self.encode(params)
11
+ params.map { |key, val| Encodings[val].call(key, val) }.join('&')
12
+ end
13
+
14
+ def self.decode(query)
15
+ Faraday::NestedParamsEncoder.decode(query)
16
+ end
17
+
18
+ def self.escape(*query)
19
+ query.map { |qs| ERB::Util.url_encode(qs) }.join('')
20
+ end
21
+
22
+ module Encodings
23
+ using QueryString
24
+
25
+ def self.[](value)
26
+ TYPES.fetch(value.class, DEFAULT)
27
+ end
28
+
29
+ TYPES = {
30
+ Array => lambda { |prefix, array|
31
+ array.each_with_index.map do |value, index|
32
+ self[value].call("#{prefix}[#{index}]", value)
33
+ end
34
+ },
35
+ Hash => lambda { |prefix, hash|
36
+ hash.map do |key, value|
37
+ self[value].call("#{prefix}[#{key}]", value)
38
+ end
39
+ },
40
+ }.freeze
41
+
42
+ DEFAULT = lambda do |key, value|
43
+ "#{QueryString.escape(key)}=#{QueryString.escape(value)}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,47 +1,33 @@
1
1
  module Airrecord
2
- # TODO: This would be much simplified if we had a schema instead. Hopefully
3
- # one day Airtable will add this, but to simplify and crush the majority of
4
- # the bugs that hide in here (which would be related to the dynamic schema) we
5
- # may just query the first page and infer a schema from there that can be
6
- # overridden on the specific classes.
7
- #
8
- # Right now I bet there's a bunch of bugs around similar named column keys (in
9
- # terms of capitalization), it's inconsistent and non-obvious that `create`
10
- # doesn't use the same column keys as everything else.
11
- #
12
- # 2018-11-01
13
- # deprecate_symbols: long-term plan is to force everyone to use raw strings,
14
- # to match the Airtable behavior. For now we'll just warn when using symbols
15
- # with a deprecation notice.
16
-
17
2
  class Table
18
- def deprecate_symbols
19
- self.class.deprecate_symbols
20
- end
21
-
22
3
  class << self
23
4
  attr_accessor :base_key, :table_name, :api_key, :associations
24
5
 
25
- def deprecate_symbols
26
- warn Kernel.caller.first + ": warning: Using symbols with airrecord is deprecated."
27
- end
28
-
29
6
  def client
30
7
  @@clients ||= {}
31
8
  @@clients[api_key] ||= Client.new(api_key)
32
9
  end
33
10
 
34
- def has_many(name, options)
35
- @associations ||= []
36
- @associations << {
37
- field: name.to_sym, # todo: deprecate_symbols
38
- }.merge(options)
11
+ def has_many(method_name, options)
12
+ define_method(method_name.to_sym) do
13
+ # Get association ids in reverse order, because Airtable’s UI and API
14
+ # sort associations in opposite directions. We want to match the UI.
15
+ ids = (self[options.fetch(:column)] || []).reverse
16
+ table = Kernel.const_get(options.fetch(:class))
17
+ options[:single] ? table.find(ids.first) : table.find_many(ids)
18
+ end
19
+
20
+ define_method("#{method_name}=".to_sym) do |value|
21
+ self[options.fetch(:column)] = Array(value).map(&:id).reverse
22
+ end
39
23
  end
40
24
 
41
- def belongs_to(name, options)
42
- has_many(name, options.merge(single: true))
25
+ def belongs_to(method_name, options)
26
+ has_many(method_name, options.merge(single: true))
43
27
  end
44
28
 
29
+ alias has_one belongs_to
30
+
45
31
  def api_key
46
32
  @api_key || Airrecord.api_key
47
33
  end
@@ -57,13 +43,22 @@ module Airrecord
57
43
  end
58
44
  end
59
45
 
46
+ def find_many(ids)
47
+ or_args = ids.map { |id| "RECORD_ID() = '#{id}'"}.join(',')
48
+ formula = "OR(#{or_args})"
49
+ records(filter: formula).sort_by { |record| or_args.index(record.id) }
50
+ end
51
+
52
+ def create(fields)
53
+ new(fields).tap { |record| record.save }
54
+ end
55
+
60
56
  def records(filter: nil, sort: nil, view: nil, offset: nil, paginate: true, fields: nil, max_records: nil, page_size: nil)
61
57
  options = {}
62
58
  options[:filterByFormula] = filter if filter
63
59
 
64
60
  if sort
65
61
  options[:sort] = sort.map { |field, direction|
66
- deprecate_symbols if field.is_a? Symbol
67
62
  { field: field.to_s, direction: direction }
68
63
  }
69
64
  end
@@ -105,7 +100,7 @@ module Airrecord
105
100
  alias_method :all, :records
106
101
  end
107
102
 
108
- attr_reader :fields, :column_mappings, :id, :created_at, :updated_keys
103
+ attr_reader :fields, :id, :created_at, :updated_keys
109
104
 
110
105
  def initialize(fields, id: nil, created_at: nil)
111
106
  @id = id
@@ -118,51 +113,22 @@ module Airrecord
118
113
  end
119
114
 
120
115
  def [](key)
121
- value = nil
122
-
123
- if fields[key]
124
- deprecate_symbols if key.is_a? Symbol
125
- value = fields[key]
126
- elsif column_mappings[key]
127
- deprecate_symbols if key.is_a? Symbol
128
- value = fields[column_mappings[key]]
129
- end
130
-
131
- if association = self.association(key)
132
- klass = Kernel.const_get(association[:class])
133
- associations = value.map { |id_or_obj|
134
- id_or_obj = id_or_obj.respond_to?(:id) ? id_or_obj.id : id_or_obj
135
- klass.find(id_or_obj)
136
- }
137
- return associations.first if association[:single]
138
- associations
139
- else
140
- type_cast(value)
141
- end
116
+ validate_key(key)
117
+ fields[key]
142
118
  end
143
119
 
144
120
  def []=(key, value)
145
- deprecate_symbols if key.is_a? Symbol
146
- if fields[key]
147
- return if fields[key] == value # no-op
148
- @updated_keys << key
149
- fields[key] = value
150
- elsif column_mappings[key]
151
- deprecate_symbols
152
- return if fields[column_mappings[key]] == value # no-op
153
- @updated_keys << column_mappings[key]
154
- fields[column_mappings[key]] = value
155
- else
156
- @updated_keys << key
157
- fields[key] = value
158
- end
121
+ validate_key(key)
122
+ return if fields[key] == value # no-op
123
+ @updated_keys << key
124
+ fields[key] = value
159
125
  end
160
126
 
161
127
  def create
162
128
  raise Error, "Record already exists (record has an id)" unless new_record?
163
129
 
164
130
  body = { fields: serializable_fields }.to_json
165
- response = client.connection.post("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}", body, { 'Content-Type': 'application/json' })
131
+ response = client.connection.post("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}", body, { 'Content-Type' => 'application/json' })
166
132
  parsed_response = client.parse(response.body)
167
133
 
168
134
  if response.success?
@@ -175,7 +141,7 @@ module Airrecord
175
141
  end
176
142
 
177
143
  def save
178
- raise Error, "Unable to save a new record" if new_record?
144
+ return create if new_record?
179
145
 
180
146
  return true if @updated_keys.empty?
181
147
 
@@ -186,11 +152,11 @@ module Airrecord
186
152
  }]
187
153
  }.to_json
188
154
 
189
- response = client.connection.patch("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}", body, { 'Content-Type': 'application/json' })
155
+ response = client.connection.patch("/v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}", body, { 'Content-Type' => 'application/json' })
190
156
  parsed_response = client.parse(response.body)
191
157
 
192
158
  if response.success?
193
- self.fields = parsed_response
159
+ self.fields = parsed_response["fields"]
194
160
  else
195
161
  client.handle_error(response.status, parsed_response)
196
162
  end
@@ -209,18 +175,8 @@ module Airrecord
209
175
  end
210
176
  end
211
177
 
212
- def serializable_fields(fields = self.fields)
213
- Hash[fields.map { |(key, value)|
214
- if association(key)
215
- value = [ value ] unless value.is_a?(Enumerable)
216
- assocs = value.map { |assoc|
217
- assoc.respond_to?(:id) ? assoc.id : assoc
218
- }
219
- [key, assocs]
220
- else
221
- [key, value]
222
- end
223
- }]
178
+ def serializable_fields
179
+ fields
224
180
  end
225
181
 
226
182
  def ==(other)
@@ -236,28 +192,11 @@ module Airrecord
236
192
 
237
193
  protected
238
194
 
239
- def association(key)
240
- if self.class.associations
241
- self.class.associations.find { |association|
242
- association[:column].to_s == column_mappings[key].to_s || association[:column].to_s == key.to_s
243
- }
244
- end
245
- end
246
-
247
195
  def fields=(fields)
248
196
  @updated_keys = []
249
- @column_mappings = Hash[fields.keys.map { |key| [underscore(key), key] }] # TODO remove (deprecate_symbols)
250
197
  @fields = fields
251
198
  end
252
199
 
253
- def self.underscore(key) # TODO remove (deprecate_symbols)
254
- key.to_s.strip.gsub(/\W+/, "_").downcase.to_sym
255
- end
256
-
257
- def underscore(key) # TODO remove (deprecate_symbols)
258
- self.class.underscore(key)
259
- end
260
-
261
200
  def created_at=(created_at)
262
201
  return unless created_at
263
202
  @created_at = Time.parse(created_at)
@@ -267,14 +206,14 @@ module Airrecord
267
206
  self.class.client
268
207
  end
269
208
 
270
- def type_cast(value)
271
- if value =~ /\d{4}-\d{2}-\d{2}/
272
- Time.parse(value + " UTC")
273
- else
274
- value
275
- end
209
+ def validate_key(key)
210
+ return true unless key.is_a?(Symbol)
211
+ raise Error, <<~MSG
212
+ Airrecord 1.0 dropped support for Symbols as field names.
213
+ Please use the full column name, a String, instead.
214
+ You might try: record['#{key.to_s.gsub('_', ' ')}']
215
+ MSG
276
216
  end
277
-
278
217
  end
279
218
 
280
219
  def self.table(api_key, base_key, table_name)
@@ -1,3 +1,3 @@
1
1
  module Airrecord
2
- VERSION = "0.2.5"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airrecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Eskildsen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-05 00:00:00.000000000 Z
11
+ date: 2018-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -113,6 +113,8 @@ files:
113
113
  - bin/setup
114
114
  - lib/airrecord.rb
115
115
  - lib/airrecord/client.rb
116
+ - lib/airrecord/faraday_rate_limiter.rb
117
+ - lib/airrecord/query_string.rb
116
118
  - lib/airrecord/table.rb
117
119
  - lib/airrecord/version.rb
118
120
  homepage: https://github.com/sirupsen/airrecord