├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── airtable.gemspec ├── lib ├── airtable.rb └── airtable │ ├── client.rb │ ├── error.rb │ ├── record.rb │ ├── record_set.rb │ ├── resource.rb │ ├── table.rb │ └── version.rb └── test ├── airtable_test.rb ├── record_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in airtable.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Nathan Esquenazi 2 | Copyright (c) 2016 Airtable 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airtable Ruby Client 2 | 3 | Easily connect to [airtable](https://airtable.com) data using ruby with access to all of the airtable features. 4 | 5 | # Note on library status 6 | 7 | We are currently transitioning this gem to be supported by 8 | Airtable. We will maintain it moving forward, but until we fully 9 | support it, it will stay in the status of "community libraries". At 10 | that time we will remove this notice and add a "ruby" section to the 11 | API docs. 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | gem 'airtable' 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install airtable 26 | 27 | ## Usage 28 | 29 | ### Creating a Client 30 | 31 | First, be sure to register for an [airtable](https://airtable.com) account, create a data worksheet and [get an api key](https://airtable.com/account). Now, setup your Airtable client: 32 | 33 | ```ruby 34 | # Pass in api key to client 35 | @client = Airtable::Client.new("keyPCx5W") 36 | ``` 37 | 38 | Your API key carries the same privileges as your user account, so be sure to keep it secret! 39 | 40 | ### Accessing a Table 41 | 42 | Now we can access any table in our Airsheet account by referencing the [API docs](https://airtable.com/api): 43 | 44 | ```ruby 45 | # Pass in the app key and table name 46 | @table = @client.table("appPo84QuCy2BPgLk", "Table Name") 47 | ``` 48 | 49 | ### Querying Records 50 | 51 | Once you have access to a table from above, we can query a set of records in the table with: 52 | 53 | ```ruby 54 | @records = @table.records 55 | ``` 56 | 57 | We can specify a `sort` order, `limit`, and `offset` as part of our query: 58 | 59 | ```ruby 60 | @records = @table.records(:sort => ["Name", :asc], :limit => 50) 61 | @records # => [#"Bill Lowry", :email=>"billery@gmail.com">, ...] 62 | @records.offset #=> "itrEN2TCbrcSN2BMs" 63 | ``` 64 | 65 | This will return the records based on the query as well as an `offset` for the next round of records. We can then access the contents of any record: 66 | 67 | ```ruby 68 | @bill = @record.first 69 | # => #"Bill Lowry", :email=>"billery@gmail.com", :id=>"rec02sKGVIzU65eV1"> 70 | @bill[:id] # => "rec02sKGVIzU65eV2" 71 | @bill[:name] # => "Bill Lowry" 72 | @bill[:email] # => "billery@gmail.com" 73 | ``` 74 | 75 | Note that you can only request a maximimum of 100 records in a single query. To retrieve more records, use the "batch" feature below. 76 | 77 | ### Batch Querying All Records 78 | 79 | We can also query all records in the table through a series of batch requests with: 80 | 81 | ```ruby 82 | @records = @table.all(:sort => ["Name", :asc]) 83 | ``` 84 | 85 | This executes a variable number of network requests (100 records per batch) to retrieve all records in a sheet. 86 | 87 | We can also use `select` method to query based on specific conditions using `formula` parameter 88 | 89 | ```ruby 90 | @records = @table.select(sort: ["Order", "asc"], formula: "Active = 1") 91 | ``` 92 | 93 | This will return all the records that has `Active` column value as `true` from table. 94 | 95 | 96 | ### Finding a Record 97 | 98 | Records can be queried by `id` using the `find` method on a table: 99 | 100 | ```ruby 101 | @record = @table.find("rec02sKGVIzU65eV2") 102 | # => #"Bill Lowry", :email=>"billery@gmail.com", :id=>"rec02sKGVIzU65eV1"> 103 | ``` 104 | 105 | ### Inserting Records 106 | 107 | Records can be inserted using the `create` method on a table: 108 | 109 | ```ruby 110 | @record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com") 111 | @table.create(@record) 112 | # => #"Sarah Jaine", :email=>"sarah@jaine.com", :id=>"rec03sKOVIzU65eV4"> 113 | ``` 114 | 115 | ### Updating Records 116 | 117 | Records can be updated using the `update` method on a table: 118 | 119 | ```ruby 120 | @record[:email] = "sarahjaine@updated.com" 121 | @table.update(record) 122 | # => #"Sarah Jaine", :email=>"sarahjaine@updated.com", :id=>"rec03sKOVIzU65eV4"> 123 | ``` 124 | 125 | ### Deleting Records 126 | 127 | Records can be destroyed using the `destroy` method on a table: 128 | 129 | ```ruby 130 | @table.destroy(record) 131 | ``` 132 | 133 | ## Contributing 134 | 135 | 1. Fork it ( https://github.com/nesquena/airtable-ruby/fork ) 136 | 2. Create your feature branch (`git checkout -b my-new-feature`) 137 | 3. Commit your changes (`git commit -am 'Add some feature'`) 138 | 4. Push to the branch (`git push origin my-new-feature`) 139 | 5. Create a new Pull Request 140 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << 'test' 6 | t.pattern = "test/*_test.rb" 7 | end 8 | -------------------------------------------------------------------------------- /airtable.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'airtable/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "airtable" 8 | spec.version = Airtable::VERSION 9 | spec.authors = ["Nathan Esquenazi", "Alexander Sorokin"] 10 | spec.email = ["nesquena@gmail.com", "syrnick@gmail.com"] 11 | spec.summary = %q{Easily connect to airtable data using ruby} 12 | spec.description = %q{Easily connect to airtable data using ruby with access to all of the airtable features.} 13 | spec.homepage = "https://github.com/nesquena/airtable-ruby" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "httparty", "~> 0.14.0" 22 | spec.add_dependency "activesupport", ">= 3.0" 23 | 24 | spec.add_development_dependency "bundler", "~> 1.6" 25 | spec.add_development_dependency "rake" 26 | spec.add_development_dependency "minitest", "~> 5.6.0" 27 | spec.add_development_dependency "webmock", "~> 2.1.0" 28 | end 29 | -------------------------------------------------------------------------------- /lib/airtable.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | require 'delegate' 3 | require 'active_support/core_ext/hash' 4 | 5 | require 'airtable/version' 6 | require 'airtable/resource' 7 | require 'airtable/record' 8 | require 'airtable/record_set' 9 | require 'airtable/table' 10 | require 'airtable/client' 11 | require 'airtable/error' 12 | -------------------------------------------------------------------------------- /lib/airtable/client.rb: -------------------------------------------------------------------------------- 1 | # Allows access to data on airtable 2 | # 3 | # Fetch all records from table: 4 | # 5 | # client = Airtable::Client.new("keyPtVG4L4sVudsCx5W") 6 | # client.table("appXXV84QuCy2BPgLk", "Sheet Name").all 7 | # 8 | 9 | module Airtable 10 | class Client 11 | def initialize(api_key) 12 | @api_key = api_key 13 | end 14 | 15 | # table("appXXV84QuCy2BPgLk", "Sheet Name") 16 | def table(app_token, worksheet_name) 17 | Table.new(@api_key, app_token, worksheet_name) 18 | end 19 | end # Client 20 | end # Airtable -------------------------------------------------------------------------------- /lib/airtable/error.rb: -------------------------------------------------------------------------------- 1 | 2 | module Airtable 3 | class Error < StandardError 4 | 5 | attr_reader :message, :type 6 | # {"error"=>{"type"=>"UNKNOWN_COLUMN_NAME", "message"=>"Could not find fields foo"}} 7 | 8 | def initialize(error_hash) 9 | @message = error_hash['message'] 10 | @type = error_hash['type'] 11 | super(@message) 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/airtable/record.rb: -------------------------------------------------------------------------------- 1 | module Airtable 2 | 3 | class Record 4 | def initialize(attrs={}) 5 | override_attributes!(attrs) 6 | end 7 | 8 | def id; @attrs["id"]; end 9 | def id=(val); @attrs["id"] = val; end 10 | 11 | # Return given attribute based on name or blank otherwise 12 | def [](name) 13 | @attrs.has_key?(to_key(name)) ? @attrs[to_key(name)] : "" 14 | end 15 | 16 | # Set the given attribute to value 17 | def []=(name, value) 18 | @column_keys << name 19 | @attrs[to_key(name)] = value 20 | define_accessor(name) unless respond_to?(name) 21 | end 22 | 23 | def inspect 24 | "##{v.inspect}" }.join(", ")}>" 25 | end 26 | 27 | # Hash of attributes with underscored column names 28 | def attributes; @attrs; end 29 | 30 | # Removes old and add new attributes for the record 31 | def override_attributes!(attrs={}) 32 | @column_keys = attrs.keys 33 | @attrs = HashWithIndifferentAccess.new(Hash[attrs.map { |k, v| [ to_key(k), v ] }]) 34 | @attrs.map { |k, v| define_accessor(k) } 35 | end 36 | 37 | # Hash with keys based on airtable original column names 38 | def fields 39 | HashWithIndifferentAccess.new(Hash[@column_keys.map { |k| [ k, @attrs[to_key(k)] ] }]) 40 | end 41 | 42 | # Airtable will complain if we pass an 'id' as part of the request body. 43 | def fields_for_update; fields.except(:id); end 44 | 45 | def method_missing(name, *args, &blk) 46 | # Accessor for attributes 47 | if args.empty? && blk.nil? && @attrs.has_key?(name) 48 | @attrs[name] 49 | else 50 | super 51 | end 52 | end 53 | 54 | def respond_to?(name, include_private = false) 55 | @attrs.has_key?(name) || super 56 | end 57 | 58 | protected 59 | 60 | def to_key(string) 61 | string.is_a?(Symbol) ? string : underscore(string).to_sym 62 | end 63 | 64 | def underscore(string) 65 | string.gsub(/::/, '/'). 66 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 67 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 68 | gsub(/\s/, '_').tr("-", "_").downcase 69 | end 70 | 71 | def define_accessor(name) 72 | self.class.send(:define_method, name) { @attrs[name] } 73 | end 74 | 75 | end # Record 76 | end # Airtable 77 | -------------------------------------------------------------------------------- /lib/airtable/record_set.rb: -------------------------------------------------------------------------------- 1 | module Airtable 2 | 3 | # Contains records and the offset after a record query 4 | class RecordSet < SimpleDelegator 5 | 6 | attr_reader :records, :offset 7 | 8 | # results = { "records" => [{ ... }], "offset" => "abc5643" } 9 | # response from records api 10 | def initialize(results) 11 | # Parse records 12 | @records = results && results["records"] ? 13 | results["records"].map { |r| Record.new(r["fields"].merge("id" => r["id"])) } : [] 14 | # Store offset 15 | @offset = results["offset"] if results 16 | # Assign delegation object 17 | __setobj__(@records) 18 | end 19 | 20 | end # Record 21 | 22 | end # Airtable -------------------------------------------------------------------------------- /lib/airtable/resource.rb: -------------------------------------------------------------------------------- 1 | module Airtable 2 | # Base class for authorized resources sending network requests 3 | class Resource 4 | include HTTParty 5 | base_uri 'https://api.airtable.com/v0/' 6 | # debug_output $stdout 7 | 8 | attr_reader :api_key, :app_token, :worksheet_name 9 | 10 | def initialize(api_key, app_token, worksheet_name) 11 | @api_key = api_key 12 | @app_token = app_token 13 | @worksheet_name = worksheet_name 14 | self.class.headers({'Authorization' => "Bearer #{@api_key}"}) 15 | end 16 | end # AuthorizedResource 17 | end # Airtable -------------------------------------------------------------------------------- /lib/airtable/table.rb: -------------------------------------------------------------------------------- 1 | module Airtable 2 | 3 | class Table < Resource 4 | # Maximum results per request 5 | LIMIT_MAX = 100 6 | 7 | # Fetch all records iterating through offsets until retrieving the entire collection 8 | # all(:sort => ["Name", :desc]) 9 | def all(options={}) 10 | offset = nil 11 | results = [] 12 | begin 13 | options.merge!(:limit => LIMIT_MAX, :offset => offset) 14 | response = records(options) 15 | results += response.records 16 | offset = response.offset 17 | end until offset.nil? || offset.empty? || results.empty? 18 | results 19 | end 20 | 21 | # Fetch records from the sheet given the list options 22 | # Options: limit = 100, offset = "as345g", sort = ["Name", "asc"] 23 | # records(:sort => ["Name", :desc], :limit => 50, :offset => "as345g") 24 | def records(options={}) 25 | options["sortField"], options["sortDirection"] = options.delete(:sort) if options[:sort] 26 | results = self.class.get(worksheet_url, query: options).parsed_response 27 | check_and_raise_error(results) 28 | RecordSet.new(results) 29 | end 30 | 31 | # Query for records using a string formula 32 | # Options: limit = 100, offset = "as345g", sort = ["Name", "asc"], 33 | # fields = [Name, Email], formula = "Count > 5", view = "Main View" 34 | # 35 | # select(limit: 10, sort: ["Name", "asc"], formula: "Order < 2") 36 | def select(options={}) 37 | options['sortField'], options['sortDirection'] = options.delete(:sort) if options[:sort] 38 | options['maxRecords'] = options.delete(:limit) if options[:limit] 39 | 40 | if options[:formula] 41 | raise_bad_formula_error unless options[:formula].is_a? String 42 | options['filterByFormula'] = options.delete(:formula) 43 | end 44 | 45 | results = self.class.get(worksheet_url, query: options).parsed_response 46 | check_and_raise_error(results) 47 | RecordSet.new(results) 48 | end 49 | 50 | def raise_bad_formula_error 51 | raise ArgumentError.new("The value for filter should be a String.") 52 | end 53 | 54 | # Returns record based given row id 55 | def find(id) 56 | result = self.class.get(worksheet_url + "/" + id).parsed_response 57 | check_and_raise_error(result) 58 | Record.new(result_attributes(result)) if result.present? && result["id"] 59 | end 60 | 61 | # Creates a record by posting to airtable 62 | def create(record) 63 | result = self.class.post(worksheet_url, 64 | :body => { "fields" => record.fields }.to_json, 65 | :headers => { "Content-type" => "application/json" }).parsed_response 66 | 67 | check_and_raise_error(result) 68 | 69 | record.override_attributes!(result_attributes(result)) 70 | record 71 | end 72 | 73 | # Replaces record in airtable based on id 74 | def update(record) 75 | result = self.class.put(worksheet_url + "/" + record.id, 76 | :body => { "fields" => record.fields_for_update }.to_json, 77 | :headers => { "Content-type" => "application/json" }).parsed_response 78 | 79 | check_and_raise_error(result) 80 | 81 | record.override_attributes!(result_attributes(result)) 82 | record 83 | 84 | end 85 | 86 | def update_record_fields(record_id, fields_for_update) 87 | result = self.class.patch(worksheet_url + "/" + record_id, 88 | :body => { "fields" => fields_for_update }.to_json, 89 | :headers => { "Content-type" => "application/json" }).parsed_response 90 | 91 | check_and_raise_error(result) 92 | 93 | Record.new(result_attributes(result)) 94 | end 95 | 96 | # Deletes record in table based on id 97 | def destroy(id) 98 | self.class.delete(worksheet_url + "/" + id).parsed_response 99 | end 100 | 101 | protected 102 | 103 | def check_and_raise_error(response) 104 | response['error'] ? raise(Error.new(response['error'])) : false 105 | end 106 | 107 | def result_attributes(res) 108 | res["fields"].merge("id" => res["id"]) if res.present? && res["id"] 109 | end 110 | 111 | def worksheet_url 112 | "/#{app_token}/#{url_encode(worksheet_name)}" 113 | end 114 | 115 | # From http://apidock.com/ruby/ERB/Util/url_encode 116 | def url_encode(s) 117 | s.to_s.dup.force_encoding("ASCII-8BIT").gsub(/[^a-zA-Z0-9_\-.]/) { 118 | sprintf("%%%02X", $&.unpack("C")[0]) 119 | } 120 | end 121 | end # Table 122 | 123 | end # Airtable 124 | -------------------------------------------------------------------------------- /lib/airtable/version.rb: -------------------------------------------------------------------------------- 1 | module Airtable 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /test/airtable_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe Airtable do 4 | before do 5 | @client_key = "12345" 6 | @app_key = "appXXV84Qu" 7 | @sheet_name = "Test" 8 | end 9 | 10 | describe "with Airtable" do 11 | it "should allow client to be created" do 12 | @client = Airtable::Client.new(@client_key) 13 | assert_kind_of Airtable::Client, @client 14 | @table = @client.table(@app_key, @sheet_name) 15 | assert_kind_of Airtable::Table, @table 16 | end 17 | 18 | it "should fetch record set" do 19 | stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", { "records" => [], "offset" => "abcde" }) 20 | @table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) 21 | @records = @table.records 22 | assert_equal "abcde", @records.offset 23 | end 24 | 25 | it "should select records based on a formula" do 26 | query_str = "OR(RECORD_ID() = 'recXYZ1', RECORD_ID() = 'recXYZ2', RECORD_ID() = 'recXYZ3', RECORD_ID() = 'recXYZ4')" 27 | escaped_query = HTTParty::Request::NON_RAILS_QUERY_STRING_NORMALIZER.call(filterByFormula: query_str) 28 | request_url = "https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}?#{escaped_query}" 29 | stub_airtable_response!(request_url, { "records" => []}) 30 | @table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) 31 | @select_records = @table.select(formula: query_str) 32 | assert_equal @select_records.records, [] 33 | end 34 | 35 | it "should raise an ArgumentError if a formula is not a string" do 36 | stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", { "records" => [], "offset" => "abcde" }) 37 | @table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) 38 | proc { @table.select(formula: {foo: 'bar'}) }.must_raise ArgumentError 39 | end 40 | 41 | it "should allow creating records" do 42 | stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", 43 | { "fields" => { "name" => "Sarah Jaine", "email" => "sarah@jaine.com", "foo" => "bar" }, "id" => "12345" }, :post) 44 | table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) 45 | record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com") 46 | table.create(record) 47 | assert_equal "12345", record["id"] 48 | assert_equal "bar", record["foo"] 49 | end 50 | 51 | it "should allow updating records" do 52 | record_id = "12345" 53 | stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}/#{record_id}", 54 | { "fields" => { "name" => "Sarah Jaine", "email" => "sarah@jaine.com", "foo" => "bar" }, "id" => record_id }, :put) 55 | table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) 56 | record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com", :id => record_id) 57 | table.update(record) 58 | assert_equal "12345", record["id"] 59 | assert_equal "bar", record["foo"] 60 | end 61 | 62 | it "should raise an error when the API returns an error" do 63 | stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", 64 | {"error"=>{"type"=>"UNKNOWN_COLUMN_NAME", "message"=>"Could not find fields foo"}}, :post, 422) 65 | table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) 66 | record = Airtable::Record.new(:foo => "bar") 67 | assert_raises Airtable::Error do 68 | table.create(record) 69 | end 70 | end 71 | 72 | end # describe Airtable 73 | end # Airtable 74 | -------------------------------------------------------------------------------- /test/record_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe Airtable do 4 | describe Airtable::Record do 5 | it "should not return id in fields_for_update" do 6 | record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com", :id => 12345) 7 | record.fields_for_update.wont_include(:id) 8 | end 9 | 10 | it "returns new columns in fields_for_update" do 11 | record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com", :id => 12345) 12 | record[:website] = "http://sarahjaine.com" 13 | record.fields_for_update.must_include(:website) 14 | end 15 | 16 | it "returns fields_for_update in original capitalization" do 17 | record = Airtable::Record.new("Name" => "Sarah Jaine") 18 | record.fields_for_update.must_include("Name") 19 | end 20 | end # describe Record 21 | end # Airtable 22 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'airtable' 2 | require 'webmock/minitest' 3 | require 'minitest/pride' 4 | require 'minitest/autorun' 5 | 6 | def stub_airtable_response!(url, response, method=:get, status=200) 7 | 8 | stub_request(method, url) 9 | .to_return( 10 | body: response.to_json, 11 | status: status, 12 | headers: { 'Content-Type' => "application/json"} 13 | ) 14 | end 15 | --------------------------------------------------------------------------------