fare 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,13 @@
1
+ module Fare
2
+ class TopicAdapter
3
+
4
+ def self.fetch(arn)
5
+ AWS::SNS.new.topics[arn]
6
+ end
7
+
8
+ def self.create(topic_name)
9
+ AWS::SNS.new.topics.create(topic_name)
10
+ end
11
+
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module Fare
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
@@ -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