active_event 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +19 -19
  3. data/README.md +9 -9
  4. data/app/models/active_event/event.rb +15 -15
  5. data/app/models/active_event/event_repository.rb +11 -11
  6. data/db/migrate/00_create_domain_events.rb +9 -9
  7. data/lib/active_event/autoload.rb +11 -9
  8. data/lib/active_event/command.rb +35 -39
  9. data/lib/active_event/domain.rb +4 -2
  10. data/lib/active_event/event_server.rb +72 -76
  11. data/lib/active_event/event_source_server.rb +149 -127
  12. data/lib/active_event/event_type.rb +29 -28
  13. data/lib/active_event/replay_server.rb +94 -98
  14. data/lib/active_event/sse.rb +26 -26
  15. data/lib/active_event/support/attr_initializer.rb +76 -74
  16. data/lib/active_event/support/attr_setter.rb +31 -29
  17. data/lib/active_event/support/autoload.rb +46 -44
  18. data/lib/active_event/support/autoloader.rb +41 -38
  19. data/lib/active_event/support/multi_logger.rb +31 -28
  20. data/lib/active_event/validations.rb +68 -68
  21. data/lib/active_event/validations_registry.rb +18 -18
  22. data/lib/active_event/version.rb +1 -1
  23. data/spec/factories/event_factory.rb +9 -9
  24. data/spec/lib/command_spec.rb +14 -14
  25. data/spec/lib/domain_spec.rb +21 -21
  26. data/spec/lib/event_server_spec.rb +29 -29
  27. data/spec/lib/event_type_spec.rb +38 -38
  28. data/spec/lib/replay_server_spec.rb +71 -68
  29. data/spec/lib/support/attr_initializer_spec.rb +55 -55
  30. data/spec/lib/support/attr_setter_spec.rb +61 -61
  31. data/spec/models/event_spec.rb +20 -20
  32. data/spec/spec_helper.rb +1 -1
  33. data/spec/support/active_record.rb +40 -38
  34. metadata +2 -4
  35. data/lib/active_event/support/hash_buffer.rb +0 -24
  36. data/lib/active_event/support/ring_buffer.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fbe081963a57c1c0584f2b7f1ff75717adaab62e
4
- data.tar.gz: 1306fc4de0cb6c5c70bdc9985db4b18d7ae4110f
3
+ metadata.gz: b7c3b449c00be517eeb926f40073a2b87c187c43
4
+ data.tar.gz: 537bb16be762e9dbfd6bfe1b8cc324df759fc4ef
5
5
  SHA512:
6
- metadata.gz: d8f84c88dfd7847f01fc0a5f04c803e94106e0c73806443b884b91e3eecce830c20bb7844929cd2c918b58d37d673a4f9695e9d487fca6a0e667879aa5689595
7
- data.tar.gz: ff371ee9763f58022bca659ad011a7709b5d58345ee06b8104407ca6f23f7d5c4348decbee031b7df58e61690a1afc296aa78681209c8712d97f8caa510c952e
6
+ metadata.gz: 28ac276f48f56e6b2262f8d17de64a9f6bd78a22fbdd1f881e51596fc4a7d6f87799d0cfd8ac2078f8d6db8077486e02abc140a72fc9d381719dab7510996659
7
+ data.tar.gz: 21a962c284462e255635882d3f8fdb0b38147d1d2ed764905a794ea312bf6016a285bdeeeec42e92218a64bce1b4573bb093101027419342dc8ef62c97ee0db4
@@ -1,20 +1,20 @@
1
- Copyright (c) 2013 Andreas Reischuck
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
1
+ Copyright (c) 2013 Andreas Reischuck
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # Active Event
2
-
3
- Contains commands, events, validations for Rails Disco.
4
-
5
- Commands are used to transport updates from Rails to the domain.
6
- Validations are used to validate commands in Rails and the domain.
7
- Events are created in the domain and processed in projections.
8
-
9
- Have a look at the [rails-disco](https://github.com/hicknhack-software/rails-disco/wiki) documentation on Github for more details.
1
+ # Active Event
2
+
3
+ Contains commands, events, validations for Rails Disco.
4
+
5
+ Commands are used to transport updates from Rails to the domain.
6
+ Validations are used to validate commands in Rails and the domain.
7
+ Events are created in the domain and processed in projections.
8
+
9
+ Have a look at the [rails-disco](https://github.com/hicknhack-software/rails-disco/wiki) documentation on Github for more details.
@@ -1,15 +1,15 @@
1
- module ActiveEvent
2
- class Event < ActiveRecord::Base
3
- self.table_name = 'domain_events'
4
- serialize :data, JSON
5
-
6
- # events are only created inside the domain
7
- def readonly?
8
- persisted?
9
- end
10
-
11
- def event_type
12
- ActiveEvent::EventType.create_instance(event, data)
13
- end
14
- end
15
- end
1
+ module ActiveEvent
2
+ class Event < ActiveRecord::Base
3
+ self.table_name = 'domain_events'
4
+ serialize :data, JSON
5
+
6
+ # events are only created inside the domain
7
+ def readonly?
8
+ persisted?
9
+ end
10
+
11
+ def event_type
12
+ ActiveEvent::EventType.create_instance(event, data)
13
+ end
14
+ end
15
+ end
@@ -1,11 +1,11 @@
1
- module ActiveEvent
2
- class EventRepository
3
- def self.ordered
4
- ActiveEvent::Event.order(:id)
5
- end
6
-
7
- def self.after_id(id)
8
- ordered.where ActiveEvent::Event.arel_table['id'].gt(id)
9
- end
10
- end
11
- end
1
+ module ActiveEvent
2
+ class EventRepository
3
+ def self.ordered
4
+ ActiveEvent::Event.order(:id)
5
+ end
6
+
7
+ def self.after_id(id)
8
+ ordered.where ActiveEvent::Event.arel_table['id'].gt(id)
9
+ end
10
+ end
11
+ end
@@ -1,9 +1,9 @@
1
- class CreateDomainEvents < ActiveRecord::Migration
2
- def change
3
- create_table :domain_events do |t|
4
- t.string :event
5
- t.text :data
6
- t.datetime :created_at
7
- end
8
- end
9
- end
1
+ class CreateDomainEvents < ActiveRecord::Migration
2
+ def change
3
+ create_table :domain_events do |t|
4
+ t.string :event
5
+ t.text :data
6
+ t.datetime :created_at
7
+ end
8
+ end
9
+ end
@@ -1,9 +1,11 @@
1
- module ActiveEvent
2
- module Autoload
3
- include ActiveEvent::Support::Autoload
4
- private
5
- def self.dir_names
6
- %W(app/commands app/validations app/events)
7
- end
8
- end
9
- end
1
+ module ActiveEvent
2
+ module Autoload
3
+ include ActiveEvent::Support::Autoload
4
+
5
+ private
6
+
7
+ def self.dir_names
8
+ %w(app/commands app/validations app/events)
9
+ end
10
+ end
11
+ end
@@ -1,39 +1,35 @@
1
- require 'active_model'
2
-
3
- module ActiveEvent
4
- class CommandInvalid < Exception
5
- attr_reader :record
6
-
7
- def initialize(record)
8
- self.record = record
9
- super 'invalid command'
10
- end
11
-
12
- private
13
-
14
- attr_writer :record
15
- end
16
-
17
- module Command
18
- extend ActiveSupport::Concern
19
- include ActiveEvent::Support::AttrSetter
20
- include ActiveModel::Validations
21
-
22
- def is_valid_do
23
- yield if valid?
24
- end
25
-
26
- module ClassMethods
27
- def form_name(name)
28
- define_singleton_method(:model_name) do
29
- @_model_name ||= begin
30
- namespace = self.parents.detect do |n|
31
- n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
32
- end
33
- ActiveModel::Name.new(self, namespace, name)
34
- end
35
- end
36
- end
37
- end
38
- end
39
- end
1
+ require 'active_model'
2
+
3
+ module ActiveEvent
4
+ class CommandInvalid < Exception
5
+ attr_reader :record
6
+
7
+ def initialize(record)
8
+ self.record = record
9
+ super 'invalid command'
10
+ end
11
+
12
+ private
13
+
14
+ attr_writer :record
15
+ end
16
+
17
+ module Command
18
+ extend ActiveSupport::Concern
19
+ include ActiveEvent::Support::AttrSetter
20
+ include ActiveModel::Validations
21
+
22
+ module ClassMethods
23
+ def form_name(name)
24
+ define_singleton_method(:model_name) do
25
+ @_model_name ||= begin
26
+ namespace = parents.find do |n|
27
+ n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
28
+ end
29
+ ActiveModel::Name.new(self, namespace, name)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -7,7 +7,9 @@ module ActiveEvent
7
7
 
8
8
  class DomainException < StandardError
9
9
  # prevent better errors from tracing this exception
10
- def __better_errors_bindings_stack; [] end
10
+ def __better_errors_bindings_stack
11
+ []
12
+ end
11
13
  end
12
14
 
13
15
  module Domain
@@ -36,7 +38,7 @@ module ActiveEvent
36
38
 
37
39
  module ClassMethods
38
40
  def run_command(command)
39
- self.instance.run_command command
41
+ instance.run_command command
40
42
  end
41
43
 
42
44
  attr_accessor :server_uri
@@ -1,76 +1,72 @@
1
- require 'singleton'
2
- require 'bunny'
3
- module ActiveEvent
4
- class EventServer
5
- include Singleton
6
-
7
- def self.publish(event)
8
- type = event.class.name
9
- body = event.to_json
10
- instance.event_exchange.publish body, type: type, headers: event.store_infos
11
- LOGGER.debug "Published #{type} with #{body}"
12
- end
13
-
14
- def self.start(options)
15
- instance.options = options
16
- instance.start
17
- end
18
-
19
- def start
20
- event_connection.start
21
- listen_for_resend_requests
22
- rescue Exception => e
23
- LOGGER.error e.message
24
- LOGGER.error e.backtrace.join("\n")
25
- raise e
26
- end
27
-
28
- def resend_events_after(id)
29
- if @replay_server_thread.nil? || !@replay_server_thread.alive?
30
- @replay_server_thread = Thread.new do
31
- Thread.current.priority = -10
32
- ReplayServer.start options, id
33
- end
34
- else
35
- ReplayServer.update id
36
- end
37
- end
38
-
39
- def listen_for_resend_requests
40
- resend_request_queue.subscribe do |delivery_info, properties, id|
41
- resend_request_received id
42
- end
43
- end
44
-
45
- def resend_request_received (id)
46
- LOGGER.debug "received resend request with id #{id}"
47
- resend_events_after id.to_i
48
- end
49
-
50
- def event_connection
51
- @event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
52
- end
53
-
54
- def event_channel
55
- @event_channel ||= event_connection.create_channel
56
- end
57
-
58
- def event_exchange
59
- @event_exchange ||= event_channel.fanout options[:event_exchange]
60
- end
61
-
62
- def resend_request_exchange
63
- @resend_request_exchange ||= event_channel.direct "resend_request_#{options[:event_exchange]}"
64
- end
65
-
66
- def resend_request_queue
67
- @resend_request_queue ||= event_channel.queue('', auto_delete: true).bind(resend_request_exchange, routing_key: 'resend_request')
68
- end
69
-
70
- def options
71
- @options
72
- end
73
-
74
- attr_writer :options
75
- end
76
- end
1
+ require 'singleton'
2
+ require 'bunny'
3
+ module ActiveEvent
4
+ class EventServer
5
+ include Singleton
6
+
7
+ def self.publish(event)
8
+ type = event.class.name
9
+ body = event.to_json
10
+ instance.event_exchange.publish body, type: type, headers: event.store_infos
11
+ LOGGER.debug "Published #{type} with #{body}"
12
+ end
13
+
14
+ def self.start(options)
15
+ instance.options = options
16
+ instance.start
17
+ end
18
+
19
+ def start
20
+ event_connection.start
21
+ listen_for_resend_requests
22
+ rescue => e
23
+ LOGGER.error e.message
24
+ LOGGER.error e.backtrace.join("\n")
25
+ raise e
26
+ end
27
+
28
+ def resend_events_after(id)
29
+ if @replay_server_thread.nil? || !@replay_server_thread.alive?
30
+ @replay_server_thread = Thread.new do
31
+ Thread.current.priority = -10
32
+ ReplayServer.start options, id
33
+ end
34
+ else
35
+ ReplayServer.update id
36
+ end
37
+ end
38
+
39
+ def listen_for_resend_requests
40
+ resend_request_queue.subscribe do |_delivery_info, _properties, id|
41
+ resend_request_received id
42
+ end
43
+ end
44
+
45
+ def resend_request_received(id)
46
+ LOGGER.debug "received resend request with id #{id}"
47
+ resend_events_after id.to_i
48
+ end
49
+
50
+ def event_connection
51
+ @event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
52
+ end
53
+
54
+ def event_channel
55
+ @event_channel ||= event_connection.create_channel
56
+ end
57
+
58
+ def event_exchange
59
+ @event_exchange ||= event_channel.fanout options[:event_exchange]
60
+ end
61
+
62
+ def resend_request_exchange
63
+ @resend_request_exchange ||= event_channel.direct "resend_request_#{options[:event_exchange]}"
64
+ end
65
+
66
+ def resend_request_queue
67
+ @resend_request_queue ||= event_channel.queue('', auto_delete: true).bind(resend_request_exchange, routing_key: 'resend_request')
68
+ end
69
+
70
+ attr_accessor :options
71
+ end
72
+ end
@@ -1,127 +1,149 @@
1
- require 'singleton'
2
- require 'bunny'
3
-
4
- module ActiveEvent
5
- class ProjectionException < StandardError
6
- # prevent better errors from tracing this exception
7
- def __better_errors_bindings_stack; [] end
8
- end
9
-
10
- class EventSourceServer
11
- include Singleton
12
-
13
- class << self
14
- def after_event_projection(event_id, projection, &block)
15
- instance.after_event_projection(event_id, projection, &block)
16
- end
17
-
18
- def projection_status(projection)
19
- instance.projection_status(projection)
20
- end
21
- end
22
-
23
- def after_event_projection(event_id, projection)
24
- mutex.synchronize do
25
- projection_status = status[projection]
26
- if projection_status.event_id < event_id
27
- cv = ConditionVariable.new
28
- begin
29
- projection_status.waiters[event_id] << cv
30
- cv.wait(mutex)
31
- ensure
32
- projection_status.waiters[event_id].delete cv
33
- end
34
- end
35
- raise ProjectionException, projection_status.error, projection_status.backtrace if projection_status.error
36
- end
37
- yield
38
- end
39
-
40
- def projection_status(projection)
41
- mutex.synchronize do
42
- projection_status = status[projection]
43
- raise ProjectionException, projection_status.error, projection_status.backtrace if projection_status.error
44
- end
45
- end
46
-
47
- private
48
-
49
- class Status
50
- attr_accessor :event_id, :waiters, :error, :backtrace
51
-
52
- def initialize
53
- self.event_id = 0
54
- self.waiters = Hash.new { |h,k| h[k] = [] }
55
- end
56
- end
57
-
58
- attr_accessor :mutex # synchronize access to status
59
- attr_accessor :status # status of all projections received so far
60
-
61
- def initialize
62
- self.mutex = Mutex.new
63
- self.status = Hash.new { |h,k| h[k] = Status.new }
64
- event_connection.start
65
- event_queue.subscribe do |delivery_info, properties, body|
66
- process_projection JSON.parse(body).symbolize_keys!
67
- end
68
- end
69
-
70
- def process_projection(data)
71
- mutex.synchronize do
72
- projection_status = status[data[:projection]]
73
- projection_status.event_id = data[:event]
74
- projection_status.error = data[:error] if data.key? :error
75
- projection_status.backtrace = data[:backtrace] if data.key? :backtrace
76
- projection_status.waiters.delete(data[:event]).to_a.each { |cv| cv.signal }
77
- end
78
- end
79
-
80
- def event_queue
81
- @event_queue ||= event_channel.queue('', auto_delete: true).bind(event_exchange)
82
- end
83
-
84
- def event_connection
85
- @event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
86
- end
87
-
88
- def event_channel
89
- @event_channel ||= event_connection.create_channel
90
- end
91
-
92
- def event_exchange
93
- @@event_exchange ||= event_channel.fanout "server_side_#{options[:event_exchange]}"
94
- end
95
-
96
- def default_options
97
- {
98
- event_connection: {
99
- scheme: 'amqp',
100
- userinfo: nil,
101
- host: '127.0.0.1',
102
- port: 9797,
103
- },
104
- event_exchange: 'events'
105
- }
106
- end
107
-
108
- def parse_options(args)
109
- options = default_options
110
- options.merge! YAML.load_file(config_file)[env].deep_symbolize_keys! unless config_file.blank?
111
- end
112
-
113
- def config_file
114
- File.expand_path('config/disco.yml', Rails.root)
115
- end
116
-
117
- def options
118
- @options ||= parse_options(ARGV)
119
- end
120
-
121
- def env
122
- @env = ENV['PROJECTION_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
123
- end
124
-
125
- attr_writer :options
126
- end
127
- end
1
+ require 'singleton'
2
+ require 'bunny'
3
+
4
+ module ActiveEvent
5
+ class ProjectionException < StandardError
6
+ # prevent better errors from tracing this exception
7
+ def __better_errors_bindings_stack
8
+ []
9
+ end
10
+ end
11
+
12
+ class EventSourceServer
13
+ include Singleton
14
+
15
+ class << self
16
+ def wait_for_event_projection(event_id, projection, options = {})
17
+ instance.wait_for_event_projection(event_id, projection, options)
18
+ end
19
+
20
+ def fail_on_projection_error(projection)
21
+ instance.fail_on_projection_error(projection)
22
+ end
23
+ end
24
+
25
+ def wait_for_event_projection(event_id, projection, options = {})
26
+ mutex.synchronize do
27
+ projection_status = status[projection]
28
+ projection_status.fail_on_error # projection will not continue if error occurred
29
+ projection_status.waiter(event_id).wait(mutex, options[:timeout])
30
+ projection_status.fail_on_error
31
+ end
32
+ end
33
+
34
+ def fail_on_projection_error(projection)
35
+ mutex.synchronize do
36
+ projection_status = status[projection]
37
+ projection_status.fail_on_error
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ class Status
44
+ module UnconditionalVariable
45
+ def self.wait(_mutex, _timeout = 0)
46
+ nil
47
+ end
48
+ end
49
+
50
+ @event_id = 0
51
+ @error = nil
52
+ @backtrace = nil
53
+
54
+ def initialize
55
+ @waiters = {}
56
+ end
57
+
58
+ def waiter(event)
59
+ if event > @event_id
60
+ @waiters[event] ||= ConditionVariable.new
61
+ else
62
+ UnconditionalVariable
63
+ end
64
+ end
65
+
66
+ def set_error(error, backtrace)
67
+ @error, @backtrace = error, backtrace if error || backtrace
68
+ end
69
+
70
+ def event=(event)
71
+ @event_id = event
72
+ cvs = []
73
+ @waiters.delete_if { |event_id, cv| (event_id <= event) && (cvs << cv) }
74
+ cvs.map &:broadcast
75
+ end
76
+
77
+ def fail_on_error
78
+ fail ProjectionException, @error, @backtrace if @error
79
+ end
80
+ end
81
+
82
+ attr_accessor :mutex # synchronize access to status
83
+ attr_accessor :status # status of all projections received so far
84
+
85
+ def initialize
86
+ self.mutex = Mutex.new
87
+ self.status = Hash.new { |h, k| h[k] = Status.new }
88
+ event_connection.start
89
+ event_queue.subscribe do |_delivery_info, _properties, body|
90
+ process_projection JSON.parse(body).symbolize_keys!
91
+ end
92
+ end
93
+
94
+ def process_projection(data)
95
+ mutex.synchronize do
96
+ projection_status = status[data[:projection]]
97
+ projection_status.set_error data[:error], data[:backtrace]
98
+ projection_status.event = data[:event]
99
+ end
100
+ end
101
+
102
+ def event_queue
103
+ @event_queue ||= event_channel.queue('', auto_delete: true).bind(event_exchange)
104
+ end
105
+
106
+ def event_connection
107
+ @event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
108
+ end
109
+
110
+ def event_channel
111
+ @event_channel ||= event_connection.create_channel
112
+ end
113
+
114
+ def event_exchange
115
+ @@event_exchange ||= event_channel.fanout "server_side_#{options[:event_exchange]}"
116
+ end
117
+
118
+ def default_options
119
+ {
120
+ event_connection: {
121
+ scheme: 'amqp',
122
+ userinfo: nil,
123
+ host: '127.0.0.1',
124
+ port: 9797,
125
+ },
126
+ event_exchange: 'events',
127
+ }
128
+ end
129
+
130
+ def parse_options(_args)
131
+ options = default_options
132
+ options.merge! YAML.load_file(config_file)[env].deep_symbolize_keys! unless config_file.blank?
133
+ end
134
+
135
+ def config_file
136
+ File.expand_path('config/disco.yml', Rails.root)
137
+ end
138
+
139
+ def options
140
+ @options ||= parse_options(ARGV)
141
+ end
142
+
143
+ def env
144
+ @env = ENV['PROJECTION_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
145
+ end
146
+
147
+ attr_writer :options
148
+ end
149
+ end