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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a512a58ee3c31a5d96debe8d835854faa1b05d1d
4
- data.tar.gz: 0cb617aec6443efcb7972c5d015d43d4533f2918
3
+ metadata.gz: c9905f3b4631008847a1ea6998fc79ce2d186727
4
+ data.tar.gz: bf443b8da55d7e0c32b7cdb99e09a8d47b585901
5
5
  SHA512:
6
- metadata.gz: 70e62a920819ba82bd6396b3849258e83177a8b99ba3b4d74548f71af68b4714012926e2cc904d101860da8b5dc982b2c2bd7c3bf24b9e0ae4ca3d6b6da192ce
7
- data.tar.gz: 318f1a9ab51099e22f5e4234161ccd24931aba3067f24eedcc1ed6b593ea6b6b4e7843fe3f270379a0191322b9d622bcb37b3b8f56aa097aae623ce10c6b75e4
6
+ metadata.gz: 2f5bc3f9544c492ed7a5307f3b5a57463e8ef12dcb9972998da421298ecc528e3f215097d33bf8cdc8ebb1275980ee32a77f70d908bec84ea22fbc08d6f89ec2
7
+ data.tar.gz: 9b42afaff1621d74441b3373ce7879bc2d167959b3fcdea5b7d46d6562a2df02395261b1591406a8f567596ef07daccfc1bc2a580ff359fd6ba072b281155be1
data/.gitignore CHANGED
@@ -8,3 +8,5 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  *.gem
11
+ .gitignore
12
+ .ruby-version
@@ -1,7 +1,7 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.1.0
4
+ - 2.1.2
5
5
  - 2.2.0
6
6
  - 2.3.0
7
7
  - 2.4.0
data/README.md CHANGED
@@ -17,88 +17,137 @@ And then execute:
17
17
 
18
18
  $ bundle
19
19
 
20
- ## Usage
20
+ ## DCI::Roles
21
21
 
22
- [Dijkstra data](https://github.com/RichOrElse/wrapper-based/blob/master/examples/dijkstra/data.rb) |
23
- [Dijkstra test](https://github.com/RichOrElse/wrapper-based/blob/master/test/dijkstra_test.rb) |
24
- Djikstra example:
22
+ Includes role components to classes.
23
+
24
+ ### Usage
25
25
 
26
26
  ```ruby
27
- module DestinationNode
28
- def shortest_path_from(neighbor, find_shortest)
29
- return [self] if equal? neighbor
30
- find_shortest.path(from: neighbor)
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
- Map = DCI::Module.new do |mod| using mod
35
- def distance_between(a, b)
36
- @distances[Edge.new(a, b)]
37
- end
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 distance_of(path)
40
- path.each_cons(2).inject(0) { |total, (to, from)| total + distance_between(from, to) }
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
- def neighbors(near:)
44
- [south_neighbor_of(near), east_neighbor_of(near)].compact # excludes nil neighbors
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 FindShortest < DCI::Context(:from, to: DestinationNode, city: Map)
49
- def initialize(city:, from: city.root, to: city.destination) super end
75
+ class SignsUser < Struct.new(:signed)
76
+ def in!(user)
77
+ signed[:user_id] = user.id
78
+ end
50
79
 
51
- def distance
52
- city.distance_of path
80
+ def out!
81
+ signed.delete(:user_id)
82
+ @user = nil
53
83
  end
54
84
 
55
- def path(from: @from)
56
- shortest_neighbor_path(from) << from
85
+ def out?
86
+ signed[:user_id].nil? || in?
57
87
  end
58
88
 
59
- private
89
+ def in?
90
+ !user.nil?
91
+ end
60
92
 
61
- def shortest_neighbor_path(current)
62
- city.neighbors(near: current).
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
- [View more examples](https://github.com/RichOrElse/wrapper-based/tree/master/examples)
97
+ def by(**identification)
98
+ User.find_by identification
99
+ end
70
100
 
71
- ## Context methods
101
+ def with(**credentials)
102
+ User.new credentials
103
+ end
72
104
 
73
- ### context#to_proc
105
+ def up!(user)
106
+ user.save && user if User.validate_registering(user)
107
+ end
108
+ end
74
109
 
75
- Returns call method as a Proc.
110
+ module UserParams
111
+ def registration
112
+ permit(:username, :email)
113
+ end
76
114
 
77
- ```ruby
78
- ['Card Wars', 'Ice Ninja Manual', 'Bacon'].map &GiftToy[gifter: 'Jake', giftee: 'Finn']
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
- ### context[params,...]
129
+ ## DCI::Context
82
130
 
83
- Square brackets are alias for call method.
131
+ ### Usage
84
132
 
85
133
  ```ruby
86
- TransferMoney[from: source_account, to: destination_account][amount: 100]
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
- ### context#rebind(**params)
90
- Assigns object to role.
142
+ #### Context#to_proc
143
+
144
+ Returns call method as a Proc.
91
145
 
92
146
  ```ruby
93
- add_member = Evaluate.new(to: 'Justice League')
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
- ## Context class methods
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
- ## DCI::Module
116
-
117
- Extention module for supporting procedural code. Define a block with the 'new' method and pass the 'mod' parameter to 'using' keyword.
118
-
119
- ```ruby
120
- AwesomeSinging = TypeWrapper::Module.new do |mod| using mod
121
- def sing
122
- "#{name} sings #{song}"
123
- end
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
@@ -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(neighbor, find_shortest)
6
- return [self] if equal? neighbor
7
- find_shortest.path(from: neighbor)
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 = DCI::Module.new do |mod| using mod
11
+ module Map
12
12
  def distance_between(a, b)
13
13
  @distances[Edge.new(a, b)]
14
14
  end
15
15
 
16
- def distance_of(path)
17
- path.each_cons(2).inject(0) { |total, (to, from)| total + distance_between(from, to) }
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 neighbors(near:)
21
- [south_neighbor_of(near), east_neighbor_of(near)].compact # excludes nil neighbors
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 FindShortest < DCI::Context(:from, to: DestinationNode, city: Map)
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
- city.distance_of path
39
+ road_distance.of path
30
40
  end
31
41
 
32
42
  def path(from: @from)
33
- shortest_neighbor_path(from) << from
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
- def shortest_neighbor_path(current)
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
@@ -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, { log: @events }, accounts
43
+ return :success, logging: @events, accounts: [@from, @to]
46
44
  rescue Funds::Insufficient => error
47
- return :failure, { message: error.message }, accounts
48
- end
49
-
50
- def accounts
51
- [@from, @to]
45
+ return :failure, message: error.message
52
46
  end
53
47
  end
54
48
  end
@@ -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
- # Behaviors
8
+ # Interactions
7
9
 
8
- module Buyer
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 PurchaseToy < DCI::Context(:purchaser)
23
- purchaser.as Buyer
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(toy)
27
- purchased = purchaser.buy toy
28
- purchaser.receive purchased
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
- # Interactions
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'].each &GiftToy[gifter: 'Jake', giftee: 'Finn']
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
@@ -1,10 +1,30 @@
1
1
  require "wrapper_based/version"
2
2
  require "wrapper_based/context"
3
- require "wrapper_based/context/producer"
4
- require "wrapper_based/context/casting"
5
- require "wrapper_based/context/type_casting"
6
- require "wrapper_based/context/casting_director"
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(**where)
4
- @_casting_director_ = CastingDirector.new(self.class)
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 [](*args, &block)
18
- call(*args, &block)
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(**where)
27
- new(where).call
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
@@ -3,9 +3,23 @@ require "type_wrapper"
3
3
  module DCI
4
4
  Module = Class.new(TypeWrapper::Module)
5
5
 
6
- @@context_producer = WrapperBased::Context::Producer.new(TypeWrapper)
6
+ @@casting_pool = WrapperBased::Casting::Pool.new(TypeWrapper)
7
7
 
8
- def self.Context(*args, &block)
9
- @@context_producer.produce(*args, &block)
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
@@ -0,0 +1,8 @@
1
+ module WrapperBased
2
+ class RoleUnknown < NoMethodError
3
+ def self.or(error)
4
+ return error unless error.message =~ /^undefined method `with_(\w..)/
5
+ new error.message.sub('undefined method', 'unknown role').sub('with_','').sub('!','')
6
+ end
7
+ end
8
+ end
@@ -1,23 +1,17 @@
1
1
  require 'delegate'
2
2
 
3
3
  module WrapperBased
4
- module Context::TypeCasting
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)
@@ -1,3 +1,3 @@
1
1
  module WrapperBased
2
- VERSION = "1.1.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -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.2"
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: 1.1.0
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-07-20 00:00:00.000000000 Z
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.2'
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.2'
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/roles.rb
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.8
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
@@ -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