esse 0.0.2 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/exec/esse +3 -1
  3. data/lib/esse/backend/index/aliases.rb +8 -4
  4. data/lib/esse/backend/index/close.rb +54 -0
  5. data/lib/esse/backend/index/create.rb +21 -10
  6. data/lib/esse/backend/index/delete.rb +15 -14
  7. data/lib/esse/backend/index/documents.rb +2 -2
  8. data/lib/esse/backend/index/existance.rb +2 -3
  9. data/lib/esse/backend/index/open.rb +54 -0
  10. data/lib/esse/backend/index/refresh.rb +43 -0
  11. data/lib/esse/backend/index/reset.rb +33 -0
  12. data/lib/esse/backend/index/update.rb +128 -4
  13. data/lib/esse/backend/index.rb +20 -4
  14. data/lib/esse/backend/index_type/documents.rb +53 -42
  15. data/lib/esse/backend/index_type.rb +7 -2
  16. data/lib/esse/cli/event_listener.rb +87 -0
  17. data/lib/esse/cli/generate.rb +9 -4
  18. data/lib/esse/cli/index/base_operation.rb +76 -0
  19. data/lib/esse/cli/index/close.rb +26 -0
  20. data/lib/esse/cli/index/create.rb +26 -0
  21. data/lib/esse/cli/index/delete.rb +26 -0
  22. data/lib/esse/cli/index/open.rb +26 -0
  23. data/lib/esse/cli/index/reset.rb +26 -0
  24. data/lib/esse/cli/index/update_aliases.rb +32 -0
  25. data/lib/esse/cli/index/update_mapping.rb +33 -0
  26. data/lib/esse/cli/index/update_settings.rb +26 -0
  27. data/lib/esse/cli/index.rb +70 -2
  28. data/lib/esse/cli/templates/config.rb.erb +20 -0
  29. data/lib/esse/cli/templates/index.rb.erb +76 -11
  30. data/lib/esse/cli/templates/type_collection.rb.erb +41 -0
  31. data/lib/esse/cli/templates/{mappings.json → type_mappings.json} +0 -0
  32. data/lib/esse/cli/templates/type_serializer.rb.erb +23 -0
  33. data/lib/esse/cli.rb +75 -3
  34. data/lib/esse/cluster.rb +22 -6
  35. data/lib/esse/config.rb +39 -5
  36. data/lib/esse/core.rb +18 -36
  37. data/lib/esse/errors.rb +47 -0
  38. data/lib/esse/events/bus.rb +103 -0
  39. data/lib/esse/events/event.rb +64 -0
  40. data/lib/esse/events/publisher.rb +119 -0
  41. data/lib/esse/events.rb +49 -0
  42. data/lib/esse/index/backend.rb +2 -1
  43. data/lib/esse/index/base.rb +4 -6
  44. data/lib/esse/index/mappings.rb +4 -6
  45. data/lib/esse/index/settings.rb +6 -8
  46. data/lib/esse/index.rb +2 -1
  47. data/lib/esse/index_mapping.rb +2 -2
  48. data/lib/esse/index_setting.rb +8 -4
  49. data/lib/esse/index_type/actions.rb +2 -1
  50. data/lib/esse/index_type/backend.rb +2 -1
  51. data/lib/esse/index_type/mappings.rb +3 -3
  52. data/lib/esse/index_type.rb +6 -1
  53. data/lib/esse/logging.rb +19 -0
  54. data/lib/esse/object_document_mapper.rb +96 -0
  55. data/lib/esse/primitives/hash_utils.rb +29 -0
  56. data/lib/esse/primitives/hstring.rb +4 -3
  57. data/lib/esse/primitives/output.rb +64 -0
  58. data/lib/esse/primitives.rb +1 -0
  59. data/lib/esse/template_loader.rb +1 -1
  60. data/lib/esse/version.rb +1 -1
  61. data/lib/esse.rb +14 -2
  62. metadata +127 -24
  63. data/.gitignore +0 -12
  64. data/.rubocop.yml +0 -128
  65. data/CHANGELOG.md +0 -0
  66. data/Gemfile +0 -7
  67. data/Gemfile.lock +0 -60
  68. data/LICENSE.txt +0 -21
  69. data/README.md +0 -50
  70. data/Rakefile +0 -4
  71. data/bin/console +0 -22
  72. data/bin/setup +0 -8
  73. data/esse.gemspec +0 -39
  74. data/lib/esse/cli/templates/serializer.rb.erb +0 -14
  75. data/lib/esse/index_type/serializer.rb +0 -87
  76. data/lib/esse/types/mapping.rb +0 -0
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ module Events
5
+ # Event bus
6
+ #
7
+ # An event bus stores listeners (callbacks) and events
8
+ #
9
+ # @api private
10
+ class Bus
11
+ # @return [Hash] A hash with events registered within a bus
12
+ attr_reader :events
13
+
14
+ # @return [Hash] A hash with event listeners registered within a bus
15
+ attr_reader :listeners
16
+
17
+ # Initialize a new event bus
18
+ #
19
+ # @param [Hash] events A hash with events
20
+ # @param [Hash] listeners A hash with listeners
21
+ #
22
+ # @api private
23
+ # @idea
24
+ # Hash is thread-safe in practice because CRuby runs
25
+ # threads one at a time and does not do context
26
+ # switching during the execution of C functions
27
+ # However, in case of jRuby or other ruby interpreters,
28
+ # this assumption may not be true. In that case, we should
29
+ # use a different data structure. I think we should use
30
+ # a Concurrent::Hash or Concurrent::Map object from
31
+ # concurrent-ruby
32
+ # @see https://github.com/ruby-concurrency/concurrent-ruby
33
+ def initialize(events: {}, listeners: Hash.new { |h, k| h[k] = [] })
34
+ @listeners = listeners
35
+ @events = events
36
+ end
37
+
38
+ # @api private
39
+ def publish(event_id, payload)
40
+ process(event_id, payload) do |event, listener|
41
+ listener.call(event)
42
+ end
43
+ end
44
+
45
+ # @api private
46
+ def attach(listener)
47
+ events.each do |id, event|
48
+ method_name = event.listener_method
49
+ next unless listener.respond_to?(method_name)
50
+
51
+ listeners[id] << listener.method(method_name)
52
+ end
53
+ end
54
+
55
+ # @api private
56
+ def detach(listener)
57
+ listeners.each do |id, arr|
58
+ arr.each do |func|
59
+ listeners[id].delete(func) if func.receiver == listener
60
+ end
61
+ end
62
+ end
63
+
64
+ # @api private
65
+ def subscribe(event_id, &block)
66
+ listeners[event_id] << block
67
+ self
68
+ end
69
+
70
+ # @api private
71
+ def subscribed?(listener)
72
+ listeners.values.any? { |value| value.any? { |func| func == listener } } || (
73
+ methods = events.values.map(&:listener_method).select(&listener.method(:respond_to?)).map(&listener.method(:method))
74
+ methods && listeners.values.any? { |value| (methods & value).size > 0 }
75
+ )
76
+ end
77
+
78
+ # @api private
79
+ def can_handle?(object_or_event_id)
80
+ case object_or_event_id
81
+ when String, Symbol
82
+ events.key?(object_or_event_id)
83
+ else
84
+ events
85
+ .values
86
+ .map(&:listener_method)
87
+ .any?(&object_or_event_id.method(:respond_to?))
88
+ end
89
+ end
90
+
91
+ protected
92
+
93
+ # @api private
94
+ def process(event_id, payload)
95
+ listeners[event_id].each do |listener|
96
+ event = events[event_id].payload(payload)
97
+
98
+ yield(event, listener)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ module Events
5
+ class Event
6
+ attr_reader :id
7
+
8
+ # Initialize a new event
9
+ #
10
+ # @param [Symbol, String] id The event identifier
11
+ # @param [Hash] payload
12
+ #
13
+ # @return [Event]
14
+ #
15
+ # @api private
16
+ def initialize(id, payload = {})
17
+ @id = id
18
+ @payload = payload
19
+ end
20
+
21
+ # Get data from the payload
22
+ #
23
+ # @param [String,Symbol] name
24
+ #
25
+ # @api public
26
+ def [](name)
27
+ @payload.fetch(name)
28
+ end
29
+
30
+ # Coerce an event to a hash
31
+ #
32
+ # @return [Hash]
33
+ #
34
+ # @api public
35
+ def to_h
36
+ @payload
37
+ end
38
+ alias_method :to_hash, :to_h
39
+
40
+ # Get or set a payload
41
+ #
42
+ # @overload
43
+ # @return [Hash] payload
44
+ #
45
+ # @overload payload(data)
46
+ # @param [Hash] data A new payload
47
+ # @return [Event] A copy of the event with the provided payload
48
+ #
49
+ # @api public
50
+ def payload(data = nil)
51
+ if data
52
+ self.class.new(id, @payload.merge(data))
53
+ else
54
+ @payload
55
+ end
56
+ end
57
+
58
+ # @api private
59
+ def listener_method
60
+ @listener_method ||= Hstring.new("on_#{id}").underscore.to_sym
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+ require_relative 'bus'
5
+
6
+ module Esse
7
+ module Events
8
+ module Publisher
9
+ def self.included(klass)
10
+ klass.extend(ClassMethods)
11
+ end
12
+
13
+ # Class interface for publishers
14
+ #
15
+ # @api public
16
+ module ClassMethods
17
+ # Register a new event type
18
+ #
19
+ # @param [Symbol,String] event_id The event identifier
20
+ # @param [Hash] payload Optional default payload
21
+ #
22
+ # @return [self]
23
+ #
24
+ # @api public
25
+ def register_event(event_id, payload = {})
26
+ __bus__.events[event_id] = Event.new(event_id, payload)
27
+ self
28
+ end
29
+
30
+ # Publish an event
31
+ #
32
+ # @param [String] event_id The event identifier
33
+ # @param [Hash] payload An optional payload
34
+ # @raise [Esse::Events::UnregisteredEventError] if the event is not registered
35
+ #
36
+ # @api public
37
+ def publish(event_id, payload = {})
38
+ if __bus__.can_handle?(event_id)
39
+ __bus__.publish(event_id, payload)
40
+ self
41
+ else
42
+ raise UnregisteredEventError, event_id
43
+ end
44
+ end
45
+
46
+ # Publish an event with extra runtime information to the payload
47
+ #
48
+ # @param [String] event_id The event identifier
49
+ # @param [Hash] payload An optional payload
50
+ # @raise [Esse::Events::UnregisteredEventError] if the event is not registered
51
+ #
52
+ # @api public
53
+ def instrument(event_id, payload = {}, &block)
54
+ publish_event = false # ensure block is also called on error
55
+ raise(UnregisteredEventError, event_id) unless __bus__.can_handle?(event_id)
56
+
57
+ payload[:__started_at__] = Time.now
58
+ block.call(payload).tap { publish_event = true }
59
+ ensure
60
+ if publish_event
61
+ payload[:runtime] ||= Time.now - payload.delete(:__started_at__) if payload[:__started_at__]
62
+ __bus__.publish(event_id, payload)
63
+ end
64
+ end
65
+
66
+ # Subscribe to events.
67
+ #
68
+ # @param [Symbol,String,Object] object_or_event_id The event identifier or a listener object
69
+ # @param [Hash] filter_hash An optional event filter
70
+ #
71
+ # @raise [Esse::Events::InvalidSubscriberError] if the subscriber is not registered
72
+ # @return [Object] self
73
+ #
74
+ #
75
+ # @api public
76
+ def subscribe(object_or_event_id, &block)
77
+ if __bus__.can_handle?(object_or_event_id)
78
+ if block
79
+ __bus__.subscribe(object_or_event_id, &block)
80
+ else
81
+ __bus__.attach(object_or_event_id)
82
+ end
83
+
84
+ self
85
+ else
86
+ raise InvalidSubscriberError, object_or_event_id
87
+ end
88
+ end
89
+
90
+ # Unsubscribe a listener
91
+ #
92
+ # @param [Object] listener The listener object
93
+ #
94
+ # @return [self]
95
+ #
96
+ # @api public
97
+ def unsubscribe(listener)
98
+ __bus__.detach(listener)
99
+ end
100
+
101
+ # Return true if a given listener has been subscribed to any event
102
+ #
103
+ # @api public
104
+ def subscribed?(listener)
105
+ __bus__.subscribed?(listener)
106
+ end
107
+
108
+ # Internal event bus
109
+ #
110
+ # @return [Bus]
111
+ #
112
+ # @api private
113
+ def __bus__
114
+ @__bus__ ||= Bus.new
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'events/publisher'
4
+
5
+ module Esse
6
+ # Extension used for classes that can pub/sub events
7
+ #
8
+ # Examples:
9
+ #
10
+ # # Publish an event
11
+ # Esse::Events.publish('elasticsearch.create_index', { definition: {index_name: 'my_index'} })
12
+ # # Subscribe to an event
13
+ # Esse::Events.subscribe('elasticsearch.create_index') do |event|
14
+ # puts event.payload
15
+ # end
16
+ #
17
+ # # Publish an event using instrumentation
18
+ # Esse::Events.instrument('elasticsearch.create_index') do |payload|
19
+ # payload[:definition] = {index_name: 'my_index'}
20
+ # # Some slow action
21
+ # end
22
+ # Esse::Events.subscribe('elasticsearch.create_index') do |event|
23
+ # puts event.payload[:runtime] # Extra information about the amount of time the action took
24
+ # end
25
+ #
26
+ # # Attach a listener to the event bus
27
+ # class MyEventListener
28
+ # def on_elasticsearch_create_index(event)
29
+ # puts event.payload
30
+ # end
31
+ # end
32
+ # listener = MyEventListener.new
33
+ # Esse::Events.attach(listener)
34
+ # # Dettash the listener
35
+ # Esse::Events.detach(listener)
36
+ #
37
+ #
38
+ module Events
39
+ include Publisher
40
+
41
+ register_event 'elasticsearch.close'
42
+ register_event 'elasticsearch.open'
43
+ register_event 'elasticsearch.create_index'
44
+ register_event 'elasticsearch.delete_index'
45
+ register_event 'elasticsearch.update_mapping'
46
+ register_event 'elasticsearch.update_settings'
47
+ register_event 'elasticsearch.update_aliases'
48
+ end
49
+ end
@@ -3,9 +3,10 @@
3
3
  module Esse
4
4
  class Index
5
5
  module ClassMethods
6
- def backend
6
+ def elasticsearch
7
7
  Esse::Backend::Index.new(self)
8
8
  end
9
+ alias_method :backend, :elasticsearch
9
10
  end
10
11
 
11
12
  extend ClassMethods
@@ -3,12 +3,10 @@
3
3
  module Esse
4
4
  class Index
5
5
  module ClassMethods
6
- attr_reader :cluster_id
7
-
8
6
  # Define a Index method on the given module that calls the Index
9
7
  # method on the receiver. This is how the Esse::Index() method is
10
8
  # defined, and allows you to define Index() methods on other modules,
11
- # making it easier to have custom index settings for all indexes under
9
+ # making it easier to have custom index settings for all indices under
12
10
  # a namespace. Example:
13
11
  #
14
12
  # module V1
@@ -35,7 +33,7 @@ module Esse
35
33
  #
36
34
  # Example:
37
35
  # # Using a custom cluster
38
- # Esse.config.clusters(:v1).client = Elasticsearch::Client.new
36
+ # Esse.config.cluster(:v1).client = Elasticsearch::Client.new
39
37
  # class UsersIndex < Esse::Index(:v1)
40
38
  # end
41
39
  #
@@ -92,12 +90,12 @@ module Esse
92
90
  def cluster
93
91
  unless Esse.config.cluster_ids.include?(cluster_id)
94
92
  raise NotImplementedError, <<~MSG
95
- There is no cluster configured for this index. Use `Esse.config.clusters(cluster_id) { ... }' define the elasticsearch
93
+ There is no cluster configured for this index. Use `Esse.config.cluster(cluster_id) { ... }' define the elasticsearch
96
94
  client connection.
97
95
  MSG
98
96
  end
99
97
 
100
- Esse.synchronize { Esse.config.clusters(cluster_id) }
98
+ Esse.synchronize { Esse.config.cluster(cluster_id) }
101
99
  end
102
100
 
103
101
  def inspect
@@ -7,19 +7,17 @@
7
7
  module Esse
8
8
  class Index
9
9
  module ClassMethods
10
- MAPPING_ROOT_KEY = 'mappings'
11
-
12
10
  # This is the actually content that will be passed through the ES api
13
11
  def mappings_hash
14
- { MAPPING_ROOT_KEY => (index_mapping || type_mapping) }
12
+ { Esse::MAPPING_ROOT_KEY => (index_mapping || type_mapping) }
15
13
  end
16
14
 
17
15
  # This method is only used to define mapping
18
16
  def mappings(hash = {}, &block)
19
17
  @mapping = Esse::IndexMapping.new(body: hash, paths: template_dirs)
20
- return unless block_given?
18
+ return unless block
21
19
 
22
- @mapping.define_singleton_method(:as_json, &block)
20
+ @mapping.define_singleton_method(:to_h, &block)
23
21
  end
24
22
 
25
23
  private
@@ -32,7 +30,7 @@ module Esse
32
30
  return if mapping.empty?
33
31
 
34
32
  hash = mapping.body
35
- hash.key?(MAPPING_ROOT_KEY) ? hash[MAPPING_ROOT_KEY] : hash
33
+ hash.key?(Esse::MAPPING_ROOT_KEY) ? hash[Esse::MAPPING_ROOT_KEY] : hash
36
34
  end
37
35
 
38
36
  def type_mapping
@@ -4,17 +4,15 @@ module Esse
4
4
  # https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/indices/put_settings.rb
5
5
  class Index
6
6
  module ClassMethods
7
- SETTING_ROOT_KEY = 'settings'
8
-
9
7
  def settings_hash
10
8
  hash = setting.body
11
- { SETTING_ROOT_KEY => (hash.key?(SETTING_ROOT_KEY) ? hash[SETTING_ROOT_KEY] : hash) }
9
+ { Esse::SETTING_ROOT_KEY => (hash.key?(Esse::SETTING_ROOT_KEY) ? hash[Esse::SETTING_ROOT_KEY] : hash) }
12
10
  end
13
11
 
14
12
  # Define /_settings definition by each index.
15
13
  #
16
14
  # +hash+: The body of the request includes the updated settings.
17
- # +block+: Overwrite default :as_json from IndexSetting instance
15
+ # +block+: Overwrite default :to_h from IndexSetting instance
18
16
  #
19
17
  # Example:
20
18
  #
@@ -30,16 +28,16 @@ module Esse
30
28
  # end
31
29
  # end
32
30
  def settings(hash = {}, &block)
33
- @setting = Esse::IndexSetting.new(body: hash, paths: template_dirs, globals: cluster.index_settings)
34
- return unless block_given?
31
+ @setting = Esse::IndexSetting.new(body: hash, paths: template_dirs, globals: -> { cluster.index_settings })
32
+ return unless block
35
33
 
36
- @setting.define_singleton_method(:as_json, &block)
34
+ @setting.define_singleton_method(:to_h, &block)
37
35
  end
38
36
 
39
37
  private
40
38
 
41
39
  def setting
42
- @setting ||= Esse::IndexSetting.new(paths: template_dirs, globals: cluster.index_settings)
40
+ @setting ||= Esse::IndexSetting.new(paths: template_dirs, globals: -> { cluster.index_settings })
43
41
  end
44
42
  end
45
43
 
data/lib/esse/index.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'core'
3
+ require_relative 'object_document_mapper'
4
4
 
5
5
  module Esse
6
6
  class Index
@@ -13,6 +13,7 @@ module Esse
13
13
  require_relative 'index/mappings'
14
14
  require_relative 'index/descendants'
15
15
  require_relative 'index/backend'
16
+ extend ObjectDocumentMapper
16
17
 
17
18
  @cluster_id = nil
18
19
 
@@ -12,14 +12,14 @@ module Esse
12
12
 
13
13
  # This method will be overwrited when passing a block during the
14
14
  # mapping defination
15
- def as_json
15
+ def to_h
16
16
  return @mappings unless @mappings.empty?
17
17
 
18
18
  from_template || @mappings
19
19
  end
20
20
 
21
21
  def body
22
- as_json
22
+ to_h
23
23
  end
24
24
 
25
25
  def empty?
@@ -3,8 +3,12 @@
3
3
  module Esse
4
4
  # https://www.elastic.co/guide/en/elasticsearch/reference/1.7/indices.html
5
5
  class IndexSetting
6
- def initialize(body: {}, paths: [], globals: {})
7
- @globals = globals || {}
6
+ # @param [Hash] options
7
+ # @option options [Proc] :globals A proc that will be called to load global settings
8
+ # @option options [Array] :paths A list of paths to load settings from
9
+ # @option options [Hash] :body A hash of settings to override
10
+ def initialize(body: {}, paths: [], globals: nil)
11
+ @globals = globals || -> { {} }
8
12
  @paths = Array(paths)
9
13
  @settings = body
10
14
  end
@@ -19,14 +23,14 @@ module Esse
19
23
  # end
20
24
  # end
21
25
  #
22
- def as_json
26
+ def to_h
23
27
  return @settings unless @settings.empty?
24
28
 
25
29
  from_template || @settings
26
30
  end
27
31
 
28
32
  def body
29
- @globals.merge(as_json)
33
+ HashUtils.deep_merge(@globals.call, to_h)
30
34
  end
31
35
 
32
36
  protected
@@ -3,7 +3,8 @@
3
3
  module Esse
4
4
  class IndexType
5
5
  module ClassMethods
6
- def action(name, options = {}, &block); end
6
+ def action(name, options = {}, &block)
7
+ end
7
8
  end
8
9
 
9
10
  extend ClassMethods
@@ -3,9 +3,10 @@
3
3
  module Esse
4
4
  class IndexType
5
5
  module ClassMethods
6
- def backend
6
+ def elasticsearch
7
7
  Esse::Backend::IndexType.new(self)
8
8
  end
9
+ alias_method :backend, :elasticsearch
9
10
  end
10
11
 
11
12
  extend ClassMethods
@@ -2,14 +2,14 @@
2
2
 
3
3
  module Esse
4
4
  class IndexType
5
- # https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/indices/put_mapping.rb
5
+ # https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-put-mapping.html
6
6
  module ClassMethods
7
7
  # This method is only used to define mapping
8
8
  def mappings(hash = {}, &block)
9
9
  @mapping = Esse::IndexMapping.new(body: hash, paths: template_dirs, filenames: mapping_filenames)
10
- return unless block_given?
10
+ return unless block
11
11
 
12
- @mapping.define_singleton_method(:as_json, &block)
12
+ @mapping.define_singleton_method(:to_h, &block)
13
13
  end
14
14
 
15
15
  # This is the actually content that will be passed through the ES api
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'object_document_mapper'
4
+
3
5
  module Esse
6
+ # Type is actually deprecated. Elasticsearch today uses _doc instead of type
7
+ # And in upcoming release it will be totally removed.
8
+ # But I want to keep compatibility with old versions of es.
4
9
  class IndexType
5
10
  require_relative 'index_type/actions'
6
11
  require_relative 'index_type/mappings'
7
- require_relative 'index_type/serializer'
8
12
  require_relative 'index_type/backend'
13
+ extend ObjectDocumentMapper
9
14
  end
10
15
  end
@@ -0,0 +1,19 @@
1
+ require 'logger'
2
+
3
+ module Esse
4
+ module Logging
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def logger
11
+ @logger ||= ::Logger.new($stdout)
12
+ end
13
+
14
+ def logger=(log)
15
+ @logger = log || ::Logger.new(File::NULL)
16
+ end
17
+ end
18
+ end
19
+ end