active_hash 3.0.0 → 3.2.1

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, 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.read_attribute(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,114 @@
1
1
  module ActiveHash
2
2
  class Relation
3
3
  include Enumerable
4
-
4
+
5
5
  delegate :each, to: :records # Make Enumerable work
6
- delegate :equal?, :==, :===, :eql?, to: :records
6
+ delegate :equal?, :==, :===, :eql?, :sort!, to: :records
7
7
  delegate :empty?, :length, :first, :second, :third, :last, to: :records
8
-
9
- def initialize(klass, all_records, query_hash = nil)
8
+ delegate :sample, to: :records
9
+
10
+ attr_reader :conditions, :order_values, :klass, :all_records
11
+
12
+ def initialize(klass, all_records, conditions = nil, order_values = nil)
10
13
  self.klass = klass
11
14
  self.all_records = all_records
12
- self.query_hash = query_hash
13
- 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
+ def pretty_print(pp)
26
+ pp.pp(entries.to_ary)
27
+ end
28
+
29
+ class WhereChain
30
+ attr_reader :relation
31
+
32
+ def initialize(relation)
33
+ @relation = relation
34
+ end
35
+
36
+ def not(conditions_hash)
37
+ relation.conditions << Condition.new(conditions_hash).invert!
38
+ relation
39
+ end
40
+ end
41
+
42
+ def order(*options)
43
+ spawn.order!(*options)
44
+ end
45
+
46
+ def reorder(*options)
47
+ spawn.reorder!(*options)
48
+ end
49
+
50
+ def where!(conditions_hash, inverted = false)
51
+ self.conditions << Condition.new(conditions_hash)
14
52
  self
15
53
  end
16
-
17
- def where(query_hash = :chain)
18
- return ActiveHash::Base::WhereChain.new(self) if query_hash == :chain
19
-
20
- self.records_dirty = true unless query_hash.nil? || query_hash.keys.empty?
21
- self.query_hash.merge!(query_hash || {})
54
+
55
+ def invert_where
56
+ spawn.invert_where!
57
+ end
58
+
59
+ def invert_where!
60
+ conditions.map(&:invert!)
61
+ self
62
+ end
63
+
64
+ def spawn
65
+ self.class.new(klass, all_records, conditions, order_values)
66
+ end
67
+
68
+ def order!(*options)
69
+ check_if_method_has_arguments!(:order, options)
70
+ self.order_values += preprocess_order_args(options)
71
+ self
72
+ end
73
+
74
+ def reorder!(*options)
75
+ check_if_method_has_arguments!(:order, options)
76
+
77
+ self.order_values = preprocess_order_args(options)
78
+ @records = apply_order_values(records, order_values)
79
+
22
80
  self
23
81
  end
24
-
82
+
83
+ def records
84
+ @records ||= begin
85
+ filtered_records = apply_conditions(all_records, conditions)
86
+ ordered_records = apply_order_values(filtered_records, order_values) # rubocop:disable Lint/UselessAssignment
87
+ end
88
+ end
89
+
90
+ def reload
91
+ @records = nil # Reset records
92
+ self
93
+ end
94
+
25
95
  def all(options = {})
26
- if options.has_key?(:conditions)
96
+ if options.key?(:conditions)
27
97
  where(options[:conditions])
28
98
  else
29
99
  where({})
30
100
  end
31
101
  end
32
-
102
+
33
103
  def find_by(options)
34
104
  where(options).first
35
105
  end
36
106
 
37
107
  def find_by!(options)
38
- find_by(options) || (raise RecordNotFound.new("Couldn't find #{klass.name}"))
108
+ find_by(options) || (raise RecordNotFound.new("Couldn't find #{klass.name}", klass.name))
39
109
  end
40
-
41
- def find(id, *args)
110
+
111
+ def find(id = nil, *args, &block)
42
112
  case id
43
113
  when :all
44
114
  all
@@ -47,82 +117,115 @@ module ActiveHash
47
117
  when Array
48
118
  id.map { |i| find(i) }
49
119
  when nil
50
- raise RecordNotFound.new("Couldn't find #{klass.name} without an ID")
120
+ raise RecordNotFound.new("Couldn't find #{klass.name} without an ID", klass.name, "id") unless block_given?
121
+ records.find(&block) # delegate to Enumerable#find if a block is given
51
122
  else
52
123
  find_by_id(id) || begin
53
- raise RecordNotFound.new("Couldn't find #{klass.name} with ID=#{id}")
124
+ raise RecordNotFound.new("Couldn't find #{klass.name} with ID=#{id}", klass.name, "id", id)
54
125
  end
55
126
  end
56
127
  end
57
-
128
+
58
129
  def find_by_id(id)
59
130
  index = klass.send(:record_index)[id.to_s] # TODO: Make index in Base publicly readable instead of using send?
60
- index and records[index]
131
+ return unless index
132
+
133
+ record = all_records[index]
134
+ record if conditions.matches?(record)
61
135
  end
62
-
136
+
63
137
  def count
64
138
  length
65
139
  end
66
-
67
- def pluck(*column_names)
68
- column_names.map { |column_name| all.map(&column_name.to_sym) }.inject(&:zip)
69
- end
70
-
71
- def reload
72
- @records = filter_all_records_by_query_hash
140
+
141
+ def size
142
+ length
73
143
  end
74
-
75
- attr_reader :query_hash, :klass, :all_records, :records_dirty
76
-
77
- private
78
-
79
- attr_writer :query_hash, :klass, :all_records, :records_dirty
80
-
81
- def records
82
- if @records.nil? || records_dirty
83
- reload
144
+
145
+ def pluck(*column_names)
146
+ symbolized_column_names = column_names.map(&:to_sym)
147
+
148
+ if symbolized_column_names.length == 1
149
+ column_name = symbolized_column_names.first
150
+ all.map { |record| record[column_name] }
84
151
  else
85
- @records
152
+ all.map do |record|
153
+ symbolized_column_names.map { |column_name| record[column_name] }
154
+ end
86
155
  end
87
156
  end
88
-
89
- def filter_all_records_by_query_hash
90
- self.records_dirty = false
91
- return all_records if query_hash.blank?
92
-
93
- # use index if searching by id
94
- if query_hash.key?(:id) || query_hash.key?("id")
95
- ids = (query_hash.delete(:id) || query_hash.delete("id"))
96
- ids = range_to_array(ids) if ids.is_a?(Range)
97
- candidates = Array.wrap(ids).map { |id| klass.find_by_id(id) }.compact
98
- end
99
-
100
- return candidates if query_hash.blank?
101
157
 
102
- (candidates || all_records || []).select do |record|
103
- match_options?(record, query_hash)
104
- end
158
+ def ids
159
+ pluck(:id)
105
160
  end
106
-
107
- def match_options?(record, options)
108
- options.all? do |col, match|
109
- if match.kind_of?(Array)
110
- match.any? { |v| normalize(v) == normalize(record[col]) }
111
- else
112
- normalize(record[col]) == normalize(match)
113
- end
161
+
162
+ def pick(*column_names)
163
+ pluck(*column_names).first
164
+ end
165
+
166
+ def to_ary
167
+ records.dup
168
+ end
169
+
170
+ def method_missing(method_name, *args)
171
+ return super unless klass.scopes&.key?(method_name)
172
+
173
+ instance_exec(*args, &klass.scopes[method_name])
174
+ end
175
+
176
+ def respond_to_missing?(method_name, include_private = false)
177
+ klass.scopes&.key?(method_name) || super
178
+ end
179
+
180
+ private
181
+
182
+ attr_writer :conditions, :order_values, :klass, :all_records
183
+
184
+ def apply_conditions(records, conditions)
185
+ return records if conditions.blank?
186
+
187
+ records.select do |record|
188
+ conditions.matches?(record)
114
189
  end
115
190
  end
116
191
 
117
- def normalize(v)
118
- v.respond_to?(:to_sym) ? v.to_sym : v
192
+ def check_if_method_has_arguments!(method_name, args)
193
+ return unless args.blank?
194
+
195
+ raise ArgumentError,
196
+ "The method .#{method_name}() must contain arguments."
119
197
  end
120
-
121
- def range_to_array(range)
122
- return range.to_a unless range.end.nil?
123
198
 
124
- e = records.last[:id]
125
- (range.begin..e).to_a
199
+ def preprocess_order_args(order_args)
200
+ order_args.reject!(&:blank?)
201
+ return order_args.reverse! unless order_args.first.is_a?(String)
202
+
203
+ ary = order_args.first.split(', ')
204
+ ary.map! { |e| e.split(/\W+/) }.reverse!
205
+ end
206
+
207
+ def apply_order_values(records, args)
208
+ ordered_records = records.dup
209
+
210
+ args.each do |arg|
211
+ field, dir = if arg.is_a?(Hash)
212
+ arg.to_a.flatten.map(&:to_sym)
213
+ elsif arg.is_a?(Array)
214
+ arg.map(&:to_sym)
215
+ else
216
+ arg.to_sym
217
+ end
218
+
219
+ ordered_records.sort! do |a, b|
220
+ if dir.present? && dir.to_sym.upcase.equal?(:DESC)
221
+ b[field] <=> a[field]
222
+ else
223
+ a[field] <=> b[field]
224
+ end
225
+ end
226
+ end
227
+
228
+ ordered_records
126
229
  end
127
230
  end
128
231
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveHash
2
2
  module Gem
3
- VERSION = "3.0.0"
3
+ VERSION = "3.2.1"
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