evnt 3.0.1 → 3.0.2
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 +5 -5
- data/README.md +118 -24
- data/lib/evnt/command.rb +3 -3
- data/lib/evnt/event.rb +1 -1
- data/lib/evnt/version.rb +2 -4
- data/lib/generators/evnt/command_generator.rb +38 -0
- data/lib/generators/evnt/event_generator.rb +36 -0
- data/lib/generators/evnt/handler_generator.rb +35 -0
- data/lib/generators/evnt/initializer_generator.rb +25 -0
- data/lib/generators/evnt/templates/command/command.rb.erb +13 -0
- data/lib/generators/evnt/templates/event/event.rb.erb +15 -0
- data/lib/generators/evnt/templates/handler/handler.rb.erb +13 -0
- data/lib/generators/evnt/templates/initializer/app/commands/application_command.rb +5 -0
- data/lib/generators/evnt/templates/initializer/app/events/application_event.rb +5 -0
- data/lib/generators/evnt/templates/initializer/app/handlers/application_handler.rb +5 -0
- data/lib/generators/evnt/templates/initializer/test/commands/application_command_test.rb +14 -0
- data/lib/generators/evnt/templates/initializer/test/events/application_event_test.rb +15 -0
- data/lib/generators/evnt/templates/initializer/test/handlers/application_handler_test.rb +13 -0
- metadata +16 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0d2cecb4f90544de842098f1f89110548ef2a2231504ac0cf47f821bf0abfbb3
|
4
|
+
data.tar.gz: 731c1460b90c981043850df9a5a5b6cc6b9a4d1d0b780859e2070cd0d94c8047
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e93adc9ffeead5ee8632b41d1bee8a030376d1876f61bf6d8e5c4f077fb5efda56c7a3a92a62493a3e3f118a153be0436dc9c5eb6ee87cd335530efc7f9300c9
|
7
|
+
data.tar.gz: 69e531d6aa91fd78f31c75bd3eb4cc30e9848f917bde1e81e191959633736f85e4b3ae7638104df84ecaf5bdaf32f301f8ec6e1348deb44c02ffeffb7895b82d
|
data/README.md
CHANGED
@@ -8,14 +8,25 @@ CQRS and Event Driven Development architecture for Ruby projects.
|
|
8
8
|
- [Event](#event)
|
9
9
|
- [Handler](#handler)
|
10
10
|
- [Rails integration](#rails-integration)
|
11
|
+
- [Generators integration](#generators-integration)
|
12
|
+
- [Command generators](#command-generators)
|
13
|
+
- [Event generators](#event-generators)
|
14
|
+
- [Handler generators](#handler-generators)
|
15
|
+
- [Manual integration](#manual-integration)
|
11
16
|
- [Development](#development)
|
12
17
|
|
13
|
-
Full documentation here: https://ideonetwork.github.io/
|
18
|
+
Full documentation here: https://ideonetwork.github.io/evnt/
|
14
19
|
|
15
20
|
## Installation
|
16
21
|
|
17
22
|
To use the gem you need to add it on your Gemfile
|
18
23
|
|
24
|
+
Latest version
|
25
|
+
```ruby
|
26
|
+
gem 'evnt', git: 'https://github.com/ideonetwork/evnt'
|
27
|
+
```
|
28
|
+
|
29
|
+
Legacy version
|
19
30
|
```ruby
|
20
31
|
gem 'evnt'
|
21
32
|
```
|
@@ -30,7 +41,7 @@ Evnt is developed to be used over all kinds of projects and frameworks (like Rub
|
|
30
41
|
|
31
42
|
### Command
|
32
43
|
|
33
|
-
Commands are used to run single tasks on the system. It's like a controller on an MVC architecture
|
44
|
+
Commands are used to run single tasks on the system. It's like a controller on an MVC architecture.
|
34
45
|
|
35
46
|
Every command has three steps to execute:
|
36
47
|
|
@@ -44,15 +55,16 @@ An example of command should be:
|
|
44
55
|
```ruby
|
45
56
|
class CreateOrderCommand < Evnt::Command
|
46
57
|
|
58
|
+
validates :user_id, type: :integer, presence: true
|
59
|
+
validates :product_id, type: :integer, presence: true
|
60
|
+
validates :quantity, type: :integer, presence: true
|
61
|
+
|
47
62
|
to_normalize_params do
|
48
63
|
params[:quantity] = params[:quantity].to_i
|
49
64
|
end
|
50
65
|
|
51
66
|
to_validate_params do
|
52
|
-
|
53
|
-
err 'User should be present' if params[:user_id].blank?
|
54
|
-
err 'Product should be present' if params[:product_id].blank?
|
55
|
-
err 'Quantity should be present' if params[:quantity].blank?
|
67
|
+
err 'Quantity should be positive' unless params[:quantity].positive?
|
56
68
|
end
|
57
69
|
|
58
70
|
to_validate_logic do
|
@@ -136,22 +148,6 @@ rescue => e
|
|
136
148
|
end
|
137
149
|
```
|
138
150
|
|
139
|
-
Some validations are similar for every command (like presence or paramter type), so you can also use general validations instead of **to_validate_params** block. An example of general validations should be:
|
140
|
-
|
141
|
-
```ruby
|
142
|
-
|
143
|
-
class CreateOrderCommand < Evnt::Command
|
144
|
-
|
145
|
-
validates :user_id, type: :integer, presence: true
|
146
|
-
validates :product_id, type: :integer, presence: true
|
147
|
-
validates :quantity, type: :integer, presence: true
|
148
|
-
|
149
|
-
# ...
|
150
|
-
|
151
|
-
end
|
152
|
-
|
153
|
-
```
|
154
|
-
|
155
151
|
### Event
|
156
152
|
|
157
153
|
Events are used to save on a persistent data structure what happends on the system.
|
@@ -180,7 +176,10 @@ class CreateOrderEvent < Evnt::Event
|
|
180
176
|
|
181
177
|
to_write_event do
|
182
178
|
# save event on database
|
183
|
-
Event.create(
|
179
|
+
Event.create(
|
180
|
+
name: name,
|
181
|
+
payload: payload
|
182
|
+
)
|
184
183
|
end
|
185
184
|
|
186
185
|
end
|
@@ -201,7 +200,7 @@ puts event.attributes # -> [:order_id, :user_id, :product_id, :quantity]
|
|
201
200
|
puts event.payload # -> { order_id: 1, user_id: 128, product_id: 534, quantity: 10, evnt: { timestamp: 2017010101, name: 'create_order' } }
|
202
201
|
```
|
203
202
|
|
204
|
-
The event payload should contain all event attributes and a
|
203
|
+
The event payload should contain all event attributes and a reserved attributes "evnt" used to store the event timestamp and the event name.
|
205
204
|
|
206
205
|
It's also possible to give datas to the event without save them on the event payload, to do this you shuld only use a key with "_" as first character. An example should be:
|
207
206
|
|
@@ -283,6 +282,101 @@ end
|
|
283
282
|
|
284
283
|
Evnt can be used with Ruby on Rails to extends the MVC pattern.
|
285
284
|
|
285
|
+
### Generators integration
|
286
|
+
|
287
|
+
There's a simple generator that can be used to inizialize a Rails application with Evnt.
|
288
|
+
|
289
|
+
```shell
|
290
|
+
|
291
|
+
rails generate evnt:initializer
|
292
|
+
|
293
|
+
```
|
294
|
+
|
295
|
+
This command should:
|
296
|
+
|
297
|
+
- Create an **application_command.rb** on app/commands directory.
|
298
|
+
- Create an **application_event.rb** on app/events directory.
|
299
|
+
- Create an **application_handler.rb** on app/handlers directory.
|
300
|
+
- Create tests for the three new classes added on the project.
|
301
|
+
- Added app/commands, app/events and app/handlers on config.autoload_paths setting of the application.
|
302
|
+
|
303
|
+
#### Command generators
|
304
|
+
|
305
|
+
Usage:
|
306
|
+
|
307
|
+
```shell
|
308
|
+
|
309
|
+
rails generate evnt:command Authentication::LoginCommand email:string password:string ip_address:string
|
310
|
+
|
311
|
+
```
|
312
|
+
|
313
|
+
Output:
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
# ./app/commands/authentication/login_command.rb
|
317
|
+
module Authentication
|
318
|
+
class LoginCommand < ApplicationCommand
|
319
|
+
|
320
|
+
validates :email, type: :string
|
321
|
+
|
322
|
+
validates :password, type: :string
|
323
|
+
|
324
|
+
validates :ip_address, type: :string
|
325
|
+
|
326
|
+
end
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
#### Event generators
|
331
|
+
|
332
|
+
Usage:
|
333
|
+
|
334
|
+
```shell
|
335
|
+
|
336
|
+
rails generate evnt:event Authentication::LoginEvent user_uuid ip_address
|
337
|
+
|
338
|
+
```
|
339
|
+
|
340
|
+
Output:
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
# ./app/events/authentication/login_event.rb
|
344
|
+
module Authentication
|
345
|
+
class LoginEvent < ApplicationEvent
|
346
|
+
|
347
|
+
name_is :authentication_login_event
|
348
|
+
|
349
|
+
attributes_are :user_uuid, :ip_address
|
350
|
+
|
351
|
+
end
|
352
|
+
end
|
353
|
+
```
|
354
|
+
|
355
|
+
#### Handler generators
|
356
|
+
|
357
|
+
Usage:
|
358
|
+
|
359
|
+
```shell
|
360
|
+
|
361
|
+
rails generate evnt:handler AuthenticationHandler authentication_login_event authentication_signup_event
|
362
|
+
|
363
|
+
```
|
364
|
+
|
365
|
+
Output:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
# ./app/handlers/authentication_handler.rb
|
369
|
+
class AuthenticationHandler < ApplicationHandler
|
370
|
+
|
371
|
+
on :authentication_login_event do; end
|
372
|
+
|
373
|
+
on :authentication_signup_event do; end
|
374
|
+
|
375
|
+
end
|
376
|
+
```
|
377
|
+
|
378
|
+
### Manual integration
|
379
|
+
|
286
380
|
To use the gem with Rails you need to create three folders inside the ./app project's path:
|
287
381
|
|
288
382
|
- **./app/commands**
|
data/lib/evnt/command.rb
CHANGED
@@ -26,7 +26,7 @@ module Evnt
|
|
26
26
|
#
|
27
27
|
# * +exceptions+ - Boolean value used to activate the throw of excetions.
|
28
28
|
##
|
29
|
-
def initialize(params)
|
29
|
+
def initialize(params = {})
|
30
30
|
_init_command_data(params)
|
31
31
|
_run_command_steps
|
32
32
|
end
|
@@ -96,7 +96,7 @@ module Evnt
|
|
96
96
|
)
|
97
97
|
|
98
98
|
# raise error if command needs exceptions
|
99
|
-
raise
|
99
|
+
raise message if @options[:exceptions]
|
100
100
|
end
|
101
101
|
|
102
102
|
# Private functions:
|
@@ -111,7 +111,7 @@ module Evnt
|
|
111
111
|
result: true,
|
112
112
|
errors: []
|
113
113
|
}
|
114
|
-
|
114
|
+
|
115
115
|
# set options
|
116
116
|
options = params[:_options] || {}
|
117
117
|
@options = {
|
data/lib/evnt/event.rb
CHANGED
data/lib/evnt/version.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module Evnt
|
6
|
+
|
7
|
+
# CommandGenerator.
|
8
|
+
class CommandGenerator < Rails::Generators::Base
|
9
|
+
|
10
|
+
source_root File.expand_path('../templates', __FILE__)
|
11
|
+
|
12
|
+
argument :informations, type: :array, optional: false
|
13
|
+
|
14
|
+
def create_comand
|
15
|
+
path = informations.first.split('::')
|
16
|
+
@command_class = path.last.camelize
|
17
|
+
@command_modules = path - [path.last]
|
18
|
+
@command_params = (informations - [informations.first]).map do |data|
|
19
|
+
data = data.split(':')
|
20
|
+
data.length > 1 ? ":#{data.first}, type: :#{data.last}" : ":#{data.first}"
|
21
|
+
end
|
22
|
+
|
23
|
+
template(
|
24
|
+
'./command/command.rb.erb',
|
25
|
+
command_path
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def command_path
|
30
|
+
path = './app/commands'
|
31
|
+
@command_modules.map { |m| path = "#{path}/#{m.underscore}" }
|
32
|
+
path = "#{path}/#{@command_class.underscore}.rb"
|
33
|
+
path
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module Evnt
|
6
|
+
|
7
|
+
# EventGenerator.
|
8
|
+
class EventGenerator < Rails::Generators::Base
|
9
|
+
|
10
|
+
source_root File.expand_path('../templates', __FILE__)
|
11
|
+
|
12
|
+
argument :informations, type: :array, optional: false
|
13
|
+
|
14
|
+
def create_event
|
15
|
+
path = informations.first.split('::')
|
16
|
+
@event_class = path.last.camelize
|
17
|
+
@event_modules = path - [path.last]
|
18
|
+
@event_name = path.map(&:underscore).join('_')
|
19
|
+
@event_attributes = (informations - [informations.first]).map { |a| ":#{a}" }.join(', ')
|
20
|
+
|
21
|
+
template(
|
22
|
+
'./event/event.rb.erb',
|
23
|
+
event_path
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def event_path
|
28
|
+
path = './app/events'
|
29
|
+
@event_modules.map { |m| path = "#{path}/#{m.underscore}" }
|
30
|
+
path = "#{path}/#{@event_class.underscore}.rb"
|
31
|
+
path
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module Evnt
|
6
|
+
|
7
|
+
# HandlerGenerator.
|
8
|
+
class HandlerGenerator < Rails::Generators::Base
|
9
|
+
|
10
|
+
source_root File.expand_path('../templates', __FILE__)
|
11
|
+
|
12
|
+
argument :informations, type: :array, optional: false
|
13
|
+
|
14
|
+
def create_handler
|
15
|
+
path = informations.first.split('::')
|
16
|
+
@handler_class = path.last.camelize
|
17
|
+
@handler_modules = path - [path.last]
|
18
|
+
@handler_events = (informations - [informations.first])
|
19
|
+
|
20
|
+
template(
|
21
|
+
'./handler/handler.rb.erb',
|
22
|
+
handler_path
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def handler_path
|
27
|
+
path = './app/handlers'
|
28
|
+
@handler_modules.map { |m| path = "#{path}/#{m.underscore}" }
|
29
|
+
path = "#{path}/#{@handler_class.underscore}.rb"
|
30
|
+
path
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module Evnt
|
6
|
+
|
7
|
+
# InitializerGenerator.
|
8
|
+
class InitializerGenerator < Rails::Generators::Base
|
9
|
+
|
10
|
+
source_root File.expand_path('../templates', __FILE__)
|
11
|
+
|
12
|
+
def create_initializer
|
13
|
+
directory './initializer', './'
|
14
|
+
update_config_application
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_config_application
|
18
|
+
application "config.autoload_paths += %W[\#{Rails.root}/app/commands]"
|
19
|
+
application "config.autoload_paths += %W[\#{Rails.root}/app/events]"
|
20
|
+
application "config.autoload_paths += %W[\#{Rails.root}/app/handlers]"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
<% @command_modules.each_with_index do |module_name, index| %>
|
3
|
+
<%= ' ' * index %>module <%= module_name %>
|
4
|
+
<% end %>
|
5
|
+
<%= ' ' * @command_modules.length %># <%= @command_class %>
|
6
|
+
<%= ' ' * @command_modules.length %>class <%= @command_class %> < ApplicationCommand
|
7
|
+
<% @command_params.each do |param| %>
|
8
|
+
<%= ' ' * (@command_modules.length + 1) %>validates <%= param %>
|
9
|
+
<% end %>
|
10
|
+
<%= ' ' * @command_modules.length %>end
|
11
|
+
<% @command_modules.each_with_index do |_module_name, index| %>
|
12
|
+
<%= ' ' * (@command_modules.length - index - 1) %>end
|
13
|
+
<% end %>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
<% @event_modules.each_with_index do |module_name, index| %>
|
3
|
+
<%= ' ' * index %>module <%= module_name %>
|
4
|
+
<% end %>
|
5
|
+
<%= ' ' * @event_modules.length %># <%= @event_class %>
|
6
|
+
<%= ' ' * @event_modules.length %>class <%= @event_class %> < ApplicationEvent
|
7
|
+
|
8
|
+
<%= ' ' * (@event_modules.length + 1) %>name_is :<%= @event_name %>
|
9
|
+
<% unless @event_attributes.blank? %>
|
10
|
+
<%= ' ' * (@event_modules.length + 1) %>attributes_are <%= @event_attributes %>
|
11
|
+
<% end %>
|
12
|
+
<%= ' ' * @event_modules.length %>end
|
13
|
+
<% @event_modules.each_with_index do |_module_name, index| %>
|
14
|
+
<%= ' ' * (@event_modules.length - index - 1) %>end
|
15
|
+
<% end %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
<% @handler_modules.each_with_index do |module_name, index| %>
|
3
|
+
<%= ' ' * index %>module <%= module_name %>
|
4
|
+
<% end %>
|
5
|
+
<%= ' ' * @handler_modules.length %># <%= @handler_class %>
|
6
|
+
<%= ' ' * @handler_modules.length %>class <%= @handler_class %> < ApplicationHandler
|
7
|
+
<% @handler_events.each do |event| %>
|
8
|
+
<%= ' ' * (@handler_modules.length + 1) %>on :<%= event %> do; end
|
9
|
+
<% end %>
|
10
|
+
<%= ' ' * @handler_modules.length %>end
|
11
|
+
<% @handler_modules.each_with_index do |_module_name, index| %>
|
12
|
+
<%= ' ' * (@handler_modules.length - index - 1) %>end
|
13
|
+
<% end %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
# ApplicationCommandTest.
|
6
|
+
class ApplicationCommandTest < ActiveSupport::TestCase
|
7
|
+
|
8
|
+
test 'it should be initialized' do
|
9
|
+
command = ApplicationCommand.new
|
10
|
+
assert_not_nil command
|
11
|
+
assert command.completed?
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
# ApplicationEventTest.
|
6
|
+
class ApplicationEventTest < ActiveSupport::TestCase
|
7
|
+
|
8
|
+
test 'it should be initialized' do
|
9
|
+
event = ApplicationEvent.new
|
10
|
+
assert_not_nil event
|
11
|
+
assert_not_nil event.payload
|
12
|
+
assert_not_nil event.payload[:evnt]
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
# ApplicationHandlerTest.
|
6
|
+
class ApplicationHandlerTest < ActiveSupport::TestCase
|
7
|
+
|
8
|
+
test 'it should be initialized' do
|
9
|
+
handler = ApplicationHandler.new
|
10
|
+
assert_not_nil handler
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: evnt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ideonetwork
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-02-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -67,6 +67,19 @@ files:
|
|
67
67
|
- lib/evnt/handler.rb
|
68
68
|
- lib/evnt/validator.rb
|
69
69
|
- lib/evnt/version.rb
|
70
|
+
- lib/generators/evnt/command_generator.rb
|
71
|
+
- lib/generators/evnt/event_generator.rb
|
72
|
+
- lib/generators/evnt/handler_generator.rb
|
73
|
+
- lib/generators/evnt/initializer_generator.rb
|
74
|
+
- lib/generators/evnt/templates/command/command.rb.erb
|
75
|
+
- lib/generators/evnt/templates/event/event.rb.erb
|
76
|
+
- lib/generators/evnt/templates/handler/handler.rb.erb
|
77
|
+
- lib/generators/evnt/templates/initializer/app/commands/application_command.rb
|
78
|
+
- lib/generators/evnt/templates/initializer/app/events/application_event.rb
|
79
|
+
- lib/generators/evnt/templates/initializer/app/handlers/application_handler.rb
|
80
|
+
- lib/generators/evnt/templates/initializer/test/commands/application_command_test.rb
|
81
|
+
- lib/generators/evnt/templates/initializer/test/events/application_event_test.rb
|
82
|
+
- lib/generators/evnt/templates/initializer/test/handlers/application_handler_test.rb
|
70
83
|
homepage: http://ideonetwork.it/
|
71
84
|
licenses:
|
72
85
|
- MIT
|
@@ -87,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
100
|
version: '0'
|
88
101
|
requirements: []
|
89
102
|
rubyforge_project:
|
90
|
-
rubygems_version: 2.
|
103
|
+
rubygems_version: 2.7.3
|
91
104
|
signing_key:
|
92
105
|
specification_version: 4
|
93
106
|
summary: CQRS and Event Driven Development architecture for Ruby
|