mongoid-history 0.6.1 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -1
  3. data/.document +5 -5
  4. data/.github/workflows/test.yml +72 -0
  5. data/.gitignore +46 -46
  6. data/.rspec +2 -2
  7. data/.rubocop.yml +6 -6
  8. data/.rubocop_todo.yml +99 -92
  9. data/CHANGELOG.md +173 -130
  10. data/CONTRIBUTING.md +117 -118
  11. data/Dangerfile +1 -1
  12. data/Gemfile +49 -35
  13. data/LICENSE.txt +20 -20
  14. data/README.md +609 -531
  15. data/RELEASING.md +66 -0
  16. data/Rakefile +24 -24
  17. data/UPGRADING.md +53 -0
  18. data/lib/mongoid/history/attributes/base.rb +72 -72
  19. data/lib/mongoid/history/attributes/create.rb +45 -50
  20. data/lib/mongoid/history/attributes/destroy.rb +34 -34
  21. data/lib/mongoid/history/attributes/update.rb +104 -43
  22. data/lib/mongoid/history/options.rb +177 -184
  23. data/lib/mongoid/history/trackable.rb +588 -501
  24. data/lib/mongoid/history/tracker.rb +247 -238
  25. data/lib/mongoid/history/version.rb +5 -5
  26. data/lib/mongoid/history.rb +77 -52
  27. data/lib/mongoid-history.rb +1 -1
  28. data/mongoid-history.gemspec +25 -25
  29. data/perf/benchmark_modified_attributes_for_create.rb +65 -0
  30. data/perf/gc_suite.rb +21 -0
  31. data/spec/integration/embedded_in_polymorphic_spec.rb +112 -135
  32. data/spec/integration/integration_spec.rb +976 -936
  33. data/spec/integration/multi_relation_spec.rb +47 -53
  34. data/spec/integration/multiple_trackers_spec.rb +68 -0
  35. data/spec/integration/nested_embedded_documents_spec.rb +64 -84
  36. data/spec/integration/nested_embedded_documents_tracked_in_parent_spec.rb +124 -0
  37. data/spec/integration/nested_embedded_polymorphic_documents_spec.rb +115 -127
  38. data/spec/integration/subclasses_spec.rb +47 -29
  39. data/spec/integration/track_history_order_spec.rb +84 -0
  40. data/spec/integration/validation_failure_spec.rb +76 -0
  41. data/spec/spec_helper.rb +32 -25
  42. data/spec/support/error_helpers.rb +7 -0
  43. data/spec/support/mongoid.rb +11 -11
  44. data/spec/support/mongoid_history.rb +12 -13
  45. data/spec/unit/attributes/base_spec.rb +141 -150
  46. data/spec/unit/attributes/create_spec.rb +342 -315
  47. data/spec/unit/attributes/destroy_spec.rb +228 -218
  48. data/spec/unit/attributes/update_spec.rb +342 -210
  49. data/spec/unit/callback_options_spec.rb +165 -0
  50. data/spec/unit/embedded_methods_spec.rb +87 -69
  51. data/spec/unit/history_spec.rb +58 -35
  52. data/spec/unit/my_instance_methods_spec.rb +555 -485
  53. data/spec/unit/options_spec.rb +365 -326
  54. data/spec/unit/singleton_methods_spec.rb +406 -338
  55. data/spec/unit/store/default_store_spec.rb +11 -11
  56. data/spec/unit/store/request_store_spec.rb +13 -13
  57. data/spec/unit/trackable_spec.rb +1057 -586
  58. data/spec/unit/tracker_spec.rb +190 -163
  59. metadata +25 -10
  60. data/.travis.yml +0 -35
@@ -1,184 +1,177 @@
1
- module Mongoid
2
- module History
3
- class Options
4
- attr_reader :trackable, :options
5
-
6
- def initialize(trackable)
7
- @trackable = trackable
8
- end
9
-
10
- def scope
11
- trackable.collection_name.to_s.singularize.to_sym
12
- end
13
-
14
- def parse(options = {})
15
- @options = default_options.merge(options)
16
- prepare_skipped_fields
17
- prepare_formatted_fields
18
- parse_tracked_fields_and_relations
19
- @options
20
- end
21
-
22
- private
23
-
24
- def default_options
25
- @default_options ||=
26
- { on: :all,
27
- except: [:created_at, :updated_at],
28
- tracker_class_name: nil,
29
- modifier_field: :modifier,
30
- version_field: :version,
31
- changes_method: :changes,
32
- scope: scope,
33
- track_create: false,
34
- track_update: true,
35
- track_destroy: false,
36
- format: nil }
37
- end
38
-
39
- # Sets the :except attributes and relations in `options` to be an [ Array <String> ]
40
- # The attribute names and relations are stored by their `database_field_name`s
41
- # Removes the `nil` and duplicate entries from skipped attributes/relations list
42
- def prepare_skipped_fields
43
- # normalize :except fields to an array of database field strings
44
- @options[:except] = Array(options[:except])
45
- @options[:except] = options[:except].map { |field| trackable.database_field_name(field) }.compact.uniq
46
- end
47
-
48
- def prepare_formatted_fields
49
- formats = {}
50
-
51
- if options[:format].class == Hash
52
- options[:format].each do |field, format|
53
- next if field.nil?
54
-
55
- field = trackable.database_field_name(field)
56
-
57
- if format.class == Hash && trackable.embeds_many?(field)
58
- relation_class = trackable.embeds_many_class(field)
59
- formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
60
- elsif format.class == Hash && trackable.embeds_one?(field)
61
- relation_class = trackable.embeds_one_class(field)
62
- formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
63
- else
64
- formats[field] = format
65
- end
66
- end
67
- end
68
-
69
- options[:format] = formats
70
- end
71
-
72
- def parse_tracked_fields_and_relations
73
- # case `options[:on]`
74
- # when `posts: [:id, :title]`, then it will convert it to `[[:posts, [:id, :title]]]`
75
- # when `:foo`, then `[:foo]`
76
- # when `[:foo, { posts: [:id, :title] }]`, then return as is
77
- @options[:on] = Array(options[:on])
78
-
79
- # # :all is just an alias to :fields for now, to support existing users of `mongoid-history`
80
- # # In future, :all will track all the fields and relations of trackable class
81
- if options[:on].include?(:all)
82
- warn "[DEPRECATION] Use :fields instead of :all to track all fields in class #{trackable}.\n\
83
- Going forward, :all will track all the fields and relations for the class"
84
- end
85
-
86
- @options[:on] = options[:on].map { |opt| (opt == :all) ? :fields : opt }
87
-
88
- if options[:on].include?(:fields)
89
- @options[:on] = options[:on].reject { |opt| opt == :fields }
90
- @options[:on] = options[:on] | trackable.fields.keys.map(&:to_sym) - reserved_fields.map(&:to_sym)
91
- end
92
-
93
- if options[:on].include?(:embedded_relations)
94
- @options[:on] = options[:on].reject { |opt| opt == :embedded_relations }
95
- @options[:on] = options[:on] | trackable.embedded_relations.keys
96
- end
97
-
98
- @options[:fields] = []
99
- @options[:dynamic] = []
100
- @options[:relations] = { embeds_one: {}, embeds_many: {} }
101
-
102
- options[:on].each do |option|
103
- field = get_database_field_name(option)
104
- field_options = get_field_options(option)
105
- categorize_tracked_option(field, field_options)
106
- end
107
- end
108
-
109
- # Returns the database_field_name key for tracked option
110
- #
111
- # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
112
- #
113
- # @return [ String ] the database field name for tracked option
114
- def get_database_field_name(option)
115
- key = if option.is_a?(Hash)
116
- option.keys.first
117
- elsif option.is_a?(Array)
118
- option.first
119
- end
120
- trackable.database_field_name(key || option)
121
- end
122
-
123
- # Returns the tracked attributes for embedded relations, otherwise `nil`
124
- #
125
- # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
126
- #
127
- # @return [ nil | Array <String | Symbol> ] the list of tracked fields for embedded relation
128
- def get_field_options(option)
129
- if option.is_a?(Hash)
130
- option.values.first
131
- elsif option.is_a?(Array)
132
- option.last
133
- end
134
- end
135
-
136
- # Tracks the passed option under:
137
- # `fields`
138
- # `dynamic`
139
- # `relations -> embeds_one` or
140
- # `relations -> embeds_many`
141
- #
142
- # @param [ String ] field The database field name of field or relation to track
143
- # @param [ nil | Array <String | Symbol> ] field_options The tracked fields for embedded relations
144
- def categorize_tracked_option(field, field_options = nil)
145
- return if options[:except].include?(field)
146
- return if reserved_fields.include?(field)
147
-
148
- field_options = Array(field_options)
149
-
150
- if trackable.embeds_one?(field)
151
- track_embeds_one(field, field_options)
152
- elsif trackable.embeds_many?(field)
153
- track_embeds_many(field, field_options)
154
- elsif trackable.fields.keys.include?(field)
155
- @options[:fields] << field
156
- else
157
- @options[:dynamic] << field
158
- end
159
- end
160
-
161
- def track_embeds_one(field, field_options)
162
- relation_class = trackable.embeds_one_class(field)
163
- @options[:relations][:embeds_one][field] = if field_options.blank?
164
- relation_class.fields.keys
165
- else
166
- %w(_id) | field_options.map { |opt| relation_class.database_field_name(opt) }
167
- end
168
- end
169
-
170
- def track_embeds_many(field, field_options)
171
- relation_class = trackable.embeds_many_class(field)
172
- @options[:relations][:embeds_many][field] = if field_options.blank?
173
- relation_class.fields.keys
174
- else
175
- %w(_id) | field_options.map { |opt| relation_class.database_field_name(opt) }
176
- end
177
- end
178
-
179
- def reserved_fields
180
- @reserved_fields ||= ['_id', '_type', options[:version_field].to_s, "#{options[:modifier_field]}_id"]
181
- end
182
- end
183
- end
184
- end
1
+ module Mongoid
2
+ module History
3
+ class Options
4
+ attr_reader :trackable, :options
5
+
6
+ def initialize(trackable, opts = {})
7
+ @trackable = trackable
8
+ @options = default_options.merge(opts)
9
+ end
10
+
11
+ def scope
12
+ trackable.collection_name.to_s.singularize.to_sym
13
+ end
14
+
15
+ def prepared
16
+ return @prepared if @prepared
17
+ @prepared = options.dup
18
+ prepare_skipped_fields
19
+ prepare_formatted_fields
20
+ parse_tracked_fields_and_relations
21
+ @prepared
22
+ end
23
+
24
+ private
25
+
26
+ def default_options
27
+ { on: :all,
28
+ except: %i[created_at updated_at],
29
+ tracker_class_name: nil,
30
+ modifier_field: :modifier,
31
+ version_field: :version,
32
+ changes_method: :changes,
33
+ scope: scope,
34
+ track_create: true,
35
+ track_update: true,
36
+ track_destroy: true,
37
+ format: nil }
38
+ end
39
+
40
+ # Sets the :except attributes and relations in `options` to be an [ Array <String> ]
41
+ # The attribute names and relations are stored by their `database_field_name`s
42
+ # Removes the `nil` and duplicate entries from skipped attributes/relations list
43
+ def prepare_skipped_fields
44
+ # normalize :except fields to an array of database field strings
45
+ @prepared[:except] = Array(@prepared[:except])
46
+ @prepared[:except] = @prepared[:except].map { |field| trackable.database_field_name(field) }.compact.uniq
47
+ end
48
+
49
+ def prepare_formatted_fields
50
+ formats = {}
51
+
52
+ if @prepared[:format].class == Hash
53
+ @prepared[:format].each do |field, format|
54
+ next if field.nil?
55
+
56
+ field = trackable.database_field_name(field)
57
+
58
+ if format.class == Hash && trackable.embeds_many?(field)
59
+ relation_class = trackable.relation_class_of(field)
60
+ formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
61
+ elsif format.class == Hash && trackable.embeds_one?(field)
62
+ relation_class = trackable.relation_class_of(field)
63
+ formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
64
+ else
65
+ formats[field] = format
66
+ end
67
+ end
68
+ end
69
+
70
+ @prepared[:format] = formats
71
+ end
72
+
73
+ def parse_tracked_fields_and_relations
74
+ # case `options[:on]`
75
+ # when `posts: [:id, :title]`, then it will convert it to `[[:posts, [:id, :title]]]`
76
+ # when `:foo`, then `[:foo]`
77
+ # when `[:foo, { posts: [:id, :title] }]`, then return as is
78
+ @prepared[:on] = Array(@prepared[:on])
79
+
80
+ @prepared[:on] = @prepared[:on].map { |opt| opt == :all ? :fields : opt }
81
+
82
+ if @prepared[:on].include?(:fields)
83
+ @prepared[:on] = @prepared[:on].reject { |opt| opt == :fields }
84
+ @prepared[:on] = @prepared[:on] | trackable.fields.keys.map(&:to_sym) - reserved_fields.map(&:to_sym)
85
+ end
86
+
87
+ if @prepared[:on].include?(:embedded_relations)
88
+ @prepared[:on] = @prepared[:on].reject { |opt| opt == :embedded_relations }
89
+ @prepared[:on] = @prepared[:on] | trackable.embedded_relations.keys
90
+ end
91
+
92
+ @prepared[:fields] = []
93
+ @prepared[:dynamic] = []
94
+ @prepared[:relations] = { embeds_one: {}, embeds_many: {} }
95
+
96
+ @prepared[:on].each do |option|
97
+ if option.is_a?(Hash)
98
+ option.each { |k, v| split_and_categorize(k => v) }
99
+ else
100
+ split_and_categorize(option)
101
+ end
102
+ end
103
+ end
104
+
105
+ def split_and_categorize(field_and_options)
106
+ field = get_database_field_name(field_and_options)
107
+ field_options = get_field_options(field_and_options)
108
+ categorize_tracked_option(field, field_options)
109
+ end
110
+
111
+ # Returns the database_field_name key for tracked option
112
+ #
113
+ # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
114
+ #
115
+ # @return [ String ] the database field name for tracked option
116
+ def get_database_field_name(option)
117
+ key = if option.is_a?(Hash)
118
+ option.keys.first
119
+ elsif option.is_a?(Array)
120
+ option.first
121
+ end
122
+ trackable.database_field_name(key || option)
123
+ end
124
+
125
+ # Returns the tracked attributes for embedded relations, otherwise `nil`
126
+ #
127
+ # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
128
+ #
129
+ # @return [ nil | Array <String | Symbol> ] the list of tracked fields for embedded relation
130
+ def get_field_options(option)
131
+ if option.is_a?(Hash)
132
+ option.values.first
133
+ elsif option.is_a?(Array)
134
+ option.last
135
+ end
136
+ end
137
+
138
+ # Tracks the passed option under:
139
+ # `fields`
140
+ # `dynamic`
141
+ # `relations -> embeds_one` or
142
+ # `relations -> embeds_many`
143
+ #
144
+ # @param [ String ] field The database field name of field or relation to track
145
+ # @param [ nil | Array <String | Symbol> ] field_options The tracked fields for embedded relations
146
+ def categorize_tracked_option(field, field_options = nil)
147
+ return if @prepared[:except].include?(field)
148
+ return if reserved_fields.include?(field)
149
+
150
+ field_options = Array(field_options)
151
+
152
+ if trackable.embeds_one?(field)
153
+ track_relation(field, :embeds_one, field_options)
154
+ elsif trackable.embeds_many?(field)
155
+ track_relation(field, :embeds_many, field_options)
156
+ elsif trackable.fields.keys.include?(field)
157
+ @prepared[:fields] << field
158
+ else
159
+ @prepared[:dynamic] << field
160
+ end
161
+ end
162
+
163
+ def track_relation(field, kind, field_options)
164
+ relation_class = trackable.relation_class_of(field)
165
+ @prepared[:relations][kind][field] = if field_options.blank?
166
+ relation_class.fields.keys
167
+ else
168
+ %w[_id] | field_options.map { |opt| relation_class.database_field_name(opt) }
169
+ end
170
+ end
171
+
172
+ def reserved_fields
173
+ @reserved_fields ||= ['_id', '_type', @prepared[:version_field].to_s, "#{@prepared[:modifier_field]}_id"]
174
+ end
175
+ end
176
+ end
177
+ end