mr 0.35.2
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 +7 -0
- data/.gitignore +19 -0
- data/Gemfile +13 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/bench/all.rb +4 -0
- data/bench/factory.rb +68 -0
- data/bench/fake_record.rb +174 -0
- data/bench/model.rb +201 -0
- data/bench/read_model.rb +191 -0
- data/bench/results/factory.txt +21 -0
- data/bench/results/fake_record.txt +37 -0
- data/bench/results/model.txt +44 -0
- data/bench/results/read_model.txt +46 -0
- data/bench/setup.rb +132 -0
- data/lib/mr.rb +11 -0
- data/lib/mr/after_commit.rb +49 -0
- data/lib/mr/after_commit/fake_record.rb +39 -0
- data/lib/mr/after_commit/record.rb +48 -0
- data/lib/mr/after_commit/record_procs_methods.rb +82 -0
- data/lib/mr/factory.rb +82 -0
- data/lib/mr/factory/config.rb +240 -0
- data/lib/mr/factory/model_factory.rb +103 -0
- data/lib/mr/factory/model_stack.rb +28 -0
- data/lib/mr/factory/read_model_factory.rb +104 -0
- data/lib/mr/factory/record_factory.rb +130 -0
- data/lib/mr/factory/record_stack.rb +219 -0
- data/lib/mr/fake_query.rb +53 -0
- data/lib/mr/fake_record.rb +58 -0
- data/lib/mr/fake_record/associations.rb +257 -0
- data/lib/mr/fake_record/attributes.rb +168 -0
- data/lib/mr/fake_record/persistence.rb +116 -0
- data/lib/mr/json_field.rb +180 -0
- data/lib/mr/json_field/fake_record.rb +31 -0
- data/lib/mr/json_field/record.rb +38 -0
- data/lib/mr/model.rb +67 -0
- data/lib/mr/model/associations.rb +161 -0
- data/lib/mr/model/configuration.rb +67 -0
- data/lib/mr/model/fields.rb +177 -0
- data/lib/mr/model/persistence.rb +79 -0
- data/lib/mr/query.rb +126 -0
- data/lib/mr/read_model.rb +83 -0
- data/lib/mr/read_model/data.rb +38 -0
- data/lib/mr/read_model/fields.rb +218 -0
- data/lib/mr/read_model/query_expression.rb +188 -0
- data/lib/mr/read_model/querying.rb +214 -0
- data/lib/mr/read_model/set_querying.rb +82 -0
- data/lib/mr/read_model/subquery.rb +98 -0
- data/lib/mr/record.rb +35 -0
- data/lib/mr/test_helpers.rb +229 -0
- data/lib/mr/type_converter.rb +85 -0
- data/lib/mr/version.rb +3 -0
- data/log/.gitkeep +0 -0
- data/mr.gemspec +29 -0
- data/test/helper.rb +21 -0
- data/test/support/db.rb +10 -0
- data/test/support/factory.rb +13 -0
- data/test/support/factory/area.rb +6 -0
- data/test/support/factory/comment.rb +14 -0
- data/test/support/factory/image.rb +6 -0
- data/test/support/factory/user.rb +6 -0
- data/test/support/models/area.rb +58 -0
- data/test/support/models/comment.rb +60 -0
- data/test/support/models/image.rb +53 -0
- data/test/support/models/user.rb +96 -0
- data/test/support/read_model/querying.rb +150 -0
- data/test/support/read_models/comment_with_user_data.rb +27 -0
- data/test/support/read_models/set_data.rb +49 -0
- data/test/support/read_models/subquery_data.rb +41 -0
- data/test/support/read_models/user_with_area_data.rb +15 -0
- data/test/support/schema.rb +39 -0
- data/test/support/setup_test_db.rb +10 -0
- data/test/system/factory/model_factory_tests.rb +87 -0
- data/test/system/factory/model_stack_tests.rb +30 -0
- data/test/system/factory/record_factory_tests.rb +84 -0
- data/test/system/factory/record_stack_tests.rb +51 -0
- data/test/system/factory_tests.rb +32 -0
- data/test/system/read_model_tests.rb +199 -0
- data/test/system/with_model_tests.rb +275 -0
- data/test/unit/after_commit/fake_record_tests.rb +110 -0
- data/test/unit/after_commit/record_procs_methods_tests.rb +177 -0
- data/test/unit/after_commit/record_tests.rb +134 -0
- data/test/unit/after_commit_tests.rb +113 -0
- data/test/unit/factory/config_tests.rb +651 -0
- data/test/unit/factory/model_factory_tests.rb +473 -0
- data/test/unit/factory/model_stack_tests.rb +97 -0
- data/test/unit/factory/read_model_factory_tests.rb +195 -0
- data/test/unit/factory/record_factory_tests.rb +446 -0
- data/test/unit/factory/record_stack_tests.rb +549 -0
- data/test/unit/factory_tests.rb +213 -0
- data/test/unit/fake_query_tests.rb +137 -0
- data/test/unit/fake_record/associations_tests.rb +585 -0
- data/test/unit/fake_record/attributes_tests.rb +265 -0
- data/test/unit/fake_record/persistence_tests.rb +239 -0
- data/test/unit/fake_record_tests.rb +106 -0
- data/test/unit/json_field/fake_record_tests.rb +75 -0
- data/test/unit/json_field/record_tests.rb +80 -0
- data/test/unit/json_field_tests.rb +302 -0
- data/test/unit/model/associations_tests.rb +346 -0
- data/test/unit/model/configuration_tests.rb +92 -0
- data/test/unit/model/fields_tests.rb +278 -0
- data/test/unit/model/persistence_tests.rb +114 -0
- data/test/unit/model_tests.rb +137 -0
- data/test/unit/query_tests.rb +300 -0
- data/test/unit/read_model/data_tests.rb +56 -0
- data/test/unit/read_model/fields_tests.rb +416 -0
- data/test/unit/read_model/query_expression_tests.rb +381 -0
- data/test/unit/read_model/querying_tests.rb +613 -0
- data/test/unit/read_model/set_querying_tests.rb +149 -0
- data/test/unit/read_model/subquery_tests.rb +242 -0
- data/test/unit/read_model_tests.rb +187 -0
- data/test/unit/record_tests.rb +45 -0
- data/test/unit/test_helpers_tests.rb +431 -0
- data/test/unit/type_converter_tests.rb +207 -0
- metadata +285 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
require 'mr/record'
|
3
|
+
|
4
|
+
module MR; end
|
5
|
+
module MR::Model
|
6
|
+
|
7
|
+
module Configuration
|
8
|
+
include MuchPlugin
|
9
|
+
|
10
|
+
# `MR::Model::Configuration` is a mixin that provides a model's record
|
11
|
+
# handling behavior. This includes reading/writing a model class'
|
12
|
+
# `record_class` and reading/writing a model's `record`. These operations
|
13
|
+
# validate what is being written to avoid confusing errors. The
|
14
|
+
# `Configuration` mixin is a base mixin for all the other model mixins.
|
15
|
+
#
|
16
|
+
# * Use the `record` protected method to access the record instance.
|
17
|
+
# * Use the `set_record` private method to write a record value.
|
18
|
+
|
19
|
+
plugin_included do
|
20
|
+
extend ClassMethods
|
21
|
+
include InstanceMethods
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
def record_class(*args)
|
27
|
+
set_record_class(*args) unless args.empty?
|
28
|
+
@record_class || raise(NoRecordClassError, "a record class hasn't been set", caller)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def set_record_class(value)
|
34
|
+
raise ArgumentError, "must be a MR::Record" unless value < MR::Record
|
35
|
+
@record_class = value
|
36
|
+
value.model_class = self
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
module InstanceMethods
|
42
|
+
|
43
|
+
def record_class
|
44
|
+
self.class.record_class
|
45
|
+
end
|
46
|
+
|
47
|
+
def record
|
48
|
+
@record || raise(NoRecordError, "a record hasn't been set", caller)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def set_record(record)
|
54
|
+
raise InvalidRecordError unless record.kind_of?(MR::Record)
|
55
|
+
@record = record
|
56
|
+
@record.model = self
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
InvalidRecordError = Class.new(ArgumentError)
|
64
|
+
NoRecordError = Class.new(RuntimeError)
|
65
|
+
NoRecordClassError = Class.new(RuntimeError)
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
require 'mr/model/configuration'
|
3
|
+
|
4
|
+
module MR; end
|
5
|
+
module MR::Model
|
6
|
+
|
7
|
+
module Fields
|
8
|
+
include MuchPlugin
|
9
|
+
|
10
|
+
plugin_included do
|
11
|
+
include MR::Model::Configuration
|
12
|
+
extend ClassMethods
|
13
|
+
include InstanceMethods
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
def fields
|
19
|
+
@fields ||= MR::Model::FieldSet.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def field_reader(*names)
|
23
|
+
names.each do |name|
|
24
|
+
self.fields.add_reader(name, self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def field_writer(*names)
|
29
|
+
names.each do |name|
|
30
|
+
self.fields.add_writer(name, self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def field_accessor(*names)
|
35
|
+
field_reader(*names)
|
36
|
+
field_writer(*names)
|
37
|
+
end
|
38
|
+
|
39
|
+
def field_names
|
40
|
+
self.fields.names
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
module InstanceMethods
|
46
|
+
|
47
|
+
def fields
|
48
|
+
self.class.fields.read_all(record)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fields=(values)
|
52
|
+
raise(ArgumentError, "must be a hash") unless values.kind_of?(Hash)
|
53
|
+
self.class.fields.batch_write(values, record)
|
54
|
+
rescue NoFieldError => exception
|
55
|
+
exception.set_backtrace(caller)
|
56
|
+
raise exception
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class FieldSet
|
64
|
+
include Enumerable
|
65
|
+
|
66
|
+
def initialize
|
67
|
+
@fields = {}
|
68
|
+
end
|
69
|
+
|
70
|
+
def names
|
71
|
+
@fields.keys
|
72
|
+
end
|
73
|
+
|
74
|
+
def find(name)
|
75
|
+
@fields[name.to_s] || raise(NoFieldError, "the '#{name}' field doesn't exist")
|
76
|
+
end
|
77
|
+
|
78
|
+
def get(name)
|
79
|
+
@fields[name.to_s] ||= Field.new(name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def each(&block)
|
83
|
+
@fields.values.each(&block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def read_all(record)
|
87
|
+
@fields.values.inject({}) do |h, field|
|
88
|
+
h.merge(field.name => field.read(record))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def batch_write(values, record)
|
93
|
+
values.each{ |name, value| find(name).write(value, record) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_reader(name, model_class)
|
97
|
+
get(name).define_reader_on(model_class)
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_writer(name, model_class)
|
101
|
+
get(name).define_writer_on(model_class)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def stringify_hash(hash)
|
107
|
+
hash.inject({}){ |h, (k, v)| h.merge(k.to_s => v) }
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
class Field
|
113
|
+
attr_reader :name
|
114
|
+
attr_reader :reader_method_name, :was_method_name, :changed_method_name
|
115
|
+
attr_reader :writer_method_name
|
116
|
+
|
117
|
+
def initialize(name)
|
118
|
+
@name = name.to_s
|
119
|
+
@reader_method_name = @name
|
120
|
+
@was_method_name = "#{@name}_was"
|
121
|
+
@changed_method_name = "#{@name}_changed?"
|
122
|
+
@writer_method_name = "#{@reader_method_name}="
|
123
|
+
@attribute_reader_method_name = @reader_method_name
|
124
|
+
@attribute_writer_method_name = @writer_method_name
|
125
|
+
@attribute_was_method_name = "#{@reader_method_name}_was"
|
126
|
+
@attribute_changed_method_name = "#{@reader_method_name}_changed?"
|
127
|
+
end
|
128
|
+
|
129
|
+
def read(record)
|
130
|
+
record.send(@attribute_reader_method_name)
|
131
|
+
end
|
132
|
+
|
133
|
+
def write(value, record)
|
134
|
+
record.send(@attribute_writer_method_name, value)
|
135
|
+
end
|
136
|
+
|
137
|
+
def was(record)
|
138
|
+
record.send(@attribute_was_method_name)
|
139
|
+
end
|
140
|
+
|
141
|
+
def changed?(record)
|
142
|
+
record.send(@attribute_changed_method_name)
|
143
|
+
end
|
144
|
+
|
145
|
+
def define_reader_on(model_class)
|
146
|
+
field = self
|
147
|
+
model_class.class_eval do
|
148
|
+
|
149
|
+
define_method(field.reader_method_name) do
|
150
|
+
field.read(record)
|
151
|
+
end
|
152
|
+
define_method(field.changed_method_name) do
|
153
|
+
field.changed?(record)
|
154
|
+
end
|
155
|
+
define_method(field.was_method_name) do
|
156
|
+
field.was(record)
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def define_writer_on(model_class)
|
163
|
+
field = self
|
164
|
+
model_class.class_eval do
|
165
|
+
|
166
|
+
define_method(field.writer_method_name) do |value|
|
167
|
+
field.write(value, record)
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
NoFieldError = Class.new(RuntimeError)
|
176
|
+
|
177
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/validations'
|
3
|
+
require 'much-plugin'
|
4
|
+
require 'mr/model/configuration'
|
5
|
+
|
6
|
+
module MR; end
|
7
|
+
module MR::Model
|
8
|
+
|
9
|
+
module Persistence
|
10
|
+
include MuchPlugin
|
11
|
+
|
12
|
+
plugin_included do
|
13
|
+
include MR::Model::Configuration
|
14
|
+
extend ClassMethods
|
15
|
+
include InstanceMethods
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def transaction(&block)
|
21
|
+
self.record_class.transaction(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module InstanceMethods
|
27
|
+
|
28
|
+
def save
|
29
|
+
self.transaction{ record.save! }
|
30
|
+
rescue ActiveRecord::RecordInvalid => exception
|
31
|
+
# `caller` is not consistent between 1.8 and 2.0, if we stop supporting
|
32
|
+
# older versions, we can switch to using `caller`
|
33
|
+
called_from = exception.backtrace[6..-1]
|
34
|
+
raise InvalidError.new(self, self.errors, called_from)
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy
|
38
|
+
record.destroy
|
39
|
+
end
|
40
|
+
|
41
|
+
def transaction(&block)
|
42
|
+
record.transaction(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def errors
|
46
|
+
record.errors.messages
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid?
|
50
|
+
record.valid?
|
51
|
+
end
|
52
|
+
|
53
|
+
def new?
|
54
|
+
record.new_record?
|
55
|
+
end
|
56
|
+
|
57
|
+
def destroyed?
|
58
|
+
record.destroyed?
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
class InvalidError < RuntimeError
|
66
|
+
attr_reader :errors
|
67
|
+
|
68
|
+
def initialize(model, errors, backtrace = nil)
|
69
|
+
@errors = errors || {}
|
70
|
+
desc = @errors.map do |(attribute, messages)|
|
71
|
+
messages.map{ |message| "#{attribute.inspect} #{message}" }
|
72
|
+
end.sort.join(', ')
|
73
|
+
super "Invalid #{model.class} couldn't be saved: #{desc}"
|
74
|
+
set_backtrace(backtrace) if backtrace
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/mr/query.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module MR
|
4
|
+
|
5
|
+
class Query
|
6
|
+
|
7
|
+
attr_reader :model_class, :relation
|
8
|
+
|
9
|
+
def initialize(model_class, relation)
|
10
|
+
@model_class = model_class
|
11
|
+
@relation = relation
|
12
|
+
end
|
13
|
+
|
14
|
+
def results
|
15
|
+
@results ||= self.results!
|
16
|
+
end
|
17
|
+
|
18
|
+
def results!
|
19
|
+
@results = self.relation.all.map{ |record| self.model_class.new(record) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def first
|
23
|
+
@first ||= self.first!
|
24
|
+
end
|
25
|
+
|
26
|
+
def first!
|
27
|
+
@first = if (record = self.relation.first)
|
28
|
+
self.model_class.new(record)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def count
|
33
|
+
@count ||= self.count!
|
34
|
+
end
|
35
|
+
|
36
|
+
def count!
|
37
|
+
@count = count_relation.count
|
38
|
+
end
|
39
|
+
|
40
|
+
def paged(page_num = nil, page_size = nil)
|
41
|
+
PagedQuery.new(self, page_num, page_size)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def count_relation
|
47
|
+
@count_relation ||= CountRelation.new(self.relation)
|
48
|
+
end
|
49
|
+
|
50
|
+
module CountRelation
|
51
|
+
def self.new(relation)
|
52
|
+
relation = relation.except(:select, :order)
|
53
|
+
if relation.group_values.empty?
|
54
|
+
relation
|
55
|
+
else
|
56
|
+
# use `SELECT 1` to count grouped results, this avoids trying to use
|
57
|
+
# a column which may not work because it's not part of the group by
|
58
|
+
# (Postgres errors if a column is selected that isn't part of the
|
59
|
+
# sql GROUP BY)
|
60
|
+
subquery = relation.select('1').to_sql
|
61
|
+
relation.klass.scoped.from("(#{subquery}) AS grouped_records")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class PagedQuery < Query
|
69
|
+
attr_reader :page_num, :page_size, :page_offset
|
70
|
+
|
71
|
+
def initialize(query, page_num = nil, page_size = nil)
|
72
|
+
@page_num = PageNumber.new(page_num)
|
73
|
+
@page_size = PageSize.new(page_size)
|
74
|
+
@page_offset = PageOffset.new(@page_num, @page_size)
|
75
|
+
|
76
|
+
@unpaged_relation = query.relation.dup
|
77
|
+
relation = query.relation.offset(@page_offset).limit(@page_size)
|
78
|
+
super query.model_class, relation
|
79
|
+
end
|
80
|
+
|
81
|
+
def total_count
|
82
|
+
@total_count ||= total_count!
|
83
|
+
end
|
84
|
+
|
85
|
+
# This isn't done in the `initialize` because it runs a query (which is
|
86
|
+
# expensive) and should only be done when it's needed. If it's never used
|
87
|
+
# then, running it in the `initialize` would be wasteful.
|
88
|
+
def total_count!
|
89
|
+
@total_count = total_count_relation.count
|
90
|
+
end
|
91
|
+
|
92
|
+
def has_next_page?
|
93
|
+
@has_next_page ||= (self.page_offset + self.page_size) < self.total_count
|
94
|
+
end
|
95
|
+
|
96
|
+
def is_last_page?
|
97
|
+
!self.has_next_page?
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def total_count_relation
|
103
|
+
@total_count_relation ||= CountRelation.new(@unpaged_relation)
|
104
|
+
end
|
105
|
+
|
106
|
+
module PageNumber
|
107
|
+
def self.new(number)
|
108
|
+
number && number.to_i > 0 ? number.to_i : 1
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
module PageSize
|
113
|
+
def self.new(number)
|
114
|
+
number && number.to_i > 0 ? number.to_i : 25
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module PageOffset
|
119
|
+
def self.new(page_number, page_size)
|
120
|
+
(page_number - 1) * page_size
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|