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.
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: []