calls 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +162 -0
- data/lib/calls.rb +57 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: aecc26178de00f8b7440cc617980fd92e44393e4dc630b593f1253f87f26459b
|
4
|
+
data.tar.gz: 39f7a41a588d9e2682f08a251d6f113cad22a6f8dc01e14740d7f98caf4cf903
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f23af889c1b7eecb68d4578d2723417cbb5a82e2e6dcaabd3e8c90d3e2bde2206c6633f0a0740c1f014ee27937200dfd998aa8e89bb87610948da3c792a40d6e
|
7
|
+
data.tar.gz: 3ff4a15d638243d65c2d606e93597a5243a03e2b28309eb65173ec9f99b8cf5e0591ffcf6644e80f22e856065ff8bc23bf59a1494f5227c5f1cb7e70de2c7017
|
data/README.md
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
# Calls
|
2
|
+
|
3
|
+
[](https://github.com/halo/calls.svg/releases)
|
4
|
+
[](https://github.com/halo/calls/blob/master/LICENSE.md)
|
5
|
+

|
6
|
+
|
7
|
+
### TL; DR
|
8
|
+
|
9
|
+
Instead of writing a method in Ruby, you might as well write an entire class for the task. That's called the [method object pattern](https://refactoring.guru/replace-method-with-method-object) and helps to reduce complexity.
|
10
|
+
|
11
|
+
|
12
|
+
This gem helps you to do that, like so:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
class SaySometing
|
16
|
+
include Calls
|
17
|
+
|
18
|
+
# Input
|
19
|
+
option :text
|
20
|
+
|
21
|
+
# Output
|
22
|
+
def call
|
23
|
+
puts text
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
SaySometihng.call(text: 'Hi there!') # => 'Hi there!'
|
28
|
+
```
|
29
|
+
|
30
|
+
## Installation
|
31
|
+
|
32
|
+
```
|
33
|
+
# Add this to your Gemfile
|
34
|
+
gem 'calls'
|
35
|
+
```
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
If you only have one mandatory, obvious argument, this is what your implementation most likely would look like:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class CalculateTax
|
43
|
+
include Calls
|
44
|
+
|
45
|
+
param :product
|
46
|
+
|
47
|
+
def call
|
48
|
+
product.price * 0.1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
bike = Bike.new(price: 50)
|
53
|
+
CalculateTax.call(bike) # => 5
|
54
|
+
```
|
55
|
+
|
56
|
+
If you prefer to use named keywords, use this instead:
|
57
|
+
|
58
|
+
````ruby
|
59
|
+
class CalculateTax
|
60
|
+
include Calls
|
61
|
+
|
62
|
+
option :product
|
63
|
+
|
64
|
+
def call
|
65
|
+
product.price * 0.1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
bike = Bike.new(price: 50)
|
70
|
+
CalculateTax.call(product: bike) # => 5
|
71
|
+
````
|
72
|
+
|
73
|
+
You can also use both params and options. They are all mandatory.
|
74
|
+
|
75
|
+
````ruby
|
76
|
+
class CalculateTax
|
77
|
+
include Calls
|
78
|
+
|
79
|
+
param :product
|
80
|
+
option :dutyfree
|
81
|
+
|
82
|
+
def call
|
83
|
+
return 0 if dutyfree
|
84
|
+
product.price * 0.1
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
bike = Bike.new(price: 50)
|
89
|
+
CalculateTax.call(bike, dutyfree: true) # => 0
|
90
|
+
````
|
91
|
+
|
92
|
+
You can make options optional by defining a default value in a proc:
|
93
|
+
|
94
|
+
````ruby
|
95
|
+
class CalculateTax
|
96
|
+
include Calls
|
97
|
+
|
98
|
+
param :product
|
99
|
+
option :dutyfree, default: -> { false }
|
100
|
+
|
101
|
+
def call
|
102
|
+
return 0 if dutyfree
|
103
|
+
product.price * 0.1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
bike = Bike.new(price: 50)
|
108
|
+
CalculateTax.call(bike) # => 5
|
109
|
+
````
|
110
|
+
|
111
|
+
That's it!
|
112
|
+
|
113
|
+
## History
|
114
|
+
|
115
|
+
A minimal implementation of the method object pattern would probably look like the following. This is sometimes also referred to as "service class".
|
116
|
+
|
117
|
+
This is what [deadlyicon/calls](https://github.com/deadlyicon/calls) originally used (that's where the gem name comes from).
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class SaySometing
|
121
|
+
def self.call(*args, &block)
|
122
|
+
new.call(*args, &block)
|
123
|
+
end
|
124
|
+
|
125
|
+
def call(text)
|
126
|
+
puts text
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
Basically everything passed to `MyClass.call(...)` would be passed on to `MyClass.new.call(...)`.
|
132
|
+
|
133
|
+
Even better still, it *should* be passed on to `MyClass.new(...).call` so that your implementation becomes cleaner:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
class SaySometing
|
137
|
+
def self.call(*args, &block)
|
138
|
+
new(*args, &block).call
|
139
|
+
end
|
140
|
+
|
141
|
+
def initialize(text:)
|
142
|
+
@text = text
|
143
|
+
end
|
144
|
+
|
145
|
+
def call
|
146
|
+
puts @text
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
People [implemented that](https://rubygems.org/gems/Calls), but in doing so reinvented the wheel. Because now you not only have the method object pattern (i.e. `call`), now you also have to deal with initialization (i.e. `new`).
|
152
|
+
|
153
|
+
That's where the popular [dry-initializer](https://dry-rb.org/gems/dry-initializer) gem comes in. It is a battle-tested way to initialize objects with mandatory and optional attributes.
|
154
|
+
|
155
|
+
The `calls` gem (you're looking at it right now), combines both the method object pattern and dry initialization. The [team](https://github.com/bukowskis), who initially came up with using `dry-initializer`, published the initial code version under the name [method_object](https://github.com/bukowskis/method_object).
|
156
|
+
# Caveats
|
157
|
+
|
158
|
+
* `params` cannot be optional (or have default values). This is because there can be several params in a row, which leads to confusion when they are optional.
|
159
|
+
|
160
|
+
# License
|
161
|
+
|
162
|
+
MIT License, see [LICENSE.md](https://github.com/halo/calls/blob/master/LICENSE.md)
|
data/lib/calls.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
# Include this module to enable dry-initialization like a method object.
|
6
|
+
module Calls
|
7
|
+
def self.included(base)
|
8
|
+
base.extend Dry::Initializer
|
9
|
+
base.extend ClassMethods
|
10
|
+
base.send(:private_class_method, :new)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Methods that are added to your class.
|
14
|
+
module ClassMethods
|
15
|
+
def call(*args, **kwargs, &block)
|
16
|
+
__check_for_unknown_options(*args, **kwargs)
|
17
|
+
|
18
|
+
if kwargs.empty?
|
19
|
+
# Preventing the warning "Passing the keyword argument as the last hash parameter is deprecated"
|
20
|
+
new(*args).call(&block)
|
21
|
+
else
|
22
|
+
new(*args, **kwargs).call(&block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Overriding the implementation of `#param` in the `dry-initializer` gem.
|
27
|
+
# Because of the positioning of multiple params, params can never be omitted in a method object.
|
28
|
+
# See https://github.com/dry-rb/dry-initializer/blob/main/lib/dry/initializer.rb
|
29
|
+
def param(name, type = nil, **opts, &block)
|
30
|
+
raise ArgumentError, "Default value for param not allowed - #{name}" if opts.key? :default
|
31
|
+
raise ArgumentError, "Optional params not supported - #{name}" if opts.fetch(:optional, false)
|
32
|
+
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def __check_for_unknown_options(*args, **kwargs)
|
37
|
+
return if __defined_options.empty?
|
38
|
+
|
39
|
+
# Checking params
|
40
|
+
opts = args.drop(__defined_params.length).first || kwargs
|
41
|
+
raise ArgumentError, "Unexpected argument #{opts}" unless opts.is_a? Hash
|
42
|
+
|
43
|
+
# Checking options
|
44
|
+
unknown_options = opts.keys - __defined_options
|
45
|
+
message = "Key(s) #{unknown_options} not found in #{__defined_options}"
|
46
|
+
raise KeyError, message if unknown_options.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
def __defined_options
|
50
|
+
dry_initializer.options.map(&:source)
|
51
|
+
end
|
52
|
+
|
53
|
+
def __defined_params
|
54
|
+
dry_initializer.params.map(&:source)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: calls
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- halo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-initializer
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop-minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: warning
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- README.md
|
76
|
+
- lib/calls.rb
|
77
|
+
homepage:
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata:
|
81
|
+
bug_tracker_uri: https://github.com/halo/calls/issues
|
82
|
+
changelog_uri: https://github.com/halo/calls/blob/master/CHANGELOG.md
|
83
|
+
rubygems_mfa_required: 'true'
|
84
|
+
source_code_uri: https://github.com/halo/calls
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.7.0
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubygems_version: 3.3.7
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: Combining the method object pattern with DRY initialization
|
104
|
+
test_files: []
|