airrecord 0.2.5 → 1.0.0

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