newman 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,29 +1,112 @@
1
+ # `Newman::MailingList` implements a simple mechanism for storing lists of email
2
+ # addresses keyed by a mailing list name.
3
+ #
4
+ # This object is meant to be used in conjunction with a
5
+ # `Newman::Store` object which is `PStore` backed, but would fairly easily map to
6
+ # arbitrary data stores via adapter objects.
7
+ #
8
+ # `Newman::MailingList` is part of Newman's **external interface**.
9
+
1
10
  module Newman
2
11
  class MailingList
12
+
13
+ # ---
14
+
15
+ # To initialize a `Newman::MailingList` object, a list name and a store object must
16
+ # be provided, i.e:
17
+ #
18
+ # store = Newman::Store.new('simple.store')
19
+ # mailing_list = Newman::MailingList.new("simple_list", store)
20
+
3
21
  def initialize(name, store)
4
22
  self.name = name
5
23
  self.store = store
6
24
  end
7
25
 
26
+ # ---
27
+
28
+ # `Newman::MailingList#subscribe` is used to add subscribers to
29
+ # the mailing list, i.e.
30
+ #
31
+ # mailing_list.subscribe('gregory.t.brown@gmail.com')
32
+ #
33
+ # If the provided email address is for a new subscriber, a new record gets
34
+ # created for that subscriber, adding them to the list. Otherwise, this
35
+ # method does not modify the mailing list.
36
+ #
37
+ # Returns true if list was modified, returns false otherwise.
38
+
8
39
  def subscribe(email)
40
+ return false if subscriber?(email)
41
+
9
42
  store[name].create(email)
43
+
44
+ true
10
45
  end
11
46
 
47
+ # ---
48
+
49
+ # `Newman::MailingList#unsubscribe` is used to remove subscribers from
50
+ # the mailing list, i.e.
51
+ #
52
+ # mailing_list.unsubscribe('gregory.t.brown@gmail.com')
53
+ #
54
+ # If the provided email address is for an existing subscriber, the record
55
+ # for that subscriber is destroyed, removing them from the list.
56
+ # Otherwise, this method does not modify the mailing list.
57
+ #
58
+ # Returns true if list was modified, returns false otherwise.
59
+
12
60
  def unsubscribe(email)
61
+ return false unless subscriber?(email)
62
+
13
63
  record = store[name].find { |e| e.contents == email }
14
64
  store[name].destroy(record.id)
65
+
66
+ true
15
67
  end
16
68
 
69
+
70
+ # ---
71
+
72
+ # `Newman::MailingList#subscriber?` is used to check if a given email address
73
+ # is on the list, i.e.
74
+ #
75
+ # mailing_list.subscriber?('gregory.t.brown@gmail.com')
76
+ #
77
+ # Returns true if a record is found which matches the given email address,
78
+ # returns false otherwise.
79
+
17
80
  def subscriber?(email)
18
81
  store[name].any? { |r| r.contents == email }
19
82
  end
20
83
 
84
+ # ---
85
+
86
+ # `Newman::MailingList#subscribers` is used to access all email addresses for
87
+ # the mailing list's subscribers, i.e:
88
+ #
89
+ # members = mailing_list.subscribers
90
+ #
91
+ # Returns an array of email addresses.
92
+
21
93
  def subscribers
22
94
  store[name].map { |r| r.contents }
23
95
  end
24
96
 
97
+ # ---
98
+
99
+ # **NOTE: Methods below this point in the file are implementation
100
+ # details, and should not be depended upon.**
101
+
25
102
  private
26
103
 
104
+ # ---
105
+
106
+ # These accessors have been made private to reflect the fact that
107
+ # `Newman::MailingList` objects are meant to point to a single
108
+ # named list within a single data store once they are created.
109
+
27
110
  attr_accessor :name, :store
28
111
  end
29
112
  end
@@ -0,0 +1,132 @@
1
+ # `Newman::Recorder` provides a simple mechanism for storing non-relational
2
+ # records within a `Newman::Store` with autoincrementing identifiers. It
3
+ # supports basic CRUD operations, and also acts as an `Enumerable` object.
4
+ #
5
+ # For an example of how to make use of `Newman::Recorder` to implement arbitrary
6
+ # persistent models, be sure to check out the implementation of the
7
+ # `Newman::MailingList` object.
8
+ #
9
+ # `Newman::Recorder` is part of Newman's **external interface**.
10
+
11
+ module Newman
12
+ Record = Struct.new(:column, :id, :contents)
13
+
14
+ class Recorder
15
+ include Enumerable
16
+
17
+ # ---
18
+
19
+ # To initialize a `Newman::Recorder` object, a `column` key
20
+ # and `store` object must be provided, i.e.
21
+ #
22
+ # store = Newman::Store.new("sample.store")
23
+ # recorder = Newman::Recorder.new(:subscribers, store)
24
+ #
25
+ # However, in most cases you should not instantiate a
26
+ # `Newman::Recorder` directly, and instead should make use of
27
+ # `Newman::Store#[]` which is syntactic sugar for the same operation.
28
+ #
29
+ # The first time a particular `column` key is referenced, two mapping
30
+ # is created for the column in the underlying data store: one which
31
+ # keeps track of the autoincrementing ids, and one that keeps track
32
+ # of the data stored within the column. It's fine to treat these
33
+ # mappings as implementation details, but we treat them as part of Newman's
34
+ # external interface because backwards-incompatible changes to them will
35
+ # result in possible data store corruption.
36
+
37
+ def initialize(column, store)
38
+ self.column = column
39
+ self.store = store
40
+
41
+ store.write do |data|
42
+ data[:identifiers][column] ||= 0
43
+ data[:columns][column] ||= {}
44
+ end
45
+ end
46
+
47
+ # ---
48
+
49
+ # `Newman::Recorder#each` iterates over all records stored in the column,
50
+ # yielding a `Newman::Record` object for each one. Because `Enumerable` is
51
+ # mixed into `Newman::Recorder`, all enumerable methods that get called on a
52
+ # recorder object end up making calls to this method.
53
+ def each
54
+ store.read do |data|
55
+ data[:columns][column].each do |id, contents|
56
+ yield(Record.new(column, id, contents))
57
+ end
58
+ end
59
+ end
60
+
61
+ # ---
62
+
63
+ # `Newman::Recorder#create` store an arbitrary Ruby object in the data
64
+ # store and returns a `Newman::Record` object which has fields for the
65
+ # `column` key, record `id`, and record `contents`. This method
66
+ # automatically generates new ids, starting with `id=1` for the
67
+ # first record and then incrementing sequentially.
68
+
69
+ def create(contents)
70
+ store.write do |data|
71
+ id = (data[:identifiers][column] += 1)
72
+
73
+ data[:columns][column][id] = contents
74
+
75
+ Record.new(column, id, contents)
76
+ end
77
+ end
78
+
79
+ # ---
80
+
81
+ # `Newman::Recorder#read` looks up a record by `id` and returns a
82
+ # `Newman::Record` object.
83
+
84
+ def read(id)
85
+ store.read do |data|
86
+ Record.new(column, id, data[:columns][column][id])
87
+ end
88
+ end
89
+
90
+ # ---
91
+
92
+ # `Newman::Recorder#update` looks up a record by `id` and yields its
93
+ # contents. The record contents are then replaced with the
94
+ # return value of the provided block.
95
+
96
+ def update(id)
97
+ store.write do |data|
98
+ data[:columns][column][id] = yield(data[:columns][column][id])
99
+
100
+ Record.new(column, id, data[:columns][column][id])
101
+ end
102
+ end
103
+
104
+ # ---
105
+
106
+ # `Newman::Recorder#destroy` looks up a record by `id` and then removes it
107
+ # from the data store. This method returns `true` whether or not a record
108
+ # was actually destroyed, which is a somewhat useless behavior and may
109
+ # need to be fixed in a future version of Newman. Patches welcome!
110
+
111
+ def destroy(id)
112
+ store.write do |data|
113
+ data[:columns][column].delete(id)
114
+ end
115
+
116
+ true
117
+ end
118
+
119
+ # ---
120
+
121
+ # **NOTE: Methods below this point in the file are implementation
122
+ # details, and should not be depended upon**
123
+ private
124
+
125
+ # ---
126
+
127
+ # These accessors have been made private to reflect the fac that
128
+ # `Newman::Recorder` objects are meant to point to a single column within a
129
+ # single data store once created.
130
+ attr_accessor :column, :store
131
+ end
132
+ end
@@ -0,0 +1,41 @@
1
+ # `Newman::RequestLogger` implements rudimentary request logging functionality,
2
+ # which is enabled by default when `Newman::Server.simple` is used to execute
3
+ # your applications.
4
+ #
5
+ # If you are only interested in making use of this logging functionality and not
6
+ # extending it or changing it in some way, you do not need to be familiar with
7
+ # the code in this file. Just be sure to note that if you add
8
+ # `service.debug_mode = true` to your configuration file, or set the Ruby
9
+ # `$DEBUG` global variable, you will get much more verbose output from
10
+ # Newman's logging system.
11
+ #
12
+ # `Newman::RequestLogger` is part of Newman's **internal interface**.
13
+
14
+ module Newman
15
+ RequestLogger = Object.new
16
+
17
+ # ---
18
+
19
+ # `Newman::RequestLogger` is implemented as a singleton object and is
20
+ # completely stateless in nature. It can be added directly as an app to
21
+ # any `Newman::Server` instance. The `Newman::Server.simple` helper method
22
+ # automatically places a `Newman::RequestLogger` at the beginning of the call chain,
23
+ # but it can be inserted at any point and will output the request email
24
+ # object at that point in the call chain.
25
+
26
+ class << RequestLogger
27
+ include EmailLogger
28
+
29
+ # ---
30
+
31
+ # `Newman::RequestLogger#call` simply delegates to
32
+ # `Newman::EmailLogger#log_email`, passing it a logger instance, the
33
+ # `"REQUEST"` prefix for the log line, and an instance
34
+ # of an email object. See `Newman::Server.tick` and `Newman::EmailLogger#log_email`
35
+ # for details.
36
+
37
+ def call(params)
38
+ log_email(params[:logger], "REQUEST", params[:request])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # `Newman::ResponseLogger` supports rudimentary response logging functionality, which is
2
+ # enabled by default when `Newman::Server.simple` is used to execute your
3
+ # applications.
4
+ #
5
+ # If you are only interested in making use of this logging functionality and not
6
+ # extending it or changing it in some way, you do not need to be familiar with
7
+ # the code in this file. Just be sure to note that if you add
8
+ # `service.debug_mode = true` to your configuration file, or set the Ruby
9
+ # `$DEBUG` global variable, you will get much more verbose output from
10
+ # Newman's logging system.
11
+ #
12
+ # `Newman::ResponseLogger` is part of Newman's **internal interface**.
13
+
14
+ module Newman
15
+ ResponseLogger = Object.new
16
+
17
+ # ---
18
+
19
+ # `Newman::ResponseLogger` is implemented as a singleton object and is
20
+ # completely stateless in nature. It can be added directly as an app to
21
+ # any `Newman::Server` instance. The `Newman::Server.simple` helper method
22
+ # automatically places a `ResponseLogger` at the end of the call chain, but
23
+ # it can be inserted at any point and will output the response email object
24
+ # at that point in the call chain.
25
+
26
+ class << ResponseLogger
27
+ include EmailLogger
28
+
29
+ # ---
30
+
31
+ # `Newman::ResponseLogger#call` simply delegates to
32
+ # `EmailLogger#log_email`, passing it a logger instance, the
33
+ # `"RESPONSE"` prefix for the log line, and an instance of an email
34
+ # object. See `Newman::Server.tick` and `Newman::EmailLogger#log_email`
35
+ # for details.
36
+
37
+ def call(params)
38
+ log_email(params[:logger], "RESPONSE", params[:response])
39
+ end
40
+ end
41
+ end
@@ -1,45 +1,219 @@
1
- module Newman
2
- Server = Object.new
3
-
4
- class << Server
5
- attr_accessor :settings, :mailer
1
+ # `Newman::Server` takes incoming mesages from a mailer object and passes them
2
+ # to applications as a request, and then delivers a response email after the
3
+ # applications have modified it.
4
+ #
5
+ # A `Newman::Server` object can be used in three distinct ways:
6
+ #
7
+ # 1) Instantiated via `Newman::Server.test_mode` and then run tick by tick
8
+ # in integration tests.
9
+ #
10
+ # 2) Instantiated via `Newman::Server.simple` which immediately executes
11
+ # an infinite polling loop.
12
+ #
13
+ # 3) Instantiated explicitly and manually configured, for maximum control.
14
+ #
15
+ # All of these different workflows are supported, but if you are simply looking
16
+ # to build applications with `Newman`, you are most likely going to end up using
17
+ # `Newman::Server.simple` because it takes care of most of the setup work for
18
+ # you and is the easiest way to run a single Newman application.
19
+ #
20
+ # `Newman::Server` is part of Newman's **external interface**.
6
21
 
7
- def test_mode(settings_file)
8
- self.settings = Newman::Settings.from_file(settings_file)
9
- self.mailer = Newman::TestMailer
22
+ module Newman
23
+ class Server
24
+ # ---
25
+
26
+ # `Newman::Server.simple` automatically generates a `Newman::Mailer` object
27
+ # and `Newman::Settings` object from the privded `settings_file`. These
28
+ # objects are then passed on to `Newman::Server.new` and a server instance
29
+ # is created. The server object is set up to run the specified `app`, with
30
+ # request and response logging support enabled. Calling this method puts
31
+ # the server in an infinite polling loop, because its final action is to
32
+ # call `Newman::Server.run`.
33
+ #
34
+ # The following example demonstrates how to use this method:
35
+ #
36
+ # ping_pong = Newman::Application.new do
37
+ # subject(:match, "ping") do
38
+ # respond(:subject => "pong")
39
+ # end
40
+ #
41
+ # default do
42
+ # respond(:subject => "You missed the ball!")
43
+ # end
44
+ # end
45
+ #
46
+ # Newman::Server.simple(ping_pong, "config/environment.rb")
47
+ #
48
+ # Given a properly configured settings file, this code will launch a polling
49
+ # server and run the simple `ping_pong` application.
10
50
 
11
- mailer.configure(settings)
51
+ def self.simple(app, settings_file)
52
+ settings = Settings.from_file(settings_file)
53
+ mailer = Mailer.new(settings)
54
+ server = new(settings, mailer)
55
+ server.apps = [RequestLogger, app, ResponseLogger]
56
+
57
+ server.run
58
+ end
59
+
60
+ # ---
61
+
62
+ # `Newman::Server.test_mode` automatically generates a `Newman::TestMailer` object
63
+ # and `Newman::Settings` object from the provided `settings_file`. These
64
+ # objects are then passed on to `Newman::Server.new` and a server instance
65
+ # which is preconfigured for use in integration testing is returned.
66
+ #
67
+ # Using the application from the `Newman::Server.simple` documentation
68
+ # above, it'd be possible to write a simple integration test using this
69
+ # method in the following way:
70
+ #
71
+ # server = Newman::Server.test_mode("config/environment.rb")
72
+ # server.apps << ping_pong
73
+ #
74
+ # mailer = server.mailer
75
+ # mailer.deliver_message(:to => "test@test.com",
76
+ # :subject => "ping)
77
+ #
78
+ # server.tick
79
+ #
80
+ # mailer.messages.first.subject.must_equal("pong")
81
+ #
82
+ # It's worth mentioning that although `Newman::Server.test_mode` is part of
83
+ # Newman's external interface, the `Newman::TestMailer` object is considered part
84
+ # of its internals. This is due to some ugly issues with global state and
85
+ # the overall brittleness of the current implementation. Expect a bit of
86
+ # weirdness if you plan to use this feature, at least until we improve upon
87
+ # it.
88
+
89
+ def self.test_mode(settings_file)
90
+ settings = Settings.from_file(settings_file)
91
+ mailer = TestMailer.new(settings)
92
+
93
+ new(settings, mailer)
12
94
  end
13
95
 
14
- def simple(apps, settings_file)
15
- self.settings = Newman::Settings.from_file(settings_file)
16
- self.mailer = Newman::Mailer
17
-
18
- mailer.configure(settings)
96
+ # ---
97
+
98
+ # To initialize a `Newman::Server` object, a settings object and mailer object must
99
+ # be provided, and a logger object may also be provided. If a logger object
100
+ # is not provided, `Newman::Server#default_logger` is called to create one.
101
+ #
102
+ # Instantiating a server object directly can be useful for building live
103
+ # integration tests, or for building cron jobs which process email
104
+ # periodically rather than in a busy-wait loop. See one of Newman's [live
105
+ # tests](https://github.com/mendicant-university/newman/blob/master/examples/live_test.rb)
106
+ # for an example of how this approach works.
19
107
 
20
- run(apps)
108
+ def initialize(settings, mailer, logger=nil)
109
+ self.settings = settings
110
+ self.mailer = mailer
111
+ self.logger = logger || default_logger
112
+ self.apps = []
21
113
  end
22
114
 
23
- def run(apps)
115
+ # ---
116
+
117
+ # These accessors are mostly meant for use with server objects under test
118
+ # mode, or server objects that have been explicitly instantiated. If you are
119
+ # using `Newman::Server.simple` to run your apps, it's safe to treat these
120
+ # as an implementation detail; all important data will get passed down
121
+ # into your apps on each `tick`.
122
+
123
+ attr_accessor :settings, :mailer, :apps, :logger
124
+
125
+ # ---
126
+
127
+ # `Newman::Server.run` kicks off a busy wait loop, alternating between
128
+ # calling `Newman::Server.tick` and sleeping for the amount of time
129
+ # specified by `settings.service.polling_interval`. We originally planned to
130
+ # use an EventMachine periodic timer here to potentially make running
131
+ # several servers within a single process easier, but had trouble coming up
132
+ # with a use case that made the extra dependency worth it.
133
+
134
+ def run
24
135
  loop do
25
- tick(apps)
136
+ tick
26
137
  sleep settings.service.polling_interval
27
138
  end
28
139
  end
29
140
 
30
- def tick(apps)
31
- mailer.messages.each do |request|
141
+ # ---
142
+
143
+ # `Newman::Server.tick` runs the following sequence for each incoming
144
+ # request.
145
+ #
146
+ # 1) A response is generated with the TO field set to the FROM field of the
147
+ # request, and the FROM field set to `settings.service.default_sender`.
148
+ # Applications can change these values later, but these are sensible
149
+ # defaults that work for most common needs.
150
+ #
151
+ # 2) The list of `apps` is iterated over sequentially, and each
152
+ # application's `call` method is invoked with a parameters hash which
153
+ # include the `request` email, the `response` email, the `settings` object
154
+ # being used by the server, and the `logger` object being used by the
155
+ # server.
156
+ #
157
+ # 2a) If any application raises an exception, that exception is caught and
158
+ # the processing of the current request is halted. Details about the failure
159
+ # are logged and if `settings.service.raise_exceptions` is enabled, the
160
+ # exception is re-raised, typically taking the server down with it. This
161
+ # setting is off by default.
162
+ #
163
+ # 3) Assuming an exception is not encountered, the response is delivered.
164
+
165
+ def tick
166
+ mailer.messages.each do |request|
32
167
  response = mailer.new_message(:to => request.from,
33
168
  :from => settings.service.default_sender)
169
+
170
+ begin
171
+ apps.each do |app|
172
+ app.call(:request => request,
173
+ :response => response,
174
+ :settings => settings,
175
+ :logger => logger)
176
+ end
177
+ rescue StandardError => e
178
+ logger.info("FAIL") { e.to_s }
179
+ logger.debug("FAIL") { "#{e.inspect}\n"+e.backtrace.join("\n ") }
34
180
 
35
- Array(apps).each do |a|
36
- a.call(:request => request,
37
- :response => response,
38
- :settings => settings)
181
+ if settings.service.raise_exceptions
182
+ raise
183
+ else
184
+ next
185
+ end
39
186
  end
40
187
 
41
188
  response.deliver
42
189
  end
43
190
  end
191
+
192
+ # ---
193
+
194
+ # **NOTE: Methods below this point in the file are implementation
195
+ # details, and should not be depended upon**
196
+
197
+ private
198
+
199
+ # ---
200
+
201
+ # `Newman::Server#default_logger` generates a logger object using
202
+ # Ruby's standard library. This object outputs to `STDERR`, and
203
+ # runs at info level by default, but will run at debug level if
204
+ # either `settings.service.debug_mode` or the Ruby `$DEBUG`
205
+ # variable is set.
206
+
207
+ def default_logger
208
+ self.logger = Logger.new(STDERR)
209
+
210
+ if settings.service.debug_mode || $DEBUG
211
+ logger.level = Logger::DEBUG
212
+ else
213
+ logger.level = Logger::INFO
214
+ end
215
+
216
+ logger
217
+ end
44
218
  end
45
219
  end