dh_easy-core 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +7 -0
  4. data/.yardopts +1 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE +21 -0
  8. data/README.md +20 -0
  9. data/Rakefile +22 -0
  10. data/dh_easy-core.gemspec +50 -0
  11. data/doc/DhEasy.html +117 -0
  12. data/doc/DhEasy/Core.html +1590 -0
  13. data/doc/DhEasy/Core/Config.html +311 -0
  14. data/doc/DhEasy/Core/Exception.html +117 -0
  15. data/doc/DhEasy/Core/Exception/OutdatedError.html +135 -0
  16. data/doc/DhEasy/Core/Helper.html +117 -0
  17. data/doc/DhEasy/Core/Helper/Cookie.html +1070 -0
  18. data/doc/DhEasy/Core/Mock.html +282 -0
  19. data/doc/DhEasy/Core/Mock/FakeDb.html +3779 -0
  20. data/doc/DhEasy/Core/Mock/FakeExecutor.html +3289 -0
  21. data/doc/DhEasy/Core/Mock/FakeFinisher.html +160 -0
  22. data/doc/DhEasy/Core/Mock/FakeParser.html +160 -0
  23. data/doc/DhEasy/Core/Mock/FakeSeeder.html +160 -0
  24. data/doc/DhEasy/Core/Plugin.html +117 -0
  25. data/doc/DhEasy/Core/Plugin/CollectionVault.html +299 -0
  26. data/doc/DhEasy/Core/Plugin/ConfigBehavior.html +541 -0
  27. data/doc/DhEasy/Core/Plugin/ContextIntegrator.html +445 -0
  28. data/doc/DhEasy/Core/Plugin/Executor.html +259 -0
  29. data/doc/DhEasy/Core/Plugin/ExecutorBehavior.html +344 -0
  30. data/doc/DhEasy/Core/Plugin/Finisher.html +265 -0
  31. data/doc/DhEasy/Core/Plugin/FinisherBehavior.html +142 -0
  32. data/doc/DhEasy/Core/Plugin/InitializeHook.html +220 -0
  33. data/doc/DhEasy/Core/Plugin/Parser.html +270 -0
  34. data/doc/DhEasy/Core/Plugin/ParserBehavior.html +235 -0
  35. data/doc/DhEasy/Core/Plugin/Seeder.html +674 -0
  36. data/doc/DhEasy/Core/Plugin/SeederBehavior.html +142 -0
  37. data/doc/DhEasy/Core/SmartCollection.html +1087 -0
  38. data/doc/_index.html +364 -0
  39. data/doc/class_list.html +51 -0
  40. data/doc/css/common.css +1 -0
  41. data/doc/css/full_list.css +58 -0
  42. data/doc/css/style.css +496 -0
  43. data/doc/file.README.html +91 -0
  44. data/doc/file_list.html +56 -0
  45. data/doc/frames.html +17 -0
  46. data/doc/index.html +91 -0
  47. data/doc/js/app.js +303 -0
  48. data/doc/js/full_list.js +216 -0
  49. data/doc/js/jquery.js +4 -0
  50. data/doc/method_list.html +939 -0
  51. data/doc/top-level-namespace.html +110 -0
  52. data/lib/dh_easy/core.rb +257 -0
  53. data/lib/dh_easy/core/config.rb +27 -0
  54. data/lib/dh_easy/core/exception.rb +8 -0
  55. data/lib/dh_easy/core/exception/outdated_error.rb +9 -0
  56. data/lib/dh_easy/core/helper.rb +8 -0
  57. data/lib/dh_easy/core/helper/cookie.rb +209 -0
  58. data/lib/dh_easy/core/mock.rb +45 -0
  59. data/lib/dh_easy/core/mock/fake_db.rb +561 -0
  60. data/lib/dh_easy/core/mock/fake_executor.rb +373 -0
  61. data/lib/dh_easy/core/mock/fake_finisher.rb +28 -0
  62. data/lib/dh_easy/core/mock/fake_parser.rb +33 -0
  63. data/lib/dh_easy/core/mock/fake_seeder.rb +28 -0
  64. data/lib/dh_easy/core/plugin.rb +19 -0
  65. data/lib/dh_easy/core/plugin/collection_vault.rb +23 -0
  66. data/lib/dh_easy/core/plugin/config_behavior.rb +43 -0
  67. data/lib/dh_easy/core/plugin/context_integrator.rb +60 -0
  68. data/lib/dh_easy/core/plugin/executor.rb +19 -0
  69. data/lib/dh_easy/core/plugin/executor_behavior.rb +32 -0
  70. data/lib/dh_easy/core/plugin/finisher.rb +19 -0
  71. data/lib/dh_easy/core/plugin/finisher_behavior.rb +9 -0
  72. data/lib/dh_easy/core/plugin/initialize_hook.rb +17 -0
  73. data/lib/dh_easy/core/plugin/parser.rb +19 -0
  74. data/lib/dh_easy/core/plugin/parser_behavior.rb +17 -0
  75. data/lib/dh_easy/core/plugin/seeder.rb +44 -0
  76. data/lib/dh_easy/core/plugin/seeder_behavior.rb +9 -0
  77. data/lib/dh_easy/core/smart_collection.rb +236 -0
  78. data/lib/dh_easy/core/version.rb +6 -0
  79. metadata +249 -0
@@ -0,0 +1,19 @@
1
+ require 'dh_easy/core/plugin/initialize_hook'
2
+ require 'dh_easy/core/plugin/context_integrator'
3
+ require 'dh_easy/core/plugin/collection_vault'
4
+ require 'dh_easy/core/plugin/config_behavior'
5
+ require 'dh_easy/core/plugin/executor_behavior'
6
+ require 'dh_easy/core/plugin/executor'
7
+ require 'dh_easy/core/plugin/parser_behavior'
8
+ require 'dh_easy/core/plugin/parser'
9
+ require 'dh_easy/core/plugin/seeder_behavior'
10
+ require 'dh_easy/core/plugin/seeder'
11
+ require 'dh_easy/core/plugin/finisher_behavior'
12
+ require 'dh_easy/core/plugin/finisher'
13
+
14
+ module DhEasy
15
+ module Core
16
+ module Plugin
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module CollectionVault
5
+ # Stored collections info as hash.
6
+ def collections
7
+ @collections ||= {}
8
+ end
9
+
10
+ # Add a new collection
11
+ #
12
+ # @param [Symbol] key Collection key used to lookup for collection name.
13
+ # @param [String] name Collection name used on outputs.
14
+ def add_collection key, name
15
+ if collections.has_key? key
16
+ raise "Can't add \"#{key}\" collection, it already exists!"
17
+ end
18
+ collections[key] = name
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module ConfigBehavior
5
+ include DhEasy::Core::Plugin::ContextIntegrator
6
+ include DhEasy::Core::Plugin::CollectionVault
7
+
8
+ attr_reader :config_collection_key
9
+
10
+ # Hook to map config behavior on self
11
+ #
12
+ # @param [Hash] opts ({}) Configuration options.
13
+ # @option opts [Array] :config_collection ([:config, 'config']) Key value pair array to se a custom collection.
14
+ #
15
+ # @example
16
+ # initialize_hook_core_config_behavior config_collection: [:my_config, 'abc']
17
+ # config_collection
18
+ # # => 'abc'
19
+ def initialize_hook_core_config_behavior opts = {}
20
+ @config_collection_key, collection = opts[:config_collection] || [:config, 'config']
21
+ add_collection config_collection_key, collection
22
+ end
23
+
24
+ # Get config collection name.
25
+ # @return [String]
26
+ def config_collection
27
+ collections[config_collection_key]
28
+ end
29
+
30
+ # Find a configuration value by item key.
31
+ #
32
+ # @param [Symbol] key Item key to find.
33
+ #
34
+ # @note Instance must implement:
35
+ # * `find_output(collection, query)`
36
+ def find_config key
37
+ value = find_output config_collection, '_id' => key
38
+ value ||= {'_collection' => config_collection, '_id' => key}
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,60 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module ContextIntegrator
5
+ # Last mocked ontext object.
6
+ attr_reader :context
7
+
8
+ # Mock a context methods into self.
9
+ #
10
+ # @param origin Object that represents the context to mock.
11
+ #
12
+ # @example
13
+ # class MyContext
14
+ # attr_accessor :message
15
+ # def initialize
16
+ # message = 'Hello world!'
17
+ # end
18
+ #
19
+ # def hello_world
20
+ # message
21
+ # end
22
+ # end
23
+ #
24
+ # class Foo
25
+ # include ContextIntegrator
26
+ #
27
+ # def hello_person
28
+ # 'Hello person!'
29
+ # end
30
+ # end
31
+ #
32
+ # context = MyContext.new
33
+ # my_object = Foo.new
34
+ # my_object.mock_context context
35
+ #
36
+ # puts my_object.hello_world
37
+ # # => 'Hello world!'
38
+ # puts my_object.hello_person
39
+ # # => 'Hello person!'
40
+ #
41
+ # context.message = 'Hello world again!'
42
+ # puts my_object.hello_world
43
+ # # => 'Hello world again!
44
+ def mock_context origin
45
+ @context = origin
46
+ DhEasy::Core.mock_instance_methods context, self
47
+ end
48
+
49
+ # Hook to mock context on initialize.
50
+ #
51
+ # @param [Hash] opts ({}) Configuration options.
52
+ # @option opts :context Object that represents the context to mock.
53
+ def initialize_hook_core_context_integrator opts = [{}]
54
+ raise ':context object is required.' if opts[:context].nil?
55
+ mock_context opts[:context]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,19 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module Executor
5
+ include DhEasy::Core::Plugin::InitializeHook
6
+ include DhEasy::Core::Plugin::ExecutorBehavior
7
+
8
+ # Initialize hooks.
9
+ #
10
+ # @param [Hash] opts ({}) Configuration options.
11
+ #
12
+ # @see DhEasy::Core::Plugin::ContextIntegrator#initialize_hook_core_context_integrator
13
+ def initialize opts = {}
14
+ initialize_hooks opts
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module ExecutorBehavior
5
+ include DhEasy::Core::Plugin::ContextIntegrator
6
+
7
+ # Enqueue a single/multiple pages for fetch. Analog to `save_pages`.
8
+ #
9
+ # @param [Array,Hash] object Pages to save being Hash when single and
10
+ # Array when many.
11
+ #
12
+ # @note Instance must implement:
13
+ # * `save_pages(pages)`
14
+ def enqueue object
15
+ object = [object] unless object.is_a? Array
16
+ save_pages object
17
+ end
18
+
19
+ # Save a single/multiple outputs. Analog to `save_outputs`.
20
+ #
21
+ # @param [Array,Hash] object Outputs to save being Hash when single and Array when many.
22
+ #
23
+ # @note Instance must implement:
24
+ # * `save_outputs(outputs)`
25
+ def save object
26
+ object = [object] unless object.is_a? Array
27
+ save_outputs object
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module Finisher
5
+ include DhEasy::Core::Plugin::InitializeHook
6
+ include DhEasy::Core::Plugin::FinisherBehavior
7
+
8
+ # Initialize finisher and hooks.
9
+ #
10
+ # @param [Hash] opts ({}) Configuration options.
11
+ #
12
+ # @see DhEasy::Core::Plugin::ContextIntegrator#initialize_hook_core_context_integrator
13
+ def initialize opts = {}
14
+ initialize_hooks opts
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module FinisherBehavior
5
+ include DhEasy::Core::Plugin::ExecutorBehavior
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module InitializeHook
5
+ # Execute all methods with `initilaize_hook_` prefix (hooks).
6
+ #
7
+ # @param [Hash] opts ({}) Configuration options sent to all hooks.
8
+ def initialize_hooks opts = {}
9
+ initializers = self.methods.select{|i|i.to_s =~ /^initialize_hook_/}
10
+ initializers.each do |method|
11
+ self.send method, opts
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module Parser
5
+ include DhEasy::Core::Plugin::InitializeHook
6
+ include DhEasy::Core::Plugin::ParserBehavior
7
+
8
+ # Initialize parser and hooks.
9
+ #
10
+ # @param [Hash] opts ({}) Configuration options.
11
+ #
12
+ # @see DhEasy::Core::Plugin::ContextIntegrator#initialize_hook_core_context_integrator
13
+ def initialize opts = {}
14
+ initialize_hooks opts
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module ParserBehavior
5
+ include DhEasy::Core::Plugin::ExecutorBehavior
6
+
7
+ # Alias to `page['vars']`.
8
+ #
9
+ # @note Instance must implement:
10
+ # * `page`
11
+ def vars
12
+ page['vars']
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module Seeder
5
+ include DhEasy::Core::Plugin::InitializeHook
6
+ include DhEasy::Core::Plugin::SeederBehavior
7
+
8
+ # Root input directory path.
9
+ # @return [String]
10
+ attr_accessor :root_input_dir
11
+
12
+ # Referer to use on page seeding.
13
+ # @return [String]
14
+ attr_accessor :referer
15
+
16
+ # Cookie to use on page seeing.
17
+ # @return [String]
18
+ attr_accessor :cookie
19
+
20
+ # Hook to initialize seeder object.
21
+ #
22
+ # @param [Hash] opts ({}) Configuration options.
23
+ # @option opts [String] :root_input_dir (nil) Root directory for inputs.
24
+ # @option opts [String] :referer (nil) New pages referer, useful to dynamic setups.
25
+ # @option opts [String] :cookie (nil) Cookie to use on seeded pages fetchs.
26
+ def initialize_hook_core_seeder opts = {}
27
+ @root_input_dir = opts[:root_input_dir]
28
+ @referer = opts[:referer]
29
+ @cookie = opts[:cookie]
30
+ end
31
+
32
+ # Initialize seeder and hooks.
33
+ #
34
+ # @param [Hash] opts ({}) Configuration options.
35
+ #
36
+ # @see DhEasy::Core::Plugin::ContextIntegrator#initialize_hook_core_context_integrator
37
+ # @see #initialize_hook_core_seeder
38
+ def initialize opts = {}
39
+ initialize_hooks opts
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ module DhEasy
2
+ module Core
3
+ module Plugin
4
+ module SeederBehavior
5
+ include DhEasy::Core::Plugin::ExecutorBehavior
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,236 @@
1
+ module DhEasy
2
+ module Core
3
+ # Smart collection capable to avoid duplicates on insert by matching id
4
+ # defined fields along events.
5
+ class SmartCollection < Array
6
+ # Implemented event list.
7
+ EVENTS = [
8
+ :before_defaults,
9
+ :before_match,
10
+ :before_insert,
11
+ :after_insert
12
+ ]
13
+
14
+ # Key fields, analog to primary keys.
15
+ attr_reader :key_fields
16
+ # Default fields values. Apply to missing fields and null values.
17
+ attr_reader :defaults
18
+
19
+ # Initialize collection
20
+ #
21
+ # @param [Array] key_fields Key fields, analog to primary keys.
22
+ # @param [Hash] opts ({}) Configuration options.
23
+ # @option opts [Array] :values ([]) Initial values; will avoid duplicates on insert.
24
+ # @option opts [Hash] :defaults ({}) Default values. `proc` values will be executed to get default value.
25
+ #
26
+ # @example With default values.
27
+ # count = 0
28
+ # defaults = {
29
+ # 'id' => lambda{|item| count += 1},
30
+ # 'aaa' => 111,
31
+ # 'bbb' => proc{|item| item['ccc'].nil? ? 'No ccc' : 'Has ccc'}
32
+ # }
33
+ # values = [
34
+ # {'aaa' => 'Defaults apply on nil values only', 'bbb' => nil},
35
+ # {'ccc' => 'ddd'},
36
+ # {'id' => 'abc123'}
37
+ # ]
38
+ # new_item = {'bbb' => 'Look mom! no ccc'}
39
+ # collection = SmartCollection.new ['id'], defaults: defaults
40
+ # collection << new_item
41
+ # collection
42
+ # # => [
43
+ # # {'id' => 1, 'aaa' => 'Defaults apply on nil values only', 'bbb' => 'No ccc'},
44
+ # # {'id' => 2, 'aaa' => 111, 'bbb' => 'Has ccc', 'ccc' => 'ddd'},
45
+ # # {'id' => 'abc123', 'aaa' => 111, 'bbb' => 'No ccc'},
46
+ # # {'id' => 3, 'aaa' => 111, 'bbb' => 'Look mom! no ccc'}
47
+ # # ]
48
+ #
49
+ # @note Defaults will apply to missing fields and null values only.
50
+ def initialize key_fields, opts = {}
51
+ @key_fields = key_fields || []
52
+ @defaults = opts[:defaults] || {}
53
+ super 0
54
+ (opts[:values] || []).each{|item| self << item}
55
+ end
56
+
57
+ # Asigned events.
58
+ # @private
59
+ def events
60
+ @events ||= {}
61
+ end
62
+
63
+ # Add event binding by key and block.
64
+ #
65
+ # @param [Symbol] key Event name.
66
+ #
67
+ # @raise [ArgumentError] When unknown event key.
68
+ #
69
+ # @example before_defaults
70
+ # defaults = {'aaa' => 111}
71
+ # collection = SmartCollection.new [],
72
+ # defaults: defaults
73
+ # collection.bind_event(:before_defaults) do |collection, item|
74
+ # puts collection
75
+ # # => []
76
+ # puts item
77
+ # # => {'bbb' => 222}
78
+ #
79
+ # # Sending the item back is required, or a new one
80
+ # # in case you want to replace item to insert.
81
+ # item
82
+ # end
83
+ # data << {'bbb' => 222}
84
+ # data
85
+ # # => [{'aaa' => 111, 'bbb' => 222}]
86
+ #
87
+ # @example before_match
88
+ # keys = ['id']
89
+ # defaults = {'aaa' => 111}
90
+ # values = [
91
+ # {'id' => 1, 'ccc' => 333}
92
+ # ]
93
+ # collection = SmartCollection.new keys,
94
+ # defaults: defaults
95
+ # values: values
96
+ # collection.bind_event(:before_match) do |collection, item|
97
+ # puts collection
98
+ # # => [{'id' => 1, 'aaa' => 111, 'ccc' => 333}]
99
+ # puts item
100
+ # # => {'id' => 1, 'aaa' => 111, 'bbb' => 222}
101
+ #
102
+ # # Sending the item back is required, or a new one
103
+ # # in case you want to replace item to insert.
104
+ # item
105
+ # end
106
+ # data << {'id' => 1, 'bbb' => 222}
107
+ # data
108
+ # # => [{'id' => 1, 'aaa' => 111, 'bbb' => 222}]
109
+ #
110
+ # @example before_insert
111
+ # keys = ['id']
112
+ # defaults = {'aaa' => 111}
113
+ # values = [
114
+ # {'id' => 1, 'ccc' => 333}
115
+ # ]
116
+ # collection = SmartCollection.new keys,
117
+ # defaults: defaults
118
+ # values: values
119
+ # collection.bind_event(:before_insert) do |collection, item, match|
120
+ # puts collection
121
+ # # => [{'id' => 1, 'aaa' => 111, 'ccc' => 333}]
122
+ # puts item
123
+ # # => {'id' => 1, 'aaa' => 111, 'bbb' => 222}
124
+ # puts match
125
+ # # => {'id' => 1, 'aaa' => 111, 'ccc' => 333}
126
+ #
127
+ # # Sending the item back is required, or a new one
128
+ # # in case you want to replace item to insert.
129
+ # item
130
+ # end
131
+ # data << {'id' => 1, 'bbb' => 222}
132
+ # data
133
+ # # => [{'id' => 1, 'aaa' => 111, 'bbb' => 222}]
134
+ #
135
+ # @example after_insert
136
+ # keys = ['id']
137
+ # defaults = {'aaa' => 111}
138
+ # values = [
139
+ # {'id' => 1, 'ccc' => 333}
140
+ # ]
141
+ # collection = SmartCollection.new keys,
142
+ # defaults: defaults
143
+ # values: values
144
+ # collection.bind_event(:after_insert) do |collection, item, match|
145
+ # puts collection
146
+ # # => [{'id' => 1, 'aaa' => 111, 'bbb' => 222}]
147
+ # puts item
148
+ # # => {'id' => 1, 'aaa' => 111, 'bbb' => 222}
149
+ # puts match
150
+ # # => {'id' => 1, 'aaa' => 111, 'ccc' => 333}
151
+ # # No need to send item back since it is already inserted
152
+ # end
153
+ # data << {'id' => 1, 'bbb' => 222}
154
+ # data
155
+ # # => [{'id' => 1, 'aaa' => 111, 'bbb' => 222}]
156
+ #
157
+ # @note Some events will expect a return value to replace item on insertion:
158
+ # * `before_match`
159
+ # * `before_defaults`
160
+ # * `before_insert`
161
+ def bind_event key, &block
162
+ unless EVENTS.include? key
163
+ raise ArgumentError.new("Unknown event '#{key}'")
164
+ end
165
+ (events[key] ||= []) << block
166
+ end
167
+
168
+ # Call an event
169
+ # @private
170
+ #
171
+ # @param [Symbol] key Event name.
172
+ # @param default Detault return value when event's return nil.
173
+ # @param args event arguments.
174
+ def call_event key, default = nil, *args
175
+ return default if events[key].nil?
176
+ result = nil
177
+ events[key].each{|event| result = event.call self, *args}
178
+ result.nil? ? default : result
179
+ end
180
+
181
+ # Check whenever two items keys match.
182
+ #
183
+ # @param [Hash] item_a Item to match.
184
+ # @param [Hash] item_b Item to match.
185
+ #
186
+ # @return [Boolean]
187
+ def match_keys? item_a, item_b
188
+ return false if key_fields.nil? || key_fields.count < 1
189
+ return true if item_a.nil? && item_b.nil?
190
+ return false if item_a.nil? || item_b.nil?
191
+ key_fields.each do |key|
192
+ return false if item_a[key] != item_b[key]
193
+ end
194
+ true
195
+ end
196
+
197
+ # Apply default values into item.
198
+ # @private
199
+ #
200
+ # @param [Hash] item Item to apply defaults.
201
+ #
202
+ # @return [Hash] Item
203
+ def apply_defaults item
204
+ defaults.each do |key, value|
205
+ next unless item[key].nil?
206
+ item[key] = value.respond_to?(:call) ? value.call(item) : value
207
+ end
208
+ end
209
+
210
+ # Find an item by matching filter keys
211
+ #
212
+ # @param [Hash] filter
213
+ #
214
+ # @return [Hash,nil] First existing item match or nil when no match.
215
+ #
216
+ # @note _Warning:_ It uses table scan to filter and will be slow.
217
+ def find_match filter
218
+ self.find do |item|
219
+ match_keys? item, filter
220
+ end
221
+ end
222
+
223
+ # Add/remplace an item avoiding duplicates
224
+ def << item
225
+ item = call_event :before_defaults, item, item
226
+ apply_defaults item
227
+ item = call_event :before_match, item, item
228
+ match = find_match item
229
+ item = call_event :before_insert, item, item, match
230
+ delete match unless match.nil?
231
+ result = super(item)
232
+ call_event :after_insert, result, item, match
233
+ end
234
+ end
235
+ end
236
+ end