fare 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +4 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/README.md +470 -0
- data/Rakefile +28 -0
- data/bin/fare +48 -0
- data/fare.gemspec +35 -0
- data/features/multiqueue.feature +60 -0
- data/features/multistack.feature +65 -0
- data/features/step_definitions/aruba.rb +1 -0
- data/features/step_definitions/fare_steps.rb +40 -0
- data/features/subscriber.feature +95 -0
- data/features/support/env.rb +34 -0
- data/lib/fare.rb +96 -0
- data/lib/fare/configuration.rb +57 -0
- data/lib/fare/configuration_dsl.rb +134 -0
- data/lib/fare/configuration_when_locked.rb +82 -0
- data/lib/fare/event.rb +26 -0
- data/lib/fare/generate_lock_file.rb +131 -0
- data/lib/fare/load_configuration_file.rb +45 -0
- data/lib/fare/middleware/logging.rb +46 -0
- data/lib/fare/middleware/newrelic.rb +35 -0
- data/lib/fare/middleware/raven.rb +47 -0
- data/lib/fare/publisher.rb +65 -0
- data/lib/fare/queue_adapter.rb +30 -0
- data/lib/fare/rspec.rb +85 -0
- data/lib/fare/subscriber.rb +35 -0
- data/lib/fare/subscriber_cli.rb +270 -0
- data/lib/fare/subscriber_stack.rb +39 -0
- data/lib/fare/test_mode.rb +204 -0
- data/lib/fare/topic.rb +25 -0
- data/lib/fare/topic_adapter.rb +13 -0
- data/lib/fare/update_cli.rb +41 -0
- data/lib/fare/version.rb +3 -0
- data/spec/logger_spec.rb +45 -0
- data/spec/raven_spec.rb +52 -0
- data/spec/rspec_integration_spec.rb +45 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/stubbed_subscribing_spec.rb +66 -0
- metadata +264 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module Fare
|
2
|
+
class SubscriberStack
|
3
|
+
|
4
|
+
attr_reader :configuration, :topics, :run, :stack
|
5
|
+
|
6
|
+
def initialize(configuration, topics, run)
|
7
|
+
@configuration = configuration
|
8
|
+
@topics = topics
|
9
|
+
@run = run
|
10
|
+
@stack = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_app
|
14
|
+
@app ||= build_stack
|
15
|
+
end
|
16
|
+
|
17
|
+
def handles?(event)
|
18
|
+
topics.any? { |topic| topic.handles?(event) }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def build_stack
|
24
|
+
instance_eval(&configuration.always_run)
|
25
|
+
instance_eval(&run)
|
26
|
+
raise "Stack is empty" if stack.empty?
|
27
|
+
stack.reverse.inject(endpoint) { |a, u| u[a] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def use(middleware, *args, &block)
|
31
|
+
stack << lambda { |app| middleware.new(app, *args, &block) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def endpoint
|
35
|
+
lambda { |event| event }
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
module Fare
|
2
|
+
module TestMode
|
3
|
+
|
4
|
+
class MessageList
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@all_messages_published = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def queue_adapter
|
11
|
+
@queue_adapter ||= QueueAdapter.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def topic_adapter
|
15
|
+
@topic_adapter ||= TopicAdapter.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def register_publish(message)
|
19
|
+
event = Event.deserialize(message)
|
20
|
+
@all_messages_published[[event.subject.to_s, event.action.to_s]] ||= event
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear
|
24
|
+
@all_messages_published.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(subject, action)
|
28
|
+
@all_messages_published[[subject.to_s, action.to_s]]
|
29
|
+
end
|
30
|
+
|
31
|
+
def size
|
32
|
+
@all_messages_published.size
|
33
|
+
end
|
34
|
+
|
35
|
+
def list
|
36
|
+
@all_messages_published.values.map { |event|
|
37
|
+
"* #{event.subject}##{event.action}"
|
38
|
+
}.join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
def given_event(event_or_queue_name, event = nil)
|
42
|
+
if event
|
43
|
+
queue_name = event_or_queue_name
|
44
|
+
else
|
45
|
+
event = event_or_queue_name
|
46
|
+
queue_name = app_name
|
47
|
+
end
|
48
|
+
serialized_event = Event.new(event).serialize
|
49
|
+
queue = fetch_queue(queue_name.to_s)
|
50
|
+
queue.publish(serialized_event)
|
51
|
+
end
|
52
|
+
|
53
|
+
def run(queue_name = app_name)
|
54
|
+
subscriber = Subscriber.new(Fare.configuration, name: queue_name)
|
55
|
+
Timeout.timeout(2) do
|
56
|
+
while message = produce_message(subscriber)
|
57
|
+
subscriber.consume(message)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def produce_message(subscriber)
|
65
|
+
q = []
|
66
|
+
subscriber.produce(q)
|
67
|
+
q.first
|
68
|
+
end
|
69
|
+
|
70
|
+
def fetch_queue(queue_name)
|
71
|
+
queue_adapter.fetch(environment, queue_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def app_name
|
75
|
+
Fare.configuration.app_name
|
76
|
+
end
|
77
|
+
|
78
|
+
def environment
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
class QueueAdapter
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
@queues = {}
|
88
|
+
end
|
89
|
+
|
90
|
+
def fetch(environment, queue_name)
|
91
|
+
@queues[queue_name] ||= FakeQueue.new(queue_name)
|
92
|
+
end
|
93
|
+
|
94
|
+
def fetch_by_arn(arn)
|
95
|
+
@queues.fetch(arn)
|
96
|
+
end
|
97
|
+
|
98
|
+
def queues
|
99
|
+
@queues.values
|
100
|
+
end
|
101
|
+
|
102
|
+
def clear
|
103
|
+
queues.each(&:clear)
|
104
|
+
end
|
105
|
+
|
106
|
+
class FakeQueue
|
107
|
+
|
108
|
+
attr_reader :arn, :messages
|
109
|
+
|
110
|
+
def initialize(name)
|
111
|
+
@arn = name
|
112
|
+
@messages = []
|
113
|
+
end
|
114
|
+
|
115
|
+
def policy=(val)
|
116
|
+
end
|
117
|
+
|
118
|
+
def url
|
119
|
+
"http://#{arn}.sqs"
|
120
|
+
end
|
121
|
+
|
122
|
+
def publish(message)
|
123
|
+
@messages << message
|
124
|
+
end
|
125
|
+
|
126
|
+
def receive_message(*)
|
127
|
+
return nil if @messages.size == 0
|
128
|
+
Item.new(self, @messages[0])
|
129
|
+
end
|
130
|
+
|
131
|
+
def delete
|
132
|
+
@messages.delete_at(0)
|
133
|
+
end
|
134
|
+
|
135
|
+
def clear
|
136
|
+
@messages = []
|
137
|
+
end
|
138
|
+
|
139
|
+
def size
|
140
|
+
@messages.size
|
141
|
+
end
|
142
|
+
|
143
|
+
Item = Struct.new(:queue, :body) do
|
144
|
+
|
145
|
+
def delete
|
146
|
+
queue.delete
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
class TopicAdapter
|
156
|
+
|
157
|
+
def initialize(message_list)
|
158
|
+
@message_list = message_list
|
159
|
+
@topics = {}
|
160
|
+
end
|
161
|
+
|
162
|
+
def create(topic_name)
|
163
|
+
@topics[topic_name] ||= Topic.new(topic_name, @message_list)
|
164
|
+
end
|
165
|
+
|
166
|
+
def fetch(arn)
|
167
|
+
@topics.fetch(arn)
|
168
|
+
end
|
169
|
+
|
170
|
+
class Topic
|
171
|
+
|
172
|
+
attr_reader :arn, :name
|
173
|
+
|
174
|
+
def initialize(name, message_list)
|
175
|
+
@name = name
|
176
|
+
@arn = name
|
177
|
+
@queues = []
|
178
|
+
@message_list = message_list
|
179
|
+
end
|
180
|
+
|
181
|
+
def subscribe(queue_arn)
|
182
|
+
@queues << queue_arn
|
183
|
+
Subscription.new
|
184
|
+
end
|
185
|
+
|
186
|
+
def owner
|
187
|
+
:owner
|
188
|
+
end
|
189
|
+
|
190
|
+
def publish(message)
|
191
|
+
@message_list.register_publish(message)
|
192
|
+
@queues.each do |arn|
|
193
|
+
Fare.queue_adapter.fetch_by_arn(arn).publish(message)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
Subscription = Struct.new(:raw_message_delivery)
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
data/lib/fare/topic.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Fare
|
2
|
+
class Topic
|
3
|
+
|
4
|
+
attr_reader :subject, :action, :version
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@subject = options.fetch(:subject)
|
8
|
+
@action = options.fetch(:action)
|
9
|
+
@version = options[:version]
|
10
|
+
end
|
11
|
+
|
12
|
+
def handles?(event)
|
13
|
+
subject.to_s == event.subject && action.to_s == event.action
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](key)
|
17
|
+
public_send(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch(key)
|
21
|
+
public_send(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Fare
|
2
|
+
class UpdateCLI
|
3
|
+
|
4
|
+
attr_reader :argv
|
5
|
+
|
6
|
+
def initialize(argv)
|
7
|
+
@argv = argv
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
options = {
|
12
|
+
filename: Fare.default_config_file,
|
13
|
+
environment: (Fare.default_environment || "development"),
|
14
|
+
force: false,
|
15
|
+
}
|
16
|
+
parser = OptionParser.new do |o|
|
17
|
+
o.banner = <<-BANNER.gsub(/^ +/, '')
|
18
|
+
Creates topics in SNS, queues in SNS and makes sure the right subscriptions are made.
|
19
|
+
Also, information is cached.
|
20
|
+
|
21
|
+
Usage: fare update [options]
|
22
|
+
BANNER
|
23
|
+
o.on "--filename FILENAME", "Location of the Fare configuration file (default: #{options[:filename]})" do |filename|
|
24
|
+
options[:filename] = filename
|
25
|
+
end
|
26
|
+
o.on_tail "-h", "--help", "Shows this help page" do
|
27
|
+
puts o
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
o.on "-E", "--environment ENVIRONMENT", "Name of the environment (default: #{options[:environment]})" do |environment|
|
31
|
+
options[:environment] = environment
|
32
|
+
end
|
33
|
+
o.on "-f", "--force", "Do it even if the lockfile is up to date" do
|
34
|
+
options[:force] = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
parser.parse!
|
38
|
+
Fare.update(options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/fare/version.rb
ADDED
data/spec/logger_spec.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
RSpec.describe "Fare::Middleware::Logging" do
|
2
|
+
|
3
|
+
let(:out) { StringIO.new }
|
4
|
+
let(:event) { Fare::Event.new(id: "abc", subject: "X", action: "Y") }
|
5
|
+
|
6
|
+
it "logs successful events" do
|
7
|
+
app = ->(env) { sleep 0.1 }
|
8
|
+
logging = logging_stack(app)
|
9
|
+
|
10
|
+
logging.call(event: event)
|
11
|
+
|
12
|
+
expect(logged.fetch("result")).to eq "success"
|
13
|
+
expect(logged.fetch("event").fetch("action")).to eq "Y"
|
14
|
+
expect(logged.fetch("event").fetch("subject")).to eq "X"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "logs errors" do
|
18
|
+
error = RuntimeError.new("my error")
|
19
|
+
app = ->(env) { sleep 0.1; raise error }
|
20
|
+
logging = logging_stack(app)
|
21
|
+
|
22
|
+
expect { logging.call(event: event) }.to raise_error error
|
23
|
+
|
24
|
+
expect(logged.fetch("result")).to eq "failure"
|
25
|
+
expect(logged.fetch("event").fetch("action")).to eq "Y"
|
26
|
+
expect(logged.fetch("event").fetch("subject")).to eq "X"
|
27
|
+
expect(logged.fetch("error_class")).to eq "RuntimeError"
|
28
|
+
expect(logged.fetch("error_message")).to eq "my error"
|
29
|
+
expect(logged.fetch("backtrace")).to eq error.backtrace
|
30
|
+
end
|
31
|
+
|
32
|
+
def logged
|
33
|
+
@logged ||= begin
|
34
|
+
out.rewind
|
35
|
+
logged = out.read
|
36
|
+
_, json = logged.split(/Handled event:/, 2)
|
37
|
+
JSON.parse(json.strip)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def logging_stack(app)
|
42
|
+
Fare::Middleware::Logging.new(app, logger: Logger.new(out))
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/spec/raven_spec.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
class Raven
|
2
|
+
|
3
|
+
attr_accessor :dsn, :logger, :environments, :excluded_exceptions, :options, :exception
|
4
|
+
|
5
|
+
def self.raven
|
6
|
+
@raven ||= new
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.configure
|
10
|
+
yield raven
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.capture_exception(exception, options = {})
|
14
|
+
raven.exception = exception
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.extra_context(options)
|
18
|
+
raven.options = { extra: options }
|
19
|
+
end
|
20
|
+
|
21
|
+
class Context
|
22
|
+
def self.clear!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec.describe "Fare::Middleware::Raven" do
|
29
|
+
|
30
|
+
it "configures Raven and logs errors" do
|
31
|
+
error = RuntimeError.new("my error")
|
32
|
+
app = ->(env) { sleep 0.1; raise error }
|
33
|
+
raven = Fare::Middleware::Raven.new(app, dsn: "http://localhost:5154", environment: "test", logger: "logger")
|
34
|
+
event = Fare::Event.new(id: "abc", subject: "X", action: "Y")
|
35
|
+
|
36
|
+
expect(Raven.raven.dsn).to eq "http://localhost:5154"
|
37
|
+
expect(Raven.raven.logger).to eq "logger"
|
38
|
+
expect(Raven.raven.environments).to eq ["test"]
|
39
|
+
expect(Raven.raven.excluded_exceptions).to eq []
|
40
|
+
|
41
|
+
expect { raven.call(event: event, foo: "bar") }.to raise_error error
|
42
|
+
|
43
|
+
expect(Raven.raven.exception).to eq error
|
44
|
+
expect(Raven.raven.options).to eq(
|
45
|
+
extra: {
|
46
|
+
"event" => Hash[event.attributes.map { |k,v| [ k.to_s, v ] }],
|
47
|
+
"foo" => "bar",
|
48
|
+
},
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
RSpec.describe "RSpec integration" do
|
2
|
+
|
3
|
+
before do
|
4
|
+
write_fare_config <<-CONFIG
|
5
|
+
publishes subject: "user", action: "login"
|
6
|
+
publishes subject: "user", action: "signup"
|
7
|
+
CONFIG
|
8
|
+
Fare.test_mode!
|
9
|
+
end
|
10
|
+
|
11
|
+
it "expects based on subject and action" do
|
12
|
+
expect {
|
13
|
+
Fare.publish(subject: "user", action: "login", payload: "payload")
|
14
|
+
}.to publish :user, :login
|
15
|
+
end
|
16
|
+
|
17
|
+
it "fails when nothing is published" do
|
18
|
+
expect {
|
19
|
+
expect { }.to publish :user, :login
|
20
|
+
}.to raise_error RSpec::Expectations::ExpectationNotMetError, "There were no events published"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "fails when the wrong event was published" do
|
24
|
+
expect {
|
25
|
+
expect {
|
26
|
+
Fare.publish(subject: "user", action: "signup", payload: "payload")
|
27
|
+
}.to publish :user, :login
|
28
|
+
}.to raise_error RSpec::Expectations::ExpectationNotMetError, "Expected event user#login, but got:\n* user#signup"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "matches the payload with the help of JsonExpressions" do
|
32
|
+
expect {
|
33
|
+
Fare.publish(subject: "user", action: "login", payload: { user_id: 123 })
|
34
|
+
}.to publish :user, :login, user_id: Fixnum
|
35
|
+
end
|
36
|
+
|
37
|
+
it "fails when payload doesn't match" do
|
38
|
+
expect {
|
39
|
+
expect {
|
40
|
+
Fare.publish(subject: "user", action: "login", payload: { user_id: "abc" })
|
41
|
+
}.to publish :user, :login, user_id: Fixnum
|
42
|
+
}.to raise_error RSpec::Expectations::ExpectationNotMetError, /Payload did not match/
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|