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,83 @@
1
+ require 'much-plugin'
2
+ require 'mr/read_model/data'
3
+ require 'mr/read_model/fields'
4
+ require 'mr/read_model/querying'
5
+
6
+ module MR
7
+
8
+ module ReadModel
9
+ include MuchPlugin
10
+
11
+ plugin_included do
12
+ include MR::ReadModelStruct
13
+ include MR::ReadModel::Querying
14
+ extend ClassMethods
15
+ end
16
+
17
+ def self.add_select_for_field(read_model_class, name, column_sql = nil, &column_sql_block)
18
+ if column_sql
19
+ read_model_class.select("#{column_sql} AS #{name}")
20
+ elsif column_sql_block
21
+ read_model_class.select do |params|
22
+ "#{column_sql_block.call(params)} AS #{name}"
23
+ end
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+
29
+ def field(name, type, column_sql = nil, &column_sql_block)
30
+ super(name, type)
31
+ MR::ReadModel.add_select_for_field(self, name, column_sql, &column_sql_block)
32
+ end
33
+
34
+ def json_struct_list(name, struct_class, column_sql = nil, &column_sql_block)
35
+ super(name, struct_class)
36
+ MR::ReadModel.add_select_for_field(self, name, column_sql, &column_sql_block)
37
+ end
38
+
39
+ def json_struct_obj(name, struct_class, column_sql = nil, &column_sql_block)
40
+ super(name, struct_class)
41
+ MR::ReadModel.add_select_for_field(self, name, column_sql, &column_sql_block)
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
48
+ module ReadModelStruct
49
+ include MuchPlugin
50
+
51
+ plugin_included do
52
+ include MR::ReadModel::Data
53
+ include MR::ReadModel::Fields
54
+ include InstanceMethods
55
+ end
56
+
57
+ module InstanceMethods
58
+
59
+ def initialize(data = nil)
60
+ set_read_model_data(data || {})
61
+ end
62
+
63
+ def ==(other)
64
+ if other.kind_of?(self.class)
65
+ self.fields == other.fields
66
+ else
67
+ super
68
+ end
69
+ end
70
+
71
+ def inspect
72
+ object_hex = (self.object_id << 1).to_s(16)
73
+ fields_inspect = self.class.fields.map do |field|
74
+ "#{field.ivar_name}=#{field.read(self.read_model_data).inspect}"
75
+ end.sort.join(" ")
76
+ "#<#{self.class}:0x#{object_hex} #{fields_inspect}>"
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ end
@@ -0,0 +1,38 @@
1
+ module MR; end
2
+ module MR::ReadModel
3
+
4
+ module Data
5
+
6
+ # `MR::ReadModel::Data` is a mixin that provides helpers for setting and
7
+ # accessing the "data" for a read model. These methods provide a strict
8
+ # interface to avoid confusing errors and ensure that the data for a
9
+ # read model should, as much as possible, work.
10
+ #
11
+ # * Use the `read_model_data` protected method to access the data object.
12
+ # * Use the `set_read_model_data` private method to write a data object.
13
+
14
+ protected
15
+
16
+ def read_model_data
17
+ @read_model_data || raise(NoDataError.new(caller))
18
+ end
19
+
20
+ private
21
+
22
+ def set_read_model_data(data)
23
+ raise InvalidDataError unless data.respond_to?(:[])
24
+ @read_model_data = data
25
+ end
26
+
27
+ end
28
+
29
+ InvalidDataError = Class.new(ArgumentError)
30
+
31
+ class NoDataError < RuntimeError
32
+ def initialize(called_from = nil)
33
+ super "the read model's data hasn't been set"
34
+ set_backtrace(called_from) if called_from
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,218 @@
1
+ require 'much-plugin'
2
+ require 'mr/json_field'
3
+ require 'mr/read_model/data'
4
+ require 'mr/type_converter'
5
+
6
+ module MR; end
7
+ module MR::ReadModel
8
+
9
+ module Fields
10
+ include MuchPlugin
11
+
12
+ plugin_included do
13
+ include MR::ReadModel::Data
14
+ extend ClassMethods
15
+ include InstanceMethods
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ def fields
21
+ @fields ||= MR::ReadModel::FieldSet.new
22
+ end
23
+
24
+ def field(name, type)
25
+ fields.add(name, type, self)
26
+ rescue InvalidFieldTypeError => exception
27
+ raise ArgumentError, exception.message, caller
28
+ end
29
+
30
+ def json_struct_list(name, struct_class)
31
+ field = JsonStructListField.new(self, name.to_s, struct_class)
32
+ ivar_name = "@#{name}"
33
+
34
+ define_method(name) do
35
+ begin
36
+ instance_variable_get(ivar_name) ||
37
+ instance_variable_set(ivar_name, field.reader(self.read_model_data))
38
+ rescue StandardError => exception
39
+ exception.set_backtrace(caller)
40
+ raise exception
41
+ end
42
+ end
43
+
44
+ (self.json_struct_lists << field).uniq!
45
+ end
46
+
47
+ def json_struct_lists
48
+ @json_struct_lists ||= []
49
+ end
50
+
51
+ def json_struct_obj(name, struct_class)
52
+ field = JsonStructObjField.new(self, name.to_s, struct_class)
53
+ ivar_name = "@#{name}"
54
+
55
+ define_method(name) do
56
+ begin
57
+ instance_variable_get(ivar_name) ||
58
+ instance_variable_set(ivar_name, field.reader(self.read_model_data))
59
+ rescue StandardError => exception
60
+ exception.set_backtrace(caller)
61
+ raise exception
62
+ end
63
+ end
64
+
65
+ (self.json_struct_objs << field).uniq!
66
+ end
67
+
68
+ def json_struct_objs
69
+ @json_struct_objs ||= []
70
+ end
71
+
72
+ end
73
+
74
+ module InstanceMethods
75
+
76
+ def fields
77
+ @fields ||= begin
78
+ fields = self.class.fields.read_all(self.read_model_data)
79
+ ( self.class.json_struct_lists +
80
+ self.class.json_struct_objs
81
+ ).inject(fields) do |h, field|
82
+ h.merge!(field.name => self.send(field.name))
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+
91
+ class FieldSet
92
+ include Enumerable
93
+
94
+ def initialize
95
+ @fields = []
96
+ end
97
+
98
+ def find(name)
99
+ @fields.detect{ |f| f.name == name.to_s }
100
+ end
101
+
102
+ def read_all(data)
103
+ inject({}) do |h, field|
104
+ h.merge(field.name => field.read(data))
105
+ end
106
+ end
107
+
108
+ def add(name, type, model_class = nil)
109
+ @fields << Field.new(name, type).tap do |field|
110
+ field.define_on(model_class) if model_class
111
+ end
112
+ end
113
+
114
+ def each(&block)
115
+ @fields.each(&block)
116
+ end
117
+
118
+ end
119
+
120
+ class Field
121
+ attr_reader :name, :type, :value
122
+ attr_reader :method_name, :ivar_name
123
+
124
+ def initialize(name, type)
125
+ @name = name.to_s
126
+ @type = type.to_sym
127
+ unless MR::TypeConverter.valid?(@type)
128
+ raise InvalidFieldTypeError, "`#{@type}` is not a valid field type"
129
+ end
130
+
131
+ @method_name = @name
132
+ @ivar_name = "@#{@name}"
133
+ @attribute_name = @name
134
+ @converter = nil
135
+ end
136
+
137
+ def read(data)
138
+ @converter ||= MR::TypeConverter.new(determine_ar_column_class(data))
139
+ @converter.send(@type, data[@attribute_name])
140
+ end
141
+
142
+ def define_on(model_class)
143
+ field = self
144
+ model_class.class_eval do
145
+
146
+ define_method(field.method_name) do
147
+ instance_variable_get(field.ivar_name) ||
148
+ instance_variable_set(field.ivar_name, field.read(self.read_model_data))
149
+ end
150
+
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def determine_ar_column_class(data)
157
+ data.class.columns.first.class if data.class.respond_to?(:columns)
158
+ end
159
+
160
+ end
161
+
162
+ class JsonStructField < Struct.new(:read_model_class, :name, :struct_class_name)
163
+
164
+ def struct_class
165
+ @struct_class ||= if self.struct_class_name.kind_of?(String)
166
+ names = self.struct_class_name.to_s.split('::').reject{ |name| name.empty? }
167
+ namespace = self.struct_class_name =~ /^::/ ? Object : self.read_model_class
168
+ names.inject(namespace){ |o, name| o.const_get(name) }
169
+ else
170
+ self.struct_class_name
171
+ end
172
+ end
173
+
174
+ def reader(read_model_data)
175
+ json_value = read_model_data[self.name]
176
+ return json_value if json_value.nil?
177
+
178
+ if json_value.kind_of?(String)
179
+ begin
180
+ return MR::JsonField.decode(json_value)
181
+ rescue MR::JsonField::InvalidJSONError => exception
182
+ message = "can't decode `#{self.name}` JSON: #{exception.message}"
183
+ raise exception.class, message, exception.backtrace
184
+ end
185
+ else
186
+ return json_value
187
+ end
188
+ end
189
+
190
+ end
191
+
192
+ class JsonStructListField < JsonStructField
193
+
194
+ def reader(read_model_data)
195
+ if !(json_datas = super(read_model_data)).nil?
196
+ json_datas.map{ |data| self.struct_class.new(data) }
197
+ else
198
+ nil
199
+ end
200
+ end
201
+
202
+ end
203
+
204
+ class JsonStructObjField < JsonStructField
205
+
206
+ def reader(read_model_data)
207
+ if !(json_data = super(read_model_data)).nil?
208
+ self.struct_class.new(json_data)
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ InvalidFieldTypeError = Class.new(RuntimeError)
217
+
218
+ end
@@ -0,0 +1,188 @@
1
+ require 'mr/read_model/subquery'
2
+
3
+ module MR::ReadModel
4
+
5
+ module QueryExpression
6
+ def self.new(type, *args, &block)
7
+ if !args.empty?
8
+ StaticQueryExpression.new(type, *args)
9
+ elsif block
10
+ DynamicQueryExpression.new(type, &block)
11
+ else
12
+ raise InvalidQueryExpressionError, "must be passed args or a block"
13
+ end
14
+ end
15
+ end
16
+
17
+ class StaticQueryExpression
18
+ attr_reader :type, :args
19
+
20
+ def initialize(type, *args)
21
+ @type = type
22
+ @args = args
23
+ end
24
+
25
+ # `apply_to` has to take params that it ignores, so it has the same
26
+ # interface as `DynamicQueryExpression` (which actually uses the params)
27
+ def apply_to(relation, params = nil)
28
+ relation.send(@type, *@args)
29
+ end
30
+ end
31
+
32
+ class DynamicQueryExpression
33
+ attr_reader :type, :block
34
+
35
+ def initialize(type, &block)
36
+ @type = type
37
+ @block = block
38
+ end
39
+
40
+ def apply_to(relation, params)
41
+ relation.send(@type, relation.instance_exec(params, &@block))
42
+ end
43
+ end
44
+
45
+ class MergeQueryExpression
46
+ attr_accessor :type, :query_expression
47
+
48
+ def initialize(type, *args, &block)
49
+ @type = type
50
+ @query_expression = QueryExpression.new(:merge, *args, &block)
51
+ end
52
+
53
+ def apply_to(relation, params = nil)
54
+ @query_expression.apply_to(relation, params)
55
+ end
56
+ end
57
+
58
+ class SubqueryExpression
59
+ TYPES = {
60
+ :joins => JoinSubquery
61
+ }.freeze
62
+
63
+ attr_reader :subquery_type, :subquery_args, :subquery_block
64
+ alias :type :subquery_type
65
+
66
+ def initialize(type, *args, &block)
67
+ raise ArgumentError, "a block must be provided" unless block
68
+ @subquery_type = type
69
+ @subquery_args = args
70
+ @subquery_block = block
71
+ end
72
+
73
+ def subquery
74
+ @subquery ||= TYPES[self.subquery_type].new(
75
+ *self.subquery_args,
76
+ &self.subquery_block
77
+ )
78
+ end
79
+
80
+ def apply_to(relation, params = nil)
81
+ relation.send(self.type, self.subquery.build_sql(params))
82
+ end
83
+ end
84
+
85
+ class FromExpression
86
+ attr_reader :record_class
87
+
88
+ def initialize(record_class)
89
+ raise ArgumentError, "must be passed a MR::Record" unless record_class < MR::Record
90
+ @record_class = record_class
91
+ end
92
+
93
+ def default_find_attr
94
+ @default_find_attr ||= [
95
+ self.record_class.table_name,
96
+ self.record_class.primary_key
97
+ ].join('.')
98
+ end
99
+
100
+ def ar_relation(params = nil)
101
+ self.record_class.scoped
102
+ end
103
+ end
104
+
105
+ class FromSubqueryExpression
106
+ attr_reader :subquery_block
107
+
108
+ def initialize(&block)
109
+ raise ArgumentError, "a block must be provided" unless block
110
+ @subquery_block = block
111
+ end
112
+
113
+ # lazy-build `FromSubquery` so its block is not evaluated until the read
114
+ # model `query` method is called, cache it so it doesn't have to be rebuilt
115
+ # for every `query` call
116
+ def from_subquery
117
+ @from_subquery ||= FromSubquery.new(&self.subquery_block)
118
+ end
119
+
120
+ def record_class
121
+ self.from_subquery.record_class
122
+ end
123
+
124
+ def default_find_attr
125
+ raise NoFindAttrError, "a `find_attr` has to be specified " \
126
+ "when using a from subquery"
127
+ end
128
+
129
+ def ar_relation(params = nil)
130
+ self.record_class.scoped.from(self.from_subquery.build_sql(params))
131
+ end
132
+ end
133
+
134
+ class NullFromExpression
135
+ def record_class; raise NoFromExpressionError; end
136
+ def default_find_attr; raise NoFromExpressionError; end
137
+ def ar_relation(params = nil); raise NoFromExpressionError; end
138
+ end
139
+
140
+ class SetExpression
141
+ OPERATOR_SQL = {
142
+ :union => 'UNION'.freeze,
143
+ :union_all => 'UNION ALL'.freeze,
144
+ :intersect => 'INTERSECT'.freeze,
145
+ :intersect_all => 'INTERSECT ALL'.freeze,
146
+ :except => 'EXCEPT'.freeze,
147
+ :except_all => 'EXCEPT ALL'.freeze
148
+ }.freeze
149
+
150
+ attr_reader :operator_sql, :read_model_block
151
+
152
+ def initialize(operator, &block)
153
+ raise ArgumentError, "a block must be provided" unless block
154
+ @operator_sql = OPERATOR_SQL[operator]
155
+ @read_model_block = block
156
+ end
157
+
158
+ def read_model_class
159
+ @read_model_class ||= begin
160
+ c = Class.new do
161
+ # TODO - fix circular require
162
+ require 'mr/read_model'
163
+ require 'mr/read_model/set_querying'
164
+ include MR::ReadModel
165
+ include MR::ReadModel::SetQuerying
166
+ end
167
+ c.class_eval(&self.read_model_block)
168
+ c
169
+ end
170
+ end
171
+
172
+ def combine_sql(sql, params = nil)
173
+ "#{sql} #{self.operator_sql} #{self.read_model_class.build_sql(params)}"
174
+ end
175
+ end
176
+
177
+ InvalidQueryExpressionError = Class.new(RuntimeError)
178
+
179
+ NoFindAttrError = Class.new(RuntimeError)
180
+
181
+ class NoFromExpressionError < RuntimeError
182
+ def initialize
183
+ super "a from expression hasn't been set - " \
184
+ "set one using `from` or `from_subquery`"
185
+ end
186
+ end
187
+
188
+ end