diff --git a/.gitignore b/.gitignore index 90d2f3a..4b2e429 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ tmp *.a mkmf.log .DS_Store +.idea +.env.docker +*.log diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..b9ccbdd --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,11 @@ +AllCops: + Exclude: + - 'spec/**/*' + TargetRubyVersion: 2.1 +Metrics/LineLength: + Max: 100 +Metrics/ClassLength: + Exclude: + - "lib/airtable/entity/table.rb" +Metrics/AbcSize: + Max: 16 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..595b444 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM ruby:2.4.2 + +ENV LIB_HOME /home/lib + +WORKDIR $LIB_HOME diff --git a/Gemfile b/Gemfile index b8bbbb2..e690800 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source 'https://rubygems.org' # Specify your gem's dependencies in airtable.gemspec gemspec + +gem 'pry' diff --git a/README.md b/README.md index 8fb0a0c..89b8be4 100644 --- a/README.md +++ b/README.md @@ -35,29 +35,45 @@ First, be sure to register for an [airtable](https://airtable.com) account, crea @client = Airtable::Client.new("keyPCx5W") ``` +Also you can have `AIRTABLE_KEY` environment variable which is your API key. + +```ruby +# if you have AIRTABLE_KEY variable +@client = Airtable::Client.new +``` + Your API key carries the same privileges as your user account, so be sure to keep it secret! +### Accessing a Base + +Now we can access any base in our Airtable account by referencing the [API docs](https://airtable.com/api): + +```ruby +# Pass in the base id +@base = @client.table("appPo84QuCy2BPgLk") +``` + ### Accessing a Table -Now we can access any table in our Airsheet account by referencing the [API docs](https://airtable.com/api): +Now we can access any table in our Airtable account by referencing the [API docs](https://airtable.com/api): ```ruby -# Pass in the app key and table name -@table = @client.table("appPo84QuCy2BPgLk", "Table Name") +# Pass in the table name +@table = @base.table("Table Name") ``` -### Querying Records +### Batch Querying All Records Once you have access to a table from above, we can query a set of records in the table with: ```ruby -@records = @table.records +@records = @table.select ``` -We can specify a `sort` order, `limit`, and `offset` as part of our query: +We can specify a `sort` order, `per_page`, `max_records` and `offset` as part of our query: ```ruby -@records = @table.records(:sort => ["Name", :asc], :limit => 50) +@records = @table.records(:sort => ["Name", :asc], :page_size => 50) @records # => [#"Bill Lowry", :email=>"billery@gmail.com">, ...] @records.offset #=> "itrEN2TCbrcSN2BMs" ``` @@ -72,16 +88,6 @@ This will return the records based on the query as well as an `offset` for the n @bill[:email] # => "billery@gmail.com" ``` -Note that you can only request a maximimum of 100 records in a single query. To retrieve more records, use the "batch" feature below. - -### Batch Querying All Records - -We can also query all records in the table through a series of batch requests with: - -```ruby -@records = @table.all(:sort => ["Name", :asc]) -``` - This executes a variable number of network requests (100 records per batch) to retrieve all records in a sheet. ### Finding a Record @@ -121,6 +127,58 @@ Records can be destroyed using the `destroy` method on a table: @table.destroy(record) ``` +## Command Line Tool + +This gem includes a simple command line tool which shows the basic functionality of the service. + +``` +$ airtable +Usage: airtable operation options + +Common options: + -k, --api_key=KEY Airtable API key + -t, --table NAME Table Name + -b, --base BASE_ID Base ID + -r, --record RECORD_ID Record ID + -f, --field FIELD_NAME Field name to update or read + -v, --value VALUE Field value for update + +Supported Operations: + get - Get Record/Field + update - Update Field + +Examples: + airtable get -B Base -t Table + airtable get -B Base -t Table -r RECORD_ID + airtable get -B Base -t Table -f FIELD_NAME + airtable get -B Base -t Table -f FIELD_NAME -r RECORD_ID + airtable update -b Base -t table -r RECORD_ID -f FIELD_NAME -v newValue + + -h, --help Show this message + --version Show version +``` + +### Get record's JSON + +``` +$ airtable get -b base_id -t Table -r record_id +{"id":"record_id","fields":{...},"createdTime":"2015-11-11 23:05:58 UTC"} +``` + +### Get record's field value + +``` +$ airtable get -b base_id -t Table -r record_id -f field_name +FIELD_VALUE +``` + +### Update record's field value + +``` +$ airtable update -b base_id -t Table -r record_id -f field_name -v NEW_VALUE +OK +``` + ## Contributing 1. Fork it ( https://github.com/nesquena/airtable-ruby/fork ) diff --git a/Rakefile b/Rakefile index d30a219..be153a9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,17 @@ -require "bundler/gem_tasks" -require 'rake/testtask' +require 'bundler' +Bundler.setup +Bundler::GemHelper.install_tasks -Rake::TestTask.new do |t| - t.libs << 'test' - t.pattern = "test/*_test.rb" +require 'rspec/core/rake_task' + +desc 'Run all tests' +RSpec::Core::RakeTask.new(:spec) do |t| + t.ruby_opts = %w[-w] +end + +desc 'Run RuboCop on the lib directory' +task :rubocop do + sh 'bundle exec rubocop lib' end + +task default: %i[spec rubocop] diff --git a/airtable.gemspec b/airtable.gemspec index ae7e853..f7185da 100644 --- a/airtable.gemspec +++ b/airtable.gemspec @@ -1,28 +1,29 @@ -# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'airtable/version' Gem::Specification.new do |spec| - spec.name = "airtable" - spec.version = Airtable::VERSION - spec.authors = ["Nathan Esquenazi", "Alexander Sorokin"] - spec.email = ["nesquena@gmail.com", "syrnick@gmail.com"] - spec.summary = %q{Easily connect to airtable data using ruby} - spec.description = %q{Easily connect to airtable data using ruby with access to all of the airtable features.} - spec.homepage = "https://github.com/nesquena/airtable-ruby" - spec.license = "MIT" + spec.name = 'airtable' + spec.version = Airtable::VERSION + spec.authors = ['Nathan Esquenazi', 'Alexander Sorokin', 'Oleksandr Simonov'] + spec.email = ['nesquena@gmail.com', 'syrnick@gmail.com', 'oleksandr@simonov.me'] + spec.summary = 'Easily connect to airtable data using ruby' + spec.description = 'Easily connect to airtable data using ruby with access to all of the airtable features.' + spec.homepage = 'https://github.com/nesquena/airtable-ruby' + spec.license = 'MIT' - spec.files = `git ls-files -z`.split("\x0") - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ["lib"] + spec.files = `git ls-files -z`.split("\x0") + spec.bindir = 'exe' + spec.executables = `git ls-files -- exe/*`.split("\n").map {|f| File.basename(f)} + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.0.0' - spec.add_dependency "httparty", "~> 0.14.0" - spec.add_dependency "activesupport", ">= 3.0" - - spec.add_development_dependency "bundler", "~> 1.6" - spec.add_development_dependency "rake" - spec.add_development_dependency "minitest", "~> 5.6.0" - spec.add_development_dependency "webmock", "~> 2.1.0" + spec.add_development_dependency 'bundler', '~> 1.6' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'rspec', '~> 3.7' + spec.add_development_dependency 'rubocop', '~> 0.51' + spec.add_development_dependency 'simplecov', '~> 0.15' + spec.add_development_dependency 'vcr', '~> 4.0' + spec.add_development_dependency 'webmock', '~> 3.1' end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..59d9b9e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '2.1' +volumes: + app-gems: + driver: local +services: + lib: + build: + context: . + dockerfile: Dockerfile + env_file: + - .env.docker + volumes: + - app-gems:/usr/local/bundle + - .:/home/lib + diff --git a/exe/airtable b/exe/airtable new file mode 100755 index 0000000..0867383 --- /dev/null +++ b/exe/airtable @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +$stderr.sync = true +require 'airtable' +require 'airtable/cli' +::Airtable::CLI.new(ARGV).start \ No newline at end of file diff --git a/lib/airtable.rb b/lib/airtable.rb index 52b7858..6179d75 100644 --- a/lib/airtable.rb +++ b/lib/airtable.rb @@ -1,11 +1,41 @@ -require 'httparty' -require 'delegate' -require 'active_support/core_ext/hash' - -require 'airtable/version' -require 'airtable/resource' -require 'airtable/record' -require 'airtable/record_set' -require 'airtable/table' +require 'logger' + +# Airtable wrapper library +module Airtable + class << self + DEFAULT_URL = 'https://api.airtable.com/v0'.freeze + attr_writer :log_path + + def debug? + !ENV['DEBUG'].nil? + end + + def log_path + @log_path ||= 'airtable.log' + end + + def logger + @logger ||= ::Logger.new(@log_path) + end + + def server_url + @server_url ||= ENV.fetch('AIRTABLE_ENDPOINT_URL') { DEFAULT_URL } + end + + def reset! + @log_path = nil + @logger = nil + @server_url = nil + end + end +end + +require 'airtable/response' +require 'airtable/request' +require 'airtable/entity' require 'airtable/client' require 'airtable/error' + +# require 'airtable/resource' +# require 'airtable/record' +# require 'airtable/record_set' diff --git a/lib/airtable/cli.rb b/lib/airtable/cli.rb new file mode 100644 index 0000000..05bd322 --- /dev/null +++ b/lib/airtable/cli.rb @@ -0,0 +1,158 @@ +require 'optparse' +require 'airtable' + +# rubocop:disable all +module Airtable + # Command line Class + class CLI + def initialize(args) + trap_interrupt + @operation = args.shift + @args = args + @options = {} + @parser = OptionParser.new + end + + def start + add_banner + add_options + add_tail_options + add_supported_operations + @parser.parse!(@args) + if @options.empty? + puts @parser + else + unless valid_options? + puts @parser + return + end + run_operation + end + end + + def add_banner + @parser.banner = 'Usage: airtable operation options' + @parser.separator '' + end + + def add_options + @parser.separator 'Common options:' + @parser.on('-kKEY', '--api_key=KEY', 'Airtable API key') do |key| + @options[:api_key] = key + end + @parser.on('-tNAME', '--table NAME', 'Table Name') do |table| + @options[:table_name] = table + end + @parser.on('-bBASE_ID', '--base BASE_ID', 'Base ID') do |base_id| + @options[:base_id] = base_id + end + @parser.on('-rRECORD_ID', '--record RECORD_ID', 'Record ID') do |record_id| + @options[:record_id] = record_id + end + @parser.on('-fFIELD_NAME', '--field FIELD_NAME', 'Field name to update or read') do |field_name| + @options[:field_name] = field_name + end + @parser.on('-vVALUE', '--value VALUE', 'Field value for update') do |field_value| + @options[:field_value] = field_value + end + end + + def add_tail_options + @parser.on_tail('-h', '--help', 'Show this message') do + puts @parser + exit + end + @parser.on_tail('--version', 'Show version') do + puts ::Airtable::VERSION + exit + end + end + + def add_supported_operations + @parser.separator '' + @parser.separator 'Supported Operations:' + @parser.separator "\tget - Get Record/Field" + @parser.separator "\tupdate - Update Field" + @parser.separator '' + @parser.separator 'Examples:' + @parser.separator "\tairtable get -B Base -t Table" + @parser.separator "\tairtable get -B Base -t Table -r RECORD_ID" + @parser.separator "\tairtable get -B Base -t Table -f FIELD_NAME" + @parser.separator "\tairtable get -B Base -t Table -f FIELD_NAME -r RECORD_ID" + @parser.separator "\tairtable update -b Base -t table -r RECORD_ID -f FIELD_NAME -v newValue" + @parser.separator '' + end + + def valid_options? + @options[:table_name] && !@options[:table_name].empty? && + @options[:base_id] && !@options[:base_id].empty? + end + + def record_id? + @options[:record_id] && !@options[:record_id].empty? + end + + def field_name? + @options[:field_name] && !@options[:field_name].empty? + end + + def run_operation + case @operation + when "get" + case + when record_id? && field_name? + print_field + when record_id? && !field_name? + print_record + when !record_id? && field_name? + print_fields + else + print_records + end + when "update" + [:field_value, :field_name, :record_id].each do |key| + return if !@options[key] || @options[key].empty? + end + update_field + end + end + + def print_records + puts (::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).select.map do |record| + { id: record.id, fields: record.fields, createdTime: record.created_at } + end).to_json + end + + def print_record + record = ::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).find(@options[:record_id]) + puts ({ id: record.id, fields: record.fields, createdTime: record.created_at }).to_json + end + + def print_field + record = ::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).find(@options[:record_id]) + puts record.fields[@options[:field_name]] + end + + def print_fields + ::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).select.each do |record| + puts record.fields[@options[:field_name]] + end + end + + def update_field + ::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]) + .update(@options[:record_id], @options[:field_name] => @options[:field_value]) + record = ::Airtable::Client.new(@options[:api_key]).base(@options[:base_id]).table(@options[:table_name]).find(@options[:record_id]) + if record.fields[@options[:field_name]] == @options[:field_value] + puts 'OK' + else + puts 'ERROR' + end + end + + def trap_interrupt + trap('INT') { exit!(1) } + end + end +end +# rubocop:enable all diff --git a/lib/airtable/client.rb b/lib/airtable/client.rb index 159366a..0b5c9e2 100644 --- a/lib/airtable/client.rb +++ b/lib/airtable/client.rb @@ -1,20 +1,27 @@ -# Allows access to data on airtable -# -# Fetch all records from table: -# -# client = Airtable::Client.new("keyPtVG4L4sVudsCx5W") -# client.table("appXXV84QuCy2BPgLk", "Sheet Name").all -# - module Airtable + # Main client to Airtable class Client - def initialize(api_key) - @api_key = api_key + # @return [String] Airtable API Key + attr_reader :api_key + + # Initialize new Airtable client + # @param api_key [String] API Key for access Airtable + # @return [::Airtable::Client] Airtable client object + def initialize(api_key = nil) + @api_key = api_key || ENV['AIRTABLE_KEY'] + raise Airtable::MissingApiKeyError if @api_key.nil? || @api_key.empty? end - # table("appXXV84QuCy2BPgLk", "Sheet Name") - def table(app_token, worksheet_name) - Table.new(@api_key, app_token, worksheet_name) + # Get the Base Airtable Entity by id + # @param id [String] Id of Base on Airtable + # @return [::Airtable::Entity::Base] Airtable Base entity object + def base(id) + ::Airtable::Entity::Base.new(self, id) end - end # Client -end # Airtable \ No newline at end of file + + # table("appXXV84QuCy2BPgLk", "Sheet Name") + # def table(app_token, worksheet_name) + # Table.new(@api_key, app_token, worksheet_name) + # end + end +end diff --git a/lib/airtable/entity.rb b/lib/airtable/entity.rb new file mode 100644 index 0000000..92991af --- /dev/null +++ b/lib/airtable/entity.rb @@ -0,0 +1,9 @@ +module Airtable + # Entity submodule + module Entity + end +end + +require 'airtable/entity/base' +require 'airtable/entity/table' +require 'airtable/entity/record' diff --git a/lib/airtable/entity/base.rb b/lib/airtable/entity/base.rb new file mode 100644 index 0000000..dc3b10a --- /dev/null +++ b/lib/airtable/entity/base.rb @@ -0,0 +1,53 @@ +module Airtable + module Entity + # Airtable Base entity + class Base + def initialize(client, id) + @id = id + @client = client + end + + def table(name) + ::Airtable::Entity::Table.new(self, name) + end + + def __make_request__(method, path, data) + url = [::Airtable.server_url, @id, path].join('/') + resp = ::Airtable::Request.new(url, data, @client.api_key) + .request(method) + if resp.success? + resp.result + else + raise_correct_error_for(resp) + end + end + + # rubocop:disable all + def raise_correct_error_for(resp) + case resp.raw.code.to_i + when 401 + raise Airtable::AuthRequiredError.new(resp.raw) + when 403 + raise ::Airtable::NotAuthorizedError.new(resp.raw) + when 404 + raise ::Airtable::NotFoundError.new(resp.raw) + when 413 + raise ::Airtable::RequestBodyTooLargeError.new(resp.raw) + when 422 + raise ::Airtable::UnprocessableEntityError.new(resp.raw) + when 429 + raise ::Airtable::TooManyRequestsError.new(resp.raw) + when 500 + raise ::Airtable::ServerError.new(resp.raw) + when 503 + raise ::Airtable::ServiceUnavailableError.new(resp.raw) + else + if resp.raw.code.to_i > 400 + raise ::Airtable::RequestError.new(resp.raw, 'An unexpected error occurred') + end + end + # rubocop:enable all + end + end + end +end diff --git a/lib/airtable/entity/record.rb b/lib/airtable/entity/record.rb new file mode 100644 index 0000000..69c3711 --- /dev/null +++ b/lib/airtable/entity/record.rb @@ -0,0 +1,92 @@ +require 'time' +# Main object for store Airtable Record entity +module Airtable + module Entity + # Airtable Record entity + class Record + extend Forwardable + attr_reader :id, :created_at, :fields + + def initialize(id, options = {}) + @id = id + parse_options(options) + end + + def new_record? + @id.nil? || @id.empty? + end + + def __create__(base, table_name) + res = base.__make_request__(:post, table_name, fields: fields) + @id = res['id'] + parse_options(fields: res['fields'], created_at: res['createdTime']) + self + end + + def __update__(base, table_name) + args = [:patch, [table_name, @id].join('/'), fields: fields] + res = base.__make_request__(*args) + parse_options(fields: res['fields']) + self + end + + def __fetch__(base, path) + res = base.__make_request__(:get, path, {}) + parse_options(fields: res['fields'], created_at: res['createdTime']) + self + end + + def __replace__(base, table_name) + res = base.__make_request__(:put, [table_name, @id].join('/'), fields: fields) + parse_options(fields: res['fields']) + self + end + + def __destroy__(base, table_name) + res = base.__make_request__(:delete, [table_name, @id].join('/'), {}) + res['deleted'] + end + + def [](key) + @fields[key.to_s] + end + + def []=(key, value) + @fields[key.to_s] = value + end + + class << self + def all(base, table_name, params) + res = [] + __fetch__(base, table_name, params, res) + res + end + + private + + def __fetch__(base, table_name, params, res) + result = base.__make_request__(:get, table_name, params) + result['records'].each do |r| + args = [ + r['id'], fields: r['fields'], created_at: r['createdTime'] + ] + res << new(*args) + end + return unless result['offset'] + __fetch__(base, table_name, params.merge(offset: result['offset']), res) + end + end + + private + + def parse_options(options = {}) + if (fields = options.delete(:fields)) && !fields.empty? + @fields = fields + end + created_at = options.delete(:created_at) + return unless created_at + @created_at = ::Time.parse(created_at) + end + end + end +end diff --git a/lib/airtable/entity/table.rb b/lib/airtable/entity/table.rb new file mode 100644 index 0000000..820279e --- /dev/null +++ b/lib/airtable/entity/table.rb @@ -0,0 +1,143 @@ +require 'cgi' +module Airtable + module Entity + # Airtable Table entity + class Table + PAGE_SIZE = 100 + DEFAULT_DIRECTION = 'asc'.freeze + + attr_reader :name, :base + + def initialize(base, name) + @name = name + @base = base + @url_name = url_encode(@name) + @table_path = [@base, @url_name] + end + + def url_encode(str) + URI.escape(str, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) + end + + def select(options = {}) + params = {} + update_default_params(params, options) + update_sort_options(params, options) + validate_params(params) + fetch_records(params.compact) + end + + def find(id) + table_path = [@base, [@url_name, id].join('/')] + ::Airtable::Entity::Record.new(id).__fetch__(*table_path) + end + + def create(fields) + ::Airtable::Entity::Record.new(nil, fields: fields).__create__(*@table_path) + end + + def update(id, fields) + ::Airtable::Entity::Record.new(id, fields: fields).__update__(*@table_path) + end + + def replace(id, fields) + ::Airtable::Entity::Record.new(id, fields: fields).__replace__(*@table_path) + end + + def destroy(id) + ::Airtable::Entity::Record.new(id).__destroy__(*@table_path) + end + + private + + def option_value_for(hash, key) + hash.delete(key) || hash.delete(key.to_s) + end + + def fetch_records(params) + ::Airtable::Entity::Record.all(@base, @url_name, params) + end + + def update_default_params(params, options) + params[:fields] = option_value_for(options, :fields) + params[:maxRecords] = option_value_for(options, :max_records) + params[:offset] = option_value_for(options, :offset) + params[:view] = option_value_for(options, :view) + params[:filterByFormula] = option_value_for(options, :filter_by_formula) + params[:pageSize] = option_value_for(options, :page_size) || PAGE_SIZE + end + + def validate_params(params) + validate_fields(params[:fields]) + param_not_empty?(params[:view], ::Airtable::ViewOptionError) + param_not_empty?(params[:filterByFormula], ::Airtable::FilterByFormulaOptionError) + raise ::Airtable::LimitOptionError if params[:pageSize].to_i <= 0 + # rubocop:disable all + if params[:maxRecords] && params[:maxRecords].to_i <= 0 + raise ::Airtable::MaxRecordsOptionError + end + # rubocop:enable all + end + + def validate_fields(value) + return if !value || value.is_a?(::Array) + raise ::Airtable::FieldsOptionError + end + + def param_not_empty?(value, klass) + return if !value || (!value.empty? && value.is_a?(::String)) + raise klass + end + + def update_sort_options(params, options) + sort_option = option_value_for(options, :sort) + case sort_option + when ::Array + add_array_sort_options(params, sort_option) + when ::Hash + add_hash_sort_option(params, sort_option) + when ::String + add_string_sort_option(params, sort_option) + end + end + + def add_array_sort_options(params, array) + raise ::Airtable::SortOptionError if array.empty? + if array.size == 2 + add_sort_options(params, array) + else + array.each do |item| + add_sort_options(params, item) + end + end + end + + def add_string_sort_option(params, string) + raise ::Airtable::SortOptionError if string.nil? || string.empty? + params[:sort] ||= [] + params[:sort] << { field: string, direction: DEFAULT_DIRECTION } + end + + def add_hash_sort_option(params, hash) + if hash.keys.map(&:to_sym).sort != %i[direction field] + raise ::Airtable::SortOptionError + end + params[:sort] ||= [] + params[:sort] << hash + end + + def add_sort_options(params, sort_option) + case sort_option + when ::Array + raise Airtable::SortOptionError if sort_option.size != 2 + params[:sort] ||= [] + params[:sort] << { field: sort_option[0], direction: sort_option[1] } + when ::Hash + add_hash_sort_option(params, sort_option) + else + raise Airtable::SortOptionError + end + end + end + end +end diff --git a/lib/airtable/error.rb b/lib/airtable/error.rb index d81872e..50fbfd2 100644 --- a/lib/airtable/error.rb +++ b/lib/airtable/error.rb @@ -1,15 +1,117 @@ - module Airtable - class Error < StandardError + # missing api key error + class MissingApiKeyError < ::ArgumentError + def initialize + super('Missing API key') + end + end + + # sort option key error + class SortOptionError < ::ArgumentError + def initialize + super('Unknown sort option format.') + end + end + + # fields option key error + class FieldsOptionError < ::ArgumentError + def initialize + super('Invalid fields option format.') + end + end + # limit option key error + class LimitOptionError < ::ArgumentError + def initialize + super('Invalid limit option format.') + end + end - attr_reader :message, :type - # {"error"=>{"type"=>"UNKNOWN_COLUMN_NAME", "message"=>"Could not find fields foo"}} + # max records option key error + class MaxRecordsOptionError < ::ArgumentError + def initialize + super('Invalid max_records option format.') + end + end + + # view is empty + class ViewOptionError < ::ArgumentError + def initialize + super('Invalid view option format.') + end + end - def initialize(error_hash) - @message = error_hash['message'] - @type = error_hash['type'] - super(@message) + # filter by formula is empty + class FilterByFormulaOptionError < ::ArgumentError + def initialize + super('Invalid filter_by_formula option format.') end + end + # Request error + class RequestError < StandardError + attr_reader :body + + def initialize(body, message) + @body = body + super(message) + end + end + + # auth error + class AuthRequiredError < ::Airtable::RequestError + def initialize(body) + super(body, 'You should provide valid api key to perform this operation') + end + end + + # not authorized + class NotAuthorizedError < ::Airtable::RequestError + def initialize(body) + super(body, 'You are not authorized to perform this operation') + end + end + + # not found error + class NotFoundError < ::Airtable::RequestError + def initialize(body) + super(body, 'Could not find what you are looking for') + end + end + + # request body too large + class RequestBodyTooLargeError < ::Airtable::RequestError + def initialize(body) + super(body, 'Request body is too large') + end + end + + # unprocessed error + class UnprocessableEntityError < ::Airtable::RequestError + def initialize(body) + super(body, 'The operation cannot be processed') + end + end + + # too many requests + class TooManyRequestsError < ::Airtable::RequestError + def initialize(body) + super(body, + 'You have made too many requests in a short period of time. Please retry your request later' + ) + end + end + + # server error + class ServerError < ::Airtable::RequestError + def initialize(body) + super(body, 'Try again. If the problem persists, contact support.') + end + end + + # service unavailable + class ServiceUnavailableError < ::Airtable::RequestError + def initialize(body) + super(body, 'The service is temporarily unavailable. Please retry shortly.') + end end end diff --git a/lib/airtable/record.rb b/lib/airtable/record.rb deleted file mode 100644 index 03ba2c6..0000000 --- a/lib/airtable/record.rb +++ /dev/null @@ -1,76 +0,0 @@ -module Airtable - - class Record - def initialize(attrs={}) - override_attributes!(attrs) - end - - def id; @attrs["id"]; end - def id=(val); @attrs["id"] = val; end - - # Return given attribute based on name or blank otherwise - def [](name) - @attrs.has_key?(to_key(name)) ? @attrs[to_key(name)] : "" - end - - # Set the given attribute to value - def []=(name, value) - @column_keys << name - @attrs[to_key(name)] = value - define_accessor(name) unless respond_to?(name) - end - - def inspect - "##{v.inspect}" }.join(", ")}>" - end - - # Hash of attributes with underscored column names - def attributes; @attrs; end - - # Removes old and add new attributes for the record - def override_attributes!(attrs={}) - @column_keys = attrs.keys - @attrs = HashWithIndifferentAccess.new(Hash[attrs.map { |k, v| [ to_key(k), v ] }]) - @attrs.map { |k, v| define_accessor(k) } - end - - # Hash with keys based on airtable original column names - def fields - HashWithIndifferentAccess.new(Hash[@column_keys.map { |k| [ k, @attrs[to_key(k)] ] }]) - end - - # Airtable will complain if we pass an 'id' as part of the request body. - def fields_for_update; fields.except(:id); end - - def method_missing(name, *args, &blk) - # Accessor for attributes - if args.empty? && blk.nil? && @attrs.has_key?(name) - @attrs[name] - else - super - end - end - - def respond_to?(name, include_private = false) - @attrs.has_key?(name) || super - end - - protected - - def to_key(string) - string.is_a?(Symbol) ? string : underscore(string).to_sym - end - - def underscore(string) - string.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). - gsub(/\s/, '_').tr("-", "_").downcase - end - - def define_accessor(name) - self.class.send(:define_method, name) { @attrs[name] } - end - - end # Record -end # Airtable diff --git a/lib/airtable/record_set.rb b/lib/airtable/record_set.rb deleted file mode 100644 index 15f7085..0000000 --- a/lib/airtable/record_set.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Airtable - - # Contains records and the offset after a record query - class RecordSet < SimpleDelegator - - attr_reader :records, :offset - - # results = { "records" => [{ ... }], "offset" => "abc5643" } - # response from records api - def initialize(results) - # Parse records - @records = results && results["records"] ? - results["records"].map { |r| Record.new(r["fields"].merge("id" => r["id"])) } : [] - # Store offset - @offset = results["offset"] if results - # Assign delegation object - __setobj__(@records) - end - - end # Record - -end # Airtable \ No newline at end of file diff --git a/lib/airtable/request.rb b/lib/airtable/request.rb new file mode 100644 index 0000000..11e37d4 --- /dev/null +++ b/lib/airtable/request.rb @@ -0,0 +1,112 @@ +require 'net/http' +require 'net/https' +require 'json' + +module Airtable + # Main Object that made all requests to server + class Request + METHODS = %i[get post put patch delete].freeze + attr_accessor :url, :body, :headers, :http + + def initialize(url, body, token) + @url = URI(url) + @body = body + @headers = { + 'Authorization' => "Bearer #{token}", + 'User-Agent' => "Airtable gem v#{::Airtable::VERSION}", + 'Content-Type' => 'application/json' + } + end + + def request(type = :get) + @http = setup_http + request = setup_request(type) + setup_headers(request) + ::Airtable::Response.new(http.request(request)) + end + + private + + def setup_http + http = ::Net::HTTP.new(url.host, url.port) + http.use_ssl = true + http + end + + def setup_get_request + ::Net::HTTP::Get.new(url.path + '?' + to_query_hash(body)) + end + + def setup_post_request + request = ::Net::HTTP::Post.new(url.path) + request.body = body.to_json + request + end + + def setup_put_request + request = ::Net::HTTP::Put.new(url.path) + request.body = body.to_json + request + end + + def setup_patch_request + request = ::Net::HTTP::Patch.new(url.path) + request.body = body.to_json + request + end + + def setup_delete_request + ::Net::HTTP::Delete.new(url.path) + end + + def setup_request(type) + raise ::Airtable::BrokenMethod unless METHODS.include?(type) + __send__("setup_#{type}_request") + end + + def setup_headers(request) + headers.each do |name, value| + request[name] = value + end + end + + def to_query_default(key, value) + "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" + end + + def to_query_hash(hash, namespace = nil) + hash.collect do |key, value| + case value + when ::Hash + to_query_hash(value, namespace ? "#{namespace}[#{key}]" : key) + when ::Array + to_query_array(value, namespace ? "#{namespace}[#{key}]" : key) + else + to_query_default(namespace ? "#{namespace}[#{key}]" : key, value) + end + end.compact.sort! * '&' + end + + def to_query_array(array, namespace) + prefix = "#{namespace}[]" + if array.empty? + to_query_default(prefix, nil) + else + to_query_collect(array, prefix) + end + end + + def to_query_collect(array, prefix) + array.collect do |value| + case value + when ::Hash + to_query_hash(value, prefix) + when ::Array + to_query_array(value, prefix) + else + to_query_default(prefix, value) + end + end.join('&') + end + end +end diff --git a/lib/airtable/resource.rb b/lib/airtable/resource.rb deleted file mode 100644 index 26ae69a..0000000 --- a/lib/airtable/resource.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Airtable - # Base class for authorized resources sending network requests - class Resource - include HTTParty - base_uri 'https://api.airtable.com/v0/' - # debug_output $stdout - - attr_reader :api_key, :app_token, :worksheet_name - - def initialize(api_key, app_token, worksheet_name) - @api_key = api_key - @app_token = app_token - @worksheet_name = worksheet_name - self.class.headers({'Authorization' => "Bearer #{@api_key}"}) - end - end # AuthorizedResource -end # Airtable \ No newline at end of file diff --git a/lib/airtable/response.rb b/lib/airtable/response.rb new file mode 100644 index 0000000..08fa45e --- /dev/null +++ b/lib/airtable/response.rb @@ -0,0 +1,18 @@ +module Airtable + # Response processor class + class Response + attr_accessor :raw, :result + + def initialize(raw_resp) + @raw = raw_resp + body = raw.body + ::Airtable.logger.info "Response: #{body}" if ::Airtable.debug? + @result = ::JSON.parse(body) + @success = @raw.code.to_i == 200 + end + + def success? + @success + end + end +end diff --git a/lib/airtable/table.rb b/lib/airtable/table.rb deleted file mode 100644 index 82dc903..0000000 --- a/lib/airtable/table.rb +++ /dev/null @@ -1,123 +0,0 @@ -module Airtable - - class Table < Resource - # Maximum results per request - LIMIT_MAX = 100 - - # Fetch all records iterating through offsets until retrieving the entire collection - # all(:sort => ["Name", :desc]) - def all(options={}) - offset = nil - results = [] - begin - options.merge!(:limit => LIMIT_MAX, :offset => offset) - response = records(options) - results += response.records - offset = response.offset - end until offset.nil? || offset.empty? || results.empty? - results - end - - # Fetch records from the sheet given the list options - # Options: limit = 100, offset = "as345g", sort = ["Name", "asc"] - # records(:sort => ["Name", :desc], :limit => 50, :offset => "as345g") - def records(options={}) - options["sortField"], options["sortDirection"] = options.delete(:sort) if options[:sort] - results = self.class.get(worksheet_url, query: options).parsed_response - check_and_raise_error(results) - RecordSet.new(results) - end - - # Query for records using a string formula - # Options: limit = 100, offset = "as345g", sort = ["Name", "asc"], - # fields = [Name, Email], formula = "Count > 5", view = "Main View" - # - # select(limit: 10, sort: ["Name", "asc"], formula: "Order < 2") - def select(options={}) - options['sortField'], options['sortDirection'] = options.delete(:sort) if options[:sort] - options['maxRecords'] = options.delete(:limit) if options[:limit] - - if options[:formula] - raise_bad_formula_error unless options[:formula].is_a? String - options['filterByFormula'] = options.delete(:formula) - end - - results = self.class.get(worksheet_url, query: options).parsed_response - check_and_raise_error(results) - RecordSet.new(results) - end - - def raise_bad_formula_error - raise ArgumentError.new("The value for filter should be a String.") - end - - # Returns record based given row id - def find(id) - result = self.class.get(worksheet_url + "/" + id).parsed_response - check_and_raise_error(result) - Record.new(result_attributes(result)) if result.present? && result["id"] - end - - # Creates a record by posting to airtable - def create(record) - result = self.class.post(worksheet_url, - :body => { "fields" => record.fields }.to_json, - :headers => { "Content-type" => "application/json" }).parsed_response - - check_and_raise_error(result) - - record.override_attributes!(result_attributes(result)) - record - end - - # Replaces record in airtable based on id - def update(record) - result = self.class.put(worksheet_url + "/" + record.id, - :body => { "fields" => record.fields_for_update }.to_json, - :headers => { "Content-type" => "application/json" }).parsed_response - - check_and_raise_error(result) - - record.override_attributes!(result_attributes(result)) - record - - end - - def update_record_fields(record_id, fields_for_update) - result = self.class.patch(worksheet_url + "/" + record_id, - :body => { "fields" => fields_for_update }.to_json, - :headers => { "Content-type" => "application/json" }).parsed_response - - check_and_raise_error(result) - - Record.new(result_attributes(result)) - end - - # Deletes record in table based on id - def destroy(id) - self.class.delete(worksheet_url + "/" + id).parsed_response - end - - protected - - def check_and_raise_error(response) - response['error'] ? raise(Error.new(response['error'])) : false - end - - def result_attributes(res) - res["fields"].merge("id" => res["id"]) if res.present? && res["id"] - end - - def worksheet_url - "/#{app_token}/#{url_encode(worksheet_name)}" - end - - # From http://apidock.com/ruby/ERB/Util/url_encode - def url_encode(s) - s.to_s.dup.force_encoding("ASCII-8BIT").gsub(/[^a-zA-Z0-9_\-.]/) { - sprintf("%%%02X", $&.unpack("C")[0]) - } - end - end # Table - -end # Airtable diff --git a/lib/airtable/version.rb b/lib/airtable/version.rb index 019ad9a..a1e4c34 100644 --- a/lib/airtable/version.rb +++ b/lib/airtable/version.rb @@ -1,3 +1,3 @@ module Airtable - VERSION = "0.1.1" + VERSION = '0.2.0'.freeze end diff --git a/spec/lib/airtable/client_spec.rb b/spec/lib/airtable/client_spec.rb new file mode 100644 index 0000000..ed76c7b --- /dev/null +++ b/spec/lib/airtable/client_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +RSpec.describe ::Airtable::Client do + context '#new' do + context '(api_key)' do + it 'should return proper ::Airtable::Client' do + c = described_class.new('TEST_API') + expect(c.instance_variable_get(:@api_key)).to eq('TEST_API') + expect(c).to respond_to(:base) + end + end + + context '()' do + it 'should return proper ::Airtable::Client' do + allow(::ENV).to receive(:[]).with('AIRTABLE_KEY').and_return('TEST_API2') + c = described_class.new + expect(c.instance_variable_get(:@api_key)).to eq('TEST_API2') + expect(c).to respond_to(:base) + end + + it 'should raise error if no key present' do + allow(::ENV).to receive(:[]).with('AIRTABLE_KEY').and_return(nil) + expect { described_class.new }.to raise_error(::Airtable::MissingApiKeyError) + end + end + end + + context '#base' do + let(:client) { described_class.new } + it 'should return ::Airtable::Entity::Base' do + expect(client.base('TEST')).to be_a(::Airtable::Entity::Base) + end + + it 'should have a proper data' do + b = client.base('TEST') + expect(b.instance_variable_get(:@id)).to eq('TEST') + expect(b.instance_variable_get(:@client)).to eq(client) + end + end +end diff --git a/spec/lib/airtable/entity/base_spec.rb b/spec/lib/airtable/entity/base_spec.rb new file mode 100644 index 0000000..0ae03ed --- /dev/null +++ b/spec/lib/airtable/entity/base_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +RSpec.describe ::Airtable::Entity::Base do + let(:client) { ::Airtable::Client.new } + let(:base_entity) { described_class.new('appnlJrQ2fxlfRsov', client) } + context '#table' do + it 'should return ::Airtable::Entity::Table' do + expect(base_entity.table('Applicants')).to be_a(::Airtable::Entity::Table) + end + end +end diff --git a/spec/lib/airtable/entity/table_spec.rb b/spec/lib/airtable/entity/table_spec.rb new file mode 100644 index 0000000..c573e82 --- /dev/null +++ b/spec/lib/airtable/entity/table_spec.rb @@ -0,0 +1,230 @@ +require 'spec_helper' +require 'pry' + +RSpec.describe ::Airtable::Entity::Table, vcr: true do + let(:client) {::Airtable::Client.new} + let(:base_id) {'appnlJrQ2fxlfRsov'} + let(:base) {::Airtable::Entity::Base.new(client, base_id)} + let(:table_entity) {described_class.new(base, 'Applicants')} + context '#select' do + context '()' do + it 'should return array of ::Airtable::Entity::Record' do + res = table_entity.select + expect(res).to be_a(::Array) + expect(res.map(&:class).uniq).to eq([::Airtable::Entity::Record]) + end + it 'should return proper ::Airtable::Entity::Record' do + record = table_entity.select[0] + expect(record.id).to eq('recQes7d2DCuEcGe0') + expect(record.created_at).to be_a(::Time) + expect(record.created_at).to eq(::Time.parse('2015-11-11T23:05:58.000Z')) + expect(record.fields).to be_a(::Hash) + expect(record).to respond_to(:[]) + expect(record).to respond_to(:[]=) + expect(record['Name']).to eq('Chippy the Potato') + expect(record[:Name]).to eq('Chippy the Potato') + end + end + + context '({max_records:2})' do + it 'should return only 2 records' do + expect(table_entity.select(max_records: 2).size).to eq(2) + end + end + + context '({page_size: 2})' do + it 'should return all records records' do + expect(table_entity.select(page_size: 2).size).to eq(2) + end + end + + context '({sort: "Name", max_records: 2})' do + it 'should sort records by Name' do + params = { + pageSize: described_class::PAGE_SIZE, + sort: [{ field: 'Name', direction: described_class::DEFAULT_DIRECTION }], + maxRecords: 2 + } + expect(table_entity).to receive(:fetch_records).with(params).and_call_original + expect {table_entity.select(sort: 'Name', max_records: 2)}.to_not raise_error + end + end + + context '({sort: ["Name", "desc"], max_records: 2})' do + it 'should sort records by Name' do + params = { + pageSize: described_class::PAGE_SIZE, + sort: [{ field: 'Name', direction: 'desc' }], + maxRecords: 2 + } + expect(table_entity).to receive(:fetch_records).with(params).and_call_original + expect {table_entity.select(sort: ['Name', 'desc'], max_records: 2)}.to_not raise_error + end + end + + context '({sort: [["Name", "desc"]], max_records: 2})' do + it 'should sort records by Name' do + params = { + pageSize: described_class::PAGE_SIZE, + sort: [{ field: 'Name', direction: 'desc' }], + maxRecords: 2 + } + expect(table_entity).to receive(:fetch_records).with(params).and_call_original + expect {table_entity.select(sort: [['Name', 'desc']], max_records: 2)}.to_not raise_error + end + end + + context '({sort: [{field: "Name", direction: "desc"}], max_records: 2})' do + it 'should sort records by Name' do + params = { + pageSize: described_class::PAGE_SIZE, + sort: [{ field: 'Name', direction: 'desc' }], + maxRecords: 2 + } + expect(table_entity).to receive(:fetch_records).with(params).and_call_original + expect {table_entity.select(sort: [{ field: 'Name', direction: 'desc' }], max_records: 2)}.to_not raise_error + end + end + + context '({sort: {field: "Name", direction: "desc"}, max_records: 2})' do + it 'should sort records by Name' do + params = { + pageSize: described_class::PAGE_SIZE, + sort: [{ field: 'Name', direction: 'desc' }], + maxRecords: 2 + } + expect(table_entity).to receive(:fetch_records).with(params).and_call_original + expect {table_entity.select(sort: { field: 'Name', direction: 'desc' }, max_records: 2)}.to_not raise_error + end + end + + context '({sort: ["Name", "desc", "other"]})' do + it 'should raise ::Airtable::SortOptionsError' do + expect {table_entity.select(sort: ["Name", "desc", "other"])}.to raise_error(::Airtable::SortOptionError) + end + end + + context '({sort: {feild: "Name", direction: "desc"}})' do + it 'should raise ::Airtable::SortOptionsError' do + expect {table_entity.select(sort: { feild: "Name", direction: "desc" })}.to raise_error(::Airtable::SortOptionError) + end + end + + context '(fields: ["Name"])' do + it 'should collect only specified fields' do + res = table_entity.select(fields: ['Name'], max_records: 2) + expect(res[0].fields.keys).to eq(['Name']) + expect(res[1].fields.keys).to eq(['Name']) + end + end + + context '(fields: "Test")' do + it 'should raise ::Airtable::FieldsOptionsError' do + expect {table_entity.select(fields: 'Test')}.to raise_error(::Airtable::FieldsOptionError) + end + end + + context '(per_page: "Test")' do + it 'should raise ::Airtable::LimitOptionsError' do + expect {table_entity.select(page_size: 'Test')}.to raise_error(::Airtable::LimitOptionError) + end + end + + context '(max_records: "Test")' do + it 'should raise ::Airtable::MaxRecordsOptionsError' do + expect {table_entity.select(max_records: 'Test')}.to raise_error(::Airtable::MaxRecordsOptionError) + end + end + + context '({view: ["view"]})' do + it 'should raise ::Airtable::ViewOptionsError' do + expect {table_entity.select(view: ['view'])}.to raise_error(::Airtable::ViewOptionError) + end + end + + context '({view: ""})' do + it 'should raise ::Airtable::ViewOptionsError' do + expect {table_entity.select(view: '')}.to raise_error(::Airtable::ViewOptionError) + end + end + + context '({filter_by_formula: ""})' do + it 'should raise ::Airtable::ViewOptionsError' do + expect {table_entity.select(filter_by_formula: '')}.to raise_error(::Airtable::FilterByFormulaOptionError) + end + end + + context '({filter_by_formula: ["view"]})' do + it 'should raise ::Airtable::FilterByFormulaOptionsError' do + expect {table_entity.select(filter_by_formula: ['view'])}.to raise_error(::Airtable::FilterByFormulaOptionError) + end + end + end + context '#find' do + it 'should return a one record' do + record = table_entity.find('recQes7d2DCuEcGe0') + expect(record).to be_a(::Airtable::Entity::Record) + expect(record.id).to eq('recQes7d2DCuEcGe0') + expect(record.created_at).to be_a(::Time) + expect(record.created_at).to eq(::Time.parse('2015-11-11T23:05:58.000Z')) + expect(record.fields).to be_a(::Hash) + expect(record).to respond_to(:[]) + expect(record).to respond_to(:[]=) + expect(record['Name']).to eq('Chippy the Potato') + expect(record[:Name]).to eq('Chippy the Potato') + end + end + + context '#create' do + it 'should create new record' do + + fields = { + 'Name' => 'Super Test Name' + } + record = table_entity.create(fields) + expect(record).to be_a(::Airtable::Entity::Record) + expect(record.id).to_not be_nil + expect(record.created_at).to be_a(::Time) + expect(record.fields).to be_a(::Hash) + expect(record['Name']).to eq(fields['Name']) + rec = table_entity.find(record.id) + expect(rec['Name']).to eq(record['Name']) + end + end + + context '#update' do + it 'should update record' do + id = 'recIsbIqSnj72dp0O' + fields = { + 'Name' => 'Super Dupper Name' + } + record = table_entity.update(id, fields) + expect(record['Name']).to eq(fields['Name']) + rec = table_entity.find(id) + expect(rec['Name']).to eq(record['Name']) + end + end + + context '#replace' do + it 'should replace record' do + id = 'recSIn39bSTqt4Swc' + fields = { + 'Name' => 'Super Dupper Pupper Name' + } + expect(table_entity.find(id)['Phone']).to eq('(646) 555-4389') + record = table_entity.replace(id, fields) + expect(record['Name']).to eq(fields['Name']) + rec = table_entity.find(id) + expect(rec['Name']).to eq(record['Name']) + expect(rec['Phone']).to be_nil + end + end + + context '#destroy' do + it 'should remove record' do + id = 'recOjo2sDyUYHNSEH' + expect(table_entity.destroy(id)).to be_truthy + end + end + +end diff --git a/spec/lib/airtable_spec.rb b/spec/lib/airtable_spec.rb new file mode 100644 index 0000000..5d7cf83 --- /dev/null +++ b/spec/lib/airtable_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +RSpec.describe ::Airtable do + context '#server_url' do + before :each do + described_class.reset! + end + it 'should return default' do + expect(described_class.server_url).to eq('https://api.airtable.com/v0') + end + + it 'should custom from ENV' do + allow(ENV).to receive(:fetch).with('AIRTABLE_ENDPOINT_URL').and_return('CUSTOM') + expect(described_class.server_url).to eq('CUSTOM') + end + end + + context '#log_path' do + it 'should return default path' do + expect(described_class.log_path).to eq('airtable.log') + end + + it 'should return a custom' do + described_class.log_path = 'another.log' + expect(described_class.log_path).to eq('another.log') + end + end + + context '#debug?' do + it 'should return false' do + expect(described_class.debug?).to be_falsey + end + + it 'should return true' do + allow(ENV).to receive(:[]).with('DEBUG').and_return('1') + expect(described_class.debug?).to be_truthy + end + end + + context '#logger' do + it 'should return logger object' do + expect(described_class.logger).to be_a(::Logger) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..c250c08 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,82 @@ +$LOAD_PATH.unshift(File.expand_path('../lib', __FILE__)) +require 'bundler/setup' +require 'simplecov' + +SimpleCov.start do + add_filter %r{^/spec/} +end + +require 'rspec' +require 'vcr' +require 'airtable' + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + + VCR.configure do |vcr_config| + vcr_config.cassette_library_dir = 'spec/vcr_cassettes' + vcr_config.hook_into :webmock + vcr_config.configure_rspec_metadata! + vcr_config.ignore_localhost = true + end + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # This setting enables warnings. It's recommended, but in some cases may + # # be too noisy due to issues in dependencies. + # config.warnings = true + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_create/should_create_new_record.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_create/should_create_new_record.yml new file mode 100644 index 0000000..c49c469 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_create/should_create_new_record.yml @@ -0,0 +1,99 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants + body: + encoding: UTF-8 + string: '{"fields":{"Name":"Super Test Name"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:24:31 GMT + Etag: + - W/"67-ERjG9vhqlhEvpl+Uv7GZxQAiIGk" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '103' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Super Test Name"},"createdTime":"2017-12-14T17:24:30.931Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:24:34 GMT +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recIsbIqSnj72dp0O + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:24:32 GMT + Etag: + - W/"67-ERjG9vhqlhEvpl+Uv7GZxQAiIGk" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '103' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Super Test Name"},"createdTime":"2017-12-14T17:24:30.931Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:24:35 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_destroy/should_remove_record.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_destroy/should_remove_record.yml new file mode 100644 index 0000000..e10d519 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_destroy/should_remove_record.yml @@ -0,0 +1,51 @@ +--- +http_interactions: +- request: + method: delete + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recOjo2sDyUYHNSEH + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:49:29 GMT + Etag: + - W/"29-bYS+sJQwWG0RieiK6J1qa9k85ko" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '41' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"deleted":true,"id":"recOjo2sDyUYHNSEH"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:49:33 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_find/should_return_a_one_record.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_find/should_return_a_one_record.yml new file mode 100644 index 0000000..411cafa --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_find/should_return_a_one_record.yml @@ -0,0 +1,59 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recQes7d2DCuEcGe0 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:03:48 GMT + Etag: + - W/"646-c8+q4A2imuACKi9LmHulkyNZAhU" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '808' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"id":"recQes7d2DCuEcGe0","fields":{"Email Address":"c.potato@example.com","Phone + Screen Score":"2 - worth consideration","Onsite Interview Date":"2013-02-14","Stage":"Decision + Needed","Onsite Interview Notes":"Seems like a really hard worker, and has + a great attitude. Very observant: He''s got eyes everywhere. But I am concerned + that he won''t be able to think outside the bag.","Phone":"(208) 555-0505","Phone + Screen Date":"2013-02-07","Name":"Chippy the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:03:50 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_replace/should_replace_record.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_replace/should_replace_record.yml new file mode 100644 index 0000000..80aeb2f --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_replace/should_replace_record.yml @@ -0,0 +1,158 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recSIn39bSTqt4Swc + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:46:56 GMT + Etag: + - W/"698-3AuHK2TpKh22V1BQ3ybhvB18XCw" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '888' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"id":"recSIn39bSTqt4Swc","fields":{"Email Address":"ohuxley@example.com","Phone + Screen Score":"3 - good candidate","Onsite Interview Date":"2013-02-19","Stage":"Interviewing","Onsite + Interview Notes":"Owldous was impressive overall, and he has a lot of experience + in the corporate world. But if he were to join with a sports team, he''d still + have a lot to learn about the industry, e.g., I asked him for his opinion + on Leon the Lion''s recent resignation and he responded, \"Whooo?\"","Phone":"(646) + 555-4389","Phone Screen Date":"2013-02-11","Name":"Owldous Huxley","Onsite + Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attxKBp3Hb4FJH2I8","url":"https://dl.airtable.com/1ghGlZqKTViBcofjpPMC_owl.jpg","filename":"owl.jpg","size":242886,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/dFcZepTFCeG0YUXQSOrA_owl.jpg","width":54,"height":36},"large":{"url":"https://dl.airtable.com/QSkAheQ2SFuD3txaTgge_owl.jpg","width":256,"height":171}}},{"id":"attZJp9i2TJ4Z5xeM","url":"https://dl.airtable.com/WwYtwDVURFCGHCoipxhM_Resume%20(O.%20Huxley).docx","filename":"Resume + (O. Huxley).docx","size":16443,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-512x663.png","width":512,"height":663}}}],"Onsite + Interview Score":"3 - good candidate","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Good analytical + skills, very articulate. Proceed to in-person interview."},"createdTime":"2015-11-11T23:19:11.000Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:47:00 GMT +- request: + method: put + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recSIn39bSTqt4Swc + body: + encoding: UTF-8 + string: '{"fields":{"Name":"Super Dupper Pupper Name"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:46:58 GMT + Etag: + - W/"70-fJeV0f8Rv4gryp/FzVC28KLqPm0" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '112' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"id":"recSIn39bSTqt4Swc","fields":{"Name":"Super Dupper Pupper Name"},"createdTime":"2015-11-11T23:19:11.000Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:47:02 GMT +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recSIn39bSTqt4Swc + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:47:00 GMT + Etag: + - W/"70-fJeV0f8Rv4gryp/FzVC28KLqPm0" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '112' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"id":"recSIn39bSTqt4Swc","fields":{"Name":"Super Dupper Pupper Name"},"createdTime":"2015-11-11T23:19:11.000Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:47:03 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_/should_return_array_of_Airtable_Entity_Record.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_/should_return_array_of_Airtable_Entity_Record.yml new file mode 100644 index 0000000..0f162fd --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_/should_return_array_of_Airtable_Entity_Record.yml @@ -0,0 +1,81 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?pageSize=100 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:03:37 GMT + Etag: + - W/"1396-s0iIZ/It90SJrQVS5uVNRPfb7Cw" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1770' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recQes7d2DCuEcGe0","fields":{"Email Address":"c.potato@example.com","Phone + Screen Score":"2 - worth consideration","Onsite Interview Date":"2013-02-14","Stage":"Decision + Needed","Onsite Interview Notes":"Seems like a really hard worker, and has + a great attitude. Very observant: He''s got eyes everywhere. But I am concerned + that he won''t be able to think outside the bag.","Phone":"(208) 555-0505","Phone + Screen Date":"2013-02-07","Name":"Chippy the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Email + Address":"ohuxley@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-19","Stage":"Interviewing","Onsite Interview Notes":"Owldous + was impressive overall, and he has a lot of experience in the corporate world. + But if he were to join with a sports team, he''d still have a lot to learn + about the industry, e.g., I asked him for his opinion on Leon the Lion''s + recent resignation and he responded, \"Whooo?\"","Phone":"(646) 555-4389","Phone + Screen Date":"2013-02-11","Name":"Owldous Huxley","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attxKBp3Hb4FJH2I8","url":"https://dl.airtable.com/1ghGlZqKTViBcofjpPMC_owl.jpg","filename":"owl.jpg","size":242886,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/dFcZepTFCeG0YUXQSOrA_owl.jpg","width":54,"height":36},"large":{"url":"https://dl.airtable.com/QSkAheQ2SFuD3txaTgge_owl.jpg","width":256,"height":171}}},{"id":"attZJp9i2TJ4Z5xeM","url":"https://dl.airtable.com/WwYtwDVURFCGHCoipxhM_Resume%20(O.%20Huxley).docx","filename":"Resume + (O. Huxley).docx","size":16443,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-512x663.png","width":512,"height":663}}}],"Onsite + Interview Score":"3 - good candidate","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Good analytical + skills, very articulate. Proceed to in-person interview."},"createdTime":"2015-11-11T23:19:11.000Z"},{"id":"recxczR4BaLmvsdU2","fields":{"Email + Address":"hrmqueenlizzy@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-17","Stage":"Decision Needed","Onsite Interview Notes":"Liz + was highly qualified, but seemed shifty. When faced with a question she was + uncomfortable with, she slithered out of it. Could be someone that is very + smart, but difficult to deal with. Not sure that she''s a team player, which + of course is a problem if you''re going to be representing a team.","Phone":"(865) + 123-4567","Phone Screen Date":"2013-02-12","Name":"Queen Elizardbeth II","Onsite + Interviewer":["recmtSmO51zqqWAqs"],"Attachments":[{"id":"attQ6vAjxpfVlQedd","url":"https://dl.airtable.com/EXBc35SWSKKHxhbTUbOe_elizardbeth.jpg","filename":"elizardbeth.jpg","size":465335,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/jl9v4DQWSx8IoqzgWRTg_elizardbeth.jpg","width":24,"height":36},"large":{"url":"https://dl.airtable.com/YHtaEY7mTie2ZpS5hfJz_elizardbeth.jpg","width":256,"height":388}}},{"id":"attZa28XJJt0GL3DB","url":"https://dl.airtable.com/XdCerQy8MqG5RacAKwdn_Q%20Elizardbeth%20Resume.docx","filename":"Q + Elizardbeth Resume.docx","size":6519,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Move to in-person."},"createdTime":"2015-11-11T23:11:29.000Z"}]}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:03:39 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_/should_return_proper_Airtable_Entity_Record.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_/should_return_proper_Airtable_Entity_Record.yml new file mode 100644 index 0000000..3602f18 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_/should_return_proper_Airtable_Entity_Record.yml @@ -0,0 +1,81 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?pageSize=100 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:03:39 GMT + Etag: + - W/"1396-s0iIZ/It90SJrQVS5uVNRPfb7Cw" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1770' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recQes7d2DCuEcGe0","fields":{"Email Address":"c.potato@example.com","Phone + Screen Score":"2 - worth consideration","Onsite Interview Date":"2013-02-14","Stage":"Decision + Needed","Onsite Interview Notes":"Seems like a really hard worker, and has + a great attitude. Very observant: He''s got eyes everywhere. But I am concerned + that he won''t be able to think outside the bag.","Phone":"(208) 555-0505","Phone + Screen Date":"2013-02-07","Name":"Chippy the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Email + Address":"ohuxley@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-19","Stage":"Interviewing","Onsite Interview Notes":"Owldous + was impressive overall, and he has a lot of experience in the corporate world. + But if he were to join with a sports team, he''d still have a lot to learn + about the industry, e.g., I asked him for his opinion on Leon the Lion''s + recent resignation and he responded, \"Whooo?\"","Phone":"(646) 555-4389","Phone + Screen Date":"2013-02-11","Name":"Owldous Huxley","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attxKBp3Hb4FJH2I8","url":"https://dl.airtable.com/1ghGlZqKTViBcofjpPMC_owl.jpg","filename":"owl.jpg","size":242886,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/dFcZepTFCeG0YUXQSOrA_owl.jpg","width":54,"height":36},"large":{"url":"https://dl.airtable.com/QSkAheQ2SFuD3txaTgge_owl.jpg","width":256,"height":171}}},{"id":"attZJp9i2TJ4Z5xeM","url":"https://dl.airtable.com/WwYtwDVURFCGHCoipxhM_Resume%20(O.%20Huxley).docx","filename":"Resume + (O. Huxley).docx","size":16443,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-512x663.png","width":512,"height":663}}}],"Onsite + Interview Score":"3 - good candidate","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Good analytical + skills, very articulate. Proceed to in-person interview."},"createdTime":"2015-11-11T23:19:11.000Z"},{"id":"recxczR4BaLmvsdU2","fields":{"Email + Address":"hrmqueenlizzy@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-17","Stage":"Decision Needed","Onsite Interview Notes":"Liz + was highly qualified, but seemed shifty. When faced with a question she was + uncomfortable with, she slithered out of it. Could be someone that is very + smart, but difficult to deal with. Not sure that she''s a team player, which + of course is a problem if you''re going to be representing a team.","Phone":"(865) + 123-4567","Phone Screen Date":"2013-02-12","Name":"Queen Elizardbeth II","Onsite + Interviewer":["recmtSmO51zqqWAqs"],"Attachments":[{"id":"attQ6vAjxpfVlQedd","url":"https://dl.airtable.com/EXBc35SWSKKHxhbTUbOe_elizardbeth.jpg","filename":"elizardbeth.jpg","size":465335,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/jl9v4DQWSx8IoqzgWRTg_elizardbeth.jpg","width":24,"height":36},"large":{"url":"https://dl.airtable.com/YHtaEY7mTie2ZpS5hfJz_elizardbeth.jpg","width":256,"height":388}}},{"id":"attZa28XJJt0GL3DB","url":"https://dl.airtable.com/XdCerQy8MqG5RacAKwdn_Q%20Elizardbeth%20Resume.docx","filename":"Q + Elizardbeth Resume.docx","size":6519,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Move to in-person."},"createdTime":"2015-11-11T23:11:29.000Z"}]}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:03:40 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_fields_Name_/should_collect_only_specified_fields.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_fields_Name_/should_collect_only_specified_fields.yml new file mode 100644 index 0000000..8a8b358 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_fields_Name_/should_collect_only_specified_fields.yml @@ -0,0 +1,53 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?fields%5B%5D=Name&maxRecords=2&pageSize=100 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 15 Dec 2017 11:45:30 GMT + Etag: + - W/"e1-+BYhrDObLAiu8+VGH4H5Cd+wFFc" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '225' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"records":[{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Super Dupper + Name"},"createdTime":"2017-12-14T17:24:31.000Z"},{"id":"recQes7d2DCuEcGe0","fields":{"Name":"Chippy + the Potato"},"createdTime":"2015-11-11T23:05:58.000Z"}]}' + http_version: + recorded_at: Fri, 15 Dec 2017 11:45:40 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_fields_Test_/should_raise_Airtable_FieldsOptionsError.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_fields_Test_/should_raise_Airtable_FieldsOptionsError.yml new file mode 100644 index 0000000..4f1147b --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_fields_Test_/should_raise_Airtable_FieldsOptionsError.yml @@ -0,0 +1,52 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?fields=Test&pageSize=100 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 422 + message: Unprocessable Entity + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 15 Dec 2017 11:50:19 GMT + Etag: + - W/"7f-ic7Ppjtti07fW0oQKWhpH6lrz8A" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '127' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"error":{"type":"INVALID_REQUEST_UNKNOWN","message":"Invalid request: + parameter validation failed. Check your request data."}}' + http_version: + recorded_at: Fri, 15 Dec 2017 11:50:30 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_filter_by_formula_/should_raise_Airtable_ViewOptionsError.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_filter_by_formula_/should_raise_Airtable_ViewOptionsError.yml new file mode 100644 index 0000000..219d357 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_filter_by_formula_/should_raise_Airtable_ViewOptionsError.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?filterByFormula=&pageSize=100 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Airtable gem v0.2.0 + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 05 Jan 2018 10:14:01 GMT + Etag: + - W/"eca-Rf28b2fTsBkZuGX4RHwEOrLFa2w" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1475' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Supper Potato"},"createdTime":"2017-12-14T17:24:31.000Z"},{"id":"recQes7d2DCuEcGe0","fields":{"Email + Address":"test@example.com","Phone Screen Score":"2 - worth consideration","Onsite + Interview Date":"2013-02-14","Stage":"Decision Needed","Onsite Interview Notes":"Seems + like a really hard worker, and has a great attitude. Very observant: He''s + got eyes everywhere. But I am concerned that he won''t be able to think outside + the bag.","Phone":"(208) 555-0505","Phone Screen Date":"2013-02-07","Name":"Chippy + the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Name":"Super + Dupper Pupper Name"},"createdTime":"2015-11-11T23:19:11.000Z"},{"id":"reccgV23NfAFo0IxD","fields":{"Name":"353f02de-6928-4c36-a7ed-6807d369e0c3"},"createdTime":"2017-12-14T17:23:05.000Z"},{"id":"recmfqZPhlsMcuJS2","fields":{"Name":"8e7f34d5-7e04-410d-aa70-8a01e60a38de"},"createdTime":"2017-12-14T17:22:26.000Z"},{"id":"recxczR4BaLmvsdU2","fields":{"Email + Address":"hrmqueenlizzy@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-17","Stage":"Decision Needed","Onsite Interview Notes":"Liz + was highly qualified, but seemed shifty. When faced with a question she was + uncomfortable with, she slithered out of it. Could be someone that is very + smart, but difficult to deal with. Not sure that she''s a team player, which + of course is a problem if you''re going to be representing a team.","Phone":"(865) + 123-4567","Phone Screen Date":"2013-02-12","Name":"Queen Elizardbeth II","Onsite + Interviewer":["recmtSmO51zqqWAqs"],"Attachments":[{"id":"attQ6vAjxpfVlQedd","url":"https://dl.airtable.com/EXBc35SWSKKHxhbTUbOe_elizardbeth.jpg","filename":"elizardbeth.jpg","size":465335,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/jl9v4DQWSx8IoqzgWRTg_elizardbeth.jpg","width":24,"height":36},"large":{"url":"https://dl.airtable.com/YHtaEY7mTie2ZpS5hfJz_elizardbeth.jpg","width":256,"height":388}}},{"id":"attZa28XJJt0GL3DB","url":"https://dl.airtable.com/XdCerQy8MqG5RacAKwdn_Q%20Elizardbeth%20Resume.docx","filename":"Q + Elizardbeth Resume.docx","size":6519,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Move to in-person."},"createdTime":"2015-11-11T23:11:29.000Z"}]}' + http_version: + recorded_at: Fri, 05 Jan 2018 10:14:02 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_filter_by_formula_view_/should_raise_Airtable_FilterByFormulaOptionsError.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_filter_by_formula_view_/should_raise_Airtable_FilterByFormulaOptionsError.yml new file mode 100644 index 0000000..63a78c6 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_filter_by_formula_view_/should_raise_Airtable_FilterByFormulaOptionsError.yml @@ -0,0 +1,52 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?pageSize=100&view%5B%5D=view + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Airtable gem v0.2.0 + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 422 + message: Unprocessable Entity + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 05 Jan 2018 10:13:18 GMT + Etag: + - W/"7f-ic7Ppjtti07fW0oQKWhpH6lrz8A" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '127' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"error":{"type":"INVALID_REQUEST_UNKNOWN","message":"Invalid request: + parameter validation failed. Check your request data."}}' + http_version: + recorded_at: Fri, 05 Jan 2018 10:13:19 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_max_records_2_/should_return_only_2_records.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_max_records_2_/should_return_only_2_records.yml new file mode 100644 index 0000000..874711f --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_max_records_2_/should_return_only_2_records.yml @@ -0,0 +1,70 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?maxRecords=2&pageSize=100 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:03:40 GMT + Etag: + - W/"ced-6o2UVrJhZP7zN/aRnJse9JOltJM" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1343' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recQes7d2DCuEcGe0","fields":{"Email Address":"c.potato@example.com","Phone + Screen Score":"2 - worth consideration","Onsite Interview Date":"2013-02-14","Stage":"Decision + Needed","Onsite Interview Notes":"Seems like a really hard worker, and has + a great attitude. Very observant: He''s got eyes everywhere. But I am concerned + that he won''t be able to think outside the bag.","Phone":"(208) 555-0505","Phone + Screen Date":"2013-02-07","Name":"Chippy the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Email + Address":"ohuxley@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-19","Stage":"Interviewing","Onsite Interview Notes":"Owldous + was impressive overall, and he has a lot of experience in the corporate world. + But if he were to join with a sports team, he''d still have a lot to learn + about the industry, e.g., I asked him for his opinion on Leon the Lion''s + recent resignation and he responded, \"Whooo?\"","Phone":"(646) 555-4389","Phone + Screen Date":"2013-02-11","Name":"Owldous Huxley","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attxKBp3Hb4FJH2I8","url":"https://dl.airtable.com/1ghGlZqKTViBcofjpPMC_owl.jpg","filename":"owl.jpg","size":242886,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/dFcZepTFCeG0YUXQSOrA_owl.jpg","width":54,"height":36},"large":{"url":"https://dl.airtable.com/QSkAheQ2SFuD3txaTgge_owl.jpg","width":256,"height":171}}},{"id":"attZJp9i2TJ4Z5xeM","url":"https://dl.airtable.com/WwYtwDVURFCGHCoipxhM_Resume%20(O.%20Huxley).docx","filename":"Resume + (O. Huxley).docx","size":16443,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-512x663.png","width":512,"height":663}}}],"Onsite + Interview Score":"3 - good candidate","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Good analytical + skills, very articulate. Proceed to in-person interview."},"createdTime":"2015-11-11T23:19:11.000Z"}]}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:03:42 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_page_size_2_/should_return_all_records_records.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_page_size_2_/should_return_all_records_records.yml new file mode 100644 index 0000000..0c0a261 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_page_size_2_/should_return_all_records_records.yml @@ -0,0 +1,168 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?pageSize=2 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Airtable gem v0.2.0 + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id, user-agent, x-airtable-user-agent + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 20 May 2018 11:44:04 GMT + Etag: + - W/"6e5-sI32pUdODr7DKyvogbpzwf+tRaI" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '878' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Supper Potato"},"createdTime":"2017-12-14T17:24:31.000Z"},{"id":"recQes7d2DCuEcGe0","fields":{"Email + Address":"test@example.com","Phone Screen Score":"2 - worth consideration","Onsite + Interview Date":"2013-02-14","Stage":"Decision Needed","Onsite Interview Notes":"Seems + like a really hard worker, and has a great attitude. Very observant: He''s + got eyes everywhere. But I am concerned that he won''t be able to think outside + the bag.","Phone":"(208) 555-0505","Phone Screen Date":"2013-02-07","Name":"Chippy + the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"}],"offset":"itrBoHdqyN9t1mJnn/recQes7d2DCuEcGe0"}' + http_version: + recorded_at: Sun, 20 May 2018 11:44:04 GMT +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?offset=itrBoHdqyN9t1mJnn/recQes7d2DCuEcGe0&pageSize=2 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Airtable gem v0.2.0 + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id, user-agent, x-airtable-user-agent + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 20 May 2018 11:44:04 GMT + Etag: + - W/"12a-PieMTkMBioEOMqsOCn9l7HI3OqY" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '298' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"records":[{"id":"recSIn39bSTqt4Swc","fields":{"Name":"Super Dupper + Pupper Name"},"createdTime":"2015-11-11T23:19:11.000Z"},{"id":"reccgV23NfAFo0IxD","fields":{"Name":"353f02de-6928-4c36-a7ed-6807d369e0c3"},"createdTime":"2017-12-14T17:23:05.000Z"}],"offset":"itrBoHdqyN9t1mJnn/reccgV23NfAFo0IxD"}' + http_version: + recorded_at: Sun, 20 May 2018 11:44:04 GMT +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?offset=itrBoHdqyN9t1mJnn/reccgV23NfAFo0IxD&pageSize=2 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Airtable gem v0.2.0 + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id, user-agent, x-airtable-user-agent + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 20 May 2018 11:44:05 GMT + Etag: + - W/"733-C2V1JrjxZiOHMIZerL0vjd/qaDY" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '952' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recmfqZPhlsMcuJS2","fields":{"Name":"8e7f34d5-7e04-410d-aa70-8a01e60a38de"},"createdTime":"2017-12-14T17:22:26.000Z"},{"id":"recxczR4BaLmvsdU2","fields":{"Email + Address":"hrmqueenlizzy@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-17","Stage":"Decision Needed","Onsite Interview Notes":"Liz + was highly qualified, but seemed shifty. When faced with a question she was + uncomfortable with, she slithered out of it. Could be someone that is very + smart, but difficult to deal with. Not sure that she''s a team player, which + of course is a problem if you''re going to be representing a team.","Phone":"(865) + 123-4567","Phone Screen Date":"2013-02-12","Name":"Queen Elizardbeth II","Onsite + Interviewer":["recmtSmO51zqqWAqs"],"Attachments":[{"id":"attQ6vAjxpfVlQedd","url":"https://dl.airtable.com/EXBc35SWSKKHxhbTUbOe_elizardbeth.jpg","filename":"elizardbeth.jpg","size":465335,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/jl9v4DQWSx8IoqzgWRTg_elizardbeth.jpg","width":24,"height":36},"large":{"url":"https://dl.airtable.com/YHtaEY7mTie2ZpS5hfJz_elizardbeth.jpg","width":256,"height":388}}},{"id":"attZa28XJJt0GL3DB","url":"https://dl.airtable.com/XdCerQy8MqG5RacAKwdn_Q%20Elizardbeth%20Resume.docx","filename":"Q + Elizardbeth Resume.docx","size":6519,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Move to in-person."},"createdTime":"2015-11-11T23:11:29.000Z"}]}' + http_version: + recorded_at: Sun, 20 May 2018 11:44:05 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_Name_desc_max_records_2_/should_sort_records_by_Name.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_Name_desc_max_records_2_/should_sort_records_by_Name.yml new file mode 100644 index 0000000..b6c2d32 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_Name_desc_max_records_2_/should_sort_records_by_Name.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?maxRecords=2&pageSize=100&sort%5B%5D%5Bdirection%5D=desc&sort%5B%5D%5Bfield%5D=Name + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:03:46 GMT + Etag: + - W/"d4f-L2lRYBpsRwSZC+5vIYM5rC9fB+4" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1386' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recxczR4BaLmvsdU2","fields":{"Email Address":"hrmqueenlizzy@example.com","Phone + Screen Score":"3 - good candidate","Onsite Interview Date":"2013-02-17","Stage":"Decision + Needed","Onsite Interview Notes":"Liz was highly qualified, but seemed shifty. + When faced with a question she was uncomfortable with, she slithered out of + it. Could be someone that is very smart, but difficult to deal with. Not sure + that she''s a team player, which of course is a problem if you''re going to + be representing a team.","Phone":"(865) 123-4567","Phone Screen Date":"2013-02-12","Name":"Queen + Elizardbeth II","Onsite Interviewer":["recmtSmO51zqqWAqs"],"Attachments":[{"id":"attQ6vAjxpfVlQedd","url":"https://dl.airtable.com/EXBc35SWSKKHxhbTUbOe_elizardbeth.jpg","filename":"elizardbeth.jpg","size":465335,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/jl9v4DQWSx8IoqzgWRTg_elizardbeth.jpg","width":24,"height":36},"large":{"url":"https://dl.airtable.com/YHtaEY7mTie2ZpS5hfJz_elizardbeth.jpg","width":256,"height":388}}},{"id":"attZa28XJJt0GL3DB","url":"https://dl.airtable.com/XdCerQy8MqG5RacAKwdn_Q%20Elizardbeth%20Resume.docx","filename":"Q + Elizardbeth Resume.docx","size":6519,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Move to in-person."},"createdTime":"2015-11-11T23:11:29.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Email + Address":"ohuxley@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-19","Stage":"Interviewing","Onsite Interview Notes":"Owldous + was impressive overall, and he has a lot of experience in the corporate world. + But if he were to join with a sports team, he''d still have a lot to learn + about the industry, e.g., I asked him for his opinion on Leon the Lion''s + recent resignation and he responded, \"Whooo?\"","Phone":"(646) 555-4389","Phone + Screen Date":"2013-02-11","Name":"Owldous Huxley","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attxKBp3Hb4FJH2I8","url":"https://dl.airtable.com/1ghGlZqKTViBcofjpPMC_owl.jpg","filename":"owl.jpg","size":242886,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/dFcZepTFCeG0YUXQSOrA_owl.jpg","width":54,"height":36},"large":{"url":"https://dl.airtable.com/QSkAheQ2SFuD3txaTgge_owl.jpg","width":256,"height":171}}},{"id":"attZJp9i2TJ4Z5xeM","url":"https://dl.airtable.com/WwYtwDVURFCGHCoipxhM_Resume%20(O.%20Huxley).docx","filename":"Resume + (O. Huxley).docx","size":16443,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-512x663.png","width":512,"height":663}}}],"Onsite + Interview Score":"3 - good candidate","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Good analytical + skills, very articulate. Proceed to in-person interview."},"createdTime":"2015-11-11T23:19:11.000Z"}]}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:03:48 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_Name_max_records_2_/should_sort_records_by_Name.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_Name_max_records_2_/should_sort_records_by_Name.yml new file mode 100644 index 0000000..41ef2b5 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_Name_max_records_2_/should_sort_records_by_Name.yml @@ -0,0 +1,70 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?maxRecords=2&pageSize=100&sort%5B%5D%5Bdirection%5D=asc&sort%5B%5D%5Bfield%5D=Name + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:03:44 GMT + Etag: + - W/"ced-6o2UVrJhZP7zN/aRnJse9JOltJM" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1343' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recQes7d2DCuEcGe0","fields":{"Email Address":"c.potato@example.com","Phone + Screen Score":"2 - worth consideration","Onsite Interview Date":"2013-02-14","Stage":"Decision + Needed","Onsite Interview Notes":"Seems like a really hard worker, and has + a great attitude. Very observant: He''s got eyes everywhere. But I am concerned + that he won''t be able to think outside the bag.","Phone":"(208) 555-0505","Phone + Screen Date":"2013-02-07","Name":"Chippy the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Email + Address":"ohuxley@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-19","Stage":"Interviewing","Onsite Interview Notes":"Owldous + was impressive overall, and he has a lot of experience in the corporate world. + But if he were to join with a sports team, he''d still have a lot to learn + about the industry, e.g., I asked him for his opinion on Leon the Lion''s + recent resignation and he responded, \"Whooo?\"","Phone":"(646) 555-4389","Phone + Screen Date":"2013-02-11","Name":"Owldous Huxley","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attxKBp3Hb4FJH2I8","url":"https://dl.airtable.com/1ghGlZqKTViBcofjpPMC_owl.jpg","filename":"owl.jpg","size":242886,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/dFcZepTFCeG0YUXQSOrA_owl.jpg","width":54,"height":36},"large":{"url":"https://dl.airtable.com/QSkAheQ2SFuD3txaTgge_owl.jpg","width":256,"height":171}}},{"id":"attZJp9i2TJ4Z5xeM","url":"https://dl.airtable.com/WwYtwDVURFCGHCoipxhM_Resume%20(O.%20Huxley).docx","filename":"Resume + (O. Huxley).docx","size":16443,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-512x663.png","width":512,"height":663}}}],"Onsite + Interview Score":"3 - good candidate","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Good analytical + skills, very articulate. Proceed to in-person interview."},"createdTime":"2015-11-11T23:19:11.000Z"}]}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:03:46 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_field_Name_direction_desc_max_records_2_/should_sort_records_by_Name.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_field_Name_direction_desc_max_records_2_/should_sort_records_by_Name.yml new file mode 100644 index 0000000..296294a --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_sort_field_Name_direction_desc_max_records_2_/should_sort_records_by_Name.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?maxRecords=2&pageSize=100&sort%5B%5D%5Bdirection%5D=desc&sort%5B%5D%5Bfield%5D=Name + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:03:47 GMT + Etag: + - W/"d4f-L2lRYBpsRwSZC+5vIYM5rC9fB+4" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1386' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recxczR4BaLmvsdU2","fields":{"Email Address":"hrmqueenlizzy@example.com","Phone + Screen Score":"3 - good candidate","Onsite Interview Date":"2013-02-17","Stage":"Decision + Needed","Onsite Interview Notes":"Liz was highly qualified, but seemed shifty. + When faced with a question she was uncomfortable with, she slithered out of + it. Could be someone that is very smart, but difficult to deal with. Not sure + that she''s a team player, which of course is a problem if you''re going to + be representing a team.","Phone":"(865) 123-4567","Phone Screen Date":"2013-02-12","Name":"Queen + Elizardbeth II","Onsite Interviewer":["recmtSmO51zqqWAqs"],"Attachments":[{"id":"attQ6vAjxpfVlQedd","url":"https://dl.airtable.com/EXBc35SWSKKHxhbTUbOe_elizardbeth.jpg","filename":"elizardbeth.jpg","size":465335,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/jl9v4DQWSx8IoqzgWRTg_elizardbeth.jpg","width":24,"height":36},"large":{"url":"https://dl.airtable.com/YHtaEY7mTie2ZpS5hfJz_elizardbeth.jpg","width":256,"height":388}}},{"id":"attZa28XJJt0GL3DB","url":"https://dl.airtable.com/XdCerQy8MqG5RacAKwdn_Q%20Elizardbeth%20Resume.docx","filename":"Q + Elizardbeth Resume.docx","size":6519,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Move to in-person."},"createdTime":"2015-11-11T23:11:29.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Email + Address":"ohuxley@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-19","Stage":"Interviewing","Onsite Interview Notes":"Owldous + was impressive overall, and he has a lot of experience in the corporate world. + But if he were to join with a sports team, he''d still have a lot to learn + about the industry, e.g., I asked him for his opinion on Leon the Lion''s + recent resignation and he responded, \"Whooo?\"","Phone":"(646) 555-4389","Phone + Screen Date":"2013-02-11","Name":"Owldous Huxley","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attxKBp3Hb4FJH2I8","url":"https://dl.airtable.com/1ghGlZqKTViBcofjpPMC_owl.jpg","filename":"owl.jpg","size":242886,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/dFcZepTFCeG0YUXQSOrA_owl.jpg","width":54,"height":36},"large":{"url":"https://dl.airtable.com/QSkAheQ2SFuD3txaTgge_owl.jpg","width":256,"height":171}}},{"id":"attZJp9i2TJ4Z5xeM","url":"https://dl.airtable.com/WwYtwDVURFCGHCoipxhM_Resume%20(O.%20Huxley).docx","filename":"Resume + (O. Huxley).docx","size":16443,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZJp9i2TJ4Z5xeM-512x663.png","width":512,"height":663}}}],"Onsite + Interview Score":"3 - good candidate","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Good analytical + skills, very articulate. Proceed to in-person interview."},"createdTime":"2015-11-11T23:19:11.000Z"}]}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:03:49 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_view_/should_raise_Airtable_ViewOptionsError.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_view_/should_raise_Airtable_ViewOptionsError.yml new file mode 100644 index 0000000..0b47f59 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_view_/should_raise_Airtable_ViewOptionsError.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?pageSize=100&view= + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Airtable gem v0.2.0 + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 05 Jan 2018 10:14:00 GMT + Etag: + - W/"eca-Rf28b2fTsBkZuGX4RHwEOrLFa2w" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '1475' + Connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: '{"records":[{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Supper Potato"},"createdTime":"2017-12-14T17:24:31.000Z"},{"id":"recQes7d2DCuEcGe0","fields":{"Email + Address":"test@example.com","Phone Screen Score":"2 - worth consideration","Onsite + Interview Date":"2013-02-14","Stage":"Decision Needed","Onsite Interview Notes":"Seems + like a really hard worker, and has a great attitude. Very observant: He''s + got eyes everywhere. But I am concerned that he won''t be able to think outside + the bag.","Phone":"(208) 555-0505","Phone Screen Date":"2013-02-07","Name":"Chippy + the Potato","Onsite Interviewer":["recdnQmTV3Dxn3Apz"],"Attachments":[{"id":"attgK5ha1ajpVWOSY","url":"https://dl.airtable.com/xwv2ejXtTBqbTqjcnbQi_Chippypotato.jpg","filename":"Chippypotato.jpg","size":59402,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/g7BoJwauSyOZIdtoQALf_Chippypotato.jpg","width":27,"height":36},"large":{"url":"https://dl.airtable.com/IRqeRk4sTkud3Ga12uYI_Chippypotato.jpg","width":256,"height":341}}},{"id":"attU5T1mSPQ8r3nJ6","url":"https://dl.airtable.com/e6kCK3Z0SUS6hhCUoKcD_chippypotatoresume.docx","filename":"chippypotatoresume.docx","size":127681,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attU5T1mSPQ8r3nJ6-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recYPK7fwrFojHO9H"],"Phone + Screen Interviewer":["recmtSmO51zqqWAqs"],"Phone Screen Notes":"Questionable, + but tentatively move to on-site interview"},"createdTime":"2015-11-11T23:05:58.000Z"},{"id":"recSIn39bSTqt4Swc","fields":{"Name":"Super + Dupper Pupper Name"},"createdTime":"2015-11-11T23:19:11.000Z"},{"id":"reccgV23NfAFo0IxD","fields":{"Name":"353f02de-6928-4c36-a7ed-6807d369e0c3"},"createdTime":"2017-12-14T17:23:05.000Z"},{"id":"recmfqZPhlsMcuJS2","fields":{"Name":"8e7f34d5-7e04-410d-aa70-8a01e60a38de"},"createdTime":"2017-12-14T17:22:26.000Z"},{"id":"recxczR4BaLmvsdU2","fields":{"Email + Address":"hrmqueenlizzy@example.com","Phone Screen Score":"3 - good candidate","Onsite + Interview Date":"2013-02-17","Stage":"Decision Needed","Onsite Interview Notes":"Liz + was highly qualified, but seemed shifty. When faced with a question she was + uncomfortable with, she slithered out of it. Could be someone that is very + smart, but difficult to deal with. Not sure that she''s a team player, which + of course is a problem if you''re going to be representing a team.","Phone":"(865) + 123-4567","Phone Screen Date":"2013-02-12","Name":"Queen Elizardbeth II","Onsite + Interviewer":["recmtSmO51zqqWAqs"],"Attachments":[{"id":"attQ6vAjxpfVlQedd","url":"https://dl.airtable.com/EXBc35SWSKKHxhbTUbOe_elizardbeth.jpg","filename":"elizardbeth.jpg","size":465335,"type":"image/jpeg","thumbnails":{"small":{"url":"https://dl.airtable.com/jl9v4DQWSx8IoqzgWRTg_elizardbeth.jpg","width":24,"height":36},"large":{"url":"https://dl.airtable.com/YHtaEY7mTie2ZpS5hfJz_elizardbeth.jpg","width":256,"height":388}}},{"id":"attZa28XJJt0GL3DB","url":"https://dl.airtable.com/XdCerQy8MqG5RacAKwdn_Q%20Elizardbeth%20Resume.docx","filename":"Q + Elizardbeth Resume.docx","size":6519,"type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","thumbnails":{"small":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-28x36.png","width":28,"height":36},"large":{"url":"https://dl.airtable.com/attZa28XJJt0GL3DB-256x331.png","width":256,"height":331}}}],"Onsite + Interview Score":"2 - worth consideration","Applying for":["recgrjFdvUdTGnDsF"],"Phone + Screen Interviewer":["recnxTtR21srnQBoG"],"Phone Screen Notes":"Move to in-person."},"createdTime":"2015-11-11T23:11:29.000Z"}]}' + http_version: + recorded_at: Fri, 05 Jan 2018 10:14:02 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_select/_view_view_/should_raise_Airtable_ViewOptionsError.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_view_view_/should_raise_Airtable_ViewOptionsError.yml new file mode 100644 index 0000000..1426252 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_select/_view_view_/should_raise_Airtable_ViewOptionsError.yml @@ -0,0 +1,52 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants?pageSize=100&view%5B%5D=view + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Airtable gem v0.2.0 + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 422 + message: Unprocessable Entity + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 05 Jan 2018 10:13:17 GMT + Etag: + - W/"7f-ic7Ppjtti07fW0oQKWhpH6lrz8A" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '127' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"error":{"type":"INVALID_REQUEST_UNKNOWN","message":"Invalid request: + parameter validation failed. Check your request data."}}' + http_version: + recorded_at: Fri, 05 Jan 2018 10:13:19 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/vcr_cassettes/Airtable_Entity_Table/_update/should_update_record.yml b/spec/vcr_cassettes/Airtable_Entity_Table/_update/should_update_record.yml new file mode 100644 index 0000000..14d0621 --- /dev/null +++ b/spec/vcr_cassettes/Airtable_Entity_Table/_update/should_update_record.yml @@ -0,0 +1,99 @@ +--- +http_interactions: +- request: + method: patch + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recIsbIqSnj72dp0O + body: + encoding: UTF-8 + string: '{"fields":{"Name":"Super Dupper Name"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:31:42 GMT + Etag: + - W/"69-S72JAuw/B5v79C0cOeqgy+rkf/g" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '105' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Super Dupper Name"},"createdTime":"2017-12-14T17:24:30.931Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:31:45 GMT +- request: + method: get + uri: https://api.airtable.com/v0/appnlJrQ2fxlfRsov/Applicants/recIsbIqSnj72dp0O + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Authorization: + - Bearer key3nfyQojyvXquoR + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Headers: + - content-type, authorization, content-length, x-requested-with, x-api-version, + x-airtable-application-id + Access-Control-Allow-Methods: + - GET,PUT,POST,PATCH,DELETE,OPTIONS + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Dec 2017 17:31:43 GMT + Etag: + - W/"69-S72JAuw/B5v79C0cOeqgy+rkf/g" + Server: + - Tengine + Vary: + - Accept-Encoding + Content-Length: + - '105' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"id":"recIsbIqSnj72dp0O","fields":{"Name":"Super Dupper Name"},"createdTime":"2017-12-14T17:24:30.931Z"}' + http_version: + recorded_at: Thu, 14 Dec 2017 17:31:46 GMT +recorded_with: VCR 4.0.0 diff --git a/test/airtable_test.rb b/test/airtable_test.rb deleted file mode 100644 index be443e7..0000000 --- a/test/airtable_test.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'test_helper' - -describe Airtable do - before do - @client_key = "12345" - @app_key = "appXXV84Qu" - @sheet_name = "Test" - end - - describe "with Airtable" do - it "should allow client to be created" do - @client = Airtable::Client.new(@client_key) - assert_kind_of Airtable::Client, @client - @table = @client.table(@app_key, @sheet_name) - assert_kind_of Airtable::Table, @table - end - - it "should fetch record set" do - stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", { "records" => [], "offset" => "abcde" }) - @table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) - @records = @table.records - assert_equal "abcde", @records.offset - end - - it "should select records based on a formula" do - query_str = "OR(RECORD_ID() = 'recXYZ1', RECORD_ID() = 'recXYZ2', RECORD_ID() = 'recXYZ3', RECORD_ID() = 'recXYZ4')" - escaped_query = HTTParty::Request::NON_RAILS_QUERY_STRING_NORMALIZER.call(filterByFormula: query_str) - request_url = "https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}?#{escaped_query}" - stub_airtable_response!(request_url, { "records" => []}) - @table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) - @select_records = @table.select(formula: query_str) - assert_equal @select_records.records, [] - end - - it "should raise an ArgumentError if a formula is not a string" do - stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", { "records" => [], "offset" => "abcde" }) - @table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) - proc { @table.select(formula: {foo: 'bar'}) }.must_raise ArgumentError - end - - it "should allow creating records" do - stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", - { "fields" => { "name" => "Sarah Jaine", "email" => "sarah@jaine.com", "foo" => "bar" }, "id" => "12345" }, :post) - table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) - record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com") - table.create(record) - assert_equal "12345", record["id"] - assert_equal "bar", record["foo"] - end - - it "should allow updating records" do - record_id = "12345" - stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}/#{record_id}", - { "fields" => { "name" => "Sarah Jaine", "email" => "sarah@jaine.com", "foo" => "bar" }, "id" => record_id }, :put) - table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) - record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com", :id => record_id) - table.update(record) - assert_equal "12345", record["id"] - assert_equal "bar", record["foo"] - end - - it "should raise an error when the API returns an error" do - stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}", - {"error"=>{"type"=>"UNKNOWN_COLUMN_NAME", "message"=>"Could not find fields foo"}}, :post, 422) - table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name) - record = Airtable::Record.new(:foo => "bar") - assert_raises Airtable::Error do - table.create(record) - end - end - - end # describe Airtable -end # Airtable diff --git a/test/record_test.rb b/test/record_test.rb deleted file mode 100644 index 1ee836a..0000000 --- a/test/record_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'test_helper' - -describe Airtable do - describe Airtable::Record do - it "should not return id in fields_for_update" do - record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com", :id => 12345) - record.fields_for_update.wont_include(:id) - end - - it "returns new columns in fields_for_update" do - record = Airtable::Record.new(:name => "Sarah Jaine", :email => "sarah@jaine.com", :id => 12345) - record[:website] = "http://sarahjaine.com" - record.fields_for_update.must_include(:website) - end - - it "returns fields_for_update in original capitalization" do - record = Airtable::Record.new("Name" => "Sarah Jaine") - record.fields_for_update.must_include("Name") - end - end # describe Record -end # Airtable diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index c0e9338..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'airtable' -require 'webmock/minitest' -require 'minitest/pride' -require 'minitest/autorun' - -def stub_airtable_response!(url, response, method=:get, status=200) - - stub_request(method, url) - .to_return( - body: response.to_json, - status: status, - headers: { 'Content-Type' => "application/json"} - ) -end