wrapper_based 1.1.0 → 2.0.0

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 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