vantiv_lite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5fc133b7d98fa13b255fa742c638a61b4967ab37
4
+ data.tar.gz: 667a4b905d9937d14f62abaefa30967dd9dd428f
5
+ SHA512:
6
+ metadata.gz: db7cea41c6b32da8c25c827de18fa7f407b933dbfce1efd26e43f8da7da8cd86f6bb2e85d9e5b1e0f6b3a6631ad3045f4b1bbac9ffc32d570fc32c9042e57f6d
7
+ data.tar.gz: 78930183f9f3581eb86dc160c77439af517ba4c549a79a8659a7250d94a6ef792c4779f2e98110086de9d249a46a90fa1c5693b0739d6d791d9c0adedbdf489b
data/LICENSE.txt ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2018 Joshua Hansen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # VantivLite
2
+
3
+ ## Overview
4
+
5
+ This gem provides a simple interface for interacting with Vantiv's (WorldPay) LitleOnline eCommerce API. There's no real modeling. It's just a convenient way to configure your environment, send requests, and receive responses in hashes without all the mess of dealing with XML. As such, it should work with any API request available.
6
+
7
+ ### Why not just use [LitleOnline](https://github.com/Vantiv/litle-sdk-for-ruby), the official SDK?
8
+
9
+ There are a number of reasons for creating this gem, despite the fact that an official Ruby implementation already exists. LitleOnline's code not particularly idiomatic Ruby. It's unlikely the developers are experienced Rubyists. That wouldn't be a show-stopper in and of itself, but the dependencies imposed by the gem are downright painful:
10
+
11
+ 1. [`libxml-ruby`](https://github.com/xml4r/libxml-ruby): A large library that requires an even larger native extension. Given the nature and size of the XML being generated, this is overkill unless you're already using `libxml-ruby` (and odds are you're not).
12
+ 2. [`xml-object`](https://github.com/burke/xml-object): In addition to being abandoned at this point, it has a dependency of [`ActiveSupport`](https://github.com/rails/rails/tree/master/activesupport)
13
+
14
+ There are no requirements outside the standard library for this gem, although you can optionally use [Nokogiri](https://github.com/sparklemotion/nokogiri) or [Ox](https://github.com/ohler55/ox) for parsing/serializing XML.
15
+
16
+ ## Installation
17
+
18
+ Pretty standard gem stuff.
19
+
20
+ $ gem install vantiv_lite
21
+
22
+ When using [Bundler](https://bundler.io) or requiring this library in general, it's important to note that it will attempt to load it's XML add-ons by default If Nokogiri or Ox is already defined, it will use them in that order. Otherwise, it will use the default of REXML. The only consideration is that REXML will get required if neither optional library is already required.
23
+
24
+ So, ensure your XML libs load first or just manually load it after the fact.
25
+
26
+ ## Configuration
27
+
28
+ Out of the proverbial box, this should Just Work™ with Vantiv's (WorldPay) test environment using version 8.22 of the API. Obviously, when you go to production, that's probably not ideal. There are a number of ways to configure this gem.
29
+
30
+ If you're integrating into a system that's using a single configuration---pretty common to just process credit cards for your institution---you can use a global configuration set either programmatically or via environment variables:
31
+
32
+ **Note:** It's unlikely you're using version 8.22. In fact, versions can be a sticky issue with this API. Ensure you're using the version that has been assigned to you! You don't want to do all your testing in the default 8.22 only to find out you're on the latest when you move to `prelive`.
33
+
34
+ ### Programmatically
35
+
36
+ ```ruby
37
+ VantivLite.configure do
38
+ env 'sandbox'
39
+ merchant_id 'default'
40
+ password 'sandbox'
41
+ proxy_url 'http://user:passsword@proxy.internal:8888'
42
+ report_group 'Default Report Group'
43
+ username 'sandbox'
44
+ version '8.22'
45
+ xml_lib 'REXML'
46
+ end
47
+ ```
48
+
49
+ Note: All values displayed above are the default values, with the exception of `proxy_url` which is `nil` by default. (There's a good chance you'll need to set `proxy_url` in `prelive` and `postlive` environments since they are IP-whitelisted.)
50
+
51
+ ### `ENV`
52
+
53
+ Prefix any configuration option with `vantiv_` and it will be automatically set:
54
+
55
+ * `ENV['vantiv_env']`
56
+ * `ENV['vantiv_merchant_id']`
57
+ * `ENV['vantiv_password']`
58
+ * `ENV['vantiv_proxy_url']`
59
+ * `ENV['vantiv_report_group']`
60
+ * `ENV['vantiv_username']`
61
+ * `ENV['vantiv_version']`
62
+ * `ENV['vantiv_xml_lib']`
63
+
64
+ You can return the configuration set by the environment with `VantivLite.env_config` which might be useful in situations where you want multiple configurations modified from a default set by the environment.
65
+
66
+ ### Multiple Configurations
67
+
68
+ If you're building a platform that allows multiple clients to plug into Vantiv's API with their own credentials, IDs, or whatever else you'll need to use different configuration options for each. This is done by creating `VantivLite::Config` objects and injecting them into requests. (The global default is used by default in new requests.)
69
+
70
+ This can be done with an existing config:
71
+
72
+ ```ruby
73
+ config = VantivLite.env_config
74
+ new_config = config.with(username: 'user', password: 'password2')
75
+ ```
76
+
77
+ Or by just creating a new one:
78
+
79
+ ```ruby
80
+ # With a hash:
81
+
82
+ config = VantivLite::Config.new(env: 'prelive' version: '11.1')
83
+
84
+ # With DSL:
85
+
86
+ config = VantivLite::Config.build
87
+ proxy_url 'http://proxy.internal:8888'
88
+ xml_lib 'Ox'
89
+ end
90
+ ```
91
+
92
+ ## Making Requests
93
+
94
+ A basic request can be made using `VantivLite.request`. This uses the global config and request objects:
95
+
96
+ ```ruby
97
+ params = {
98
+ register_token_request: {
99
+ order_id: '50',
100
+ account_number: '4457119922390123'
101
+ }
102
+ }
103
+
104
+ response = VantivLite.request(params) # => #<VantivLite::Response>
105
+ ```
106
+
107
+ This will return a `VantivLite::Response` which itself operates much like a hash:
108
+
109
+ ```ruby
110
+ response.dig(:register_token_response, :litle_token) # => "1111222233330123"
111
+ ```
112
+
113
+ For many simple transactions the `*_request` and `*_response` keys get a little tedious. So, this can be abreviated to the following:
114
+
115
+ ```ruby
116
+ params = {
117
+ order_id: '50',
118
+ account_number: '4457119922390123'
119
+ }
120
+
121
+ response = VantivLite.register_token(params).dig(:litle_token) # => "1111222233330123"
122
+ ```
123
+
124
+ There are shortcuts for the requests:
125
+
126
+ * `auth_reversal`
127
+ * `authorization`
128
+ * `capture`
129
+ * `credit`
130
+ * `register_token`
131
+ * `sale`
132
+ * `void`
133
+
134
+ Note that the only transformation that is done is underscoring and symbolizing keys. No other mapping is done.
135
+
136
+ ### Requests With Multiple Configurations
137
+
138
+ `VantiveLite.request` (and the various convenience versions) simply uses `Vantiv.default_request` which is just an instance of `VantivLite::Request`. The request object itself can be used similarly with the methods by invoking `#call`:
139
+
140
+ ```ruby
141
+ params = {
142
+ register_token_request: {
143
+ order_id: '50',
144
+ account_number: '4457119922390123'
145
+ }
146
+ }
147
+
148
+ response = VantivLite::Request.new(custom_config).(params) # => #<VantivLite::Response>
149
+
150
+ # Shortcut methods also work:
151
+
152
+ params = {
153
+ order_id: '50',
154
+ account_number: '4457119922390123'
155
+ }
156
+
157
+ response = VantivLite::Request.new(custom_config).register_token(params)
158
+ ```
159
+
160
+ ### Elements and Attributes
161
+
162
+ Obviously, XML doesn't map nice and neat to a hash and vice-versa. However, Vantiv's API doesn't make heavy use of attributes so, on serialization, certain keys are serialized into attributes and return hashes just merge everything together. For example, if you wanted to set an `id` attribute, you would do the following:
163
+
164
+ ```ruby
165
+ params = {
166
+ register_token_request: {
167
+ id: 'abcdef',
168
+ order_id: '50',
169
+ account_number: '4457119922390123'
170
+ }
171
+ }
172
+ ```
173
+
174
+ See `VantivLite::XML::Serializer` for a list of defaults.
175
+
176
+ ### Order Matters
177
+
178
+ This library does not use any sort of models and the mapping done is purely to rename keys into something more "Ruby-ish." Unfortunately, the API XML XSDs from Vantiv are relatively picky. Your hash keys should be in the same order they appear in the documentation.
179
+
180
+ If you get an error like this:
181
+
182
+ ```
183
+ VantivLite::Response::ServerError: Error validating xml data against the schema cvc-complex-type.2.4.a: Invalid content was found starting with element 'orderId'. One of '{"http://www.litle.com/schema":cardValidationNum}' is expected.
184
+ ```
185
+
186
+ It probably means your keys are out of order.
187
+
188
+ Yes, this could be solved by consuming the XSDs and validating requests or modeling every single object. However, the overhead probably isn't worth it and can require version specific changes. Developers are just going to wrap these lower level calls in their own objects anyway where order can be enforced as necessary.
189
+
190
+ ## Vantiv Environments
191
+
192
+ Valid environments are:
193
+
194
+ * `sandbox`
195
+ * `prelive`
196
+ * `postlive`
197
+
198
+ This configures the API url. At the moment, that's really all it does.
199
+
200
+ ## Contributing
201
+
202
+ ### Issue Guidelines
203
+
204
+ GitHub issues are for bugs, not support. As of right now, there is no official support for this gem. You can try reaching out to the author, [Joshua Hansen](mailto:joshua@epicbanality.com?subject=VantiveLite) if you're really stuck, but there's a pretty high chance that won't go anywhere at the moment or you'll get a response like this:
205
+
206
+ > Hi. I'm super busy. It's nothing personal. Check the README first if you haven't already. If you don 't find your answer there, it's time to start reading the source. Have fun! Let me know if I screwed something up.
207
+
208
+ ### Pull Request Guidelines
209
+
210
+ * Include tests with your PRs.
211
+ * Run `rubocop` to ensure your style fits with the rest of the project.
212
+
213
+ ### Code of Conduct
214
+
215
+ Be nice. After all, this is free code. I have a day job.
216
+
217
+ ## License
218
+
219
+ See [`LICENSE.txt`](LICENSE.txt).
220
+
221
+ ## What if I stop maintaining this?
222
+
223
+ The codebase isn't huge. If you opt to rely on this code and I die/get bored/find enlightenment you should be able to maintain it. Sadly, that's the only guarantee at the moment!
@@ -0,0 +1,98 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module VantivLite
7
+ class Config
8
+ InvalidEnvironment = Class.new(StandardError)
9
+
10
+ ENVS = {
11
+ 'sandbox' => URI('https://www.testvantivcnp.com/sandbox/communicator/online'),
12
+ 'prelive' => URI('https://payments.vantivprelive.com/vap/communicator/online'),
13
+ 'postlive' => URI('https://payments.vantivcnp.com/vap/communicator/online')
14
+ }.freeze
15
+
16
+ OPTS = %i[env merchant_id password proxy_url report_group username version xml_lib].freeze
17
+
18
+ class Builder
19
+ def self.call(&blk)
20
+ new.(&blk)
21
+ end
22
+
23
+ def initialize
24
+ @opts = {}
25
+ end
26
+
27
+ OPTS.each { |o| define_method(o) { |val| @opts[o] = val.to_s } }
28
+
29
+ def call(&blk)
30
+ instance_eval(&blk) if block_given?
31
+ Config.new(@opts)
32
+ end
33
+ end
34
+
35
+ def self.build(&blk)
36
+ Config::Builder.(&blk)
37
+ end
38
+
39
+ attr_reader :proxy_uri, :sandbox, :uri
40
+ alias sandbox? sandbox
41
+
42
+ def initialize(**opts)
43
+ @opts = opts.each_with_object({}) { |(k, v), h| OPTS.include?(k = k.to_sym) && h[k] = v.to_s }
44
+ defaults!
45
+ load_xml_lib
46
+ env_valid?
47
+ proxy_uri!
48
+ @uri = ENVS[@opts[:env]]
49
+ end
50
+
51
+ def opts
52
+ @opts.dup
53
+ end
54
+
55
+ def proxy_args
56
+ @proxy_uri ? [@proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password] : []
57
+ end
58
+
59
+ def with(**opts)
60
+ new(@opts.merge(opts))
61
+ end
62
+
63
+ OPTS.each { |o| define_method(o) { @opts[o] } }
64
+
65
+ private
66
+
67
+ def default_xml_lib
68
+ return 'Ox' if defined?(::Ox)
69
+ return 'Nokogiri' if defined?(::Nokogiri)
70
+ 'REXML'
71
+ end
72
+
73
+ def defaults!
74
+ @opts[:env] ||= 'sandbox'
75
+ @opts[:report_group] ||= 'Default Report Group'
76
+ @opts[:version] ||= '8.22'
77
+ @opts[:xml_lib] ||= default_xml_lib
78
+ return unless (@sandbox = (opts[:env] == 'sandbox'))
79
+ @opts[:merchant_id] ||= 'default'
80
+ @opts[:password] ||= 'sandbox'
81
+ @opts[:username] ||= 'sandbox'
82
+ end
83
+
84
+ def env_valid?
85
+ raise InvalidEnvironment, %(:env must be set to one of: "#{ENVS.keys.join('", "')}") unless
86
+ ENVS.key?(@opts[:env])
87
+ end
88
+
89
+ def load_xml_lib
90
+ require "vantiv_lite/xml/#{@opts[:xml_lib].downcase}"
91
+ end
92
+
93
+ def proxy_uri!
94
+ return unless (url = @opts['proxy_url'] || ENV['HTTP_PROXY'] || ENV['http_proxy'])
95
+ @proxy_uri = URI(url)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,92 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'securerandom'
4
+ require 'vantiv_lite/response'
5
+ require 'vantiv_lite/xml'
6
+
7
+ module VantivLite
8
+ TRANSACTIONS = %w[
9
+ auth_reversal
10
+ authorization
11
+ capture
12
+ credit
13
+ register_token
14
+ sale
15
+ void
16
+ ].freeze
17
+
18
+ class Request
19
+ InvalidConfig = Class.new(StandardError)
20
+ ResponseError = Class.new(StandardError)
21
+
22
+ attr_reader :config, :http, :serializer
23
+
24
+ def initialize(config = VantivLite.default_config, http: nil, serializer: nil)
25
+ raise InvalidConfig, 'invalid or missing config' unless config.is_a?(Config)
26
+ @config = config
27
+ @http = http || _http
28
+ @parser = XML.parser_with(config.xml_lib)
29
+ @serializer = serializer || XML.serializer_with(config.xml_lib)
30
+ end
31
+
32
+ def call(request_hash, *dig_keys)
33
+ Response.new(post(serializer.(format_request(request_hash))), *dig_keys, parser: @parser)
34
+ end
35
+
36
+ def post(xml)
37
+ http.start { |h| h.request(post_request(xml)) }
38
+ end
39
+
40
+ TRANSACTIONS.each do |t|
41
+ define_method(t) { |hash| call({ :"#{t}_request" => hash }, :"#{t}_response") }
42
+ end
43
+
44
+ private
45
+
46
+ def _http
47
+ Net::HTTP.new(config.uri.host, config.uri.port, *config.proxy_args).tap do |h|
48
+ h.use_ssl = true if config.uri.scheme == 'https'
49
+ end
50
+ end
51
+
52
+ def default_attributes_with(hash)
53
+ hash['id'] ||= SecureRandom.hex(12)
54
+ hash['reportGroup'] ||= config.report_group
55
+ hash
56
+ end
57
+
58
+ def format_request(request_hash)
59
+ {
60
+ 'litleOnlineRequest' => {
61
+ 'xmlns' => 'http://www.litle.com/schema',
62
+ 'version' => config.version,
63
+ 'merchantId' => config.merchant_id,
64
+ 'authentication' => { 'user' => config.username, 'password' => config.password }
65
+ }.merge(insert_default_attributes(lower_camelize_keys(request_hash)))
66
+ }
67
+ end
68
+
69
+ def insert_default_attributes(request_hash)
70
+ request_hash.each_with_object({}) do |(k, obj), h|
71
+ h[k] = XML.hash_or_array(obj) { |o| default_attributes_with(o) }
72
+ end
73
+ end
74
+
75
+ def lower_camelize_keys(hash)
76
+ XML.transform_keys(hash) do |k|
77
+ if k.is_a?(String)
78
+ k
79
+ else
80
+ k.to_s.gsub(/_[a-z]/i) { |m| m[1].upcase }.tap { |s| s[0] = s[0].downcase }
81
+ end
82
+ end
83
+ end
84
+
85
+ def post_request(xml)
86
+ Net::HTTP::Post.new(config.uri.path).tap do |r|
87
+ r['Content-Type'] ||= 'text/xml; charset=UTF-8'
88
+ r.body = xml
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,72 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'vantiv_lite/xml'
4
+
5
+ module VantivLite
6
+ class Response
7
+ ServerError = Class.new(StandardError)
8
+ ROOT_KEY = :litle_online_response
9
+
10
+ module Refinements
11
+ [Array, Hash].each do |klass|
12
+ next if klass.public_instance_methods.include?(:dig)
13
+ refine klass do
14
+ def dig(key, *next_keys)
15
+ Dig.(self, key, *next_keys)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ using Refinements
22
+
23
+ Dig = lambda do |obj, key, *next_keys|
24
+ begin
25
+ next_obj = obj[key]
26
+ next_obj.nil? || next_keys.empty? ? next_obj : next_obj.dig(*next_keys)
27
+ rescue NoMethodError
28
+ raise TypeError, "#{next_obj.class.name} does not have #dig method"
29
+ end
30
+ end
31
+
32
+ include Enumerable
33
+
34
+ attr_reader :to_h
35
+ alias to_hash to_h
36
+
37
+ def initialize(http_response, *dig_keys, parser:)
38
+ http_ok?(http_response)
39
+ @to_h = response_hash_with(parser.(http_response.body), dig_keys)
40
+ end
41
+
42
+ def [](key)
43
+ @to_h[key]
44
+ end
45
+
46
+ def dig(key, *next_keys)
47
+ Dig.(@to_h, key, *next_keys)
48
+ end
49
+
50
+ def each(*args, &blk)
51
+ @to_h.each(*args, &blk)
52
+ end
53
+
54
+ private
55
+
56
+ def http_ok?(http_response)
57
+ raise ServerError, "server responded with #{http_response.code} instead of 200" unless
58
+ http_response.code == '200'
59
+ end
60
+
61
+ def response_hash_with(response_hash, dig_keys)
62
+ response_hash = underscore_symbolize_keys(response_hash)
63
+ raise ServerError, "missing root :#{ROOT_KEY}" unless (root_hash = response_hash[ROOT_KEY])
64
+ raise ServerError, root_hash[:message] unless root_hash[:response] == '0'
65
+ dig_keys.any? ? root_hash.dig(*dig_keys) : root_hash
66
+ end
67
+
68
+ def underscore_symbolize_keys(hash)
69
+ XML.transform_keys(hash) { |k| k.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }.to_sym }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,12 @@
1
+ # frozen-string-literal: true
2
+
3
+ module VantivLite
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ TINY = 0
7
+ VERSION = [MAJOR, MINOR, TINY].join('.').freeze
8
+
9
+ def self.version
10
+ VERSION
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'vantiv_lite/xml'
5
+
6
+ module VantivLite
7
+ module XML
8
+ module Nokogiri
9
+ class Parser
10
+ include XML::Parser
11
+
12
+ private
13
+
14
+ def root(xml)
15
+ ::Nokogiri::XML(xml) { |c| c.options = ::Nokogiri::XML::ParseOptions::NOBLANKS }.root
16
+ end
17
+
18
+ def value_with!(element)
19
+ return element.text if element.attributes.empty? && element.elements.empty?
20
+ element.to_h.merge(hash_with(*element.elements))
21
+ end
22
+ end
23
+
24
+ class Serializer
25
+ include XML::Serializer
26
+
27
+ def call(hash)
28
+ ::Nokogiri::XML::Document.new.tap { |d| add_xml_elements!(d, hash) }.to_s
29
+ end
30
+
31
+ private
32
+
33
+ def attributes_or_elements!(parent, key, value)
34
+ return parent[key] = text_with(value) if attributes.include?(key)
35
+ e = ::Nokogiri::XML::Element.new(key, parent)
36
+ parent.add_child(e)
37
+ add_xml_elements!(e, value)
38
+ end
39
+
40
+ def insert_text!(element, text)
41
+ element.add_child(text_with(text))
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'ox'
4
+ require 'vantiv_lite/xml'
5
+
6
+ module VantivLite
7
+ module XML
8
+ module Ox
9
+ class Parser
10
+ include XML::Parser
11
+
12
+ private
13
+
14
+ def root(xml)
15
+ ::Ox.load(xml, symbolize_keys: false)
16
+ end
17
+
18
+ def value_with!(node)
19
+ node.text ? node.text : node.attributes.merge(hash_with(*node.nodes))
20
+ end
21
+ end
22
+
23
+ class Serializer
24
+ include XML::Serializer
25
+
26
+ def call(hash)
27
+ ::Ox.dump(add_xml_elements!(::Ox::Document.new(version: '1.0'), hash))
28
+ end
29
+
30
+ private
31
+
32
+ def attributes_or_elements!(parent, key, value)
33
+ return parent[key] = text_with(value) if attributes.include?(key)
34
+ e = ::Ox::Element.new(key)
35
+ parent << e
36
+ add_xml_elements!(e, value)
37
+ end
38
+
39
+ def insert_text!(node, text)
40
+ node << text_with(text)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ # frozen-string-literal: true
2
+
3
+ module VantivLite
4
+ module XML
5
+ module Parser
6
+ def call(xml)
7
+ hash_with(root(xml))
8
+ end
9
+
10
+ private
11
+
12
+ def hash_with(*nodes)
13
+ nodes.each_with_object({}) do |n, h|
14
+ inject_or_merge!(h, n.name, value_with!(n))
15
+ end
16
+ end
17
+
18
+ def inject_or_merge!(hash, key, value)
19
+ if hash.key?(key)
20
+ cv = hash[key]
21
+ value = cv.is_a?(Array) ? cv.push(value) : [cv, value]
22
+ end
23
+ hash[key] = value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'rexml/document'
4
+ require 'vantiv_lite/xml'
5
+
6
+ module VantivLite
7
+ module XML
8
+ module REXML
9
+ class Parser
10
+ include XML::Parser
11
+
12
+ private
13
+
14
+ def attribute_hash(element)
15
+ element.attributes.each_with_object({}) { |(k, v), h| h[k] = v.to_s }
16
+ end
17
+
18
+ def root(xml)
19
+ ::REXML::Document.new(xml).root
20
+ end
21
+
22
+ def value_with!(element)
23
+ children = element.elements.to_a
24
+ return element.text if element.attributes.empty? && children.empty?
25
+ attribute_hash(element).merge(hash_with(*children))
26
+ end
27
+ end
28
+
29
+ class Serializer
30
+ include XML::Serializer
31
+
32
+ def call(hash)
33
+ # NOTE: This forces attributes to be delimited with double quotes. Despite the fact that
34
+ # the Litle API returns single-quoted responses, requests with single-quoted attributes
35
+ # will yield an internal server error.
36
+ ::REXML::Document.new(nil, attribute_quote: :quote).tap do |d|
37
+ d.add(::REXML::XMLDecl.new)
38
+ add_xml_elements!(d, hash)
39
+ end.to_s
40
+ end
41
+
42
+ private
43
+
44
+ def attributes_or_elements!(parent, key, value)
45
+ return parent.add_attribute(key, text_with(value)) if attributes.include?(key)
46
+ add_xml_elements!(::REXML::Element.new(key, parent, parent.context), value)
47
+ end
48
+
49
+ def insert_text!(element, text)
50
+ element.add_text(text_with(text))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,41 @@
1
+ # frozen-string-literal: true
2
+
3
+ module VantivLite
4
+ module XML
5
+ module Serializer
6
+ ATTRIBUTES = %w[id merchantId reportGroup version xmlns].freeze
7
+
8
+ @@type_coercions = {}
9
+ def self.coerce(type, obj = nil, &blk)
10
+ raise TypeError, '`type` must be a `Class`' unless type.is_a?(Class)
11
+ obj ||= blk
12
+ raise TypeError, '`obj` must respond to `call`' unless obj.respond_to?(:call)
13
+ @@type_coercions[type] = obj
14
+ end
15
+
16
+ attr_reader :attributes
17
+
18
+ def initialize(attributes: ATTRIBUTES)
19
+ @attributes = attributes
20
+ end
21
+
22
+ private
23
+
24
+ def add_xml_elements!(parent, obj)
25
+ case obj
26
+ when Hash
27
+ obj.each { |k, v| attributes_or_elements!(parent, k, v) }
28
+ when Array
29
+ obj.each { |v| add_xml_elements!(parent, v) }
30
+ else
31
+ insert_text!(parent, obj)
32
+ end
33
+ parent
34
+ end
35
+
36
+ def text_with(obj)
37
+ (callable = @@type_coercions[obj.class]) ? callable.call(obj) : obj.to_s
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'vantiv_lite/xml/parser'
4
+ require 'vantiv_lite/xml/serializer'
5
+
6
+ module VantivLite
7
+ module XML
8
+ class << self
9
+ def hash_or_array(obj)
10
+ case obj
11
+ when Hash
12
+ yield(obj)
13
+ when Array
14
+ obj.map { |o| yield(o) }
15
+ else
16
+ obj
17
+ end
18
+ end
19
+
20
+ def parser_with(name)
21
+ const_get(name)::Parser.new
22
+ end
23
+
24
+ def serializer_with(name, attributes: Serializer::ATTRIBUTES)
25
+ const_get(name)::Serializer.new(attributes: attributes)
26
+ end
27
+
28
+ def transform_keys(hash, &blk)
29
+ return hash unless hash.is_a?(Hash)
30
+ hash.each_with_object({}) do |(k, obj), h|
31
+ h[yield(k)] = XML.hash_or_array(obj) { |o| transform_keys(o, &blk) }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'vantiv_lite/config'
4
+ require 'vantiv_lite/request'
5
+
6
+ module VantivLite
7
+ class << self
8
+ attr_reader :default_config, :default_request
9
+
10
+ def configure(config = env_config, &blk)
11
+ @default_config = block_given? ? Config.build(&blk) : Config.new(config)
12
+ @default_request = Request.new(@default_config)
13
+ end
14
+
15
+ def env_config
16
+ Config::OPTS.each_with_object({}) do |k, h|
17
+ env_key = "vaniv_#{k}"
18
+ h[k] = ENV[env_key] if ENV.key?(env_key)
19
+ end
20
+ end
21
+
22
+ def request(request_hash)
23
+ default_request.(request_hash)
24
+ end
25
+ alias call request
26
+
27
+ TRANSACTIONS.each { |t| define_method(t) { |hash| default_request.public_send(t, hash) } }
28
+ end
29
+ configure
30
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vantiv_lite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joshua
8
+ - Hansen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-05-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.16'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.16'
28
+ - !ruby/object:Gem::Dependency
29
+ name: minitest
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '5.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '5.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: nokogiri
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.8'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.8'
56
+ - !ruby/object:Gem::Dependency
57
+ name: ox
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '2.9'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2.9'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '10.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rubocop
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0.56'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '0.56'
98
+ description: A Simplified Vanitiv/WorldPay (LitleOnline) API Library
99
+ email:
100
+ - joshua@epicbanality.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - LICENSE.txt
106
+ - README.md
107
+ - lib/vantiv_lite.rb
108
+ - lib/vantiv_lite/config.rb
109
+ - lib/vantiv_lite/request.rb
110
+ - lib/vantiv_lite/response.rb
111
+ - lib/vantiv_lite/version.rb
112
+ - lib/vantiv_lite/xml.rb
113
+ - lib/vantiv_lite/xml/nokogiri.rb
114
+ - lib/vantiv_lite/xml/ox.rb
115
+ - lib/vantiv_lite/xml/parser.rb
116
+ - lib/vantiv_lite/xml/rexml.rb
117
+ - lib/vantiv_lite/xml/serializer.rb
118
+ homepage: https://github.com/binarypaladin/vantiv_lite
119
+ licenses:
120
+ - MIT
121
+ metadata: {}
122
+ post_install_message:
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: 2.2.0
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 2.4.5.4
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: A Simplified Vanitiv/WorldPay (LitleOnline) API Library
142
+ test_files: []