octobat 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.travis.yml +16 -0
  4. data/CONTRIBUTORS +1 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +23 -0
  7. data/History.txt +4 -0
  8. data/LICENSE +21 -0
  9. data/README.rdoc +41 -0
  10. data/Rakefile +7 -0
  11. data/VERSION +1 -0
  12. data/bin/octobat-console +7 -0
  13. data/gemfiles/default-with-activesupport.gemfile +10 -0
  14. data/gemfiles/json.gemfile +12 -0
  15. data/gemfiles/yajl.gemfile +12 -0
  16. data/lib/data/ca-certificates.crt +5165 -0
  17. data/lib/octobat.rb +286 -0
  18. data/lib/octobat/api_operations/create.rb +17 -0
  19. data/lib/octobat/api_operations/delete.rb +11 -0
  20. data/lib/octobat/api_operations/list.rb +17 -0
  21. data/lib/octobat/api_operations/update.rb +57 -0
  22. data/lib/octobat/api_resource.rb +32 -0
  23. data/lib/octobat/certificate_blacklist.rb +55 -0
  24. data/lib/octobat/credit_note_numbering_sequence.rb +5 -0
  25. data/lib/octobat/customer.rb +7 -0
  26. data/lib/octobat/errors/api_connection_error.rb +4 -0
  27. data/lib/octobat/errors/api_error.rb +4 -0
  28. data/lib/octobat/errors/authentication_error.rb +4 -0
  29. data/lib/octobat/errors/invalid_request_error.rb +10 -0
  30. data/lib/octobat/errors/octobat_error.rb +20 -0
  31. data/lib/octobat/invoice.rb +17 -0
  32. data/lib/octobat/invoice_item.rb +4 -0
  33. data/lib/octobat/list_object.rb +37 -0
  34. data/lib/octobat/numbering_sequence.rb +5 -0
  35. data/lib/octobat/octobat_object.rb +190 -0
  36. data/lib/octobat/payment.rb +4 -0
  37. data/lib/octobat/payment_mode.rb +7 -0
  38. data/lib/octobat/singleton_api_resource.rb +20 -0
  39. data/lib/octobat/util.rb +144 -0
  40. data/lib/octobat/version.rb +3 -0
  41. data/octobat.gemspec +28 -0
  42. metadata +134 -0
@@ -0,0 +1,5 @@
1
+ module Octobat
2
+ class CreditNoteNumberingSequence < APIResource
3
+ include Octobat::APIOperations::List
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module Octobat
2
+ class Customer < APIResource
3
+ include Octobat::APIOperations::List
4
+ include Octobat::APIOperations::Create
5
+ include Octobat::APIOperations::Update
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module Octobat
2
+ class APIConnectionError < OctobatError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Octobat
2
+ class APIError < OctobatError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Octobat
2
+ class AuthenticationError < OctobatError
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Octobat
2
+ class InvalidRequestError < OctobatError
3
+ attr_accessor :param
4
+
5
+ def initialize(message, param, http_status=nil, http_body=nil, json_body=nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Octobat
2
+ class OctobatError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_status
5
+ attr_reader :http_body
6
+ attr_reader :json_body
7
+
8
+ def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
9
+ @message = message
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ @json_body = json_body
13
+ end
14
+
15
+ def to_s
16
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
17
+ "#{status_string}#{@message}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module Octobat
2
+ class Invoice < APIResource
3
+ include Octobat::APIOperations::List
4
+ include Octobat::APIOperations::Create
5
+
6
+ def pay(payment_data)
7
+ response, api_key = Octobat.request(:patch, pay_url, @api_key, {payment: payment_data})
8
+ refresh_from(response, api_key)
9
+ end
10
+
11
+ private
12
+
13
+ def pay_url
14
+ url + '/pay'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ module Octobat
2
+ class InvoiceItem < APIResource
3
+ end
4
+ end
@@ -0,0 +1,37 @@
1
+ module Octobat
2
+ class ListObject < OctobatObject
3
+
4
+ def [](k)
5
+ case k
6
+ when String, Symbol
7
+ super
8
+ else
9
+ raise ArgumentError.new("You tried to access the #{k.inspect} index, but ListObject types only support Octobat keys. (HINT: List calls return an object with a 'data' (which is the data array). You likely want to call #data[#{k.inspect}])")
10
+ end
11
+ end
12
+
13
+ def each(&blk)
14
+ self.data.each(&blk)
15
+ end
16
+
17
+ def retrieve(id, api_key=nil)
18
+ api_key ||= @api_key
19
+ response, api_key = Octobat.request(:get,"#{url}/#{CGI.escape(id)}", api_key)
20
+ Util.convert_to_octobat_object(response, api_key)
21
+ end
22
+
23
+ def create(params={}, opts={})
24
+ api_key, headers = Util.parse_opts(opts)
25
+ api_key ||= @api_key
26
+ response, api_key = Octobat.request(:post, url, api_key, params, headers)
27
+ Util.convert_to_octobat_object(response, api_key)
28
+ end
29
+
30
+ def all(params={}, opts={})
31
+ api_key, headers = Util.parse_opts(opts)
32
+ api_key ||= @api_key
33
+ response, api_key = Octobat.request(:get, url, api_key, params, headers)
34
+ Util.convert_to_octobat_object(response, api_key)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ module Octobat
2
+ class NumberingSequence < APIResource
3
+ include Octobat::APIOperations::List
4
+ end
5
+ end
@@ -0,0 +1,190 @@
1
+ module Octobat
2
+ class OctobatObject
3
+ include Enumerable
4
+
5
+ attr_accessor :api_key
6
+ @@permanent_attributes = Set.new([:api_key, :id])
7
+
8
+ # The default :id method is deprecated and isn't useful to us
9
+ if method_defined?(:id)
10
+ undef :id
11
+ end
12
+
13
+ def initialize(id=nil, api_key=nil)
14
+ # parameter overloading!
15
+ if id.kind_of?(Hash)
16
+ @retrieve_options = id.dup
17
+ @retrieve_options.delete(:id)
18
+ id = id[:id]
19
+ else
20
+ @retrieve_options = {}
21
+ end
22
+
23
+ @api_key = api_key
24
+ @values = {}
25
+ # This really belongs in APIResource, but not putting it there allows us
26
+ # to have a unified inspect method
27
+ @unsaved_values = Set.new
28
+ @transient_values = Set.new
29
+ @values[:id] = id if id
30
+ end
31
+
32
+ def self.construct_from(values, api_key=nil)
33
+ self.new(values[:id], api_key).refresh_from(values, api_key)
34
+ end
35
+
36
+ def to_s(*args)
37
+ JSON.pretty_generate(@values)
38
+ end
39
+
40
+ def inspect
41
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
42
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
43
+ end
44
+
45
+ def refresh_from(values, api_key, partial=false)
46
+ @api_key = api_key
47
+
48
+ @previous_metadata = values[:metadata]
49
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
50
+ added = Set.new(values.keys - @values.keys)
51
+ # Wipe old state before setting new. This is useful for e.g. updating a
52
+ # customer, where there is no persistent card parameter. Mark those values
53
+ # which don't persist as transient
54
+
55
+ instance_eval do
56
+ remove_accessors(removed)
57
+ add_accessors(added)
58
+ end
59
+ removed.each do |k|
60
+ @values.delete(k)
61
+ @transient_values.add(k)
62
+ @unsaved_values.delete(k)
63
+ end
64
+ values.each do |k, v|
65
+ @values[k] = Util.convert_to_octobat_object(v, api_key)
66
+ @transient_values.delete(k)
67
+ @unsaved_values.delete(k)
68
+ end
69
+
70
+ return self
71
+ end
72
+
73
+ def [](k)
74
+ @values[k.to_sym]
75
+ end
76
+
77
+ def []=(k, v)
78
+ send(:"#{k}=", v)
79
+ end
80
+
81
+ def keys
82
+ @values.keys
83
+ end
84
+
85
+ def values
86
+ @values.values
87
+ end
88
+
89
+ def to_json(*a)
90
+ JSON.generate(@values)
91
+ end
92
+
93
+ def as_json(*a)
94
+ @values.as_json(*a)
95
+ end
96
+
97
+ def to_hash
98
+ @values.inject({}) do |acc, (key, value)|
99
+ acc[key] = value.respond_to?(:to_hash) ? value.to_hash : value
100
+ acc
101
+ end
102
+ end
103
+
104
+ def each(&blk)
105
+ @values.each(&blk)
106
+ end
107
+
108
+ def _dump(level)
109
+ Marshal.dump([@values, @api_key])
110
+ end
111
+
112
+ def self._load(args)
113
+ values, api_key = Marshal.load(args)
114
+ construct_from(values, api_key)
115
+ end
116
+
117
+ if RUBY_VERSION < '1.9.2'
118
+ def respond_to?(symbol)
119
+ @values.has_key?(symbol) || super
120
+ end
121
+ end
122
+
123
+ protected
124
+
125
+ def metaclass
126
+ class << self; self; end
127
+ end
128
+
129
+ def remove_accessors(keys)
130
+ metaclass.instance_eval do
131
+ keys.each do |k|
132
+ next if @@permanent_attributes.include?(k)
133
+ k_eq = :"#{k}="
134
+ remove_method(k) if method_defined?(k)
135
+ remove_method(k_eq) if method_defined?(k_eq)
136
+ end
137
+ end
138
+ end
139
+
140
+ def add_accessors(keys)
141
+ metaclass.instance_eval do
142
+ keys.each do |k|
143
+ next if @@permanent_attributes.include?(k)
144
+ k_eq = :"#{k}="
145
+ define_method(k) { @values[k] }
146
+ define_method(k_eq) do |v|
147
+ if v == ""
148
+ raise ArgumentError.new(
149
+ "You cannot set #{k} to an empty string." \
150
+ "We interpret empty strings as nil in requests." \
151
+ "You may set #{self}.#{k} = nil to delete the property.")
152
+ end
153
+ @values[k] = v
154
+ @unsaved_values.add(k)
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ def method_missing(name, *args)
161
+ # TODO: only allow setting in updateable classes.
162
+ if name.to_s.end_with?('=')
163
+ attr = name.to_s[0...-1].to_sym
164
+ add_accessors([attr])
165
+ begin
166
+ mth = method(name)
167
+ rescue NameError
168
+ raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
169
+ end
170
+ return mth.call(args[0])
171
+ else
172
+ return @values[name] if @values.has_key?(name)
173
+ end
174
+
175
+ begin
176
+ super
177
+ rescue NoMethodError => e
178
+ if @transient_values.include?(name)
179
+ 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 Octobat's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
180
+ else
181
+ raise
182
+ end
183
+ end
184
+ end
185
+
186
+ def respond_to_missing?(symbol, include_private = false)
187
+ @values && @values.has_key?(symbol) || super
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,4 @@
1
+ module Octobat
2
+ class Payment < APIResource
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module Octobat
2
+ class PaymentMode < APIResource
3
+ include Octobat::APIOperations::List
4
+ include Octobat::APIOperations::Create
5
+ include Octobat::APIOperations::Update
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ module Octobat
2
+ class SingletonAPIResource < APIResource
3
+ def self.url
4
+ if self == SingletonAPIResource
5
+ raise NotImplementedError.new('SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Account, etc.)')
6
+ end
7
+ "/v1/#{CGI.escape(class_name.downcase)}"
8
+ end
9
+
10
+ def url
11
+ self.class.url
12
+ end
13
+
14
+ def self.retrieve(api_key=nil)
15
+ instance = self.new(nil, api_key)
16
+ instance.refresh
17
+ instance
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,144 @@
1
+ module Octobat
2
+ module Util
3
+ def self.expand_nested_objects(h)
4
+ case h
5
+ when Hash
6
+ res = {}
7
+ h.each { |k, v| res[k] = expand_nested_objects(v) unless v.nil? }
8
+ res
9
+ when Array
10
+ res = {'' => h.map{|v| expand_nested_objects(v)}}
11
+ res
12
+ else
13
+ h
14
+ end
15
+ end
16
+
17
+ def self.objects_to_ids(h)
18
+ case h
19
+ when APIResource
20
+ h.id
21
+ when Hash
22
+ res = {}
23
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
24
+ res
25
+ when Array
26
+ h.map { |v| objects_to_ids(v) }
27
+ else
28
+ h
29
+ end
30
+ end
31
+
32
+ def self.object_classes
33
+ @object_classes ||= {
34
+ # data structures
35
+ 'list' => ListObject,
36
+
37
+ # business objects
38
+ 'payment_mode' => PaymentMode,
39
+ 'payment' => Payment,
40
+ 'numbering_sequence' => NumberingSequence,
41
+ 'credit_note_numbering_sequence' => CreditNoteNumberingSequence,
42
+ 'invoice' => Invoice,
43
+ 'invoice_item' => InvoiceItem,
44
+ 'customer' => Customer
45
+ }
46
+ end
47
+
48
+ def self.convert_to_octobat_object(resp, api_key)
49
+ case resp
50
+ when Array
51
+ resp.map { |i| convert_to_octobat_object(i, api_key) }
52
+ when Hash
53
+ # Try converting to a known object class. If none available, fall back to generic OctobatObject
54
+ object_classes.fetch(resp[:object], OctobatObject).construct_from(resp, api_key)
55
+ else
56
+ resp
57
+ end
58
+ end
59
+
60
+ def self.file_readable(file)
61
+ # This is nominally equivalent to File.readable?, but that can
62
+ # report incorrect results on some more oddball filesystems
63
+ # (such as AFS)
64
+ begin
65
+ File.open(file) { |f| }
66
+ rescue
67
+ false
68
+ else
69
+ true
70
+ end
71
+ end
72
+
73
+ def self.symbolize_names(object)
74
+ case object
75
+ when Hash
76
+ new_hash = {}
77
+ object.each do |key, value|
78
+ key = (key.to_sym rescue key) || key
79
+ new_hash[key] = symbolize_names(value)
80
+ end
81
+ new_hash
82
+ when Array
83
+ object.map { |value| symbolize_names(value) }
84
+ else
85
+ object
86
+ end
87
+ end
88
+
89
+ def self.url_encode(key)
90
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
91
+ end
92
+
93
+ def self.flatten_params(params, parent_key=nil)
94
+ result = []
95
+ params.each do |key, value|
96
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
97
+ if value.is_a?(Hash)
98
+ result += flatten_params(value, calculated_key)
99
+ elsif value.is_a?(Array)
100
+ result += flatten_params_array(value, calculated_key)
101
+ else
102
+ result << [calculated_key, value]
103
+ end
104
+ end
105
+ result
106
+ end
107
+
108
+ def self.flatten_params_array(value, calculated_key)
109
+ result = []
110
+ value.each do |elem|
111
+ if elem.is_a?(Hash)
112
+ result += flatten_params(elem, calculated_key)
113
+ elsif elem.is_a?(Array)
114
+ result += flatten_params_array(elem, calculated_key)
115
+ else
116
+ result << ["#{calculated_key}[]", elem]
117
+ end
118
+ end
119
+ result
120
+ end
121
+
122
+ # The secondary opts argument can either be a string or hash
123
+ # Turn this value into an api_key and a set of headers
124
+ def self.parse_opts(opts)
125
+ case opts
126
+ when NilClass
127
+ return nil, {}
128
+ when String
129
+ return opts, {}
130
+ when Hash
131
+ headers = {}
132
+ if opts[:idempotency_key]
133
+ headers[:idempotency_key] = opts[:idempotency_key]
134
+ end
135
+ if opts[:octobat_account]
136
+ headers[:octobat_account] = opts[:octobat_account]
137
+ end
138
+ return opts[:api_key], headers
139
+ else
140
+ raise TypeError.new("parse_opts expects a string or a hash")
141
+ end
142
+ end
143
+ end
144
+ end