module Ardm

Approach

We need to detect whether or not the underlying Hash or Array changed and update the dirty-ness of the encapsulating Resource accordingly (so that it will actually save).

DM’s state-tracking code only triggers dirty-ness by comparing the new value against the instance’s Property’s current value. WRT mutation, we have to choose one of the following approaches:

(1) mutate a copy ("after"), then invoke the Resource assignment and State
    tracking

(2) create a copy ("before"), mutate self ("after"), then invoke the
    Resource assignment and State tracking

(1) seemed simpler at first, but it required additional steps to alias the original (pre-hooked) methods before overriding them (so they could be invoked externally, ala self.clone.send(“orig_…”)), and more importantly it resulted in any external references keeping their old value (instead of getting the new), like so:

copy = instance.json
copy[:some] = :value
instance.json[:some] == :value
 => true
copy[:some] == :value
 => false  # fk!

In order to do (2) and still have State tracking trigger normally, we need to ensure the Property has a different value other than self when the State tracking does the comparison. This equates to setting the Property directly to the “before” value (a clone and thus a different object/value) before invoking the Resource Property/attribute assignment.

The cloning of any value might sound expensive, but it’s identical in cost to what you already had to do: assign a cloned copy in order to trigger dirty-ness (e.g. ::Ardm::Property::Json):

model.json = model.json.merge({:some=>:value})

Hooking Core Classes

We want to hook certain methods on Hash and Array to trigger dirty-ness in the resource. However, because these are core classes, they are individually mapped to C primitives and thus cannot be hooked through send/#__send__. We have to override each method, but we don’t want to write a lot of code.

Minimally Invasive

We also want to extend behaviour of existing class instances instead of impersonating/delegating from a proxy class of our own, or overriding a global class behaviour. This is the most flexible approach and least prone to error, since it leaves open the option for consumers to proxy or override global classes, and is less likely to interfere with method_missing/etc shenanigans.

Nested Object Mutations

Since we use {Array,Hash}#hash to compare before & after, and hash accounts for/traverses nested structures, no “deep” inspection logic is technically necessary. However, Resource#dirty? only queries a cache of dirtied attributes, whose own population strategy is to hook assignment (instead of interrogating properties on demand). So the approach is still limited to top-level mutators.

Maybe consider optional “advisory” Property#dirty? method for Resource#dirty? that custom properties could use for this purpose.

TODO: add support for detecting mutations in nested objects, but we can’t

catch the assignment from here (yet?).

TODO: ensure we covered all indirectly-mutable classes that DM uses underneath

a property type

TODO: figure out how to hook core class methods on RBX (which do use send)

Public Ardm Logger API

To replace an existing logger with a new one:

Ardm::Logger.set_log(log{String, IO},level{Symbol, String})

Available logging levels are

Ardm::Logger::{ Fatal, Error, Warn, Info, Debug }

Logging via:

Ardm.logger.fatal(message<String>,&block)
Ardm.logger.error(message<String>,&block)
Ardm.logger.warn(message<String>,&block)
Ardm.logger.info(message<String>,&block)
Ardm.logger.debug(message<String>,&block)

Logging with autoflush:

Ardm.logger.fatal!(message<String>,&block)
Ardm.logger.error!(message<String>,&block)
Ardm.logger.warn!(message<String>,&block)
Ardm.logger.info!(message<String>,&block)
Ardm.logger.debug!(message<String>,&block)

Flush the buffer to

Ardm.logger.flush

Remove the current log object

Ardm.logger.close

Private Ardm Logger API

To initialize the logger you create a new object, proxies to set_log.

Ardm::Logger.new(log{String, IO},level{Symbol, String})

Constants

Collection
NotImplemented
Property
Record
RecordNotFound
SaveFailureError
VERSION
Validations

Attributes

logger[RW]

Public Class Methods

active_record()
Alias for: ar
active_record?()
Alias for: ar?
activerecord()
Alias for: ar
activerecord?()
Alias for: ar?
ar() { || ... } click to toggle source

Yield if Ardm has loaded ActiveRecord ORM.

@api public

# File lib/ardm.rb, line 93
def ar
  yield if block_given? && ar?
end
Also aliased as: activerecord, active_record
ar?() click to toggle source

Return true if Ardm has loaded ActiveRecord ORM.

@api public

# File lib/ardm.rb, line 75
def ar?
  orm == :ar
end
Also aliased as: activerecord?, active_record?
data_mapper()
Alias for: dm
data_mapper?()
Alias for: dm?
datamapper()
Alias for: dm
datamapper?()
Alias for: dm?
define_datamapper_constant!() click to toggle source
# File lib/ardm/ar.rb, line 40
def self.define_datamapper_constant!
  require 'ardm/ar/data_mapper_constant'
end
dm() { || ... } click to toggle source

Yield if Ardm has loaded DataMapper ORM.

@api public

# File lib/ardm.rb, line 102
def dm
  yield if block_given? && dm?
end
Also aliased as: datamapper, data_mapper
dm?() click to toggle source

Return true if Ardm has loaded DataMapper ORM.

@api public

# File lib/ardm.rb, line 84
def dm?
  orm == :dm
end
Also aliased as: datamapper?, data_mapper?
lib() click to toggle source
# File lib/ardm.rb, line 68
def lib
  "ardm/#{orm}"
end
orm() click to toggle source

Check which ORM is loaded in Ardm.

@api public

# File lib/ardm.rb, line 36
def orm
  if @orm
    return @orm
  else
    self.orm = ENV['ORM']
  end
  @orm
end
orm=(orm) click to toggle source

Set which orm to load.

@api public

# File lib/ardm.rb, line 48
def orm=(orm)
  neworm =
    case orm.to_s
    when /(ar|active_?record)/ then :ar
    when /(dm|data_?mapper)/   then :dm
    when "" then raise "Specify Ardm.orm by assigning :ar or :dm or by setting ENV['ORM']"
    else raise "Unknown Ardm.orm. Expected: (ar|dm). Got: #{orm.inspect}"
    end

  if @orm == neworm
    return @orm
  end

  if defined?(Ardm::Ar) || defined?(Ardm::Dm)
    raise "Cannot change Ardm.orm when #{self.orm} libs are already loaded."
  end

  @orm = neworm
end
rails3?() click to toggle source
# File lib/ardm.rb, line 109
def rails3?
  ar? && ::ActiveRecord::VERSION::STRING >= "3.0" && ::ActiveRecord::VERSION::STRING <= "4.0"
end
rails4?() click to toggle source
# File lib/ardm.rb, line 113
def rails4?
  ar? && !rails3?
end
setup(orm=nil) { |self| ... } click to toggle source

Setup the ORM using orm arg or $ORM, then require the correct shim libs.

If an ORM is not specified as an argument, ENV will be used. If $ORM is not set, then Ardm will raise.

Execute the block if one is given. This is a good time to require active_record or dm-core using Ardm.ar or Ardm.dm blocks.

Ardm.setup ENV['ORM'] do
  Ardm.ar do
    Bundler.require(:active_record)
    require "active_record/railtie"
  end
  Ardm.dm { Bundler.require(:data_mapper) }
end

The Ardm shim libs will be required after the block returns.

@api public

# File lib/ardm.rb, line 27
def setup(orm=nil)
  self.orm = orm if orm
  yield self if block_given?
  require lib
end