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
data/lib/assistant/service.rb
CHANGED
|
@@ -1,61 +1,350 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'execute_callbacks'
|
|
3
4
|
require_relative 'input_builder'
|
|
4
5
|
require_relative 'log_list'
|
|
5
6
|
|
|
6
7
|
module Assistant
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Core Service base class. Subclasses declare inputs via the
|
|
9
|
+
# InputBuilder DSL, optionally register before/after/around execute
|
|
10
|
+
# hooks via ExecuteCallbacks, and implement `#execute`.
|
|
11
|
+
#
|
|
12
|
+
# @example Minimal service
|
|
13
|
+
# class Greet < Assistant::Service
|
|
14
|
+
# input :name, type: String, required: true
|
|
15
|
+
#
|
|
16
|
+
# def execute
|
|
17
|
+
# "Hello, #{name}!"
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Greet.run(name: 'Ada')
|
|
22
|
+
# # => { result: "Hello, Ada!", status: :ok, warnings: [] }
|
|
23
|
+
class Service # rubocop:disable Metrics/ClassLength
|
|
24
|
+
include Assistant::LogList
|
|
25
|
+
|
|
26
|
+
# Public reader for the full log timeline (info + warning + error), in
|
|
27
|
+
# insertion order. See docs/v1/02-features.md M4.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<Assistant::LogItem>]
|
|
30
|
+
attr_reader :logs
|
|
10
31
|
|
|
11
32
|
class << self
|
|
12
|
-
include InputBuilder
|
|
33
|
+
include Assistant::InputBuilder
|
|
34
|
+
include Assistant::ExecuteCallbacks
|
|
13
35
|
|
|
36
|
+
# Convenience: build a service with the given keyword arguments
|
|
37
|
+
# and immediately invoke `#run`, returning the result hash.
|
|
38
|
+
# Equivalent to `new(**inputs).run`.
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash] the result payload — see {Service#run}
|
|
14
41
|
def run(**)
|
|
15
42
|
new(**).run
|
|
16
43
|
end
|
|
44
|
+
|
|
45
|
+
# M-S4: per-subclass `Data` class whose members are the declared
|
|
46
|
+
# input names, in declaration order. Memoised on the subclass and
|
|
47
|
+
# transparently rebuilt if `input_definitions` changes (e.g. a
|
|
48
|
+
# late `input :foo` after the first snapshot call). Used by
|
|
49
|
+
# `Service#input_snapshot`; users normally never touch it.
|
|
50
|
+
#
|
|
51
|
+
# @return [Class] a `Data.define` subclass
|
|
52
|
+
def input_snapshot_class
|
|
53
|
+
keys = input_definitions.keys
|
|
54
|
+
return @input_snapshot_class if @input_snapshot_class && @input_snapshot_class_keys == keys
|
|
55
|
+
|
|
56
|
+
@input_snapshot_class_keys = keys
|
|
57
|
+
@input_snapshot_class = Data.define(*keys)
|
|
58
|
+
end
|
|
17
59
|
end
|
|
18
60
|
|
|
61
|
+
# @param args [Hash] keyword arguments matching the declared inputs.
|
|
62
|
+
# Unknown keys are accepted but excluded from {#input_snapshot}.
|
|
19
63
|
def initialize(**args)
|
|
20
64
|
@inputs = args
|
|
65
|
+
apply_input_defaults
|
|
21
66
|
@logs = []
|
|
22
67
|
end
|
|
23
68
|
|
|
69
|
+
# Execute the validation + execute pipeline once and return the
|
|
70
|
+
# result payload. Idempotent: calling `#run` a second time returns
|
|
71
|
+
# an updated payload based on the memoised `#result`, but `#execute`
|
|
72
|
+
# itself runs only once (M-S1 hook chain is gated by {#result}'s
|
|
73
|
+
# `||=`).
|
|
74
|
+
#
|
|
75
|
+
# @return [Hash{Symbol => Object}] either
|
|
76
|
+
# - `{ result: Object, status: :ok | :with_warnings, warnings: Array<LogItem> }`
|
|
77
|
+
# on success, or
|
|
78
|
+
# - `{ errors: Array<LogItem>, result: nil, status: :with_errors }`
|
|
79
|
+
# when any error has been logged before or during validation.
|
|
24
80
|
def run
|
|
81
|
+
@run_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
82
|
+
notify(:service_started)
|
|
83
|
+
|
|
25
84
|
validate_inputs
|
|
26
85
|
validate
|
|
86
|
+
notify(:service_validated)
|
|
27
87
|
|
|
28
|
-
if errors.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
88
|
+
return failed_payload if errors.any?
|
|
89
|
+
|
|
90
|
+
# Trigger `#execute` (through the M-S1 hook chain) eagerly so any
|
|
91
|
+
# error logged by a `before_/after_/around_execute` hook influences
|
|
92
|
+
# the terminal event and the payload's `:status` field.
|
|
93
|
+
result
|
|
94
|
+
errors.empty? ? executed_payload : failed_payload
|
|
33
95
|
end
|
|
34
96
|
|
|
97
|
+
# Memoised return value of {#execute}, threaded through the
|
|
98
|
+
# registered before/around/after execute hooks (M-S1).
|
|
99
|
+
#
|
|
100
|
+
# @return [Object] whatever the subclass's `#execute` returned
|
|
35
101
|
def result
|
|
36
|
-
@result ||=
|
|
102
|
+
@result ||= run_execute_with_callbacks
|
|
37
103
|
end
|
|
38
104
|
|
|
105
|
+
# @return [Boolean] true when no `:error` entries have been logged
|
|
39
106
|
def success?
|
|
40
107
|
errors.empty?
|
|
41
108
|
end
|
|
42
109
|
|
|
110
|
+
# @return [Boolean] true when at least one `:error` entry has been logged
|
|
43
111
|
def failure?
|
|
44
112
|
errors.any?
|
|
45
113
|
end
|
|
46
114
|
|
|
115
|
+
# Terminal status for the success path. Failure runs use
|
|
116
|
+
# `:with_errors` directly in the result payload (see {#run}).
|
|
117
|
+
#
|
|
118
|
+
# @return [Symbol] `:ok` when there are no warnings, otherwise `:with_warnings`
|
|
47
119
|
def status
|
|
48
120
|
warnings.empty? ? :ok : :with_warnings
|
|
49
121
|
end
|
|
50
122
|
|
|
123
|
+
# M-S2: instantiate `klass`, run it, merge its log timeline into the
|
|
124
|
+
# current service, and return the inner service instance.
|
|
125
|
+
#
|
|
126
|
+
# The full log timeline of the inner service (info + warning +
|
|
127
|
+
# error) is concatenated onto the outer service's `@logs` via
|
|
128
|
+
# `merge_logs`. Because the outer service's `errors`, `warnings`,
|
|
129
|
+
# and `status` are derived by filtering `@logs`, an inner error
|
|
130
|
+
# automatically downgrades the outer terminal status to
|
|
131
|
+
# `:with_errors`, and inner warnings surface as `:with_warnings`
|
|
132
|
+
# when no errors are present — without any special handling in
|
|
133
|
+
# the caller.
|
|
134
|
+
#
|
|
135
|
+
# The returned inner instance exposes `#result`, `#success?`,
|
|
136
|
+
# `#failure?`, etc. so the caller can branch on the inner outcome:
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# def execute
|
|
140
|
+
# other = call_service(OtherService, foo: 1)
|
|
141
|
+
# return if failure?
|
|
142
|
+
# other.result + 1
|
|
143
|
+
# end
|
|
144
|
+
#
|
|
145
|
+
# `call_service` does **not** rescue exceptions raised by the inner
|
|
146
|
+
# service's `#execute` or by `Assistant.notifier`; those propagate
|
|
147
|
+
# to the caller, matching the base `Service#run` contract. To run
|
|
148
|
+
# an inner service that may raise, wrap the call in a `begin/rescue`
|
|
149
|
+
# and use `add_log(level: :error, …)` to record the failure.
|
|
150
|
+
#
|
|
151
|
+
# @param klass [Class<Assistant::Service>] the inner service class
|
|
152
|
+
# @param inputs [Hash] keyword arguments forwarded to `klass.new`
|
|
153
|
+
# @return [Assistant::Service] the inner service instance, already run
|
|
154
|
+
# @raise [ArgumentError] if `klass` is not a subclass of {Assistant::Service}
|
|
155
|
+
def call_service(klass, **inputs)
|
|
156
|
+
unless klass.is_a?(Class) && klass <= Assistant::Service
|
|
157
|
+
raise ArgumentError, "call_service expects an Assistant::Service subclass, got #{klass.inspect}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
inner = klass.new(**inputs)
|
|
161
|
+
inner.run
|
|
162
|
+
merge_logs(logs: inner.logs)
|
|
163
|
+
inner
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# M-S4: a read-only `Data` view over the declared inputs of this
|
|
167
|
+
# service, post-`default:` / post-`allow_nil:`. Members are the
|
|
168
|
+
# input names declared via `Service.input` / `Service.inputs`, in
|
|
169
|
+
# declaration order; values are read from `@inputs` after
|
|
170
|
+
# `apply_input_defaults` has run, so callers see the same values
|
|
171
|
+
# the per-input getters expose.
|
|
172
|
+
#
|
|
173
|
+
# @example
|
|
174
|
+
# class Greet < Assistant::Service
|
|
175
|
+
# input :name, type: String, required: true
|
|
176
|
+
# input :loud, type: TrueClass, default: false
|
|
177
|
+
# end
|
|
178
|
+
#
|
|
179
|
+
# Greet.new(name: 'Ada').input_snapshot
|
|
180
|
+
# # => #<data name="Ada", loud=false>
|
|
181
|
+
#
|
|
182
|
+
# The returned object is a `Data` instance, so it is structurally
|
|
183
|
+
# immutable: no member can be reassigned. Member values that are
|
|
184
|
+
# themselves mutable (e.g. an `Array` passed as an input) keep
|
|
185
|
+
# their normal mutability — the snapshot does not deep-freeze.
|
|
186
|
+
#
|
|
187
|
+
# Only declared inputs appear in the snapshot. Extra keyword
|
|
188
|
+
# arguments accepted by `#initialize` (which live in `@inputs`
|
|
189
|
+
# but have no `input :foo` declaration) are intentionally excluded
|
|
190
|
+
# so the snapshot's shape matches the public DSL.
|
|
191
|
+
#
|
|
192
|
+
# A declared input with no default and no caller-supplied value
|
|
193
|
+
# appears with a `nil` member value, mirroring the behaviour of
|
|
194
|
+
# the per-input getter.
|
|
195
|
+
#
|
|
196
|
+
# @return [Data] read-only view over `input_definitions.keys`
|
|
197
|
+
def input_snapshot
|
|
198
|
+
keys = self.class.input_definitions.keys
|
|
199
|
+
self.class.input_snapshot_class.new(**keys.to_h { |name| [name, @inputs[name]] })
|
|
200
|
+
end
|
|
201
|
+
|
|
51
202
|
private
|
|
52
203
|
|
|
204
|
+
# Build the success-path result hash and fire the terminal
|
|
205
|
+
# `:service_executed` event. Triggers `#execute` lazily via `#result`.
|
|
206
|
+
def executed_payload
|
|
207
|
+
payload = { result:, status:, warnings: }
|
|
208
|
+
notify(:service_executed)
|
|
209
|
+
payload
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Build the failure-path result hash and fire the terminal
|
|
213
|
+
# `:service_failed` event. Does not invoke `#execute`.
|
|
214
|
+
def failed_payload
|
|
215
|
+
payload = { errors:, result: nil, status: :with_errors }
|
|
216
|
+
notify(:service_failed)
|
|
217
|
+
payload
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# M1: apply input defaults declared via `input :name, default: ...`.
|
|
221
|
+
# A default fires when the key is absent, or when the value is an
|
|
222
|
+
# explicit `nil` and the input is not `allow_nil: true` (M2). Procs
|
|
223
|
+
# are invoked with no arguments (zero-arity enforced at
|
|
224
|
+
# class-definition time); literals are used as-is. Defaulted values
|
|
225
|
+
# are subject to the same type / required / if validation as
|
|
226
|
+
# caller-supplied values.
|
|
227
|
+
def apply_input_defaults
|
|
228
|
+
input_definitions_needing_default.each do |attr_name, options|
|
|
229
|
+
provider = options[:default]
|
|
230
|
+
@inputs[attr_name] = provider.is_a?(Proc) ? provider.call : provider
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Input definitions that declare a `:default` and whose key was not
|
|
235
|
+
# already supplied by the caller (with `allow_nil:` honoured).
|
|
236
|
+
def input_definitions_needing_default
|
|
237
|
+
self.class.input_definitions.select do |attr_name, options|
|
|
238
|
+
options.key?(:default) && !input_supplied?(attr_name, options)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# An explicit nil counts as "not supplied" so the default fires,
|
|
243
|
+
# unless the input opted into `allow_nil: true` — in which case the
|
|
244
|
+
# caller's nil is honoured and the default is skipped.
|
|
245
|
+
def input_supplied?(attr_name, options)
|
|
246
|
+
@inputs.key?(attr_name) && (options[:allow_nil] == true || !@inputs[attr_name].nil?)
|
|
247
|
+
end
|
|
248
|
+
|
|
53
249
|
def validate_inputs
|
|
54
|
-
|
|
250
|
+
# M9: regex matches only the canonical `valid_required_*?` /
|
|
251
|
+
# `valid_required_conditional_*?` / `valid_type_*?` predicates so
|
|
252
|
+
# the deprecated `valid_require_*?` aliases are not auto-invoked
|
|
253
|
+
# (which would emit the M9 deprecation warning from inside the
|
|
254
|
+
# framework itself).
|
|
255
|
+
methods.grep(/valid_(required|type)_\w+\?$/).each do |validation_method|
|
|
55
256
|
send(validation_method)
|
|
56
257
|
end
|
|
57
258
|
end
|
|
58
259
|
|
|
260
|
+
# M-S3: dispatch a frozen-set instrumentation event to the configured
|
|
261
|
+
# `Assistant.notifier`. Payload always includes `:service_class` and
|
|
262
|
+
# `:duration_s` (Float seconds since the start of `#run`). The notifier
|
|
263
|
+
# is treated as untrusted infra: any `StandardError` it raises is
|
|
264
|
+
# caught and surfaced via `Kernel.warn` so a misconfigured notifier
|
|
265
|
+
# cannot tear down every service in the process. Non-`StandardError`
|
|
266
|
+
# exceptions (e.g. `SystemExit`, `Interrupt`) are intentionally
|
|
267
|
+
# allowed to propagate.
|
|
268
|
+
def notify(event)
|
|
269
|
+
duration_s = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @run_started_at
|
|
270
|
+
Assistant.notifier.call(event, { service_class: self.class, duration_s: duration_s })
|
|
271
|
+
rescue StandardError => e
|
|
272
|
+
Kernel.warn "assistant: notifier raised during #{event} for #{self.class}: #{e.class}: #{e.message}"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# M-S1: thread `#execute` through the registered before/around/after
|
|
276
|
+
# hook chains. Called once via `#result`'s memoization, so each
|
|
277
|
+
# service instance runs hooks at most once even if `#result` is
|
|
278
|
+
# invoked repeatedly.
|
|
279
|
+
def run_execute_with_callbacks
|
|
280
|
+
run_before_execute_hooks
|
|
281
|
+
result = run_around_execute_chain
|
|
282
|
+
run_after_execute_hooks(result)
|
|
283
|
+
result
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# M-S1: invoke every registered `before_execute` hook in declaration
|
|
287
|
+
# order. Each hook is independent — a `StandardError` from one is
|
|
288
|
+
# logged via `add_log(level: :error, source: :hook, …)` and the
|
|
289
|
+
# remaining hooks still fire.
|
|
290
|
+
def run_before_execute_hooks
|
|
291
|
+
self.class.before_execute_hooks.each do |hook|
|
|
292
|
+
hook.bind_call(self)
|
|
293
|
+
rescue StandardError => e
|
|
294
|
+
log_hook_error(:before_execute, e)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# M-S1: invoke every registered `after_execute` hook in declaration
|
|
299
|
+
# order, passing the execute result as the single positional arg.
|
|
300
|
+
def run_after_execute_hooks(execute_result)
|
|
301
|
+
self.class.after_execute_hooks.each do |hook|
|
|
302
|
+
hook.bind_call(self, execute_result)
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
log_hook_error(:after_execute, e)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# M-S1: build the around-hook chain from the innermost layer (the
|
|
309
|
+
# raw `#execute` call) outwards. Declaration order wraps: the
|
|
310
|
+
# first-declared hook is the outermost layer. If an around hook
|
|
311
|
+
# raises before yielding to its continuation, that layer (and any
|
|
312
|
+
# inner layers, including `#execute` itself) does not run; the
|
|
313
|
+
# layer returns `nil` and outer hooks still wrap normally.
|
|
314
|
+
def run_around_execute_chain
|
|
315
|
+
chain = -> { execute }
|
|
316
|
+
self.class.around_execute_hooks.reverse_each do |hook|
|
|
317
|
+
chain = wrap_around_layer(hook, chain)
|
|
318
|
+
end
|
|
319
|
+
chain.call
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Build a single around layer: a lambda that runs `hook` with
|
|
323
|
+
# `inner` as its continuation. Hook exceptions raised before the
|
|
324
|
+
# continuation runs are caught, logged, and the layer returns nil.
|
|
325
|
+
def wrap_around_layer(hook, inner)
|
|
326
|
+
lambda do
|
|
327
|
+
hook.bind_call(self, &inner)
|
|
328
|
+
rescue StandardError => e
|
|
329
|
+
log_hook_error(:around_execute, e)
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# M-S1: shared hook-error logger. Stamps `source: :hook` and uses
|
|
335
|
+
# the hook type as `detail:` so callers can grep their log timeline
|
|
336
|
+
# by either field. The exception's backtrace is preserved on the
|
|
337
|
+
# `LogItem#trace` field for downstream diagnostics.
|
|
338
|
+
def log_hook_error(hook_type, exception)
|
|
339
|
+
add_log(
|
|
340
|
+
level: :error,
|
|
341
|
+
source: :hook,
|
|
342
|
+
detail: hook_type,
|
|
343
|
+
message: "#{exception.class}: #{exception.message}",
|
|
344
|
+
trace: exception.backtrace
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
|
|
59
348
|
attr_reader :inputs
|
|
60
349
|
|
|
61
350
|
def execute; end
|
data/lib/assistant/service.rbs
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Type signatures for `lib/assistant/service.rb`. See
|
|
2
|
+
# docs/v1/01-api-surface.md for the frozen 1.0 surface.
|
|
3
|
+
#
|
|
4
|
+
# NOTE: per-input methods generated by `Service.input :name, type: T`
|
|
5
|
+
# (`#name`, `#name?`, `#valid_type_name?`, `#valid_required_name?`,
|
|
6
|
+
# `#valid_required_conditional_name?`, and the deprecated
|
|
7
|
+
# `#valid_require_name?` / `#valid_require_conditional_name?` aliases)
|
|
8
|
+
# are *not* declared here because the names are only known at runtime.
|
|
9
|
+
# Users can run `bin/assistant-rbs` (M11, ships with 1.0) to generate
|
|
10
|
+
# per-class signatures for their own `Assistant::Service` subclasses.
|
|
2
11
|
|
|
3
12
|
class Assistant::Service
|
|
13
|
+
include Assistant::LogList
|
|
4
14
|
|
|
15
|
+
extend Assistant::InputBuilder
|
|
16
|
+
extend Assistant::ExecuteCallbacks
|
|
17
|
+
|
|
18
|
+
@inputs: Hash[Symbol, untyped]
|
|
19
|
+
@logs: Array[Assistant::LogItem]
|
|
20
|
+
@result: untyped
|
|
21
|
+
@run_started_at: Float
|
|
22
|
+
|
|
23
|
+
attr_reader logs: Array[Assistant::LogItem]
|
|
24
|
+
|
|
25
|
+
def self.run: (**untyped) -> Hash[Symbol, untyped]
|
|
26
|
+
|
|
27
|
+
# M-S4: per-subclass Data class whose members are the declared input
|
|
28
|
+
# names. Memoised; rebuilt when `input_definitions` changes.
|
|
29
|
+
def self.input_snapshot_class: () -> Class
|
|
30
|
+
|
|
31
|
+
def initialize: (**untyped) -> void
|
|
32
|
+
|
|
33
|
+
def run: () -> Hash[Symbol, untyped]
|
|
34
|
+
|
|
35
|
+
def result: () -> untyped
|
|
36
|
+
|
|
37
|
+
def success?: () -> bool
|
|
38
|
+
|
|
39
|
+
def failure?: () -> bool
|
|
40
|
+
|
|
41
|
+
def status: () -> Symbol
|
|
42
|
+
|
|
43
|
+
# M-S2: run a child `Assistant::Service` subclass, merge its log
|
|
44
|
+
# timeline into `self`, return the inner instance. Raises
|
|
45
|
+
# ArgumentError if `klass` is not an `Assistant::Service` subclass.
|
|
46
|
+
def call_service: (Class klass, **untyped inputs) -> Assistant::Service
|
|
47
|
+
|
|
48
|
+
# M-S4: read-only Data view over declared inputs, post-default and
|
|
49
|
+
# post-allow_nil. Only declared inputs are members; undeclared
|
|
50
|
+
# kwargs from `#initialize` are excluded.
|
|
51
|
+
def input_snapshot: () -> Data
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
attr_reader inputs: Hash[Symbol, untyped]
|
|
56
|
+
|
|
57
|
+
def executed_payload: () -> Hash[Symbol, untyped]
|
|
58
|
+
|
|
59
|
+
def failed_payload: () -> Hash[Symbol, untyped]
|
|
60
|
+
|
|
61
|
+
def apply_input_defaults: () -> void
|
|
62
|
+
|
|
63
|
+
def input_definitions_needing_default: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
64
|
+
|
|
65
|
+
def input_supplied?: (Symbol attr_name, Hash[Symbol, untyped] options) -> bool
|
|
66
|
+
|
|
67
|
+
def validate_inputs: () -> Array[Symbol]
|
|
68
|
+
|
|
69
|
+
def notify: (Symbol event) -> void
|
|
70
|
+
|
|
71
|
+
def run_execute_with_callbacks: () -> untyped
|
|
72
|
+
|
|
73
|
+
def run_before_execute_hooks: () -> Array[UnboundMethod]
|
|
74
|
+
|
|
75
|
+
def run_after_execute_hooks: (untyped execute_result) -> Array[UnboundMethod]
|
|
76
|
+
|
|
77
|
+
def run_around_execute_chain: () -> untyped
|
|
78
|
+
|
|
79
|
+
def wrap_around_layer: (UnboundMethod hook, ^() -> untyped inner) -> ^() -> untyped
|
|
80
|
+
|
|
81
|
+
def log_hook_error: (Symbol hook_type, Exception exception) -> void
|
|
82
|
+
|
|
83
|
+
def execute: () -> untyped
|
|
84
|
+
|
|
85
|
+
def validate: () -> void
|
|
5
86
|
end
|
data/lib/assistant/version.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Assistant
|
|
4
|
-
|
|
4
|
+
# Semantic version of the `assistant` gem. Follows the semver contract
|
|
5
|
+
# documented in `docs/v1/01-api-surface.md` from 1.0.0 onward.
|
|
6
|
+
#
|
|
7
|
+
# @return [String]
|
|
8
|
+
VERSION = '1.0.0.rc1'
|
|
5
9
|
end
|
data/lib/assistant.rb
CHANGED
|
@@ -1,9 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Predeclare the namespace shells that multi-segment files
|
|
4
|
+
# (`module Assistant::Refinements::StringBlankness`, etc.) need to exist
|
|
5
|
+
# before they load, and declare the M-S3 instrumentation notifier
|
|
6
|
+
# accessor. Two sibling submodules in one wrapper avoid the
|
|
7
|
+
# `Style/CompactModuleNesting` single-child collapse trigger.
|
|
8
|
+
module Assistant
|
|
9
|
+
# M-S3: frozen no-op default notifier. Identity-compared by
|
|
10
|
+
# `Assistant.notifier=` so callers can detect the unconfigured state
|
|
11
|
+
# if they ever need to. See docs/v1/02-features.md (M-S3) and
|
|
12
|
+
# docs/v1/01-api-surface.md.
|
|
13
|
+
DEFAULT_NOTIFIER = ->(_event, _payload) {}
|
|
14
|
+
DEFAULT_NOTIFIER.freeze
|
|
15
|
+
|
|
16
|
+
# Namespace for the InputBuilder DSL and its submodules. Populated by
|
|
17
|
+
# `lib/assistant/input_builder*.rb`.
|
|
18
|
+
module InputBuilder; end
|
|
19
|
+
|
|
20
|
+
# Namespace for refinements bundled with the gem (`Refinements::*`).
|
|
21
|
+
module Refinements; end
|
|
22
|
+
|
|
23
|
+
@notifier = DEFAULT_NOTIFIER
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Reader for the configured instrumentation callable. Always returns
|
|
27
|
+
# a callable; returns `DEFAULT_NOTIFIER` when never assigned or when
|
|
28
|
+
# explicitly reset with `Assistant.notifier = nil`.
|
|
29
|
+
attr_reader :notifier
|
|
30
|
+
|
|
31
|
+
# Writer for the instrumentation callable. Accepts any object
|
|
32
|
+
# responding to `#call(event, payload)`, or `nil` to reset to the
|
|
33
|
+
# built-in no-op default. Anything else raises `ArgumentError`
|
|
34
|
+
# immediately so misconfiguration surfaces at boot rather than at
|
|
35
|
+
# the first service run.
|
|
36
|
+
def notifier=(callable)
|
|
37
|
+
@notifier =
|
|
38
|
+
if callable.nil?
|
|
39
|
+
DEFAULT_NOTIFIER
|
|
40
|
+
elsif callable.respond_to?(:call)
|
|
41
|
+
callable
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"Assistant.notifier= expected nil or an object responding to #call, got #{callable.inspect}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Core building blocks for the Assistant gem. Listed alphabetically so the
|
|
51
|
+
# top-level entry point exposes every public constant after a bare
|
|
52
|
+
# `require "assistant"` (M6).
|
|
53
|
+
require 'assistant/execute_callbacks'
|
|
54
|
+
require 'assistant/input_builder'
|
|
3
55
|
require 'assistant/log_item'
|
|
56
|
+
require 'assistant/log_list'
|
|
57
|
+
require 'assistant/refinements/string_blankness'
|
|
4
58
|
require 'assistant/service'
|
|
5
59
|
require 'assistant/version'
|
|
6
|
-
|
|
7
|
-
# Main Assistant lib entry point
|
|
8
|
-
module Assistant
|
|
9
|
-
end
|
data/lib/assistant.rbs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant.rb` — declares the top-level
|
|
2
|
+
# `Assistant` namespace, the empty namespace shells for
|
|
3
|
+
# `Assistant::InputBuilder` and `Assistant::Refinements` that the
|
|
4
|
+
# multi-segment submodule files reopen, and the M-S3 instrumentation
|
|
5
|
+
# notifier accessor. See docs/v1/01-api-surface.md.
|
|
6
|
+
|
|
7
|
+
module Assistant
|
|
8
|
+
# M-S3: instrumentation notifier. The setter accepts any object
|
|
9
|
+
# responding to `#call(event, payload)` or `nil` (reset to default).
|
|
10
|
+
# The contract is documented in docs/v1/01-api-surface.md; we use
|
|
11
|
+
# `untyped` for the setter parameter because the runtime check is
|
|
12
|
+
# `respond_to?(:call)` rather than a structural Proc type.
|
|
13
|
+
DEFAULT_NOTIFIER: ^(Symbol, Hash[Symbol, untyped]) -> void
|
|
14
|
+
|
|
15
|
+
self.@notifier: ^(Symbol, Hash[Symbol, untyped]) -> void
|
|
16
|
+
|
|
17
|
+
def self.notifier: () -> ^(Symbol, Hash[Symbol, untyped]) -> void
|
|
18
|
+
def self.notifier=: (untyped callable) -> ^(Symbol, Hash[Symbol, untyped]) -> void
|
|
19
|
+
|
|
20
|
+
module InputBuilder
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Refinements
|
|
24
|
+
end
|
|
25
|
+
end
|
data/mise.toml
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Generated by assistant-rbs; do not edit.
|
|
2
|
+
|
|
3
|
+
module Examples
|
|
4
|
+
class Greeter < Assistant::Service
|
|
5
|
+
|
|
6
|
+
def name: () -> String
|
|
7
|
+
def name?: () -> bool
|
|
8
|
+
def age: () -> (Integer | Float)
|
|
9
|
+
def age?: () -> bool
|
|
10
|
+
def nickname: () -> String?
|
|
11
|
+
def nickname?: () -> bool
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
end
|