rddd 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +12 -10
- data/README.md +3 -61
- data/documentation/rddd-elements.png +0 -0
- data/lib/rddd/configuration.rb +4 -1
- data/lib/rddd/presenters/cache_entry.rb +28 -0
- data/lib/rddd/presenters/cacheable.rb +148 -0
- data/lib/rddd/presenters/presenter.rb +64 -0
- data/lib/rddd/services/remote_service.rb +59 -0
- data/lib/rddd/services/service_bus.rb +10 -1
- data/lib/rddd/version.rb +1 -1
- data/spec/lib/presenters/integration_spec.rb +188 -0
- data/spec/lib/services/remote_service_spec.rb +38 -0
- data/spec/lib/services/service_bus_spec.rb +11 -0
- metadata +11 -7
- data/lib/rddd/views/cache.rb +0 -24
- data/lib/rddd/views/cacheable.rb +0 -50
- data/lib/rddd/views/view.rb +0 -38
- data/spec/lib/views/integration_spec.rb +0 -153
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,29 +1,31 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rddd (0.2.
|
4
|
+
rddd (0.2.1)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
+
curb (0.8.3)
|
9
10
|
diff-lcs (1.1.3)
|
10
11
|
metaclass (0.0.1)
|
11
|
-
mocha (0.
|
12
|
+
mocha (0.13.1)
|
12
13
|
metaclass (~> 0.0.1)
|
13
|
-
rake (0.
|
14
|
-
rspec (2.
|
15
|
-
rspec-core (~> 2.
|
16
|
-
rspec-expectations (~> 2.
|
17
|
-
rspec-mocks (~> 2.
|
18
|
-
rspec-core (2.
|
19
|
-
rspec-expectations (2.
|
14
|
+
rake (10.0.3)
|
15
|
+
rspec (2.12.0)
|
16
|
+
rspec-core (~> 2.12.0)
|
17
|
+
rspec-expectations (~> 2.12.0)
|
18
|
+
rspec-mocks (~> 2.12.0)
|
19
|
+
rspec-core (2.12.2)
|
20
|
+
rspec-expectations (2.12.1)
|
20
21
|
diff-lcs (~> 1.1.3)
|
21
|
-
rspec-mocks (2.
|
22
|
+
rspec-mocks (2.12.1)
|
22
23
|
|
23
24
|
PLATFORMS
|
24
25
|
ruby
|
25
26
|
|
26
27
|
DEPENDENCIES
|
28
|
+
curb
|
27
29
|
mocha
|
28
30
|
rake
|
29
31
|
rddd!
|
data/README.md
CHANGED
@@ -14,69 +14,11 @@ Intention of rddd is to provide basic skeleton for DDD ruby application. Its fra
|
|
14
14
|
* Provide basic DDD elements
|
15
15
|
* Support separation of domain from delivery mechanism (mostly web framework such Rails or Sinatra)
|
16
16
|
* Support separation of domain from persistancy mechanism
|
17
|
+
* Make it easy to postpone decisions about background jobs processing or spliting the application into pieces
|
17
18
|
|
18
|
-
##
|
19
|
+
## Elements
|
19
20
|
|
20
|
-
![
|
21
|
-
|
22
|
-
There are two major entry points to the domain layer from the delivery mechanism. Service bus, for a command execution and
|
23
|
-
Presenter factory, to build up the presented business data (views / reports).
|
24
|
-
|
25
|
-
### Service bus
|
26
|
-
|
27
|
-
To further enhance the separation of delivery mechanism and domain from each other, every call to the domain service is done through the ```Service bus```. The key design goal is to decouple concrete service implementation from the non-domain code, so they never have to be directly instantiated outside the domain itself. Instead, service bus defines simple protocol for any object, which can call its ```execute``` method with service name and parameters. The instantiation of the service object then becomes implementation detail of domain code, thus leaves the flexibility for alternative solutions within domain.
|
28
|
-
|
29
|
-
In Rails application, it might look like:
|
30
|
-
|
31
|
-
class ProjectsController < ApplicationController
|
32
|
-
include ServiceBus
|
33
|
-
|
34
|
-
def create
|
35
|
-
execute(:create_project, params) do |errors|
|
36
|
-
render :new, :errors => errors
|
37
|
-
return
|
38
|
-
end
|
39
|
-
|
40
|
-
redirect_to projects_path, :notice => 'Project was successfully created!'
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
### Presenter factory
|
45
|
-
|
46
|
-
We dont wanna DM (Delivery mechanism) know too much about domain implementation internals. Neither about specific presenter classes. We apply same approach as for Service bus and define simple interface to load a specific view.
|
47
|
-
|
48
|
-
In Rails application, it might be used as:
|
49
|
-
|
50
|
-
class ProjectsController < ApplicationController
|
51
|
-
def index
|
52
|
-
PresenterFactory.build(:projects_by_account, :account_id => params[:account_id])
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
### Services
|
57
|
-
|
58
|
-
Service is the object to represent a single use case. Its responsibility is to orchestrate the cooperation of multiple aggregate roots (and entities), in a given order defined by use case.
|
59
|
-
|
60
|
-
Service takes a list of attributes, which are necessary to execute the given use case. At the end nothing is returned, so service represent the command to the domain and doesn't return any data back. Therefore, client code should construct and pass all the data for the given use case and keep them, so it doesn't have to use presenters to load the result of the operation back.
|
61
|
-
|
62
|
-
### Presenters
|
63
|
-
|
64
|
-
Presenter is the view to the domain data, which is typically aggregation over multiple aggregate roots and entities. In other
|
65
|
-
language presenter is report. In Rddd, views output plain Ruby hashes, thus no domain object is leaking outside the domain
|
66
|
-
itself. The key design goal was to dont let framework later call any additional adhoc methods. Thats why framework doesn't interact with view object directly and pure Hash is returned back.
|
67
|
-
|
68
|
-
### Aggregate root
|
69
|
-
|
70
|
-
### Entity
|
71
|
-
|
72
|
-
### Repository
|
73
|
-
|
74
|
-
### Factory
|
75
|
-
|
76
|
-
## Planned features
|
77
|
-
|
78
|
-
* Asynchronous notifications from services - Services might be executed synchronous but also asynchronous way. Simple extension of Service bus would do the trick (add ```execute_async``` method). Its a common case that client code might
|
79
|
-
be waiting for the result from background job execution so it can talk back to user. There is a plan to add ```Notifier``` interface, which would be available during service execution. This interface should get concrete implementation within delivery mechanism (websockets, ...).
|
21
|
+
![Elements](https://github.com/petrjanda/rddd/blob/master/documentation/rddd-elements.png?raw=true)
|
80
22
|
|
81
23
|
## Project file structure
|
82
24
|
|
Binary file
|
data/lib/rddd/configuration.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Rddd
|
2
|
+
module Presenters
|
3
|
+
#
|
4
|
+
# Cache object providing several caching methods on top of the chosen
|
5
|
+
# strategy.
|
6
|
+
#
|
7
|
+
class CacheEntry
|
8
|
+
def initialize(key, strategy)
|
9
|
+
@key = key
|
10
|
+
@strategy = strategy
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete
|
14
|
+
write(nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
def read
|
18
|
+
@strategy.get(@key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(data, timeout = nil)
|
22
|
+
expire_at = timeout ? Time.now + timeout : nil
|
23
|
+
|
24
|
+
@strategy.set(@key, data, expire_at)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Rddd
|
2
|
+
module Presenters
|
3
|
+
#
|
4
|
+
# View extension, which would make it cacheable. In case you wanna use it
|
5
|
+
# be sure to have a CacheStrategy set in the configuration. As soon as you
|
6
|
+
# include the module, View is gonna start caching without any timeout.
|
7
|
+
#
|
8
|
+
# Here is how you configure your caching with simple strategy:
|
9
|
+
#
|
10
|
+
# class InMemoryStrategy
|
11
|
+
# def get(key)
|
12
|
+
# @entries[key]
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def set(key, value, options)
|
16
|
+
# @entries ||= {}
|
17
|
+
# @entries[key] = value
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Rddd.configure do |config|
|
22
|
+
# config.cache_strategy = InMemoryStrategy.new
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
#
|
26
|
+
# Each time you hit data method, cache is first checked for presence of the
|
27
|
+
# data and if none is there build method is called to compose the view,
|
28
|
+
# result is updated to the cache and returned.
|
29
|
+
#
|
30
|
+
# Usage:
|
31
|
+
#
|
32
|
+
# class ProjectsView < Rddd::Presenters::Presenter
|
33
|
+
# extend Rddd::Presenters::Cacheable
|
34
|
+
#
|
35
|
+
# def build
|
36
|
+
# Projects.find_by_account_id(@id).map { |project| format(project) }
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def format(project)
|
40
|
+
# {
|
41
|
+
# :name => project.name,
|
42
|
+
# :deathline => project.deathline.strftime("%d %B %Y")
|
43
|
+
# }
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# ProjectsView.new(account_id).data #= [{:name => 'Rddd', :deathline => '01 January 2013'}]
|
48
|
+
#
|
49
|
+
# ## Timeout
|
50
|
+
#
|
51
|
+
# By default, there is no expiration to the values you store in the cache.
|
52
|
+
# This means, value is theoretically stored for infinite time or until some
|
53
|
+
# purging mechanism clean the code from cache as the less queried one.
|
54
|
+
#
|
55
|
+
# If you want to have more control of when the certain key should be removed
|
56
|
+
# you can use :timeout option. Its configurable per view.
|
57
|
+
#
|
58
|
+
# class ProjectsView < Rddd::Presenters::Presenter
|
59
|
+
# extend Rddd::Presenters::Cacheable
|
60
|
+
#
|
61
|
+
# cache :timeout => 60 * 24
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# Above call to ```cache``` with timeout changed its behavior. Each key for a given
|
65
|
+
# view now expires in 24 * 60 minutes, therefore in a day from its creation.
|
66
|
+
#
|
67
|
+
# ## Serialization
|
68
|
+
#
|
69
|
+
# By default cache does no serialization so in general String values are expected
|
70
|
+
# as caches almost exclusively work with Strings. You can inject serializor with
|
71
|
+
# cache configuration.
|
72
|
+
#
|
73
|
+
# class ProjectsView < Rddd::Presenters::Presenter
|
74
|
+
# extend Rddd::Presenters::Cacheable
|
75
|
+
#
|
76
|
+
# cache :serializer => Marshal
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# Above would call ```Marshal::dump``` each time before write to cache and
|
80
|
+
# ```Marshal::load``` each time the value is retrieved from it. Yo can inject
|
81
|
+
# your custom serializor class. Make sure it implements the same interface as
|
82
|
+
# Ruby standard ```Marshal::dump``` and ```Marshal::load```.
|
83
|
+
#
|
84
|
+
module Cacheable
|
85
|
+
def self.included(base)
|
86
|
+
base.extend CacheableConfig
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.extended(base)
|
90
|
+
base.class.extend CacheableConfig
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Invalidate the given view so next time its requested no cached value
|
95
|
+
# would be given.
|
96
|
+
#
|
97
|
+
def invalidate
|
98
|
+
__cache__.delete
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Trigger the warming up of the cache for a given view.
|
103
|
+
#
|
104
|
+
def warm_up
|
105
|
+
__update__(build)
|
106
|
+
end
|
107
|
+
|
108
|
+
def data
|
109
|
+
__read__ || __update__(build)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def __read__
|
115
|
+
data = __cache__.read
|
116
|
+
|
117
|
+
return nil unless data
|
118
|
+
|
119
|
+
self.class.serializer ? self.class.serializer::load(data) : data
|
120
|
+
end
|
121
|
+
|
122
|
+
def __update__(data)
|
123
|
+
data = self.class.serializer::dump(data) if self.class.serializer
|
124
|
+
|
125
|
+
__cache__.write(data, self.class.timeout)
|
126
|
+
|
127
|
+
data
|
128
|
+
end
|
129
|
+
|
130
|
+
def __cache__
|
131
|
+
CacheEntry.new(
|
132
|
+
"#{self.class.name.downcase.to_sym}#{id}",
|
133
|
+
@strategy ||= Configuration.instance.caching_strategy
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
module CacheableConfig
|
139
|
+
attr_reader :timeout, :serializer
|
140
|
+
|
141
|
+
def cache(attributes)
|
142
|
+
@timeout = attributes.fetch(:timeout, nil)
|
143
|
+
@timeout = @timeout && 60 * @timeout
|
144
|
+
@serializer = attributes.fetch(:serializer, nil)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rddd/presenters/cacheable'
|
2
|
+
require 'rddd/presenters/cache_entry'
|
3
|
+
|
4
|
+
module Rddd
|
5
|
+
#
|
6
|
+
# Presenter is simply a view on the domain data. Presenters should be used
|
7
|
+
# each time, you are about to get some data out of the domain. The result
|
8
|
+
# returned from presenter should be either very dumb DTO (data transfer object)
|
9
|
+
# or one of the base data types like Array, Hash or even some scalar value.
|
10
|
+
#
|
11
|
+
# Key goal is to make a clear boundary to the domain, so none full fledged
|
12
|
+
# business object is leaked outside of it.
|
13
|
+
#
|
14
|
+
# Presenter output is composed inside ```build``` method, which is called each
|
15
|
+
# time someone tries to access view ```data```. Lets take a look at simple example:
|
16
|
+
#
|
17
|
+
# class EventsByCityAndDatePresenter < Rddd::Presenters::View
|
18
|
+
# def initialize(city_id, date)
|
19
|
+
# @city_id = city_id
|
20
|
+
# @date = date
|
21
|
+
#
|
22
|
+
# super("#{city_id}-#{date}")
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# private
|
26
|
+
#
|
27
|
+
# def build
|
28
|
+
# format(City.find(@city_id).events_by_date(@date))
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def format(events)
|
32
|
+
# events.map { |event| {:name => event.name, :time => event.time} }
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# EventsByCityAndDatePresenter.new(city_id, Date.today).data #= [{:name => 'Happy new year!', :time => '00:00'}]
|
37
|
+
#
|
38
|
+
# ## Caching
|
39
|
+
#
|
40
|
+
# So far we didn't got too many benefits from use of the cache, although
|
41
|
+
# because the View made a clear boundary, there are some benefits which
|
42
|
+
# we can easily build on top of it. First is the caching. In order to plug
|
43
|
+
# it in simply extend Rddd::Presenters::Cacheable. See its documentation.
|
44
|
+
#
|
45
|
+
module Presenters
|
46
|
+
class View
|
47
|
+
attr_reader :id
|
48
|
+
|
49
|
+
def initialize(id)
|
50
|
+
@id = id
|
51
|
+
end
|
52
|
+
|
53
|
+
def data
|
54
|
+
build
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def build
|
60
|
+
raise NotImplementedError
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'curb'
|
3
|
+
require 'rddd/services/service'
|
4
|
+
|
5
|
+
module Rddd
|
6
|
+
module Services
|
7
|
+
#
|
8
|
+
# Remote service is the way to execute logic on different application.
|
9
|
+
# Currently JSON encoded, HTTP based transport is supported. In future
|
10
|
+
# there is a plan for more generic RPC support.
|
11
|
+
#
|
12
|
+
# Usage:
|
13
|
+
#
|
14
|
+
# # Configure the remote endpoint.
|
15
|
+
# Rddd.configure do |config|
|
16
|
+
# config.remote_services = [
|
17
|
+
# {:namespace => 'projects', :endpoint => 'http://products.dev/'}
|
18
|
+
# ]
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # Execute service
|
22
|
+
# class Test < Sinatra:Base
|
23
|
+
# get '/:account_id/projects' do
|
24
|
+
# execute_service(:projects__list, params[:account_id])
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # Finally here is the remote app
|
29
|
+
# class Projects < Sinatra:Base
|
30
|
+
# post ':service_name' do
|
31
|
+
# execute_service(params[:service_name], request.body)
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# As you can see remote app uses the service bus as well. This architecture
|
36
|
+
# would allow you pull part of your application to separate box without need
|
37
|
+
# for any change in your client or services code base.
|
38
|
+
#
|
39
|
+
class RemoteService < Service
|
40
|
+
def initialize(url, attributes = {})
|
41
|
+
super(attributes)
|
42
|
+
|
43
|
+
@url = url
|
44
|
+
end
|
45
|
+
|
46
|
+
def execute
|
47
|
+
JSON.parse(Curl.post(@url, @attributes).body_str)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.build(namespace, service_name, attributes)
|
51
|
+
remote = Configuration.instance.remote_services.find do |item|
|
52
|
+
item[:namespace] == namespace
|
53
|
+
end
|
54
|
+
|
55
|
+
RemoteService.new("#{remote[:endpoint]}#{service_name}", attributes)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rddd/services/service_factory'
|
2
|
+
require 'rddd/services/remote_service'
|
2
3
|
|
3
4
|
module Rddd
|
4
5
|
#
|
@@ -41,7 +42,11 @@ module Rddd
|
|
41
42
|
# @param [Block] optional error callback block.
|
42
43
|
#
|
43
44
|
def execute_service(service_name, attributes = {})
|
44
|
-
|
45
|
+
namespace, service_name = service_name.to_s.split('__') if service_name.to_s.include?('__')
|
46
|
+
|
47
|
+
service = namespace ? \
|
48
|
+
build_remote_service(namespace.to_sym, service_name.to_sym, attributes) : \
|
49
|
+
build_service(service_name, attributes)
|
45
50
|
|
46
51
|
unless service.valid?
|
47
52
|
yield(service.errors) if block_given?
|
@@ -56,6 +61,10 @@ module Rddd
|
|
56
61
|
def build_service(service_name, attributes)
|
57
62
|
ServiceFactory.build(service_name, attributes)
|
58
63
|
end
|
64
|
+
|
65
|
+
def build_remote_service(namespace, service_name, attributes)
|
66
|
+
RemoteService.build(namespace, service_name, attributes)
|
67
|
+
end
|
59
68
|
end
|
60
69
|
end
|
61
70
|
end
|
data/lib/rddd/version.rb
CHANGED
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rddd/presenters/presenter'
|
3
|
+
require 'rddd/aggregates/repositories/base'
|
4
|
+
|
5
|
+
class NilStrategy
|
6
|
+
def get(id)
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def set(id, value, timeout)
|
11
|
+
value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Rddd::Presenters::View do
|
16
|
+
before do
|
17
|
+
Rddd.configure do |config|
|
18
|
+
config.caching_strategy = NilStrategy.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:id) { :id }
|
23
|
+
|
24
|
+
let(:view) { Rddd::Presenters::View.new(id).tap {|view| view.extend Rddd::Presenters::Cacheable } }
|
25
|
+
|
26
|
+
let(:nil_strategy) { stub('nil_strategy') }
|
27
|
+
|
28
|
+
let(:data) { stub('data') }
|
29
|
+
|
30
|
+
subject { view }
|
31
|
+
|
32
|
+
describe '#initialize' do
|
33
|
+
its(:id) { should == id}
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#data' do
|
37
|
+
subject { view.data }
|
38
|
+
|
39
|
+
it { lambda {subject}.should raise_exception NotImplementedError }
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#warm_up' do
|
43
|
+
subject { view.warm_up }
|
44
|
+
|
45
|
+
before do
|
46
|
+
view.expects(:build).returns(data)
|
47
|
+
|
48
|
+
NilStrategy.any_instance.expects(:set).with("rddd::presenters::#{:view}#{:id}", data, anything)
|
49
|
+
end
|
50
|
+
|
51
|
+
it { subject }
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#invalidate' do
|
55
|
+
subject { view.invalidate }
|
56
|
+
|
57
|
+
before do
|
58
|
+
NilStrategy.any_instance.expects(:set).with("rddd::presenters::#{:view}#{:id}", nil, nil)
|
59
|
+
end
|
60
|
+
|
61
|
+
it { subject }
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#data' do
|
65
|
+
subject { view.data }
|
66
|
+
|
67
|
+
context 'without view repository' do
|
68
|
+
before do
|
69
|
+
view.expects(:build).returns(data)
|
70
|
+
end
|
71
|
+
|
72
|
+
it { subject.should == data }
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'not cached' do
|
76
|
+
before do
|
77
|
+
NilStrategy.any_instance.expects(:get).with("rddd::presenters::#{:view}#{:id}").returns(nil)
|
78
|
+
|
79
|
+
view.expects(:build).returns(data)
|
80
|
+
|
81
|
+
NilStrategy.any_instance.expects(:set).with("rddd::presenters::#{:view}#{:id}", data, nil)
|
82
|
+
end
|
83
|
+
|
84
|
+
it { subject.should == data }
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'cached' do
|
88
|
+
before do
|
89
|
+
NilStrategy.any_instance.expects(:get).with("rddd::presenters::#{:view}#{:id}").returns(data)
|
90
|
+
end
|
91
|
+
|
92
|
+
it { subject.should == data }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'view without cache' do
|
97
|
+
class CacheLessView < Rddd::Presenters::View
|
98
|
+
def build
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
let(:id) { stub('id') }
|
103
|
+
|
104
|
+
let(:cache_less_view) do
|
105
|
+
CacheLessView.new(id)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should not try to load data from cache' do
|
109
|
+
NilStrategy.expects(:new).never
|
110
|
+
|
111
|
+
cache_less_view.expects(:build).returns(data)
|
112
|
+
|
113
|
+
cache_less_view.data.should == data
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe 'timeout enabling' do
|
118
|
+
class TimeoutingView < Rddd::Presenters::View
|
119
|
+
include Rddd::Presenters::Cacheable
|
120
|
+
|
121
|
+
cache :timeout => 24 * 60
|
122
|
+
|
123
|
+
def build
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
let(:id) { :id }
|
128
|
+
|
129
|
+
let(:timeouting_view) do
|
130
|
+
TimeoutingView.new(id)
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should set timeout to cache data' do
|
134
|
+
NilStrategy.any_instance.expects(:get).with("#{:timeoutingview}#{:id}").returns(nil)
|
135
|
+
|
136
|
+
timeouting_view.expects(:build).returns(data)
|
137
|
+
|
138
|
+
NilStrategy.any_instance.expects(:set).with do |got_id, got_data, got_time|
|
139
|
+
got_id == "#{:timeoutingview}#{:id}" &&
|
140
|
+
got_data == data &&
|
141
|
+
got_time.to_i == (Time.now + 24 * 60 * 60).to_i
|
142
|
+
end
|
143
|
+
|
144
|
+
timeouting_view.data
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe 'with serializer' do
|
149
|
+
class SerializingView < Rddd::Presenters::View
|
150
|
+
include Rddd::Presenters::Cacheable
|
151
|
+
|
152
|
+
cache :serializer => Marshal
|
153
|
+
|
154
|
+
def build
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
let(:id) { :id }
|
159
|
+
|
160
|
+
let(:data) { SerializingView.new(1) }
|
161
|
+
|
162
|
+
let(:serializing_view) do
|
163
|
+
SerializingView.new(id)
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should marshal data on write' do
|
167
|
+
NilStrategy.any_instance.expects(:get).with("#{:serializingview}#{:id}").returns(nil)
|
168
|
+
|
169
|
+
serializing_view.expects(:build).returns(data)
|
170
|
+
|
171
|
+
NilStrategy.any_instance.expects(:set).with do |got_id, got_data, got_time|
|
172
|
+
got_id == "#{:serializingview}#{:id}" &&
|
173
|
+
got_data == Marshal::dump(data) &&
|
174
|
+
got_time == nil
|
175
|
+
end
|
176
|
+
|
177
|
+
serializing_view.data
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'should marshal data on read' do
|
181
|
+
NilStrategy.any_instance.expects(:get).twice.with("#{:serializingview}#{:id}") \
|
182
|
+
.returns(Marshal::dump(data))
|
183
|
+
|
184
|
+
serializing_view.data.should be_instance_of SerializingView
|
185
|
+
serializing_view.data.id.should be 1
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rddd/services/remote_service'
|
3
|
+
|
4
|
+
module Rddd
|
5
|
+
module Services
|
6
|
+
describe RemoteService do
|
7
|
+
let(:attributes) { stub('attributes') }
|
8
|
+
|
9
|
+
let(:url) { 'http://remote.dev/' }
|
10
|
+
|
11
|
+
describe '#initialize' do
|
12
|
+
subject { RemoteService.new(url, attributes) }
|
13
|
+
|
14
|
+
it 'should store attributes' do
|
15
|
+
subject.instance_variable_get(:@attributes).should == attributes
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#valid?' do
|
20
|
+
subject { RemoteService.new(url).valid? }
|
21
|
+
|
22
|
+
it { should be_true }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#execute' do
|
26
|
+
subject { RemoteService.new(url, attributes).execute }
|
27
|
+
|
28
|
+
let(:curl) { stub('curl', :body_str => '{"foo": "bar"}')}
|
29
|
+
|
30
|
+
it 'should raise not implemented' do
|
31
|
+
Curl.expects(:post).with(url, attributes).returns(curl)
|
32
|
+
|
33
|
+
subject.should == {'foo' => 'bar'}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -22,6 +22,17 @@ describe Rddd::Services::ServiceBus do
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
context 'namespaced service with remote http transport' do
|
26
|
+
let(:service) { stub('service', :valid? => true, :execute => nil) }
|
27
|
+
|
28
|
+
it do
|
29
|
+
Rddd::Services::RemoteService.expects(:build).with(:remote, :foo, attributes).returns(service)
|
30
|
+
service.expects(:execute)
|
31
|
+
|
32
|
+
controller.execute_service(:remote__foo, attributes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
25
36
|
context 'not valid call to service' do
|
26
37
|
context 'without block' do
|
27
38
|
before { service.stubs(:valid?).returns(false) }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rddd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-19 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Ruby DDD framework
|
15
15
|
email:
|
@@ -27,6 +27,7 @@ files:
|
|
27
27
|
- LICENSE.txt
|
28
28
|
- README.md
|
29
29
|
- Rakefile
|
30
|
+
- documentation/rddd-elements.png
|
30
31
|
- documentation/rddd.png
|
31
32
|
- lib/core_ext/string.rb
|
32
33
|
- lib/rddd.rb
|
@@ -39,13 +40,14 @@ files:
|
|
39
40
|
- lib/rddd/authorization/rule.rb
|
40
41
|
- lib/rddd/authorization/rules_builder.rb
|
41
42
|
- lib/rddd/configuration.rb
|
43
|
+
- lib/rddd/presenters/cache_entry.rb
|
44
|
+
- lib/rddd/presenters/cacheable.rb
|
45
|
+
- lib/rddd/presenters/presenter.rb
|
46
|
+
- lib/rddd/services/remote_service.rb
|
42
47
|
- lib/rddd/services/service.rb
|
43
48
|
- lib/rddd/services/service_bus.rb
|
44
49
|
- lib/rddd/services/service_factory.rb
|
45
50
|
- lib/rddd/version.rb
|
46
|
-
- lib/rddd/views/cache.rb
|
47
|
-
- lib/rddd/views/cacheable.rb
|
48
|
-
- lib/rddd/views/view.rb
|
49
51
|
- rddd.gemspec
|
50
52
|
- spec/integration_spec.rb
|
51
53
|
- spec/lib/aggregates/entity_spec.rb
|
@@ -54,12 +56,13 @@ files:
|
|
54
56
|
- spec/lib/authorization/integration_spec.rb
|
55
57
|
- spec/lib/authorization/rule_spec.rb
|
56
58
|
- spec/lib/authorization/rules_builder_spec.rb
|
59
|
+
- spec/lib/presenters/integration_spec.rb
|
57
60
|
- spec/lib/repositories/repository_factory_spec.rb
|
58
61
|
- spec/lib/repositories/repository_spec.rb
|
62
|
+
- spec/lib/services/remote_service_spec.rb
|
59
63
|
- spec/lib/services/service_bus_spec.rb
|
60
64
|
- spec/lib/services/service_factory_spec.rb
|
61
65
|
- spec/lib/services/service_spec.rb
|
62
|
-
- spec/lib/views/integration_spec.rb
|
63
66
|
- spec/spec_helper.rb
|
64
67
|
homepage: http://blog.ngneers.com
|
65
68
|
licenses: []
|
@@ -95,11 +98,12 @@ test_files:
|
|
95
98
|
- spec/lib/authorization/integration_spec.rb
|
96
99
|
- spec/lib/authorization/rule_spec.rb
|
97
100
|
- spec/lib/authorization/rules_builder_spec.rb
|
101
|
+
- spec/lib/presenters/integration_spec.rb
|
98
102
|
- spec/lib/repositories/repository_factory_spec.rb
|
99
103
|
- spec/lib/repositories/repository_spec.rb
|
104
|
+
- spec/lib/services/remote_service_spec.rb
|
100
105
|
- spec/lib/services/service_bus_spec.rb
|
101
106
|
- spec/lib/services/service_factory_spec.rb
|
102
107
|
- spec/lib/services/service_spec.rb
|
103
|
-
- spec/lib/views/integration_spec.rb
|
104
108
|
- spec/spec_helper.rb
|
105
109
|
has_rdoc:
|
data/lib/rddd/views/cache.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
module Rddd
|
2
|
-
module Views
|
3
|
-
class Cache
|
4
|
-
def initialize(id, repository)
|
5
|
-
@id = id
|
6
|
-
@repository = repository
|
7
|
-
end
|
8
|
-
|
9
|
-
def read
|
10
|
-
@repository.get(@id) if @repository
|
11
|
-
end
|
12
|
-
|
13
|
-
def invalidate
|
14
|
-
@repository.set(@id, nil)
|
15
|
-
end
|
16
|
-
|
17
|
-
def write(data, timeout)
|
18
|
-
expire_at = timeout ? Time.now + timeout : nil
|
19
|
-
|
20
|
-
@repository.set(@id, data, expire_at)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
data/lib/rddd/views/cacheable.rb
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
module Rddd
|
2
|
-
module Views
|
3
|
-
module Caching
|
4
|
-
attr_reader :cache_disabled, :timeout
|
5
|
-
|
6
|
-
def cache(attributes)
|
7
|
-
@cache_disabled = attributes.has_key?(:enabled) ? !attributes[:enabled] : false
|
8
|
-
@timeout = attributes.has_key?(:timeout) ? 60 * attributes[:timeout] : nil
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
module Cacheable
|
13
|
-
def invalidate
|
14
|
-
cache.invalidate if cache
|
15
|
-
end
|
16
|
-
|
17
|
-
def warm_cache
|
18
|
-
update_cache(build)
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def read_cache
|
24
|
-
cache.read if cache
|
25
|
-
end
|
26
|
-
|
27
|
-
def update_cache(data)
|
28
|
-
cache.write(data, self.class.timeout) if cache
|
29
|
-
end
|
30
|
-
|
31
|
-
def cache
|
32
|
-
nil unless repository
|
33
|
-
|
34
|
-
@cache ||= Cache.new(cache_id, repository)
|
35
|
-
end
|
36
|
-
|
37
|
-
def cache_id
|
38
|
-
"#{name}#{id}"
|
39
|
-
end
|
40
|
-
|
41
|
-
def repository
|
42
|
-
begin
|
43
|
-
@repository ||= ViewRepository.new
|
44
|
-
rescue
|
45
|
-
nil
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
data/lib/rddd/views/view.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'rddd/views/cache'
|
2
|
-
require 'rddd/views/cacheable'
|
3
|
-
|
4
|
-
module Rddd
|
5
|
-
module Views
|
6
|
-
class View
|
7
|
-
include Cacheable
|
8
|
-
extend Caching
|
9
|
-
|
10
|
-
attr_reader :id
|
11
|
-
|
12
|
-
def initialize(id)
|
13
|
-
@id = id
|
14
|
-
end
|
15
|
-
|
16
|
-
def name
|
17
|
-
self.class.name.downcase.to_sym
|
18
|
-
end
|
19
|
-
|
20
|
-
def build
|
21
|
-
raise NotImplementedError
|
22
|
-
end
|
23
|
-
|
24
|
-
def data
|
25
|
-
return build if self.class.cache_disabled
|
26
|
-
|
27
|
-
cached_data = read_cache
|
28
|
-
|
29
|
-
return cached_data if cached_data
|
30
|
-
|
31
|
-
data = build
|
32
|
-
update_cache(data)
|
33
|
-
|
34
|
-
data
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
@@ -1,153 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'rddd/views/view'
|
3
|
-
require 'rddd/aggregates/repositories/base'
|
4
|
-
|
5
|
-
class ViewRepository < Rddd::Repositories::Base
|
6
|
-
def get(id)
|
7
|
-
end
|
8
|
-
|
9
|
-
def set(id, value, timeout)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
describe Rddd::Views::View do
|
14
|
-
let(:id) { :id }
|
15
|
-
|
16
|
-
let(:view) { Rddd::Views::View.new(id) }
|
17
|
-
|
18
|
-
let(:view_repository) { stub('view_repository') }
|
19
|
-
|
20
|
-
let(:data) { stub('data') }
|
21
|
-
|
22
|
-
subject { view }
|
23
|
-
|
24
|
-
describe '#initialize' do
|
25
|
-
its(:id) { should == id}
|
26
|
-
end
|
27
|
-
|
28
|
-
describe '#name' do
|
29
|
-
its(:name) { should == :'rddd::views::view' }
|
30
|
-
end
|
31
|
-
|
32
|
-
describe '#build' do
|
33
|
-
subject { view.build }
|
34
|
-
|
35
|
-
it { lambda {subject}.should raise_exception NotImplementedError }
|
36
|
-
end
|
37
|
-
|
38
|
-
describe '#warm_cache' do
|
39
|
-
subject { view.warm_cache }
|
40
|
-
|
41
|
-
before do
|
42
|
-
ViewRepository.expects(:new).returns(view_repository)
|
43
|
-
|
44
|
-
view.expects(:build).returns(data)
|
45
|
-
|
46
|
-
view_repository.expects(:set).with("rddd::views::#{:view}#{:id}", data, anything)
|
47
|
-
end
|
48
|
-
|
49
|
-
it { subject }
|
50
|
-
end
|
51
|
-
|
52
|
-
describe '#invalidate' do
|
53
|
-
subject { view.invalidate }
|
54
|
-
|
55
|
-
before do
|
56
|
-
ViewRepository.expects(:new).returns(view_repository)
|
57
|
-
|
58
|
-
view_repository.expects(:set).with("rddd::views::#{:view}#{:id}", nil)
|
59
|
-
end
|
60
|
-
|
61
|
-
it { subject }
|
62
|
-
end
|
63
|
-
|
64
|
-
describe '#data' do
|
65
|
-
subject { view.data }
|
66
|
-
|
67
|
-
context 'without view repository' do
|
68
|
-
before do
|
69
|
-
view.expects(:build).returns(data)
|
70
|
-
end
|
71
|
-
|
72
|
-
it { subject.should == data }
|
73
|
-
end
|
74
|
-
|
75
|
-
context 'not cached' do
|
76
|
-
before do
|
77
|
-
ViewRepository.expects(:new).returns(view_repository)
|
78
|
-
end
|
79
|
-
|
80
|
-
before do
|
81
|
-
view_repository.expects(:get).with("rddd::views::#{:view}#{:id}").returns(nil)
|
82
|
-
|
83
|
-
view.expects(:build).returns(data)
|
84
|
-
|
85
|
-
view_repository.expects(:set).with("rddd::views::#{:view}#{:id}", data, nil)
|
86
|
-
end
|
87
|
-
|
88
|
-
it { subject.should == data }
|
89
|
-
end
|
90
|
-
|
91
|
-
context 'cached' do
|
92
|
-
before do
|
93
|
-
ViewRepository.expects(:new).returns(view_repository)
|
94
|
-
view_repository.expects(:get).with("rddd::views::#{:view}#{:id}").returns(data)
|
95
|
-
end
|
96
|
-
|
97
|
-
it { subject.should == data }
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
describe 'cache disabling' do
|
102
|
-
class CacheLessView < Rddd::Views::View
|
103
|
-
cache :enabled => false
|
104
|
-
|
105
|
-
def build
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
let(:id) { stub('id') }
|
110
|
-
|
111
|
-
let(:cache_less_view) do
|
112
|
-
CacheLessView.new(id)
|
113
|
-
end
|
114
|
-
|
115
|
-
it 'should not try to load data from cache' do
|
116
|
-
ViewRepository.expects(:new).never
|
117
|
-
|
118
|
-
cache_less_view.expects(:build).returns(data)
|
119
|
-
|
120
|
-
cache_less_view.data.should == data
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
describe 'timeout enabling' do
|
125
|
-
class TimeoutingView < Rddd::Views::View
|
126
|
-
cache :timeout => 24 * 60
|
127
|
-
|
128
|
-
def build
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
let(:id) { :id }
|
133
|
-
|
134
|
-
let(:timeouting_view) do
|
135
|
-
TimeoutingView.new(id)
|
136
|
-
end
|
137
|
-
|
138
|
-
it 'should set timeout to cache data' do
|
139
|
-
ViewRepository.expects(:new).returns(view_repository)
|
140
|
-
view_repository.expects(:get).with("#{:timeoutingview}#{:id}").returns(nil)
|
141
|
-
|
142
|
-
timeouting_view.expects(:build).returns(data)
|
143
|
-
|
144
|
-
view_repository.expects(:set).with do |got_id, got_data, got_time|
|
145
|
-
got_id == "#{:timeoutingview}#{:id}" &&
|
146
|
-
got_data == data &&
|
147
|
-
got_time.to_i == (Time.now + 24 * 60 * 60).to_i
|
148
|
-
end
|
149
|
-
|
150
|
-
timeouting_view.data
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|