wrapper_based 1.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +1 -1
- data/README.md +105 -62
- data/examples/acapella.rb +55 -0
- data/examples/background_job.rb +29 -0
- data/examples/dijkstra.rb +27 -17
- data/examples/http_api_client.rb +46 -0
- data/examples/money_transfer.rb +5 -11
- data/examples/toy_shop.rb +12 -17
- data/examples/users_facade.rb +23 -0
- data/lib/wrapper_based.rb +24 -4
- data/lib/wrapper_based/casting.rb +56 -0
- data/lib/wrapper_based/casting/director.rb +28 -0
- data/lib/wrapper_based/casting/pool.rb +22 -0
- data/lib/wrapper_based/casting/type.rb +31 -0
- data/lib/wrapper_based/context.rb +6 -44
- data/lib/wrapper_based/dci.rb +17 -3
- data/lib/wrapper_based/role_unknown.rb +8 -0
- data/lib/wrapper_based/{context/type_casting.rb → type_casting.rb} +3 -9
- data/lib/wrapper_based/version.rb +1 -1
- data/wrapper_based.gemspec +3 -1
- metadata +43 -10
- data/lib/wrapper_based/context/casting.rb +0 -31
- data/lib/wrapper_based/context/casting_director.rb +0 -20
- data/lib/wrapper_based/context/producer.rb +0 -21
- data/lib/wrapper_based/roles.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9905f3b4631008847a1ea6998fc79ce2d186727
|
4
|
+
data.tar.gz: bf443b8da55d7e0c32b7cdb99e09a8d47b585901
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f5bc3f9544c492ed7a5307f3b5a57463e8ef12dcb9972998da421298ecc528e3f215097d33bf8cdc8ebb1275980ee32a77f70d908bec84ea22fbc08d6f89ec2
|
7
|
+
data.tar.gz: 9b42afaff1621d74441b3373ce7879bc2d167959b3fcdea5b7d46d6562a2df02395261b1591406a8f567596ef07daccfc1bc2a580ff359fd6ba072b281155be1
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -17,88 +17,137 @@ And then execute:
|
|
17
17
|
|
18
18
|
$ bundle
|
19
19
|
|
20
|
-
##
|
20
|
+
## DCI::Roles
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
Includes role components to classes.
|
23
|
+
|
24
|
+
### Usage
|
25
25
|
|
26
26
|
```ruby
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
class ApplicationController
|
28
|
+
include DCI::Roles(:current_user, logged: SignsUser)
|
29
|
+
|
30
|
+
before_action -> { with! logged: session }
|
31
|
+
helper_method :logged, :current_user
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def authenticate_user!
|
36
|
+
if logged.out?
|
37
|
+
flash[:error] = "You must be logged in to access this section"
|
38
|
+
redirect_to login_url
|
39
|
+
else
|
40
|
+
with_current_user! logged.user
|
41
|
+
end
|
31
42
|
end
|
32
43
|
end
|
33
44
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
45
|
+
class LoginController < ApplicationController
|
46
|
+
include DCI::Roles(signing: SignsUser, user_params: UserParams, login: HasEncryptedPassword)
|
47
|
+
|
48
|
+
before_action -> { with! signing: session, user_params: params.require(:user) }
|
49
|
+
before_action -> { with! login: signing.by(user_params.login) }
|
38
50
|
|
39
|
-
def
|
40
|
-
|
51
|
+
def new; end
|
52
|
+
|
53
|
+
def create
|
54
|
+
if login.authenticate(user_params.password)
|
55
|
+
signing.in! login
|
56
|
+
redirect_to login, notice: 'Welcome, you are now logged in.'
|
57
|
+
else
|
58
|
+
flash[:danger] = 'Invalid credentials.'
|
59
|
+
render :new, status: :unauthorized
|
60
|
+
end
|
41
61
|
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class LogoutController < ApplicationController
|
65
|
+
include DCI::Roles(signing: SignsUser)
|
42
66
|
|
43
|
-
|
44
|
-
|
67
|
+
before_action -> { with! signing: session }
|
68
|
+
|
69
|
+
def destroy
|
70
|
+
signing.out!
|
71
|
+
redirect_to root_url
|
45
72
|
end
|
46
73
|
end
|
47
74
|
|
48
|
-
class
|
49
|
-
def
|
75
|
+
class SignsUser < Struct.new(:signed)
|
76
|
+
def in!(user)
|
77
|
+
signed[:user_id] = user.id
|
78
|
+
end
|
50
79
|
|
51
|
-
def
|
52
|
-
|
80
|
+
def out!
|
81
|
+
signed.delete(:user_id)
|
82
|
+
@user = nil
|
53
83
|
end
|
54
84
|
|
55
|
-
def
|
56
|
-
|
85
|
+
def out?
|
86
|
+
signed[:user_id].nil? || in?
|
57
87
|
end
|
58
88
|
|
59
|
-
|
89
|
+
def in?
|
90
|
+
!user.nil?
|
91
|
+
end
|
60
92
|
|
61
|
-
def
|
62
|
-
|
63
|
-
map { |neighbor| to.shortest_path_from(neighbor, self) }.
|
64
|
-
min_by { |neighbor_path| city.distance_of neighbor_path }
|
93
|
+
def user
|
94
|
+
@user ||= by(id: signed[:user_id])
|
65
95
|
end
|
66
|
-
end
|
67
|
-
```
|
68
96
|
|
69
|
-
|
97
|
+
def by(**identification)
|
98
|
+
User.find_by identification
|
99
|
+
end
|
70
100
|
|
71
|
-
|
101
|
+
def with(**credentials)
|
102
|
+
User.new credentials
|
103
|
+
end
|
72
104
|
|
73
|
-
|
105
|
+
def up!(user)
|
106
|
+
user.save && user if User.validate_registering(user)
|
107
|
+
end
|
108
|
+
end
|
74
109
|
|
75
|
-
|
110
|
+
module UserParams
|
111
|
+
def registration
|
112
|
+
permit(:username, :email)
|
113
|
+
end
|
76
114
|
|
77
|
-
|
78
|
-
|
115
|
+
def login
|
116
|
+
permit(:username, :email)
|
117
|
+
end
|
118
|
+
|
119
|
+
def security
|
120
|
+
permit(:password, :password_confirmation, :code)
|
121
|
+
end
|
122
|
+
|
123
|
+
def password
|
124
|
+
self[:password]
|
125
|
+
end
|
126
|
+
end
|
79
127
|
```
|
80
128
|
|
81
|
-
|
129
|
+
## DCI::Context
|
82
130
|
|
83
|
-
|
131
|
+
### Usage
|
84
132
|
|
85
133
|
```ruby
|
86
|
-
|
134
|
+
class GiftToy < DCI::Context(:toy, gifter: Buyer, giftee: Recipient)
|
135
|
+
def call(toy = @toy)
|
136
|
+
gift = gifter.buy
|
137
|
+
giftee.receive gift
|
138
|
+
end
|
139
|
+
end
|
87
140
|
```
|
88
141
|
|
89
|
-
|
90
|
-
|
142
|
+
#### Context#to_proc
|
143
|
+
|
144
|
+
Returns call method as a Proc.
|
91
145
|
|
92
146
|
```ruby
|
93
|
-
|
94
|
-
['Batman', Superman', 'Wonder Woman'].each do |founder|
|
95
|
-
add_member.rebind(member: founder).(recruit: 'Supergirl')
|
96
|
-
end
|
147
|
+
['Card Wars', 'Ice Ninja Manual', 'Bacon'].map &GiftToy[gifter: 'Jake', giftee: 'Finn']
|
97
148
|
```
|
98
149
|
|
99
|
-
|
100
|
-
|
101
|
-
### klass#call(**params)
|
150
|
+
#### Context::call
|
102
151
|
|
103
152
|
A shortcut for instantiating the context by passing the collaborators and then executing the context call method.
|
104
153
|
|
@@ -112,21 +161,15 @@ Which is equivalent to:
|
|
112
161
|
Funds::TransferMoney.new(from: @account1, to: @account2, amount: 50).call
|
113
162
|
```
|
114
163
|
|
115
|
-
##
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
def song
|
126
|
-
"Everything is AWESOME!!!"
|
127
|
-
end
|
128
|
-
end
|
129
|
-
```
|
164
|
+
## Context Examples
|
165
|
+
[Money Transfer](examples/money_transfer.rb) |
|
166
|
+
[Djikstra](examples/djikstra.rb) |
|
167
|
+
[Toy Shop](examples/toy_shop.rb) |
|
168
|
+
[Acapella](examples/acapella.rb) |
|
169
|
+
[Background Job](examples/background_job.rb) |
|
170
|
+
[Facade](examples/users_facade.rb) |
|
171
|
+
[HTTP API Client](examples/http_api_client.rb) |
|
172
|
+
[all examples](examples)
|
130
173
|
|
131
174
|
## Development
|
132
175
|
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'wrapper_based'
|
2
|
+
|
3
|
+
module SoftSinging
|
4
|
+
def sing(lyrics)
|
5
|
+
puts "#{name}: #{lyrics.to_s.downcase}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module LoudSinging
|
10
|
+
def sing(lyrics)
|
11
|
+
puts "#{name}: #{lyrics.to_s.upcase}!"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module NormalSinging
|
16
|
+
def sing(lyrics)
|
17
|
+
puts "#{name}: #{lyrics}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Trio < DCI::Context(normaly: NormalSinging, softly: SoftSinging, loudly: LoudSinging)
|
22
|
+
def initialize(singing = nil, normaly: singing, softly: singing, loudly: singing)
|
23
|
+
super(normaly: normaly, softly: softly, loudly: loudly)
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform(song)
|
27
|
+
song.each_slice(3).each do |first, second, third|
|
28
|
+
softly.sing(first)
|
29
|
+
loudly.sing(second)
|
30
|
+
normaly.sing(third)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Duet < DCI::Context(female: SoftSinging, male: LoudSinging)
|
36
|
+
def initialize(singing = nil, female: singing, male: singing)
|
37
|
+
super(female: female, male: male)
|
38
|
+
end
|
39
|
+
|
40
|
+
def perform(song)
|
41
|
+
song.each_slice(2).each do |first, last|
|
42
|
+
female.sing(first)
|
43
|
+
male.sing(last)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Singer = Struct.new(:name)
|
49
|
+
singer = Singer['Justin']
|
50
|
+
artist = Trio.new(singer)
|
51
|
+
|
52
|
+
words = ["Na", "Hey", "Baby", "Oh", "Ooh", "You"]
|
53
|
+
|
54
|
+
pop_song = words.permutation.flat_map(&:to_a).shuffle.first(20)
|
55
|
+
artist.perform pop_song
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# https://gist.github.com/fj/c67a81a8ded757ccc075
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module RangeQuery
|
5
|
+
def weekly(since, till = since + 1.week)
|
6
|
+
where(created_at: since...till)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class SendWeeklySummary < DCI::Context(:author, posts: RangeQuery)
|
11
|
+
def initialize(author) super(author: author, posts: author.posts) end
|
12
|
+
|
13
|
+
def perform since: Date.today.at_beginning_of_week, weekly_posts: posts.weekly(since)
|
14
|
+
weekly_digest = PostMailer.summary(weekly_posts, to: @author, title: "Your Posts this week")
|
15
|
+
weekly_digest.deliver unless weekly_posts.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
# load decoded job
|
19
|
+
def init_with(coder)
|
20
|
+
initialize Author.find coder['author_id']
|
21
|
+
end
|
22
|
+
|
23
|
+
# encode job
|
24
|
+
def encode_with(coder)
|
25
|
+
coder['author_id'] = @author.id
|
26
|
+
end
|
27
|
+
|
28
|
+
public :author
|
29
|
+
end
|
data/examples/dijkstra.rb
CHANGED
@@ -2,42 +2,52 @@ require_relative 'dijkstra/data'
|
|
2
2
|
# See https://github.com/RichOrElse/wrapper-based/blob/master/test/dijkstra_test.rb
|
3
3
|
|
4
4
|
module DestinationNode
|
5
|
-
def shortest_path_from(
|
6
|
-
return [self] if equal?
|
7
|
-
|
5
|
+
def shortest_path_from(node, finds_shortest)
|
6
|
+
return [self] if equal? node
|
7
|
+
finds_shortest.path from: node
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
Map
|
11
|
+
module Map
|
12
12
|
def distance_between(a, b)
|
13
13
|
@distances[Edge.new(a, b)]
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
16
|
+
def neighbors_of(node)
|
17
|
+
[south_neighbor_of(node), east_neighbor_of(node)].compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class FindsDistance < DCI::Context(city: Map)
|
22
|
+
def initialize(city) super(city: city) end
|
23
|
+
|
24
|
+
def between(from, to)
|
25
|
+
city.distance_between(from, to)
|
18
26
|
end
|
19
27
|
|
20
|
-
def
|
21
|
-
|
28
|
+
def of(path)
|
29
|
+
path.each_cons(2).inject(0) { |total_distance, (to, from)| total_distance + between(from, to) }
|
22
30
|
end
|
31
|
+
|
32
|
+
alias_method :call, :of
|
23
33
|
end
|
24
34
|
|
25
|
-
class
|
26
|
-
def initialize(city:, from: city.root, to: city.destination) super end
|
35
|
+
class FindsShortest < DCI::Context(:from, to: DestinationNode, city: Map, road_distance: FindsDistance)
|
36
|
+
def initialize(city:, from: city.root, to: city.destination, road_distance: city) super end
|
27
37
|
|
28
38
|
def distance
|
29
|
-
|
39
|
+
road_distance.of path
|
30
40
|
end
|
31
41
|
|
32
42
|
def path(from: @from)
|
33
|
-
|
43
|
+
city.neighbors_of(from).map(&to_shortest_path).min_by(&road_distance) << from
|
44
|
+
end
|
45
|
+
|
46
|
+
def call(neighbor = @from)
|
47
|
+
to.shortest_path_from(neighbor, self)
|
34
48
|
end
|
35
49
|
|
36
50
|
private
|
37
51
|
|
38
|
-
|
39
|
-
city.neighbors(near: current).
|
40
|
-
map { |neighbor| to.shortest_path_from(neighbor, self) }.
|
41
|
-
min_by { |neighbor_path| city.distance_of neighbor_path }
|
42
|
-
end
|
52
|
+
alias_method :to_shortest_path, :to_proc
|
43
53
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ServiceAPI
|
2
|
+
InvalidToken = Class.new(StandardError)
|
3
|
+
UnknownContact = Class.new(StandardError)
|
4
|
+
|
5
|
+
module Client
|
6
|
+
REQUEST_PATH = Rails.application.secrets.service_api_url + '/api/v2'
|
7
|
+
MAX_PER_PAGE = 100
|
8
|
+
|
9
|
+
def contacts(page: 1, per: MAX_PER_PAGE)
|
10
|
+
fail InvalidToken, 'Please provide valid token.' if service_api_token.blank?
|
11
|
+
url = REQUEST_PATH + '/contacts?' + { token: service_api_token, page: page, per: per }.to_query
|
12
|
+
JSON(RestClient.get url, header: { content_type: :json })
|
13
|
+
rescue RestClient::Unauthorized => error
|
14
|
+
fail InvalidToken, 'Please provide valid token.'
|
15
|
+
end
|
16
|
+
|
17
|
+
def send_message(composition)
|
18
|
+
fail InvalidToken, 'Please provide valid token.' if service_api_token.blank?
|
19
|
+
url = REQUEST_PATH + '/messages?' + { token: service_api_token }.to_query
|
20
|
+
RestClient.post(url, composition.to_json, { content_type: :json, accept: :json }).body
|
21
|
+
rescue RestClient::UnprocessableEntity, RestClient::NotFound => error
|
22
|
+
fail UnknownContact, 'Contact not recognized.'
|
23
|
+
rescue RestClient::Unauthorized => error
|
24
|
+
fail InvalidToken, 'Please provide valid token.'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module MessageComposingRecipient
|
29
|
+
def compose(body)
|
30
|
+
fail UnknownContact, 'Contact not recognized.' if blank?
|
31
|
+
{
|
32
|
+
recipients: ["contact-#{self}"],
|
33
|
+
message: { body: body }
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class SendsContactMessage < DCI::Context(:message, user: ServiceAPI::Client, to: ServiceAPI::MessageComposingRecipient)
|
40
|
+
def call(message = @message)
|
41
|
+
results = user.send_message to.compose(message)
|
42
|
+
return :success, results
|
43
|
+
rescue ServiceAPI::UnknownContact => error
|
44
|
+
return :failure, message: error.message
|
45
|
+
end
|
46
|
+
end
|
data/examples/money_transfer.rb
CHANGED
@@ -21,18 +21,16 @@ module Funds
|
|
21
21
|
end
|
22
22
|
|
23
23
|
class TransferMoney < DCI::Context(:amount, from: SourceAccount, to: DestinationAccount, events: Logging)
|
24
|
-
def initialize(amount: 0, events: [], **accounts)
|
25
|
-
super
|
26
|
-
end
|
24
|
+
def initialize(amount: 0, events: [], **accounts) super end
|
27
25
|
|
28
26
|
def withdraw(amount)
|
29
27
|
from.decrease_balance_by(amount)
|
30
|
-
events.log "Withdrew", amount, from
|
28
|
+
events.log "Withdrew", amount, @from
|
31
29
|
end
|
32
30
|
|
33
31
|
def deposit(amount)
|
34
32
|
to.increase_balance_by(amount)
|
35
|
-
events.log "Deposited", amount, to
|
33
|
+
events.log "Deposited", amount, @to
|
36
34
|
end
|
37
35
|
|
38
36
|
def transfer(amount)
|
@@ -42,13 +40,9 @@ module Funds
|
|
42
40
|
|
43
41
|
def call(amount: @amount)
|
44
42
|
transfer(amount)
|
45
|
-
return :success,
|
43
|
+
return :success, logging: @events, accounts: [@from, @to]
|
46
44
|
rescue Funds::Insufficient => error
|
47
|
-
return :failure,
|
48
|
-
end
|
49
|
-
|
50
|
-
def accounts
|
51
|
-
[@from, @to]
|
45
|
+
return :failure, message: error.message
|
52
46
|
end
|
53
47
|
end
|
54
48
|
end
|
data/examples/toy_shop.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
+
require 'wrapper_based'
|
2
|
+
|
1
3
|
# Data
|
2
4
|
|
3
5
|
Purchase = Struct.new(:toy, :buyer)
|
4
6
|
Deliver = Struct.new(:toy, :recipient, :purchase, :status)
|
5
7
|
|
6
|
-
#
|
8
|
+
# Interactions
|
7
9
|
|
8
|
-
module
|
10
|
+
module Shopper
|
9
11
|
def buy(toy)
|
10
12
|
Purchase.new toy, self
|
11
13
|
end
|
@@ -19,32 +21,25 @@ end
|
|
19
21
|
|
20
22
|
# Contexts
|
21
23
|
|
22
|
-
class
|
23
|
-
|
24
|
-
purchaser.as Recipient
|
24
|
+
class BuyToy < DCI::Context(shopper: Shopper, recipient: Recipient)
|
25
|
+
def initialize(shopper:, recipient: shopper) super end
|
25
26
|
|
26
|
-
def call(
|
27
|
-
|
28
|
-
|
27
|
+
def call(item)
|
28
|
+
bought = shopper.buy item
|
29
|
+
recipient.receive bought
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
class GiftToy < DCI::Context(:gifter, :giftee)
|
34
|
-
gifter.as Buyer
|
35
|
-
giftee.as Recipient
|
36
|
-
|
33
|
+
class GiftToy < DCI::Context(gifter: Shopper, giftee: Recipient)
|
37
34
|
def call(toy)
|
38
35
|
gift = gifter.buy toy
|
39
36
|
giftee.receive gift
|
40
37
|
end
|
41
38
|
end
|
42
39
|
|
43
|
-
|
44
|
-
|
45
|
-
finn_purchase_toy = PurchaseToy[purchaser: 'Finn']
|
40
|
+
finn_purchase_toy = BuyToy[shopper: 'Finn']
|
46
41
|
finn_purchase_toy.call 'Rusty sword'
|
47
42
|
finn_purchase_toy.('Armor of Zeldron')
|
48
43
|
finn_purchase_toy['The Enchiridion']
|
49
44
|
|
50
|
-
['Card Wars', 'Ice Ninja Manual', 'Bacon'].
|
45
|
+
puts ['Card Wars', 'Ice Ninja Manual', 'Bacon'].map &GiftToy[gifter: 'Jake', giftee: 'Finn']
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# See: https://medium.com/kkempin/facade-design-pattern-in-ruby-on-rails-710aa88326f
|
2
|
+
|
3
|
+
class UsersFacade < DCI::Context(:user, :users, :active_users, vip: VipUsersPresenter)
|
4
|
+
def initialize(user:, users: user.class, active_users: users.active, vip: active_users.vip) super end
|
5
|
+
|
6
|
+
def new_user
|
7
|
+
users.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def last_active_users
|
11
|
+
@last_active_users ||= active_users.order(:created_at).limit(10)
|
12
|
+
end
|
13
|
+
|
14
|
+
def vip_users
|
15
|
+
@vip_users ||= vip.users
|
16
|
+
end
|
17
|
+
|
18
|
+
def messages
|
19
|
+
@messages ||= user.messages
|
20
|
+
end
|
21
|
+
|
22
|
+
private :users, :active_users, :vip
|
23
|
+
end
|
data/lib/wrapper_based.rb
CHANGED
@@ -1,10 +1,30 @@
|
|
1
1
|
require "wrapper_based/version"
|
2
2
|
require "wrapper_based/context"
|
3
|
-
require "wrapper_based/
|
4
|
-
require "wrapper_based/
|
5
|
-
require "wrapper_based/
|
6
|
-
require "wrapper_based/
|
3
|
+
require "wrapper_based/casting"
|
4
|
+
require "wrapper_based/casting/type"
|
5
|
+
require "wrapper_based/casting/director"
|
6
|
+
require "wrapper_based/casting/pool"
|
7
7
|
require "wrapper_based/dci"
|
8
|
+
require "wrapper_based/type_casting"
|
9
|
+
require "wrapper_based/role_unknown"
|
8
10
|
|
9
11
|
module WrapperBased
|
12
|
+
def with!(**role_players)
|
13
|
+
role_players.each { |role, player| send :"with_#{role}!", player }
|
14
|
+
self
|
15
|
+
rescue NoMethodError => error
|
16
|
+
raise RoleUnknown.or(error)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
using TypeCasting
|
22
|
+
|
23
|
+
def cast_as(role, actor, &recast)
|
24
|
+
_components[role].as_role_played_by(actor, &recast)
|
25
|
+
end
|
26
|
+
|
27
|
+
def _components
|
28
|
+
@_components ||= {}
|
29
|
+
end
|
10
30
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module WrapperBased
|
2
|
+
RoleValueMissing = Class.new(StandardError)
|
3
|
+
|
4
|
+
class Casting < Module
|
5
|
+
def initialize
|
6
|
+
include WrapperBased
|
7
|
+
end
|
8
|
+
|
9
|
+
def casts(role)
|
10
|
+
role_writer = :"#{role}="
|
11
|
+
|
12
|
+
define_method(:"with_#{role}!") do |value|
|
13
|
+
send role_writer, yield(value)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
private_reader role
|
18
|
+
private_writer role
|
19
|
+
end
|
20
|
+
|
21
|
+
def casts_as(role, &recast)
|
22
|
+
role_player = :"@#{role}"
|
23
|
+
|
24
|
+
define_method(:"with_#{role}!") do |actor|
|
25
|
+
_components[role] = cast_as(role, actor, &recast)
|
26
|
+
instance_variable_set(role_player, actor)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
private_reader role
|
31
|
+
private_writer role
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def private_reader(role)
|
37
|
+
define_method(role) do
|
38
|
+
_components.fetch(role) { raise RoleValueMissing, "Role `#{role}' is missing.", caller }
|
39
|
+
end
|
40
|
+
|
41
|
+
private role
|
42
|
+
end
|
43
|
+
|
44
|
+
def private_writer(role)
|
45
|
+
writer = :"#{role}="
|
46
|
+
role_player = :"@#{role}"
|
47
|
+
|
48
|
+
define_method(writer) do |value|
|
49
|
+
_components[role] = value
|
50
|
+
instance_variable_set(role_player, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
private writer
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module WrapperBased
|
2
|
+
TALENTLESS = -> actor { actor }
|
3
|
+
|
4
|
+
class Casting::Director < Struct.new(:casting, :talent_pool)
|
5
|
+
def initialize(talent_pool) super(Casting.new, talent_pool) end
|
6
|
+
|
7
|
+
def adds(role, talent = TALENTLESS)
|
8
|
+
case talent
|
9
|
+
when ::Proc, ::Method, ::Symbol
|
10
|
+
casting.casts role, &talent
|
11
|
+
when ::Class
|
12
|
+
casting.casts role, &talent.method(:new)
|
13
|
+
when ::Module
|
14
|
+
casting.casts_as role, &type_for(role).has(talent).method(:typecast)
|
15
|
+
else
|
16
|
+
throw :wrong_talent_type, [role, talent, "expected Module, Class, Proc, Method or Symbol"]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_proc
|
21
|
+
method(:adds).to_proc
|
22
|
+
end
|
23
|
+
|
24
|
+
def type_for(role)
|
25
|
+
Casting::Type.new(role, talent_pool)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module WrapperBased
|
2
|
+
class Casting::Pool
|
3
|
+
def initialize(wrapper_for)
|
4
|
+
@talent_pool = Hash.new do |cache, type_casting|
|
5
|
+
cache[type_casting] = wrapper_for[*type_casting]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def casting_for(*supporting_roles, **leading_roles)
|
10
|
+
WrapperBased::Casting::Director.new(@talent_pool).tap do |director|
|
11
|
+
supporting_roles.each(&director)
|
12
|
+
leading_roles.each { |role, talent| director.adds role, talent }
|
13
|
+
end.casting
|
14
|
+
end
|
15
|
+
|
16
|
+
def context_for(*roles, &script)
|
17
|
+
klass = Class.new(Context).include casting_for(*roles)
|
18
|
+
klass.class_exec(&script) if block_given?
|
19
|
+
klass
|
20
|
+
end
|
21
|
+
end # Context class
|
22
|
+
end # WrapperBased module
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module WrapperBased
|
4
|
+
class Casting::Type
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name, talent_pool)
|
8
|
+
@name = name.to_sym
|
9
|
+
@talents = Set.new
|
10
|
+
@lookup = talent_pool
|
11
|
+
end
|
12
|
+
|
13
|
+
def has(talent)
|
14
|
+
@talents << talent
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](*where)
|
19
|
+
@lookup[where]
|
20
|
+
end
|
21
|
+
|
22
|
+
def cast_type(type)
|
23
|
+
self[type, *@talents]
|
24
|
+
end
|
25
|
+
|
26
|
+
def typecast(actor)
|
27
|
+
return actor if @talents.empty?
|
28
|
+
cast_type(actor.class).new actor
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,60 +1,22 @@
|
|
1
1
|
module WrapperBased
|
2
2
|
class Context
|
3
|
-
def initialize(
|
4
|
-
|
5
|
-
rebind where
|
6
|
-
end
|
7
|
-
|
8
|
-
def rebind(**where)
|
9
|
-
where.each { |role, player| public_send :"#{role}=", player }
|
10
|
-
self
|
3
|
+
def initialize(*role_players)
|
4
|
+
with!(*role_players)
|
11
5
|
end
|
12
6
|
|
13
7
|
def to_proc
|
14
8
|
method(:call).to_proc
|
15
9
|
end
|
16
10
|
|
17
|
-
def
|
18
|
-
|
11
|
+
def call(*)
|
12
|
+
raise NotImplementedError, "Context must implement a call method."
|
19
13
|
end
|
20
14
|
|
21
|
-
UnassignedRole = Class.new(StandardError)
|
22
|
-
|
23
15
|
class << self
|
24
16
|
alias_method :[], :new
|
25
17
|
|
26
|
-
def call(
|
27
|
-
new(
|
28
|
-
end
|
29
|
-
|
30
|
-
def cast_as(role, actor)
|
31
|
-
send(role).typecast(actor)
|
32
|
-
end
|
33
|
-
|
34
|
-
protected
|
35
|
-
|
36
|
-
def add_role(role, casting)
|
37
|
-
add_reader_for(role)
|
38
|
-
add_writer_for(role)
|
39
|
-
add_role_to_class role, casting
|
40
|
-
end
|
41
|
-
|
42
|
-
def add_reader_for(role)
|
43
|
-
define_method(role) do
|
44
|
-
@_casting_director_.fetch(role) { raise UnassignedRole, "Role '#{role}' is missing.", caller }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def add_writer_for(role)
|
49
|
-
role_player = :"@#{role}"
|
50
|
-
define_method(:"#{role}=") do |actor|
|
51
|
-
instance_variable_set(role_player, actor)
|
52
|
-
@_casting_director_.cast_as role, actor
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def add_role_to_class(role, casting)
|
57
|
-
define_singleton_method(role) { casting }
|
18
|
+
def call(*role_players, &blk)
|
19
|
+
new(*role_players).call(&blk)
|
58
20
|
end
|
59
21
|
end # class methods
|
60
22
|
end # Context class
|
data/lib/wrapper_based/dci.rb
CHANGED
@@ -3,9 +3,23 @@ require "type_wrapper"
|
|
3
3
|
module DCI
|
4
4
|
Module = Class.new(TypeWrapper::Module)
|
5
5
|
|
6
|
-
@@
|
6
|
+
@@casting_pool = WrapperBased::Casting::Pool.new(TypeWrapper)
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
class << self
|
9
|
+
def Context(*roles, &block)
|
10
|
+
fail_on_wrong_talent_type { return @@casting_pool.context_for(*roles, &block) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def Roles(*roles)
|
14
|
+
fail_on_wrong_talent_type { return @@casting_pool.casting_for(*roles) }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def fail_on_wrong_talent_type(&blk)
|
20
|
+
key, wrong, expected = catch(:wrong_talent_type, &blk)
|
21
|
+
message = "'#{key}: #{wrong}' has wrong key value type #{wrong.class} (#{expected})"
|
22
|
+
raise TypeError, message, caller
|
23
|
+
end
|
10
24
|
end
|
11
25
|
end
|
@@ -1,23 +1,17 @@
|
|
1
1
|
require 'delegate'
|
2
2
|
|
3
3
|
module WrapperBased
|
4
|
-
module
|
4
|
+
module TypeCasting
|
5
5
|
refine Object do
|
6
6
|
def as_role_played_by(actor)
|
7
|
-
actor
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
refine NilClass do
|
12
|
-
def as_role_played_by(actor)
|
13
|
-
yield
|
7
|
+
yield actor
|
14
8
|
end
|
15
9
|
end
|
16
10
|
|
17
11
|
refine Delegator do
|
18
12
|
def as_role_played_by(actor)
|
19
13
|
return replace_role_player_with(actor) if actor.instance_of?(role_type)
|
20
|
-
yield
|
14
|
+
yield actor
|
21
15
|
end
|
22
16
|
|
23
17
|
def replace_role_player_with(actor)
|
data/wrapper_based.gemspec
CHANGED
@@ -21,9 +21,11 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
spec.required_ruby_version = ">= 2.1.0"
|
23
23
|
|
24
|
-
spec.add_runtime_dependency "type_wrapper", "~> 1.
|
24
|
+
spec.add_runtime_dependency "type_wrapper", "~> 1.3"
|
25
25
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.15"
|
27
27
|
spec.add_development_dependency "rake", "~> 10.0"
|
28
28
|
spec.add_development_dependency "minitest", "~> 5.0"
|
29
|
+
spec.add_development_dependency "mocha", "~> 1.2.1"
|
30
|
+
spec.add_development_dependency "activesupport", "4.2.9"
|
29
31
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wrapper_based
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ritchie Paul Buitre
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: type_wrapper
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.3'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
26
|
+
version: '1.3'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +66,34 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mocha
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.2.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.2.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 4.2.9
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 4.2.9
|
69
97
|
description:
|
70
98
|
email:
|
71
99
|
- ritchie@richorelse.com
|
@@ -81,18 +109,23 @@ files:
|
|
81
109
|
- Rakefile
|
82
110
|
- bin/console
|
83
111
|
- bin/setup
|
112
|
+
- examples/acapella.rb
|
113
|
+
- examples/background_job.rb
|
84
114
|
- examples/dijkstra.rb
|
85
115
|
- examples/dijkstra/data.rb
|
116
|
+
- examples/http_api_client.rb
|
86
117
|
- examples/money_transfer.rb
|
87
118
|
- examples/toy_shop.rb
|
119
|
+
- examples/users_facade.rb
|
88
120
|
- lib/wrapper_based.rb
|
121
|
+
- lib/wrapper_based/casting.rb
|
122
|
+
- lib/wrapper_based/casting/director.rb
|
123
|
+
- lib/wrapper_based/casting/pool.rb
|
124
|
+
- lib/wrapper_based/casting/type.rb
|
89
125
|
- lib/wrapper_based/context.rb
|
90
|
-
- lib/wrapper_based/context/casting.rb
|
91
|
-
- lib/wrapper_based/context/casting_director.rb
|
92
|
-
- lib/wrapper_based/context/producer.rb
|
93
|
-
- lib/wrapper_based/context/type_casting.rb
|
94
126
|
- lib/wrapper_based/dci.rb
|
95
|
-
- lib/wrapper_based/
|
127
|
+
- lib/wrapper_based/role_unknown.rb
|
128
|
+
- lib/wrapper_based/type_casting.rb
|
96
129
|
- lib/wrapper_based/version.rb
|
97
130
|
- wrapper_based.gemspec
|
98
131
|
homepage: https://github.com/RichOrElse/wrapper-based/
|
@@ -115,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
148
|
version: '0'
|
116
149
|
requirements: []
|
117
150
|
rubyforge_project:
|
118
|
-
rubygems_version: 2.6.
|
151
|
+
rubygems_version: 2.6.13
|
119
152
|
signing_key:
|
120
153
|
specification_version: 4
|
121
154
|
summary: Wrapper Based DCI framework for OOP done right.
|
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'set'
|
2
|
-
|
3
|
-
module WrapperBased
|
4
|
-
class Context::Casting
|
5
|
-
attr_reader :name
|
6
|
-
|
7
|
-
def initialize(name, wrappers)
|
8
|
-
@name = name.to_sym
|
9
|
-
@casting = Set.new
|
10
|
-
@wrappers = wrappers
|
11
|
-
end
|
12
|
-
|
13
|
-
def as(extention)
|
14
|
-
@casting << extention
|
15
|
-
self
|
16
|
-
end
|
17
|
-
|
18
|
-
def wrapper_for(*args)
|
19
|
-
@wrappers[args]
|
20
|
-
end
|
21
|
-
|
22
|
-
def cast_type(type)
|
23
|
-
wrapper_for type, *@casting
|
24
|
-
end
|
25
|
-
|
26
|
-
def typecast(actor)
|
27
|
-
return actor if @casting.empty?
|
28
|
-
cast_type(actor.class).new actor
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
require 'forwardable'
|
2
|
-
|
3
|
-
module WrapperBased
|
4
|
-
class Context::CastingDirector
|
5
|
-
extend Forwardable
|
6
|
-
using Context::TypeCasting
|
7
|
-
|
8
|
-
def initialize(context_class)
|
9
|
-
@casts = {}
|
10
|
-
@re = context_class
|
11
|
-
end
|
12
|
-
|
13
|
-
def cast_as(role, actor)
|
14
|
-
@casts[role] = @casts[role].as_role_played_by(actor) { recast_as(role, actor) }
|
15
|
-
end
|
16
|
-
|
17
|
-
def_delegator :@re, :cast_as, :recast_as
|
18
|
-
def_delegators :@casts, :fetch, :[]
|
19
|
-
end
|
20
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module WrapperBased
|
2
|
-
class Context
|
3
|
-
class Producer
|
4
|
-
def initialize(wrapper_for)
|
5
|
-
@wrapper_cache = Hash.new do |cache, type_casting|
|
6
|
-
cache[type_casting] = wrapper_for[*type_casting]
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
def produce(*supporting, **leading, &script)
|
11
|
-
wrappers = @wrapper_cache
|
12
|
-
roles = supporting.map(&:to_sym) | leading.keys
|
13
|
-
Class.new(Context) do
|
14
|
-
roles.each { |role| add_role role, Casting.new(role, wrappers) }
|
15
|
-
leading.each_pair { |role, trait| send(role).as trait }
|
16
|
-
class_eval(&script) unless script.nil?
|
17
|
-
end
|
18
|
-
end # produce method
|
19
|
-
end # Producer class
|
20
|
-
end # Context class
|
21
|
-
end # WrapperBased module
|
data/lib/wrapper_based/roles.rb
DELETED
@@ -1,64 +0,0 @@
|
|
1
|
-
module WrapperBased
|
2
|
-
class Roles < Module
|
3
|
-
Unassigned = Class.new(StandardError)
|
4
|
-
|
5
|
-
def initialize(wrappers, *roles, **where, &block)
|
6
|
-
@casting_agent = (where.keys | roles.map(&:to_sym)).inject {|cast, role| cast[role] = Casting.new(role, wrappers) }
|
7
|
-
@casting_agent.keys.each { |role| add_role role }
|
8
|
-
where.each_pair {|role, trait| @casting[role].as trait }
|
9
|
-
define_casting_director(@casting_agent)
|
10
|
-
include InstanceMethods
|
11
|
-
end
|
12
|
-
|
13
|
-
def cast_as role, actor
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
def defing_casting_director(casting_agent)
|
18
|
-
define_method(:_casting_director_) { @_casting_director ||= CastingDirector.new(casting_agent) }
|
19
|
-
end
|
20
|
-
|
21
|
-
def cast_as(role, actor)
|
22
|
-
@casting[role].typecast actor
|
23
|
-
end
|
24
|
-
|
25
|
-
def add_role(role)
|
26
|
-
add_reader_for(role)
|
27
|
-
add_writer_for(role)
|
28
|
-
end
|
29
|
-
|
30
|
-
def add_reader_for(role)
|
31
|
-
define_method(role) do
|
32
|
-
_casting_director_.fetch(role) { raise Roles::Unassigned, "Role '#{role}' is missing.", caller }
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def add_writer_for(role)
|
37
|
-
role_player = :"@#{role}"
|
38
|
-
define_method(:"#{role}=") do |actor|
|
39
|
-
instance_variable_set(role_player, actor)
|
40
|
-
_casting_director_.cast_as role, actor
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def add_role_to_class(role, casting)
|
45
|
-
define_singleton_method(role) { casting }
|
46
|
-
end
|
47
|
-
|
48
|
-
def define_casting_director
|
49
|
-
roles = self
|
50
|
-
end
|
51
|
-
|
52
|
-
def included(base)
|
53
|
-
base.extend ClassMethods
|
54
|
-
end
|
55
|
-
|
56
|
-
module InstanceMethods
|
57
|
-
def rebind(**where, &block)
|
58
|
-
where.each { |role, player| public_send :"#{role}=", player }
|
59
|
-
instance_eval &block if block_given?
|
60
|
-
self
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|