scribesend 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/CHANGELOG +3 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.markdown +17 -0
- data/VERSION +1 -0
- data/bin/scribesend-console +7 -0
- data/lib/scribesend/account.rb +9 -0
- data/lib/scribesend/api_resource.rb +77 -0
- data/lib/scribesend/entry.rb +27 -0
- data/lib/scribesend/entry_line.rb +30 -0
- data/lib/scribesend/error.rb +17 -0
- data/lib/scribesend/list_object.rb +16 -0
- data/lib/scribesend/scribesend_object.rb +241 -0
- data/lib/scribesend/util.rb +89 -0
- data/lib/scribesend/version.rb +3 -0
- data/lib/scribesend.rb +173 -0
- data/scribesend.gemspec +24 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 81015fd2ae5f77f5e6045f9d95d77edb912cb993
|
4
|
+
data.tar.gz: ea7630f5d1f27d7ba2f57522545c2264fd1bfc0a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1c5fd6b0a1f1c9f8831e4ce5e74a05796893584da95316f7b37beb875cf6d82fa0f9c84fca05ca21d8913745b565715000842488505559810fe665b6a96ed340
|
7
|
+
data.tar.gz: 102319d7d4eec6f712b6f254bdd2c52b09c18a26ac57e4eb75af7cdeac5508f77d61894344d481eb42d5cd9734c86e8959a873c8ebb9620b8dc11e63c4052e94
|
data/.gitignore
ADDED
data/CHANGELOG
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2015- Scribesend
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Scribesend
|
2
|
+
|
3
|
+
Ruby bindings for [https://scribesend.com](https://scribesend.com)
|
4
|
+
|
5
|
+
## Documentation
|
6
|
+
|
7
|
+
[API Docs](https://scribesend.com/docs/ruby)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
To install, simply run:
|
12
|
+
|
13
|
+
gem install scribesend
|
14
|
+
|
15
|
+
And then import the client in your application:
|
16
|
+
|
17
|
+
require 'scribesend'
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.4
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Scribesend
|
2
|
+
class Account < APIResource
|
3
|
+
def self.retrieve_entry_lines(id, opts={})
|
4
|
+
instance = self.new(id, opts)
|
5
|
+
response, opts = instance.class.request(:get, instance.url + "/entry_lines", opts)
|
6
|
+
Util.convert_to_scribesend_object(response, opts)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Scribesend
|
2
|
+
class APIResource < ScribesendObject
|
3
|
+
def self.class_name
|
4
|
+
self.name.split('::')[-1]
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.url
|
8
|
+
if self == APIResource
|
9
|
+
raise NotImplementedError.new("APIResource is an abstract class. You should perform actions on its subclasses (Account, Entry, etc.)")
|
10
|
+
end
|
11
|
+
if (self.class_name[-1..-1] == 's' || self.class_name[-1..-1] == 'h')
|
12
|
+
return "/#{CGI.escape(self.class_name.downcase)}es"
|
13
|
+
elsif (self.class_name[-1..-1] == 'y')
|
14
|
+
return "/#{CGI.escape(self.class_name.downcase[0..-2])}ies"
|
15
|
+
else
|
16
|
+
return "/#{CGI.escape(class_name.downcase)}s"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def url
|
21
|
+
unless id = self['id']
|
22
|
+
raise Error.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", "id")
|
23
|
+
end
|
24
|
+
"#{self.class.url}/#{CGI.escape(id)}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# API Methods
|
28
|
+
|
29
|
+
def self.request(method, url, params={}, opts={})
|
30
|
+
headers = opts.clone
|
31
|
+
api_key = headers.delete(:api_key)
|
32
|
+
api_base = headers.delete(:api_base)
|
33
|
+
|
34
|
+
response, opts = Scribesend.request(method, url, api_key, params, headers, api_base)
|
35
|
+
[response, opts]
|
36
|
+
end
|
37
|
+
|
38
|
+
def refresh
|
39
|
+
response, opts = self.class.request(:get, url, @retrieve_params)
|
40
|
+
refresh_from(response, opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.retrieve(id, opts={})
|
44
|
+
instance = self.new(id, opts)
|
45
|
+
instance.refresh
|
46
|
+
instance
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.all(filters={}, opts={})
|
50
|
+
response, opts = self.request(:get, url, filters, opts)
|
51
|
+
Util.convert_to_scribesend_object(response, opts)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.create(params={}, opts={})
|
55
|
+
response, opts = self.request(:post, url, params, opts)
|
56
|
+
Util.convert_to_scribesend_object(response, opts)
|
57
|
+
end
|
58
|
+
|
59
|
+
# def delete(params={}, opts={})
|
60
|
+
# response, opts = self.class.request(:delete, url, params, opts)
|
61
|
+
# refresh_from(response, opts)
|
62
|
+
# self
|
63
|
+
# end
|
64
|
+
|
65
|
+
def save(params={})
|
66
|
+
values = self.class.serialize_params(self).merge(params)
|
67
|
+
|
68
|
+
if values.length > 0
|
69
|
+
values.delete(:id)
|
70
|
+
|
71
|
+
response, opts = self.class.request(:post, url, values)
|
72
|
+
refresh_from(response, opts)
|
73
|
+
end
|
74
|
+
self
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Scribesend
|
2
|
+
class Entry < APIResource
|
3
|
+
def self.create(params={}, opts={})
|
4
|
+
opts = {
|
5
|
+
:content_type => 'application/json'
|
6
|
+
}.merge(opts)
|
7
|
+
|
8
|
+
response, opts = self.request(:post, url, params.to_json, opts)
|
9
|
+
Util.convert_to_scribesend_object(response, opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.create_charge_entry(params, opts={})
|
13
|
+
response, opts = self.request(:post, url + "/charge", params, opts)
|
14
|
+
Util.convert_to_scribesend_object(response, opts)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create_capture_entry(params, opts={})
|
18
|
+
response, opts = self.request(:post, url + "/capture", params, opts)
|
19
|
+
Util.convert_to_scribesend_object(response, opts)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.create_charge_and_capture_entry(params, opts={})
|
23
|
+
response, opts = self.request(:post, url + "/charge_and_capture", params, opts)
|
24
|
+
Util.convert_to_scribesend_object(response, opts)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Scribesend
|
2
|
+
class EntryLine < APIResource
|
3
|
+
def self.url
|
4
|
+
return nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def url
|
8
|
+
return nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.retrieve(id, opts=nil)
|
12
|
+
raise NotImplementedError.new("Entry_lines cannot be retrieved individually. Retrieve an entry instead.")
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.all
|
16
|
+
raise NotImplementedError.new("Entry_lines cannot be retrieved individually. Retrieve an entry instead.")
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.create
|
20
|
+
raise NotImplementedError.new("Entry_lines cannot be created outside of an entry.")
|
21
|
+
end
|
22
|
+
|
23
|
+
def save
|
24
|
+
raise NotImplementedError.new("Entry_lines cannot be updated within an entry. " \
|
25
|
+
"If you want to make changes to an entry_line, you will need to create " \
|
26
|
+
"a new entry that adjusts the corresponding accounts and values. " \
|
27
|
+
"See https://scribesend.com/docs for more details.")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Scribesend
|
2
|
+
class Error < StandardError
|
3
|
+
attr_reader :message, :http_status, :http_body, :json_body
|
4
|
+
|
5
|
+
def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
|
6
|
+
@message = message
|
7
|
+
@http_status = http_status
|
8
|
+
@http_body = http_body
|
9
|
+
@json_body = json_body
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
s = @http_status.nil? ? "" : "(Status #{@http_status}): "
|
14
|
+
"#{s}#{@message}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Scribesend
|
2
|
+
class ListObject < ScribesendObject
|
3
|
+
def [](k)
|
4
|
+
case k
|
5
|
+
when String, Symbol
|
6
|
+
super
|
7
|
+
else
|
8
|
+
raise ArgumentError.new("You tried to access the #{k.inspect} index, but ListObject types only support String keys. (HINT: List calls return an object with a 'data' (which is the data array). You likely want to call #data[#{k.inspect}])")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&blk)
|
13
|
+
self.data.each(&blk)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
module Scribesend
|
2
|
+
class ScribesendObject
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
@@immutable_values = Set.new([:id])
|
6
|
+
|
7
|
+
# The default :id method is deprecated and isn't useful to us
|
8
|
+
if method_defined?(:id)
|
9
|
+
undef :id
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(id=nil, opts={})
|
13
|
+
if id.kind_of?(Hash)
|
14
|
+
@retrieve_params = id.dup
|
15
|
+
@retrieve_params.delete(:id)
|
16
|
+
id = id[:id]
|
17
|
+
else
|
18
|
+
@retrieve_params = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
@opts = opts
|
22
|
+
@values = {}
|
23
|
+
@unsaved_values = Set.new
|
24
|
+
@transient_values = Set.new
|
25
|
+
@values[:id] = id if id
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.construct_from(values, opts={})
|
29
|
+
self.new(values[:id]).refresh_from(values, opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s(*args)
|
33
|
+
JSON.pretty_generate(@values)
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
|
38
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
|
39
|
+
end
|
40
|
+
|
41
|
+
def refresh_from(values, opts, partial=false)
|
42
|
+
@opts = opts
|
43
|
+
@original_values = Marshal.load(Marshal.dump(values))
|
44
|
+
removed = partial ? Set.new : Set.new(@values.keys - values.keys)
|
45
|
+
added = Set.new(values.keys - @values.keys)
|
46
|
+
|
47
|
+
instance_eval do
|
48
|
+
remove_accessors(removed)
|
49
|
+
add_accessors(added)
|
50
|
+
end
|
51
|
+
removed.each do |k|
|
52
|
+
@values.delete(k)
|
53
|
+
@transient_values.add(k)
|
54
|
+
@unsaved_values.delete(k)
|
55
|
+
end
|
56
|
+
values.each do |k, v|
|
57
|
+
@values[k] = Util.convert_to_scribesend_object(v, @opts)
|
58
|
+
@transient_values.delete(k)
|
59
|
+
@unsaved_values.delete(k)
|
60
|
+
end
|
61
|
+
|
62
|
+
return self
|
63
|
+
end
|
64
|
+
|
65
|
+
def [](k)
|
66
|
+
@values[k.to_sym]
|
67
|
+
end
|
68
|
+
|
69
|
+
def []=(k, v)
|
70
|
+
send(:"#{k}=", v)
|
71
|
+
end
|
72
|
+
|
73
|
+
def keys
|
74
|
+
@values.keys
|
75
|
+
end
|
76
|
+
|
77
|
+
def values
|
78
|
+
@values.values
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_json(*a)
|
82
|
+
JSON.generate(@values)
|
83
|
+
end
|
84
|
+
|
85
|
+
def as_json(*a)
|
86
|
+
@values.as_json(*a)
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_hash
|
90
|
+
@values.inject({}) do |acc, (key, value)|
|
91
|
+
acc[key] = value.respond_to?(:to_hash) ? value.to_hash : value
|
92
|
+
acc
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def each(&blk)
|
97
|
+
@values.each(&blk)
|
98
|
+
end
|
99
|
+
|
100
|
+
def serialize_nested_object(key)
|
101
|
+
new_value = @values[key]
|
102
|
+
if new_value.is_a?(APIResource)
|
103
|
+
return {}
|
104
|
+
end
|
105
|
+
|
106
|
+
if @unsaved_values.include?(key)
|
107
|
+
# the object has been reassigned
|
108
|
+
# e.g. as object.key = {foo => bar}
|
109
|
+
update = new_value
|
110
|
+
new_keys = update.keys.map(&:to_sym)
|
111
|
+
|
112
|
+
# remove keys at the server, but not known locally
|
113
|
+
if @original_values.include?(key)
|
114
|
+
keys_to_unset = @original_values[key].keys - new_keys
|
115
|
+
keys_to_unset.each {|key| update[key] = ''}
|
116
|
+
end
|
117
|
+
|
118
|
+
update
|
119
|
+
else
|
120
|
+
# can be serialized normally
|
121
|
+
self.class.serialize_params(new_value)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.serialize_params(obj, original_value=nil)
|
126
|
+
case obj
|
127
|
+
when nil
|
128
|
+
''
|
129
|
+
when ScribesendObject
|
130
|
+
unsaved_keys = obj.instance_variable_get(:@unsaved_values)
|
131
|
+
obj_values = obj.instance_variable_get(:@values)
|
132
|
+
update_hash = {}
|
133
|
+
|
134
|
+
unsaved_keys.each do |k|
|
135
|
+
update_hash[k] = serialize_params(obj_values[k])
|
136
|
+
end
|
137
|
+
|
138
|
+
obj_values.each do |k, v|
|
139
|
+
if v.is_a?(ScribesendObject) || v.is_a?(Hash)
|
140
|
+
update_hash[k] = obj.serialize_nested_object(k)
|
141
|
+
elsif v.is_a?(Array)
|
142
|
+
original_value = obj.instance_variable_get(:@original_values)[k]
|
143
|
+
if original_value && original_value.length > v.length
|
144
|
+
# url params provide no mechanism for deleting an item in an array,
|
145
|
+
# just overwriting the whole array or adding new items. So let's not
|
146
|
+
# allow deleting without a full overwrite until we have a solution.
|
147
|
+
raise ArgumentError.new(
|
148
|
+
"You cannot delete an item from an array, you must instead set a new array"
|
149
|
+
)
|
150
|
+
end
|
151
|
+
update_hash[k] = serialize_params(v, original_value)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
update_hash
|
156
|
+
when Array
|
157
|
+
update_hash = {}
|
158
|
+
obj.each_with_index do |value, index|
|
159
|
+
update = serialize_params(value)
|
160
|
+
if update != {} && (!original_value || update != original_value[index])
|
161
|
+
update_hash[index] = update
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
if update_hash == {}
|
166
|
+
nil
|
167
|
+
else
|
168
|
+
update_hash
|
169
|
+
end
|
170
|
+
else
|
171
|
+
obj
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
def metaclass
|
178
|
+
class << self; self; end
|
179
|
+
end
|
180
|
+
|
181
|
+
def remove_accessors(keys)
|
182
|
+
metaclass.instance_eval do
|
183
|
+
keys.each do |k|
|
184
|
+
next if @@immutable_values.include?(k)
|
185
|
+
k_eq = :"#{k}="
|
186
|
+
remove_method(k) if method_defined?(k)
|
187
|
+
remove_method(k_eq) if method_defined?(k_eq)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def add_accessors(keys)
|
193
|
+
metaclass.instance_eval do
|
194
|
+
keys.each do |k|
|
195
|
+
next if @@immutable_values.include?(k)
|
196
|
+
k_eq = :"#{k}="
|
197
|
+
define_method(k) { @values[k] }
|
198
|
+
define_method(k_eq) do |v|
|
199
|
+
if v == ""
|
200
|
+
raise ArgumentError.new(
|
201
|
+
"You cannot set #{k} to an empty string." \
|
202
|
+
"We interpret empty strings as nil in requests." \
|
203
|
+
"You may set #{self}.#{k} = nil to delete the property.")
|
204
|
+
end
|
205
|
+
@values[k] = v
|
206
|
+
@unsaved_values.add(k)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def method_missing(name, *args)
|
213
|
+
if name.to_s.end_with?('=')
|
214
|
+
attr = name.to_s[0...-1].to_sym
|
215
|
+
add_accessors([attr])
|
216
|
+
begin
|
217
|
+
mth = method(name)
|
218
|
+
rescue NameError
|
219
|
+
raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
|
220
|
+
end
|
221
|
+
return mth.call(args[0])
|
222
|
+
else
|
223
|
+
return @values[name] if @values.has_key?(name)
|
224
|
+
end
|
225
|
+
|
226
|
+
begin
|
227
|
+
super
|
228
|
+
rescue NoMethodError => e
|
229
|
+
if @transient_values.include?(name)
|
230
|
+
raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Scribesend's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
|
231
|
+
else
|
232
|
+
raise
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def respond_to_missing?(symbol, include_private = false)
|
238
|
+
@values && @values.has_key?(symbol) || super
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Scribesend
|
2
|
+
module Util
|
3
|
+
def self.objects_to_ids(h)
|
4
|
+
case h
|
5
|
+
when APIResource
|
6
|
+
h.id
|
7
|
+
when Hash
|
8
|
+
res = {}
|
9
|
+
h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
|
10
|
+
res
|
11
|
+
when Array
|
12
|
+
h.map { |v| objects_to_ids(v) }
|
13
|
+
else
|
14
|
+
h
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.object_classes
|
19
|
+
@object_classes ||= {
|
20
|
+
'list' => ListObject,
|
21
|
+
|
22
|
+
'account' => Account,
|
23
|
+
'entry' => Entry,
|
24
|
+
'entry_line' => EntryLine
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.convert_to_scribesend_object(resp, opts)
|
29
|
+
case resp
|
30
|
+
when Array
|
31
|
+
resp.map { |i| convert_to_scribesend_object(i, opts) }
|
32
|
+
when Hash
|
33
|
+
# Convert to a known object class, or ScribesendObject if not found
|
34
|
+
object_classes.fetch(resp[:object], ScribesendObject).construct_from(resp, opts)
|
35
|
+
else
|
36
|
+
resp
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.symbolize_names(object)
|
41
|
+
case object
|
42
|
+
when Hash
|
43
|
+
new_hash = {}
|
44
|
+
object.each do |key, value|
|
45
|
+
key = (key.to_sym rescue key) || key
|
46
|
+
new_hash[key] = symbolize_names(value)
|
47
|
+
end
|
48
|
+
new_hash
|
49
|
+
when Array
|
50
|
+
object.map { |value| symbolize_names(value) }
|
51
|
+
else
|
52
|
+
object
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.url_encode(key)
|
57
|
+
URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.flatten_params(params, parent_key=nil)
|
61
|
+
result = []
|
62
|
+
params.each do |key, value|
|
63
|
+
calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
|
64
|
+
if value.is_a?(Hash)
|
65
|
+
result += flatten_params(value, calculated_key)
|
66
|
+
elsif value.is_a?(Array)
|
67
|
+
result += flatten_params_array(value, calculated_key)
|
68
|
+
else
|
69
|
+
result << [calculated_key, value]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.flatten_params_array(value, calculated_key)
|
76
|
+
result = []
|
77
|
+
value.each do |elem|
|
78
|
+
if elem.is_a?(Hash)
|
79
|
+
result += flatten_params(elem, calculated_key)
|
80
|
+
elsif elem.is_a?(Array)
|
81
|
+
result += flatten_params_array(elem, calculated_key)
|
82
|
+
else
|
83
|
+
result << ["#{calculated_key}[]", elem]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
result
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/scribesend.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# Scribesend Ruby bindings (https://scribesend.com/docs)
|
2
|
+
|
3
|
+
# Third-party libraries
|
4
|
+
require 'cgi'
|
5
|
+
require 'set'
|
6
|
+
require 'rest-client'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
# API Resources
|
10
|
+
require 'scribesend/version'
|
11
|
+
require 'scribesend/util'
|
12
|
+
require 'scribesend/scribesend_object'
|
13
|
+
require 'scribesend/api_resource'
|
14
|
+
require 'scribesend/list_object'
|
15
|
+
require 'scribesend/account'
|
16
|
+
require 'scribesend/entry'
|
17
|
+
require 'scribesend/entry_line'
|
18
|
+
|
19
|
+
# Errors
|
20
|
+
require 'scribesend/error'
|
21
|
+
|
22
|
+
module Scribesend
|
23
|
+
@api_key = nil
|
24
|
+
@api_base = "https://api.scribesend.com/v0"
|
25
|
+
|
26
|
+
def self.api_url(url='', api_base_url=nil)
|
27
|
+
(api_base_url || @api_base) + url
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.api_key=(api_key)
|
31
|
+
@api_key = api_key
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.api_key
|
35
|
+
@api_key
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.api_base
|
39
|
+
@api_base
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.request(method, url, api_key, params={}, headers={}, api_base_url=nil)
|
43
|
+
api_base_url = api_base_url || @api_base
|
44
|
+
|
45
|
+
unless api_key ||= @api_key
|
46
|
+
raise Error.new("No API key provided. " \
|
47
|
+
"Set your API key using 'Scribesend.api_key = <API-KEY>'.")
|
48
|
+
end
|
49
|
+
|
50
|
+
params = Util.objects_to_ids(params)
|
51
|
+
url = api_url(url, api_base_url)
|
52
|
+
|
53
|
+
case method.to_s.downcase.to_sym
|
54
|
+
when :get, :head, :delete
|
55
|
+
# Make params into GET parameters
|
56
|
+
url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
|
57
|
+
payload = nil
|
58
|
+
else
|
59
|
+
if headers[:content_type] && (headers[:content_type] == "multipart/form-data" || headers[:content_type] == "application/json")
|
60
|
+
payload = params
|
61
|
+
else
|
62
|
+
payload = uri_encode(params)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if headers[:content_type] && headers[:content_type] == "application/json"
|
67
|
+
content_type = headers[:content_type]
|
68
|
+
else
|
69
|
+
content_type = "application/x-www-form-urlencoded"
|
70
|
+
end
|
71
|
+
|
72
|
+
headers = {
|
73
|
+
:user_agent => "Scribesend/v0 RubyClient/#{Scribesend::VERSION}",
|
74
|
+
:authorization => "Bearer #{api_key}",
|
75
|
+
:content_type => content_type
|
76
|
+
}.update(headers)
|
77
|
+
|
78
|
+
request_opts = {
|
79
|
+
:method => method,
|
80
|
+
:headers => headers,
|
81
|
+
:url => url,
|
82
|
+
:payload => payload,
|
83
|
+
:verify_ssl => false,
|
84
|
+
:open_timeout => 30,
|
85
|
+
:timeout => 80,
|
86
|
+
}
|
87
|
+
|
88
|
+
begin
|
89
|
+
response = execute_request(request_opts)
|
90
|
+
rescue SocketError => e
|
91
|
+
handle_restclient_error(e, api_base_url)
|
92
|
+
rescue NoMethodError => e
|
93
|
+
# Work around RestClient bug
|
94
|
+
if e.message =~ /\WRequestFailed\W/
|
95
|
+
e = Error.new('Unexpected HTTP response code')
|
96
|
+
handle_restclient_error(e, api_base_url)
|
97
|
+
else
|
98
|
+
raise
|
99
|
+
end
|
100
|
+
rescue RestClient::ExceptionWithResponse => e
|
101
|
+
if rcode = e.http_code and rbody = e.http_body
|
102
|
+
handle_api_error(rcode, rbody)
|
103
|
+
else
|
104
|
+
handle_restclient_error(e, api_base_url)
|
105
|
+
end
|
106
|
+
rescue RestClient::Exception, Errno::ECONNREFUSED => e
|
107
|
+
handle_restclient_error(e, api_base_url)
|
108
|
+
end
|
109
|
+
|
110
|
+
[parse(response), api_key]
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
def self.uri_encode(params)
|
116
|
+
Util.flatten_params(params).
|
117
|
+
map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.execute_request(opts)
|
121
|
+
RestClient::Request.execute(opts)
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.parse(response)
|
125
|
+
begin
|
126
|
+
response = JSON.parse(response.body)
|
127
|
+
rescue JSON::ParserError
|
128
|
+
raise Error.new('Unexpected API response', response.code, response.body)
|
129
|
+
end
|
130
|
+
|
131
|
+
Util.symbolize_names(response)
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.handle_api_error(rcode, rbody)
|
135
|
+
begin
|
136
|
+
error_obj = JSON.parse(rbody)
|
137
|
+
error_obj = Util.symbolize_names(error_obj)
|
138
|
+
error = error_obj[:error][:message] or raise Error.new # escape from parsing
|
139
|
+
rescue JSON::ParserError, Error
|
140
|
+
raise Error.new("Invalid response object from API: #{rbody.inspect} " +
|
141
|
+
"(HTTP response code was #{rcode})", rcode, rbody)
|
142
|
+
end
|
143
|
+
|
144
|
+
raise Error.new(error, rcode, rbody, error_obj)
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.handle_restclient_error(e, api_base_url=nil)
|
148
|
+
api_base_url = @api_base unless api_base_url
|
149
|
+
connection_message = "Please check your internet connection and try again. " \
|
150
|
+
"If this problem persists, let us know at team@scribesend.com."
|
151
|
+
|
152
|
+
case e
|
153
|
+
when RestClient::RequestTimeout
|
154
|
+
message = "Could not connect to Scribesend (#{api_base_url}). #{connection_message}"
|
155
|
+
|
156
|
+
when RestClient::ServerBrokeConnection
|
157
|
+
message = "The connection to the server (#{api_base_url}) broke before the " \
|
158
|
+
"request completed. #{connection_message}"
|
159
|
+
|
160
|
+
when SocketError
|
161
|
+
message = "Unexpected error communicating when trying to connect to Scribesend. " \
|
162
|
+
"You may be seeing this message because your DNS is not working. " \
|
163
|
+
"To check, try running 'host scribesend.com' from the command line."
|
164
|
+
|
165
|
+
else
|
166
|
+
message = "Unexpected error communicating with Scribesend. " \
|
167
|
+
"If this problem persists, let us know at team@scribesend.com."
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
raise Error.new(message + "\n\n(Network error: #{e.message})")
|
172
|
+
end
|
173
|
+
end
|
data/scribesend.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
2
|
+
|
3
|
+
require 'scribesend/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'scribesend'
|
7
|
+
spec.version = Scribesend::VERSION
|
8
|
+
spec.date = Time.now.strftime("%Y-%m-%d")
|
9
|
+
spec.summary = 'Ruby bindings for Scribesend'
|
10
|
+
spec.description = 'Scribesend builds APIs for accounting and business management: https://scribesend.com'
|
11
|
+
spec.authors = ['Frank Wu']
|
12
|
+
spec.email = 'frank@scribesend.com'
|
13
|
+
spec.homepage = 'https://www.scribesend.com/docs'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.add_dependency 'rest-client', '~> 1.4'
|
17
|
+
spec.add_dependency 'mime-types', '>= 1.25', '< 3.0'
|
18
|
+
spec.add_dependency 'json', '~> 1.8.1'
|
19
|
+
|
20
|
+
spec.files = `git ls-files`.split("\n")
|
21
|
+
spec.test_files = `git ls-files -- test/*`.split("\n")
|
22
|
+
spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scribesend
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Frank Wu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rest-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mime-types
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.25'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '3.0'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.25'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: json
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.8.1
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.8.1
|
61
|
+
description: 'Scribesend builds APIs for accounting and business management: https://scribesend.com'
|
62
|
+
email: frank@scribesend.com
|
63
|
+
executables:
|
64
|
+
- scribesend-console
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- ".gitignore"
|
69
|
+
- CHANGELOG
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE
|
72
|
+
- README.markdown
|
73
|
+
- VERSION
|
74
|
+
- bin/scribesend-console
|
75
|
+
- lib/scribesend.rb
|
76
|
+
- lib/scribesend/account.rb
|
77
|
+
- lib/scribesend/api_resource.rb
|
78
|
+
- lib/scribesend/entry.rb
|
79
|
+
- lib/scribesend/entry_line.rb
|
80
|
+
- lib/scribesend/error.rb
|
81
|
+
- lib/scribesend/list_object.rb
|
82
|
+
- lib/scribesend/scribesend_object.rb
|
83
|
+
- lib/scribesend/util.rb
|
84
|
+
- lib/scribesend/version.rb
|
85
|
+
- scribesend.gemspec
|
86
|
+
homepage: https://www.scribesend.com/docs
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.4.5
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Ruby bindings for Scribesend
|
110
|
+
test_files: []
|