paypal_api 0.3.0
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.
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.markdown +161 -0
- data/Rakefile +1 -0
- data/init.rb +1 -0
- data/lib/paypal_api/apis/api.rb +149 -0
- data/lib/paypal_api/apis/mass_pay.rb +25 -0
- data/lib/paypal_api/apis/payments_pro.rb +155 -0
- data/lib/paypal_api/support/parameter.rb +195 -0
- data/lib/paypal_api/support/request.rb +125 -0
- data/lib/paypal_api/support/response.rb +77 -0
- data/lib/paypal_api/version.rb +3 -0
- data/lib/paypal_api.rb +21 -0
- data/paypal_api.gemspec +26 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/parameter_spec.rb +189 -0
- data/spec/support/request_spec.rb +143 -0
- data/spec/support/response_spec.rb +33 -0
- data/spec/unit/api_spec.rb +133 -0
- data/spec/unit/mass_pay_spec.rb +84 -0
- data/spec/unit/payments_pro_spec.rb +121 -0
- metadata +118 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# Paypal Api Gem
|
2
|
+
|
3
|
+
a ruby library to handle the entire paypal api.
|
4
|
+
|
5
|
+
paypals documentation sucks, and there do not appear to be any officially supported paypal gems.
|
6
|
+
the gems that do exist do not cover the entire api.
|
7
|
+
|
8
|
+
# Usage
|
9
|
+
|
10
|
+
## Interfacing with the gem:
|
11
|
+
```ruby
|
12
|
+
require "paypal_api"
|
13
|
+
|
14
|
+
request = Paypal::PaymentsPro.do_direct_payment # returns instance of Paypal::DoDirectPaymentRequest
|
15
|
+
|
16
|
+
# Set required fields
|
17
|
+
request.first_name = "mark"
|
18
|
+
request.last_name = "winton"
|
19
|
+
request.amt = 10.00
|
20
|
+
|
21
|
+
# Add a list type field
|
22
|
+
request.item.push {
|
23
|
+
:l_email => "bro@dudeman.com",
|
24
|
+
:l_amt => 23.0
|
25
|
+
}
|
26
|
+
|
27
|
+
response = request.make
|
28
|
+
|
29
|
+
response.success? # true if successful
|
30
|
+
|
31
|
+
response[:correlation_id] # correlation id string returned by paypal
|
32
|
+
response[:transaction_id] # transaction id string, not return on all calls
|
33
|
+
```
|
34
|
+
|
35
|
+
## Configure
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
Paypal::Request.version = "84.0"
|
39
|
+
Paypal::Request.environment = "development" # or "production"
|
40
|
+
Paypal::Request.user = "user_api1.something.com"
|
41
|
+
Paypal::Request.pwd = "some_password_they_gave_you"
|
42
|
+
Paypal::Request.signature = "some_signature"
|
43
|
+
```
|
44
|
+
|
45
|
+
paypal api credentials for production can be found here: [https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-api-signature](https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-api-signature)
|
46
|
+
|
47
|
+
sandbox credentials can be found here: [https://developer.paypal.com/cgi-bin/devscr?cmd=_certs-session&login_access=0](https://developer.paypal.com/cgi-bin/devscr?cmd=_certs-session&login_access=0)
|
48
|
+
|
49
|
+
## Rails
|
50
|
+
|
51
|
+
if you'd like to have multi environment configuration in rails, place a file at `config/paypal.yml` and the gem will read from it accordingly
|
52
|
+
|
53
|
+
```yml
|
54
|
+
test:
|
55
|
+
environment: "sandbox"
|
56
|
+
username: "user_api1.something.com"
|
57
|
+
password: "some_password_they_gave_you"
|
58
|
+
signature: "some_signature"
|
59
|
+
|
60
|
+
production:
|
61
|
+
environment: "production"
|
62
|
+
username: <%= ENV["PAYPAL_USERNAME"] %>
|
63
|
+
password: <%= ENV["PAYPAL_PASSWORD"] %>
|
64
|
+
signature: <%= ENV["PAYPAL_SIGNATURE"] %>
|
65
|
+
```
|
66
|
+
|
67
|
+
# Current Status
|
68
|
+
|
69
|
+
alpha
|
70
|
+
|
71
|
+
the work i've done so far is in order to get up and running on a project. once the project is settled, i'll be spending
|
72
|
+
some more time making the gem complete. once i feel it is in the beta stage i will push it to the official ruby gems
|
73
|
+
repository. in order to get to that stage, i will need to add support for adaptive payments, express checkout, etc...
|
74
|
+
|
75
|
+
# How To Contribute
|
76
|
+
|
77
|
+
right now the most help i could use is in writing the specs for the various api calls from the Payments Pro api. i will be working on
|
78
|
+
separating out the different access methods shortly (there's a huge difference between how to call the Payments Pro api vs the Adaptive Payments api).
|
79
|
+
|
80
|
+
as this is my first gem, i could also use help with some of the niceties with rails. ideally there will be a generator for migrating your db
|
81
|
+
to store ipn messages, and a generated class with callbacks to handle the various cases. this will probably take a lot of effort since there
|
82
|
+
are many intricacies in the meanings of the different ipn's.
|
83
|
+
|
84
|
+
for contributing to the api method request specs, look at `lib/paypal_api/apis/payments_pro.rb`
|
85
|
+
|
86
|
+
this is my first gem, so i'll be excited for any contributions :'(
|
87
|
+
|
88
|
+
# Paypal API Checklist
|
89
|
+
|
90
|
+
here's a list of api methods, and whether or not they are implemented (please take a look at `lib/paypal_api/apis/payments_pro.rb` if you'd
|
91
|
+
like to contribute, i've made it pretty easy to add compatibility for a new api call)
|
92
|
+
|
93
|
+
## Payments Pro
|
94
|
+
|
95
|
+
* do_direct_payment - ✓
|
96
|
+
|
97
|
+
* do_reference_transaction - ✓
|
98
|
+
|
99
|
+
* do_capture - ✓
|
100
|
+
|
101
|
+
* do_void - ✓
|
102
|
+
|
103
|
+
* get_recurring_payments_profile_details - started
|
104
|
+
|
105
|
+
* address_verify
|
106
|
+
|
107
|
+
* bill_outstanding_amount
|
108
|
+
|
109
|
+
* callback
|
110
|
+
|
111
|
+
* create_recurring_payments_profile
|
112
|
+
|
113
|
+
* do_authorization
|
114
|
+
|
115
|
+
* do_express_checkout_payment
|
116
|
+
|
117
|
+
* do_nonreferenced_credit
|
118
|
+
|
119
|
+
* do_reauthorization
|
120
|
+
|
121
|
+
* get_balance
|
122
|
+
|
123
|
+
* get_billing_agreement_customer_details
|
124
|
+
|
125
|
+
* get_express_checkout_details
|
126
|
+
|
127
|
+
* get_transaction_details
|
128
|
+
|
129
|
+
* manage_pending_transaction_status
|
130
|
+
|
131
|
+
* manage_recurring_payments_profile_status
|
132
|
+
|
133
|
+
* refund_transaction
|
134
|
+
|
135
|
+
* set_customer_billing_agreement
|
136
|
+
|
137
|
+
* set_express_checkout
|
138
|
+
|
139
|
+
* transaction_search
|
140
|
+
|
141
|
+
* update_recurring_payments_profile
|
142
|
+
|
143
|
+
## Mass Pay
|
144
|
+
|
145
|
+
note that you need to request that paypal enable mass pay for your account before it will work
|
146
|
+
|
147
|
+
* mass_pay - ✓
|
148
|
+
|
149
|
+
## Instant Pay Notifications
|
150
|
+
|
151
|
+
## Express Checkout
|
152
|
+
|
153
|
+
## Adaptive Payments
|
154
|
+
|
155
|
+
## Adaptive Accounts
|
156
|
+
|
157
|
+
## Invoicing
|
158
|
+
|
159
|
+
## Button Manager
|
160
|
+
|
161
|
+
## Permissions
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "paypal_api"
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Paypal
|
2
|
+
module Formatters
|
3
|
+
def escape_uri_component(string)
|
4
|
+
string = string.to_s
|
5
|
+
return URI.escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_key(symbol)
|
9
|
+
return symbol.to_s.gsub(/[^a-z0-9]/i, "").upcase
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Api
|
14
|
+
|
15
|
+
attr_accessor :request
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def self.set_accessor(klass, name, type = nil)
|
20
|
+
set_reader(klass, name, type)
|
21
|
+
set_writer(klass, name, type)
|
22
|
+
end
|
23
|
+
|
24
|
+
# writers should validate before assigning value
|
25
|
+
def self.set_writer(klass, field, type)
|
26
|
+
klass.class_eval do
|
27
|
+
define_method("#{field}=") do |val|
|
28
|
+
# arbitrary value
|
29
|
+
if type.nil?
|
30
|
+
instance_variable_set("@#{field}", val)
|
31
|
+
# special types
|
32
|
+
elsif type.class == Regexp
|
33
|
+
if match = type.match(val)
|
34
|
+
instance_variable_set("@#{field}", match[0])
|
35
|
+
else
|
36
|
+
raise Paypal::InvalidParameter, "#{field} expects a string that matches #{type}"
|
37
|
+
end
|
38
|
+
elsif type.class == Optional
|
39
|
+
instance_variable_set("@#{field}", type.parse(val))
|
40
|
+
elsif type.class == Enum
|
41
|
+
instance_variable_set("@#{field}", type.parse(val))
|
42
|
+
elsif type.class == Coerce
|
43
|
+
instance_variable_set("@#{field}", type.parse(val))
|
44
|
+
elsif type.class == Default
|
45
|
+
instance_variable_set("@#{field}", type.parse(val))
|
46
|
+
# custom type
|
47
|
+
elsif type.class == Proc && type.call(val)
|
48
|
+
instance_variable_set("@#{field}", val)
|
49
|
+
# is_a type
|
50
|
+
elsif type == val.class
|
51
|
+
instance_variable_set("@#{field}",val)
|
52
|
+
else
|
53
|
+
raise Paypal::InvalidParameter, "#{field}'s spec was set incorrectly"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.set_reader(klass, m, type, constant = false)
|
60
|
+
variable = "@#{m}"
|
61
|
+
|
62
|
+
klass.class_eval do
|
63
|
+
define_method(m) do
|
64
|
+
if constant
|
65
|
+
return type
|
66
|
+
elsif type.class == Default
|
67
|
+
instance_variable_set(variable, type.value) unless instance_variable_defined?(variable)
|
68
|
+
instance_variable_get(variable)
|
69
|
+
else
|
70
|
+
instance_variable_get(variable)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.set_sequential_reader(klass, k, v)
|
77
|
+
variable = "@#{k}"
|
78
|
+
|
79
|
+
klass.class_eval do
|
80
|
+
define_method(k) do
|
81
|
+
instance_variable_set(variable, v.clone) unless instance_variable_defined?(variable)
|
82
|
+
instance_variable_get(variable)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# collected from set request signature
|
88
|
+
def self.set_required(klass, keys)
|
89
|
+
klass.class_eval do
|
90
|
+
@required = keys
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# collected from set request signature
|
95
|
+
def self.set_sequential(klass, keys)
|
96
|
+
klass.class_eval do
|
97
|
+
@sequential = keys
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.symbol_to_camel(symbol)
|
102
|
+
return symbol.to_s.downcase.split("_").map(&:capitalize).join
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.set_request_signature(name, hash)
|
106
|
+
# create request object
|
107
|
+
class_name = "#{self.symbol_to_camel name}Request"
|
108
|
+
self.class.class_eval <<-EOS
|
109
|
+
class Paypal::#{class_name} < Request; end
|
110
|
+
EOS
|
111
|
+
klass = Kernel.const_get("Paypal").const_get(class_name)
|
112
|
+
|
113
|
+
# add setters/getters
|
114
|
+
required = []
|
115
|
+
sequential = []
|
116
|
+
hash.each do |k,v|
|
117
|
+
if v.class == String || v.class == Fixnum || v.class == Float
|
118
|
+
set_reader klass, k, v, true
|
119
|
+
elsif v.class == Sequential
|
120
|
+
set_sequential_reader klass, k, v
|
121
|
+
else
|
122
|
+
set_accessor klass, k, v
|
123
|
+
end
|
124
|
+
|
125
|
+
required.push(k) unless v.class == Optional || v.class == Sequential
|
126
|
+
sequential.push(k) if v.class == Sequential
|
127
|
+
end
|
128
|
+
|
129
|
+
# set which keys are required for the request
|
130
|
+
set_required(klass, required)
|
131
|
+
set_sequential(klass, sequential)
|
132
|
+
|
133
|
+
# create api method
|
134
|
+
self.class_eval <<-EOS
|
135
|
+
def self.#{name}(hash = {})
|
136
|
+
return #{klass}.new(hash)
|
137
|
+
end
|
138
|
+
EOS
|
139
|
+
end
|
140
|
+
|
141
|
+
# TODO: make this useful :'(
|
142
|
+
# def set_response_signature(hash)
|
143
|
+
# hash.each do |k,v|
|
144
|
+
# set_reader k, v
|
145
|
+
# end
|
146
|
+
# end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Paypal
|
2
|
+
class MassPay < Paypal::Api
|
3
|
+
set_request_signature :mass_pay, {
|
4
|
+
:method => "MassPay",
|
5
|
+
:email_subject => Optional.new(String), # max 255 char
|
6
|
+
:currency_code => Default.new("USD", String),
|
7
|
+
:receiver_type => Default.new("EmailAddress", Enum.new(:email_address => "EmailAddress", :user_id => "UserID")),
|
8
|
+
:payee => Sequential.new({
|
9
|
+
:email => String,
|
10
|
+
:amt => Float,
|
11
|
+
:unique_id => Optional.new(String)
|
12
|
+
}, 250, lambda {|key, i| "L_#{key.to_s.gsub("_","").upcase}#{i}"})
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
class MassPayRequest
|
17
|
+
|
18
|
+
protected
|
19
|
+
def validate!
|
20
|
+
if @payee.length == 0
|
21
|
+
raise Paypal::InvalidRequest, "you pust provide at least one payee"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Paypal
|
2
|
+
class PaymentsPro < Paypal::Api
|
3
|
+
|
4
|
+
set_request_signature :do_direct_payment, {
|
5
|
+
|
6
|
+
# DoDirectPayment Request Fields
|
7
|
+
:method => "DoDirectPayment",
|
8
|
+
:payment_action => Optional.new(Enum.new("Authorization", "Sale")),
|
9
|
+
:ip_address => /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
10
|
+
:return_mf_details => Optional.new(
|
11
|
+
Coerce.new( lambda do |val|
|
12
|
+
return [1, "1", true].include?(val) ? 1 : 0
|
13
|
+
end)
|
14
|
+
),
|
15
|
+
|
16
|
+
|
17
|
+
# Credit Card Details Fields
|
18
|
+
:credit_card_type => Optional.new(Enum.new("Visa", "MasterCard", "Discover", "Amex", "Maestro")),
|
19
|
+
:acct => /\d+/, # this should be better
|
20
|
+
:exp_date => /^\d{6}$/, # "MMYYYY"
|
21
|
+
:cvv2 => /^\d{3,4}$/,
|
22
|
+
:start_date => Optional.new(/^\d{6}$/), # maestro only
|
23
|
+
:issue_number => Optional.new(/^\d{,2}$/), #maestro only
|
24
|
+
|
25
|
+
|
26
|
+
# Payer Information Fields
|
27
|
+
:email => Optional.new(/email/), # max 127 char
|
28
|
+
:first_name => String, # max 25 char
|
29
|
+
:last_name => String, # max 25 char
|
30
|
+
|
31
|
+
|
32
|
+
# Address Fields
|
33
|
+
:street => String, # max 100 char
|
34
|
+
:street_2 => Optional.new(String), # max 100 char
|
35
|
+
:city => String, # max 40 char
|
36
|
+
:state => String, # max 40 char
|
37
|
+
:country_code => Default.new("US", /^[a-z]{2}$/i),
|
38
|
+
:zip => String, # max 20 char
|
39
|
+
:ship_to_phone_num => Optional.new(String), # max 20 char
|
40
|
+
|
41
|
+
|
42
|
+
# Payment Details Fields
|
43
|
+
:amt => Float, # complicated: https://www.x.com/developers/paypal/documentation-tools/api/dodirectpayment-api-operation-nvp
|
44
|
+
:currency_code => Default.new("USD", /^[a-z]{3}$/i),
|
45
|
+
|
46
|
+
# TODO:
|
47
|
+
:item_amt => Optional.new,
|
48
|
+
:shipping_amt => Optional.new,
|
49
|
+
:insurance_amt => Optional.new,
|
50
|
+
:shipdisc_amt => Optional.new,
|
51
|
+
:handling_amt => Optional.new,
|
52
|
+
:tax_amt => Optional.new,
|
53
|
+
|
54
|
+
:desc => Optional.new(String), # max 127 char
|
55
|
+
:custom => Optional.new(String), # max 256 char
|
56
|
+
:inv_num => Optional.new(String), # max 127 char
|
57
|
+
:button_source => Optional.new(String), # max 32 char
|
58
|
+
|
59
|
+
:notify_url => Optional.new, # hard to tell if this is part of this api or not from the wording in the docs
|
60
|
+
:recurring => Default.new("N", lambda {|anything| "Y" }),
|
61
|
+
|
62
|
+
|
63
|
+
# TODO: Payment Details Item Fields
|
64
|
+
# TODO: Ebay Item Payment Details Item Fields
|
65
|
+
# TODO: Ship To Address Fields
|
66
|
+
# TODO: 3D Secure Request Fields (U.K. Merchants Only)
|
67
|
+
|
68
|
+
}
|
69
|
+
|
70
|
+
set_request_signature :do_reference_transaction, {
|
71
|
+
:method => "DoReferenceTransaction",
|
72
|
+
:reference_id => String,
|
73
|
+
:payment_action => Default.new("Sale", Enum.new("Authorization", "Sale")),
|
74
|
+
:return_mf_details => Optional.new(
|
75
|
+
Coerce.new( lambda do |val|
|
76
|
+
return [1, "1", true].include?(val) ? 1 : 0
|
77
|
+
end)
|
78
|
+
),
|
79
|
+
:soft_descriptor =>Optional.new(lambda {|val|
|
80
|
+
if val.match(/^([a-z0-9]|\.|-|\*| )*$/i) && val.length <= 22
|
81
|
+
return true
|
82
|
+
else
|
83
|
+
return false
|
84
|
+
end
|
85
|
+
}),
|
86
|
+
|
87
|
+
# ship to address fields
|
88
|
+
:ship_to_name => Optional.new(String), # max 32
|
89
|
+
:ship_to_street => Optional.new(String), # max 100
|
90
|
+
:ship_to_street_2 => Optional.new(String), # max 100
|
91
|
+
:ship_to_city => Optional.new(String), # max 40
|
92
|
+
:ship_to_state => Optional.new(String), # max 40
|
93
|
+
:ship_to_zip => Optional.new(String), # max 20
|
94
|
+
:ship_to_country => Optional.new(String), # max 2
|
95
|
+
:ship_to_phone_num => Optional.new(/[0-9+-]+/), # max 20
|
96
|
+
|
97
|
+
# payment details fields
|
98
|
+
:amt => Float,
|
99
|
+
:currency_code => Default.new("USD", /^[a-z]{3}$/i),
|
100
|
+
|
101
|
+
# TODO:
|
102
|
+
:item_amt => Optional.new,
|
103
|
+
:shipping_amt => Optional.new,
|
104
|
+
:insurance_amt => Optional.new,
|
105
|
+
:shipdisc_amt => Optional.new,
|
106
|
+
:handling_amt => Optional.new,
|
107
|
+
:tax_amt => Optional.new,
|
108
|
+
|
109
|
+
:desc => Optional.new(String), # max 127 char
|
110
|
+
:custom => Optional.new(String), # max 256 char
|
111
|
+
:inv_num => Optional.new(String), # max 127 char
|
112
|
+
:button_source => Optional.new(String), # max 32 char
|
113
|
+
|
114
|
+
:notify_url => Optional.new, # hard to tell if this is part of this api or not from the wording in the docs
|
115
|
+
:recurring => Default.new("N", lambda {|anything| "Y" }),
|
116
|
+
|
117
|
+
:item => Sequential.new({
|
118
|
+
:l_item_category => Enum.new("Digital", "Physical")
|
119
|
+
})
|
120
|
+
|
121
|
+
}
|
122
|
+
|
123
|
+
set_request_signature :do_capture, {
|
124
|
+
:method => "DoCapture",
|
125
|
+
:authorization_id => String, # max 19 char
|
126
|
+
:amt => Float,
|
127
|
+
:currency_code => Default.new("USD", /^[a-z]{3}$/i),
|
128
|
+
:complete_type => Default.new("Complete", Enum.new("Complete", "NotComplete")),
|
129
|
+
:inv_num => Optional.new(String), # max 127 char
|
130
|
+
:note => Optional.new(String), # max 255 char
|
131
|
+
:soft_descriptor => Optional.new(lambda {|val|
|
132
|
+
if val.match(/^([a-z0-9]|\.|-|\*| )*$/i) && val.length <= 22
|
133
|
+
return true
|
134
|
+
else
|
135
|
+
return false
|
136
|
+
end
|
137
|
+
}),
|
138
|
+
|
139
|
+
:store_id => Optional.new(String), # max 50 char
|
140
|
+
:terminal_id => Optional.new(String) # max 50 char
|
141
|
+
}
|
142
|
+
|
143
|
+
set_request_signature :do_void, {
|
144
|
+
:method => "DoVoid",
|
145
|
+
:authorization_id => String, # Note: If you are voiding a transaction that has been reauthorized, use the ID from the original authorization, and not the reauthorization.
|
146
|
+
:note => Optional.new(String) # max 255 char
|
147
|
+
}
|
148
|
+
|
149
|
+
set_request_signature :get_recurring_payments_profile_details, {
|
150
|
+
:method => "GetRecurringPaymentsProfileDetails",
|
151
|
+
:profile_id => String # max 19 char
|
152
|
+
}
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
|
2
|
+
module Paypal
|
3
|
+
|
4
|
+
class Api
|
5
|
+
|
6
|
+
class Parameter
|
7
|
+
attr_accessor :value
|
8
|
+
|
9
|
+
def initialize(value)
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(anything)
|
14
|
+
@value = anything
|
15
|
+
return @value
|
16
|
+
end
|
17
|
+
|
18
|
+
def parameter_parse(val)
|
19
|
+
if @parameter.class == Class
|
20
|
+
if val.class == @parameter
|
21
|
+
return val
|
22
|
+
else
|
23
|
+
raise Paypal::InvalidParameter, "'#{val}'' is not of type #{@parameter.class}"
|
24
|
+
end
|
25
|
+
elsif @parameter.class == Regexp
|
26
|
+
match = @parameter.match(val)
|
27
|
+
if match
|
28
|
+
return match[0]
|
29
|
+
else
|
30
|
+
raise Paypal::InvalidParameter, "'#{val}' does not match #{@parameter}"
|
31
|
+
end
|
32
|
+
elsif @parameter.class < Parameter
|
33
|
+
return @parameter.parse(val)
|
34
|
+
else
|
35
|
+
raise Paypal::InvalidParameter, "#{@parameter.class} is an invalid parameter specification"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Sequential < Parameter
|
41
|
+
include Paypal::Formatters
|
42
|
+
|
43
|
+
attr_accessor :list
|
44
|
+
|
45
|
+
# allows you to specify an optional key -> paypal_key proc due to nonstandard key formatting
|
46
|
+
def initialize(hash = {}, limit = nil, key_proc = nil)
|
47
|
+
@list = []
|
48
|
+
@schema = hash
|
49
|
+
@required = hash.map{|(k,v)| k if v.class != Optional }.compact
|
50
|
+
@key_proc = key_proc if key_proc
|
51
|
+
@limit = limit
|
52
|
+
end
|
53
|
+
|
54
|
+
# necessary because sequential stores request state, need a new list created for each
|
55
|
+
# request instance
|
56
|
+
def clone
|
57
|
+
return self.class.new(@schema, @limit, @key_proc)
|
58
|
+
end
|
59
|
+
|
60
|
+
def length
|
61
|
+
return @list.length
|
62
|
+
end
|
63
|
+
|
64
|
+
def push(hash)
|
65
|
+
raise Paypal::InvalidParameter, "missing required parameter for sequential field" unless (@required - hash.keys).empty?
|
66
|
+
raise Paypal::InvalidParameter, "field cannot have more than #{@limit} items, #{@list.length} provided" if !@limit.nil? && @list.length == @limit
|
67
|
+
|
68
|
+
hash.each do |k,val|
|
69
|
+
type = @schema[k]
|
70
|
+
|
71
|
+
if type.nil?
|
72
|
+
hash[k] = val
|
73
|
+
elsif type.class == Regexp
|
74
|
+
if match = type.match(val)
|
75
|
+
hash[k] = match[0]
|
76
|
+
else
|
77
|
+
raise Paypal::InvalidParameter, "'#{val}' did not match #{type}"
|
78
|
+
end
|
79
|
+
elsif [Optional, Enum, Coerce, Default].include?(type.class)
|
80
|
+
hash[k] = type.parse(val)
|
81
|
+
elsif type.class == Proc && type.call(val)
|
82
|
+
hash[k] = val
|
83
|
+
elsif type == val.class
|
84
|
+
hash[k] = val
|
85
|
+
else
|
86
|
+
raise Paypal::InvalidParameter, "#{type.class} is an invalid parameter specification"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
@list.push(hash)
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_key(symbol, i)
|
94
|
+
if @key_proc
|
95
|
+
return @key_proc.call(symbol, i)
|
96
|
+
else
|
97
|
+
return symbol.to_s.split("_", 2).map{|s| s.gsub("_", "") }.join("_").gsub(/[^a-z0-9_]/i, "").upcase + "#{i}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_query_string
|
102
|
+
output = ""
|
103
|
+
@list.each_index do |i|
|
104
|
+
@list[i].each do |(k,v)|
|
105
|
+
output = "#{output}&#{to_key(k, i)}=#{escape_uri_component(@list[i][k])}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
return output
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
class Coerce < Parameter
|
114
|
+
|
115
|
+
attr_reader :method
|
116
|
+
|
117
|
+
def initialize(method)
|
118
|
+
@method = method
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse(val)
|
122
|
+
return @method.call(val)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class Enum < Parameter
|
127
|
+
|
128
|
+
# needs to return the exact string if given instead of symbol
|
129
|
+
attr_reader :allowed_values
|
130
|
+
|
131
|
+
def initialize(*values)
|
132
|
+
if values.length == 1 && values[0].is_a?(::Hash)
|
133
|
+
@hash_enum = true
|
134
|
+
@allowed_values = values[0]
|
135
|
+
else
|
136
|
+
@allowed_values = values
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse(val)
|
141
|
+
if @hash_enum
|
142
|
+
if @allowed_values.include?(val)
|
143
|
+
return @allowed_values[val]
|
144
|
+
else
|
145
|
+
raise Paypal::InvalidParameter, "'#{val}' must be a key in #{@allowed_values.keys}"
|
146
|
+
end
|
147
|
+
else
|
148
|
+
if @allowed_values.include?(normalize(val))
|
149
|
+
return normalize(val)
|
150
|
+
else
|
151
|
+
raise Paypal::InvalidParameter, "'#{val}' must be one of #{@allowed_values}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def normalize(val)
|
157
|
+
return val if val.class == String
|
158
|
+
return Paypal::Api.symbol_to_camel(val) if val.class == Symbol
|
159
|
+
return nil
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class Hash < Parameter
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
# Optional and Default can take other parameters as input
|
168
|
+
|
169
|
+
class Optional < Parameter
|
170
|
+
def initialize(parameter = nil)
|
171
|
+
@parameter = parameter.is_a?(Sequential) ? parameter.clone : parameter
|
172
|
+
end
|
173
|
+
|
174
|
+
def parse(val)
|
175
|
+
return parameter_parse(val)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
class Default < Parameter
|
181
|
+
|
182
|
+
def initialize(value, parameter)
|
183
|
+
@value = value
|
184
|
+
@parameter = parameter.is_a?(Sequential) ? parameter.clone : parameter
|
185
|
+
end
|
186
|
+
|
187
|
+
def parse(val)
|
188
|
+
return parameter_parse(val)
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|