active_event 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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