playhouse 0.1.1
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 +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +69 -0
- data/README.md +207 -0
- data/Rakefile +16 -0
- data/lib/playhouse/context.rb +120 -0
- data/lib/playhouse/part.rb +29 -0
- data/lib/playhouse/play.rb +59 -0
- data/lib/playhouse/production.rb +33 -0
- data/lib/playhouse/role.rb +55 -0
- data/lib/playhouse/scouts/build_with_composer.rb +33 -0
- data/lib/playhouse/scouts/can_construct_object.rb +15 -0
- data/lib/playhouse/scouts/direct_value.rb +9 -0
- data/lib/playhouse/scouts/entity_from_repository.rb +23 -0
- data/lib/playhouse/support/default_hash_values.rb +23 -0
- data/lib/playhouse/support/files.rb +7 -0
- data/lib/playhouse/talent_scout.rb +42 -0
- data/lib/playhouse/theatre.rb +62 -0
- data/lib/playhouse/validation/actors_validator.rb +23 -0
- data/lib/playhouse/validation/required_actor_validator.rb +17 -0
- data/lib/playhouse/validation/validation_errors.rb +76 -0
- data/playhouse.gemspec +20 -0
- data/spec/playhouse/context_spec.rb +111 -0
- data/spec/playhouse/part_spec.rb +19 -0
- data/spec/playhouse/play_spec.rb +64 -0
- data/spec/playhouse/production_spec.rb +47 -0
- data/spec/playhouse/role_spec.rb +72 -0
- data/spec/playhouse/support/default_hash_values_spec.rb +34 -0
- data/spec/playhouse/talent_scout_spec.rb +95 -0
- data/spec/playhouse/theatre_spec.rb +34 -0
- data/spec/playhouse/validation/actors_validator_spec.rb +38 -0
- data/spec/spec_helper.rb +5 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7ee92d35c5cc99200df7de30aa77d0936c7a6b6f
|
4
|
+
data.tar.gz: 1bf5df285920713b73c523645869286351ee6440
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e17951ca5e9cbdc74c41b2a9d1a663943c7b92aa9c8d9e7c2614762d69576156c3e8dbbca453de3015a3a48ac6e120c9b64e0eb719239d7af976c586cb15a15c
|
7
|
+
data.tar.gz: 9b907d2714c1462d8210fb1c3e07d712fffbd212b5961389020c91264b2e2a8bd5aadcdd79b5727c837377b79aa1128b0d6eb938079c1a7bdcd91df942e3cd4c
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.idea
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.0.0-p195
|
data/.travis.yml
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
rvm:
|
2
|
+
- "2.0.0"
|
3
|
+
script: "rake ci"
|
4
|
+
before_script: "rake setup_ci"
|
5
|
+
notifications:
|
6
|
+
hipchat:
|
7
|
+
rooms:
|
8
|
+
- 3d318fc3e1f401238a50171784b534@Craftworks General
|
9
|
+
template: '%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} (<a href="%{build_url}">Details</a>/<a href="%{compare_url}">Change view</a>)'
|
10
|
+
format: html
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
playhouse (0.1.1)
|
5
|
+
activerecord
|
6
|
+
activesupport
|
7
|
+
rake
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (4.0.2)
|
13
|
+
activesupport (= 4.0.2)
|
14
|
+
builder (~> 3.1.0)
|
15
|
+
activerecord (4.0.2)
|
16
|
+
activemodel (= 4.0.2)
|
17
|
+
activerecord-deprecated_finders (~> 1.0.2)
|
18
|
+
activesupport (= 4.0.2)
|
19
|
+
arel (~> 4.0.0)
|
20
|
+
activerecord-deprecated_finders (1.0.3)
|
21
|
+
activesupport (4.0.2)
|
22
|
+
i18n (~> 0.6, >= 0.6.4)
|
23
|
+
minitest (~> 4.2)
|
24
|
+
multi_json (~> 1.3)
|
25
|
+
thread_safe (~> 0.1)
|
26
|
+
tzinfo (~> 0.3.37)
|
27
|
+
arel (4.0.1)
|
28
|
+
atomic (1.1.14)
|
29
|
+
builder (3.1.4)
|
30
|
+
colorize (0.5.8)
|
31
|
+
coveralls (0.6.3)
|
32
|
+
colorize
|
33
|
+
multi_json (~> 1.3)
|
34
|
+
rest-client
|
35
|
+
simplecov (>= 0.7)
|
36
|
+
thor
|
37
|
+
diff-lcs (1.2.5)
|
38
|
+
i18n (0.6.9)
|
39
|
+
mime-types (1.21)
|
40
|
+
minitest (4.7.5)
|
41
|
+
multi_json (1.7.3)
|
42
|
+
rake (10.0.4)
|
43
|
+
rest-client (1.6.7)
|
44
|
+
mime-types (>= 1.16)
|
45
|
+
rspec (2.14.1)
|
46
|
+
rspec-core (~> 2.14.0)
|
47
|
+
rspec-expectations (~> 2.14.0)
|
48
|
+
rspec-mocks (~> 2.14.0)
|
49
|
+
rspec-core (2.14.7)
|
50
|
+
rspec-expectations (2.14.4)
|
51
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
52
|
+
rspec-mocks (2.14.4)
|
53
|
+
simplecov (0.7.1)
|
54
|
+
multi_json (~> 1.0)
|
55
|
+
simplecov-html (~> 0.7.1)
|
56
|
+
simplecov-html (0.7.1)
|
57
|
+
thor (0.18.0)
|
58
|
+
thread_safe (0.1.3)
|
59
|
+
atomic
|
60
|
+
tzinfo (0.3.38)
|
61
|
+
|
62
|
+
PLATFORMS
|
63
|
+
ruby
|
64
|
+
|
65
|
+
DEPENDENCIES
|
66
|
+
coveralls
|
67
|
+
playhouse!
|
68
|
+
rake
|
69
|
+
rspec (= 2.14.1)
|
data/README.md
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
#Playhouse
|
2
|
+
|
3
|
+
A framework for structing a ruby application using the DCI (Data, Context and Interaction)
|
4
|
+
pattern. Playhouse makes no assumptions about whether it's a web app (or any other sort),
|
5
|
+
it just helps you to structure your application logic. Playhouse is not used to structure
|
6
|
+
presentation logic, it is typically connected to some sort of delivery layer.
|
7
|
+
|
8
|
+
[](https://codeclimate.com/github/enspiral/playhouse)
|
9
|
+
[](https://travis-ci.org/enspiral/playhouse)
|
10
|
+
[](https://coveralls.io/r/enspiral/playhouse)
|
11
|
+
|
12
|
+
##Status
|
13
|
+
|
14
|
+
Playhouse is not yet at version 1.0.
|
15
|
+
|
16
|
+
It is being used for its first production apps now, but its interface may change rapidly and
|
17
|
+
at any point, so doing so is not advised unless you are actively involved in Playhouse
|
18
|
+
development.
|
19
|
+
|
20
|
+
##Installation
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'playhouse', git: 'git://github.com/enspiral/playhouse.git'
|
24
|
+
```
|
25
|
+
|
26
|
+
You may wish to organise your app such that the three main parts of the DCI pattern have their
|
27
|
+
own folders. We are currently using:
|
28
|
+
|
29
|
+
```
|
30
|
+
lib/entities
|
31
|
+
lib/roles
|
32
|
+
lib/context
|
33
|
+
```
|
34
|
+
|
35
|
+
##Getting Started
|
36
|
+
|
37
|
+
There are three main parts of a Playhouse app, Entities, Roles and Contexts. Additionally,
|
38
|
+
there is some overall structure that makes it easy to create an entry point to the
|
39
|
+
application logic.
|
40
|
+
|
41
|
+
###Entities
|
42
|
+
|
43
|
+
Entities are the "Data" part of DCI. They represent your Domain models that you probably
|
44
|
+
want to persist to a data store of some sort. To avoid the sort of complexity that often
|
45
|
+
occurs in models in Rails apps, Playhouse entities should have no functionality other than
|
46
|
+
defining their data structure and connecting to the persistance layer.
|
47
|
+
|
48
|
+
Playhouse does not care what persistance library you use. ActiveRecord works fine, just add
|
49
|
+
the gem to your app and start using it. We recommend you don't use validations (Contexts do
|
50
|
+
validations in Playhouse), keep relationships to necessary ones only, and don't use scopes
|
51
|
+
(queries go in Roles).
|
52
|
+
|
53
|
+
Playhouse actually has no Entity class. This is just a concept that you need to create yourself.
|
54
|
+
|
55
|
+
Entities are often used as Actors by Contexts. Actors can also be other basic types (or indeed
|
56
|
+
any object).
|
57
|
+
|
58
|
+
### Roles
|
59
|
+
|
60
|
+
Roles are modules that are mixed into to Actors at runtime. Specifically note that they are
|
61
|
+
used to extend objects, not classes. If you're not familiar with this, go read up on DCI.
|
62
|
+
|
63
|
+
Playhouse defines a Role module to provide this behaviour, although it is implemented just
|
64
|
+
using Ruby's `extend` method. A role in your Playhouse app looks as follows:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
require 'playhouse/role'
|
68
|
+
|
69
|
+
module YourApp
|
70
|
+
module TransferSource
|
71
|
+
include Playhouse::Role
|
72
|
+
|
73
|
+
actor_dependency :minimum_balance
|
74
|
+
actor_dependency :bank
|
75
|
+
|
76
|
+
def some_method
|
77
|
+
# do something
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Using a role is as simple as:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
TransferSource.cast_actor(my_account)
|
87
|
+
```
|
88
|
+
|
89
|
+
Although Contexts will do this for you automatically. Specifying actor dependencies on your
|
90
|
+
role is a good way of documenting the duck type that the role expects to extend. When you
|
91
|
+
call cast_actor, then it will raise an exception if the actor you supply does not support
|
92
|
+
the methods specified (minimum_balance and bank in the above example).
|
93
|
+
|
94
|
+
###Contexts
|
95
|
+
|
96
|
+
Each of your contexts is a command that your app performs, which you could also think of as
|
97
|
+
a use case. In essence, a context is supplied with Actors, "casts" them in various Roles and
|
98
|
+
then executes some behaviour. In keeping with conventions of most people using DCI in ruby,
|
99
|
+
executing a context is done by calling its `call` method.
|
100
|
+
|
101
|
+
Playhouse provides a base Context class for you to derive from. Rather than implementing
|
102
|
+
`call` directly though, please override our `perform` method so that we can perform some
|
103
|
+
checks before your code executes. Here's an example.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
require 'playhouse/context'
|
107
|
+
require 'economatic/roles/account_transaction_collection'
|
108
|
+
require 'economatic/entities/account'
|
109
|
+
|
110
|
+
module Economatic
|
111
|
+
class AccountBalanceEnquiry < Playhouse::Context
|
112
|
+
actor :account, role: AccountTransactionCollection, repository: Account
|
113
|
+
|
114
|
+
def perform
|
115
|
+
account.balance
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
This Balance enquiry context is fairly simple. Your context perform method might have more
|
121
|
+
lines than this, and it might be good if it lists the main high level steps for
|
122
|
+
performing this feature. However, the serious application logic goes into your roles.
|
123
|
+
|
124
|
+
To calculate a balance, this context just needs one actor, an account, and it casts it
|
125
|
+
as a role (AccountTransactionCollection) which actually knows how to calculate a balance
|
126
|
+
by summing transactions. Actors are all required by default (unless you specify the
|
127
|
+
`optional: true` option), and so building this context without an account will raise an
|
128
|
+
exception. Specifying the Account repository can be used to find accounts, allows other
|
129
|
+
parts of Playhouse to build this Context by asking Account to fetch an account given an
|
130
|
+
id. Remember as well that the AccountTransactionCollection role will check that the account
|
131
|
+
has the methods it is dependent on.
|
132
|
+
|
133
|
+
The return value from your context is returned to the code calling your application
|
134
|
+
(which is often your delivery layer or another application), and we suggest that this
|
135
|
+
should be a fairly dumb object. Context should return data, you shouldn't use their return value
|
136
|
+
in ways that transform it, save data, etc.
|
137
|
+
|
138
|
+
##An Interface to Your Application
|
139
|
+
|
140
|
+
The external interface of your application is essentially the Contexts that are available
|
141
|
+
to be called, although some Contexts might be just for calling from other Contexts. To
|
142
|
+
organise these a bit to present to the outside world, you can group these into an API
|
143
|
+
object which Playhouse calls a Play.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
require 'playhouse/play'
|
147
|
+
|
148
|
+
module Economatic
|
149
|
+
class Play < Playhouse::Play
|
150
|
+
context AccountBalanceEnquiry
|
151
|
+
context ApproveTransfer
|
152
|
+
context BankBalanceEnquiry
|
153
|
+
context TransferMoney
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
Contexts can be called via the play just as methods:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
play = Economatic::Play.new
|
162
|
+
play.account_balance_enquiry(account: some_account_object)
|
163
|
+
```
|
164
|
+
|
165
|
+
If you call a context this way, we also use our TalentScout to process the parameters you
|
166
|
+
supply and find actors if given ids, or build actors that are composed of several parts,
|
167
|
+
for example, this will work if calling via the play (but wouldn't work if you construct
|
168
|
+
the Context manually):
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
play.account_balance_enquiry(account_id: 1)
|
172
|
+
```
|
173
|
+
|
174
|
+
The other advantage of a Play is that you can ask it about the context that it supports,
|
175
|
+
and the parts available for Actors in that context. This allows you to present structured
|
176
|
+
information about your API, such as auto-generating documentation.
|
177
|
+
|
178
|
+
###A Delivery Layer
|
179
|
+
|
180
|
+
While you can call methods on a Play directly, often this will be done from some user input
|
181
|
+
of some sort. This layer knows about how you are delivering your app (as a JSON web service,
|
182
|
+
a console app, a GUI app, etc), and it knows about your application somewhat (often by
|
183
|
+
interrogating your Plays). However, your core application should never know about your
|
184
|
+
delivery layer(s). Even if you're expecting to build a web app, don't put web concepts
|
185
|
+
into your app, make it generic.
|
186
|
+
|
187
|
+
Playhouse doesn't do delivery layers for you, but it provides a known structure to allow
|
188
|
+
other gems to help you out with this. We suggest you first try out our playhouse-console
|
189
|
+
gem which provides you with a simple console app with one command for each Context.
|
190
|
+
|
191
|
+
For a web app, it's quite possible to use Sinatra or Rails as your delivery layer.
|
192
|
+
|
193
|
+
##Licence
|
194
|
+
|
195
|
+
Playhouse is licenced under the MIT licence. Copyright 2013 Enspiral Services Ltd.
|
196
|
+
|
197
|
+
##Contributing
|
198
|
+
|
199
|
+
Your contributions are welcome. Send us a pull request, or start a discussion in the github
|
200
|
+
issues first.
|
201
|
+
|
202
|
+
##Credits
|
203
|
+
|
204
|
+
From Enspiral Craftworks:
|
205
|
+
|
206
|
+
* Craig Ambrose (@craigambrose)
|
207
|
+
* Joshua Vial (@joshuavial)
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
desc "Run specs"
|
4
|
+
RSpec::Core::RakeTask.new do |t|
|
5
|
+
end
|
6
|
+
|
7
|
+
desc "Setup this library to perform ci task"
|
8
|
+
task :setup_ci do
|
9
|
+
puts "nothing to setup"
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Test this console interface"
|
13
|
+
task :ci => [:spec] do
|
14
|
+
end
|
15
|
+
|
16
|
+
task default: :ci
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
require 'playhouse/part'
|
3
|
+
require 'playhouse/validation/actors_validator'
|
4
|
+
|
5
|
+
module Playhouse
|
6
|
+
class Context
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def parts
|
10
|
+
@actor_definitions ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def actor(name, options = {})
|
14
|
+
raise InvalidActorKeyError.new(self.class.name, name) unless name.is_a?(Symbol)
|
15
|
+
|
16
|
+
parts << Part.new(name, options)
|
17
|
+
|
18
|
+
define_method name do
|
19
|
+
@actors[name]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def part_for(name)
|
24
|
+
raise InvalidActorKeyError.new(self.class.name, name) unless name.is_a?(Symbol)
|
25
|
+
parts.detect {|definition| definition.name == name}
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_name
|
29
|
+
context_name_parts.join('').underscore.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
def http_method(method=:post)
|
33
|
+
@http_methods ||= []
|
34
|
+
if method.is_a?(Array)
|
35
|
+
@http_methods.concat method
|
36
|
+
else
|
37
|
+
@http_methods << method
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def http_methods
|
42
|
+
return [:get] if @http_methods.nil?
|
43
|
+
@http_methods.uniq
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def context_name_parts
|
49
|
+
name.split('::')[1..-1].reverse
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(actors = {})
|
54
|
+
store_expected_actors(actors)
|
55
|
+
end
|
56
|
+
|
57
|
+
def inherit_actors_from(parent)
|
58
|
+
parent.send(:actors).each do |name, actor|
|
59
|
+
if actors[name].nil? && self.class.part_for(name)
|
60
|
+
store_actor name, actor
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def call
|
66
|
+
validate_actors
|
67
|
+
cast_actors
|
68
|
+
perform
|
69
|
+
end
|
70
|
+
|
71
|
+
def perform
|
72
|
+
raise NotImplementedError.new("Context #{self.class.name} needs to override the perform method")
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def validator
|
78
|
+
ActorsValidator.new
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def validate_actors
|
84
|
+
validator.validate_actors(self.class.parts, @actors)
|
85
|
+
end
|
86
|
+
|
87
|
+
def store_expected_actors(actors)
|
88
|
+
@actors = {}
|
89
|
+
actors.each do |name, actor|
|
90
|
+
store_actor name, actor
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def store_actor(name, actor)
|
95
|
+
part = self.class.part_for(name)
|
96
|
+
if part
|
97
|
+
@actors[name] = actor
|
98
|
+
else
|
99
|
+
raise UnknownActorKeyError.new(self.class.name, name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def cast_actors
|
104
|
+
@actors.each do |name, actor|
|
105
|
+
part = self.class.part_for(name)
|
106
|
+
@actors[name] = part.cast(actor)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def actors
|
111
|
+
@actors
|
112
|
+
end
|
113
|
+
|
114
|
+
def actors_except(*exceptions)
|
115
|
+
actors.reject do |key, value|
|
116
|
+
exceptions.include?(key)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|