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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +366 -0
- data/README.md +11 -8
- data/active_hash.gemspec +8 -1
- data/lib/active_file/base.rb +11 -4
- data/lib/active_file/hash_and_array_files.rb +1 -1
- data/lib/active_hash/base.rb +26 -49
- data/lib/active_hash/condition.rb +44 -0
- data/lib/active_hash/conditions.rb +21 -0
- data/lib/active_hash/relation.rb +107 -78
- data/lib/active_hash/version.rb +1 -1
- data/lib/active_hash.rb +2 -0
- data/lib/active_yaml/base.rb +10 -2
- data/lib/associations/associations.rb +27 -14
- data/lib/associations/reflection_extensions.rb +25 -0
- metadata +15 -8
- data/CHANGELOG +0 -242
|
@@ -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
|
data/lib/active_hash/relation.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
14
|
-
self.
|
|
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
|
|
19
|
-
|
|
55
|
+
def invert_where
|
|
56
|
+
spawn.invert_where!
|
|
57
|
+
end
|
|
20
58
|
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
171
|
+
return super unless klass.scopes&.key?(method_name)
|
|
110
172
|
|
|
111
|
-
instance_exec(*args, &
|
|
173
|
+
instance_exec(*args, &klass.scopes[method_name])
|
|
112
174
|
end
|
|
113
175
|
|
|
114
|
-
|
|
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
|
-
|
|
129
|
-
self.records_dirty = false
|
|
130
|
-
return all_records if query_hash.blank?
|
|
180
|
+
private
|
|
131
181
|
|
|
132
|
-
|
|
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
|
-
|
|
184
|
+
def apply_conditions(records, conditions)
|
|
185
|
+
return records if conditions.blank?
|
|
140
186
|
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/active_hash/version.rb
CHANGED
data/lib/active_hash.rb
CHANGED
data/lib/active_yaml/base.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
klass_name = options.key?(:class_name) ? options[:class_name] : name.to_s.camelize
|
|
8
12
|
klass =
|
|
9
13
|
begin
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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: []
|