Class: Recurly::Resource

Inherits:
Object
  • Object
show all
Defined in:
lib/recurly/resource.rb,
lib/recurly/resource/pager.rb,
lib/recurly/resource/errors.rb,
lib/recurly/resource/association.rb

Overview

The base class for all Recurly resources (e.g. Account, Subscription, Transaction).

Resources behave much like ActiveModel classes, especially like ActiveRecord.

Life Cycle

To take you through the typical life cycle of a resource, we'll use Account as an example.

Creating a Record

You can instantiate a record before attempting to save it.

 = Recurly::Account.new :first_name => 'Walter'

Once instantiated, you can assign and reassign any attribute.

.first_name = 'Walt'
.last_name = 'White'

When you're ready to save, do so.

.save # => false

If save returns false, validation likely failed. You can check the record for errors.

.errors # => {"account_code"=>["can't be blank"]}

Once the errors are fixed, you can try again.

. = 'heisenberg'
.save # => true

The object will be updated with any information provided by the server (including any UUIDs set).

.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
   = Recurly::Account.new :first_name => 'Junior'
  .save!
rescue Recurly::Resource::Invalid
  p .errors
end

You can access the invalid record from the exception itself (if, for example, you use the create! 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.

 = 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.

.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.

.last_name = 'McGill'
.save # Alternatively, call save!

Deleting a Record

To delete (deactivate, close, etc.) a fetched record, merely call destroy on it.

.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 { || p  }

Defined Under Namespace

Classes: Association, Errors, Invalid, NotFound, Pager

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = {}) {|_self| ... } ⇒ Resource

Returns A new resource instance.

Parameters:

  • attributes (Hash) (defaults to: {})

    A hash of attributes.

Yields:

  • (_self)

Yield Parameters:



620
621
622
623
624
625
626
627
628
629
# File 'lib/recurly/resource.rb', line 620

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

Class Attribute Details

.attribute_namesArray? (readonly)

Returns The list of attribute names defined for the resource class.

Returns:

  • (Array, nil)

    The list of attribute names defined for the resource class.



241
242
243
# File 'lib/recurly/resource.rb', line 241

def attribute_names
  @attribute_names
end

Instance Attribute Details

#attributesHash

Returns The raw hash of record attributes.

Returns:

  • (Hash)

    The raw hash of record attributes.



606
607
608
# File 'lib/recurly/resource.rb', line 606

def attributes
  @attributes
end

#etagString? (readonly)

Returns An ETag for the current record.

Returns:

  • (String, nil)

    An ETag for the current record.



613
614
615
# File 'lib/recurly/resource.rb', line 613

def etag
  @etag
end

#responseNet::HTTPResponse? (readonly)

Returns The most recent response object for the record (updated during #save and #destroy).

Returns:

  • (Net::HTTPResponse, nil)

    The most recent response object for the record (updated during #save and #destroy).



610
611
612
# File 'lib/recurly/resource.rb', line 610

def response
  @response
end

#uriString?

Returns The unique resource identifier (URI) of the record (if persisted).

Examples:

Recurly::Account.new(:account_code => "account_code").uri # => nil
Recurly::Account.find("account_code").uri
# => "https://api.recurly.com/v2/accounts/account_code"

Returns:

  • (String, nil)

    The unique resource identifier (URI) of the record (if persisted).



955
956
957
# File 'lib/recurly/resource.rb', line 955

def uri
  @href ||= ((API.base_uri + path).to_s if persisted?)
end

Class Method Details

.all(options = {}) ⇒ Object



262
263
264
# File 'lib/recurly/resource.rb', line 262

def all(options = {})
  paginate(options).to_a
end

.association_class_name(resource_class) ⇒ String?

for the current class if the resource class does not match the actual class.

Returns:

  • (String, nil)

    The actual associated resource class name



491
492
493
494
# File 'lib/recurly/resource.rb', line 491

def association_class_name(resource_class)
  association = find_association(resource_class)
  association.class_name if association
end

.associationsArray

Returns A list of associations for the current class.

Returns:

  • (Array)

    A list of associations for the current class.



478
479
480
# File 'lib/recurly/resource.rb', line 478

def associations
  @associations ||= []
end

.associations_for_relation(relation) ⇒ Array

the relation [:has_many, :has_one, :belongs_to] for the current class.

Returns:

  • (Array)

    A list of associated resource classes with



484
485
486
# File 'lib/recurly/resource.rb', line 484

def associations_for_relation(relation)
  associations.select{ |a| a.relation == relation }.map(&:resource_class)
end

.associations_helperObject



502
503
504
# File 'lib/recurly/resource.rb', line 502

def associations_helper
  @associations_helper ||= Module.new.tap { |helper| include helper }
end

.belongs_to(parent_name, options = {}) ⇒ Proc

Establishes a belongs_to association.

Parameters:

  • parent_name (Symbol)

    Association name.

  • options (Hash) (defaults to: {})

    A hash of association options.

Options Hash (options):

  • :readonly (true, false)

    Don't define a setter.

    String

    :class_name Actual associated resource class name

    if not same as parent_name.

Returns:

  • (Proc)


578
579
580
581
582
583
584
585
586
587
588
# File 'lib/recurly/resource.rb', line 578

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

.collection_nameString Also known as: collection_path

Returns The underscored, pluralized name of the resource class.

Examples:

Recurly::Account.collection_name # => "accounts"

Returns:

  • (String)

    The underscored, pluralized name of the resource class.



178
179
180
# File 'lib/recurly/resource.rb', line 178

def collection_name
  Helper.pluralize Helper.underscore(resource_name)
end

.countInteger

Returns The total record count of the resource in question.

Examples:

Recurly::Account.count # => 42

Returns:

  • (Integer)

    The total record count of the resource in question.

See Also:



302
303
304
# File 'lib/recurly/resource.rb', line 302

def count
  paginate.count
end

.create(attributes = {}) ⇒ Resource

Instantiates and attempts to save a record.

Returns:

Raises:

See Also:



351
352
353
# File 'lib/recurly/resource.rb', line 351

def create(attributes = {})
  new(attributes) { |record| record.save }
end

.create!(attributes = {}) ⇒ Resource

Instantiates and attempts to save a record.

Returns:

Raises:

See Also:



361
362
363
# File 'lib/recurly/resource.rb', line 361

def create!(attributes = {})
  new(attributes) { |record| record.save! }
end

.define_attribute_methods(attribute_names) ⇒ Array

Returns Per attribute, defines readers, writers, boolean and change-tracking methods.

Examples:

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"]

Parameters:

  • attribute_names (Array)

    An array of attribute names.

Returns:

  • (Array)

    Per attribute, defines readers, writers, boolean and change-tracking methods.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/recurly/resource.rb', line 218

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

.embedded!(root_index = false) ⇒ Object



596
597
598
599
600
601
602
# File 'lib/recurly/resource.rb', line 596

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

.find(uuid, options = {}) ⇒ Resource

Returns A record matching the designated unique identifier.

Examples:

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

Parameters:

  • uuid (String)

    The unique identifier of the resource to be retrieved.

  • options (Hash) (defaults to: {})

    A hash of options.

Options Hash (options):

  • :etag (String)

    When set, will raise API::NotModified if the record content has not changed.

Returns:

  • (Resource)

    A record matching the designated unique identifier.

Raises:

  • (Error)

    If the resource has no identifier (and thus cannot be retrieved).

  • (NotFound)

    If no resource can be found for the supplied identifier (or the supplied identifier is nil).

  • (API::NotModified)

    If the :etag option is set and matches the server's.



333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/recurly/resource.rb', line 333

def find(uuid, options = {})
  if uuid.nil? || uuid.to_s.empty?
    raise NotFound, "can't find a record with nil identifier"
  end

  uri = uuid =~ /^http/ ? uuid : member_path(uuid)
  begin
    from_response API.get(uri, {}, options)
  rescue API::NotFound => e
    raise NotFound, e.description
  end
end

.find_association(resource_class) ⇒ Association?

Returns Find association for the current class with resource class name.

Returns:

  • (Association, nil)

    Find association for the current class with resource class name.



498
499
500
# File 'lib/recurly/resource.rb', line 498

def find_association(resource_class)
  associations.find{ |a| a.resource_class.to_s == resource_class.to_s }
end

.find_each(per_page = 50) {|record| ... } ⇒ nil

Iterates through every record by automatically paging.

Examples:

Recurly::Account.find_each { |a| p a }

Parameters:

  • per_page (Integer) (defaults to: 50)

    The number of records returned per request.

Yields:

  • (record)

Returns:

  • (nil)

See Also:



294
295
296
# File 'lib/recurly/resource.rb', line 294

def find_each(per_page = 50, &block)
  paginate(:per_page => per_page).find_each(&block)
end

.firstResource?

Returns:



308
309
310
# File 'lib/recurly/resource.rb', line 308

def first
  paginate(:per_page => 1).first
end

.from_response(response) ⇒ Resource

Instantiates a record from an HTTP response, setting the record's response attribute in the process.

Parameters:

  • response (Net::HTTPResponse)

Returns:



370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/recurly/resource.rb', line 370

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

.from_xml(xml) ⇒ Resource

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.

Parameters:

  • xml (String, REXML::Element, Nokogiri::XML::Node)

Returns:

See Also:



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/recurly/resource.rb', line 393

def from_xml(xml)
  xml = XML.new xml

  if self != Resource || xml.name == member_name
    record = new
  elsif Recurly.const_defined?(class_name = Helper.classify(xml.name), false)
    klass = Recurly.const_get class_name, false
    record = klass.send :new
  elsif root = xml.root and root.elements.empty?
    return XML.cast root
  else
    record = {}
  end
  klass ||= self

  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 el.children.empty? && href = el.attribute('href')
      klass_name = Helper.classify(klass.association_class_name(el.name) ||
                                   el.attribute('type') ||
                                   el.name)

      next unless Recurly.const_defined?(klass_name)

      resource_class = Recurly.const_get(klass_name, false)

      case el.name
      when *klass.associations_for_relation(:has_many)
        record[el.name] = Pager.new(
          resource_class, :uri => href.value, :parent => record
        )
      when *(klass.associations_for_relation(:has_one) + klass.associations_for_relation(:belongs_to))
        record.links[el.name] = {
          :resource_class => resource_class,
          :method => :get,
          :href => href.value
        }
      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

.has_many(collection_name, options = {}) ⇒ Proc?

Establishes a has_many association.

Parameters:

  • collection_name (Symbol)

    Association name.

  • options (Hash) (defaults to: {})

    A hash of association options.

Options Hash (options):

  • :readonly (true, false)

    Don't define a setter.

    String

    :class_name Actual associated resource class name

    if not same as collection_name.

Returns:

  • (Proc, nil)


514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/recurly/resource.rb', line 514

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] = []
      end
    end
    if options.key?(:readonly) && options[:readonly] == false
      define_method "#{collection_name}=" do |collection|
        self[collection_name] = collection
      end
    end
  end
end

.has_one(member_name, options = {}) ⇒ Proc?

Establishes a has_one association.

Parameters:

  • member_name (Symbol)

    Association name.

  • options (Hash) (defaults to: {})

    A hash of association options.

Options Hash (options):

  • :readonly (true, false)

    Don't define a setter.

    String

    :class_name Actual associated resource class name

    if not same as member_name.

Returns:

  • (Proc, nil)


540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# File 'lib/recurly/resource.rb', line 540

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}/#{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

.member_nameString

Returns The underscored name of the resource class.

Examples:

Recurly::Account.member_name # => "account"

Returns:

  • (String)

    The underscored name of the resource class.



186
187
188
# File 'lib/recurly/resource.rb', line 186

def member_name
  Helper.underscore resource_name
end

.member_path(uuid) ⇒ String

Returns The relative path to a resource's identifier from the API's base URI.

Examples:

Recurly::Account.member_path "code" # => "accounts/code"
Recurly::Account.member_path nil    # => "accounts"

Parameters:

  • uuid (String, nil)

Returns:

  • (String)

    The relative path to a resource's identifier from the API's base URI.



196
197
198
199
# File 'lib/recurly/resource.rb', line 196

def member_path(uuid)
  uuid = ERB::Util.url_encode(uuid) if uuid
  [collection_path, uuid].compact.join '/'
end

.paginate(options = {}) ⇒ Pager Also known as: scoped, where

Returns A pager with an iterable collection of records

Examples:

Fetch 50 records and iterate over them

Recurly::Account.paginate(:per_page => 50).each { |a| p a }

Fetch records before January 1, 2011

Recurly::Account.paginate(:cursor => Time.new(2011, 1, 1))

Parameters:

  • options (Hash) (defaults to: {})

    A hash of pagination options

Options Hash (options):

  • :per_page (Integer)

    The number of records returned per page

  • :cursor (DateTime, Time, Integer)

    A timestamp that the pager will skim back to and return records created before it

  • :etag (String)

    When set, will raise API::NotModified if the pager's loaded page content has not changed

Returns:

  • (Pager)

    A pager with an iterable collection of records



256
257
258
# File 'lib/recurly/resource.rb', line 256

def paginate(options = {})
  Pager.new self, options
end

.reflect_on_association(name) ⇒ :has_many, ...

Returns An association type.

Returns:

  • (:has_many, :has_one, :belongs_to, nil)

    An association type.



591
592
593
594
# File 'lib/recurly/resource.rb', line 591

def reflect_on_association(name)
  a = find_association(name)
  a.relation if a
end

.resource_nameString

Returns The demodulized name of the resource class.

Examples:

Recurly::Account.name # => "Account"

Returns:

  • (String)

    The demodulized name of the resource class.



170
171
172
# File 'lib/recurly/resource.rb', line 170

def resource_name
  Helper.demodulize name
end

.scope(name, params = {}) ⇒ Proc

Defines a new resource scope.

Parameters:

  • name (Symbol)

    the scope name

  • params (Hash) (defaults to: {})

    the scope params

Returns:

  • (Proc)


281
282
283
284
# File 'lib/recurly/resource.rb', line 281

def scope(name, params = {})
  scopes[name = name.to_s] = params
  scopes_helper.send(:define_method, name) { paginate scopes[name] }
end

.scopesHash

Returns Defined scopes per resource.

Returns:

  • (Hash)

    Defined scopes per resource.



267
268
269
# File 'lib/recurly/resource.rb', line 267

def scopes
  @scopes ||= Recurly::Helper.hash_with_indifferent_read_access
end

.scopes_helperModule

Returns Module of scopes methods.

Returns:

  • (Module)

    Module of scopes methods.



272
273
274
# File 'lib/recurly/resource.rb', line 272

def scopes_helper
  @scopes_helper ||= Module.new.tap { |helper| extend helper }
end

Instance Method Details

#==(other) ⇒ Object



982
983
984
# File 'lib/recurly/resource.rb', line 982

def ==(other)
  other.is_a?(self.class) && other.to_s == to_s
end

#apply_errors(exception) ⇒ Object



1026
1027
1028
1029
1030
1031
1032
1033
# File 'lib/recurly/resource.rb', line 1026

def apply_errors(exception)
  @response = exception.response
  document = XML.new exception.response.body
  document.each_element 'error' do |el|
    attribute_path = el.attribute('field').value.split '.'
    invalid! attribute_path[1, attribute_path.length], el.text
  end
end

#as_json(options = nil) ⇒ Object



760
761
762
# File 'lib/recurly/resource.rb', line 760

def as_json(options = nil)
  attributes.reject { |k, v| v.is_a?(Recurly::Resource::Pager) }
end

#changedArray

Returns A list of changed attribute keys.

Returns:

  • (Array)

    A list of changed attribute keys.



655
656
657
# File 'lib/recurly/resource.rb', line 655

def changed
  changed_attributes.keys
end

#changed?true, false

Do any attributes have unsaved changes?

Returns:

  • (true, false)


661
662
663
# File 'lib/recurly/resource.rb', line 661

def changed?
  !changed_attributes.empty?
end

#changed_attributesHash

Returns Hash of changed attributes.

Returns:

  • (Hash)

    Hash of changed attributes.

See Also:



650
651
652
# File 'lib/recurly/resource.rb', line 650

def changed_attributes
  @changed_attributes ||= {}
end

#changesHash

Returns Map of changed attributes to original value and new value.

Returns:

  • (Hash)

    Map of changed attributes to original value and new value.



666
667
668
669
670
# File 'lib/recurly/resource.rb', line 666

def changes
  changed_attributes.inject({}) { |changes, (key, original_value)|
    changes[key] = [original_value, self[key]] and changes
  }
end

#destroytrue, false

Attempts to destroy the record.

(if the record does not persist on Recurly).

Examples:

 = Recurly::Account.find 
race_condition = Recurly::Account.find 
.destroy        # => true
.destroy        # => false (already destroyed)
race_condition.destroy # raises Recurly::Resource::NotFound

Returns:

  • (true, false)

    true if successful, false if unable to destroy

Raises:

  • (NotFound)

    The record cannot be found.



970
971
972
973
974
975
976
# File 'lib/recurly/resource.rb', line 970

def destroy
  return false unless persisted?
  @response = API.delete uri
  @destroyed = true
rescue API::NotFound => e
  raise NotFound, e.description
end

#destroyed?true, false

Has the record been destroyed? (Set true after a successful destroy.)

Returns:

  • (true, false)

See Also:



691
692
693
# File 'lib/recurly/resource.rb', line 691

def destroyed?
  @destroyed
end

#errorsHash

Returns 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.

Examples:

.errors                # => {"account_code"=>["can't be blank"]}
.errors[:account_code] # => ["can't be blank"]

Returns:

  • (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.



930
931
932
# File 'lib/recurly/resource.rb', line 930

def errors
  @errors ||= Errors.new { |h, k| h[k] = [] }
end

Fetch the value of a link by following the associated href.

Examples:

.read_link :billing_info # => <Recurly::BillingInfo>

Parameters:

  • key (Symbol, String)

    The name of the link to be followed.

  • options (Hash) (defaults to: {})

    A hash of API options.



784
785
786
787
788
789
790
791
792
793
794
795
# File 'lib/recurly/resource.rb', line 784

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

#inspect(attributes = self.class.attribute_names.to_a) ⇒ String Also known as: to_s

Returns:

  • (String)


1015
1016
1017
1018
1019
1020
1021
1022
1023
# File 'lib/recurly/resource.rb', line 1015

def inspect(attributes = self.class.attribute_names.to_a)
  string = "#<#{self.class}"
  string << "##@type" if instance_variable_defined? :@type
  attributes += %w(errors) if errors.any?
  string << " %s" % attributes.map { |k|
    "#{k}: #{self.send(k).inspect}"
  }.join(', ')
  string << '>'
end

#link?(key) ⇒ Boolean

Whether a record has a link with the given name.

Examples:

.link? :billing_info # => true

Parameters:

  • key (Symbol, String)

    The name of the link to check for.

Returns:

  • (Boolean)


774
775
776
# File 'lib/recurly/resource.rb', line 774

def link?(key)
  links.key?(key.to_s)
end

Returns The raw hash of record href links.

Returns:

  • (Hash)

    The raw hash of record href links.



765
766
767
# File 'lib/recurly/resource.rb', line 765

def links
  @links ||= {}
end

#marshal_dumpObject



986
987
988
989
990
991
992
993
994
995
996
997
998
999
# File 'lib/recurly/resource.rb', line 986

def marshal_dump
  [
    @attributes.reject { |k, v| v.is_a?(Proc) },
    @new_record,
    @destroyed,
    @uri,
    @href,
    changed_attributes,
    previous_changes,
    response,
    etag,
    links
  ]
end

#marshal_load(serialization) ⇒ Object



1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
# File 'lib/recurly/resource.rb', line 1001

def marshal_load(serialization)
  @attributes,
    @new_record,
    @destroyed,
    @uri,
    @href,
    @changed_attributes,
    @previous_changes,
    @response,
    @etag,
    @links = serialization
end

#new_record?true, false

Is the record new (i.e., not saved on Recurly's servers)?

Returns:

  • (true, false)

See Also:



683
684
685
# File 'lib/recurly/resource.rb', line 683

def new_record?
  @new_record
end

#persist!(saved = false) ⇒ true

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.)

Returns:

  • (true)


940
941
942
943
944
945
946
947
# File 'lib/recurly/resource.rb', line 940

def persist!(saved = false)
  @new_record, @uri = false
  if changed?
    @previous_changes = changes if saved
    changed_attributes.clear
  end
  true
end

#persisted?true, false

Has the record persisted (i.e., saved on Recurly's servers)?

Returns:

  • (true, false)

See Also:



700
701
702
# File 'lib/recurly/resource.rb', line 700

def persisted?
  !(new_record? || destroyed?)
end

#previous_changesHash

Returns Previously-changed attributes.

Returns:

  • (Hash)

    Previously-changed attributes.

See Also:



674
675
676
# File 'lib/recurly/resource.rb', line 674

def previous_changes
  @previous_changes ||= {}
end

#read_attribute(key) ⇒ Object Also known as: []

The value of a specified attribute, lazily fetching any defined association.

Examples:

.read_attribute :first_name # => "Ted"
[:last_name]                # => "Beneke"

Parameters:

  • key (Symbol, String)

    The name of the attribute to be fetched.

See Also:



712
713
714
715
716
717
718
719
720
# File 'lib/recurly/resource.rb', line 712

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

#reload(response = nil) ⇒ self

Returns Reloads the record from the server.

Returns:

  • (self)

    Reloads the record from the server.



632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
# File 'lib/recurly/resource.rb', line 632

def reload(response = nil)
  if response
    return if response.body.to_s.length.zero?
    fresh = self.class.from_response response
  else
    fresh = self.class.find(
      @href || to_param, :etag => (etag unless changed?)
    )
  end
  fresh and copy_from fresh
  persist! true
  self
rescue API::NotModified
  self
end

#savetrue, false

Attempts to save the record, returning the success of the request.

Examples:

 = Recurly::Account.new
.save # => false
. = 'account_code'
.save # => true

Returns:

  • (true, false)

Raises:

See Also:



845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
# File 'lib/recurly/resource.rb', line 845

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))
  false
end

#save!true

Attempts to save the record, returning true if the record was saved and raising Invalid otherwise.

Examples:

 = Recurly::Account.new
.save! # raises Recurly::Resource::Invalid
. = 'account_code'
.save! # => true

Returns:

  • (true)

Raises:

See Also:



873
874
875
# File 'lib/recurly/resource.rb', line 873

def save!
  save || raise(Invalid.new(self))
end

#signable_attributesObject



978
979
980
# File 'lib/recurly/resource.rb', line 978

def signable_attributes
  Hash[xml_keys.map { |key| [key, self[key]] }]
end

#to_xml(options = {}) ⇒ String

Serializes the record to XML.

Examples:

Recurly::Account.new(:account_code => 'code').to_xml
# => "<account><account_code>code</account_code></account>"

Parameters:

  • options (Hash) (defaults to: {})

    A hash of XML options.

Returns:

  • (String)

    An XML string.



804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
# File 'lib/recurly/resource.rb', line 804

def to_xml(options = {})
  builder = options[:builder] || XML.new("<#{self.class.member_name}/>")
  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, Subscription::AddOns
      value.to_xml options.merge(:builder => node)
    when Array
      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

#update_attributes(attributes = {}) ⇒ true, false

Update a record with a given hash of attributes.

Examples:

 = Account.find 'junior'
.update_attributes :account_code => 'flynn' # => true

Parameters:

  • attributes (Hash) (defaults to: {})

    A hash of attributes.

Returns:

  • (true, false)

    The success of the update.

Raises:

See Also:



906
907
908
# File 'lib/recurly/resource.rb', line 906

def update_attributes(attributes = {})
  self.attributes = attributes and save
end

#update_attributes!(attributes = {}) ⇒ true

Update a record with a given hash of attributes.

Examples:

 = Account.find 'gale_boetticher'
.update_attributes! :account_code => nil # Raises an exception.

Parameters:

  • attributes (Hash) (defaults to: {})

    A hash of attributes.

Returns:

  • (true)

    The update was successful.

Raises:

See Also:



920
921
922
# File 'lib/recurly/resource.rb', line 920

def update_attributes!(attributes = {})
  self.attributes = attributes and save!
end

#valid?true, ...

Returns 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).

Examples:

 = Recurly::Account.new
.valid? # => nil
.save   # => false
.valid? # => false
. = 'account_code'
.save   # => true
.valid? # => true

Returns:

  • (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).



890
891
892
893
894
895
# File 'lib/recurly/resource.rb', line 890

def valid?
  return true if persisted? && !changed?
  errors_empty = errors.values.flatten.empty?
  return if errors_empty && changed?
  errors_empty
end

#write_attribute(key, value) ⇒ Object Also known as: []=

Sets the value of a specified attribute.

Examples:

.write_attribute :first_name, 'Gus'
[:company_name] = 'Los Pollos Hermanos'

Parameters:

  • key (Symbol, String)

    The name of the attribute to be set.

  • value (Object)

    The value the attribute will be set to.

See Also:



731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
# File 'lib/recurly/resource.rb', line 731

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 value && key.end_with?('_in_cents') && !respond_to?(:currency)
    value = Money.new(value, self, key) unless value.is_a?(Money)
  end

  attributes[key] = value
end