elastictastic 0.5.0 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +161 -10
  3. data/lib/elastictastic/adapter.rb +84 -0
  4. data/lib/elastictastic/association.rb +6 -0
  5. data/lib/elastictastic/basic_document.rb +213 -0
  6. data/lib/elastictastic/bulk_persistence_strategy.rb +64 -19
  7. data/lib/elastictastic/callbacks.rb +18 -12
  8. data/lib/elastictastic/child_collection_proxy.rb +15 -11
  9. data/lib/elastictastic/client.rb +47 -24
  10. data/lib/elastictastic/configuration.rb +59 -4
  11. data/lib/elastictastic/dirty.rb +43 -28
  12. data/lib/elastictastic/discrete_persistence_strategy.rb +48 -23
  13. data/lib/elastictastic/document.rb +1 -85
  14. data/lib/elastictastic/embedded_document.rb +34 -0
  15. data/lib/elastictastic/errors.rb +17 -5
  16. data/lib/elastictastic/field.rb +3 -0
  17. data/lib/elastictastic/mass_assignment_security.rb +2 -4
  18. data/lib/elastictastic/middleware.rb +66 -84
  19. data/lib/elastictastic/multi_get.rb +30 -0
  20. data/lib/elastictastic/multi_search.rb +70 -0
  21. data/lib/elastictastic/nested_document.rb +3 -27
  22. data/lib/elastictastic/new_relic_instrumentation.rb +8 -8
  23. data/lib/elastictastic/observing.rb +8 -6
  24. data/lib/elastictastic/optimistic_locking.rb +57 -0
  25. data/lib/elastictastic/parent_child.rb +56 -54
  26. data/lib/elastictastic/persistence.rb +16 -16
  27. data/lib/elastictastic/properties.rb +136 -96
  28. data/lib/elastictastic/railtie.rb +1 -1
  29. data/lib/elastictastic/rotor.rb +105 -0
  30. data/lib/elastictastic/scope.rb +186 -56
  31. data/lib/elastictastic/server_error.rb +20 -1
  32. data/lib/elastictastic/test_helpers.rb +152 -97
  33. data/lib/elastictastic/thrift/constants.rb +12 -0
  34. data/lib/elastictastic/thrift/rest.rb +83 -0
  35. data/lib/elastictastic/thrift/types.rb +124 -0
  36. data/lib/elastictastic/thrift_adapter.rb +61 -0
  37. data/lib/elastictastic/transport_methods.rb +27 -0
  38. data/lib/elastictastic/validations.rb +11 -13
  39. data/lib/elastictastic/version.rb +1 -1
  40. data/lib/elastictastic.rb +148 -27
  41. data/spec/environment.rb +1 -1
  42. data/spec/examples/bulk_persistence_strategy_spec.rb +151 -23
  43. data/spec/examples/callbacks_spec.rb +65 -34
  44. data/spec/examples/dirty_spec.rb +160 -1
  45. data/spec/examples/document_spec.rb +168 -106
  46. data/spec/examples/middleware_spec.rb +1 -61
  47. data/spec/examples/multi_get_spec.rb +127 -0
  48. data/spec/examples/multi_search_spec.rb +113 -0
  49. data/spec/examples/observing_spec.rb +24 -3
  50. data/spec/examples/optimistic_locking_spec.rb +417 -0
  51. data/spec/examples/parent_child_spec.rb +73 -33
  52. data/spec/examples/properties_spec.rb +53 -0
  53. data/spec/examples/rotor_spec.rb +132 -0
  54. data/spec/examples/scope_spec.rb +78 -18
  55. data/spec/examples/search_spec.rb +26 -0
  56. data/spec/examples/validation_spec.rb +7 -1
  57. data/spec/models/author.rb +1 -1
  58. data/spec/models/blog.rb +2 -0
  59. data/spec/models/comment.rb +1 -1
  60. data/spec/models/photo.rb +9 -0
  61. data/spec/models/post.rb +3 -0
  62. metadata +97 -78
  63. data/lib/elastictastic/resource.rb +0 -4
  64. data/spec/examples/active_model_lint_spec.rb +0 -20
@@ -9,21 +9,27 @@ module Elastictastic
9
9
  define_model_callbacks(*HOOKS)
10
10
  end
11
11
 
12
- module InstanceMethods
13
- def save
14
- run_callbacks(:save) { super }
15
- end
12
+ def save(options = {})
13
+ with_callbacks(:save, options) { super }
14
+ end
16
15
 
17
- def create
18
- run_callbacks(:create) { super }
19
- end
16
+ def create(options = {})
17
+ with_callbacks(:create, options) { super }
18
+ end
20
19
 
21
- def update
22
- run_callbacks(:update) { super }
23
- end
20
+ def update(options = {})
21
+ with_callbacks(:update, options) { super }
22
+ end
23
+
24
+ def destroy(options = {})
25
+ with_callbacks(:destroy, options) { super }
26
+ end
27
+
28
+ private
24
29
 
25
- def destroy
26
- run_callbacks(:destroy) { super }
30
+ def with_callbacks(name, options)
31
+ if options[:callbacks] == false then yield
32
+ else run_callbacks(name) { yield }
27
33
  end
28
34
  end
29
35
  end
@@ -1,6 +1,6 @@
1
1
  module Elastictastic
2
2
  class ChildCollectionProxy < Scope
3
- attr_reader :parent, :transient_children
3
+ attr_reader :parent
4
4
 
5
5
  def initialize(association, parent)
6
6
  super(
@@ -12,10 +12,10 @@ module Elastictastic
12
12
  'filter' => { 'term' => { '_parent' => parent.id }}
13
13
  }
14
14
  }
15
- )
15
+ ),
16
+ self
16
17
  )
17
18
  @parent = parent
18
- @parent_collection = self
19
19
  @transient_children = []
20
20
  end
21
21
 
@@ -25,28 +25,32 @@ module Elastictastic
25
25
  end
26
26
 
27
27
  def first
28
- super || @transient_children.first
28
+ super || transient_children.first
29
29
  end
30
30
 
31
31
  def each(&block)
32
32
  if block
33
- super
34
- @transient_children.each(&block)
33
+ super if @parent.persisted?
34
+ transient_children.each(&block)
35
35
  else
36
36
  ::Enumerator.new(self, :each)
37
37
  end
38
38
  end
39
39
 
40
- def persisted!(child)
41
- @transient_children.delete(child)
42
- end
43
-
44
40
  def <<(child)
45
- child.parent_collection = self
41
+ child.parent = @parent
46
42
  @transient_children << child
47
43
  self
48
44
  end
49
45
 
46
+ def transient_children
47
+ @transient_children.tap do |children|
48
+ children.reject! do |child|
49
+ !child.transient?
50
+ end
51
+ end
52
+ end
53
+
50
54
  private
51
55
 
52
56
  def params_for_find
@@ -1,48 +1,70 @@
1
- require 'faraday'
2
-
3
1
  module Elastictastic
4
2
  class Client
5
3
  attr_reader :connection
6
4
 
7
5
  def initialize(config)
8
- builder = Faraday::Builder.new do |builder|
9
- builder.use Middleware::RaiseServerErrors
10
- builder.use Middleware::JsonEncodeBody
11
- builder.use Middleware::JsonDecodeResponse
12
- if config.logger
13
- builder.use Middleware::LogRequests, config.logger
14
- end
15
- end
6
+ adapter_options = {
7
+ :request_timeout => config.request_timeout,
8
+ :connect_timeout => config.connect_timeout
9
+ }
16
10
  if config.hosts.length == 1
17
- builder.adapter config.adapter
18
- @connection =
19
- Faraday.new(:url => config.hosts.first, :builder => builder)
11
+ connection = Adapter[config.adapter].
12
+ new(config.hosts.first, adapter_options)
20
13
  else
21
- builder.use Middleware::Rotor, *config.hosts
22
- builder.adapter config.adapter
23
- @connection = Faraday.new(:builder => builder)
14
+ connection = Rotor.new(
15
+ config.hosts,
16
+ adapter_options.merge(
17
+ :adapter => config.adapter,
18
+ :backoff_threshold => config.backoff_threshold,
19
+ :backoff_start => config.backoff_start,
20
+ :backoff_max => config.backoff_max
21
+ )
22
+ )
24
23
  end
24
+ if config.logger
25
+ connection = Middleware::LogRequests.new(connection, config.logger)
26
+ end
27
+ connection = Middleware::JsonDecodeResponse.new(connection)
28
+ connection = Middleware::JsonEncodeBody.new(connection)
29
+ connection = Middleware::RaiseServerErrors.new(connection)
30
+ @connection = connection
25
31
  end
26
32
 
27
33
  def create(index, type, id, doc, params = {})
28
34
  if id
29
35
  @connection.put(
30
- path_with_query("/#{index}/#{type}/#{id}/_create", params), doc)
36
+ path_with_query("/#{index}/#{type}/#{id}/_create", params),
37
+ doc
38
+ )
31
39
  else
32
- @connection.post(path_with_query("/#{index}/#{type}", params), doc)
40
+ @connection.post(
41
+ path_with_query("/#{index}/#{type}", params),
42
+ doc
43
+ )
33
44
  end.body
34
45
  end
35
46
 
36
47
  def update(index, type, id, doc, params = {})
37
- @connection.put(path_with_query("/#{index}/#{type}/#{id}", params), doc)
48
+ @connection.put(
49
+ path_with_query("/#{index}/#{type}/#{id}", params),
50
+ doc
51
+ ).body
38
52
  end
39
53
 
40
54
  def bulk(commands, params = {})
41
55
  @connection.post(path_with_query('/_bulk', params), commands).body
42
56
  end
43
57
 
58
+ def exists?(index, type, id, params = {})
59
+ @connection.head(
60
+ path_with_query("/#{index}/#{type}/#{id}", params)
61
+ ).status == 200
62
+ end
63
+
44
64
  def get(index, type, id, params = {})
45
- @connection.get(path_with_query("/#{index}/#{type}/#{id}", params)).body
65
+ @connection.get(
66
+ path_with_query("/#{index}/#{type}/#{id}", params)
67
+ ).body
46
68
  end
47
69
 
48
70
  def mget(docspec, index = nil, type = nil)
@@ -67,11 +89,12 @@ module Elastictastic
67
89
  ).body
68
90
  end
69
91
 
92
+ def msearch(search_bodies)
93
+ @connection.post('/_msearch', search_bodies).body
94
+ end
95
+
70
96
  def scroll(id, options = {})
71
- @connection.post(
72
- "/_search/scroll?#{options.to_query}",
73
- id
74
- ).body
97
+ @connection.post("/_search/scroll?#{options.to_query}", id).body
75
98
  end
76
99
 
77
100
  def put_mapping(index, type, mapping)
@@ -1,15 +1,20 @@
1
1
  module Elastictastic
2
2
  class Configuration
3
3
 
4
- attr_writer :hosts, :adapter, :default_index, :auto_refresh, :default_batch_size
5
- attr_accessor :logger
4
+ attr_writer :hosts, :default_index, :auto_refresh, :default_batch_size, :adapter
5
+ attr_accessor :logger, :connect_timeout, :request_timeout, :backoff_threshold, :backoff_start, :backoff_max
6
+ attr_reader :extra_middlewares
7
+
8
+ def initialize
9
+ @extra_middlewares = []
10
+ end
6
11
 
7
12
  def host=(host)
8
13
  @hosts = [host]
9
14
  end
10
15
 
11
16
  def hosts
12
- @hosts ||= ['http://localhost:9200']
17
+ @hosts ||= [default_host]
13
18
  end
14
19
 
15
20
  def adapter
@@ -17,7 +22,12 @@ module Elastictastic
17
22
  end
18
23
 
19
24
  def default_index
20
- @default_index ||= 'default'
25
+ return @default_index if defined? @default_index
26
+ if url_from_env && url_from_env.path =~ /^\/([^\/]+)/
27
+ @default_index = $1
28
+ else
29
+ @default_index = 'default'
30
+ end
21
31
  end
22
32
 
23
33
  def auto_refresh
@@ -28,6 +38,51 @@ module Elastictastic
28
38
  @default_batch_size ||= 100
29
39
  end
30
40
 
41
+ def json_engine=(json_engine)
42
+ original_engine = MultiJson.engine
43
+ MultiJson.engine = json_engine
44
+ @json_engine = MultiJson.engine
45
+ ensure
46
+ MultiJson.engine = original_engine
47
+ end
48
+
49
+ def json_engine
50
+ @json_engine || MultiJson.engine
51
+ end
52
+
53
+ def use_middleware(*args)
54
+ @extra_middlewares << args
55
+ end
56
+
57
+ def presets
58
+ @presets ||= ActiveSupport::HashWithIndifferentAccess.new
59
+ end
60
+
61
+ def presets=(new_presets)
62
+ presets.merge!(new_presets)
63
+ end
64
+
65
+ private
66
+
67
+ def default_host
68
+ if url_from_env
69
+ url_from_env.class.build(
70
+ :host => url_from_env.host,
71
+ :port => url_from_env.port
72
+ )
73
+ else
74
+ 'http://localhost:9200'
75
+ end
76
+ end
77
+
78
+ def url_from_env
79
+ return @url_from_env if defined? @url_from_env
80
+ @url_from_env =
81
+ if ENV['ELASTICSEARCH_URL']
82
+ URI.parse(ENV['ELASTICSEARCH_URL'])
83
+ end
84
+ end
85
+
31
86
  ActiveModel::Observing::ClassMethods.public_instance_methods(false).each do |method|
32
87
  delegate method, :to => :"::Elastictastic::Observing"
33
88
  end
@@ -50,54 +50,68 @@ module Elastictastic
50
50
  end
51
51
  end
52
52
 
53
- module InstanceMethods
54
- def write_attribute(field, value)
55
- attribute_will_change!(field)
56
- super
57
- end
53
+ def write_attribute(field, value)
54
+ attribute_may_change!(field) { super }
55
+ end
58
56
 
59
- def write_embed(field, value)
60
- attribute_will_change!(field)
57
+ def write_embed(field, value)
58
+ attribute_may_change!(field) do
61
59
  if Array === value
62
60
  value.each do |el|
63
61
  el.nesting_document = self
64
62
  el.nesting_association = field
65
63
  end
66
64
  super(field, NestedCollectionProxy.new(self, field, value))
67
- else
65
+ elsif value
68
66
  value.nesting_document = self
69
67
  value.nesting_association = field
70
68
  super
69
+ else
70
+ super
71
71
  end
72
72
  end
73
+ end
73
74
 
74
- def save
75
- super
76
- clean_attributes!
77
- end
75
+ def save(options = {})
76
+ super
77
+ clean_attributes!
78
+ end
78
79
 
79
- def elasticsearch_doc=(doc)
80
- super
81
- clean_attributes!
82
- end
80
+ def elasticsearch_doc=(doc)
81
+ super
82
+ clean_attributes!
83
+ end
83
84
 
84
- protected
85
+ protected
85
86
 
86
- def clean_attributes!
87
- changed_attributes.clear
88
- @embeds.each_pair do |name, embedded|
89
- Util.call_or_map(embedded) { |doc| doc.clean_attributes! }
90
- end
87
+ def clean_attributes!
88
+ changed_attributes.clear
89
+ @_embeds.each_pair do |name, embedded|
90
+ Util.call_or_map(embedded) { |doc| doc && doc.clean_attributes! }
91
91
  end
92
92
  end
93
93
 
94
- module NestedDocumentMethods
94
+ def attribute_may_change!(field)
95
+ attribute_will_change!(field) unless changed_attributes.key?(field)
96
+ old_value = changed_attributes[field]
97
+ yield
98
+ attribute_not_changed!(field) if old_value == __send__(field)
99
+ end
100
+
101
+ def attribute_not_changed!(field)
102
+ changed_attributes.delete(field)
103
+ end
104
+
105
+ module EmbeddedDocumentMethods
95
106
  attr_writer :nesting_document, :nesting_association
96
107
 
97
- def attribute_will_change!(field)
98
- super
108
+ def attribute_may_change!(field)
99
109
  if @nesting_document
100
- @nesting_document.__send__("attribute_will_change!", @nesting_association)
110
+ @nesting_document.attribute_may_change!(@nesting_association) do
111
+ super
112
+ end
113
+ else
114
+ super
101
115
  end
102
116
  end
103
117
  end
@@ -116,8 +130,9 @@ module Elastictastic
116
130
  ].each do |destructive_method|
117
131
  module_eval <<-RUBY, __FILE__, __LINE__+1
118
132
  def #{destructive_method}(*args)
119
- @owner.__send__("\#{@embed_name}_will_change!")
120
- super
133
+ @owner.__send__(:attribute_may_change!, @embed_name) do
134
+ super
135
+ end
121
136
  end
122
137
  RUBY
123
138
  end
@@ -4,39 +4,61 @@ module Elastictastic
4
4
  class DiscretePersistenceStrategy
5
5
  include Singleton
6
6
 
7
+ DEFAULT_HANDLER = proc { |e| raise(e) if e }
8
+
7
9
  attr_accessor :auto_refresh
8
10
 
9
- def create(doc)
10
- response = Elastictastic.client.create(
11
- doc.index,
12
- doc.class.type,
13
- doc.id,
14
- doc.elasticsearch_doc,
15
- params_for(doc)
16
- )
11
+ def create(doc, &block)
12
+ block ||= DEFAULT_HANDLER
13
+ begin
14
+ response = Elastictastic.client.create(
15
+ doc.index,
16
+ doc.class.type,
17
+ doc.id,
18
+ doc.elasticsearch_doc,
19
+ params_for(doc)
20
+ )
21
+ rescue => e
22
+ return block.call(e)
23
+ end
17
24
  doc.id = response['_id']
25
+ doc.version = response['_version']
18
26
  doc.persisted!
27
+ block.call
19
28
  end
20
29
 
21
- def update(doc)
22
- Elastictastic.client.update(
23
- doc.index,
24
- doc.class.type,
25
- doc.id,
26
- doc.elasticsearch_doc,
27
- params_for(doc)
28
- )
30
+ def update(doc, &block)
31
+ block ||= DEFAULT_HANDLER
32
+ begin
33
+ response = Elastictastic.client.update(
34
+ doc.index,
35
+ doc.class.type,
36
+ doc.id,
37
+ doc.elasticsearch_doc,
38
+ params_for(doc)
39
+ )
40
+ rescue => e
41
+ return block.call(e)
42
+ end
43
+ doc.version = response['_version']
29
44
  doc.persisted!
45
+ block.call
30
46
  end
31
47
 
32
- def destroy(doc)
33
- response = Elastictastic.client.delete(
34
- doc.index.name,
35
- doc.class.type,
36
- doc.id,
37
- params_for(doc)
38
- )
48
+ def destroy(doc, &block)
49
+ block ||= DEFAULT_HANDLER
50
+ begin
51
+ response = Elastictastic.client.delete(
52
+ doc.index.name,
53
+ doc.class.type,
54
+ doc.id,
55
+ params_for(doc)
56
+ )
57
+ rescue => e
58
+ return block.call(e)
59
+ end
39
60
  doc.transient!
61
+ block.call
40
62
  response['found']
41
63
  end
42
64
 
@@ -46,6 +68,9 @@ module Elastictastic
46
68
  {}.tap do |params|
47
69
  params[:refresh] = true if Elastictastic.config.auto_refresh
48
70
  params[:parent] = doc._parent_id if doc._parent_id
71
+ params[:version] = doc.version if doc.version
72
+ routing = doc.class.route(doc)
73
+ params[:routing] = routing if routing
49
74
  end
50
75
  end
51
76
  end
@@ -3,96 +3,12 @@ module Elastictastic
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- extend Scoped
7
- include Properties
8
- include Persistence
9
- include ParentChild
6
+ include BasicDocument
10
7
  include Callbacks
11
8
  include Observing
12
9
  include Dirty
13
10
  include MassAssignmentSecurity
14
11
  include Validations
15
-
16
- extend ActiveModel::Naming
17
- include ActiveModel::Conversion
18
- end
19
-
20
- module ClassMethods
21
- delegate :find, :destroy_all, :sync_mapping, :inspect, :find_each,
22
- :find_in_batches, :first, :count, :empty?, :any?, :all,
23
- :query, :filter, :from, :size, :sort, :highlight, :fields,
24
- :script_fields, :preference, :facets, :to => :current_scope
25
-
26
- def mapping
27
- { type => { 'properties' => properties }}
28
- end
29
-
30
- def type
31
- name.underscore
32
- end
33
-
34
- def in_index(name_or_index)
35
- Scope.new(Elastictastic::Index(name_or_index), self)
36
- end
37
-
38
- def scoped(params)
39
- current_scope.scoped(params)
40
- end
41
-
42
- private
43
-
44
- def default_scope
45
- in_index(Index.default)
46
- end
47
- end
48
-
49
- module InstanceMethods
50
- attr_reader :id
51
-
52
- def initialize(attributes = {})
53
- self.class.current_scope.initialize_instance(self)
54
- end
55
-
56
- def elasticsearch_hit=(hit) #:nodoc:
57
- @id = hit['_id']
58
- @index = Index.new(hit['_index'])
59
- persisted!
60
-
61
- doc = {}
62
- doc.merge!(hit['_source']) if hit['_source']
63
- fields = hit['fields']
64
- if fields
65
- unflattened_fields =
66
- Util.unflatten_hash(fields.reject { |k, v| v.nil? })
67
- if unflattened_fields.has_key?('_source')
68
- doc.merge!(unflattened_fields.delete('_source'))
69
- end
70
- doc.merge!(unflattened_fields)
71
- end
72
- self.elasticsearch_doc=(doc)
73
- end
74
-
75
- def id=(id)
76
- assert_transient!
77
- @id = id
78
- end
79
-
80
- def index
81
- return @index if defined? @index
82
- @index = Index.default
83
- end
84
-
85
- def ==(other)
86
- index == other.index && id == other.id
87
- end
88
-
89
- def inspect
90
- inspected = "#<#{self.class.name} id: #{id}, index: #{index.name}"
91
- attributes.each_pair do |attr, value|
92
- inspected << ", #{attr}: #{value.inspect}"
93
- end
94
- inspected << ">"
95
- end
96
12
  end
97
13
  end
98
14
  end
@@ -0,0 +1,34 @@
1
+ module Elastictastic
2
+ module EmbeddedDocument
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Properties
7
+ include Dirty
8
+ include Dirty::EmbeddedDocumentMethods
9
+ include MassAssignmentSecurity
10
+ include Validations
11
+
12
+ include ActiveModel::Serializers::JSON
13
+ include ActiveModel::Serializers::Xml
14
+
15
+ self.include_root_in_json = false
16
+ end
17
+
18
+ def initialize_copy(original)
19
+ self.write_attributes(original.read_attributes.dup)
20
+ end
21
+
22
+ def attributes
23
+ {}
24
+ end
25
+
26
+ def ==(other)
27
+ other.nil? ? false : @_attributes == other.read_attributes && @_embeds == other.read_embeds
28
+ end
29
+
30
+ def eql?(other)
31
+ self.class == other.class && self == other
32
+ end
33
+ end
34
+ end
@@ -1,7 +1,19 @@
1
1
  module Elastictastic
2
- CancelBulkOperation = Class.new(StandardError)
3
- IllegalModificationError = Class.new(StandardError)
4
- OperationNotAllowed = Class.new(StandardError)
5
- NoServerAvailable = Class.new(StandardError)
6
- RecordInvalid = Class.new(StandardError)
2
+ Error = Class.new(StandardError)
3
+ CancelSave = Class.new(Error)
4
+ IllegalModificationError = Class.new(Error)
5
+ OperationNotAllowed = Class.new(Error)
6
+ MissingParameter = Class.new(Error)
7
+
8
+ class ConnectionFailed < Error
9
+ attr_reader :source
10
+
11
+ def initialize(source)
12
+ super(source.message)
13
+ @source = source
14
+ end
15
+ end
16
+
17
+ NoServerAvailable = Class.new(ConnectionFailed)
18
+ RecordInvalid = Class.new(Error)
7
19
  end
@@ -10,6 +10,9 @@ module Elastictastic
10
10
 
11
11
  def self.with_defaults(options)
12
12
  options = Util.deep_stringify(options)
13
+ if preset = options.delete('preset')
14
+ options = ::Elastictastic.config.presets[preset].merge(options)
15
+ end
13
16
  { 'type' => 'string' }.merge(options).tap do |field_properties|
14
17
  if field_properties['type'].to_s == 'date'
15
18
  field_properties['format'] = 'date_time_no_millis'
@@ -6,10 +6,8 @@ module Elastictastic
6
6
  include ActiveModel::MassAssignmentSecurity
7
7
  end
8
8
 
9
- module InstanceMethods
10
- def attributes=(attributes)
11
- super(sanitize_for_mass_assignment(attributes))
12
- end
9
+ def attributes=(attributes)
10
+ super(sanitize_for_mass_assignment(attributes))
13
11
  end
14
12
  end
15
13
  end