active_hash 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,14 @@
1
1
  module ActiveHash
2
-
3
2
  class RecordNotFound < StandardError
3
+ attr_reader :model, :primary_key, :id
4
+
5
+ def initialize(message = nil, model = nil, primary_key = nil, id = nil)
6
+ @primary_key = primary_key
7
+ @model = model
8
+ @id = id
9
+
10
+ super(message)
11
+ end
4
12
  end
5
13
 
6
14
  class ReservedFieldError < StandardError
@@ -13,50 +21,7 @@ module ActiveHash
13
21
  end
14
22
 
15
23
  class Base
16
-
17
- class_attribute :_data, :dirty, :default_attributes
18
-
19
- class WhereChain
20
- def initialize(scope)
21
- @scope = scope
22
- @records = @scope.all
23
- end
24
-
25
- def not(options)
26
- return @scope if options.blank?
27
-
28
- # use index if searching by id
29
- if options.key?(:id) || options.key?("id")
30
- ids = @scope.pluck(:id) - Array.wrap(options.delete(:id) || options.delete("id"))
31
- candidates = ids.map { |id| @scope.find_by_id(id) }.compact
32
- end
33
- return candidates if options.blank?
34
-
35
- filtered_records = (candidates || @records || []).reject do |record|
36
- match_options?(record, options)
37
- end
38
-
39
- ActiveHash::Relation.new(@scope.klass, filtered_records, {})
40
- end
41
-
42
- def match_options?(record, options)
43
- options.all? do |col, match|
44
- if match.kind_of?(Array)
45
- match.any? { |v| normalize(v) == normalize(record[col]) }
46
- else
47
- normalize(record[col]) == normalize(match)
48
- end
49
- end
50
- end
51
-
52
- private :match_options?
53
-
54
- def normalize(v)
55
- v.respond_to?(:to_sym) ? v.to_sym : v
56
- end
57
-
58
- private :normalize
59
- end
24
+ class_attribute :_data, :dirty, :default_attributes, :scopes
60
25
 
61
26
  if Object.const_defined?(:ActiveModel)
62
27
  extend ActiveModel::Naming
@@ -120,9 +85,17 @@ module ActiveHash
120
85
  end
121
86
  end
122
87
 
123
- def exists?(record)
124
- if record.id.present?
125
- record_index[record.id.to_s].present?
88
+ def exists?(args = nil)
89
+ if args.respond_to?(:id)
90
+ record_index[args.id.to_s].present?
91
+ elsif args == false
92
+ false
93
+ elsif args.nil?
94
+ all.present?
95
+ elsif args.is_a?(Hash)
96
+ all.where(args).present?
97
+ else
98
+ all.where(id: args.to_i).present?
126
99
  end
127
100
  end
128
101
 
@@ -137,7 +110,7 @@ module ActiveHash
137
110
  end
138
111
 
139
112
  def next_id
140
- max_record = all.max { |a, b| a.id <=> b.id }
113
+ max_record = all_in_process.max { |a, b| a.id <=> b.id }
141
114
  if max_record.nil?
142
115
  1
143
116
  elsif max_record.id.is_a?(Numeric)
@@ -145,6 +118,11 @@ module ActiveHash
145
118
  end
146
119
  end
147
120
 
121
+ def all_in_process
122
+ all
123
+ end
124
+ private :all_in_process
125
+
148
126
  def record_index
149
127
  @record_index ||= {}
150
128
  end
@@ -185,10 +163,12 @@ module ActiveHash
185
163
  end
186
164
 
187
165
  def all(options = {})
188
- ActiveHash::Relation.new(self, @records || [], options[:conditions] || {})
166
+ relation = ActiveHash::Relation.new(self, @records || [])
167
+ relation = relation.where!(options[:conditions]) if options[:conditions]
168
+ relation
189
169
  end
190
170
 
191
- delegate :where, :find, :find_by, :find_by!, :find_by_id, :count, :pluck, :first, :last, :order, to: :all
171
+ delegate :where, :find, :find_by, :find_by!, :find_by_id, :count, :pluck, :ids, :pick, :first, :last, :order, to: :all
192
172
 
193
173
  def transaction
194
174
  yield
@@ -217,7 +197,7 @@ module ActiveHash
217
197
  validate_field(field_name)
218
198
  field_names << field_name
219
199
 
220
- add_default_value(field_name, options[:default]) if options[:default]
200
+ add_default_value(field_name, options[:default]) if options.key?(:default)
221
201
  define_getter_method(field_name, options[:default])
222
202
  define_setter_method(field_name)
223
203
  define_interrogator_method(field_name)
@@ -388,10 +368,13 @@ module ActiveHash
388
368
  end
389
369
 
390
370
  private :mark_clean
391
-
371
+
392
372
  def scope(name, body)
393
373
  raise ArgumentError, 'body needs to be callable' unless body.respond_to?(:call)
394
-
374
+
375
+ self.scopes ||= {}
376
+ self.scopes[name] = body
377
+
395
378
  the_meta_class.instance_eval do
396
379
  define_method(name) do |*args|
397
380
  instance_exec(*args, &body)
@@ -472,7 +455,11 @@ module ActiveHash
472
455
  when new_record?
473
456
  "#{self.class.cache_key}/new"
474
457
  when timestamp = self[:updated_at]
475
- "#{self.class.cache_key}/#{id}-#{timestamp.to_s(:number)}"
458
+ if ActiveSupport::VERSION::MAJOR < 7
459
+ "#{self.class.cache_key}/#{id}-#{timestamp.to_s(:number)}"
460
+ else
461
+ "#{self.class.cache_key}/#{id}-#{timestamp.to_fs(:number)}"
462
+ end
476
463
  else
477
464
  "#{self.class.cache_key}/#{id}"
478
465
  end
@@ -0,0 +1,44 @@
1
+ class ActiveHash::Relation::Condition
2
+ attr_reader :constraints, :inverted
3
+
4
+ def initialize(constraints)
5
+ @constraints = constraints
6
+ @inverted = false
7
+ end
8
+
9
+ def invert!
10
+ @inverted = !inverted
11
+
12
+ self
13
+ end
14
+
15
+ def matches?(record)
16
+ match = begin
17
+ return true unless constraints
18
+
19
+ expectation_method = inverted ? :any? : :all?
20
+
21
+ constraints.send(expectation_method) do |attribute, expected|
22
+ value = record.public_send(attribute)
23
+
24
+ matches_value?(value, expected)
25
+ end
26
+ end
27
+
28
+ inverted ? !match : match
29
+ end
30
+
31
+ private
32
+
33
+ def matches_value?(value, comparison)
34
+ return comparison.any? { |v| matches_value?(value, v) } if comparison.is_a?(Array)
35
+ return comparison.cover?(value) if comparison.is_a?(Range)
36
+ return comparison.match?(value) if comparison.is_a?(Regexp)
37
+
38
+ normalize(value) == normalize(comparison)
39
+ end
40
+
41
+ def normalize(value)
42
+ value.respond_to?(:to_s) ? value.to_s : value
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ class ActiveHash::Relation::Conditions
2
+ attr_reader :conditions
3
+
4
+ delegate :<<, :map, to: :conditions
5
+
6
+ def initialize(conditions = [])
7
+ @conditions = conditions
8
+ end
9
+
10
+ def matches?(record)
11
+ conditions.all? do |condition|
12
+ condition.matches?(record)
13
+ end
14
+ end
15
+
16
+ def self.wrap(conditions)
17
+ return conditions if conditions.is_a?(self)
18
+
19
+ new(conditions)
20
+ end
21
+ end
@@ -1,44 +1,109 @@
1
1
  module ActiveHash
2
2
  class Relation
3
3
  include Enumerable
4
-
4
+
5
5
  delegate :each, to: :records # Make Enumerable work
6
6
  delegate :equal?, :==, :===, :eql?, :sort!, to: :records
7
7
  delegate :empty?, :length, :first, :second, :third, :last, to: :records
8
8
  delegate :sample, to: :records
9
-
10
- def initialize(klass, all_records, query_hash = nil)
9
+
10
+ attr_reader :conditions, :order_values, :klass, :all_records
11
+
12
+ def initialize(klass, all_records, conditions = nil, order_values = nil)
11
13
  self.klass = klass
12
14
  self.all_records = all_records
13
- self.query_hash = query_hash
14
- self.records_dirty = false
15
+ self.conditions = Conditions.wrap(conditions || [])
16
+ self.order_values = order_values || []
17
+ end
18
+
19
+ def where(conditions_hash = :chain)
20
+ return WhereChain.new(self) if conditions_hash == :chain
21
+
22
+ spawn.where!(conditions_hash)
23
+ end
24
+
25
+ class WhereChain
26
+ attr_reader :relation
27
+
28
+ def initialize(relation)
29
+ @relation = relation
30
+ end
31
+
32
+ def not(conditions_hash)
33
+ relation.conditions << Condition.new(conditions_hash).invert!
34
+ relation
35
+ end
36
+ end
37
+
38
+ def order(*options)
39
+ spawn.order!(*options)
40
+ end
41
+
42
+ def reorder(*options)
43
+ spawn.reorder!(*options)
44
+ end
45
+
46
+ def where!(conditions_hash, inverted = false)
47
+ self.conditions << Condition.new(conditions_hash)
48
+ self
49
+ end
50
+
51
+ def invert_where
52
+ spawn.invert_where!
53
+ end
54
+
55
+ def invert_where!
56
+ conditions.map(&:invert!)
57
+ self
58
+ end
59
+
60
+ def spawn
61
+ self.class.new(klass, all_records, conditions, order_values)
62
+ end
63
+
64
+ def order!(*options)
65
+ check_if_method_has_arguments!(:order, options)
66
+ self.order_values += preprocess_order_args(options)
15
67
  self
16
68
  end
17
-
18
- def where(query_hash = :chain)
19
- return ActiveHash::Base::WhereChain.new(self) if query_hash == :chain
20
-
21
- self.records_dirty = true unless query_hash.nil? || query_hash.keys.empty?
22
- self.query_hash.merge!(query_hash || {})
69
+
70
+ def reorder!(*options)
71
+ check_if_method_has_arguments!(:order, options)
72
+
73
+ self.order_values = preprocess_order_args(options)
74
+ @records = apply_order_values(records, order_values)
75
+
76
+ self
77
+ end
78
+
79
+ def records
80
+ @records ||= begin
81
+ filtered_records = apply_conditions(all_records, conditions)
82
+ ordered_records = apply_order_values(filtered_records, order_values) # rubocop:disable Lint/UselessAssignment
83
+ end
84
+ end
85
+
86
+ def reload
87
+ @records = nil # Reset records
23
88
  self
24
89
  end
25
-
90
+
26
91
  def all(options = {})
27
- if options.has_key?(:conditions)
92
+ if options.key?(:conditions)
28
93
  where(options[:conditions])
29
94
  else
30
95
  where({})
31
96
  end
32
97
  end
33
-
98
+
34
99
  def find_by(options)
35
100
  where(options).first
36
101
  end
37
102
 
38
103
  def find_by!(options)
39
- find_by(options) || (raise RecordNotFound.new("Couldn't find #{klass.name}"))
104
+ find_by(options) || (raise RecordNotFound.new("Couldn't find #{klass.name}", klass.name))
40
105
  end
41
-
106
+
42
107
  def find(id = nil, *args, &block)
43
108
  case id
44
109
  when :all
@@ -48,101 +113,76 @@ module ActiveHash
48
113
  when Array
49
114
  id.map { |i| find(i) }
50
115
  when nil
51
- raise RecordNotFound.new("Couldn't find #{klass.name} without an ID") unless block_given?
116
+ raise RecordNotFound.new("Couldn't find #{klass.name} without an ID", klass.name, "id") unless block_given?
52
117
  records.find(&block) # delegate to Enumerable#find if a block is given
53
118
  else
54
119
  find_by_id(id) || begin
55
- raise RecordNotFound.new("Couldn't find #{klass.name} with ID=#{id}")
120
+ raise RecordNotFound.new("Couldn't find #{klass.name} with ID=#{id}", klass.name, "id", id)
56
121
  end
57
122
  end
58
123
  end
59
-
124
+
60
125
  def find_by_id(id)
61
126
  index = klass.send(:record_index)[id.to_s] # TODO: Make index in Base publicly readable instead of using send?
62
- index and records[index]
127
+ return unless index
128
+
129
+ record = all_records[index]
130
+ record if conditions.matches?(record)
63
131
  end
64
-
132
+
65
133
  def count
66
134
  length
67
135
  end
68
-
69
- def pluck(*column_names)
70
- column_names.map { |column_name| all.map(&column_name.to_sym) }.inject(&:zip)
71
- end
72
-
73
- def reload
74
- @records = filter_all_records_by_query_hash
136
+
137
+ def size
138
+ length
75
139
  end
76
140
 
77
- def order(*options)
78
- check_if_method_has_arguments!(:order, options)
79
- relation = where({})
80
- return relation if options.blank?
141
+ def pluck(*column_names)
142
+ symbolized_column_names = column_names.map(&:to_sym)
81
143
 
82
- processed_args = preprocess_order_args(options)
83
- candidates = relation.dup
144
+ if symbolized_column_names.length == 1
145
+ column_name = symbolized_column_names.first
146
+ all.map { |record| record[column_name] }
147
+ else
148
+ all.map do |record|
149
+ symbolized_column_names.map { |column_name| record[column_name] }
150
+ end
151
+ end
152
+ end
84
153
 
85
- order_by_args!(candidates, processed_args)
154
+ def ids
155
+ pluck(:id)
156
+ end
86
157
 
87
- candidates
158
+ def pick(*column_names)
159
+ pluck(*column_names).first
88
160
  end
89
-
161
+
90
162
  def to_ary
91
163
  records.dup
92
164
  end
93
-
94
165
 
95
- attr_reader :query_hash, :klass, :all_records, :records_dirty
96
-
97
- private
98
-
99
- attr_writer :query_hash, :klass, :all_records, :records_dirty
100
-
101
- def records
102
- if @records.nil? || records_dirty
103
- reload
104
- else
105
- @records
106
- end
107
- end
108
-
109
- def filter_all_records_by_query_hash
110
- self.records_dirty = false
111
- return all_records if query_hash.blank?
112
-
113
- # use index if searching by id
114
- if query_hash.key?(:id) || query_hash.key?("id")
115
- ids = (query_hash.delete(:id) || query_hash.delete("id"))
116
- ids = range_to_array(ids) if ids.is_a?(Range)
117
- candidates = Array.wrap(ids).map { |id| klass.find_by_id(id) }.compact
118
- end
119
-
120
- return candidates if query_hash.blank?
166
+ def method_missing(method_name, *args)
167
+ return super unless klass.scopes.key?(method_name)
121
168
 
122
- (candidates || all_records || []).select do |record|
123
- match_options?(record, query_hash)
124
- end
125
- end
126
-
127
- def match_options?(record, options)
128
- options.all? do |col, match|
129
- if match.kind_of?(Array)
130
- match.any? { |v| normalize(v) == normalize(record[col]) }
131
- else
132
- normalize(record[col]) == normalize(match)
133
- end
134
- end
169
+ instance_exec(*args, &klass.scopes[method_name])
135
170
  end
136
171
 
137
- def normalize(v)
138
- v.respond_to?(:to_sym) ? v.to_sym : v
172
+ def respond_to_missing?(method_name, include_private = false)
173
+ klass.scopes.key?(method_name) || super
139
174
  end
140
-
141
- def range_to_array(range)
142
- return range.to_a unless range.end.nil?
143
175
 
144
- e = records.last[:id]
145
- (range.begin..e).to_a
176
+ private
177
+
178
+ attr_writer :conditions, :order_values, :klass, :all_records
179
+
180
+ def apply_conditions(records, conditions)
181
+ return records if conditions.blank?
182
+
183
+ records.select do |record|
184
+ conditions.matches?(record)
185
+ end
146
186
  end
147
187
 
148
188
  def check_if_method_has_arguments!(method_name, args)
@@ -160,7 +200,9 @@ module ActiveHash
160
200
  ary.map! { |e| e.split(/\W+/) }.reverse!
161
201
  end
162
202
 
163
- def order_by_args!(candidates, args)
203
+ def apply_order_values(records, args)
204
+ ordered_records = records.dup
205
+
164
206
  args.each do |arg|
165
207
  field, dir = if arg.is_a?(Hash)
166
208
  arg.to_a.flatten.map(&:to_sym)
@@ -170,7 +212,7 @@ module ActiveHash
170
212
  arg.to_sym
171
213
  end
172
214
 
173
- candidates.sort! do |a, b|
215
+ ordered_records.sort! do |a, b|
174
216
  if dir.present? && dir.to_sym.upcase.equal?(:DESC)
175
217
  b[field] <=> a[field]
176
218
  else
@@ -178,6 +220,8 @@ module ActiveHash
178
220
  end
179
221
  end
180
222
  end
223
+
224
+ ordered_records
181
225
  end
182
226
  end
183
227
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveHash
2
2
  module Gem
3
- VERSION = "3.1.0"
3
+ VERSION = "3.2.0"
4
4
  end
5
5
  end
data/lib/active_hash.rb CHANGED
@@ -13,6 +13,8 @@ end
13
13
 
14
14
  require 'active_hash/base'
15
15
  require 'active_hash/relation'
16
+ require 'active_hash/condition'
17
+ require 'active_hash/conditions'
16
18
  require 'active_file/multiple_files'
17
19
  require 'active_file/hash_and_array_files'
18
20
  require 'active_file/base'
@@ -5,6 +5,8 @@ module ActiveYaml
5
5
  base.extend(ClassMethods)
6
6
  end
7
7
 
8
+ ALIAS_KEY_REGEXP = /^\//.freeze
9
+
8
10
  module ClassMethods
9
11
 
10
12
  def insert(record)
@@ -12,16 +14,19 @@ module ActiveYaml
12
14
  end
13
15
 
14
16
  def raw_data
15
- super.reject do |k, v|
16
- v.kind_of? Hash and k.match(/^\//i)
17
+ d = super
18
+ if d.kind_of?(Array)
19
+ d.reject do |h|
20
+ h.keys.any? { |k| k.match(ALIAS_KEY_REGEXP) }
21
+ end
22
+ else
23
+ d.reject do |k, v|
24
+ v.kind_of?(Hash) && k.match(ALIAS_KEY_REGEXP)
25
+ end
17
26
  end
18
27
  end
19
28
 
20
29
  end
21
-
22
- def initialize(attributes={})
23
- super unless attributes.keys.index { |k| k.to_s.match(/^\//i) }
24
- end
25
30
  end
26
31
 
27
32
  end
@@ -4,12 +4,16 @@ module ActiveYaml
4
4
 
5
5
  class Base < ActiveFile::Base
6
6
  extend ActiveFile::HashAndArrayFiles
7
+
8
+ cattr_accessor :process_erb, instance_accessor: false
9
+ @@process_erb = true
10
+
7
11
  class << self
8
12
  def load_file
9
13
  if (data = raw_data).is_a?(Array)
10
14
  data
11
15
  elsif data.respond_to?(:values)
12
- data.values
16
+ data.map{ |key, value| {"key" => key}.merge(value) }
13
17
  end
14
18
  end
15
19
 
@@ -18,9 +22,19 @@ module ActiveYaml
18
22
  end
19
23
 
20
24
  private
25
+ if Psych::VERSION >= "4.0.0"
21
26
  def load_path(path)
22
- YAML.load(ERB.new(File.read(path)).result)
27
+ result = File.read(path)
28
+ result = ERB.new(result).result if process_erb
29
+ YAML.unsafe_load(result)
23
30
  end
31
+ else
32
+ def load_path(path)
33
+ result = File.read(path)
34
+ result = ERB.new(result).result if process_erb
35
+ YAML.load(result)
36
+ end
37
+ end
24
38
  end
25
39
  end
26
40
  end