recurly 2.20.3 → 3.0.0.beta.1
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.
- checksums.yaml +4 -4
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +158 -110
- data/Rakefile +6 -0
- data/bin/bundle +105 -0
- data/bin/coderay +29 -0
- data/bin/console +14 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/lib/data/ca-certificates.crt +31 -0
- data/lib/recurly/client/operations.rb +935 -0
- data/lib/recurly/client.rb +198 -0
- data/lib/recurly/errors/api_errors.rb +35 -0
- data/lib/recurly/errors/network_errors.rb +8 -0
- data/lib/recurly/errors.rb +34 -0
- data/lib/recurly/pager.rb +119 -0
- data/lib/recurly/request.rb +30 -0
- data/lib/recurly/requests/account_acquisition_updatable.rb +22 -0
- data/lib/recurly/requests/account_create_only.rb +18 -0
- data/lib/recurly/requests/account_updatable.rb +50 -0
- data/lib/recurly/requests/add_on_create.rb +38 -0
- data/lib/recurly/requests/add_on_update.rb +38 -0
- data/lib/recurly/requests/address.rb +42 -0
- data/lib/recurly/requests/billing_info_create.rb +58 -0
- data/lib/recurly/requests/coupon_create_only.rb +66 -0
- data/lib/recurly/requests/coupon_updatable.rb +30 -0
- data/lib/recurly/requests/create_account.rb +62 -0
- data/lib/recurly/requests/create_coupon.rb +90 -0
- data/lib/recurly/requests/invoice_create.rb +42 -0
- data/lib/recurly/requests/invoice_refund.rb +30 -0
- data/lib/recurly/requests/line_item_create.rb +46 -0
- data/lib/recurly/requests/plan_create.rb +66 -0
- data/lib/recurly/requests/plan_update.rb +70 -0
- data/lib/recurly/requests/shipping_address_create.rb +58 -0
- data/lib/recurly/requests/shipping_address_update.rb +62 -0
- data/lib/recurly/requests/subscription_add_on_create.rb +22 -0
- data/lib/recurly/requests/subscription_change_create.rb +42 -0
- data/lib/recurly/requests/subscription_create.rb +86 -0
- data/lib/recurly/requests/subscription_update.rb +42 -0
- data/lib/recurly/requests/update_coupon.rb +30 -0
- data/lib/recurly/resource.rb +16 -1114
- data/lib/recurly/resources/account.rb +86 -0
- data/lib/recurly/resources/account_acquisition.rb +42 -0
- data/lib/recurly/resources/account_balance.rb +22 -0
- data/lib/recurly/resources/account_note.rb +30 -0
- data/lib/recurly/resources/add_on.rb +62 -0
- data/lib/recurly/resources/address.rb +42 -0
- data/lib/recurly/resources/billing_info.rb +62 -0
- data/lib/recurly/resources/coupon.rb +110 -0
- data/lib/recurly/resources/coupon_discount.rb +22 -0
- data/lib/recurly/resources/coupon_redemption.rb +46 -0
- data/lib/recurly/resources/credit_payment.rb +62 -0
- data/lib/recurly/resources/error.rb +18 -0
- data/lib/recurly/resources/error_may_have_transaction.rb +22 -0
- data/lib/recurly/resources/invoice.rb +138 -0
- data/lib/recurly/resources/invoice_collection.rb +18 -0
- data/lib/recurly/resources/line_item.rb +166 -0
- data/lib/recurly/resources/plan.rb +86 -0
- data/lib/recurly/resources/settings.rb +18 -0
- data/lib/recurly/resources/shipping_address.rb +74 -0
- data/lib/recurly/resources/site.rb +46 -0
- data/lib/recurly/resources/subscription.rb +134 -0
- data/lib/recurly/resources/subscription_add_on.rb +42 -0
- data/lib/recurly/resources/subscription_change.rb +54 -0
- data/lib/recurly/resources/tax_info.rb +18 -0
- data/lib/recurly/resources/transaction.rb +146 -0
- data/lib/recurly/resources/unique_coupon_code.rb +38 -0
- data/lib/recurly/resources/user.rb +38 -0
- data/lib/recurly/schema/json_deserializer.rb +53 -0
- data/lib/recurly/schema/json_parser.rb +71 -0
- data/lib/recurly/schema/request_caster.rb +66 -0
- data/lib/recurly/schema/schema_factory.rb +50 -0
- data/lib/recurly/schema/schema_validator.rb +125 -0
- data/lib/recurly/schema.rb +114 -0
- data/lib/recurly/version.rb +1 -10
- data/lib/recurly.rb +14 -167
- data/recurly.gemspec +32 -0
- data/scripts/build +4 -0
- data/scripts/clean +6 -0
- data/scripts/test +3 -0
- metadata +129 -196
- data/lib/recurly/account.rb +0 -230
- data/lib/recurly/account_acquisition.rb +0 -27
- data/lib/recurly/account_balance.rb +0 -23
- data/lib/recurly/add_on.rb +0 -52
- data/lib/recurly/address.rb +0 -25
- data/lib/recurly/adjustment.rb +0 -100
- data/lib/recurly/api/errors.rb +0 -208
- data/lib/recurly/api/net_http_adapter.rb +0 -111
- data/lib/recurly/api.rb +0 -110
- data/lib/recurly/billing_info.rb +0 -134
- data/lib/recurly/business_entity.rb +0 -35
- data/lib/recurly/coupon.rb +0 -136
- data/lib/recurly/credit_payment.rb +0 -32
- data/lib/recurly/currency_percentage_tier.rb +0 -17
- data/lib/recurly/custom_field.rb +0 -15
- data/lib/recurly/custom_field_definition.rb +0 -14
- data/lib/recurly/customer_permission.rb +0 -10
- data/lib/recurly/delivery.rb +0 -19
- data/lib/recurly/dunning_campaign.rb +0 -30
- data/lib/recurly/dunning_cycle.rb +0 -18
- data/lib/recurly/entitlement.rb +0 -19
- data/lib/recurly/error.rb +0 -13
- data/lib/recurly/external_account.rb +0 -17
- data/lib/recurly/external_charge.rb +0 -20
- data/lib/recurly/external_invoice.rb +0 -27
- data/lib/recurly/external_payment_phase.rb +0 -27
- data/lib/recurly/external_product.rb +0 -34
- data/lib/recurly/external_product_reference.rb +0 -18
- data/lib/recurly/external_subscription.rb +0 -62
- data/lib/recurly/gateway_attribute.rb +0 -10
- data/lib/recurly/general_ledger_account.rb +0 -16
- data/lib/recurly/gift_card.rb +0 -85
- data/lib/recurly/helper.rb +0 -51
- data/lib/recurly/invoice.rb +0 -354
- data/lib/recurly/invoice_collection.rb +0 -14
- data/lib/recurly/invoice_template.rb +0 -14
- data/lib/recurly/item.rb +0 -36
- data/lib/recurly/js.rb +0 -14
- data/lib/recurly/juris_detail.rb +0 -15
- data/lib/recurly/measured_unit.rb +0 -16
- data/lib/recurly/money.rb +0 -120
- data/lib/recurly/note.rb +0 -14
- data/lib/recurly/percentage_tier.rb +0 -17
- data/lib/recurly/performance_obligation.rb +0 -15
- data/lib/recurly/plan.rb +0 -59
- data/lib/recurly/plan_ramp_interval.rb +0 -10
- data/lib/recurly/purchase.rb +0 -239
- data/lib/recurly/redemption.rb +0 -46
- data/lib/recurly/resource/association.rb +0 -16
- data/lib/recurly/resource/errors.rb +0 -20
- data/lib/recurly/resource/pager.rb +0 -313
- data/lib/recurly/rev_rec.rb +0 -18
- data/lib/recurly/shipping_address.rb +0 -26
- data/lib/recurly/shipping_fee.rb +0 -17
- data/lib/recurly/shipping_method.rb +0 -13
- data/lib/recurly/sub_add_on_percentage_tier.rb +0 -17
- data/lib/recurly/subscription/add_ons.rb +0 -82
- data/lib/recurly/subscription.rb +0 -374
- data/lib/recurly/subscription_add_on.rb +0 -64
- data/lib/recurly/subscription_ramp_interval.rb +0 -12
- data/lib/recurly/tax_detail.rb +0 -18
- data/lib/recurly/tax_type.rb +0 -13
- data/lib/recurly/tier.rb +0 -18
- data/lib/recurly/transaction/errors.rb +0 -119
- data/lib/recurly/transaction.rb +0 -132
- data/lib/recurly/usage.rb +0 -29
- data/lib/recurly/verify.rb +0 -12
- data/lib/recurly/webhook/account_notification.rb +0 -13
- data/lib/recurly/webhook/billing_info_update_failed_notification.rb +0 -6
- data/lib/recurly/webhook/billing_info_updated_notification.rb +0 -6
- data/lib/recurly/webhook/canceled_account_notification.rb +0 -6
- data/lib/recurly/webhook/canceled_gift_card_notification.rb +0 -6
- data/lib/recurly/webhook/canceled_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/closed_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/closed_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/credit_payment_notification.rb +0 -12
- data/lib/recurly/webhook/deactivated_item_notification.rb +0 -6
- data/lib/recurly/webhook/deleted_shipping_address_notification.rb +0 -6
- data/lib/recurly/webhook/dunning_notification.rb +0 -14
- data/lib/recurly/webhook/expired_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/failed_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/failed_payment_notification.rb +0 -6
- data/lib/recurly/webhook/fraud_info_updated_notification.rb +0 -6
- data/lib/recurly/webhook/gift_card_notification.rb +0 -8
- data/lib/recurly/webhook/invoice_notification.rb +0 -12
- data/lib/recurly/webhook/item_notification.rb +0 -7
- data/lib/recurly/webhook/low_balance_gift_card_notification.rb +0 -6
- data/lib/recurly/webhook/new_account_notification.rb +0 -6
- data/lib/recurly/webhook/new_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/new_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/new_credit_payment_notification.rb +0 -6
- data/lib/recurly/webhook/new_dunning_event_notification.rb +0 -6
- data/lib/recurly/webhook/new_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/new_item_notification.rb +0 -6
- data/lib/recurly/webhook/new_shipping_address_notification.rb +0 -6
- data/lib/recurly/webhook/new_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/new_usage_notification.rb +0 -8
- data/lib/recurly/webhook/notification.rb +0 -18
- data/lib/recurly/webhook/paid_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/past_due_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/past_due_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/paused_subscription_renewal_notification.rb +0 -6
- data/lib/recurly/webhook/prerenewal_notification.rb +0 -6
- data/lib/recurly/webhook/processing_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/processing_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/processing_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/processing_payment_notification.rb +0 -6
- data/lib/recurly/webhook/purchased_gift_card_notification.rb +0 -7
- data/lib/recurly/webhook/reactivated_account_notification.rb +0 -6
- data/lib/recurly/webhook/reactivated_item_notification.rb +0 -6
- data/lib/recurly/webhook/redeemed_gift_card_notification.rb +0 -7
- data/lib/recurly/webhook/regenerated_gift_card_notification.rb +0 -6
- data/lib/recurly/webhook/renewed_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/reopened_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/reopened_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/scheduled_payment_notification.rb +0 -6
- data/lib/recurly/webhook/scheduled_subscription_pause_notification.rb +0 -6
- data/lib/recurly/webhook/scheduled_subscription_update_notification.rb +0 -6
- data/lib/recurly/webhook/subscription_notification.rb +0 -12
- data/lib/recurly/webhook/subscription_pause_canceled_notification.rb +0 -6
- data/lib/recurly/webhook/subscription_pause_modified_notification.rb +0 -6
- data/lib/recurly/webhook/subscription_paused_notification.rb +0 -6
- data/lib/recurly/webhook/subscription_resumed_notification.rb +0 -6
- data/lib/recurly/webhook/successful_payment_notification.rb +0 -6
- data/lib/recurly/webhook/successful_refund_notification.rb +0 -6
- data/lib/recurly/webhook/transaction_authorized_notification.rb +0 -6
- data/lib/recurly/webhook/transaction_notification.rb +0 -12
- data/lib/recurly/webhook/transaction_status_updated_notification.rb +0 -6
- data/lib/recurly/webhook/updated_account_notification.rb +0 -6
- data/lib/recurly/webhook/updated_balance_gift_card_notification.rb +0 -7
- data/lib/recurly/webhook/updated_gift_card_notification.rb +0 -6
- data/lib/recurly/webhook/updated_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/updated_item_notification.rb +0 -6
- data/lib/recurly/webhook/updated_shipping_address_notification.rb +0 -6
- data/lib/recurly/webhook/updated_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/void_payment_notification.rb +0 -6
- data/lib/recurly/webhook/voided_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/voided_credit_payment_notification.rb +0 -6
- data/lib/recurly/webhook.rb +0 -113
- data/lib/recurly/xml/nokogiri.rb +0 -60
- data/lib/recurly/xml/rexml.rb +0 -52
- data/lib/recurly/xml.rb +0 -122
data/lib/recurly/resource.rb
CHANGED
@@ -1,1133 +1,35 @@
|
|
1
|
-
require 'date'
|
2
|
-
require 'erb'
|
3
|
-
require_relative './rev_rec'
|
4
|
-
|
5
1
|
module Recurly
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# {ActiveModel}[http://rubydoc.info/gems/activemodel] classes, especially
|
11
|
-
# like {ActiveRecord}[http://rubydoc.info/gems/activerecord].
|
12
|
-
#
|
13
|
-
# == Life Cycle
|
14
|
-
#
|
15
|
-
# To take you through the typical life cycle of a resource, we'll use
|
16
|
-
# {Recurly::Account} as an example.
|
17
|
-
#
|
18
|
-
# === Creating a Record
|
19
|
-
#
|
20
|
-
# You can instantiate a record before attempting to save it.
|
21
|
-
#
|
22
|
-
# account = Recurly::Account.new :first_name => 'Walter'
|
23
|
-
#
|
24
|
-
# Once instantiated, you can assign and reassign any attribute.
|
25
|
-
#
|
26
|
-
# account.first_name = 'Walt'
|
27
|
-
# account.last_name = 'White'
|
28
|
-
#
|
29
|
-
# When you're ready to save, do so.
|
30
|
-
#
|
31
|
-
# account.save # => false
|
32
|
-
#
|
33
|
-
# If save returns +false+, validation likely failed. You can check the record
|
34
|
-
# for errors.
|
35
|
-
#
|
36
|
-
# account.errors # => {"account_code"=>["can't be blank"]}
|
37
|
-
#
|
38
|
-
# Once the errors are fixed, you can try again.
|
39
|
-
#
|
40
|
-
# account.account_code = 'heisenberg'
|
41
|
-
# account.save # => true
|
42
|
-
#
|
43
|
-
# The object will be updated with any information provided by the server
|
44
|
-
# (including any UUIDs set).
|
45
|
-
#
|
46
|
-
# account.created_at # => 2011-04-30 07:13:35 -0700
|
47
|
-
#
|
48
|
-
# You can also create accounts in one fell swoop.
|
49
|
-
#
|
50
|
-
# Recurly::Account.create(
|
51
|
-
# :first_name => 'Jesse'
|
52
|
-
# :last_name => 'Pinkman'
|
53
|
-
# :account_code => 'capn_cook'
|
54
|
-
# )
|
55
|
-
# # => #<Recurly::Account account_code: "capn_cook" ...>
|
56
|
-
#
|
57
|
-
# You can use alternative "bang" methods for exception control. If the record
|
58
|
-
# fails to save, a Recurly::Resource::Invalid exception will be raised.
|
59
|
-
#
|
60
|
-
# begin
|
61
|
-
# account = Recurly::Account.new :first_name => 'Junior'
|
62
|
-
# account.save!
|
63
|
-
# rescue Recurly::Resource::Invalid
|
64
|
-
# p account.errors
|
65
|
-
# end
|
66
|
-
#
|
67
|
-
# You can access the invalid record from the exception itself (if, for
|
68
|
-
# example, you use the <tt>create!</tt> method).
|
69
|
-
#
|
70
|
-
# begin
|
71
|
-
# Recurly::Account.create! :first_name => 'Skylar', :last_name => 'White'
|
72
|
-
# rescue Recurly::Resource::Invalid => e
|
73
|
-
# p e.record.errors
|
74
|
-
# end
|
75
|
-
#
|
76
|
-
# === Fetching a Record
|
77
|
-
#
|
78
|
-
# Records are fetched by their unique identifiers.
|
79
|
-
#
|
80
|
-
# account = Recurly::Account.find 'better_call_saul'
|
81
|
-
# # => #<Recurly::Account account_code: "better_call_saul" ...>
|
82
|
-
#
|
83
|
-
# If the record doesn't exist, a Recurly::Resource::NotFound exception will
|
84
|
-
# be raised.
|
85
|
-
#
|
86
|
-
# === Updating a Record
|
87
|
-
#
|
88
|
-
# Once fetched, a record can be updated with a hash of attributes.
|
89
|
-
#
|
90
|
-
# account.update_attributes :first_name => 'Saul', :last_name => 'Goodman'
|
91
|
-
# # => true
|
92
|
-
#
|
93
|
-
# (A bang method, update_attributes!, will raise Recurly::Resource::Invalid.)
|
94
|
-
#
|
95
|
-
# You can also update a record by setting attributes and calling save.
|
96
|
-
#
|
97
|
-
# account.last_name = 'McGill'
|
98
|
-
# account.save # Alternatively, call save!
|
99
|
-
#
|
100
|
-
# === Deleting a Record
|
101
|
-
#
|
102
|
-
# To delete (deactivate, close, etc.) a fetched record, merely call destroy
|
103
|
-
# on it.
|
104
|
-
#
|
105
|
-
# account.destroy # => true
|
106
|
-
#
|
107
|
-
# === Fetching a List of Records
|
108
|
-
#
|
109
|
-
# If you want to iterate over a list of accounts, you can use a Pager.
|
110
|
-
#
|
111
|
-
# pager = Account.paginate :per_page => 50
|
112
|
-
#
|
113
|
-
# If you want to iterate over _every_ record, a convenience method will
|
114
|
-
# automatically paginate:
|
115
|
-
#
|
116
|
-
# Account.find_each { |account| p account }
|
2
|
+
# This class represents an object as it exists on the
|
3
|
+
# Recurly servers. It is generated from a response. If you wish to
|
4
|
+
# update or change a resource, you need to send a request to the server
|
5
|
+
# and get a new Resource.
|
117
6
|
class Resource
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
# Raised when a record cannot be found.
|
123
|
-
#
|
124
|
-
# @example
|
125
|
-
# begin
|
126
|
-
# Recurly::Account.find 'tortuga'
|
127
|
-
# rescue Recurly::Resource::NotFound => e
|
128
|
-
# e.message # => "Can't find Account with account_code = tortuga"
|
129
|
-
# end
|
130
|
-
class NotFound < API::NotFound
|
131
|
-
def initialize(message)
|
132
|
-
set_message message
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
# Raised when a record is invalid.
|
137
|
-
#
|
138
|
-
# @example
|
139
|
-
# begin
|
140
|
-
# Recurly::Account.create! :first_name => "Flynn"
|
141
|
-
# rescue Recurly::Resource::Invalid => e
|
142
|
-
# e.record.errors # => errors: {"account_code"=>["can't be blank"]}>
|
143
|
-
# end
|
144
|
-
class Invalid < Error
|
145
|
-
# @return [Resource, nil] The invalid record.
|
146
|
-
attr_reader :record
|
147
|
-
|
148
|
-
def initialize(message)
|
149
|
-
if message.is_a? Resource
|
150
|
-
@record = message
|
151
|
-
set_message(record_to_message)
|
152
|
-
else
|
153
|
-
set_message(message)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
private
|
158
|
-
|
159
|
-
def record_to_message
|
160
|
-
@record.errors.map do |k, v|
|
161
|
-
message = v.join(', ')
|
162
|
-
k == 'base' ? message : "#{k} #{message}"
|
163
|
-
end.join('; ')
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
class << self
|
168
|
-
# @return [String] The demodulized name of the resource class.
|
169
|
-
# @example
|
170
|
-
# Recurly::Account.name # => "Account"
|
171
|
-
def resource_name
|
172
|
-
Helper.demodulize name
|
173
|
-
end
|
174
|
-
|
175
|
-
# @return [String] The underscored, pluralized name of the resource
|
176
|
-
# class.
|
177
|
-
# @example
|
178
|
-
# Recurly::Account.collection_name # => "accounts"
|
179
|
-
def collection_name
|
180
|
-
Helper.pluralize Helper.underscore(resource_name)
|
181
|
-
end
|
182
|
-
alias collection_path collection_name
|
183
|
-
|
184
|
-
# @return [String] The underscored name of the resource class.
|
185
|
-
# @example
|
186
|
-
# Recurly::Account.member_name # => "account"
|
187
|
-
def member_name
|
188
|
-
Helper.underscore resource_name
|
189
|
-
end
|
190
|
-
|
191
|
-
# @return [String] The relative path to a resource's identifier from the
|
192
|
-
# API's base URI.
|
193
|
-
# @param uuid [String, nil]
|
194
|
-
# @example
|
195
|
-
# Recurly::Account.member_path "code" # => "accounts/code"
|
196
|
-
# Recurly::Account.member_path nil # => "accounts"
|
197
|
-
def member_path(uuid)
|
198
|
-
uuid = ERB::Util.url_encode(uuid) if uuid
|
199
|
-
[collection_path, uuid].compact.join '/'
|
200
|
-
end
|
201
|
-
|
202
|
-
# @return [String] The root key for this resource's xml document
|
203
|
-
def xml_root_key
|
204
|
-
self.member_name
|
205
|
-
end
|
206
|
-
|
207
|
-
# @return [Array] Per attribute, defines readers, writers, boolean and
|
208
|
-
# change-tracking methods.
|
209
|
-
# @param attribute_names [Array] An array of attribute names.
|
210
|
-
# @example
|
211
|
-
# class Account < Resource
|
212
|
-
# define_attribute_methods [:name]
|
213
|
-
# end
|
214
|
-
#
|
215
|
-
# a = Account.new
|
216
|
-
# a.name? # => false
|
217
|
-
# a.name # => nil
|
218
|
-
# a.name = "Stephen"
|
219
|
-
# a.name? # => true
|
220
|
-
# a.name # => "Stephen"
|
221
|
-
# a.name_changed? # => true
|
222
|
-
# a.name_was # => nil
|
223
|
-
# a.name_change # => [nil, "Stephen"]
|
224
|
-
def define_attribute_methods(attribute_names)
|
225
|
-
@attribute_names = attribute_names.map! { |m| m.to_s }.sort!.freeze
|
226
|
-
remove_const :AttributeMethods if constants.include? :AttributeMethods
|
227
|
-
include const_set :AttributeMethods, Module.new {
|
228
|
-
attribute_names.each do |name|
|
229
|
-
define_method(name) { self[name] } # Get.
|
230
|
-
define_method("#{name}=") { |value| self[name] = value } # Set.
|
231
|
-
define_method("#{name}?") { !!self[name] } # Present.
|
232
|
-
define_method("#{name}_change") { changes[name] } # Dirt...
|
233
|
-
define_method("#{name}_changed?") { changed_attributes.key? name }
|
234
|
-
define_method("#{name}_was") { changed_attributes[name] }
|
235
|
-
define_method("#{name}_previously_changed?") {
|
236
|
-
previous_changes.key? name
|
237
|
-
}
|
238
|
-
define_method("#{name}_previously_was") {
|
239
|
-
previous_changes[name].first if previous_changes.key? name
|
240
|
-
}
|
241
|
-
end
|
242
|
-
}
|
243
|
-
end
|
244
|
-
|
245
|
-
# @return [Array, nil] The list of attribute names defined for the
|
246
|
-
# resource class.
|
247
|
-
attr_reader :attribute_names
|
248
|
-
|
249
|
-
# @return [Pager] A pager with an iterable collection of records
|
250
|
-
# @param options [Hash] A hash of pagination options
|
251
|
-
# @option options [Integer] :per_page The number of records returned per
|
252
|
-
# page
|
253
|
-
# @option options [DateTime, Time, Integer] :cursor A timestamp that the
|
254
|
-
# pager will skim back to and return records created before it
|
255
|
-
# @option options [String] :etag When set, will raise
|
256
|
-
# {Recurly::API::NotModified} if the pager's loaded page content has
|
257
|
-
# not changed
|
258
|
-
# @example Fetch 50 records and iterate over them
|
259
|
-
# Recurly::Account.paginate(:per_page => 50).each { |a| p a }
|
260
|
-
# @example Fetch records before January 1, 2011
|
261
|
-
# Recurly::Account.paginate(:cursor => Time.new(2011, 1, 1))
|
262
|
-
def paginate(options = {})
|
263
|
-
Pager.new self, options
|
264
|
-
end
|
265
|
-
alias scoped paginate
|
266
|
-
alias where paginate
|
267
|
-
|
268
|
-
def all(options = {})
|
269
|
-
paginate(options).to_a
|
270
|
-
end
|
271
|
-
|
272
|
-
# @return [Hash] Defined scopes per resource.
|
273
|
-
def scopes
|
274
|
-
@scopes ||= Recurly::Helper.hash_with_indifferent_read_access
|
275
|
-
end
|
276
|
-
|
277
|
-
# @return [Module] Module of scopes methods.
|
278
|
-
def scopes_helper
|
279
|
-
@scopes_helper ||= Module.new.tap { |helper| extend helper }
|
280
|
-
end
|
281
|
-
|
282
|
-
# Defines a new resource scope.
|
283
|
-
#
|
284
|
-
# @return [Proc]
|
285
|
-
# @param [Symbol] name the scope name
|
286
|
-
# @param [Hash] params the scope params
|
287
|
-
def scope(name, params = {})
|
288
|
-
scopes[name = name.to_s] = params
|
289
|
-
scopes_helper.send(:define_method, name) { paginate scopes[name] }
|
290
|
-
end
|
291
|
-
|
292
|
-
# Iterates through every record by automatically paging.
|
293
|
-
#
|
294
|
-
# @option options [Hash] Optional hash to pass to Pager#paginate
|
295
|
-
#
|
296
|
-
# @return [nil]
|
297
|
-
# @param [Integer] per_page The number of records returned per request.
|
298
|
-
# @yield [record]
|
299
|
-
# @see Pager#paginate
|
300
|
-
# @example
|
301
|
-
# Recurly::Account.find_each { |a| p a }
|
302
|
-
# @example With sorting and filter
|
303
|
-
# opts = {
|
304
|
-
# begin_time: DateTime.new(2016,1,1),
|
305
|
-
# sort: :updated_at
|
306
|
-
# }
|
307
|
-
# Recurly::Account.find_each(opts) do |a|
|
308
|
-
# puts a.inspect
|
309
|
-
# end
|
310
|
-
def find_each(options = {}, &block)
|
311
|
-
paginate(options).find_each(&block)
|
312
|
-
end
|
313
|
-
|
314
|
-
# @return [Integer] The total record count of the resource in question.
|
315
|
-
# @see Pager#count
|
316
|
-
# @example
|
317
|
-
# Recurly::Account.count # => 42
|
318
|
-
def count
|
319
|
-
paginate.count
|
320
|
-
end
|
321
|
-
|
322
|
-
# @api internal
|
323
|
-
# @return [Resource, nil]
|
324
|
-
def first
|
325
|
-
paginate(:per_page => 1).first
|
326
|
-
end
|
327
|
-
|
328
|
-
# @return [Resource] A record matching the designated unique identifier.
|
329
|
-
# @param [String] uuid The unique identifier of the resource to be
|
330
|
-
# retrieved.
|
331
|
-
# @param [Hash] options A hash of options.
|
332
|
-
# @option options [String] :etag When set, will raise {API::NotModified}
|
333
|
-
# if the record content has not changed.
|
334
|
-
# @raise [Error] If the resource has no identifier (and thus cannot be
|
335
|
-
# retrieved).
|
336
|
-
# @raise [NotFound] If no resource can be found for the supplied
|
337
|
-
# identifier (or the supplied identifier is +nil+).
|
338
|
-
# @raise [API::NotModified] If the <tt>:etag</tt> option is set and
|
339
|
-
# matches the server's.
|
340
|
-
# @example
|
341
|
-
# Recurly::Account.find "heisenberg"
|
342
|
-
# # => #<Recurly::Account account_code: "heisenberg", ...>
|
343
|
-
# Use the following identifiers for these types of objects:
|
344
|
-
# for accounts use account_code
|
345
|
-
# for plans use plan_code
|
346
|
-
# for invoices use invoice_number
|
347
|
-
# for subscriptions use uuid
|
348
|
-
# for transactions use uuid
|
349
|
-
def find(uuid, options = {})
|
350
|
-
if uuid.nil? || uuid.to_s.empty?
|
351
|
-
raise NotFound, "can't find a record with nil identifier"
|
352
|
-
end
|
353
|
-
|
354
|
-
begin
|
355
|
-
from_response API.get(member_path(uuid), {}, options)
|
356
|
-
rescue API::NotFound => e
|
357
|
-
raise NotFound, e.description
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
|
-
# Instantiates and attempts to save a record.
|
362
|
-
#
|
363
|
-
# @return [Resource] The record.
|
364
|
-
# @raise [Transaction::Error] A monetary transaction failed.
|
365
|
-
# @see create!
|
366
|
-
def create(attributes = {})
|
367
|
-
new(attributes) { |record| record.save }
|
368
|
-
end
|
369
|
-
|
370
|
-
# Instantiates and attempts to save a record.
|
371
|
-
#
|
372
|
-
# @return [Resource] The saved record.
|
373
|
-
# @raise [Invalid] The record is invalid.
|
374
|
-
# @raise [Transaction::Error] A monetary transaction failed.
|
375
|
-
# @see create
|
376
|
-
def create!(attributes = {})
|
377
|
-
new(attributes) { |record| record.save! }
|
378
|
-
end
|
379
|
-
|
380
|
-
# Instantiates a record from an HTTP response, setting the record's
|
381
|
-
# response attribute in the process.
|
382
|
-
#
|
383
|
-
# @return [Resource]
|
384
|
-
# @param response [Net::HTTPResponse]
|
385
|
-
def from_response(response)
|
386
|
-
content_type = response['Content-Type']
|
387
|
-
|
388
|
-
case content_type
|
389
|
-
when %r{application/pdf}
|
390
|
-
response.body
|
391
|
-
when %r{application/xml}
|
392
|
-
record = from_xml response.body
|
393
|
-
record.instance_eval { @etag, @response = response['ETag'], response }
|
394
|
-
record
|
395
|
-
else
|
396
|
-
raise Recurly::Error, "Content-Type \"#{content_type}\" is not accepted"
|
397
|
-
end
|
398
|
-
end
|
399
|
-
|
400
|
-
# Instantiates a record from an XML blob: either a String or XML element.
|
401
|
-
#
|
402
|
-
# Assuming the record is from an API response, the record is flagged as
|
403
|
-
# persisted.
|
404
|
-
#
|
405
|
-
# @return [Resource]
|
406
|
-
# @param xml [String, REXML::Element, Nokogiri::XML::Node]
|
407
|
-
# @see from_response
|
408
|
-
def from_xml(xml)
|
409
|
-
xml = XML.new xml
|
410
|
-
record = new
|
411
|
-
|
412
|
-
xml.root.attributes.each do |name, value|
|
413
|
-
record.instance_variable_set "@#{name}", value.to_s
|
414
|
-
end
|
415
|
-
|
416
|
-
xml.each_element do |el|
|
417
|
-
# skip this element if it's an xml comment
|
418
|
-
next if defined?(Nokogiri::XML::Node::TEXT_NODE) && el.is_a?(Nokogiri::XML::Comment)
|
419
|
-
|
420
|
-
if el.name == 'a'
|
421
|
-
record.links[el.attribute('name').value] = {
|
422
|
-
:method => el.attribute('method').to_s,
|
423
|
-
:href => el.attribute('href').value
|
424
|
-
}
|
425
|
-
next
|
426
|
-
end
|
7
|
+
extend Schema::SchemaFactory
|
8
|
+
extend Schema::JsonDeserializer
|
9
|
+
include Schema::SchemaValidator
|
427
10
|
|
428
|
-
# Nokogiri on Jruby-1.7.19 likes to throw NullPointer exceptions
|
429
|
-
# if you try to run certian operations like el.attribute(''). Since
|
430
|
-
# we dont care about text nodes, let's just skip them
|
431
|
-
next if defined?(Nokogiri::XML::Node::TEXT_NODE) && el.node_type == Nokogiri::XML::Node::TEXT_NODE
|
432
|
-
|
433
|
-
if association = find_association(el.name)
|
434
|
-
class_name = association_class_name(association, el.name)
|
435
|
-
resource_class = Recurly.const_get(class_name)
|
436
|
-
is_many = association.relation == :has_many
|
437
|
-
|
438
|
-
# Is this a link, or is it embedded data?
|
439
|
-
if el.children.empty? && href = el.attribute('href')
|
440
|
-
if is_many
|
441
|
-
record[el.name] = Pager.new(
|
442
|
-
resource_class, :uri => href.value, :parent => record
|
443
|
-
)
|
444
|
-
else
|
445
|
-
record.links[el.name] = {
|
446
|
-
:resource_class => resource_class,
|
447
|
-
:method => :get,
|
448
|
-
:href => href.value
|
449
|
-
}
|
450
|
-
end
|
451
|
-
else
|
452
|
-
if is_many
|
453
|
-
resources = el.elements.map { |e| resource_class.from_xml(e) }
|
454
|
-
record[el.name] = resources
|
455
|
-
else
|
456
|
-
record[el.name] = resource_class.from_xml(el)
|
457
|
-
end
|
458
|
-
end
|
459
|
-
else
|
460
|
-
# TODO name tax_type conflicts with the TaxType
|
461
|
-
# class so if we get to this point was can assume
|
462
|
-
# it's the string. Will need to refactor this
|
463
|
-
if el.name == 'tax_type'
|
464
|
-
record[el.name] = el.text
|
465
|
-
else
|
466
|
-
val = XML.cast(el)
|
467
|
-
|
468
|
-
# TODO we have to clear changed attributes after
|
469
|
-
# parsing here or else it always serializes. Need
|
470
|
-
# a better way of handling changed attributes
|
471
|
-
if el.name == 'address' && val.kind_of?(Hash)
|
472
|
-
address = Address.new(val)
|
473
|
-
address.instance_variable_set(:@changed_attributes, {})
|
474
|
-
record[el.name] = address
|
475
|
-
else
|
476
|
-
record[el.name] = val
|
477
|
-
end
|
478
|
-
end
|
479
|
-
end
|
480
|
-
end
|
481
|
-
|
482
|
-
record.persist! if record.respond_to? :persist!
|
483
|
-
record
|
484
|
-
end
|
485
|
-
|
486
|
-
# @return [Array] A list of associations for the current class.
|
487
|
-
def associations
|
488
|
-
@associations ||= []
|
489
|
-
end
|
490
|
-
|
491
|
-
# @return [Array] A list of associated resource classes with
|
492
|
-
# the relation [:has_many, :has_one, :belongs_to] for the current class.
|
493
|
-
def associations_for_relation(relation)
|
494
|
-
associations.select{ |a| a.relation == relation }.map(&:resource_class)
|
495
|
-
end
|
496
|
-
|
497
|
-
def association_class_name(association, el_name)
|
498
|
-
return association.class_name if association.class_name
|
499
|
-
Helper.classify(el_name)
|
500
|
-
end
|
501
|
-
|
502
|
-
# @return [Association, nil] Find association for the current class
|
503
|
-
# with resource class name.
|
504
|
-
def find_association(resource_class)
|
505
|
-
associations.find{ |a| a.resource_class.to_s == resource_class.to_s }
|
506
|
-
end
|
507
|
-
|
508
|
-
def associations_helper
|
509
|
-
@associations_helper ||= Module.new.tap { |helper| include helper }
|
510
|
-
end
|
511
|
-
|
512
|
-
# Establishes a has_many association.
|
513
|
-
#
|
514
|
-
# @return [Proc, nil]
|
515
|
-
# @param collection_name [Symbol] Association name.
|
516
|
-
# @param options [Hash] A hash of association options.
|
517
|
-
# @option options [true, false] :readonly Define a setter when false, defaults to true
|
518
|
-
# [String] :class_name Actual associated resource class name
|
519
|
-
# if not same as collection_name.
|
520
|
-
def has_many(collection_name, options = {})
|
521
|
-
associations << Association.new(:has_many, collection_name.to_s, options)
|
522
|
-
associations_helper.module_eval do
|
523
|
-
define_method collection_name do
|
524
|
-
if self[collection_name]
|
525
|
-
self[collection_name]
|
526
|
-
else
|
527
|
-
attributes[collection_name.to_s] = []
|
528
|
-
end
|
529
|
-
end
|
530
|
-
if options.key?(:readonly) && options[:readonly] == false
|
531
|
-
define_method "#{collection_name}=" do |collection|
|
532
|
-
self[collection_name] = collection
|
533
|
-
end
|
534
|
-
end
|
535
|
-
end
|
536
|
-
end
|
537
|
-
|
538
|
-
# Establishes a has_one association.
|
539
|
-
#
|
540
|
-
# @return [Proc, nil]
|
541
|
-
# @param member_name [Symbol] Association name.
|
542
|
-
# @param options [Hash] A hash of association options.
|
543
|
-
# @option options [true, false] :readonly Don't define a setter.
|
544
|
-
# [String] :class_name Actual associated resource class name
|
545
|
-
# if not same as member_name.
|
546
|
-
def has_one(member_name, options = {})
|
547
|
-
associations << Association.new(:has_one, member_name.to_s, options)
|
548
|
-
associations_helper.module_eval do
|
549
|
-
define_method(member_name) { self[member_name] }
|
550
|
-
if options.key?(:readonly) && options[:readonly] == false
|
551
|
-
associated = Recurly.const_get Helper.classify(member_name), false
|
552
|
-
define_method "#{member_name}=" do |member|
|
553
|
-
associated_uri = "#{path}/#{member_name}"
|
554
|
-
self[member_name] = case member
|
555
|
-
when Hash
|
556
|
-
associated.send :new, member.merge(:uri => associated_uri)
|
557
|
-
when associated
|
558
|
-
member.uri = associated_uri and member
|
559
|
-
else
|
560
|
-
raise ArgumentError, "expected #{associated}"
|
561
|
-
end
|
562
|
-
end
|
563
|
-
define_method "build_#{member_name}" do |*args|
|
564
|
-
attributes = args.shift || {}
|
565
|
-
self[member_name] = associated.send(
|
566
|
-
:new, attributes.merge(:uri => "#{path}/#{associated.member_name}")
|
567
|
-
).tap { |child| child.attributes[self.class.member_name] = self }
|
568
|
-
end
|
569
|
-
define_method "create_#{member_name}" do |*args|
|
570
|
-
send("build_#{member_name}", *args).tap { |child| child.save }
|
571
|
-
end
|
572
|
-
end
|
573
|
-
end
|
574
|
-
end
|
575
|
-
|
576
|
-
# Establishes a belongs_to association.
|
577
|
-
#
|
578
|
-
# @return [Proc]
|
579
|
-
# @param parent_name [Symbol] Association name.
|
580
|
-
# @param options [Hash] A hash of association options.
|
581
|
-
# @option options [true, false] :readonly Don't define a setter.
|
582
|
-
# [String] :class_name Actual associated resource class name
|
583
|
-
# if not same as parent_name.
|
584
|
-
def belongs_to(parent_name, options = {})
|
585
|
-
associations << Association.new(:belongs_to, parent_name.to_s, options)
|
586
|
-
associations_helper.module_eval do
|
587
|
-
define_method(parent_name) { self[parent_name] }
|
588
|
-
if options.key?(:readonly) && options[:readonly] == false
|
589
|
-
define_method "#{parent_name}=" do |parent|
|
590
|
-
self[parent_name] = parent
|
591
|
-
end
|
592
|
-
end
|
593
|
-
end
|
594
|
-
end
|
595
|
-
|
596
|
-
# @return [:has_many, :has_one, :belongs_to, nil] An association type.
|
597
|
-
def reflect_on_association(name)
|
598
|
-
a = find_association(name)
|
599
|
-
a.relation if a
|
600
|
-
end
|
601
|
-
|
602
|
-
def embedded!(root_index = false)
|
603
|
-
protected :initialize
|
604
|
-
private_class_method(*%w(create create!))
|
605
|
-
unless root_index
|
606
|
-
private_class_method(*%w(all find_each first paginate scoped where))
|
607
|
-
end
|
608
|
-
end
|
609
|
-
|
610
|
-
def find_resource_class(name)
|
611
|
-
resource_name = Helper.classify(name)
|
612
|
-
if Recurly.const_defined?(resource_name, false) && Recurly.const_get(resource_name, false).instance_of?(Class)
|
613
|
-
Recurly.const_get(resource_name, false)
|
614
|
-
end
|
615
|
-
end
|
616
|
-
end
|
617
|
-
|
618
|
-
# @return [Hash] The raw hash of record attributes.
|
619
11
|
attr_reader :attributes
|
620
12
|
|
621
|
-
|
622
|
-
# record (updated during {#save} and {#destroy}).
|
623
|
-
attr_reader :response
|
624
|
-
|
625
|
-
# @return [String, nil] An ETag for the current record.
|
626
|
-
attr_reader :etag
|
627
|
-
|
628
|
-
# @return [String, nil] A writer to override the URI the record saves to.
|
629
|
-
attr_writer :uri
|
630
|
-
|
631
|
-
# @return [Resource] A new resource instance.
|
632
|
-
# @param attributes [Hash] A hash of attributes.
|
633
|
-
def initialize(attributes = {})
|
634
|
-
if instance_of? Resource
|
635
|
-
raise Error,
|
636
|
-
"#{self.class} is an abstract class and cannot be instantiated"
|
637
|
-
end
|
638
|
-
|
639
|
-
@attributes, @new_record, @destroyed, @uri, @href = {}, true, false
|
640
|
-
self.attributes = attributes
|
641
|
-
yield self if block_given?
|
642
|
-
end
|
643
|
-
|
644
|
-
# @return [self] Reloads the record from the server.
|
645
|
-
def reload(response = nil)
|
646
|
-
if response
|
647
|
-
return if response.body.to_s.length.zero?
|
648
|
-
fresh = self.class.from_response response
|
649
|
-
else
|
650
|
-
options = {:etag => (etag unless changed?)}
|
651
|
-
fresh = if @href
|
652
|
-
self.class.from_response API.get(@href, {}, options)
|
653
|
-
else
|
654
|
-
self.class.find(to_param, options)
|
655
|
-
end
|
656
|
-
end
|
657
|
-
fresh and copy_from fresh
|
658
|
-
persist! true
|
659
|
-
self
|
660
|
-
rescue API::NotModified
|
661
|
-
self
|
662
|
-
end
|
663
|
-
|
664
|
-
# @return [Hash] Hash of changed attributes.
|
665
|
-
# @see #changes
|
666
|
-
def changed_attributes
|
667
|
-
@changed_attributes ||= {}
|
668
|
-
end
|
669
|
-
|
670
|
-
# @return [Array] A list of changed attribute keys.
|
671
|
-
def changed
|
672
|
-
changed_attributes.keys
|
673
|
-
end
|
674
|
-
|
675
|
-
# Do any attributes have unsaved changes?
|
676
|
-
# @return [true, false]
|
677
|
-
def changed?
|
678
|
-
!changed_attributes.empty?
|
679
|
-
end
|
680
|
-
|
681
|
-
# @return [Hash] Map of changed attributes to original value and new value.
|
682
|
-
def changes
|
683
|
-
changed_attributes.inject({}) { |changes, (key, original_value)|
|
684
|
-
changes[key] = [original_value, self[key]] and changes
|
685
|
-
}
|
686
|
-
end
|
687
|
-
|
688
|
-
# @return [Hash] Previously-changed attributes.
|
689
|
-
# @see #changes
|
690
|
-
def previous_changes
|
691
|
-
@previous_changes ||= {}
|
692
|
-
end
|
693
|
-
|
694
|
-
# Is the record new (i.e., not saved on Recurly's servers)?
|
695
|
-
#
|
696
|
-
# @return [true, false]
|
697
|
-
# @see #persisted?
|
698
|
-
# @see #destroyed?
|
699
|
-
def new_record?
|
700
|
-
@new_record
|
701
|
-
end
|
702
|
-
|
703
|
-
# Has the record been destroyed? (Set +true+ after a successful destroy.)
|
704
|
-
# @return [true, false]
|
705
|
-
# @see #new_record?
|
706
|
-
# @see #persisted?
|
707
|
-
def destroyed?
|
708
|
-
@destroyed
|
709
|
-
end
|
710
|
-
|
711
|
-
# Has the record persisted (i.e., saved on Recurly's servers)?
|
712
|
-
#
|
713
|
-
# @return [true, false]
|
714
|
-
# @see #new_record?
|
715
|
-
# @see #destroyed?
|
716
|
-
def persisted?
|
717
|
-
!(new_record? || destroyed?)
|
718
|
-
end
|
719
|
-
|
720
|
-
# The value of a specified attribute, lazily fetching any defined
|
721
|
-
# association.
|
722
|
-
#
|
723
|
-
# @param key [Symbol, String] The name of the attribute to be fetched.
|
724
|
-
# @example
|
725
|
-
# account.read_attribute :first_name # => "Ted"
|
726
|
-
# account[:last_name] # => "Beneke"
|
727
|
-
# @see #write_attribute
|
728
|
-
def read_attribute(key)
|
729
|
-
key = key.to_s
|
730
|
-
if attributes.key? key
|
731
|
-
value = attributes[key]
|
732
|
-
elsif links.key?(key) && self.class.reflect_on_association(key)
|
733
|
-
value = attributes[key] = follow_link key
|
734
|
-
end
|
735
|
-
value
|
736
|
-
end
|
737
|
-
alias [] read_attribute
|
738
|
-
|
739
|
-
# Sets the value of a specified attribute.
|
740
|
-
#
|
741
|
-
# @param key [Symbol, String] The name of the attribute to be set.
|
742
|
-
# @param value [Object] The value the attribute will be set to.
|
743
|
-
# @example
|
744
|
-
# account.write_attribute :first_name, 'Gus'
|
745
|
-
# account[:company_name] = 'Los Pollos Hermanos'
|
746
|
-
# @see #read_attribute
|
747
|
-
def write_attribute(key, value)
|
748
|
-
if changed_attributes.key?(key = key.to_s)
|
749
|
-
changed_attributes.delete key if changed_attributes[key] == value
|
750
|
-
elsif self[key] != value
|
751
|
-
changed_attributes[key] = self[key]
|
752
|
-
end
|
753
|
-
|
754
|
-
association = self.class.find_association(key)
|
755
|
-
if association
|
756
|
-
value = fetch_associated(key, value)
|
757
|
-
# FIXME: More explicit; less magic.
|
758
|
-
elsif add_money_tag?(key, value)
|
759
|
-
value = Money.new(value, self, key)
|
760
|
-
end
|
761
|
-
|
762
|
-
attributes[key] = value
|
763
|
-
end
|
764
|
-
alias []= write_attribute
|
765
|
-
|
766
|
-
# Apply a given hash of attributes to a record.
|
767
|
-
#
|
768
|
-
# @return [Hash]
|
769
|
-
# @param attributes [Hash] A hash of attributes.
|
770
|
-
def attributes=(attributes = {})
|
771
|
-
attributes.each_pair { |k, v|
|
772
|
-
respond_to?(name = "#{k}=") and send(name, v) or self[k] = v
|
773
|
-
}
|
774
|
-
end
|
775
|
-
|
776
|
-
def as_json(options = nil)
|
777
|
-
attributes.reject { |k, v| v.is_a?(Recurly::Resource::Pager) }
|
778
|
-
end
|
779
|
-
|
780
|
-
# @return [Hash] The raw hash of record href links.
|
781
|
-
def links
|
782
|
-
@links ||= {}
|
783
|
-
end
|
784
|
-
|
785
|
-
# Whether a record has a link with the given name.
|
786
|
-
#
|
787
|
-
# @param key [Symbol, String] The name of the link to check for.
|
788
|
-
# @example
|
789
|
-
# account.link? :billing_info # => true
|
790
|
-
def link?(key)
|
791
|
-
links.key?(key.to_s)
|
792
|
-
end
|
793
|
-
|
794
|
-
# Fetch the value of a link by following the associated href.
|
795
|
-
#
|
796
|
-
# @param key [Symbol, String] The name of the link to be followed.
|
797
|
-
# @param options [Hash] A hash of API options.
|
798
|
-
# @example
|
799
|
-
# account.read_link :billing_info # => <Recurly::BillingInfo>
|
800
|
-
def follow_link(key, options = {})
|
801
|
-
if link = links[key = key.to_s]
|
802
|
-
response = API.send link[:method], link[:href], options[:body], options
|
803
|
-
if resource_class = link[:resource_class]
|
804
|
-
response = resource_class.from_response response
|
805
|
-
response.attributes[self.class.member_name] = self
|
806
|
-
end
|
807
|
-
response
|
808
|
-
end
|
809
|
-
rescue Recurly::API::NotFound
|
810
|
-
raise unless resource_class
|
811
|
-
end
|
812
|
-
|
813
|
-
# Serializes the record to XML.
|
814
|
-
#
|
815
|
-
# @return [String] An XML string.
|
816
|
-
# @param options [Hash] A hash of XML options.
|
817
|
-
# @example
|
818
|
-
# Recurly::Account.new(:account_code => 'code').to_xml
|
819
|
-
# # => "<account><account_code>code</account_code></account>"
|
820
|
-
def to_xml(options = {})
|
821
|
-
builder = options[:builder] || XML.new("<#{self.class.xml_root_key}/>")
|
822
|
-
xml_keys.each { |key|
|
823
|
-
value = respond_to?(key) ? send(key) : self[key]
|
824
|
-
node = builder.add_element key
|
825
|
-
|
826
|
-
# Duck-typing here is problematic because of ActiveSupport's #to_xml.
|
827
|
-
case value
|
828
|
-
when Resource
|
829
|
-
value.to_xml options.merge(:builder => node)
|
830
|
-
when Array, Subscription::AddOns
|
831
|
-
value.each do |e|
|
832
|
-
if e.is_a? Recurly::Resource
|
833
|
-
# create a node to hold this resource
|
834
|
-
e_node = node.add_element Helper.singularize(key)
|
835
|
-
# serialize the resource into this node
|
836
|
-
e.to_xml(options.merge(builder: e_node))
|
837
|
-
else
|
838
|
-
# it's just a primitive value
|
839
|
-
node.add_element(Helper.singularize(key), e)
|
840
|
-
end
|
841
|
-
end
|
842
|
-
when Hash, Recurly::Money
|
843
|
-
value.each_pair { |k, v| node.add_element k.to_s, v }
|
844
|
-
else
|
845
|
-
node.text = value
|
846
|
-
end
|
847
|
-
}
|
848
|
-
builder.to_s
|
849
|
-
end
|
850
|
-
|
851
|
-
# Attempts to save the record, returning the success of the request.
|
852
|
-
#
|
853
|
-
# @return [true, false]
|
854
|
-
# @raise [Transaction::Error] A monetary transaction failed.
|
855
|
-
# @example
|
856
|
-
# account = Recurly::Account.new
|
857
|
-
# account.save # => false
|
858
|
-
# account.account_code = 'account_code'
|
859
|
-
# account.save # => true
|
860
|
-
# @see #save!
|
861
|
-
def save
|
862
|
-
if new_record? || changed?
|
863
|
-
clear_errors
|
864
|
-
@response = API.send(
|
865
|
-
persisted? ? :put : :post, path, to_xml
|
866
|
-
)
|
867
|
-
reload response
|
868
|
-
persist! true
|
869
|
-
end
|
870
|
-
true
|
871
|
-
rescue API::UnprocessableEntity => e
|
872
|
-
apply_errors e
|
873
|
-
Transaction::Error.validate! e, (self if is_a?(Transaction))
|
13
|
+
def requires_client?
|
874
14
|
false
|
875
15
|
end
|
876
16
|
|
877
|
-
|
878
|
-
|
879
|
-
#
|
880
|
-
# @return [true]
|
881
|
-
# @raise [Invalid] The record was invalid.
|
882
|
-
# @raise [Transaction::Error] A monetary transaction failed.
|
883
|
-
# @example
|
884
|
-
# account = Recurly::Account.new
|
885
|
-
# account.save! # raises Recurly::Resource::Invalid
|
886
|
-
# account.account_code = 'account_code'
|
887
|
-
# account.save! # => true
|
888
|
-
# @see #save
|
889
|
-
def save!
|
890
|
-
save || raise(Invalid.new(self))
|
891
|
-
end
|
892
|
-
|
893
|
-
# @return [true, false, nil] The validity of the record: +true+ if the
|
894
|
-
# record was successfully saved (or persisted and unchanged), +false+ if
|
895
|
-
# the record was not successfully saved, or +nil+ for a record with an
|
896
|
-
# unknown state (i.e. (i.e. new records that haven't been saved and
|
897
|
-
# persisted records with changed attributes).
|
898
|
-
# @example
|
899
|
-
# account = Recurly::Account.new
|
900
|
-
# account.valid? # => nil
|
901
|
-
# account.save # => false
|
902
|
-
# account.valid? # => false
|
903
|
-
# account.account_code = 'account_code'
|
904
|
-
# account.save # => true
|
905
|
-
# account.valid? # => true
|
906
|
-
def valid?
|
907
|
-
return true if persisted? && !changed?
|
908
|
-
errors_empty = errors.values.flatten.empty?
|
909
|
-
return if errors_empty && changed?
|
910
|
-
errors_empty
|
911
|
-
end
|
912
|
-
|
913
|
-
# Update a record with a given hash of attributes.
|
914
|
-
#
|
915
|
-
# @return [true, false] The success of the update.
|
916
|
-
# @param attributes [Hash] A hash of attributes.
|
917
|
-
# @raise [Transaction::Error] A monetary transaction failed.
|
918
|
-
# @example
|
919
|
-
# account = Account.find 'junior'
|
920
|
-
# account.update_attributes :account_code => 'flynn' # => true
|
921
|
-
# @see #update_attributes!
|
922
|
-
def update_attributes(attributes = {})
|
923
|
-
self.attributes = attributes and save
|
924
|
-
end
|
925
|
-
|
926
|
-
# Update a record with a given hash of attributes.
|
927
|
-
#
|
928
|
-
# @return [true] The update was successful.
|
929
|
-
# @param attributes [Hash] A hash of attributes.
|
930
|
-
# @raise [Invalid] The record was invalid.
|
931
|
-
# @raise [Transaction::Error] A monetary transaction failed.
|
932
|
-
# @example
|
933
|
-
# account = Account.find 'gale_boetticher'
|
934
|
-
# account.update_attributes! :account_code => nil # Raises an exception.
|
935
|
-
# @see #update_attributes
|
936
|
-
def update_attributes!(attributes = {})
|
937
|
-
self.attributes = attributes and save!
|
938
|
-
end
|
939
|
-
|
940
|
-
# @return [Hash] A hash with indifferent read access containing any
|
941
|
-
# validation errors where the key is the attribute name and the value is
|
942
|
-
# an array of error messages.
|
943
|
-
# @example
|
944
|
-
# account.errors # => {"account_code"=>["can't be blank"]}
|
945
|
-
# account.errors[:account_code] # => ["can't be blank"]
|
946
|
-
def errors
|
947
|
-
@errors ||= Errors.new { |h, k| h[k] = [] }
|
948
|
-
end
|
949
|
-
|
950
|
-
# Marks a record as persisted, i.e. not a new or deleted record, resetting
|
951
|
-
# any tracked attribute changes in the process. (This is an internal method
|
952
|
-
# and should probably not be called unless you know what you're doing.)
|
953
|
-
#
|
954
|
-
# @api internal
|
955
|
-
# @return [true]
|
956
|
-
def persist!(saved = false)
|
957
|
-
@new_record, @uri = false
|
958
|
-
if changed?
|
959
|
-
@previous_changes = changes if saved
|
960
|
-
changed_attributes.clear
|
961
|
-
end
|
962
|
-
true
|
963
|
-
end
|
964
|
-
|
965
|
-
# @return [String, nil] The unique resource identifier (URI) of the record
|
966
|
-
# (if persisted).
|
967
|
-
# @example
|
968
|
-
# Recurly::Account.new(:account_code => "account_code").uri # => nil
|
969
|
-
# Recurly::Account.find("account_code").uri
|
970
|
-
# # => "https://api.recurly.com/v2/accounts/account_code"
|
971
|
-
def uri
|
972
|
-
@href ||= ((API.base_uri + path).to_s if persisted?)
|
973
|
-
end
|
974
|
-
|
975
|
-
# Attempts to destroy the record.
|
976
|
-
#
|
977
|
-
# @return [true, false] +true+ if successful, +false+ if unable to destroy
|
978
|
-
# (if the record does not persist on Recurly).
|
979
|
-
# @raise [NotFound] The record cannot be found.
|
980
|
-
# @example
|
981
|
-
# account = Recurly::Account.find account_code
|
982
|
-
# race_condition = Recurly::Account.find account_code
|
983
|
-
# account.destroy # => true
|
984
|
-
# account.destroy # => false (already destroyed)
|
985
|
-
# race_condition.destroy # raises Recurly::Resource::NotFound
|
986
|
-
def destroy
|
987
|
-
return false unless persisted?
|
988
|
-
@response = API.delete uri
|
989
|
-
@destroyed = true
|
990
|
-
rescue API::NotFound => e
|
991
|
-
raise NotFound, e.description
|
992
|
-
end
|
993
|
-
|
994
|
-
def signable_attributes
|
995
|
-
Hash[xml_keys.map { |key| [key, self[key]] }]
|
996
|
-
end
|
997
|
-
|
998
|
-
def ==(other)
|
999
|
-
other.is_a?(self.class) && other.to_s == to_s
|
1000
|
-
end
|
1001
|
-
|
1002
|
-
def marshal_dump
|
1003
|
-
[
|
1004
|
-
@attributes.reject { |k, v| v.is_a?(Proc) },
|
1005
|
-
@new_record,
|
1006
|
-
@destroyed,
|
1007
|
-
@uri,
|
1008
|
-
@href,
|
1009
|
-
changed_attributes,
|
1010
|
-
previous_changes,
|
1011
|
-
response,
|
1012
|
-
etag,
|
1013
|
-
links,
|
1014
|
-
@type
|
1015
|
-
]
|
1016
|
-
end
|
1017
|
-
|
1018
|
-
def marshal_load(serialization)
|
1019
|
-
@attributes,
|
1020
|
-
@new_record,
|
1021
|
-
@destroyed,
|
1022
|
-
@uri,
|
1023
|
-
@href,
|
1024
|
-
@changed_attributes,
|
1025
|
-
@previous_changes,
|
1026
|
-
@response,
|
1027
|
-
@etag,
|
1028
|
-
@links,
|
1029
|
-
@type = serialization
|
1030
|
-
end
|
1031
|
-
|
1032
|
-
# @return [String]
|
1033
|
-
def inspect(attributes = self.class.attribute_names.to_a)
|
1034
|
-
string = "#<#{self.class}"
|
1035
|
-
string << "##@type" if respond_to?(:type)
|
1036
|
-
attributes += %w(errors) if errors.any?
|
1037
|
-
string << " %s" % attributes.map { |k|
|
1038
|
-
"#{k}: #{self.send(k).inspect}"
|
1039
|
-
}.join(', ')
|
1040
|
-
string << '>'
|
1041
|
-
end
|
1042
|
-
alias to_s inspect
|
1043
|
-
|
1044
|
-
def apply_errors(exception)
|
1045
|
-
@response = exception.response
|
1046
|
-
document = XML.new exception.response.body
|
1047
|
-
|
1048
|
-
if document.root.name == 'error'
|
1049
|
-
# Single error is returned from the API
|
1050
|
-
attribute_path = document['symbol'].text.split '.'
|
1051
|
-
invalid! [attribute_path[1]], document['description'].text
|
1052
|
-
else
|
1053
|
-
# Array of errors was returned by the API
|
1054
|
-
document.each_element 'error' do |el|
|
1055
|
-
attribute_path = el.attribute('field').value.split '.'
|
1056
|
-
invalid! attribute_path[1, attribute_path.length], el.text
|
1057
|
-
end
|
1058
|
-
end
|
17
|
+
def ==(other_resource)
|
18
|
+
self.attributes == other_resource.attributes
|
1059
19
|
end
|
1060
20
|
|
1061
21
|
protected
|
1062
22
|
|
1063
|
-
def
|
1064
|
-
@
|
1065
|
-
self.class.member_path to_param
|
1066
|
-
else
|
1067
|
-
self.class.collection_path
|
1068
|
-
end
|
1069
|
-
end
|
1070
|
-
|
1071
|
-
def invalid!(attribute_path, error)
|
1072
|
-
if attribute_path.length == 1
|
1073
|
-
errors[attribute_path[0]] << error
|
1074
|
-
else
|
1075
|
-
child, k, v = attribute_path.shift.scan(/[^\[\]=]+/)
|
1076
|
-
if c = k ? self[child].find { |d| d[k] == v } : self[child]
|
1077
|
-
c.invalid! attribute_path, error if c.methods.include? :invalid!
|
1078
|
-
e = errors[child] << 'is invalid' and e.uniq!
|
1079
|
-
end
|
1080
|
-
end
|
1081
|
-
end
|
1082
|
-
|
1083
|
-
def clear_errors
|
1084
|
-
errors.clear
|
1085
|
-
self.class.associations do |association|
|
1086
|
-
next unless respond_to? "#{association}=" # Clear writable only.
|
1087
|
-
[*self[association]].each do |associated|
|
1088
|
-
associated.clear_errors if associated.respond_to? :clear_errors
|
1089
|
-
end
|
1090
|
-
end
|
1091
|
-
end
|
1092
|
-
|
1093
|
-
def copy_from(other)
|
1094
|
-
other.instance_variables.each do |ivar|
|
1095
|
-
instance_variable_set ivar, other.instance_variable_get(ivar)
|
1096
|
-
end
|
1097
|
-
end
|
1098
|
-
|
1099
|
-
private
|
1100
|
-
|
1101
|
-
def fetch_associated(name, value, options = {})
|
1102
|
-
case value
|
1103
|
-
when Array
|
1104
|
-
value.map do |v|
|
1105
|
-
fetch_associated(Helper.singularize(name), v, association_name: name)
|
1106
|
-
end
|
1107
|
-
when Hash
|
1108
|
-
association_name = options[:association_name] || name
|
1109
|
-
associated_class_name = self.class.find_association(association_name).class_name
|
1110
|
-
associated_class_name ||= Helper.classify(name)
|
1111
|
-
Recurly.const_get(associated_class_name, false).send(:new, value)
|
1112
|
-
when Proc, Resource, Resource::Pager, nil
|
1113
|
-
value
|
1114
|
-
else
|
1115
|
-
raise "unexpected association #{name.inspect}=#{value.inspect}"
|
1116
|
-
end
|
23
|
+
def initialize(attributes = {})
|
24
|
+
@attributes = attributes.clone
|
1117
25
|
end
|
1118
26
|
|
1119
|
-
def
|
1120
|
-
|
27
|
+
def to_s
|
28
|
+
self.inspect
|
1121
29
|
end
|
1122
30
|
|
1123
|
-
def
|
1124
|
-
|
1125
|
-
key.end_with?('_in_cents') &&
|
1126
|
-
!respond_to?(:currency) &&
|
1127
|
-
!value.is_a?(Money) &&
|
1128
|
-
!is_a?(PercentageTier)&&
|
1129
|
-
!is_a?(SubAddOnPercentageTier)&&
|
1130
|
-
!is_a?(SubscriptionRampInterval)
|
31
|
+
def schema
|
32
|
+
self.class.schema
|
1131
33
|
end
|
1132
34
|
end
|
1133
35
|
end
|