telephone 1.0.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: b95df7745edfdcccffb9d141f444fb2b487b55d0cd20ae6e1dd86a51033f064a
4
- data.tar.gz: 3d680e1fecccb620ab8823e0e46cba6b9fa506f1718b5486e38750ccf4ab86ab
3
+ metadata.gz: 45fbcb9aaecf2dc2d66072e93b97b168c2a505f6d4e0cc1ed08cfad4e8267bb2
4
+ data.tar.gz: 4f3992fbc6840c3d624fadb7de3faa56f3952ba4b4b5eae7cde0b61ecdf343f6
5
5
  SHA512:
6
- metadata.gz: dd79447632f18c1ac6237254589df28980098b1988172886b10c3a46e2e2ea6bf6122d81219653111a51dbfc75edefea2b79450905c099875bb48ab5e6fcdd1b
7
- data.tar.gz: d40dd3b806a554920571c40841ccc539a1531eef7cde07800a171cfc9b5cbcf4a2f98c140fa702f22bbc1e3d2e68077c55cd21c45eafbf2af464dfdc9a2431cb
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}"
@@ -20,10 +20,26 @@ module Telephone
20
20
  # Primary responsibility of initialize is to instantiate the
21
21
  # attributes of the service object with the expected values.
22
22
  def initialize(attributes = {})
23
- self.class.defaults.merge(attributes).each do |key, value|
23
+ attributes = attributes.transform_keys(&:to_sym)
24
+
25
+ attributes.each do |key, value|
24
26
  send("#{key}=", value)
25
27
  end
26
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
+
27
43
  super
28
44
  yield self if block_given?
29
45
  end
@@ -51,21 +67,44 @@ module Telephone
51
67
  # to pass in a default, or set the argument to "required" to add a validation
52
68
  # that runs before executing the block.
53
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
+ #
54
85
  # @example
55
86
  # class SomeService < Telephone::Service
56
- # argument :foo, default: "bar"
57
- # 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
58
92
  #
59
93
  # def call
60
- # puts foo
61
- # puts baz
94
+ # puts full_name
95
+ # puts timestamp
62
96
  # end
63
97
  # end
64
- def argument(arg, default: nil, required: false)
65
- send(:attr_accessor, arg.to_sym)
66
- 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
67
106
 
68
- defaults[arg.to_sym] = default
107
+ defaults[arg] = default
69
108
  end
70
109
 
71
110
  ##
@@ -82,8 +121,9 @@ module Telephone
82
121
  #
83
122
  # @example
84
123
  # Telephone::Service.call(foo: bar)
85
- def call(**args)
86
- new(args).call
124
+ # Telephone::Service.call({ foo: bar })
125
+ def call(*args, **kwargs)
126
+ new(args.extract_options!.merge(kwargs)).call
87
127
  end
88
128
 
89
129
  ##
@@ -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 = "1.0.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: 1.0.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: 2022-08-05 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.1.6
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: []