assistant 0.1.0 → 1.0.0.rc1
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 +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +39 -0
- data/.github/workflows/ci.yml +99 -0
- data/.github/workflows/docs.yml +64 -0
- data/.github/workflows/release.yml +1 -1
- data/.gitignore +5 -1
- data/.opencode/.gitignore +4 -0
- data/.opencode/opencode.json +13 -0
- data/.opencode/skills/create-pr/SKILL.md +138 -0
- data/.opencode/skills/ruby-services/SKILL.md +81 -0
- data/.rubocop.yml +14 -4
- data/.yardopts +17 -0
- data/CHANGELOG.md +378 -0
- data/CONTRIBUTING.md +131 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +196 -29
- data/README.md +125 -16
- data/Rakefile +45 -0
- data/SECURITY.md +50 -0
- data/Steepfile +49 -0
- data/_config.yml +87 -0
- data/assistant.gemspec +24 -7
- data/docs/api-reference.md +264 -0
- data/docs/changelog.md +26 -0
- data/docs/deprecations.md +86 -0
- data/docs/examples/cli-handler.md +17 -0
- data/docs/examples/composing-services.md +17 -0
- data/docs/examples/execute-callbacks.md +17 -0
- data/docs/examples/index.md +29 -0
- data/docs/examples/instrumentation-notifier.md +17 -0
- data/docs/examples/rails-service.md +17 -0
- data/docs/examples/rbs-generator.md +17 -0
- data/docs/examples/sidekiq-worker.md +17 -0
- data/docs/getting-started.md +136 -0
- data/docs/guides/composing-services.md +222 -0
- data/docs/guides/index.md +25 -0
- data/docs/guides/inputs.md +333 -0
- data/docs/guides/logging-and-results.md +202 -0
- data/docs/guides/rbs-and-types.md +16 -0
- data/docs/guides/validation.md +180 -0
- data/docs/index.md +69 -0
- data/docs/roadmap.md +33 -0
- data/exe/assistant-rbs +7 -0
- data/lib/assistant/execute_callbacks.rb +103 -0
- data/lib/assistant/execute_callbacks.rbs +30 -0
- data/lib/assistant/input_builder/accessors.rb +36 -0
- data/lib/assistant/input_builder/accessors.rbs +10 -0
- data/lib/assistant/input_builder/default_option.rb +41 -0
- data/lib/assistant/input_builder/default_option.rbs +11 -0
- data/lib/assistant/input_builder/dsl.rb +37 -0
- data/lib/assistant/input_builder/dsl.rbs +12 -0
- data/lib/assistant/input_builder/optional_option.rb +45 -0
- data/lib/assistant/input_builder/optional_option.rbs +10 -0
- data/lib/assistant/input_builder/registry.rb +27 -0
- data/lib/assistant/input_builder/registry.rbs +13 -0
- data/lib/assistant/input_builder/require_validator.rb +104 -0
- data/lib/assistant/input_builder/require_validator.rbs +24 -0
- data/lib/assistant/input_builder/type_validator.rb +47 -0
- data/lib/assistant/input_builder/type_validator.rbs +18 -0
- data/lib/assistant/input_builder.rb +25 -81
- data/lib/assistant/input_builder.rbs +15 -0
- data/lib/assistant/log_item.rb +74 -16
- data/lib/assistant/log_item.rbs +40 -0
- data/lib/assistant/log_list.rb +43 -17
- data/lib/assistant/log_list.rbs +48 -0
- data/lib/assistant/rbs_generator/cli.rb +109 -0
- data/lib/assistant/rbs_generator/cli.rbs +24 -0
- data/lib/assistant/rbs_generator/renderer.rb +67 -0
- data/lib/assistant/rbs_generator/renderer.rbs +11 -0
- data/lib/assistant/rbs_generator/writer.rb +65 -0
- data/lib/assistant/rbs_generator/writer.rbs +24 -0
- data/lib/assistant/rbs_generator.rb +38 -0
- data/lib/assistant/rbs_generator.rbs +5 -0
- data/lib/assistant/refinements/string_blankness.rb +9 -13
- data/lib/assistant/refinements/string_blankness.rbs +6 -0
- data/lib/assistant/service.rb +300 -11
- data/lib/assistant/service.rbs +82 -1
- data/lib/assistant/version.rb +5 -1
- data/lib/assistant/version.rbs +5 -0
- data/lib/assistant.rb +54 -4
- data/lib/assistant.rbs +25 -0
- data/mise.toml +2 -0
- data/sig/examples/greeter.rbs +14 -0
- metadata +142 -38
- data/.fasterer.yml +0 -19
- data/.rubocop_todo.yml +0 -7
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inputs
|
|
3
|
+
parent: Guides
|
|
4
|
+
nav_order: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- markdownlint-disable MD013 MD024 -->
|
|
8
|
+
# Inputs
|
|
9
|
+
|
|
10
|
+
> **TL;DR** — Declare every input with `input :name, type: Type` at the
|
|
11
|
+
> top of your service class. Inputs are positional in the DSL but the
|
|
12
|
+
> service is constructed with keyword arguments. Use `required:`,
|
|
13
|
+
> `optional:`, `default:`, `allow_nil:`, `if:`, and array types
|
|
14
|
+
> (`type: [String, Symbol]`) to describe the shape exactly. `Steep`
|
|
15
|
+
> users get per-class RBS signatures via `bundle exec assistant-rbs`.
|
|
16
|
+
|
|
17
|
+
This guide covers every option you can pass to `input` (and the bulk
|
|
18
|
+
`inputs` helper). See [`api-reference.md`](../api-reference.md#class-methods)
|
|
19
|
+
for the canonical signatures and stability labels.
|
|
20
|
+
|
|
21
|
+
## The DSL at a glance
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class CreateUser < Assistant::Service
|
|
25
|
+
input :email, type: String, required: true
|
|
26
|
+
input :name, type: String, required: true
|
|
27
|
+
input :age, type: Integer, allow_nil: true, default: nil
|
|
28
|
+
input :role, type: Symbol, default: :member
|
|
29
|
+
inputs %i[street city], type: String, optional: true
|
|
30
|
+
|
|
31
|
+
def execute
|
|
32
|
+
# email, name, age, role, street, city are all readers here
|
|
33
|
+
{ email:, name:, age:, role:, street:, city: }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Three things to notice:
|
|
39
|
+
|
|
40
|
+
1. **`input` and `inputs` take a leading positional name** (`:email`,
|
|
41
|
+
`%i[street city]`). Every other DSL option is a keyword argument.
|
|
42
|
+
This is the only place in the gem where a positional argument
|
|
43
|
+
survives the M12 keyword-only sweep — see
|
|
44
|
+
[`api-reference.md`](../api-reference.md#class-methods).
|
|
45
|
+
2. **The constructor is keyword-only.** You call
|
|
46
|
+
`CreateUser.run(email: 'a@b.com', name: 'Alice')`, never
|
|
47
|
+
`CreateUser.run('a@b.com', 'Alice')`.
|
|
48
|
+
3. **Per-input methods are generated for you.** For every `input :name`
|
|
49
|
+
you get `#name`, `#name?`, `#valid_type_name?`, and (when
|
|
50
|
+
`required: true`) `#valid_required_name?`. See
|
|
51
|
+
[`api-reference.md`](../api-reference.md#generated-per-input-methods).
|
|
52
|
+
|
|
53
|
+
## `type:` — the only required option
|
|
54
|
+
|
|
55
|
+
Every input must declare a `type:`. The most common values are plain
|
|
56
|
+
classes:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
input :email, type: String
|
|
60
|
+
input :age, type: Integer
|
|
61
|
+
input :tags, type: Array
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A `type:` mismatch logs an error and short-circuits `#execute`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class TouchEmail < Assistant::Service
|
|
68
|
+
input :email, type: String
|
|
69
|
+
|
|
70
|
+
def execute
|
|
71
|
+
email.upcase
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
TouchEmail.run(email: 42)
|
|
76
|
+
# => { result: nil, status: :with_errors,
|
|
77
|
+
# errors: [#<LogItem detail: :email,
|
|
78
|
+
# message: "Service argument with name email is not a String but Integer">] }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Multi-type inputs (M3)
|
|
82
|
+
|
|
83
|
+
Pass an array of classes when more than one is acceptable:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class TouchIdentifier < Assistant::Service
|
|
87
|
+
input :id, type: [String, Integer]
|
|
88
|
+
|
|
89
|
+
def execute
|
|
90
|
+
id.to_s
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
TouchIdentifier.run(id: 'abc').fetch(:result) # => "abc"
|
|
95
|
+
TouchIdentifier.run(id: 42).fetch(:result) # => "42"
|
|
96
|
+
TouchIdentifier.run(id: :nope).fetch(:status) # => :with_errors
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The error message lists every accepted type.
|
|
100
|
+
|
|
101
|
+
## `required: true`
|
|
102
|
+
|
|
103
|
+
Mark an input required and the gem generates a
|
|
104
|
+
`#valid_required_<name>?` validator. Missing or whitespace-only string
|
|
105
|
+
values log an error:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
class CreateUser < Assistant::Service
|
|
109
|
+
input :email, type: String, required: true
|
|
110
|
+
|
|
111
|
+
def execute
|
|
112
|
+
{ email: }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
CreateUser.run(email: '')
|
|
117
|
+
# => { result: nil, status: :with_errors,
|
|
118
|
+
# errors: [#<LogItem detail: :email,
|
|
119
|
+
# message: "Service is missing argument with name email">] }
|
|
120
|
+
|
|
121
|
+
CreateUser.run(email: 'a@b.com')
|
|
122
|
+
# => { result: { email: "a@b.com" }, status: :ok, warnings: [] }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The deprecated 0.x name `#valid_require_<name>?` still works in 1.x —
|
|
126
|
+
calls emit a one-time `Kernel.warn` per call site and delegate to the
|
|
127
|
+
canonical predicate. See [`docs/deprecations.md`](../deprecations.md).
|
|
128
|
+
|
|
129
|
+
## `default:` (M1)
|
|
130
|
+
|
|
131
|
+
Provide a fallback when the caller omits an input. Pass a callable
|
|
132
|
+
(method, lambda, or proc) to compute the default lazily — `assistant`
|
|
133
|
+
warns if you pass a mutable literal like `[]` or `{}` that would be
|
|
134
|
+
shared across calls.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class TouchRole < Assistant::Service
|
|
138
|
+
input :role, type: Symbol, default: :member
|
|
139
|
+
|
|
140
|
+
def execute
|
|
141
|
+
role
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
TouchRole.run.fetch(:result) # => :member
|
|
146
|
+
TouchRole.run(role: :admin).fetch(:result) # => :admin
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Lazy defaults are invoked with no arguments:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
input :token, type: String, default: -> { SecureRandom.uuid }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A `default:` provider that takes arguments raises `ArgumentError` at
|
|
156
|
+
class-definition time.
|
|
157
|
+
|
|
158
|
+
## `allow_nil:` (M2)
|
|
159
|
+
|
|
160
|
+
By default, `nil` for a typed input logs a type-mismatch error.
|
|
161
|
+
`allow_nil: true` makes `nil` a legal value:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class TouchAge < Assistant::Service
|
|
165
|
+
input :age, type: Integer, allow_nil: true, default: nil
|
|
166
|
+
|
|
167
|
+
def execute
|
|
168
|
+
age
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
TouchAge.run.fetch(:result) # => nil
|
|
173
|
+
TouchAge.run(age: nil).fetch(:result) # => nil
|
|
174
|
+
TouchAge.run(age: 30).fetch(:result) # => 30
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Combine with `default:` to express "optional integer that defaults to
|
|
178
|
+
nil and may be set to nil explicitly".
|
|
179
|
+
|
|
180
|
+
## `optional: true` (M7)
|
|
181
|
+
|
|
182
|
+
`optional: true` is a shorthand for "skip the presence check entirely;
|
|
183
|
+
do not generate `#valid_required_name?`". It is mutually exclusive
|
|
184
|
+
with `required: true`:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
class TouchNickname < Assistant::Service
|
|
188
|
+
input :nickname, type: String, optional: true
|
|
189
|
+
|
|
190
|
+
def execute
|
|
191
|
+
nickname.to_s.upcase
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
TouchNickname.run.fetch(:result) # => ""
|
|
196
|
+
TouchNickname.run(nickname: 'ada').fetch(:result) # => "ADA"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If you actually want a typed-but-nullable value, prefer
|
|
200
|
+
`allow_nil: true` plus `default: nil`; reserve `optional: true` for
|
|
201
|
+
inputs whose absence simply means "don't bother".
|
|
202
|
+
|
|
203
|
+
## `if:` — conditional requirement
|
|
204
|
+
|
|
205
|
+
`if:` combined with `required: true` makes the presence check fire
|
|
206
|
+
only when the predicate returns truthy. The predicate is called with
|
|
207
|
+
the input's current value:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
class CreateUser < Assistant::Service
|
|
211
|
+
input :role, type: Symbol, default: :member
|
|
212
|
+
input :email, type: String, required: true, if: ->(_value) { caller_wants_email? }
|
|
213
|
+
|
|
214
|
+
def execute
|
|
215
|
+
{ email:, role: }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def caller_wants_email?
|
|
221
|
+
role == :admin
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
> **Predicate semantics.** Under the hood the validator requires
|
|
227
|
+
> `email` to be present **and** the predicate to return truthy. If you
|
|
228
|
+
> want the inverse — "email is allowed-but-not-required when role is
|
|
229
|
+
> admin" — combine `optional: true` with a manual `validate` check
|
|
230
|
+
> instead. See [`validation.md`](./validation.md) for the manual route.
|
|
231
|
+
|
|
232
|
+
## `inputs` — bulk declaration
|
|
233
|
+
|
|
234
|
+
`inputs` takes a list of names and applies the same `type:` /
|
|
235
|
+
`options` to all of them. Use it when several inputs share a shape:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class ShipAddress < Assistant::Service
|
|
239
|
+
inputs %i[street city zip], type: String, required: true
|
|
240
|
+
|
|
241
|
+
def execute
|
|
242
|
+
"#{street}, #{city} #{zip}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
This is exactly equivalent to writing three `input` calls.
|
|
248
|
+
|
|
249
|
+
## Reading inputs back: `#input_snapshot`
|
|
250
|
+
|
|
251
|
+
`#input_snapshot` returns a frozen `Data` instance carrying the
|
|
252
|
+
post-default, post-`allow_nil` values. It's useful for forwarding the
|
|
253
|
+
inputs of one service into another, for instrumentation, or for tests
|
|
254
|
+
that want a structural snapshot:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
class CreateUser < Assistant::Service
|
|
258
|
+
input :email, type: String, required: true
|
|
259
|
+
input :role, type: Symbol, default: :member
|
|
260
|
+
|
|
261
|
+
def execute
|
|
262
|
+
[input_snapshot.email, input_snapshot.role]
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
CreateUser.run(email: 'a@b.com').fetch(:result)
|
|
267
|
+
# => ["a@b.com", :member]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
See [`composing-services.md`](./composing-services.md) for a worked
|
|
271
|
+
example that snapshots the outer service's inputs into an inner one.
|
|
272
|
+
|
|
273
|
+
## Using `assistant-rbs` for Steep users
|
|
274
|
+
|
|
275
|
+
`Assistant::Service` is metaprogramming-heavy: per-input methods are
|
|
276
|
+
generated at class-definition time by `Service.input`, which means a
|
|
277
|
+
generic `.rbs` for `Service` can't know that your `CreateUser#email`
|
|
278
|
+
returns `String`. That's R1 in
|
|
279
|
+
[`docs/v1/05-quality-and-tooling.md`](https://github.com/ramongr/assistant/blob/main/docs/v1/05-quality-and-tooling.md).
|
|
280
|
+
|
|
281
|
+
The bundled `assistant-rbs` CLI (M11) closes the gap by emitting
|
|
282
|
+
per-class `.rbs` files. Run it once after editing your services:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
bundle exec assistant-rbs lib --output sig
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
For the `CreateUser` example above it writes
|
|
289
|
+
`sig/CreateUser.rbs` with:
|
|
290
|
+
|
|
291
|
+
```rbs
|
|
292
|
+
class CreateUser < Assistant::Service
|
|
293
|
+
def email: () -> String
|
|
294
|
+
def email?: () -> bool
|
|
295
|
+
def role: () -> Symbol
|
|
296
|
+
def role?: () -> bool
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Multi-type inputs produce union types
|
|
301
|
+
(`String | Integer`), and `allow_nil: true` produces nullable types
|
|
302
|
+
(`String?`). The generator is idempotent — re-running with no input
|
|
303
|
+
changes is a no-op.
|
|
304
|
+
|
|
305
|
+
The CLI is labelled **Experimental** for 1.0 because its output
|
|
306
|
+
format may evolve in 1.x; see
|
|
307
|
+
[`api-reference.md`](../api-reference.md#assistant-rbs-cli) for the
|
|
308
|
+
stability label.
|
|
309
|
+
|
|
310
|
+
## Common pitfalls
|
|
311
|
+
|
|
312
|
+
- **Passing a positional name to the constructor.** `Service.new('a')`
|
|
313
|
+
always raises. Call `Service.new(email: 'a')`, or just use
|
|
314
|
+
`Service.run(email: 'a')`.
|
|
315
|
+
- **Sharing a mutable default literal.** `default: []` would share
|
|
316
|
+
one array across calls; the gem warns and recommends a lambda
|
|
317
|
+
(`default: -> { [] }`).
|
|
318
|
+
- **Mixing `required: true` with `optional: true`.** They contradict
|
|
319
|
+
each other; the gem raises at class-definition time.
|
|
320
|
+
- **Expecting `if:` to inhibit presence.** The validator requires
|
|
321
|
+
presence *and* the predicate. Use `optional: true` plus a `validate`
|
|
322
|
+
hook when you need the inverse.
|
|
323
|
+
|
|
324
|
+
## See also
|
|
325
|
+
|
|
326
|
+
- [Validation guide](./validation.md) — `validate` hook, when to log a
|
|
327
|
+
warning vs. error.
|
|
328
|
+
- [Logging and results](./logging-and-results.md) — `LogItem`,
|
|
329
|
+
`log_item_*` shorthands, the result hash.
|
|
330
|
+
- [Composing services](./composing-services.md) — `call_service`,
|
|
331
|
+
callbacks, `#input_snapshot` between services.
|
|
332
|
+
- [API reference: class methods](../api-reference.md#class-methods).
|
|
333
|
+
- [API reference: generated per-input methods](../api-reference.md#generated-per-input-methods).
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Logging and results
|
|
3
|
+
parent: Guides
|
|
4
|
+
nav_order: 3
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- markdownlint-disable MD013 MD024 -->
|
|
8
|
+
# Logging and results
|
|
9
|
+
|
|
10
|
+
> **TL;DR** — Every service maintains a `#logs` timeline of
|
|
11
|
+
> `Assistant::LogItem`s. Use `log_item_info / _warning / _error` to
|
|
12
|
+
> add entries, `#logs` / `#infos` / `#warnings` / `#errors` to read
|
|
13
|
+
> them, and the result hash returned by `.run` to consume the
|
|
14
|
+
> service from outside. `LogItem.new` raises `ArgumentError` for
|
|
15
|
+
> invalid attributes (M10) — prefer the helpers.
|
|
16
|
+
|
|
17
|
+
This guide covers the data model, the writer helpers, the reader
|
|
18
|
+
predicates, and the shape of the result hash. See the
|
|
19
|
+
[Validation guide](./validation.md) for *when* to log a warning vs.
|
|
20
|
+
an error.
|
|
21
|
+
|
|
22
|
+
## `Assistant::LogItem` at a glance
|
|
23
|
+
|
|
24
|
+
Every entry on `#logs` is an `Assistant::LogItem` with the following
|
|
25
|
+
fields:
|
|
26
|
+
|
|
27
|
+
| Field | Type | Notes |
|
|
28
|
+
|------------|--------------------------------|----------------------------------------------------------------|
|
|
29
|
+
| `level` | `Symbol` | One of `:info`, `:warning`, `:error`. |
|
|
30
|
+
| `source` | `Symbol` | High-level subsystem (`:initialize`, `:execute`, `:hook`, ...).|
|
|
31
|
+
| `detail` | `Symbol` | Finer-grained tag; usually an input attribute name. |
|
|
32
|
+
| `message` | `String` | Human-readable text. |
|
|
33
|
+
| `trace` | `Array<String>` or `nil` | Optional backtrace captured at construction. |
|
|
34
|
+
|
|
35
|
+
Constraints (enforced strictly in 1.0 — M10):
|
|
36
|
+
|
|
37
|
+
- `source != detail`.
|
|
38
|
+
- `source` and `detail` must each be non-empty.
|
|
39
|
+
- `message` must contain at least one non-whitespace character.
|
|
40
|
+
- `level` must be one of `Assistant::LogItem::VALID_LEVELS`.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
Assistant::LogItem::VALID_LEVELS
|
|
44
|
+
# => [:info, :warning, :error]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Writing log entries
|
|
48
|
+
|
|
49
|
+
The three shorthand helpers (M5) are the recommended call sites
|
|
50
|
+
inside `#validate` and `#execute`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class CreateUser < Assistant::Service
|
|
54
|
+
input :email, type: String, required: true
|
|
55
|
+
input :age, type: Integer, allow_nil: true, default: nil
|
|
56
|
+
|
|
57
|
+
def validate
|
|
58
|
+
return if email.include?('@')
|
|
59
|
+
|
|
60
|
+
log_item_error(source: :validate, detail: :email, message: 'invalid email')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def execute
|
|
64
|
+
log_item_info(source: :execute, detail: :age, message: "age=#{age.inspect}")
|
|
65
|
+
log_item_warning(source: :execute, detail: :age, message: 'age missing') if age.nil?
|
|
66
|
+
|
|
67
|
+
{ id: 42, email:, age: }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`add_log(level:, source:, detail:, message:, trace: nil)` is the
|
|
73
|
+
generic form when you need to set the level dynamically:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
level = problem.severe? ? :error : :warning
|
|
77
|
+
add_log(level:, source: :execute, detail: :payment, message: problem.to_s)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`#log_item_error_initialize(attr_name:, message:)` is used internally
|
|
81
|
+
by the generated `valid_required_*?` / `valid_type_*?` validators to
|
|
82
|
+
record per-input errors. Service code can call it directly when an
|
|
83
|
+
ad-hoc validation needs the same `:initialize` source as the
|
|
84
|
+
declarative checks.
|
|
85
|
+
|
|
86
|
+
## Reading log entries
|
|
87
|
+
|
|
88
|
+
A service exposes three readers, one per level, plus the full
|
|
89
|
+
timeline:
|
|
90
|
+
|
|
91
|
+
| Method | Returns |
|
|
92
|
+
|---------------|--------------------------------------|
|
|
93
|
+
| `#logs` | `Array<LogItem>` — every entry, in insertion order. |
|
|
94
|
+
| `#infos` | `Array<LogItem>` — entries with `level == :info`. |
|
|
95
|
+
| `#warnings` | `Array<LogItem>` — entries with `level == :warning`.|
|
|
96
|
+
| `#errors` | `Array<LogItem>` — entries with `level == :error`. |
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
service = CreateUser.new(email: 'a@b.com')
|
|
100
|
+
service.run
|
|
101
|
+
|
|
102
|
+
service.logs.size # => however many entries
|
|
103
|
+
service.infos.first.message
|
|
104
|
+
service.warnings.any?
|
|
105
|
+
service.errors.empty?
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`#status` is derived from `#errors` and `#warnings`:
|
|
109
|
+
|
|
110
|
+
- `:with_errors` if `#errors.any?`.
|
|
111
|
+
- `:with_warnings` if `#warnings.any?` and no errors.
|
|
112
|
+
- `:ok` otherwise.
|
|
113
|
+
|
|
114
|
+
`#success?` is `true` for `:ok` and `:with_warnings`; `#failure?` is
|
|
115
|
+
`true` only for `:with_errors`.
|
|
116
|
+
|
|
117
|
+
## The result hash
|
|
118
|
+
|
|
119
|
+
`Service.run` (and `Service#run`) returns one of two shapes:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# Success — status is :ok or :with_warnings
|
|
123
|
+
{ result: <Object>, status: :ok | :with_warnings, warnings: Array<LogItem> }
|
|
124
|
+
|
|
125
|
+
# Failure — :with_errors
|
|
126
|
+
{ result: nil, status: :with_errors, errors: Array<LogItem> }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The success shape always includes `:warnings` (possibly empty); the
|
|
130
|
+
failure shape always includes `:errors` (always non-empty) and
|
|
131
|
+
`result: nil`. Pattern-matching is the cleanest way to consume it:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
case CreateUser.run(email: 'a@b.com')
|
|
135
|
+
in { result:, status: :ok }
|
|
136
|
+
result
|
|
137
|
+
in { result:, status: :with_warnings, warnings: }
|
|
138
|
+
WarningsLogger.log(warnings)
|
|
139
|
+
result
|
|
140
|
+
in { errors:, status: :with_errors }
|
|
141
|
+
raise Errors::InvalidRequest, errors.map(&:message).join(', ')
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`#infos` are intentionally **not** part of the result hash. They live
|
|
146
|
+
on the service instance for inspection (and for tests), but the
|
|
147
|
+
public contract is the warnings/errors split.
|
|
148
|
+
|
|
149
|
+
## Merging logs across services
|
|
150
|
+
|
|
151
|
+
`#merge_logs(logs:)` concatenates another timeline onto the current
|
|
152
|
+
service's `#logs`. It's mostly used by `#call_service` (see
|
|
153
|
+
[`composing-services.md`](./composing-services.md)), but you can call
|
|
154
|
+
it directly when you need to forward log items from a non-`Service`
|
|
155
|
+
collaborator:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
def execute
|
|
159
|
+
outcome = MyLibrary.do_thing
|
|
160
|
+
merge_logs(logs: outcome.log_items.map { |item| Assistant::LogItem.new(**item) })
|
|
161
|
+
outcome.value
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
> **M12.** `#merge_logs` is keyword-only in 1.0. Passing positional
|
|
166
|
+
> arguments raises `ArgumentError`. The
|
|
167
|
+
> [migration guide](https://github.com/ramongr/assistant/blob/main/docs/v1/06-migration-0x-to-1.md) covers the
|
|
168
|
+
> mechanical rewrite.
|
|
169
|
+
|
|
170
|
+
## Inspecting an entry
|
|
171
|
+
|
|
172
|
+
Every `LogItem` has a `#item` method that returns a `Hash` view —
|
|
173
|
+
handy for JSON serialization or test assertions:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
service.errors.first.item
|
|
177
|
+
# => { level: :error, source: :validate, detail: :email,
|
|
178
|
+
# message: "invalid email", trace: nil }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Common pitfalls
|
|
182
|
+
|
|
183
|
+
- **Pushing onto `@logs` directly.** Don't — always go through the
|
|
184
|
+
helpers so the M10 strict construction runs and so future
|
|
185
|
+
middleware (e.g. an instrumentation hook around `#add_log`) can
|
|
186
|
+
see the entry.
|
|
187
|
+
- **Using `LogItem.new` with `source == detail`.** Raises
|
|
188
|
+
`ArgumentError`. Pick distinct symbols.
|
|
189
|
+
- **Treating `#infos` as part of the contract.** They're for
|
|
190
|
+
introspection only; the result hash never includes them.
|
|
191
|
+
- **Calling `merge_logs(other.logs)` (positional).** M12 requires
|
|
192
|
+
the keyword form: `merge_logs(logs: other.logs)`.
|
|
193
|
+
|
|
194
|
+
## See also
|
|
195
|
+
|
|
196
|
+
- [Validation guide](./validation.md) — choosing warning vs. error,
|
|
197
|
+
conditional checks, `#validate` mechanics.
|
|
198
|
+
- [Composing services](./composing-services.md) — how `#call_service`
|
|
199
|
+
merges inner logs into the outer timeline.
|
|
200
|
+
- [API reference: LogItem](../api-reference.md#assistantlogitem).
|
|
201
|
+
- [API reference: LogList](../api-reference.md#assistantloglist).
|
|
202
|
+
- [Migration guide](https://github.com/ramongr/assistant/blob/main/docs/v1/06-migration-0x-to-1.md) for M10 + M12.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: RBS and types
|
|
3
|
+
parent: Guides
|
|
4
|
+
nav_order: 5
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# RBS and types
|
|
8
|
+
|
|
9
|
+
> **Status:** placeholder — full content lands in
|
|
10
|
+
> [P4](https://github.com/ramongr/assistant/blob/main/docs/v1/08-github-pages.md) of the GitHub Pages plan. For now,
|
|
11
|
+
> see the [RBS subsection in the Inputs guide](inputs.md#using-assistant-rbs-for-steep-users)
|
|
12
|
+
> and the [Recipe in the 0.x → 1.x migration guide](https://github.com/ramongr/assistant/blob/main/docs/v1/06-migration-0x-to-1.md#recipe-binassistant-rbs-for-steep-users).
|
|
13
|
+
|
|
14
|
+
This page will cover the `bin/assistant-rbs` per-class generator (M11),
|
|
15
|
+
the R1 metaprogramming limitation that motivates it, and how to wire
|
|
16
|
+
the generated `.rbs` files into a Steep-checked project.
|