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,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