mr 0.35.2

Sign up to get free protection for your applications and to get access to all the features.
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