telephone 0.1.0 → 1.1.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
  SHA256:
3
- metadata.gz: 2961c1545d60c8daef9cdc90a8ecea01c12ecd14357d446dbfe86cae84cc43c2
4
- data.tar.gz: 27dad4361e48e380776fb3c32ea59855f95804e024b1aad879d84ad292f96a23
3
+ metadata.gz: 45fbcb9aaecf2dc2d66072e93b97b168c2a505f6d4e0cc1ed08cfad4e8267bb2
4
+ data.tar.gz: 4f3992fbc6840c3d624fadb7de3faa56f3952ba4b4b5eae7cde0b61ecdf343f6
5
5
  SHA512:
6
- metadata.gz: 70159c4118b77a694873f4cf9f14d28bfbaae52692c28eaa95eafec80b06a91e8fd65dd76db1a849efdd5e7b5a294c5d1836a5b924c6df81eb9b9a636145c14e
7
- data.tar.gz: 2bd95f410bc0bc92960c1e6065ecf5b0971dbc6792dff760cebef1494d1cdbc8efd5e87499d53a7eaae1786f8904a4280b570c9d59327995877a17fad20a8773
6
+ metadata.gz: ef63c3786eb25e545259ed90894ace7a67af3098028ebcdfc9206aa7bd995729dafa3ed5533a8109bd42696255216efb4db84e53fd9160304f8c7c868862dde5
7
+ data.tar.gz: 3a9419a284e42bde8b0de7bd8517a4dc7986e9f59b8c685a41059c8e5d81660d36c4aea2d3c3bba4610213211d243fceb2007103f2d79ac7f1fadea9be673431
data/README.md CHANGED
@@ -94,6 +94,48 @@ You can also give a default value for an argument.
94
94
  argument :name, default: "Benjamin"
95
95
  ```
96
96
 
97
+ Defaults can also be callable (procs or lambdas) that are evaluated at runtime. Callable defaults have access to other attributes and are processed in definition order:
98
+
99
+ ```ruby
100
+ class GreetingService < ApplicationService
101
+ argument :first_name, default: "John"
102
+ argument :last_name, default: "Doe"
103
+ argument :full_name, default: -> { "#{first_name} #{last_name}" }
104
+ argument :created_at, default: -> { Time.current }
105
+
106
+ def call
107
+ "Hello, #{full_name}!"
108
+ end
109
+ end
110
+
111
+ GreetingService.call.result #=> "Hello, John Doe!"
112
+ GreetingService.call(first_name: "Jane").result #=> "Hello, Jane Doe!"
113
+ ```
114
+
115
+ Arguments can also be validated against a type. When `type:` is provided, the argument is checked with `is_a?` before the service runs. `nil` is allowed unless the argument is also marked as `required:`.
116
+
117
+ ```ruby
118
+ class UpdateUserService < ApplicationService
119
+ argument :user, type: User, required: true
120
+
121
+ def call
122
+ user.update!(status: "active")
123
+ end
124
+ end
125
+
126
+ s = UpdateUserService.call(user: "not a user")
127
+ s.success? #=> false
128
+ s.errors.full_messages #=> ["User must be a User"]
129
+ s.result #=> nil
130
+ ```
131
+
132
+ Type validation works with any class or module:
133
+
134
+ ```ruby
135
+ argument :tags, type: Array
136
+ argument :metadata, type: Enumerable
137
+ ```
138
+
97
139
  ### Validations
98
140
 
99
141
  Since `Telephone::Service` includes `ActiveModel::Model`, you can define validations in the same way you would for an ActiveRecord model.
@@ -107,13 +149,49 @@ def admin_user?
107
149
  end
108
150
  ```
109
151
 
110
- If a validation fails, the service object will not execute and return `nil` as the result of th call. You can check the status of the service object by calling `success?`.
152
+ If a validation fails, the service object will not execute and return `nil` as the result of the call. You can check the status of the service object by calling `success?`.
111
153
 
112
154
  ```ruby
113
155
  s = SomeService.call
114
156
  s.success? #=> false
115
157
  ```
116
158
 
159
+ #### Internationalization
160
+
161
+ Type validation errors are translated through I18n. Telephone ships with translations for:
162
+
163
+ * `en` — English
164
+ * `es` — Spanish
165
+ * `fr` — French
166
+ * `de` — German
167
+ * `pt` — Portuguese
168
+ * `ja` — Japanese
169
+ * `zh-CN` — Chinese (Simplified)
170
+ * `it` — Italian
171
+ * `nl` — Dutch
172
+ * `ru` — Russian
173
+ * `ko` — Korean
174
+
175
+ In a Rails app, these are used automatically based on the current locale:
176
+
177
+ ```ruby
178
+ I18n.locale = :es
179
+
180
+ s = UpdateUserService.call(user: "not a user")
181
+ s.errors.full_messages #=> ["User debe ser un User"]
182
+ ```
183
+
184
+ You can also override the default message in your own locale files:
185
+
186
+ ```yaml
187
+ # config/locales/telephone.en.yml
188
+ en:
189
+ activemodel:
190
+ errors:
191
+ messages:
192
+ invalid_type: "must be an instance of %{type}"
193
+ ```
194
+
117
195
  ## Best Practices
118
196
 
119
197
  Service objects are a great way to keep your code DRY and encapsulate business logic. As a rule of thumb, try to keep your service objects to a single responsibility. If you find yourself dealing with very complex logic, consider breaking it up into smaller services.
@@ -0,0 +1,5 @@
1
+ de:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "muss ein %{type} sein"
@@ -0,0 +1,5 @@
1
+ en:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "must be a %{type}"
@@ -0,0 +1,5 @@
1
+ es:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "debe ser un %{type}"
@@ -0,0 +1,5 @@
1
+ fr:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "doit être un %{type}"
@@ -0,0 +1,5 @@
1
+ it:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "deve essere un %{type}"
@@ -0,0 +1,5 @@
1
+ ja:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "%{type}である必要があります"
@@ -0,0 +1,5 @@
1
+ ko:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "%{type}이어야 합니다"
@@ -0,0 +1,5 @@
1
+ nl:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "moet een %{type} zijn"
@@ -0,0 +1,5 @@
1
+ pt:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "deve ser um %{type}"
@@ -0,0 +1,5 @@
1
+ ru:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "должно быть %{type}"
@@ -0,0 +1,5 @@
1
+ "zh-CN":
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ invalid_type: "必须是 %{type}"
@@ -16,6 +16,34 @@ module Telephone
16
16
  # Telephone::Service.call(foo: bar).result # "baz"
17
17
  attr_accessor :result
18
18
 
19
+ ##
20
+ # Primary responsibility of initialize is to instantiate the
21
+ # attributes of the service object with the expected values.
22
+ def initialize(attributes = {})
23
+ attributes = attributes.transform_keys(&:to_sym)
24
+
25
+ attributes.each do |key, value|
26
+ send("#{key}=", value)
27
+ end
28
+
29
+ self.class.defaults.each do |key, value|
30
+ next if attributes.key?(key)
31
+
32
+ resolved = if value.is_a?(Proc)
33
+ instance_exec(&value)
34
+ elsif value.respond_to?(:call)
35
+ value.call
36
+ else
37
+ value
38
+ end
39
+
40
+ send("#{key}=", resolved)
41
+ end
42
+
43
+ super
44
+ yield self if block_given?
45
+ end
46
+
19
47
  ##
20
48
  # Determines whether or not the action of the service
21
49
  # object was successful.
@@ -28,27 +56,55 @@ module Telephone
28
56
  errors.empty?
29
57
  end
30
58
 
59
+ def call
60
+ self.result = __call if valid?
61
+ self
62
+ end
63
+
31
64
  class << self
32
65
  ##
33
66
  # Defines a getter/setter for a service object argument. This also allows you
34
67
  # to pass in a default, or set the argument to "required" to add a validation
35
68
  # that runs before executing the block.
36
69
  #
70
+ # The default value can be a static value or any callable object (Proc, lambda,
71
+ # method, or any object that responds to #call) that will be evaluated at
72
+ # runtime when the service is instantiated.
73
+ #
74
+ # Callable defaults are evaluated in the context of the service instance,
75
+ # so they can access other attributes. They are processed in definition order,
76
+ # meaning a callable can depend on any argument defined before it.
77
+ #
78
+ # To store a Proc as the actual value, wrap it in another lambda:
79
+ # argument :my_proc, default: -> { -> { puts "hi" } }
80
+ #
81
+ # You can also pass +type:+ to validate that the argument is an instance of
82
+ # the given class or module. Nil values are allowed unless the argument is
83
+ # also marked as required.
84
+ #
37
85
  # @example
38
86
  # class SomeService < Telephone::Service
39
- # argument :foo, default: "bar"
40
- # argument :baz, required: true
87
+ # argument :first_name, default: "John"
88
+ # argument :last_name, default: "Doe"
89
+ # argument :full_name, default: -> { "#{first_name} #{last_name}" }
90
+ # argument :timestamp, default: -> { DateTime.current }
91
+ # argument :user, type: User
41
92
  #
42
93
  # def call
43
- # puts foo
44
- # puts baz
94
+ # puts full_name
95
+ # puts timestamp
45
96
  # end
46
97
  # end
47
- def argument(arg, default: nil, required: false)
48
- send(:attr_accessor, arg.to_sym)
49
- send(:validates, arg.to_sym, presence: true) if required
98
+ def argument(arg, default: nil, required: false, type: nil)
99
+ arg = arg.to_sym
100
+ send(:attr_accessor, arg)
101
+ send(:validates, arg, presence: true) if required
102
+
103
+ if type
104
+ send(:validates_with, Telephone::TypeValidator, attributes: arg, with: type, allow_nil: true)
105
+ end
50
106
 
51
- defaults[arg.to_sym] = default
107
+ defaults[arg] = default
52
108
  end
53
109
 
54
110
  ##
@@ -65,10 +121,21 @@ module Telephone
65
121
  #
66
122
  # @example
67
123
  # Telephone::Service.call(foo: bar)
68
- def call(**args)
69
- instance = new(defaults.merge(args))
70
- instance.result = instance.call if instance.valid?
71
- instance
124
+ # Telephone::Service.call({ foo: bar })
125
+ def call(*args, **kwargs)
126
+ new(args.extract_options!.merge(kwargs)).call
127
+ end
128
+
129
+ ##
130
+ # When the subclass overwrites the #call method, reassign it to #__call.
131
+ # This allows us to still control what happens in the instance level of #call.
132
+ def method_added(method_name)
133
+ if method_name == :call
134
+ alias_method :__call, :call
135
+ send(:remove_method, :call)
136
+ else
137
+ super
138
+ end
72
139
  end
73
140
  end
74
141
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module Telephone
6
+ class TypeValidator < ActiveModel::EachValidator
7
+ def validate_each(record, attribute, value)
8
+ expected_type = options[:with]
9
+
10
+ return if value.is_a?(expected_type)
11
+
12
+ record.errors.add(
13
+ attribute,
14
+ :invalid_type,
15
+ **options.except(:with).merge(type: type_name(expected_type))
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def type_name(type)
22
+ type.name || type.to_s
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Telephone
4
- VERSION = "0.1.0"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/telephone.rb CHANGED
@@ -1,3 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "i18n"
4
+
5
+ I18n.load_path += Dir[File.join(__dir__, "telephone", "locale", "*.yml")]
6
+
7
+ require "telephone/validators/type_validator"
3
8
  require "telephone/service"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telephone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Hargett
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-07-31 00:00:00.000000000 Z
10
+ date: 2026-06-08 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activemodel
@@ -24,6 +23,20 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: i18n
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
27
40
  description: Utility class for creating and calling service objects.
28
41
  email: hargettbenjamin@gmail.com
29
42
  executables: []
@@ -32,7 +45,19 @@ extra_rdoc_files: []
32
45
  files:
33
46
  - README.md
34
47
  - lib/telephone.rb
48
+ - lib/telephone/locale/de.yml
49
+ - lib/telephone/locale/en.yml
50
+ - lib/telephone/locale/es.yml
51
+ - lib/telephone/locale/fr.yml
52
+ - lib/telephone/locale/it.yml
53
+ - lib/telephone/locale/ja.yml
54
+ - lib/telephone/locale/ko.yml
55
+ - lib/telephone/locale/nl.yml
56
+ - lib/telephone/locale/pt.yml
57
+ - lib/telephone/locale/ru.yml
58
+ - lib/telephone/locale/zh-CN.yml
35
59
  - lib/telephone/service.rb
60
+ - lib/telephone/validators/type_validator.rb
36
61
  - lib/telephone/version.rb
37
62
  homepage: https://github.com/bharget/telephone
38
63
  licenses: []
@@ -40,7 +65,6 @@ metadata:
40
65
  bug_tracker_uri: https://github.com/bharget/telephone/issues
41
66
  changelog_uri: https://github.com/bharget/telephone/blob/master/CHANGELOG.md
42
67
  source_code_uri: https://github.com/bharget/telephone
43
- post_install_message:
44
68
  rdoc_options: []
45
69
  require_paths:
46
70
  - lib
@@ -48,7 +72,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
48
72
  requirements:
49
73
  - - ">="
50
74
  - !ruby/object:Gem::Version
51
- version: 2.6.0
75
+ version: 3.2.0
52
76
  required_rubygems_version: !ruby/object:Gem::Requirement
53
77
  requirements:
54
78
  - - ">="
@@ -56,8 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
80
  version: '0'
57
81
  requirements:
58
82
  - none
59
- rubygems_version: 3.0.3
60
- signing_key:
83
+ rubygems_version: 3.6.3
61
84
  specification_version: 4
62
85
  summary: Utility class for creating and calling service objects.
63
86
  test_files: []