lib/recurly/resource.rb in recurly-2.20.3 vs lib/recurly/resource.rb in recurly-3.0.0.beta.1
- old
+ new
@@ -1,1133 +1,35 @@
-require 'date'
-require 'erb'
-require_relative './rev_rec'
-
module Recurly
- # The base class for all Recurly resources (e.g. {Account}, {Subscription},
- # {Transaction}).
- #
- # Resources behave much like
- # {ActiveModel}[http://rubydoc.info/gems/activemodel] classes, especially
- # like {ActiveRecord}[http://rubydoc.info/gems/activerecord].
- #
- # == Life Cycle
- #
- # To take you through the typical life cycle of a resource, we'll use
- # {Recurly::Account} as an example.
- #
- # === Creating a Record
- #
- # You can instantiate a record before attempting to save it.
- #
- # account = Recurly::Account.new :first_name => 'Walter'
- #
- # Once instantiated, you can assign and reassign any attribute.
- #
- # account.first_name = 'Walt'
- # account.last_name = 'White'
- #
- # When you're ready to save, do so.
- #
- # account.save # => false
- #
- # If save returns +false+, validation likely failed. You can check the record
- # for errors.
- #
- # account.errors # => {"account_code"=>["can't be blank"]}
- #
- # Once the errors are fixed, you can try again.
- #
- # account.account_code = 'heisenberg'
- # account.save # => true
- #
- # The object will be updated with any information provided by the server
- # (including any UUIDs set).
- #
- # account.created_at # => 2011-04-30 07:13:35 -0700
- #
- # You can also create accounts in one fell swoop.
- #
- # Recurly::Account.create(
- # :first_name => 'Jesse'
- # :last_name => 'Pinkman'
- # :account_code => 'capn_cook'
- # )
- # # => #<Recurly::Account account_code: "capn_cook" ...>
- #
- # You can use alternative "bang" methods for exception control. If the record
- # fails to save, a Recurly::Resource::Invalid exception will be raised.
- #
- # begin
- # account = Recurly::Account.new :first_name => 'Junior'
- # account.save!
- # rescue Recurly::Resource::Invalid
- # p account.errors
- # end
- #
- # You can access the invalid record from the exception itself (if, for
- # example, you use the <tt>create!</tt> method).
- #
- # begin
- # Recurly::Account.create! :first_name => 'Skylar', :last_name => 'White'
- # rescue Recurly::Resource::Invalid => e
- # p e.record.errors
- # end
- #
- # === Fetching a Record
- #
- # Records are fetched by their unique identifiers.
- #
- # account = Recurly::Account.find 'better_call_saul'
- # # => #<Recurly::Account account_code: "better_call_saul" ...>
- #
- # If the record doesn't exist, a Recurly::Resource::NotFound exception will
- # be raised.
- #
- # === Updating a Record
- #
- # Once fetched, a record can be updated with a hash of attributes.
- #
- # account.update_attributes :first_name => 'Saul', :last_name => 'Goodman'
- # # => true
- #
- # (A bang method, update_attributes!, will raise Recurly::Resource::Invalid.)
- #
- # You can also update a record by setting attributes and calling save.
- #
- # account.last_name = 'McGill'
- # account.save # Alternatively, call save!
- #
- # === Deleting a Record
- #
- # To delete (deactivate, close, etc.) a fetched record, merely call destroy
- # on it.
- #
- # account.destroy # => true
- #
- # === Fetching a List of Records
- #
- # If you want to iterate over a list of accounts, you can use a Pager.
- #
- # pager = Account.paginate :per_page => 50
- #
- # If you want to iterate over _every_ record, a convenience method will
- # automatically paginate:
- #
- # Account.find_each { |account| p account }
+ # This class represents an object as it exists on the
+ # Recurly servers. It is generated from a response. If you wish to
+ # update or change a resource, you need to send a request to the server
+ # and get a new Resource.
class Resource
- require 'recurly/resource/errors'
- require 'recurly/resource/pager'
- require 'recurly/resource/association'
+ extend Schema::SchemaFactory
+ extend Schema::JsonDeserializer
+ include Schema::SchemaValidator
- # Raised when a record cannot be found.
- #
- # @example
- # begin
- # Recurly::Account.find 'tortuga'
- # rescue Recurly::Resource::NotFound => e
- # e.message # => "Can't find Account with account_code = tortuga"
- # end
- class NotFound < API::NotFound
- def initialize(message)
- set_message message
- end
- end
-
- # Raised when a record is invalid.
- #
- # @example
- # begin
- # Recurly::Account.create! :first_name => "Flynn"
- # rescue Recurly::Resource::Invalid => e
- # e.record.errors # => errors: {"account_code"=>["can't be blank"]}>
- # end
- class Invalid < Error
- # @return [Resource, nil] The invalid record.
- attr_reader :record
-
- def initialize(message)
- if message.is_a? Resource
- @record = message
- set_message(record_to_message)
- else
- set_message(message)
- end
- end
-
- private
-
- def record_to_message
- @record.errors.map do |k, v|
- message = v.join(', ')
- k == 'base' ? message : "#{k} #{message}"
- end.join('; ')
- end
- end
-
- class << self
- # @return [String] The demodulized name of the resource class.
- # @example
- # Recurly::Account.name # => "Account"
- def resource_name
- Helper.demodulize name
- end
-
- # @return [String] The underscored, pluralized name of the resource
- # class.
- # @example
- # Recurly::Account.collection_name # => "accounts"
- def collection_name
- Helper.pluralize Helper.underscore(resource_name)
- end
- alias collection_path collection_name
-
- # @return [String] The underscored name of the resource class.
- # @example
- # Recurly::Account.member_name # => "account"
- def member_name
- Helper.underscore resource_name
- end
-
- # @return [String] The relative path to a resource's identifier from the
- # API's base URI.
- # @param uuid [String, nil]
- # @example
- # Recurly::Account.member_path "code" # => "accounts/code"
- # Recurly::Account.member_path nil # => "accounts"
- def member_path(uuid)
- uuid = ERB::Util.url_encode(uuid) if uuid
- [collection_path, uuid].compact.join '/'
- end
-
- # @return [String] The root key for this resource's xml document
- def xml_root_key
- self.member_name
- end
-
- # @return [Array] Per attribute, defines readers, writers, boolean and
- # change-tracking methods.
- # @param attribute_names [Array] An array of attribute names.
- # @example
- # class Account < Resource
- # define_attribute_methods [:name]
- # end
- #
- # a = Account.new
- # a.name? # => false
- # a.name # => nil
- # a.name = "Stephen"
- # a.name? # => true
- # a.name # => "Stephen"
- # a.name_changed? # => true
- # a.name_was # => nil
- # a.name_change # => [nil, "Stephen"]
- def define_attribute_methods(attribute_names)
- @attribute_names = attribute_names.map! { |m| m.to_s }.sort!.freeze
- remove_const :AttributeMethods if constants.include? :AttributeMethods
- include const_set :AttributeMethods, Module.new {
- attribute_names.each do |name|
- define_method(name) { self[name] } # Get.
- define_method("#{name}=") { |value| self[name] = value } # Set.
- define_method("#{name}?") { !!self[name] } # Present.
- define_method("#{name}_change") { changes[name] } # Dirt...
- define_method("#{name}_changed?") { changed_attributes.key? name }
- define_method("#{name}_was") { changed_attributes[name] }
- define_method("#{name}_previously_changed?") {
- previous_changes.key? name
- }
- define_method("#{name}_previously_was") {
- previous_changes[name].first if previous_changes.key? name
- }
- end
- }
- end
-
- # @return [Array, nil] The list of attribute names defined for the
- # resource class.
- attr_reader :attribute_names
-
- # @return [Pager] A pager with an iterable collection of records
- # @param options [Hash] A hash of pagination options
- # @option options [Integer] :per_page The number of records returned per
- # page
- # @option options [DateTime, Time, Integer] :cursor A timestamp that the
- # pager will skim back to and return records created before it
- # @option options [String] :etag When set, will raise
- # {Recurly::API::NotModified} if the pager's loaded page content has
- # not changed
- # @example Fetch 50 records and iterate over them
- # Recurly::Account.paginate(:per_page => 50).each { |a| p a }
- # @example Fetch records before January 1, 2011
- # Recurly::Account.paginate(:cursor => Time.new(2011, 1, 1))
- def paginate(options = {})
- Pager.new self, options
- end
- alias scoped paginate
- alias where paginate
-
- def all(options = {})
- paginate(options).to_a
- end
-
- # @return [Hash] Defined scopes per resource.
- def scopes
- @scopes ||= Recurly::Helper.hash_with_indifferent_read_access
- end
-
- # @return [Module] Module of scopes methods.
- def scopes_helper
- @scopes_helper ||= Module.new.tap { |helper| extend helper }
- end
-
- # Defines a new resource scope.
- #
- # @return [Proc]
- # @param [Symbol] name the scope name
- # @param [Hash] params the scope params
- def scope(name, params = {})
- scopes[name = name.to_s] = params
- scopes_helper.send(:define_method, name) { paginate scopes[name] }
- end
-
- # Iterates through every record by automatically paging.
- #
- # @option options [Hash] Optional hash to pass to Pager#paginate
- #
- # @return [nil]
- # @param [Integer] per_page The number of records returned per request.
- # @yield [record]
- # @see Pager#paginate
- # @example
- # Recurly::Account.find_each { |a| p a }
- # @example With sorting and filter
- # opts = {
- # begin_time: DateTime.new(2016,1,1),
- # sort: :updated_at
- # }
- # Recurly::Account.find_each(opts) do |a|
- # puts a.inspect
- # end
- def find_each(options = {}, &block)
- paginate(options).find_each(&block)
- end
-
- # @return [Integer] The total record count of the resource in question.
- # @see Pager#count
- # @example
- # Recurly::Account.count # => 42
- def count
- paginate.count
- end
-
- # @api internal
- # @return [Resource, nil]
- def first
- paginate(:per_page => 1).first
- end
-
- # @return [Resource] A record matching the designated unique identifier.
- # @param [String] uuid The unique identifier of the resource to be
- # retrieved.
- # @param [Hash] options A hash of options.
- # @option options [String] :etag When set, will raise {API::NotModified}
- # if the record content has not changed.
- # @raise [Error] If the resource has no identifier (and thus cannot be
- # retrieved).
- # @raise [NotFound] If no resource can be found for the supplied
- # identifier (or the supplied identifier is +nil+).
- # @raise [API::NotModified] If the <tt>:etag</tt> option is set and
- # matches the server's.
- # @example
- # Recurly::Account.find "heisenberg"
- # # => #<Recurly::Account account_code: "heisenberg", ...>
- # Use the following identifiers for these types of objects:
- # for accounts use account_code
- # for plans use plan_code
- # for invoices use invoice_number
- # for subscriptions use uuid
- # for transactions use uuid
- def find(uuid, options = {})
- if uuid.nil? || uuid.to_s.empty?
- raise NotFound, "can't find a record with nil identifier"
- end
-
- begin
- from_response API.get(member_path(uuid), {}, options)
- rescue API::NotFound => e
- raise NotFound, e.description
- end
- end
-
- # Instantiates and attempts to save a record.
- #
- # @return [Resource] The record.
- # @raise [Transaction::Error] A monetary transaction failed.
- # @see create!
- def create(attributes = {})
- new(attributes) { |record| record.save }
- end
-
- # Instantiates and attempts to save a record.
- #
- # @return [Resource] The saved record.
- # @raise [Invalid] The record is invalid.
- # @raise [Transaction::Error] A monetary transaction failed.
- # @see create
- def create!(attributes = {})
- new(attributes) { |record| record.save! }
- end
-
- # Instantiates a record from an HTTP response, setting the record's
- # response attribute in the process.
- #
- # @return [Resource]
- # @param response [Net::HTTPResponse]
- def from_response(response)
- content_type = response['Content-Type']
-
- case content_type
- when %r{application/pdf}
- response.body
- when %r{application/xml}
- record = from_xml response.body
- record.instance_eval { @etag, @response = response['ETag'], response }
- record
- else
- raise Recurly::Error, "Content-Type \"#{content_type}\" is not accepted"
- end
- end
-
- # Instantiates a record from an XML blob: either a String or XML element.
- #
- # Assuming the record is from an API response, the record is flagged as
- # persisted.
- #
- # @return [Resource]
- # @param xml [String, REXML::Element, Nokogiri::XML::Node]
- # @see from_response
- def from_xml(xml)
- xml = XML.new xml
- record = new
-
- xml.root.attributes.each do |name, value|
- record.instance_variable_set "@#{name}", value.to_s
- end
-
- xml.each_element do |el|
- # skip this element if it's an xml comment
- next if defined?(Nokogiri::XML::Node::TEXT_NODE) && el.is_a?(Nokogiri::XML::Comment)
-
- if el.name == 'a'
- record.links[el.attribute('name').value] = {
- :method => el.attribute('method').to_s,
- :href => el.attribute('href').value
- }
- next
- end
-
- # Nokogiri on Jruby-1.7.19 likes to throw NullPointer exceptions
- # if you try to run certian operations like el.attribute(''). Since
- # we dont care about text nodes, let's just skip them
- next if defined?(Nokogiri::XML::Node::TEXT_NODE) && el.node_type == Nokogiri::XML::Node::TEXT_NODE
-
- if association = find_association(el.name)
- class_name = association_class_name(association, el.name)
- resource_class = Recurly.const_get(class_name)
- is_many = association.relation == :has_many
-
- # Is this a link, or is it embedded data?
- if el.children.empty? && href = el.attribute('href')
- if is_many
- record[el.name] = Pager.new(
- resource_class, :uri => href.value, :parent => record
- )
- else
- record.links[el.name] = {
- :resource_class => resource_class,
- :method => :get,
- :href => href.value
- }
- end
- else
- if is_many
- resources = el.elements.map { |e| resource_class.from_xml(e) }
- record[el.name] = resources
- else
- record[el.name] = resource_class.from_xml(el)
- end
- end
- else
- # TODO name tax_type conflicts with the TaxType
- # class so if we get to this point was can assume
- # it's the string. Will need to refactor this
- if el.name == 'tax_type'
- record[el.name] = el.text
- else
- val = XML.cast(el)
-
- # TODO we have to clear changed attributes after
- # parsing here or else it always serializes. Need
- # a better way of handling changed attributes
- if el.name == 'address' && val.kind_of?(Hash)
- address = Address.new(val)
- address.instance_variable_set(:@changed_attributes, {})
- record[el.name] = address
- else
- record[el.name] = val
- end
- end
- end
- end
-
- record.persist! if record.respond_to? :persist!
- record
- end
-
- # @return [Array] A list of associations for the current class.
- def associations
- @associations ||= []
- end
-
- # @return [Array] A list of associated resource classes with
- # the relation [:has_many, :has_one, :belongs_to] for the current class.
- def associations_for_relation(relation)
- associations.select{ |a| a.relation == relation }.map(&:resource_class)
- end
-
- def association_class_name(association, el_name)
- return association.class_name if association.class_name
- Helper.classify(el_name)
- end
-
- # @return [Association, nil] Find association for the current class
- # with resource class name.
- def find_association(resource_class)
- associations.find{ |a| a.resource_class.to_s == resource_class.to_s }
- end
-
- def associations_helper
- @associations_helper ||= Module.new.tap { |helper| include helper }
- end
-
- # Establishes a has_many association.
- #
- # @return [Proc, nil]
- # @param collection_name [Symbol] Association name.
- # @param options [Hash] A hash of association options.
- # @option options [true, false] :readonly Define a setter when false, defaults to true
- # [String] :class_name Actual associated resource class name
- # if not same as collection_name.
- def has_many(collection_name, options = {})
- associations << Association.new(:has_many, collection_name.to_s, options)
- associations_helper.module_eval do
- define_method collection_name do
- if self[collection_name]
- self[collection_name]
- else
- attributes[collection_name.to_s] = []
- end
- end
- if options.key?(:readonly) && options[:readonly] == false
- define_method "#{collection_name}=" do |collection|
- self[collection_name] = collection
- end
- end
- end
- end
-
- # Establishes a has_one association.
- #
- # @return [Proc, nil]
- # @param member_name [Symbol] Association name.
- # @param options [Hash] A hash of association options.
- # @option options [true, false] :readonly Don't define a setter.
- # [String] :class_name Actual associated resource class name
- # if not same as member_name.
- def has_one(member_name, options = {})
- associations << Association.new(:has_one, member_name.to_s, options)
- associations_helper.module_eval do
- define_method(member_name) { self[member_name] }
- if options.key?(:readonly) && options[:readonly] == false
- associated = Recurly.const_get Helper.classify(member_name), false
- define_method "#{member_name}=" do |member|
- associated_uri = "#{path}/#{member_name}"
- self[member_name] = case member
- when Hash
- associated.send :new, member.merge(:uri => associated_uri)
- when associated
- member.uri = associated_uri and member
- else
- raise ArgumentError, "expected #{associated}"
- end
- end
- define_method "build_#{member_name}" do |*args|
- attributes = args.shift || {}
- self[member_name] = associated.send(
- :new, attributes.merge(:uri => "#{path}/#{associated.member_name}")
- ).tap { |child| child.attributes[self.class.member_name] = self }
- end
- define_method "create_#{member_name}" do |*args|
- send("build_#{member_name}", *args).tap { |child| child.save }
- end
- end
- end
- end
-
- # Establishes a belongs_to association.
- #
- # @return [Proc]
- # @param parent_name [Symbol] Association name.
- # @param options [Hash] A hash of association options.
- # @option options [true, false] :readonly Don't define a setter.
- # [String] :class_name Actual associated resource class name
- # if not same as parent_name.
- def belongs_to(parent_name, options = {})
- associations << Association.new(:belongs_to, parent_name.to_s, options)
- associations_helper.module_eval do
- define_method(parent_name) { self[parent_name] }
- if options.key?(:readonly) && options[:readonly] == false
- define_method "#{parent_name}=" do |parent|
- self[parent_name] = parent
- end
- end
- end
- end
-
- # @return [:has_many, :has_one, :belongs_to, nil] An association type.
- def reflect_on_association(name)
- a = find_association(name)
- a.relation if a
- end
-
- def embedded!(root_index = false)
- protected :initialize
- private_class_method(*%w(create create!))
- unless root_index
- private_class_method(*%w(all find_each first paginate scoped where))
- end
- end
-
- def find_resource_class(name)
- resource_name = Helper.classify(name)
- if Recurly.const_defined?(resource_name, false) && Recurly.const_get(resource_name, false).instance_of?(Class)
- Recurly.const_get(resource_name, false)
- end
- end
- end
-
- # @return [Hash] The raw hash of record attributes.
attr_reader :attributes
- # @return [Net::HTTPResponse, nil] The most recent response object for the
- # record (updated during {#save} and {#destroy}).
- attr_reader :response
-
- # @return [String, nil] An ETag for the current record.
- attr_reader :etag
-
- # @return [String, nil] A writer to override the URI the record saves to.
- attr_writer :uri
-
- # @return [Resource] A new resource instance.
- # @param attributes [Hash] A hash of attributes.
- def initialize(attributes = {})
- if instance_of? Resource
- raise Error,
- "#{self.class} is an abstract class and cannot be instantiated"
- end
-
- @attributes, @new_record, @destroyed, @uri, @href = {}, true, false
- self.attributes = attributes
- yield self if block_given?
- end
-
- # @return [self] Reloads the record from the server.
- def reload(response = nil)
- if response
- return if response.body.to_s.length.zero?
- fresh = self.class.from_response response
- else
- options = {:etag => (etag unless changed?)}
- fresh = if @href
- self.class.from_response API.get(@href, {}, options)
- else
- self.class.find(to_param, options)
- end
- end
- fresh and copy_from fresh
- persist! true
- self
- rescue API::NotModified
- self
- end
-
- # @return [Hash] Hash of changed attributes.
- # @see #changes
- def changed_attributes
- @changed_attributes ||= {}
- end
-
- # @return [Array] A list of changed attribute keys.
- def changed
- changed_attributes.keys
- end
-
- # Do any attributes have unsaved changes?
- # @return [true, false]
- def changed?
- !changed_attributes.empty?
- end
-
- # @return [Hash] Map of changed attributes to original value and new value.
- def changes
- changed_attributes.inject({}) { |changes, (key, original_value)|
- changes[key] = [original_value, self[key]] and changes
- }
- end
-
- # @return [Hash] Previously-changed attributes.
- # @see #changes
- def previous_changes
- @previous_changes ||= {}
- end
-
- # Is the record new (i.e., not saved on Recurly's servers)?
- #
- # @return [true, false]
- # @see #persisted?
- # @see #destroyed?
- def new_record?
- @new_record
- end
-
- # Has the record been destroyed? (Set +true+ after a successful destroy.)
- # @return [true, false]
- # @see #new_record?
- # @see #persisted?
- def destroyed?
- @destroyed
- end
-
- # Has the record persisted (i.e., saved on Recurly's servers)?
- #
- # @return [true, false]
- # @see #new_record?
- # @see #destroyed?
- def persisted?
- !(new_record? || destroyed?)
- end
-
- # The value of a specified attribute, lazily fetching any defined
- # association.
- #
- # @param key [Symbol, String] The name of the attribute to be fetched.
- # @example
- # account.read_attribute :first_name # => "Ted"
- # account[:last_name] # => "Beneke"
- # @see #write_attribute
- def read_attribute(key)
- key = key.to_s
- if attributes.key? key
- value = attributes[key]
- elsif links.key?(key) && self.class.reflect_on_association(key)
- value = attributes[key] = follow_link key
- end
- value
- end
- alias [] read_attribute
-
- # Sets the value of a specified attribute.
- #
- # @param key [Symbol, String] The name of the attribute to be set.
- # @param value [Object] The value the attribute will be set to.
- # @example
- # account.write_attribute :first_name, 'Gus'
- # account[:company_name] = 'Los Pollos Hermanos'
- # @see #read_attribute
- def write_attribute(key, value)
- if changed_attributes.key?(key = key.to_s)
- changed_attributes.delete key if changed_attributes[key] == value
- elsif self[key] != value
- changed_attributes[key] = self[key]
- end
-
- association = self.class.find_association(key)
- if association
- value = fetch_associated(key, value)
- # FIXME: More explicit; less magic.
- elsif add_money_tag?(key, value)
- value = Money.new(value, self, key)
- end
-
- attributes[key] = value
- end
- alias []= write_attribute
-
- # Apply a given hash of attributes to a record.
- #
- # @return [Hash]
- # @param attributes [Hash] A hash of attributes.
- def attributes=(attributes = {})
- attributes.each_pair { |k, v|
- respond_to?(name = "#{k}=") and send(name, v) or self[k] = v
- }
- end
-
- def as_json(options = nil)
- attributes.reject { |k, v| v.is_a?(Recurly::Resource::Pager) }
- end
-
- # @return [Hash] The raw hash of record href links.
- def links
- @links ||= {}
- end
-
- # Whether a record has a link with the given name.
- #
- # @param key [Symbol, String] The name of the link to check for.
- # @example
- # account.link? :billing_info # => true
- def link?(key)
- links.key?(key.to_s)
- end
-
- # Fetch the value of a link by following the associated href.
- #
- # @param key [Symbol, String] The name of the link to be followed.
- # @param options [Hash] A hash of API options.
- # @example
- # account.read_link :billing_info # => <Recurly::BillingInfo>
- def follow_link(key, options = {})
- if link = links[key = key.to_s]
- response = API.send link[:method], link[:href], options[:body], options
- if resource_class = link[:resource_class]
- response = resource_class.from_response response
- response.attributes[self.class.member_name] = self
- end
- response
- end
- rescue Recurly::API::NotFound
- raise unless resource_class
- end
-
- # Serializes the record to XML.
- #
- # @return [String] An XML string.
- # @param options [Hash] A hash of XML options.
- # @example
- # Recurly::Account.new(:account_code => 'code').to_xml
- # # => "<account><account_code>code</account_code></account>"
- def to_xml(options = {})
- builder = options[:builder] || XML.new("<#{self.class.xml_root_key}/>")
- xml_keys.each { |key|
- value = respond_to?(key) ? send(key) : self[key]
- node = builder.add_element key
-
- # Duck-typing here is problematic because of ActiveSupport's #to_xml.
- case value
- when Resource
- value.to_xml options.merge(:builder => node)
- when Array, Subscription::AddOns
- value.each do |e|
- if e.is_a? Recurly::Resource
- # create a node to hold this resource
- e_node = node.add_element Helper.singularize(key)
- # serialize the resource into this node
- e.to_xml(options.merge(builder: e_node))
- else
- # it's just a primitive value
- node.add_element(Helper.singularize(key), e)
- end
- end
- when Hash, Recurly::Money
- value.each_pair { |k, v| node.add_element k.to_s, v }
- else
- node.text = value
- end
- }
- builder.to_s
- end
-
- # Attempts to save the record, returning the success of the request.
- #
- # @return [true, false]
- # @raise [Transaction::Error] A monetary transaction failed.
- # @example
- # account = Recurly::Account.new
- # account.save # => false
- # account.account_code = 'account_code'
- # account.save # => true
- # @see #save!
- def save
- if new_record? || changed?
- clear_errors
- @response = API.send(
- persisted? ? :put : :post, path, to_xml
- )
- reload response
- persist! true
- end
- true
- rescue API::UnprocessableEntity => e
- apply_errors e
- Transaction::Error.validate! e, (self if is_a?(Transaction))
+ def requires_client?
false
end
- # Attempts to save the record, returning +true+ if the record was saved and
- # raising {Invalid} otherwise.
- #
- # @return [true]
- # @raise [Invalid] The record was invalid.
- # @raise [Transaction::Error] A monetary transaction failed.
- # @example
- # account = Recurly::Account.new
- # account.save! # raises Recurly::Resource::Invalid
- # account.account_code = 'account_code'
- # account.save! # => true
- # @see #save
- def save!
- save || raise(Invalid.new(self))
+ def ==(other_resource)
+ self.attributes == other_resource.attributes
end
- # @return [true, false, nil] The validity of the record: +true+ if the
- # record was successfully saved (or persisted and unchanged), +false+ if
- # the record was not successfully saved, or +nil+ for a record with an
- # unknown state (i.e. (i.e. new records that haven't been saved and
- # persisted records with changed attributes).
- # @example
- # account = Recurly::Account.new
- # account.valid? # => nil
- # account.save # => false
- # account.valid? # => false
- # account.account_code = 'account_code'
- # account.save # => true
- # account.valid? # => true
- def valid?
- return true if persisted? && !changed?
- errors_empty = errors.values.flatten.empty?
- return if errors_empty && changed?
- errors_empty
- end
-
- # Update a record with a given hash of attributes.
- #
- # @return [true, false] The success of the update.
- # @param attributes [Hash] A hash of attributes.
- # @raise [Transaction::Error] A monetary transaction failed.
- # @example
- # account = Account.find 'junior'
- # account.update_attributes :account_code => 'flynn' # => true
- # @see #update_attributes!
- def update_attributes(attributes = {})
- self.attributes = attributes and save
- end
-
- # Update a record with a given hash of attributes.
- #
- # @return [true] The update was successful.
- # @param attributes [Hash] A hash of attributes.
- # @raise [Invalid] The record was invalid.
- # @raise [Transaction::Error] A monetary transaction failed.
- # @example
- # account = Account.find 'gale_boetticher'
- # account.update_attributes! :account_code => nil # Raises an exception.
- # @see #update_attributes
- def update_attributes!(attributes = {})
- self.attributes = attributes and save!
- end
-
- # @return [Hash] A hash with indifferent read access containing any
- # validation errors where the key is the attribute name and the value is
- # an array of error messages.
- # @example
- # account.errors # => {"account_code"=>["can't be blank"]}
- # account.errors[:account_code] # => ["can't be blank"]
- def errors
- @errors ||= Errors.new { |h, k| h[k] = [] }
- end
-
- # Marks a record as persisted, i.e. not a new or deleted record, resetting
- # any tracked attribute changes in the process. (This is an internal method
- # and should probably not be called unless you know what you're doing.)
- #
- # @api internal
- # @return [true]
- def persist!(saved = false)
- @new_record, @uri = false
- if changed?
- @previous_changes = changes if saved
- changed_attributes.clear
- end
- true
- end
-
- # @return [String, nil] The unique resource identifier (URI) of the record
- # (if persisted).
- # @example
- # Recurly::Account.new(:account_code => "account_code").uri # => nil
- # Recurly::Account.find("account_code").uri
- # # => "https://api.recurly.com/v2/accounts/account_code"
- def uri
- @href ||= ((API.base_uri + path).to_s if persisted?)
- end
-
- # Attempts to destroy the record.
- #
- # @return [true, false] +true+ if successful, +false+ if unable to destroy
- # (if the record does not persist on Recurly).
- # @raise [NotFound] The record cannot be found.
- # @example
- # account = Recurly::Account.find account_code
- # race_condition = Recurly::Account.find account_code
- # account.destroy # => true
- # account.destroy # => false (already destroyed)
- # race_condition.destroy # raises Recurly::Resource::NotFound
- def destroy
- return false unless persisted?
- @response = API.delete uri
- @destroyed = true
- rescue API::NotFound => e
- raise NotFound, e.description
- end
-
- def signable_attributes
- Hash[xml_keys.map { |key| [key, self[key]] }]
- end
-
- def ==(other)
- other.is_a?(self.class) && other.to_s == to_s
- end
-
- def marshal_dump
- [
- @attributes.reject { |k, v| v.is_a?(Proc) },
- @new_record,
- @destroyed,
- @uri,
- @href,
- changed_attributes,
- previous_changes,
- response,
- etag,
- links,
- @type
- ]
- end
-
- def marshal_load(serialization)
- @attributes,
- @new_record,
- @destroyed,
- @uri,
- @href,
- @changed_attributes,
- @previous_changes,
- @response,
- @etag,
- @links,
- @type = serialization
- end
-
- # @return [String]
- def inspect(attributes = self.class.attribute_names.to_a)
- string = "#<#{self.class}"
- string << "##@type" if respond_to?(:type)
- attributes += %w(errors) if errors.any?
- string << " %s" % attributes.map { |k|
- "#{k}: #{self.send(k).inspect}"
- }.join(', ')
- string << '>'
- end
- alias to_s inspect
-
- def apply_errors(exception)
- @response = exception.response
- document = XML.new exception.response.body
-
- if document.root.name == 'error'
- # Single error is returned from the API
- attribute_path = document['symbol'].text.split '.'
- invalid! [attribute_path[1]], document['description'].text
- else
- # Array of errors was returned by the API
- document.each_element 'error' do |el|
- attribute_path = el.attribute('field').value.split '.'
- invalid! attribute_path[1, attribute_path.length], el.text
- end
- end
- end
-
protected
- def path
- @href or @uri or if persisted?
- self.class.member_path to_param
- else
- self.class.collection_path
- end
+ def initialize(attributes = {})
+ @attributes = attributes.clone
end
- def invalid!(attribute_path, error)
- if attribute_path.length == 1
- errors[attribute_path[0]] << error
- else
- child, k, v = attribute_path.shift.scan(/[^\[\]=]+/)
- if c = k ? self[child].find { |d| d[k] == v } : self[child]
- c.invalid! attribute_path, error if c.methods.include? :invalid!
- e = errors[child] << 'is invalid' and e.uniq!
- end
- end
+ def to_s
+ self.inspect
end
- def clear_errors
- errors.clear
- self.class.associations do |association|
- next unless respond_to? "#{association}=" # Clear writable only.
- [*self[association]].each do |associated|
- associated.clear_errors if associated.respond_to? :clear_errors
- end
- end
- end
-
- def copy_from(other)
- other.instance_variables.each do |ivar|
- instance_variable_set ivar, other.instance_variable_get(ivar)
- end
- end
-
- private
-
- def fetch_associated(name, value, options = {})
- case value
- when Array
- value.map do |v|
- fetch_associated(Helper.singularize(name), v, association_name: name)
- end
- when Hash
- association_name = options[:association_name] || name
- associated_class_name = self.class.find_association(association_name).class_name
- associated_class_name ||= Helper.classify(name)
- Recurly.const_get(associated_class_name, false).send(:new, value)
- when Proc, Resource, Resource::Pager, nil
- value
- else
- raise "unexpected association #{name.inspect}=#{value.inspect}"
- end
- end
-
- def xml_keys
- changed_attributes.keys.sort
- end
-
- def add_money_tag?(key, value)
- value &&
- key.end_with?('_in_cents') &&
- !respond_to?(:currency) &&
- !value.is_a?(Money) &&
- !is_a?(PercentageTier)&&
- !is_a?(SubAddOnPercentageTier)&&
- !is_a?(SubscriptionRampInterval)
+ def schema
+ self.class.schema
end
end
end