docdata 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/.gitignore +4 -1
- data/.travis.yml +10 -0
- data/LICENSE +1 -1
- data/README.md +173 -7
- data/Rakefile +8 -0
- data/docdata.gemspec +18 -11
- data/lib/docdata.rb +74 -1
- data/lib/docdata/bank.rb +27 -0
- data/lib/docdata/config.rb +41 -0
- data/lib/docdata/docdata_error.rb +8 -0
- data/lib/docdata/engine.rb +13 -0
- data/lib/docdata/ideal.rb +40 -0
- data/lib/docdata/line_item.rb +99 -0
- data/lib/docdata/payment.rb +196 -0
- data/lib/docdata/response.rb +173 -0
- data/lib/docdata/shopper.rb +112 -0
- data/lib/docdata/version.rb +1 -1
- data/lib/docdata/xml/bank-list.xml +39 -0
- data/lib/docdata/xml/cancel.xml.erb +9 -0
- data/lib/docdata/xml/create.xml.erb +98 -0
- data/lib/docdata/xml/start.xml.erb +67 -0
- data/lib/docdata/xml/status.xml.erb +9 -0
- data/php-example/create.xml.erb +140 -0
- data/php-example/index.html +78 -0
- data/php-example/process.php +182 -0
- data/php-example/return.php +36 -0
- data/php-example/soap.rb +21 -0
- data/spec/config_spec.rb +53 -0
- data/spec/ideal_spec.rb +19 -0
- data/spec/line_item_spec.rb +55 -0
- data/spec/payment_spec.rb +162 -0
- data/spec/response_spec.rb +206 -0
- data/spec/shopper_spec.rb +50 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/xml/status-canceled-creditcard.xml +34 -0
- data/spec/xml/status-canceled-ideal.xml +29 -0
- data/spec/xml/status-new.xml +20 -0
- data/spec/xml/status-paid-creditcard.xml +33 -0
- data/spec/xml/status-paid-ideal.xml +33 -0
- data/spec/xml/status-paid-sofort.xml +33 -0
- metadata +145 -13
- data/LICENSE.txt +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5ff4754891764a2fadf457caa3103234ab0ff66
|
4
|
+
data.tar.gz: 4a88eccbd3b38ca964800661cbff4f8caa01d856
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14bb38e8034b1b17ce035e0e2a49637491622a699556b1886aa803bedd78e52685de32fa2cb4110c975cea5c8bb52d29d7e46af219b200f0ad1f4e0911042ab3
|
7
|
+
data.tar.gz: bf6a93bb98864ffb8a576bf6c9e4ef07e6832fc3221d1cd5805302dcd21bc0fd0da042f11ef17dcdff66b25d9ee3017775ec4dfcfd7328cbe24c04e9be7cbff7
|
data/.coveralls.yml
ADDED
data/.gitignore
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
*.gem
|
2
2
|
*.rbc
|
3
|
+
.DS_Store
|
3
4
|
.bundle
|
4
5
|
.config
|
5
6
|
.yardoc
|
@@ -7,11 +8,13 @@ Gemfile.lock
|
|
7
8
|
InstalledFiles
|
8
9
|
_yardoc
|
9
10
|
coverage
|
10
|
-
doc/
|
11
11
|
lib/bundler/man
|
12
12
|
pkg
|
13
|
+
doc
|
13
14
|
rdoc
|
14
15
|
spec/reports
|
15
16
|
test/tmp
|
16
17
|
test/version_tmp
|
17
18
|
tmp
|
19
|
+
spec/vcr_cassettes
|
20
|
+
test-credentials.yaml
|
data/.travis.yml
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
language: ruby
|
2
|
+
script: bundle exec rake --trace
|
3
|
+
rvm:
|
4
|
+
- '2.0'
|
5
|
+
env:
|
6
|
+
global:
|
7
|
+
- secure: TIDimYgwlRDIMSeNUDsc3Bwb1dCSdfyF89clBbGssrolmxDSOMCWEkKg+XVeiR8ZTaprule80X35rw8xqoNqpYuZedH0Z6owu1JQ6C3XkUW9TXVhcIQ3WF7KF8YVXwSLYTf9W4F9j8KNW8azb2PmmuLbDqmqAhBxMsG+i1hgPaw=
|
8
|
+
- secure: ZrkG1/8uInY6xiP8xD7HhjDxgxmvbxRHdmzveN/qh9CIsOjCAESfgjd9WjiP8IiW2/G5tYRkVeqYijDLnTkWuyn2lsiWRjE+CflwqTo8XHQCKLCKGbLJzR13i+ulos5ByjUV7OVPTZ241Lxaaw7JBgUgFP3BUqA0127pX8mxpm0=
|
9
|
+
- secure: MQLynyivQIoClPKv3mA62S3E3SVR1ho/8rQ3xs43fsT4PNClwftOO+fRiH0djkmZ4dzJSAD5H70ikcAJoOQ3dAul2+xA5uPAt4AUBk0sindcT2PeEnjPIH9rt/AxLYG67MWZihTr2xcQ38xh6nNJZslyFSglntCTFPmyBy3D0KM=
|
10
|
+
- secure: FrWAE8ZvvBnI6+VwxdBZIHQwQPQKOqFMP9g3XVMyJOPCF4HWlzBor1e5MZ9UquVYaL+/2OL0aDxhgN33y0YsX3/2c5Z25cPGglzE3VsPzRdzDqarwPEz/tEzTYG/bKKPIEI9q+cKQNnSBuMtk4EELBmOJ9iA8jPD0OCNgRedlRw=
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# Docdata
|
2
|
+
[![Build Status](https://secure.travis-ci.org/henkm/docdata.png)](http://travis-ci.org/henkm/docdata)
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/docdata.svg)](http://badge.fury.io/rb/docdata)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/henkm/docdata/badges/gpa.svg)](https://codeclimate.com/github/henkm/docdata)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/henkm/docdata/badge.png?branch=master)](https://coveralls.io/r/henkm/docdata)
|
2
6
|
|
3
|
-
|
7
|
+
Docdata is a Ruby binder for Docdata Payments. Current status: **in progress, not stable**.
|
8
|
+
|
9
|
+
This gem relies on the awesom Savon gem to communicate with Docdata Payments' SOAP API.
|
4
10
|
|
5
11
|
## Installation
|
6
12
|
|
@@ -16,14 +22,174 @@ Or install it yourself as:
|
|
16
22
|
|
17
23
|
$ gem install docdata
|
18
24
|
|
19
|
-
##
|
25
|
+
## Workflow
|
26
|
+
Each transaction consists of 2 - optionally 3 - parts:
|
27
|
+
|
28
|
+
- `Docdata::Shopper` (details about the shopper: name, email, etc.)
|
29
|
+
- `Docdata::Payment` (details about the payment: currency, gross amount, etc.)
|
30
|
+
- `Docdata::LineItem` (optionally list the products of this payment) **currently not working!**
|
31
|
+
|
32
|
+
|
33
|
+
The general workflow is as follows:
|
34
|
+
|
35
|
+
1. Set up a `Docdata::Shopper` object with the details of your shopper: `@shopper = Docdata::Shopper.new`
|
36
|
+
2. Set up a `Docdata::Payment` object with the details of your order: `@payment = Docdata::Payment.new(shopper: @shopper)`
|
37
|
+
3. Call the `create` method (`@payment.create`)
|
38
|
+
4. On success, store the payment key and use `@payment.redirect_url` to redirect the consumer to the transaction page.
|
39
|
+
5. When the consumer gets back to your application, use the `Docdata::Payment.find("PA1M3NTK3Y").status.paid` to check if the order was paid for.
|
40
|
+
|
41
|
+
## Parameters
|
42
|
+
All the payment details that Docdata Payments requires, are - obviously - also required to make payments via this gem.
|
43
|
+
|
44
|
+
#### Docdata::Shopper:
|
45
|
+
| Name | Type | Required | Defaults to |
|
46
|
+
|-----------|------------|---------|----|
|
47
|
+
| id | String (ID for own reference) | Yes | |
|
48
|
+
| first_name | String | Yes | First Name |
|
49
|
+
| last_name | String | Yes | Last Name |
|
50
|
+
| street | String | Yes | Main Street |
|
51
|
+
| house_number | String | Yes | 123 |
|
52
|
+
| postal_code | String | Yes | 2244 |
|
53
|
+
| city | String | Yes | City |
|
54
|
+
| country_code | String (ISO country code) | Yes | NL |
|
55
|
+
| language_code | String (ISO language code) | Yes | nl |
|
56
|
+
| email | String | Yes | random@example.com |
|
57
|
+
|
58
|
+
#### Docdata::Payment:
|
59
|
+
| Name | Type | Required |
|
60
|
+
|-----------|------------|---------|
|
61
|
+
| amount | Integer (amount in cents) | Yes |
|
62
|
+
| currency | String (ISO currency code) | Yes |
|
63
|
+
| order_reference | String (your own unique reference) | Yes |
|
64
|
+
| profile | String (name of your Docdata Payment profile)| Yes |
|
65
|
+
| shopper | Docdata::Shopper | Yes |
|
66
|
+
| line_items | Array (of Docdata::LineItem objects) | No |
|
67
|
+
| bank_id | String | No |
|
68
|
+
| prefered_payment_method | String | No |
|
69
|
+
| key | String (is availabel after successful 'create' action) | No (readonly)
|
70
|
+
|
71
|
+
|
72
|
+
## Default values
|
73
|
+
A quick warning about the default values for the Shopper object: **For some payment methods, Docdata Payments needs the actual information in order for the payment to take place.**
|
74
|
+
|
75
|
+
If you use `GIROPAY`, `SEPA` and `AFTERPAY` this is the case. (Maybe also in other payment methods, please let me know!)
|
76
|
+
|
77
|
+
## Example usage in Rails application
|
78
|
+
The example below assumes you have your application set up with a Order model, which contains the information needed for this transaction (amount, name, etc.).
|
79
|
+
```ruby
|
80
|
+
# orders_controller.rb
|
81
|
+
def start_transaction
|
82
|
+
# find the order from your database
|
83
|
+
@order = Order.find(params[:id])
|
84
|
+
|
85
|
+
# initialize a shopper, use details from your order
|
86
|
+
shopper = Docdata::Shopper.new(first_name: @order.first_name, last_name: @order.last_name)
|
87
|
+
|
88
|
+
# set up a payment
|
89
|
+
@payment = Docdata::Payment.new(
|
90
|
+
amount: @order.total,
|
91
|
+
currency: @order.currency,
|
92
|
+
shopper: shopper,
|
93
|
+
profile: "My Default Profile",
|
94
|
+
order_reference: "order ##{@order.id}"
|
95
|
+
)
|
96
|
+
|
97
|
+
# create the payment via the docdata api and collect the result
|
98
|
+
result = @payment.create
|
99
|
+
|
100
|
+
if result.success?
|
101
|
+
# Set the transaction key for future reference
|
102
|
+
@order.update_column :docdata_key, result.key
|
103
|
+
# redirect the user to the docdata payment page
|
104
|
+
redirect_to @payment.redirect_url
|
105
|
+
else
|
106
|
+
# TODO: Display the error and warn the user that something went wrong.
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
## Ideal
|
112
|
+
|
113
|
+
For transactions in the Netherlands, iDeal is the most common option. To redirect a user directly to the bank page (skipping the Docdata web menu page), you can ask your user to choose a bank from any of the banks listed in the `Docdata::Ideal.banks` method.
|
114
|
+
|
115
|
+
In `Docdata::Payment` you can set `bank_id` to any value. If you do, the redirect URI will redirect your user directly to the bank page.
|
116
|
+
|
117
|
+
Example code:
|
118
|
+
```ruby
|
119
|
+
# orders_controller.rb
|
120
|
+
def ideal_checkout
|
121
|
+
@order = Order.find(params[:order_id])
|
122
|
+
@banks = Docdata::Ideal.banks
|
123
|
+
end
|
124
|
+
|
125
|
+
def start_ideal_transaction
|
126
|
+
@order = Order.find(params[:order_id])
|
127
|
+
|
128
|
+
# initialize a shopper, use details from your order
|
129
|
+
shopper = Docdata::Shopper.new(first_name: @order.first_name, last_name: @order.last_name)
|
130
|
+
|
131
|
+
# set up a payment
|
132
|
+
@payment = Docdata::Payment.new(
|
133
|
+
amount: @order.total,
|
134
|
+
currency: @order.currency,
|
135
|
+
shopper: shopper,
|
136
|
+
profile: "My Default Profile",
|
137
|
+
order_reference: "order ##{@order.id}",
|
138
|
+
bank_id: params[:bank_id],
|
139
|
+
default_act: true # redirect directly to the bank, skipping the Docdata web menu
|
140
|
+
)
|
141
|
+
|
142
|
+
# create the payment via the docdata api and collect the result
|
143
|
+
result = @payment.create
|
144
|
+
|
145
|
+
if result.success?
|
146
|
+
# Set the transaction key for future reference
|
147
|
+
@order.update_column :docdata_key, result.key
|
148
|
+
# redirect the user to the bank page
|
149
|
+
redirect_to @payment.redirect_url
|
150
|
+
else
|
151
|
+
# TODO: Display the error and warn the user that something went wrong.
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
View template (ideal_checkout.html.erb):
|
157
|
+
|
158
|
+
```erb
|
159
|
+
<h2>Choose your bank</h2>
|
160
|
+
<%= form_tag start_ideal_transaction_path, method: :post, target: "_blank" do %>
|
161
|
+
<%= select_tag "bank_id", options_from_collection_for_select(@banks, "id", "name") %>
|
162
|
+
<%= hidden_field_tag :order_id, @order.id %>
|
163
|
+
<%= submit_tag "Proceed to checkout" %>
|
164
|
+
<% end %>
|
165
|
+
```
|
166
|
+
|
167
|
+
## Tips and samples
|
168
|
+
|
169
|
+
#### Redirect directly to bank page (skip Docdata web menu)
|
170
|
+
When making a new `Docdata::Payment`, use the `default_act` parameter to redirect consumers directly to the acquirers website. Example:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
@payment = Docdata::Payment.new(
|
174
|
+
amount: @order.total,
|
175
|
+
currency: @order.currency,
|
176
|
+
shopper: shopper,
|
177
|
+
profile: "My Default Profile",
|
178
|
+
order_reference: "order ##{@order.id}",
|
179
|
+
bank_id: params[:bank_id],
|
180
|
+
default_act: true # redirect directly to the bank, skipping the Docdata web menu
|
181
|
+
)
|
182
|
+
```
|
20
183
|
|
21
|
-
|
184
|
+
#### Retrieve a list of iDeal banks to show
|
185
|
+
`Docata::Ideal.banks` returns an Array.
|
22
186
|
|
23
187
|
## Contributing
|
24
188
|
|
25
189
|
1. Fork it
|
26
190
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
-
3.
|
28
|
-
4.
|
29
|
-
5.
|
191
|
+
3. Make changes, document them and add tests (rspec)
|
192
|
+
4. Run the entire test suite and make sure all tests pass
|
193
|
+
5. Commit your changes (`git commit -am 'Add some feature'`)
|
194
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
195
|
+
7. Create new Pull Request
|
data/Rakefile
CHANGED
data/docdata.gemspec
CHANGED
@@ -8,23 +8,30 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Docdata::VERSION
|
9
9
|
spec.authors = ["Henk Meijer", "Eskes Media"]
|
10
10
|
spec.email = ["meijerhenk@gmail.com"]
|
11
|
-
spec.description = %q{A ruby binder for the
|
12
|
-
spec.summary = %q{This gem provides a ruby
|
13
|
-
spec.homepage = ""
|
11
|
+
spec.description = %q{A ruby binder for the Docdata Payment API.}
|
12
|
+
spec.summary = %q{This gem provides a ruby interface for the Docdata Payment API. You basically create a Payment object and receive a reirect_url back to take your users to the checkout page.}
|
13
|
+
spec.homepage = "http://rdoc.info/github/henkm/docdata/"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler" #, "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
16
23
|
spec.add_development_dependency "rspec"
|
17
24
|
spec.add_development_dependency "vcr"
|
18
25
|
spec.add_development_dependency "fakeweb"
|
19
26
|
spec.add_development_dependency "coveralls"
|
20
27
|
spec.add_development_dependency "simplecov"
|
21
28
|
spec.add_development_dependency "yard"
|
29
|
+
spec.add_development_dependency "rubyntlm"
|
22
30
|
|
23
|
-
spec.
|
24
|
-
spec.
|
25
|
-
spec.
|
26
|
-
spec.
|
27
|
-
|
28
|
-
spec.
|
29
|
-
|
30
|
-
end
|
31
|
+
# spec.add_runtime_dependency 'savon', git: 'savonrb/savon'
|
32
|
+
spec.add_dependency 'savon', '~> 2.0'
|
33
|
+
spec.add_dependency 'nori'
|
34
|
+
spec.add_dependency 'veto'
|
35
|
+
spec.add_dependency 'nokogiri'
|
36
|
+
spec.add_dependency("railties")
|
37
|
+
end
|
data/lib/docdata.rb
CHANGED
@@ -1,5 +1,78 @@
|
|
1
|
+
# Libraries
|
2
|
+
require 'ostruct'
|
3
|
+
require 'rails'
|
4
|
+
require 'savon'
|
5
|
+
require 'active_support/dependencies'
|
6
|
+
require 'active_support'
|
7
|
+
require 'open-uri'
|
8
|
+
require 'nokogiri'
|
9
|
+
require 'veto'
|
10
|
+
|
11
|
+
|
12
|
+
# Files
|
1
13
|
require "docdata/version"
|
14
|
+
require "docdata/docdata_error"
|
15
|
+
require "docdata/shopper"
|
16
|
+
require "docdata/payment"
|
17
|
+
require "docdata/line_item"
|
18
|
+
require "docdata/response"
|
19
|
+
require "docdata/ideal"
|
20
|
+
require "docdata/bank"
|
2
21
|
|
22
|
+
include Savon
|
23
|
+
|
24
|
+
#
|
25
|
+
# Docdata Module
|
26
|
+
#
|
3
27
|
module Docdata
|
4
|
-
|
28
|
+
API_VERSION = 1
|
29
|
+
|
30
|
+
# @return [String] Your DocData username
|
31
|
+
# @note The is a required parameter.
|
32
|
+
mattr_accessor :username
|
33
|
+
@@username = nil
|
34
|
+
|
35
|
+
# @return [String] Your DocData password
|
36
|
+
mattr_accessor :password
|
37
|
+
@@password = nil
|
38
|
+
|
39
|
+
# @return [Boolean] Test mode switch
|
40
|
+
mattr_accessor :test_mode
|
41
|
+
@@test_mode = true
|
42
|
+
|
43
|
+
# @param [String] Set the url of your website where docdata can send messages to
|
44
|
+
mattr_accessor :return_url
|
45
|
+
@@return_url = nil
|
46
|
+
|
47
|
+
# returns the version number
|
48
|
+
def self.version
|
49
|
+
VERSION
|
50
|
+
end
|
51
|
+
|
52
|
+
# sets up configuration
|
53
|
+
def self.setup
|
54
|
+
yield self
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.url
|
58
|
+
if test_mode
|
59
|
+
"https://test.docdatapayments.com/ps/services/paymentservice/1_1?wsdl"
|
60
|
+
else
|
61
|
+
"https://www.docdatapayments.com/ps/services/paymentservice/1_1?wsdl"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# For testing purpose only: set the username and password
|
66
|
+
# in environment variables to make the tests pass with your test
|
67
|
+
# credentials.
|
68
|
+
def self.set_credentials_from_environment
|
69
|
+
self.password = ENV["DOCDATA_PASSWORD"]
|
70
|
+
self.username = ENV["DOCDATA_USERNAME"]
|
71
|
+
self.return_url = ENV["DOCDATA_RETURN_URL"]
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.client
|
75
|
+
Savon.client(wsdl: url)
|
76
|
+
end
|
77
|
+
|
5
78
|
end
|
data/lib/docdata/bank.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Docdata
|
2
|
+
#
|
3
|
+
# Object representing a "Bank" object with attributes provided by Mollie
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# Bank.new({
|
7
|
+
# :id => "0031",
|
8
|
+
# :name => "ABN AMRO"
|
9
|
+
# })
|
10
|
+
class Bank
|
11
|
+
# @return [String] The id of the bank provided by Mollie.
|
12
|
+
attr_accessor :id
|
13
|
+
# @return [String] The name of the bank.
|
14
|
+
attr_accessor :name
|
15
|
+
|
16
|
+
#
|
17
|
+
# Initializer to transform a +Hash+ into an Bank object
|
18
|
+
#
|
19
|
+
# @param [Hash] values
|
20
|
+
def initialize(values=nil)
|
21
|
+
return if values.nil?
|
22
|
+
|
23
|
+
@id = values[:id].to_s
|
24
|
+
@name = values[:name].to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#
|
2
|
+
# Configuration object for storing some parameters required for making transactions
|
3
|
+
#
|
4
|
+
module Docdata::Config
|
5
|
+
class << self
|
6
|
+
# @return [String] Your DocData username
|
7
|
+
# @note The is a required parameter.
|
8
|
+
attr_accessor :username
|
9
|
+
# @return [String] Your DocData password
|
10
|
+
attr_accessor :password
|
11
|
+
# @return [Boolean] Test mode switch
|
12
|
+
attr_accessor :test_mode
|
13
|
+
|
14
|
+
|
15
|
+
# Set's the default value's to nil and false
|
16
|
+
# @return [Hash] configuration options
|
17
|
+
def init!
|
18
|
+
@defaults = {
|
19
|
+
:@username => nil,
|
20
|
+
:@password => nil,
|
21
|
+
:@test_mode => true
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Resets the value's to there previous value (instance_variable)
|
26
|
+
# @return [Hash] configuration options
|
27
|
+
def reset!
|
28
|
+
@defaults.each { |key, value| instance_variable_set(key, value) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set's the new value's as instance variables
|
32
|
+
# @return [Hash] configuration options
|
33
|
+
def update!
|
34
|
+
@defaults.each do |key, value|
|
35
|
+
instance_variable_set(key, value) unless instance_variable_defined?(key)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
init!
|
40
|
+
reset!
|
41
|
+
end
|