fare 0.1.0
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.
- 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
|