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.
@@ -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