Module: Ardm

Defined in:
lib/ardm/support/logger.rb,
lib/ardm.rb,
lib/ardm/version.rb,
lib/ardm/property.rb,
lib/ardm/property.rb,
lib/ardm/data_mapper.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/active_record.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/property/class.rb,
lib/ardm/property/float.rb,
lib/ardm/query/operator.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/active_record/is.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/active_record/base.rb,
lib/ardm/data_mapper/record.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/active_record/dirty.rb,
lib/ardm/active_record/hooks.rb,
lib/ardm/active_record/query.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/active_record/record.rb,
lib/ardm/property/bcrypt_hash.rb,
lib/ardm/active_record/finalize.rb,
lib/ardm/active_record/property.rb,
lib/ardm/active_record/relation.rb,
lib/ardm/property/discriminator.rb,
lib/ardm/property/support/flags.rb,
lib/ardm/support/descendant_set.rb,
lib/ardm/active_record/collection.rb,
lib/ardm/active_record/repository.rb,
lib/ardm/active_record/inheritance.rb,
lib/ardm/active_record/validations.rb,
lib/ardm/property/paranoid_boolean.rb,
lib/ardm/active_record/associations.rb,
lib/ardm/property/paranoid_datetime.rb,
lib/ardm/support/local_object_space.rb,
lib/ardm/support/naming_conventions.rb,
lib/ardm/active_record/serialization.rb,
lib/ardm/active_record/storage_names.rb,
lib/ardm/property/invalid_value_error.rb,
lib/ardm/property/comma_separated_list.rb,
lib/ardm/property/support/dirty_minder.rb,
lib/ardm/active_record/is/state_machine.rb,
lib/ardm/property/support/paranoid_base.rb,
lib/ardm/active_record/predicate_builder/rails3.rb,
lib/ardm/active_record/predicate_builder/rails4.rb,
lib/ardm/active_record/data_mapper_constant_proxy.rb,
lib/ardm/active_record/predicate_builder/array_handler.rb,
lib/ardm/active_record/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: ActiveRecord, Assertions, DataMapper, Deprecate, Equalizer, Ext, Hook, LocalObjectSpace, NamingConventions, Query, Subject Classes: DescendantSet, Logger, Mash, OrderedSet, Property, PropertySet, SubjectSet

Constant Summary collapse

NotImplemented =
Class.new(RuntimeError)
VERSION =
'0.2.0'
Record =
Ardm::ActiveRecord::Record
SaveFailureError =
::ActiveRecord::RecordNotSaved
RecordNotFound =
::ActiveRecord::RecordNotFound
Collection =
::ActiveRecord::Relation

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

.active_recordObject

Yield if Ardm has loaded ActiveRecord ORM.



43
44
45
# File 'lib/ardm.rb', line 43

def self.active_record
  yield if block_given? && active_record?
end

.active_record?Boolean

Return true if Ardm has loaded ActiveRecord ORM.

Returns:

  • (Boolean)


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

def self.active_record?
  orm == :active_record
end

.data_mapperObject

Yield if Ardm has loaded DataMapper ORM.



58
59
60
# File 'lib/ardm.rb', line 58

def self.data_mapper
  yield if block_given? && data_mapper?
end

.data_mapper?Boolean

Return true if Ardm has loaded DataMapper ORM.

Returns:

  • (Boolean)


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

def self.data_mapper?
  orm == :data_mapper
end

.define_datamapper_constant!Object



38
39
40
# File 'lib/ardm/active_record.rb', line 38

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

.libObject



62
63
64
# File 'lib/ardm.rb', line 62

def self.lib
  "ardm/#{orm}"
end

.ormObject

Check which ORM is loaded in Ardm.



7
8
9
# File 'lib/ardm.rb', line 7

def self.orm
  @orm ||= :active_record
end

.orm=(orm) ⇒ Object

Set which orm to load.



14
15
16
17
18
19
20
21
22
23
24
# File 'lib/ardm.rb', line 14

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

  @orm = case orm.to_s
         when /active_?record/, '' then :active_record
         when /data_?mapper/       then :data_mapper
         else raise "Unknown ENV['ORM']: #{ENV['ORM']}"
         end
end

.rails3?Boolean

Returns:

  • (Boolean)


47
48
49
# File 'lib/ardm.rb', line 47

def self.rails3?
  self.active_record? && ::ActiveRecord::VERSION::STRING >= "3.0" && ::ActiveRecord::VERSION::STRING <= "4.0"
end

.rails4?Boolean

Returns:

  • (Boolean)


51
52
53
# File 'lib/ardm.rb', line 51

def self.rails4?
  self.active_record? && !self.rails3?
end