Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions lib/airtable/httparty_patched_hash_conversions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module HTTParty
module PatchedHashConversions
# @return <String> This hash as a query string
#
# @example
# { name: "Bob",
# address: {
# street: '111 Ruby Ave.',
# city: 'Ruby Central',
# phones: ['111-111-1111', '222-222-2222']
# }
# }.to_params
# #=> "name=Bob&address[city]=Ruby Central&address[phones][0]=111-111-1111&address[phones][1]=222-222-2222&address[street]=111 Ruby Ave."
def self.to_params(hash)
hash.to_hash.map { |k, v| normalize_param(k, v) }.join.chop
end

# @param key<Object> The key for the param.
# @param value<Object> The value for the param.
#
# @return <String> This key value pair as a param
#
# @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
def self.normalize_param(key, value)
param = ''
stack = []

if value.respond_to?(:to_ary)
param << value.to_ary.each_with_index.map { |element, index| normalize_param("#{key}[#{index}]", element) }.join
elsif value.respond_to?(:to_hash)
stack << [key, value.to_hash]
else
param << "#{key}=#{ERB::Util.url_encode(value.to_s)}&"
end

stack.each do |parent, hash|
hash.each do |k, v|
if v.respond_to?(:to_hash)
stack << ["#{parent}[#{k}]", v.to_hash]
else
param << normalize_param("#{parent}[#{k}]", v)
end
end
end

param
end
end
end
8 changes: 7 additions & 1 deletion lib/airtable/resource.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
require 'airtable/httparty_patched_hash_conversions'

module Airtable
# Base class for authorized resources sending network requests
class Resource
include HTTParty
base_uri 'https://api.airtable.com/v0/'
base_uri (ENV['AIRTABLE_ENDPOINT_URL'] || 'https://api.airtable.com/') + 'v0/'
# debug_output $stdout

attr_reader :api_key, :app_token, :worksheet_name

query_string_normalizer(proc do |query|
HTTParty::PatchedHashConversions.to_params(query)
end)

def initialize(api_key, app_token, worksheet_name)
@api_key = api_key
@app_token = app_token
Expand Down
47 changes: 44 additions & 3 deletions lib/airtable/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,25 @@ def all(options={})

# Fetch records from the sheet given the list options
# Options: limit = 100, offset = "as345g", sort = ["Name", "asc"]
# sort could be an array
# 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
update_sort_options!(options)
raw_response = self.class.get(worksheet_url, query: options)
case raw_response.code
when 200
# ok
when 422
results = raw_response.parsed_response
check_and_raise_error(results)
raise Error.new('Invalid request')
when 503
raise Error.new('Service not available')
when 500...600
puts "Server error #{response.code}"
raise Error.new('Server error please contact [email protected]')
end
results = raw_response.parsed_response
check_and_raise_error(results)
RecordSet.new(results)
end
Expand All @@ -34,7 +49,7 @@ def records(options={})
#
# select(limit: 10, sort: ["Name", "asc"], formula: "Order < 2")
def select(options={})
options['sortField'], options['sortDirection'] = options.delete(:sort) if options[:sort]
update_sort_options!(options)
options['maxRecords'] = options.delete(:limit) if options[:limit]

if options[:formula]
Expand All @@ -47,6 +62,32 @@ def select(options={})
RecordSet.new(results)
end

def update_sort_options!(options)
sortOption = options.delete(:sort) || options.delete('sort')
if sortOption && sortOption.is_a?(Array)
if sortOption.length > 0
if sortOption[0].is_a? String
singleSortField, singleSortDirection = sortOption
options["sort"] = [{field: singleSortField, direction: singleSortDirection}]
elsif sortOption[0].is_a?(Hash)
options["sort"] = sortOption.to_a
elsif sortOption.is_a?(Array) && sortOption[0].is_a?(Array)
options["sort"] = sortOption.map {|(sortField, sortDirection)| {field: sortField, direction: sortDirection} }
else
raise ArgumentError.new("Unknown sort options format.")
end
end
elsif sortOption
options["sort"] = sortOption
end

if options["sort"]
# standardize the sort spec
options["sort"] = options["sort"].map {|sortSpec| {field: sortSpec[:field].to_s, direction: sortSpec[:direction].to_s.downcase} }
raise ArgumentError.new("Unknown sort direction") unless options["sort"].all? {|sortObj| ['asc', 'desc'].include? sortObj[:direction]}
end
end

def raise_bad_formula_error
raise ArgumentError.new("The value for filter should be a String.")
end
Expand Down
7 changes: 7 additions & 0 deletions test/airtable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
assert_equal @select_records.records, []
end

it "should sort by multiple pairs" do
stub_airtable_response!("https://api.airtable.com/v0/#{@app_key}/#{@sheet_name}?sort[0][field]=foo&sort[0][direction]=asc&sort[1][field]=bar&sort[1][direction]=desc", { "records" => [], "offset" => "abcde" })
@table = Airtable::Client.new(@client_key).table(@app_key, @sheet_name)
@select_records = @table.select(sort: [['foo', 'asc'], ['bar', 'desc']])
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)
Expand Down
3 changes: 1 addition & 2 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
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"}
headers: { 'Content-Type' => 'application/json' }
)
end