memory_model 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +15 -0
  2. data/.ruby-version +1 -0
  3. data/.travis.yml +7 -4
  4. data/Appraisals +9 -0
  5. data/Guardfile +7 -1
  6. data/README.md +5 -1
  7. data/Rakefile +12 -0
  8. data/gemfiles/rails_3.gemfile +8 -0
  9. data/gemfiles/rails_3.gemfile.lock +109 -0
  10. data/gemfiles/rails_4.gemfile +8 -0
  11. data/gemfiles/rails_4.gemfile.lock +117 -0
  12. data/lib/memory_model/base/actions/class_methods.rb +27 -0
  13. data/lib/memory_model/base/actions.rb +75 -0
  14. data/lib/memory_model/base/attributes.rb +87 -0
  15. data/lib/memory_model/base/auto_increment.rb +47 -0
  16. data/lib/memory_model/base/collectible.rb +26 -0
  17. data/lib/memory_model/base/conversion.rb +11 -0
  18. data/lib/memory_model/base/fields/field.rb +57 -0
  19. data/lib/memory_model/base/fields/field_set.rb +87 -0
  20. data/lib/memory_model/base/fields.rb +49 -0
  21. data/lib/memory_model/base/operations/comparisons.rb +25 -0
  22. data/lib/memory_model/base/operations.rb +10 -0
  23. data/lib/memory_model/base/persistence.rb +13 -11
  24. data/lib/memory_model/base.rb +58 -43
  25. data/lib/memory_model/collection/finders.rb +75 -0
  26. data/lib/memory_model/collection/index/multi.rb +61 -0
  27. data/lib/memory_model/collection/index/unique.rb +79 -0
  28. data/lib/memory_model/collection/index.rb +86 -0
  29. data/lib/memory_model/collection/initializers.rb +48 -0
  30. data/lib/memory_model/collection/loader_delegate.rb +63 -0
  31. data/lib/memory_model/collection/marshaled_record.rb +23 -0
  32. data/lib/memory_model/collection/operations.rb +82 -0
  33. data/lib/memory_model/collection.rb +18 -73
  34. data/lib/memory_model/version.rb +1 -1
  35. data/lib/memory_model.rb +7 -7
  36. data/memory_model.gemspec +8 -3
  37. data/spec/benchmark/benchmark.rb +126 -0
  38. data/spec/memory_model/base/{actionable_spec.rb → actions_spec.rb} +34 -103
  39. data/spec/memory_model/base/{attributable_spec.rb → attributes_spec.rb} +4 -6
  40. data/spec/memory_model/base/{collectable_spec.rb → collectible_spec.rb} +1 -1
  41. data/spec/memory_model/base/fieldable/field_set_spec.rb +23 -37
  42. data/spec/memory_model/base/fieldable/field_spec.rb +16 -16
  43. data/spec/memory_model/base/{fieldable_spec.rb → fields_spec.rb} +1 -1
  44. data/spec/memory_model/base/{comparable_spec.rb → operations/comparisons_spec.rb} +4 -4
  45. data/spec/memory_model/base/persistence_spec.rb +2 -2
  46. data/spec/memory_model/base_spec.rb +10 -9
  47. data/spec/memory_model/collection_spec.rb +24 -146
  48. data/spec/spec_helper.rb +11 -0
  49. data/spec/support/delegate_matcher.rb +40 -0
  50. metadata +120 -65
  51. data/.idea/.rakeTasks +0 -7
  52. data/.idea/dictionaries/jwaldrip.xml +0 -3
  53. data/.idea/encodings.xml +0 -5
  54. data/.idea/memory_model.iml +0 -47
  55. data/.idea/misc.xml +0 -8
  56. data/.idea/modules.xml +0 -9
  57. data/.idea/scopes/scope_settings.xml +0 -5
  58. data/.idea/vcs.xml +0 -7
  59. data/.idea/workspace.xml +0 -701
  60. data/lib/memory_model/base/actionable.rb +0 -95
  61. data/lib/memory_model/base/attributable.rb +0 -76
  62. data/lib/memory_model/base/collectable.rb +0 -22
  63. data/lib/memory_model/base/comparable.rb +0 -16
  64. data/lib/memory_model/base/fieldable/field.rb +0 -35
  65. data/lib/memory_model/base/fieldable/field_set.rb +0 -74
  66. data/lib/memory_model/base/fieldable.rb +0 -45
  67. data/lib/memory_model/base/versionable.rb +0 -17
  68. data/lib/memory_model/core_ext/object.rb +0 -5
  69. data/spec/memory_model/base/versionable_spec.rb +0 -31
  70. data/spec/memory_model/core_ext/object_spec.rb +0 -12
@@ -0,0 +1,87 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/hash_with_indifferent_access'
3
+ require 'active_support/core_ext/string'
4
+ require 'set'
5
+
6
+ module MemoryModel
7
+ class Base
8
+ module Fields
9
+ class FieldSet < Set
10
+
11
+ def [](name)
12
+ find { |f| f.name == name.to_sym }
13
+ end
14
+
15
+ def add(name, options={})
16
+ delete_if { |f| f == name }
17
+ self << Field.new(name, options)
18
+ end
19
+
20
+ def include?(name)
21
+ self[name].present?
22
+ end
23
+
24
+ def comparable
25
+ select(&:comparable?).map(&:to_sym)
26
+ end
27
+
28
+ def set_default_values(model, attributes={})
29
+ model.attributes = self.map(&:name).reduce(attributes.with_indifferent_access) do |hash, field|
30
+ hash[field] ||= fetch_default_value(model, field)
31
+ hash
32
+ end
33
+ end
34
+
35
+ def set_default_value(model, field)
36
+ model.write_attribute field, fetch_default_value(model, field)
37
+ end
38
+
39
+ def fetch_default_value(model, field)
40
+ default = self[field].default
41
+ send("fetch_value_using_#{default.class.name.underscore}", model, default)
42
+ rescue NoMethodError => e
43
+ raise ArgumentError, "#{default} must be a string, symbol, lambda or proc"
44
+ end
45
+
46
+ private
47
+
48
+ def fetch_value_using_proc(model, proc)
49
+ raise TypeError, 'value must be a Proc' unless proc.is_a? Proc
50
+ if proc.lambda? && proc.arity == 0
51
+ proc.call
52
+ elsif proc.arity < 1
53
+ model.instance_eval(&proc)
54
+ elsif proc.arity == 1
55
+ proc.yield model
56
+ else
57
+ raise ArgumentError, "#{proc} must have an arity of 0..1, got #{proc.arity}"
58
+ end
59
+ end
60
+
61
+ def fetch_value_using_string(model, string)
62
+ raise TypeError, 'value must be a String' unless string.is_a? String
63
+ string
64
+ end
65
+
66
+ def fetch_value_using_symbol(model, symbol)
67
+ raise TypeError, 'value must be a Symbol' unless symbol.is_a? Symbol
68
+ model.instance_eval(&symbol)
69
+ end
70
+
71
+ def fetch_value_using_nil_class(model, nil_object)
72
+ raise TypeError, 'value must be a NilClass' unless nil_object.is_a? NilClass
73
+ nil
74
+ end
75
+
76
+ def method_missing(m, *args, &block)
77
+ if to_a.respond_to? m
78
+ to_a.send m, *args, &block
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/dependencies/autoload'
3
+ require 'securerandom'
4
+
5
+ module MemoryModel
6
+ class Base
7
+ module Fields
8
+
9
+ extend ConcernedInheritance
10
+ extend ActiveSupport::Concern
11
+ extend ActiveSupport::Autoload
12
+ include ActiveModel::AttributeMethods
13
+
14
+ autoload :FieldSet
15
+ autoload :Field
16
+
17
+ inherited do
18
+ instance_variable_set :@fields, baseclass.fields
19
+ end
20
+
21
+ module ClassMethods
22
+ def field(attr, options={})
23
+ define_attribute_method attr unless instance_method_already_implemented? attr
24
+ fields.add(attr.to_sym, options)
25
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
26
+ def #{attr}
27
+ read_attribute :#{attr}
28
+ end
29
+
30
+ def #{attr}=(value)
31
+ write_attribute :#{attr}, value
32
+ end
33
+ RUBY
34
+ end
35
+
36
+ def fields
37
+ return nil if self == MemoryModel::Base
38
+ @fields ||= FieldSet.new
39
+ end
40
+
41
+ end
42
+
43
+ def fields
44
+ self.class.fields
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/hash_with_indifferent_access'
3
+
4
+ module MemoryModel
5
+ class Base
6
+ module Operations
7
+ module Comparisons
8
+
9
+ def ==(other_object)
10
+ attributes.slice(*fields.comparable) ==
11
+ other_object.to_hash.with_indifferent_access.slice(*fields.comparable)
12
+ end
13
+
14
+ def !=(other_object)
15
+ !(self == other_object)
16
+ end
17
+
18
+ def ===(other_object)
19
+ other_object.kind_of?(self.class) && self == other_object
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module MemoryModel
2
+ class Base
3
+ module Operations
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Comparisons
7
+
8
+ end
9
+ end
10
+ end
@@ -1,15 +1,17 @@
1
- module MemoryModel::Base::Persistence
1
+ module MemoryModel
2
+ class Base
3
+ module Persistence
2
4
 
3
- def persisted?
4
- !!self.class.find(self.id)
5
- rescue MemoryModel::RecordNotFoundError
6
- false
7
- end
5
+ def persisted?
6
+ !!self.class.find_by(_uuid_: self._uuid_)
7
+ end
8
8
 
9
- alias :exists? :persisted?
9
+ alias :exists? :persisted?
10
10
 
11
- def new_record?
12
- !persisted?
13
- end
11
+ def new_record?
12
+ !persisted?
13
+ end
14
14
 
15
- end
15
+ end
16
+ end
17
+ end
@@ -1,51 +1,66 @@
1
- require "concerned_inheritance"
1
+ require 'concerned_inheritance'
2
2
  require 'active_support/core_ext/object'
3
3
  require 'active_support/core_ext/hash'
4
4
  require 'active_support/dependencies/autoload'
5
- require 'active_support/core_ext/hash/indifferent_access'
6
5
  require 'active_model'
7
6
 
8
- class MemoryModel::Base
9
- extend ActiveSupport::Autoload
10
- extend ConcernedInheritance
11
-
12
- autoload :Fieldable
13
- autoload :Collectable
14
- autoload :Comparable
15
- autoload :Actionable
16
- autoload :Attributable
17
- autoload :Versionable
18
- autoload :Persistence
19
-
20
- # Active Model Additions
21
- extend ActiveModel::Callbacks
22
- extend ActiveModel::Naming
23
- extend ActiveModel::Translation
24
- include ActiveModel::Conversion
25
- include ActiveModel::MassAssignmentSecurity
26
- include ActiveModel::Observing
27
- include ActiveModel::Serialization
28
- include ActiveModel::Validations
29
-
30
- # Memory Model Additions
31
- include Fieldable
32
- include Collectable
33
- include Comparable
34
- include Actionable
35
- include Attributable
36
- include Versionable
37
- include Persistence
38
-
39
- # Active Model Callbacks
40
- define_model_callbacks :initialize, only: [:after]
41
-
42
- def initialize(attributes={ })
43
- unless self.class.collection.is_a? MemoryModel::Collection
44
- raise MemoryModel::InvalidCollectionError, "#{self.class} does not have an assigned collection"
7
+ module MemoryModel
8
+
9
+ class InvalidCollectionError < Error ; end
10
+
11
+ class Base
12
+ extend ActiveSupport::Autoload
13
+ extend ConcernedInheritance
14
+
15
+ autoload :Fields
16
+ autoload :Collectible
17
+ autoload :Comparison
18
+ autoload :Actions
19
+ autoload :Attributes
20
+ autoload :Persistence
21
+ autoload :Operations
22
+ autoload :Conversion
23
+ autoload :AutoIncrement
24
+
25
+ # Active Model Additions
26
+ extend ActiveModel::Callbacks
27
+ extend ActiveModel::Naming
28
+ extend ActiveModel::Translation
29
+ include ActiveModel::Conversion
30
+ include ActiveModel::Serialization
31
+ include ActiveModel::Validations
32
+
33
+ # 3.2 Only Active Model Additions
34
+ if ActiveModel::VERSION::MAJOR < 4 || (ActiveModel::VERSION::MAJOR == 3 && ActiveModel::VERSION::MINOR > 2)
35
+ include ActiveModel::MassAssignmentSecurity
36
+ include ActiveModel::Observing
45
37
  end
46
- @attributes = fields.default_values(self, attributes).with_indifferent_access
47
- @deleted = false
48
- run_callbacks :initialize
49
- end
50
38
 
39
+ # Memory Model Additions
40
+ include Fields
41
+ include Collectible
42
+ include Operations::Comparisons
43
+ include Actions
44
+ include Attributes
45
+ include Persistence
46
+ include Conversion
47
+ include AutoIncrement
48
+
49
+ # Active Model Callbacks
50
+ define_model_callbacks :initialize, only: [:after]
51
+
52
+ def initialize(attributes={})
53
+ unless self.class.collection.is_a? MemoryModel::Collection
54
+ raise MemoryModel::InvalidCollectionError, "#{self.class} does not have an assigned collection"
55
+ end
56
+ fields.set_default_values(self, attributes)
57
+ run_callbacks :initialize
58
+ end
59
+
60
+ def initialize_dup(other)
61
+ @attributes = other.attributes.dup
62
+ reset_incremented_fields!
63
+ end
64
+
65
+ end
51
66
  end
@@ -0,0 +1,75 @@
1
+ module MemoryModel
2
+
3
+ class RecordNotFoundError < Error;
4
+ end
5
+
6
+ class Collection
7
+ module Finders
8
+
9
+ def all
10
+ LoaderDelegate.new records
11
+ end
12
+
13
+ def count
14
+ _uuids_.count
15
+ end
16
+
17
+ def find(key)
18
+ read(key).load
19
+ rescue NoMethodError
20
+ raise RecordNotFoundError
21
+ end
22
+
23
+ def find_all(*ids)
24
+ read_all(*ids).map(&:load)
25
+ end
26
+
27
+ def find_by(hash)
28
+ where(hash).first
29
+ end
30
+
31
+ def find_or_initialize_by(hash)
32
+ find_by(hash) || model.new(hash)
33
+ end
34
+
35
+ def find_or_create_by(hash)
36
+ find_by(hash) || model.create(hash)
37
+ end
38
+
39
+ def find_or_create_by!(hash)
40
+ find_by(hash) || model.create!(hash)
41
+ end
42
+
43
+ def where(hash)
44
+ matched_ids = hash.symbolize_keys.reduce(_uuids_) do |array, (attr, value)|
45
+ records = if indexes.has_key?(attr)
46
+ where_in_index(attr, value).compact.map(&:uuid)
47
+ else
48
+ where_in_all(attr, value).map(&:_uuid_)
49
+ end
50
+ array & records
51
+ end
52
+ load_all(*matched_ids)
53
+ end
54
+
55
+ private
56
+
57
+ def _uuids_
58
+ indexes[:_uuid_].keys
59
+ end
60
+
61
+ def records
62
+ indexes[:_uuid_].values
63
+ end
64
+
65
+ def where_in_all(attr, value)
66
+ all.select { |record| record.read_attribute(attr) == value }
67
+ end
68
+
69
+ def where_in_index(attr, value)
70
+ indexes[attr].where value
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ module MemoryModel
2
+ class Collection
3
+ class Index
4
+ class Multi < MemoryModel::Collection::Index
5
+
6
+ # `create` should implement creating a new record, raising an error if an item with the matching storage id already
7
+ # exists in the index.
8
+ def create(key, item)
9
+ insert_into_key(key, item)
10
+ end
11
+
12
+ # `update` should find a record in the collection by its storage_id, remove it, and add with the new value.
13
+ def update(key, item)
14
+ raise(RecordNotInIndexError, [self, item]) unless exists? item
15
+ delete(item)
16
+ create(key, item)
17
+ end
18
+
19
+ # `read` should find a record in the collection by its indexed_value, remove it, and add with the new value.
20
+ def read(key)
21
+ index[key].first
22
+ end
23
+
24
+ # `delete` should find a record in the collection by its indexed_value, and remove it.
25
+ def delete(key)
26
+ index.values.map { |refs| refs.delete key }
27
+ end
28
+
29
+ # `exists?` return whether or not an item with the given storage id exists.
30
+ def exists?(item)
31
+ index.values.any? { |refs| refs.has_key? item.uuid }
32
+ end
33
+
34
+ # `values` should return the values of the index
35
+ def values
36
+ index.values.map(&:values)
37
+ end
38
+
39
+ private
40
+
41
+ def where_using_default(matcher)
42
+ Array.wrap index[matcher].try(:values)
43
+ end
44
+
45
+ def where_using_proc(matcher)
46
+ index.slice(*index.keys.select(&matcher)).values.map(&:values).flatten
47
+ end
48
+
49
+ def where_using_regexp(matcher)
50
+ where_using_proc ->(key) { key =~ matcher }
51
+ end
52
+
53
+ def insert_into_key(key, item)
54
+ index[key] ||= {}
55
+ index[key][item.uuid] = item
56
+ end
57
+
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,79 @@
1
+ module MemoryModel
2
+
3
+ class NilValueError < IndexError
4
+
5
+ def initialize(index)
6
+ super "`#{index.name}` cannot be nil"
7
+ end
8
+
9
+ end
10
+
11
+ class RecordNotUniqueError < IndexError
12
+
13
+ def initialize(args)
14
+ index, key = args
15
+ super "`#{index.name}` with `#{key}` already exists"
16
+ end
17
+
18
+ end
19
+
20
+ class Collection
21
+ class Index
22
+ class MemoryModel::Collection::Index::Unique < MemoryModel::Collection::Index
23
+
24
+ delegate :values_at, to: :index
25
+
26
+ # `create` should implement creating a new record, raising an error if an item with the matching storage id already
27
+ # exists in the index.
28
+ def create(key, item)
29
+ raise(NilValueError, self) if key.nil? && !options[:allow_nil]
30
+ raise(RecordNotUniqueError, [self, key]) if index.has_key?(key)
31
+ return if key.nil?
32
+ index[key] = item
33
+ end
34
+
35
+ # `update` should find a record in the collection by its storage_id, remove it, and add with the new value.
36
+ def update(key, item)
37
+ raise(RecordNotInIndexError, [self, item]) unless exists? item
38
+ delete(item.uuid)
39
+ create(key, item)
40
+ end
41
+
42
+ # `read` should find a record in the collection by its indexed_value, remove it, and add with the new value.
43
+ def read(key)
44
+ index[key]
45
+ end
46
+
47
+ # `delete` should find a record in the collection by its indexed_value, and remove it.
48
+ def delete(key)
49
+ index.delete_if { |k, value| key == value.uuid }
50
+ end
51
+
52
+ # `exists?` return whether or not an item with the given storage id exists.
53
+ def exists?(item)
54
+ index.any? { |key, value| item.uuid == value.uuid }
55
+ end
56
+
57
+ # `values` should return the values of the index
58
+ def values
59
+ index.values
60
+ end
61
+
62
+ private
63
+
64
+ def where_using_default(matcher)
65
+ [read(matcher)]
66
+ end
67
+
68
+ def where_using_proc(matcher)
69
+ index.values_at *index.keys.select(&matcher)
70
+ end
71
+
72
+ def where_using_regexp(matcher)
73
+ where_using_proc ->(key) { key =~ matcher }
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,86 @@
1
+ module MemoryModel
2
+
3
+ class IndexError < Error
4
+ end
5
+
6
+ class InvalidWhereQuery < IndexError
7
+
8
+ def initialize(matcher_class)
9
+ super "Unable to perform a where with #{matcher_class}"
10
+ end
11
+
12
+ end
13
+
14
+ class RecordNotInIndexError < IndexError
15
+
16
+ def initialize(args)
17
+ item, index = args
18
+ super "record `#{item.uuid}` is missing from index `#{index.name}`"
19
+ end
20
+
21
+ end
22
+
23
+ class Collection
24
+ class Index
25
+ extend ActiveSupport::Autoload
26
+
27
+ autoload :Unique
28
+ autoload :Multi
29
+
30
+ attr_reader :name, :options, :index
31
+
32
+ delegate :clear, :keys, to: :index
33
+ delegate :count, to: :values
34
+
35
+ def initialize(name, options)
36
+ @name = name
37
+ @options = options
38
+ @index = {}
39
+ end
40
+
41
+ # This is the base index, each method below must be implemented on each subclass
42
+
43
+ # `create` should implement creating a new record, raising an error if an item with the matching storage id already
44
+ # exists in the index.
45
+ def create(key, item)
46
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
47
+ end
48
+
49
+ # `update` should find a record in the collection by its storage_id, remove it, and add with the new value.
50
+ def update(key, item)
51
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
52
+ end
53
+
54
+ # `read` should find a record in the collection by its indexed_value, remove it, and add with the new value.
55
+ def read(key)
56
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
57
+ end
58
+
59
+ # `delete` should find a record in the collection by its indexed_value, and remove it.
60
+ def delete(key)
61
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
62
+ end
63
+
64
+ # `exists?` return whether or not an item with the given storage id exists.
65
+ def exists?(item)
66
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
67
+ end
68
+
69
+ # `where` should allow me to specify complex arguments and return an array
70
+ def where(matcher)
71
+ matcher_class = matcher.class.name.underscore
72
+ send("where_using_#{matcher_class}", matcher)
73
+ rescue NoMethodError
74
+ respond_to?(:where_using_default, true) ? where_using_default(matcher) :
75
+ raise(InvalidWhereQuery, matcher_class)
76
+ end
77
+
78
+ # `values` should return the values of the index
79
+ def values
80
+ raise NotImplementedError, "#{__method__} has not been implemented for this #{name} index"
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,48 @@
1
+ module MemoryModel
2
+ class Collection
3
+ module Initializers
4
+ extend ActiveSupport::Concern
5
+
6
+ def initialize(model)
7
+ @model = model
8
+ set_primary_key :_uuid_, default: nil
9
+ end
10
+
11
+ def add_index(name, options={})
12
+ type = :unique if options.delete(:unique)
13
+ type ||= options.delete(:type) || :multi
14
+ indexes[name] = Index.const_get(type.to_s.camelize).new(name, options)
15
+ rescue NameError => e
16
+ raise TypeError, "#{type.inspect} is not a valid index"
17
+ end
18
+
19
+ def indexes
20
+ @indexes ||= {}
21
+ end
22
+
23
+ def index_names
24
+ indexes.keys
25
+ end
26
+
27
+ def set_primary_key(key, options={})
28
+ if options[:auto_increment] != false && !options.has_key?(:default)
29
+ options[:auto_increment] = true
30
+ end
31
+ options[:comparable] ||= false
32
+ @model.field key, options
33
+ add_index key, type: :unique
34
+ @primary_key = key
35
+ end
36
+
37
+ module ClassMethods
38
+
39
+ def all
40
+ MemoryModel::Collection.instance_variable_get(:@all) ||
41
+ MemoryModel::Collection.instance_variable_set(:@all, [])
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end