active_hash 3.1.1 → 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.
@@ -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
@@ -7,24 +7,93 @@ module ActiveHash
7
7
  delegate :empty?, :length, :first, :second, :third, :last, to: :records
8
8
  delegate :sample, to: :records
9
9
 
10
- def initialize(klass, all_records, query_hash = nil)
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
+ 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)
15
52
  self
16
53
  end
17
54
 
18
- def where(query_hash = :chain)
19
- return ActiveHash::Base::WhereChain.new(self) if query_hash == :chain
55
+ def invert_where
56
+ spawn.invert_where!
57
+ end
20
58
 
21
- self.records_dirty = true unless query_hash.nil? || query_hash.keys.empty?
22
- self.query_hash.merge!(query_hash || {})
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
+
80
+ self
81
+ end
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
23
92
  self
24
93
  end
25
94
 
26
95
  def all(options = {})
27
- if options.has_key?(:conditions)
96
+ if options.key?(:conditions)
28
97
  where(options[:conditions])
29
98
  else
30
99
  where({})
@@ -58,10 +127,11 @@ module ActiveHash
58
127
  end
59
128
 
60
129
  def find_by_id(id)
61
- return where(id: id).first if query_hash.present?
62
-
63
130
  index = klass.send(:record_index)[id.to_s] # TODO: Make index in Base publicly readable instead of using send?
64
- index and records[index]
131
+ return unless index
132
+
133
+ record = all_records[index]
134
+ record if conditions.matches?(record)
65
135
  end
66
136
 
67
137
  def count
@@ -73,7 +143,16 @@ module ActiveHash
73
143
  end
74
144
 
75
145
  def pluck(*column_names)
76
- column_names.map { |column_name| all.map(&column_name.to_sym) }.inject(&:zip)
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] }
151
+ else
152
+ all.map do |record|
153
+ symbolized_column_names.map { |column_name| record[column_name] }
154
+ end
155
+ end
77
156
  end
78
157
 
79
158
  def ids
@@ -84,86 +163,32 @@ module ActiveHash
84
163
  pluck(*column_names).first
85
164
  end
86
165
 
87
- def reload
88
- @records = filter_all_records_by_query_hash
89
- end
90
-
91
- def order(*options)
92
- check_if_method_has_arguments!(:order, options)
93
- relation = where({})
94
- return relation if options.blank?
95
-
96
- processed_args = preprocess_order_args(options)
97
- candidates = relation.dup
98
-
99
- order_by_args!(candidates, processed_args)
100
-
101
- candidates
102
- end
103
-
104
166
  def to_ary
105
167
  records.dup
106
168
  end
107
169
 
108
170
  def method_missing(method_name, *args)
109
- return super unless self.klass.scopes.key?(method_name)
171
+ return super unless klass.scopes&.key?(method_name)
110
172
 
111
- instance_exec(*args, &self.klass.scopes[method_name])
173
+ instance_exec(*args, &klass.scopes[method_name])
112
174
  end
113
175
 
114
- attr_reader :query_hash, :klass, :all_records, :records_dirty
115
-
116
- private
117
-
118
- attr_writer :query_hash, :klass, :all_records, :records_dirty
119
-
120
- def records
121
- if !defined?(@records) || @records.nil? || records_dirty
122
- reload
123
- else
124
- @records
125
- end
176
+ def respond_to_missing?(method_name, include_private = false)
177
+ klass.scopes&.key?(method_name) || super
126
178
  end
127
179
 
128
- def filter_all_records_by_query_hash
129
- self.records_dirty = false
130
- return all_records if query_hash.blank?
180
+ private
131
181
 
132
- # use index if searching by id
133
- if query_hash.key?(:id) || query_hash.key?("id")
134
- ids = (query_hash.delete(:id) || query_hash.delete("id"))
135
- ids = range_to_array(ids) if ids.is_a?(Range)
136
- candidates = Array.wrap(ids).map { |id| klass.find_by_id(id) }.compact
137
- end
182
+ attr_writer :conditions, :order_values, :klass, :all_records
138
183
 
139
- return candidates if query_hash.blank?
184
+ def apply_conditions(records, conditions)
185
+ return records if conditions.blank?
140
186
 
141
- (candidates || all_records || []).select do |record|
142
- match_options?(record, query_hash)
187
+ records.select do |record|
188
+ conditions.matches?(record)
143
189
  end
144
190
  end
145
191
 
146
- def match_options?(record, options)
147
- options.all? do |col, match|
148
- if match.kind_of?(Array)
149
- match.any? { |v| normalize(v) == normalize(record[col]) }
150
- else
151
- normalize(match) === normalize(record[col])
152
- end
153
- end
154
- end
155
-
156
- def normalize(v)
157
- v.respond_to?(:to_sym) ? v.to_sym : v
158
- end
159
-
160
- def range_to_array(range)
161
- return range.to_a unless range.end.nil?
162
-
163
- e = records.last[:id]
164
- (range.begin..e).to_a
165
- end
166
-
167
192
  def check_if_method_has_arguments!(method_name, args)
168
193
  return unless args.blank?
169
194
 
@@ -179,7 +204,9 @@ module ActiveHash
179
204
  ary.map! { |e| e.split(/\W+/) }.reverse!
180
205
  end
181
206
 
182
- def order_by_args!(candidates, args)
207
+ def apply_order_values(records, args)
208
+ ordered_records = records.dup
209
+
183
210
  args.each do |arg|
184
211
  field, dir = if arg.is_a?(Hash)
185
212
  arg.to_a.flatten.map(&:to_sym)
@@ -189,7 +216,7 @@ module ActiveHash
189
216
  arg.to_sym
190
217
  end
191
218
 
192
- candidates.sort! do |a, b|
219
+ ordered_records.sort! do |a, b|
193
220
  if dir.present? && dir.to_sym.upcase.equal?(:DESC)
194
221
  b[field] <=> a[field]
195
222
  else
@@ -197,6 +224,8 @@ module ActiveHash
197
224
  end
198
225
  end
199
226
  end
227
+
228
+ ordered_records
200
229
  end
201
230
  end
202
231
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveHash
2
2
  module Gem
3
- VERSION = "3.1.1"
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'
@@ -4,6 +4,10 @@ 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)
@@ -20,11 +24,15 @@ module ActiveYaml
20
24
  private
21
25
  if Psych::VERSION >= "4.0.0"
22
26
  def load_path(path)
23
- YAML.unsafe_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)
24
30
  end
25
31
  else
26
32
  def load_path(path)
27
- YAML.load(ERB.new(File.read(path)).result)
33
+ result = File.read(path)
34
+ result = ERB.new(result).result if process_erb
35
+ YAML.load(result)
28
36
  end
29
37
  end
30
38
  end
@@ -3,20 +3,24 @@ module ActiveHash
3
3
 
4
4
  module ActiveRecordExtensions
5
5
 
6
+ def self.extended(base)
7
+ require_relative 'reflection_extensions'
8
+ end
9
+
6
10
  def belongs_to(name, scope = nil, **options)
7
- options = {:class_name => name.to_s.camelize }.merge(options)
11
+ klass_name = options.key?(:class_name) ? options[:class_name] : name.to_s.camelize
8
12
  klass =
9
13
  begin
10
- options[:class_name].constantize
11
- rescue
12
- nil
13
- rescue LoadError
14
+ klass_name.constantize
15
+ rescue StandardError, LoadError
14
16
  nil
15
17
  end
18
+
16
19
  if klass && klass < ActiveHash::Base
20
+ options = { class_name: klass_name }.merge(options)
17
21
  belongs_to_active_hash(name, options)
18
22
  else
19
- super(name, **options)
23
+ super
20
24
  end
21
25
  end
22
26
 
@@ -49,13 +53,20 @@ module ActiveHash
49
53
  end
50
54
 
51
55
  if ActiveRecord::Reflection.respond_to?(:create)
52
- reflection = ActiveRecord::Reflection.create(
53
- :belongs_to,
54
- association_id.to_sym,
55
- nil,
56
- options,
57
- self
58
- )
56
+ if defined?(ActiveHash::Reflection::BelongsToReflection)
57
+ reflection = ActiveHash::Reflection::BelongsToReflection.new(association_id.to_sym, nil, options, self)
58
+ if options[:through]
59
+ reflection = ActiveRecord::ThroughReflection.new(reflection)
60
+ end
61
+ else
62
+ reflection = ActiveRecord::Reflection.create(
63
+ :belongs_to,
64
+ association_id.to_sym,
65
+ nil,
66
+ options,
67
+ self
68
+ )
69
+ end
59
70
 
60
71
  ActiveRecord::Reflection.add_reflection(
61
72
  self,
@@ -90,7 +101,6 @@ module ActiveHash
90
101
 
91
102
  module Methods
92
103
  def has_many(association_id, options = {})
93
-
94
104
  define_method(association_id) do
95
105
  options = {
96
106
  :class_name => association_id.to_s.classify,
@@ -110,6 +120,9 @@ module ActiveHash
110
120
  klass.where(foreign_key => primary_key_value)
111
121
  end
112
122
  end
123
+ define_method("#{association_id.to_s.underscore.singularize}_ids") do
124
+ public_send(association_id).map(&:id)
125
+ end
113
126
  end
114
127
 
115
128
  def has_one(association_id, options = {})
@@ -0,0 +1,25 @@
1
+ module ActiveHash
2
+ module Reflection
3
+ class BelongsToReflection < ActiveRecord::Reflection::BelongsToReflection
4
+ def compute_class(name)
5
+ if polymorphic?
6
+ raise ArgumentError, "Polymorphic associations do not support computing the class."
7
+ end
8
+
9
+ begin
10
+ klass = active_record.send(:compute_type, name)
11
+ rescue NameError => error
12
+ if error.name.match?(/(?:\A|::)#{name}\z/)
13
+ message = "Missing model class #{name} for the #{active_record}##{self.name} association."
14
+ message += " You can specify a different model class with the :class_name option." unless options[:class_name]
15
+ raise NameError.new(message, name)
16
+ else
17
+ raise
18
+ end
19
+ end
20
+
21
+ klass
22
+ end
23
+ end
24
+ end
25
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_hash
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.1
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Dean
@@ -26,10 +26,10 @@ authors:
26
26
  - Brett Richardson
27
27
  - Rachel Heaton
28
28
  - Keisuke Izumiya
29
- autorequire:
29
+ autorequire:
30
30
  bindir: bin
31
31
  cert_chain: []
32
- date: 2022-07-14 00:00:00.000000000 Z
32
+ date: 2023-10-12 00:00:00.000000000 Z
33
33
  dependencies:
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: activesupport
@@ -66,7 +66,7 @@ executables: []
66
66
  extensions: []
67
67
  extra_rdoc_files: []
68
68
  files:
69
- - CHANGELOG
69
+ - CHANGELOG.md
70
70
  - LICENSE
71
71
  - README.md
72
72
  - active_hash.gemspec
@@ -75,18 +75,25 @@ files:
75
75
  - lib/active_file/multiple_files.rb
76
76
  - lib/active_hash.rb
77
77
  - lib/active_hash/base.rb
78
+ - lib/active_hash/condition.rb
79
+ - lib/active_hash/conditions.rb
78
80
  - lib/active_hash/relation.rb
79
81
  - lib/active_hash/version.rb
80
82
  - lib/active_json/base.rb
81
83
  - lib/active_yaml/aliases.rb
82
84
  - lib/active_yaml/base.rb
83
85
  - lib/associations/associations.rb
86
+ - lib/associations/reflection_extensions.rb
84
87
  - lib/enum/enum.rb
85
88
  homepage: http://github.com/active-hash/active_hash
86
89
  licenses:
87
90
  - MIT
88
- metadata: {}
89
- post_install_message:
91
+ metadata:
92
+ homepage_uri: http://github.com/active-hash/active_hash
93
+ changelog_uri: https://github.com/active-hash/active_hash/blob/master/CHANGELOG.md
94
+ source_code_uri: http://github.com/active-hash/active_hash
95
+ bug_tracker_uri: https://github.com/active-hash/active_hash/issues
96
+ post_install_message:
90
97
  rdoc_options: []
91
98
  require_paths:
92
99
  - lib
@@ -101,8 +108,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
108
  - !ruby/object:Gem::Version
102
109
  version: '0'
103
110
  requirements: []
104
- rubygems_version: 3.2.22
105
- signing_key:
111
+ rubygems_version: 3.4.19
112
+ signing_key:
106
113
  specification_version: 4
107
114
  summary: An ActiveRecord-like model that uses a hash or file as a datasource
108
115
  test_files: []