Module: Ardm

Defined in:
lib/ardm/support/logger.rb,
lib/ardm.rb,
lib/ardm/ar.rb,
lib/ardm/dm.rb,
lib/ardm/ar/is.rb,
lib/ardm/ar/base.rb,
lib/ardm/version.rb,
lib/ardm/ar/dirty.rb,
lib/ardm/ar/hooks.rb,
lib/ardm/ar/query.rb,
lib/ardm/property.rb,
lib/ardm/property.rb,
lib/ardm/ar/record.rb,
lib/ardm/dm/record.rb,
lib/ardm/ar/finalize.rb,
lib/ardm/ar/property.rb,
lib/ardm/ar/relation.rb,
lib/ardm/deprecation.rb,
lib/ardm/property/csv.rb,
lib/ardm/property/uri.rb,
lib/ardm/property_set.rb,
lib/ardm/support/hook.rb,
lib/ardm/support/mash.rb,
lib/ardm/ar/collection.rb,
lib/ardm/ar/repository.rb,
lib/ardm/property/date.rb,
lib/ardm/property/enum.rb,
lib/ardm/property/flag.rb,
lib/ardm/property/json.rb,
lib/ardm/property/slug.rb,
lib/ardm/property/text.rb,
lib/ardm/property/time.rb,
lib/ardm/property/uuid.rb,
lib/ardm/property/yaml.rb,
lib/ardm/ar/inheritance.rb,
lib/ardm/ar/persistence.rb,
lib/ardm/ar/validations.rb,
lib/ardm/property/class.rb,
lib/ardm/property/float.rb,
lib/ardm/query/operator.rb,
lib/ardm/ar/associations.rb,
lib/ardm/property/binary.rb,
lib/ardm/property/lookup.rb,
lib/ardm/property/object.rb,
lib/ardm/property/regexp.rb,
lib/ardm/property/serial.rb,
lib/ardm/property/string.rb,
lib/ardm/support/subject.rb,
lib/ardm/ar/serialization.rb,
lib/ardm/ar/storage_names.rb,
lib/ardm/property/api_key.rb,
lib/ardm/property/boolean.rb,
lib/ardm/property/decimal.rb,
lib/ardm/property/integer.rb,
lib/ardm/property/numeric.rb,
lib/ardm/query/expression.rb,
lib/ardm/query/ext/symbol.rb,
lib/ardm/support/ext/hash.rb,
lib/ardm/property/datetime.rb,
lib/ardm/support/deprecate.rb,
lib/ardm/support/equalizer.rb,
lib/ardm/support/ext/array.rb,
lib/ardm/support/ext/blank.rb,
lib/ardm/property/file_path.rb,
lib/ardm/support/assertions.rb,
lib/ardm/support/ext/module.rb,
lib/ardm/support/ext/object.rb,
lib/ardm/support/ext/string.rb,
lib/ardm/ar/is/state_machine.rb,
lib/ardm/property/epoch_time.rb,
lib/ardm/property/ip_address.rb,
lib/ardm/property/validation.rb,
lib/ardm/support/ext/try_dup.rb,
lib/ardm/support/ordered_set.rb,
lib/ardm/support/subject_set.rb,
lib/ardm/property/bcrypt_hash.rb,
lib/ardm/property/discriminator.rb,
lib/ardm/property/support/flags.rb,
lib/ardm/support/descendant_set.rb,
lib/ardm/property/paranoid_boolean.rb,
lib/ardm/property/paranoid_datetime.rb,
lib/ardm/support/local_object_space.rb,
lib/ardm/support/naming_conventions.rb,
lib/ardm/ar/predicate_builder/rails3.rb,
lib/ardm/ar/predicate_builder/rails4.rb,
lib/ardm/property/invalid_value_error.rb,
lib/ardm/ar/data_mapper_constant_proxy.rb,
lib/ardm/property/comma_separated_list.rb,
lib/ardm/property/support/dirty_minder.rb,
lib/ardm/property/support/paranoid_base.rb,
lib/ardm/ar/predicate_builder/array_handler.rb,
lib/ardm/ar/predicate_builder/relation_handler.rb

Overview

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)

Defined Under Namespace

Modules: Ar, Assertions, Deprecate, Deprecation, Dm, Equalizer, Ext, Hook, LocalObjectSpace, NamingConventions, Query, Subject Classes: DescendantSet, Logger, Mash, OrderedSet, Property, PropertySet, SubjectSet

Constant Summary collapse

NotImplemented =
Class.new(RuntimeError)
Record =
Ardm::Dm::Record
Validations =
Ardm::Ar::Validations
SaveFailureError =
::DataMapper::SaveFailureError
RecordNotFound =
::DataMapper::ObjectNotFoundError
Collection =
::ActiveRecord::Relation
VERSION =
'0.4.0'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.loggerObject

Returns the value of attribute logger.



36
37
38
# File 'lib/ardm/support/logger.rb', line 36

def logger
  @logger
end

Class Method Details

.arObject Also known as: activerecord, active_record

Yield if Ardm has loaded ActiveRecord ORM.



93
94
95
# File 'lib/ardm.rb', line 93

def ar
  yield if block_given? && ar?
end

.ar?Boolean Also known as: activerecord?, active_record?

Return true if Ardm has loaded ActiveRecord ORM.

Returns:

  • (Boolean)


75
76
77
# File 'lib/ardm.rb', line 75

def ar?
  orm == :ar
end

.define_datamapper_constant!Object



40
41
42
# File 'lib/ardm/ar.rb', line 40

def self.define_datamapper_constant!
  require 'ardm/ar/data_mapper_constant'
end

.dmObject Also known as: datamapper, data_mapper

Yield if Ardm has loaded DataMapper ORM.



102
103
104
# File 'lib/ardm.rb', line 102

def dm
  yield if block_given? && dm?
end

.dm?Boolean Also known as: datamapper?, data_mapper?

Return true if Ardm has loaded DataMapper ORM.

Returns:

  • (Boolean)


84
85
86
# File 'lib/ardm.rb', line 84

def dm?
  orm == :dm
end

.libObject



68
69
70
# File 'lib/ardm.rb', line 68

def lib
  "ardm/#{orm}"
end

.ormObject

Check which ORM is loaded in Ardm.



36
37
38
39
40
41
42
43
# File 'lib/ardm.rb', line 36

def orm
  if @orm
    return @orm
  else
    self.orm = ENV['ORM']
  end
  @orm
end

.orm=(orm) ⇒ Object

Set which orm to load.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 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?Boolean

Returns:

  • (Boolean)


109
110
111
# File 'lib/ardm.rb', line 109

def rails3?
  ar? && ::ActiveRecord::VERSION::STRING >= "3.0" && ::ActiveRecord::VERSION::STRING <= "4.0"
end

.rails4?Boolean

Returns:

  • (Boolean)


113
114
115
# File 'lib/ardm.rb', line 113

def rails4?
  ar? && !rails3?
end

.setup(orm = nil) {|_self| ... } ⇒ Object

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.

Yields:

  • (_self)

Yield Parameters:

  • _self (Ardm)

    the object that the method was called on



27
28
29
30
31
# File 'lib/ardm.rb', line 27

def setup(orm=nil)
  self.orm = orm if orm
  yield self if block_given?
  require lib
end