active_hash 3.1.1 → 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.
@@ -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
@@ -7,24 +7,89 @@ 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
+ 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
69
 
18
- def where(query_hash = :chain)
19
- return ActiveHash::Base::WhereChain.new(self) if query_hash == :chain
70
+ def reorder!(*options)
71
+ check_if_method_has_arguments!(:order, options)
20
72
 
21
- self.records_dirty = true unless query_hash.nil? || query_hash.keys.empty?
22
- self.query_hash.merge!(query_hash || {})
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({})
@@ -58,10 +123,11 @@ module ActiveHash
58
123
  end
59
124
 
60
125
  def find_by_id(id)
61
- return where(id: id).first if query_hash.present?
62
-
63
126
  index = klass.send(:record_index)[id.to_s] # TODO: Make index in Base publicly readable instead of using send?
64
- index and records[index]
127
+ return unless index
128
+
129
+ record = all_records[index]
130
+ record if conditions.matches?(record)
65
131
  end
66
132
 
67
133
  def count
@@ -73,7 +139,16 @@ module ActiveHash
73
139
  end
74
140
 
75
141
  def pluck(*column_names)
76
- column_names.map { |column_name| all.map(&column_name.to_sym) }.inject(&:zip)
142
+ symbolized_column_names = column_names.map(&:to_sym)
143
+
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
77
152
  end
78
153
 
79
154
  def ids
@@ -84,86 +159,32 @@ module ActiveHash
84
159
  pluck(*column_names).first
85
160
  end
86
161
 
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
162
  def to_ary
105
163
  records.dup
106
164
  end
107
165
 
108
166
  def method_missing(method_name, *args)
109
- return super unless self.klass.scopes.key?(method_name)
167
+ return super unless klass.scopes.key?(method_name)
110
168
 
111
- instance_exec(*args, &self.klass.scopes[method_name])
169
+ instance_exec(*args, &klass.scopes[method_name])
112
170
  end
113
171
 
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
172
+ def respond_to_missing?(method_name, include_private = false)
173
+ klass.scopes.key?(method_name) || super
126
174
  end
127
175
 
128
- def filter_all_records_by_query_hash
129
- self.records_dirty = false
130
- return all_records if query_hash.blank?
131
-
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
176
+ private
138
177
 
139
- return candidates if query_hash.blank?
178
+ attr_writer :conditions, :order_values, :klass, :all_records
140
179
 
141
- (candidates || all_records || []).select do |record|
142
- match_options?(record, query_hash)
143
- end
144
- end
180
+ def apply_conditions(records, conditions)
181
+ return records if conditions.blank?
145
182
 
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
183
+ records.select do |record|
184
+ conditions.matches?(record)
153
185
  end
154
186
  end
155
187
 
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
188
  def check_if_method_has_arguments!(method_name, args)
168
189
  return unless args.blank?
169
190
 
@@ -179,7 +200,9 @@ module ActiveHash
179
200
  ary.map! { |e| e.split(/\W+/) }.reverse!
180
201
  end
181
202
 
182
- def order_by_args!(candidates, args)
203
+ def apply_order_values(records, args)
204
+ ordered_records = records.dup
205
+
183
206
  args.each do |arg|
184
207
  field, dir = if arg.is_a?(Hash)
185
208
  arg.to_a.flatten.map(&:to_sym)
@@ -189,7 +212,7 @@ module ActiveHash
189
212
  arg.to_sym
190
213
  end
191
214
 
192
- candidates.sort! do |a, b|
215
+ ordered_records.sort! do |a, b|
193
216
  if dir.present? && dir.to_sym.upcase.equal?(:DESC)
194
217
  b[field] <=> a[field]
195
218
  else
@@ -197,6 +220,8 @@ module ActiveHash
197
220
  end
198
221
  end
199
222
  end
223
+
224
+ ordered_records
200
225
  end
201
226
  end
202
227
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveHash
2
2
  module Gem
3
- VERSION = "3.1.1"
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'
@@ -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
@@ -4,19 +4,19 @@ module ActiveHash
4
4
  module ActiveRecordExtensions
5
5
 
6
6
  def belongs_to(name, scope = nil, **options)
7
- options = {:class_name => name.to_s.camelize }.merge(options)
7
+ klass_name = options.key?(:class_name) ? options[:class_name] : name.to_s.camelize
8
8
  klass =
9
9
  begin
10
- options[:class_name].constantize
11
- rescue
12
- nil
13
- rescue LoadError
10
+ klass_name.constantize
11
+ rescue StandardError, LoadError
14
12
  nil
15
13
  end
14
+
16
15
  if klass && klass < ActiveHash::Base
16
+ options = { class_name: klass_name }.merge(options)
17
17
  belongs_to_active_hash(name, options)
18
18
  else
19
- super(name, **options)
19
+ super
20
20
  end
21
21
  end
22
22
 
@@ -49,13 +49,20 @@ module ActiveHash
49
49
  end
50
50
 
51
51
  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
- )
52
+ if defined?(ActiveHash::Reflection::BelongsToReflection)
53
+ reflection = ActiveHash::Reflection::BelongsToReflection.new(association_id.to_sym, nil, options, self)
54
+ if options[:through]
55
+ reflection = ActiveRecord::ThroughReflection.new(reflection)
56
+ end
57
+ else
58
+ reflection = ActiveRecord::Reflection.create(
59
+ :belongs_to,
60
+ association_id.to_sym,
61
+ nil,
62
+ options,
63
+ self
64
+ )
65
+ end
59
66
 
60
67
  ActiveRecord::Reflection.add_reflection(
61
68
  self,
@@ -85,12 +92,12 @@ module ActiveHash
85
92
  end
86
93
 
87
94
  def self.included(base)
95
+ require_relative "reflection_extensions"
88
96
  base.extend Methods
89
97
  end
90
98
 
91
99
  module Methods
92
100
  def has_many(association_id, options = {})
93
-
94
101
  define_method(association_id) do
95
102
  options = {
96
103
  :class_name => association_id.to_s.classify,
@@ -110,6 +117,9 @@ module ActiveHash
110
117
  klass.where(foreign_key => primary_key_value)
111
118
  end
112
119
  end
120
+ define_method("#{association_id.to_s.underscore.singularize}_ids") do
121
+ public_send(association_id).map(&:id)
122
+ end
113
123
  end
114
124
 
115
125
  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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Dean
@@ -29,7 +29,7 @@ authors:
29
29
  autorequire:
30
30
  bindir: bin
31
31
  cert_chain: []
32
- date: 2022-07-14 00:00:00.000000000 Z
32
+ date: 2023-05-06 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,17 +75,24 @@ 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: {}
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
89
96
  post_install_message:
90
97
  rdoc_options: []
91
98
  require_paths:
@@ -101,7 +108,7 @@ 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
111
+ rubygems_version: 3.2.33
105
112
  signing_key:
106
113
  specification_version: 4
107
114
  summary: An ActiveRecord-like model that uses a hash or file as a datasource