calls 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +162 -0
  3. data/lib/calls.rb +57 -0
  4. 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
+ [![Version](https://img.shields.io/gem/v/calls)](https://github.com/halo/calls.svg/releases)
4
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/halo/calls/blob/master/LICENSE.md)
5
+ ![Build Status](https://github.com/halo/calls/actions/workflows/tests.yml/badge.svg)
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: []