recurly 2.17.5 → 4.18.0

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