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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE +22 -0
  5. data/README.md +29 -0
  6. data/bench/all.rb +4 -0
  7. data/bench/factory.rb +68 -0
  8. data/bench/fake_record.rb +174 -0
  9. data/bench/model.rb +201 -0
  10. data/bench/read_model.rb +191 -0
  11. data/bench/results/factory.txt +21 -0
  12. data/bench/results/fake_record.txt +37 -0
  13. data/bench/results/model.txt +44 -0
  14. data/bench/results/read_model.txt +46 -0
  15. data/bench/setup.rb +132 -0
  16. data/lib/mr.rb +11 -0
  17. data/lib/mr/after_commit.rb +49 -0
  18. data/lib/mr/after_commit/fake_record.rb +39 -0
  19. data/lib/mr/after_commit/record.rb +48 -0
  20. data/lib/mr/after_commit/record_procs_methods.rb +82 -0
  21. data/lib/mr/factory.rb +82 -0
  22. data/lib/mr/factory/config.rb +240 -0
  23. data/lib/mr/factory/model_factory.rb +103 -0
  24. data/lib/mr/factory/model_stack.rb +28 -0
  25. data/lib/mr/factory/read_model_factory.rb +104 -0
  26. data/lib/mr/factory/record_factory.rb +130 -0
  27. data/lib/mr/factory/record_stack.rb +219 -0
  28. data/lib/mr/fake_query.rb +53 -0
  29. data/lib/mr/fake_record.rb +58 -0
  30. data/lib/mr/fake_record/associations.rb +257 -0
  31. data/lib/mr/fake_record/attributes.rb +168 -0
  32. data/lib/mr/fake_record/persistence.rb +116 -0
  33. data/lib/mr/json_field.rb +180 -0
  34. data/lib/mr/json_field/fake_record.rb +31 -0
  35. data/lib/mr/json_field/record.rb +38 -0
  36. data/lib/mr/model.rb +67 -0
  37. data/lib/mr/model/associations.rb +161 -0
  38. data/lib/mr/model/configuration.rb +67 -0
  39. data/lib/mr/model/fields.rb +177 -0
  40. data/lib/mr/model/persistence.rb +79 -0
  41. data/lib/mr/query.rb +126 -0
  42. data/lib/mr/read_model.rb +83 -0
  43. data/lib/mr/read_model/data.rb +38 -0
  44. data/lib/mr/read_model/fields.rb +218 -0
  45. data/lib/mr/read_model/query_expression.rb +188 -0
  46. data/lib/mr/read_model/querying.rb +214 -0
  47. data/lib/mr/read_model/set_querying.rb +82 -0
  48. data/lib/mr/read_model/subquery.rb +98 -0
  49. data/lib/mr/record.rb +35 -0
  50. data/lib/mr/test_helpers.rb +229 -0
  51. data/lib/mr/type_converter.rb +85 -0
  52. data/lib/mr/version.rb +3 -0
  53. data/log/.gitkeep +0 -0
  54. data/mr.gemspec +29 -0
  55. data/test/helper.rb +21 -0
  56. data/test/support/db.rb +10 -0
  57. data/test/support/factory.rb +13 -0
  58. data/test/support/factory/area.rb +6 -0
  59. data/test/support/factory/comment.rb +14 -0
  60. data/test/support/factory/image.rb +6 -0
  61. data/test/support/factory/user.rb +6 -0
  62. data/test/support/models/area.rb +58 -0
  63. data/test/support/models/comment.rb +60 -0
  64. data/test/support/models/image.rb +53 -0
  65. data/test/support/models/user.rb +96 -0
  66. data/test/support/read_model/querying.rb +150 -0
  67. data/test/support/read_models/comment_with_user_data.rb +27 -0
  68. data/test/support/read_models/set_data.rb +49 -0
  69. data/test/support/read_models/subquery_data.rb +41 -0
  70. data/test/support/read_models/user_with_area_data.rb +15 -0
  71. data/test/support/schema.rb +39 -0
  72. data/test/support/setup_test_db.rb +10 -0
  73. data/test/system/factory/model_factory_tests.rb +87 -0
  74. data/test/system/factory/model_stack_tests.rb +30 -0
  75. data/test/system/factory/record_factory_tests.rb +84 -0
  76. data/test/system/factory/record_stack_tests.rb +51 -0
  77. data/test/system/factory_tests.rb +32 -0
  78. data/test/system/read_model_tests.rb +199 -0
  79. data/test/system/with_model_tests.rb +275 -0
  80. data/test/unit/after_commit/fake_record_tests.rb +110 -0
  81. data/test/unit/after_commit/record_procs_methods_tests.rb +177 -0
  82. data/test/unit/after_commit/record_tests.rb +134 -0
  83. data/test/unit/after_commit_tests.rb +113 -0
  84. data/test/unit/factory/config_tests.rb +651 -0
  85. data/test/unit/factory/model_factory_tests.rb +473 -0
  86. data/test/unit/factory/model_stack_tests.rb +97 -0
  87. data/test/unit/factory/read_model_factory_tests.rb +195 -0
  88. data/test/unit/factory/record_factory_tests.rb +446 -0
  89. data/test/unit/factory/record_stack_tests.rb +549 -0
  90. data/test/unit/factory_tests.rb +213 -0
  91. data/test/unit/fake_query_tests.rb +137 -0
  92. data/test/unit/fake_record/associations_tests.rb +585 -0
  93. data/test/unit/fake_record/attributes_tests.rb +265 -0
  94. data/test/unit/fake_record/persistence_tests.rb +239 -0
  95. data/test/unit/fake_record_tests.rb +106 -0
  96. data/test/unit/json_field/fake_record_tests.rb +75 -0
  97. data/test/unit/json_field/record_tests.rb +80 -0
  98. data/test/unit/json_field_tests.rb +302 -0
  99. data/test/unit/model/associations_tests.rb +346 -0
  100. data/test/unit/model/configuration_tests.rb +92 -0
  101. data/test/unit/model/fields_tests.rb +278 -0
  102. data/test/unit/model/persistence_tests.rb +114 -0
  103. data/test/unit/model_tests.rb +137 -0
  104. data/test/unit/query_tests.rb +300 -0
  105. data/test/unit/read_model/data_tests.rb +56 -0
  106. data/test/unit/read_model/fields_tests.rb +416 -0
  107. data/test/unit/read_model/query_expression_tests.rb +381 -0
  108. data/test/unit/read_model/querying_tests.rb +613 -0
  109. data/test/unit/read_model/set_querying_tests.rb +149 -0
  110. data/test/unit/read_model/subquery_tests.rb +242 -0
  111. data/test/unit/read_model_tests.rb +187 -0
  112. data/test/unit/record_tests.rb +45 -0
  113. data/test/unit/test_helpers_tests.rb +431 -0
  114. data/test/unit/type_converter_tests.rb +207 -0
  115. 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
@@ -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