newman 0.1.1 → 0.2.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.
@@ -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