Recharge 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module HappyMapper
2
+ class Attribute < Item; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module HappyMapper
2
+ class Element < Item; end
3
+ end
@@ -0,0 +1,179 @@
1
+ module HappyMapper
2
+ class Item
3
+ attr_accessor :name, :type, :tag, :options, :namespace
4
+
5
+ Types = [String, Float, Time, Date, DateTime, Integer, Boolean]
6
+
7
+ # options:
8
+ # :deep => Boolean False to only parse element's children, True to include
9
+ # grandchildren and all others down the chain (// in expath)
10
+ # :namespace => String Element's namespace if it's not the global or inherited
11
+ # default
12
+ # :parser => Symbol Class method to use for type coercion.
13
+ # :raw => Boolean Use raw node value (inc. tags) when parsing.
14
+ # :single => Boolean False if object should be collection, True for single object
15
+ # :tag => String Element name if it doesn't match the specified name.
16
+ def initialize(name, type, o={})
17
+ self.name = name.to_s
18
+ self.type = type
19
+ self.tag = o.delete(:tag) || name.to_s
20
+ self.options = o
21
+
22
+ @xml_type = self.class.to_s.split('::').last.downcase
23
+ end
24
+
25
+ def constant
26
+ @constant ||= constantize(type)
27
+ end
28
+
29
+ def from_xml_node(node, namespace)
30
+ if primitive?
31
+ find(node, namespace) do |n|
32
+ if n.respond_to?(:content)
33
+ typecast(n.content)
34
+ else
35
+ typecast(n.to_s)
36
+ end
37
+ end
38
+ else
39
+ if options[:parser]
40
+ find(node, namespace) do |n|
41
+ if n.respond_to?(:content) && !options[:raw]
42
+ value = n.content
43
+ else
44
+ value = n.to_s
45
+ end
46
+
47
+ begin
48
+ constant.send(options[:parser].to_sym, value)
49
+ rescue
50
+ nil
51
+ end
52
+ end
53
+ else
54
+ constant.parse(node, options)
55
+ end
56
+ end
57
+ end
58
+
59
+ def xpath(namespace = self.namespace)
60
+ xpath = ''
61
+ xpath += './/' if options[:deep]
62
+ xpath += "#{DEFAULT_NS}:" if namespace
63
+ xpath += tag
64
+ # puts "xpath: #{xpath}"
65
+ xpath
66
+ end
67
+
68
+ def primitive?
69
+ Types.include?(constant)
70
+ end
71
+
72
+ def element?
73
+ @xml_type == 'element'
74
+ end
75
+
76
+ def attribute?
77
+ !element?
78
+ end
79
+
80
+ def method_name
81
+ @method_name ||= name.tr('-', '_')
82
+ end
83
+
84
+ def typecast(value)
85
+ return value if value.kind_of?(constant) || value.nil?
86
+ begin
87
+ if constant == String then value.to_s
88
+ elsif constant == Float then value.to_f
89
+ elsif constant == Time then Time.parse(value.to_s)
90
+ elsif constant == Date then Date.parse(value.to_s)
91
+ elsif constant == DateTime then DateTime.parse(value.to_s)
92
+ elsif constant == Boolean then ['true', 't', '1'].include?(value.to_s.downcase)
93
+ elsif constant == Integer
94
+ # ganked from datamapper
95
+ value_to_i = value.to_i
96
+ if value_to_i == 0 && value != '0'
97
+ value_to_s = value.to_s
98
+ begin
99
+ Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
100
+ rescue ArgumentError
101
+ nil
102
+ end
103
+ else
104
+ value_to_i
105
+ end
106
+ else
107
+ value
108
+ end
109
+ rescue
110
+ value
111
+ end
112
+ end
113
+
114
+ private
115
+ def constantize(type)
116
+ if type.is_a?(String)
117
+ names = type.split('::')
118
+ constant = Object
119
+ names.each do |name|
120
+ constant = constant.const_defined?(name) ?
121
+ constant.const_get(name) :
122
+ constant.const_missing(name)
123
+ end
124
+ constant
125
+ else
126
+ type
127
+ end
128
+ end
129
+
130
+ def find(node, namespace, &block)
131
+ if options[:namespace] == false
132
+ namespace = nil
133
+ elsif options[:namespace]
134
+ # from an element definition
135
+ namespace = "#{DEFAULT_NS}:#{options[:namespace]}"
136
+ elsif self.namespace
137
+ # this node has a custom namespace (that is present in the doc)
138
+ namespace = "#{DEFAULT_NS}:#{self.namespace}"
139
+ end
140
+
141
+ if element?
142
+ if(options[:single].nil? || options[:single])
143
+ result = node.find_first(xpath(namespace), namespace)
144
+ else
145
+ result = node.find(xpath(namespace))
146
+ end
147
+ # puts "vfxn: #{xpath} #{result.inspect}"
148
+ if result
149
+ if(options[:single].nil? || options[:single])
150
+ value = yield(result)
151
+ else
152
+ value = []
153
+
154
+ result.each do |res|
155
+ value << yield(res)
156
+ end
157
+ end
158
+ if options[:attributes].is_a?(Hash)
159
+ result.attributes.each do |xml_attribute|
160
+ if attribute_options = options[:attributes][xml_attribute.name.to_sym]
161
+ attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).from_xml_node(result, namespace)
162
+ result.instance_eval <<-EOV
163
+ def value.#{xml_attribute.name}
164
+ #{attribute_value.inspect}
165
+ end
166
+ EOV
167
+ end
168
+ end
169
+ end
170
+ value
171
+ else
172
+ nil
173
+ end
174
+ else
175
+ yield(node[tag])
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,3 @@
1
+ module HappyMapper
2
+ Version = '0.4.0'
3
+ end
@@ -0,0 +1,315 @@
1
+ require 'rubygems'
2
+ require 'date'
3
+ require 'time'
4
+ require 'xml'
5
+
6
+ class Boolean; end
7
+
8
+ module HappyMapper
9
+
10
+ DEFAULT_NS = "happymapper"
11
+
12
+ def self.included(base)
13
+ base.instance_variable_set("@attributes", {})
14
+ base.instance_variable_set("@elements", {})
15
+ base.instance_variable_set("@registered_namespaces", {})
16
+
17
+ base.extend ClassMethods
18
+ end
19
+
20
+ module ClassMethods
21
+ def attribute(name, type, options={})
22
+ attribute = Attribute.new(name, type, options)
23
+ @attributes[to_s] ||= []
24
+ @attributes[to_s] << attribute
25
+ attr_accessor attribute.method_name.intern
26
+ end
27
+
28
+ def attributes
29
+ @attributes[to_s] || []
30
+ end
31
+
32
+ def element(name, type, options={})
33
+ element = Element.new(name, type, options)
34
+ @elements[to_s] ||= []
35
+ @elements[to_s] << element
36
+ attr_accessor element.method_name.intern
37
+ end
38
+
39
+ def content(name)
40
+ @content = name
41
+ attr_accessor name
42
+ end
43
+
44
+ def after_parse_callbacks
45
+ @after_parse_callbacks ||= []
46
+ end
47
+
48
+ def after_parse(&block)
49
+ after_parse_callbacks.push(block)
50
+ end
51
+
52
+ def elements
53
+ @elements[to_s] || []
54
+ end
55
+
56
+ def has_one(name, type, options={})
57
+ element name, type, {:single => true}.merge(options)
58
+ end
59
+
60
+ def has_many(name, type, options={})
61
+ element name, type, {:single => false}.merge(options)
62
+ end
63
+
64
+ # Specify a namespace if a node and all its children are all namespaced
65
+ # elements. This is simpler than passing the :namespace option to each
66
+ # defined element.
67
+ def namespace(namespace = nil)
68
+ @namespace = namespace if namespace
69
+ @namespace
70
+ end
71
+
72
+ def register_namespace(namespace, ns)
73
+ @registered_namespaces.merge!(namespace => ns)
74
+ end
75
+
76
+ def tag(new_tag_name)
77
+ @tag_name = new_tag_name.to_s
78
+ end
79
+
80
+ def tag_name
81
+ @tag_name ||= to_s.split('::')[-1].downcase
82
+ end
83
+
84
+ def parse(xml, options = {})
85
+ if xml.is_a?(XML::Node)
86
+ node = xml
87
+ else
88
+ if xml.is_a?(XML::Document)
89
+ node = xml.root
90
+ else
91
+ node = XML::Parser.string(xml).parse.root
92
+ end
93
+
94
+ root = node.name == tag_name
95
+ end
96
+
97
+ namespace = @namespace || (node.namespaces && node.namespaces.default)
98
+ namespace = "#{DEFAULT_NS}:#{namespace}" if namespace
99
+
100
+ xpath = root ? '/' : './/'
101
+ xpath += "#{DEFAULT_NS}:" if namespace
102
+ xpath += tag_name
103
+
104
+ nodes = node.find(xpath, Array(namespace))
105
+ collection = nodes.collect do |n|
106
+ obj = new
107
+
108
+ attributes.each do |attr|
109
+ obj.send("#{attr.method_name}=",
110
+ attr.from_xml_node(n, namespace))
111
+ end
112
+
113
+ elements.each do |elem|
114
+ obj.send("#{elem.method_name}=",
115
+ elem.from_xml_node(n, namespace))
116
+ end
117
+
118
+ obj.send("#{@content}=", n.content) if @content
119
+
120
+ obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
121
+
122
+ obj
123
+ end
124
+
125
+ # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
126
+ nodes = nil
127
+
128
+ if options[:single] || root
129
+ collection.first
130
+ else
131
+ collection
132
+ end
133
+ end
134
+
135
+ end
136
+
137
+ #
138
+ # Create an xml representation of the specified class based on defined
139
+ # HappyMapper elements and attributes. The method is defined in a way
140
+ # that it can be called recursively by classes that are also HappyMapper
141
+ # classes, allowg for the composition of classes.
142
+ #
143
+ def to_xml(parent_node = nil, default_namespace = nil)
144
+
145
+ #
146
+ # Create a tag that uses the tag name of the class that has no contents
147
+ # but has the specified namespace or uses the default namespace
148
+ #
149
+ current_node = XML::Node.new(self.class.tag_name)
150
+
151
+
152
+ if parent_node
153
+ #
154
+ # if #to_xml has been called with a parent_node that means this method
155
+ # is being called recursively (or a special case) and we want to return
156
+ # the parent_node with the new node as a child
157
+ #
158
+ parent_node << current_node
159
+ else
160
+ #
161
+ # If #to_xml has been called without a Node (and namespace) that
162
+ # means we want to return an xml document
163
+ #
164
+ write_out_to_xml = true
165
+ end
166
+
167
+ #
168
+ # Add all the registered namespaces to the current node and the current node's
169
+ # root element. Without adding it to the root element it is not possible to
170
+ # parse or use xpath to find elements.
171
+ #
172
+ if self.class.instance_variable_get('@registered_namespaces')
173
+
174
+ # Given a node, continue moving up to parents until there are no more parents
175
+ find_root_node = lambda {|node| while node.parent? ; node = node.parent ; end ; node }
176
+ root_node = find_root_node.call(current_node)
177
+
178
+ # Add the registered namespace to the found root node only if it does not already have one defined
179
+ self.class.instance_variable_get('@registered_namespaces').each_pair do |prefix,href|
180
+ XML::Namespace.new(current_node,prefix,href)
181
+ XML::Namespace.new(root_node,prefix,href) unless root_node.namespaces.find_by_prefix(prefix)
182
+ end
183
+ end
184
+
185
+ #
186
+ # Determine the tag namespace if one has been specified. This value takes
187
+ # precendence over one that is handed down to composed sub-classes.
188
+ #
189
+ tag_namespace = current_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
190
+
191
+ # Set the namespace of the current node to the specified namespace
192
+ current_node.namespaces.namespace = tag_namespace if tag_namespace
193
+
194
+ #
195
+ # Add all the attribute tags to the current node with their namespace, if one
196
+ # is defined, or the namespace handed down to the node.
197
+ #
198
+ self.class.attributes.each do |attribute|
199
+ attribute_namespace = current_node.namespaces.find_by_prefix(attribute.options[:namespace]) || default_namespace
200
+
201
+ value = send(attribute.method_name)
202
+
203
+ #
204
+ # If the attribute has a :on_save attribute defined that is a proc or
205
+ # a defined method, then call those with the current value.
206
+ #
207
+ if on_save_operation = attribute.options[:on_save]
208
+ if on_save_operation.is_a?(Proc)
209
+ value = on_save_operation.call(value)
210
+ elsif respond_to?(on_save_operation)
211
+ value = send(on_save_operation,value)
212
+ end
213
+ end
214
+
215
+ current_node[ "#{attribute_namespace ? "#{attribute_namespace.prefix}:" : ""}#{attribute.tag}" ] = value
216
+ end
217
+
218
+ #
219
+ # All all the elements defined (e.g. has_one, has_many, element) ...
220
+ #
221
+ self.class.elements.each do |element|
222
+
223
+ tag = element.tag || element.name
224
+
225
+ element_namespace = current_node.namespaces.find_by_prefix(element.options[:namespace]) || tag_namespace
226
+
227
+ value = send(element.name)
228
+
229
+ #
230
+ # If the element defines an :on_save lambda/proc then we will call that
231
+ # operation on the specified value. This allows for operations to be
232
+ # performed to convert the value to a specific value to be saved to the xml.
233
+ #
234
+ if on_save_operation = element.options[:on_save]
235
+ if on_save_operation.is_a?(Proc)
236
+ value = on_save_operation.call(value)
237
+ elsif respond_to?(on_save_operation)
238
+ value = send(on_save_operation,value)
239
+ end
240
+ end
241
+
242
+ #
243
+ # Normally a nil value would be ignored, however if specified then
244
+ # an empty element will be written to the xml
245
+ #
246
+ if value.nil? && element.options[:state_when_nil]
247
+ current_node << XML::Node.new(tag,nil,element_namespace)
248
+ end
249
+
250
+ #
251
+ # To allow for us to treat both groups of items and singular items
252
+ # equally we wrap the value and treat it as an array.
253
+ #
254
+ if value.nil?
255
+ values = []
256
+ elsif value.respond_to?(:to_ary) && !element.options[:single]
257
+ values = value.to_ary
258
+ else
259
+ values = [value]
260
+ end
261
+
262
+
263
+ values.each do |item|
264
+
265
+ if item.is_a?(HappyMapper)
266
+
267
+ #
268
+ # Other HappyMapper items that are convertable should not be called
269
+ # with the current node and the namespace defined for the element.
270
+ #
271
+ item.to_xml(current_node,element_namespace)
272
+
273
+ elsif item
274
+
275
+ #
276
+ # When a value exists we should append the value for the tag
277
+ #
278
+ current_node << XML::Node.new(tag,item.to_s,element_namespace)
279
+
280
+ else
281
+
282
+ #
283
+ # Normally a nil value would be ignored, however if specified then
284
+ # an empty element will be written to the xml
285
+ #
286
+ current_node << XML.Node.new(tag,nil,element_namespace) if element.options[:state_when_nil]
287
+
288
+ end
289
+
290
+ end
291
+
292
+ end
293
+
294
+
295
+ #
296
+ # Generate xml from a document if no node was passed as a parameter. Otherwise
297
+ # this method is being called recursively (or special case) and we should
298
+ # return the node with this node attached as a child.
299
+ #
300
+ if write_out_to_xml
301
+ document = XML::Document.new
302
+ document.root = current_node
303
+ document.to_s
304
+ else
305
+ parent_node
306
+ end
307
+
308
+ end
309
+
310
+
311
+ end
312
+
313
+ require File.dirname(__FILE__) + '/happymapper/item'
314
+ require File.dirname(__FILE__) + '/happymapper/attribute'
315
+ require File.dirname(__FILE__) + '/happymapper/element'
@@ -0,0 +1,45 @@
1
+ module Recharge
2
+ class Base
3
+ class << self
4
+ def get(uri)
5
+ client = Client.new()
6
+ return client.request('GET', uri)
7
+ end
8
+
9
+ def post(uri, data=nil)
10
+ client = Client.new()
11
+ return client.request('POST', uri, data)
12
+ end
13
+
14
+ def delete(uri)
15
+ client = Client.new()
16
+ return client.request('DELETE', uri)
17
+ end
18
+
19
+ def returnImportantXML(data)
20
+ require "rexml/document"
21
+ doc = REXML::Document.new(data)
22
+
23
+ if doc.elements[1].elements[3].length == 1
24
+ return doc.elements[1].elements[3].elements[1]
25
+ else
26
+ return doc.elements[1].elements[3]
27
+ end
28
+ end
29
+
30
+ def returnErrorXML(data)
31
+ require "rexml/document"
32
+ doc = REXML::Document.new(data)
33
+
34
+ doc.elements["/response/result/resultDescription"].text
35
+ end
36
+
37
+ def returnStatusXML(data)
38
+ require "rexml/document"
39
+ doc = REXML::Document.new(data)
40
+
41
+ doc.elements["/response/result/resultDescription"]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ module Recharge
2
+ class Charge
3
+ include HappyMapper
4
+
5
+ tag 'charge'
6
+
7
+ element :id, String
8
+ element :product, String
9
+ element :payMethod, String
10
+ element :billingStartDate, String
11
+ element :billingEndDate, String
12
+ element :billingEndAmount, String
13
+ element :price, String
14
+ element :intervalValue, String
15
+ element :intervalUnit, String
16
+ element :nextChargeDate, String
17
+ element :totalRevenue, String
18
+ element :pubID, String
19
+ element :oneTime, String
20
+ element :error, String
21
+
22
+ def initialize(id='')
23
+ @id = id
24
+ end
25
+ def self.find_all
26
+ responseXML = Recharge::Base.get('charges')
27
+ parse(responseXML.to_s)
28
+ end
29
+ def self.find (id)
30
+ responseXML = Recharge::Base.get('charges/'+id)
31
+ parse(responseXML.to_s)
32
+ end
33
+ def self.create (attributes = {})
34
+ responseXML = Recharge::Base.post('charges', attributes)
35
+ parse(responseXML.to_s)
36
+ end
37
+ def update (attributes = {})
38
+ responseXML = Recharge::Base.post('charges/'+self.id, attributes)
39
+ Charge.parse(responseXML.to_s)
40
+ end
41
+ def destroy
42
+ responseXML = Recharge::Base.delete('charges/'+self.id)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
1
+ class Client
2
+
3
+ def baseURL
4
+ "https://www.rechargebilling.com/API/v2/"
5
+ end
6
+
7
+ def request(method, uri, data=nil)
8
+ response = sendRequest(method, uri, data)
9
+ response.assertValidResponse
10
+ if response.code.to_i == 200
11
+ if method != "DELETE"
12
+ Recharge::Base.returnImportantXML(response.body)
13
+ else
14
+ Recharge::Base.returnStatusXML(response.body)
15
+ end
16
+ else
17
+ Recharge::Base.returnErrorXML(response.body)
18
+ end
19
+
20
+ end
21
+
22
+ def sendRequest(method, uri, data=nil)
23
+ require "net/https"
24
+ require "uri"
25
+
26
+ if !Recharge.api_key
27
+ puts "no api key"
28
+ return
29
+ end
30
+
31
+ uri = baseURL+uri
32
+ uri = URI.parse(uri)
33
+
34
+ if method == "GET"
35
+ request = Net::HTTP::Get.new(uri.request_uri)
36
+ end
37
+
38
+ if method == "POST"
39
+ request = Net::HTTP::Post.new(uri.request_uri)
40
+ request.set_form_data(data)
41
+ end
42
+
43
+ if method == "DELETE"
44
+ request = Net::HTTP::Delete.new(uri.request_uri)
45
+ end
46
+
47
+ request.basic_auth(Recharge.api_key, "")
48
+
49
+ http = Net::HTTP.new(uri.host, uri.port)
50
+ http.use_ssl = true
51
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
52
+
53
+ response = http.request(request)
54
+
55
+ return Response.new(response.code, response.body)
56
+
57
+ end
58
+ end
@@ -0,0 +1,53 @@
1
+ module Recharge
2
+ class Customer
3
+ include HappyMapper
4
+
5
+ tag 'customer'
6
+
7
+ element :id, String
8
+ element :refID, String
9
+ element :firstName, String
10
+ element :lastName, String
11
+ element :email, String
12
+ element :company, String
13
+ element :phone, String
14
+ element :billingAddress1, String
15
+ element :billingAddress2, String
16
+ element :billingCity, String
17
+ element :billingState, String
18
+ element :billingZIP, String
19
+ element :shippingAddress1, String
20
+ element :shippingAddress2, String
21
+ element :shippingCity, String
22
+ element :shippingState, String
23
+ element :shippingZIP, String
24
+ element :emailOption, String
25
+ element :signupDate, String
26
+
27
+ has_many :payMethods, PayMethod
28
+ has_many :charges, Charge
29
+
30
+ def initialize(id='')
31
+ @id = id
32
+ end
33
+ def self.find_all
34
+ responseXML = Recharge::Base.get('customers')
35
+ parse(responseXML.to_s)
36
+ end
37
+ def self.find (id)
38
+ responseXML = Recharge::Base.get('customers/'+id)
39
+ parse(responseXML.to_s)
40
+ end
41
+ def self.create (attributes = {})
42
+ responseXML = Recharge::Base.post('customers', attributes)
43
+ parse(responseXML.to_s)
44
+ end
45
+ def update (attributes = {})
46
+ responseXML = Recharge::Base.post('customers/'+self.id, attributes)
47
+ Customer.parse(responseXML.to_s)
48
+ end
49
+ def destroy
50
+ responseXML = Recharge::Base.delete('customers/'+self.id)
51
+ end
52
+ end
53
+ end