assistant 1.0.0.rc1 → 1.0.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.
data/docs/404.html ADDED
@@ -0,0 +1,292 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+
8
+ <title>Assistant &mdash; tiny, dependency-free soft-fail service objects for Ruby</title>
9
+ <meta name="description"
10
+ content="Assistant is a tiny, dependency-free Ruby gem for soft-fail service objects with a uniform result shape, structured log items, and first-class RBS/Steep support.">
11
+ <meta name="theme-color" content="#2f4f5a">
12
+
13
+ <link rel="icon" type="image/svg+xml" href="/assistant/_media/assistant_icon.svg">
14
+ <link rel="apple-touch-icon" sizes="180x180" href="/assistant/_media/apple-touch-icon.png">
15
+ <link rel="preconnect" href="https://fonts.googleapis.com">
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17
+ <link href="https://fonts.googleapis.com/css2?family=MuseoModerno:ital,wght@0,800;1,800&display=swap" rel="stylesheet">
18
+
19
+ <!-- Open Graph / Twitter card -->
20
+ <meta property="og:title" content="Assistant">
21
+ <meta property="og:description" content="Tiny, dependency-free soft-fail service objects for Ruby.">
22
+ <meta property="og:image" content="https://ramongr.github.io/assistant/_media/repo-card.png">
23
+ <meta property="og:image:width" content="1200">
24
+ <meta property="og:image:height" content="630">
25
+ <meta name="twitter:card" content="summary_large_image">
26
+ <meta name="twitter:image" content="https://ramongr.github.io/assistant/_media/repo-card.png">
27
+
28
+ <!-- Docsify default theme + dark/light theme overlay -->
29
+ <link rel="stylesheet"
30
+ href="//cdn.jsdelivr.net/npm/docsify-darklight-theme@latest/dist/style.min.css"
31
+ title="docsify-darklight-theme"
32
+ type="text/css" />
33
+
34
+ <!-- Prism syntax-highlighting theme (light by default; swapped on dark toggle) -->
35
+ <link rel="stylesheet"
36
+ href="//cdn.jsdelivr.net/npm/prism-themes/themes/prism-material-light.min.css"
37
+ id="prism-theme"
38
+ type="text/css" />
39
+
40
+ <style>
41
+ :root {
42
+ /* Brand palette
43
+ #22223b — Space Cadet (dark text / dark bg)
44
+ #a07178 — Rose Taupe (secondary accent / success)
45
+ #4d7c8a — Steel Teal (dark-mode highlight; brand asset color)
46
+ #2f4f5a — Steel Teal Dark (light-mode links / inline code /
47
+ theme color — 7.87:1 on #f4f2f3,
48
+ 8.30:1 on #ffffff, both AAA)
49
+ #f4f2f3 — Cultured (light background)
50
+ #ffd166 — Naples Yellow (primary accent) */
51
+ --theme-color: #2f4f5a;
52
+ --assistant-space-cadet: #22223b;
53
+ --assistant-naples-yellow: #ffd166;
54
+ }
55
+ .app-name-link {
56
+ align-items: center;
57
+ display: flex;
58
+ justify-content: center;
59
+ padding: 0.75rem 0 1rem;
60
+ }
61
+ .assistant-app-name {
62
+ display: block;
63
+ font-family: 'MuseoModerno', 'PT Sans', system-ui, sans-serif;
64
+ font-size: 5.5rem;
65
+ font-style: normal;
66
+ font-weight: 800;
67
+ letter-spacing: -0.09em;
68
+ line-height: 0.82;
69
+ }
70
+ .assistant-home-wordmark {
71
+ color: #22223b;
72
+ font-family: 'MuseoModerno', 'PT Sans', system-ui, sans-serif;
73
+ font-size: clamp(4.25rem, 16vw, 16rem);
74
+ font-style: normal;
75
+ font-weight: 800;
76
+ letter-spacing: -0.09em;
77
+ line-height: 0.82;
78
+ margin: 0 0 1rem;
79
+ text-align: center;
80
+ }
81
+ body.assistant-sidebar-shell .sidebar {
82
+ padding-top: 4rem;
83
+ }
84
+ body.assistant-sidebar-shell .sidebar-toggle {
85
+ align-items: center;
86
+ background: var(--assistant-space-cadet);
87
+ border: 1px solid rgba(255, 209, 102, 0.42);
88
+ border-radius: 999px;
89
+ bottom: auto;
90
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.24);
91
+ color: var(--assistant-naples-yellow);
92
+ display: flex;
93
+ height: 2.25rem;
94
+ justify-content: center;
95
+ left: 0.875rem;
96
+ padding: 0;
97
+ top: 0.875rem;
98
+ transition: background-color 0.2s ease, border-color 0.2s ease,
99
+ color 0.2s ease, transform 0.2s ease;
100
+ width: 2.25rem;
101
+ z-index: 60;
102
+ }
103
+ body.assistant-sidebar-shell .sidebar-toggle:hover {
104
+ background: var(--assistant-naples-yellow);
105
+ border-color: var(--assistant-naples-yellow);
106
+ color: var(--assistant-space-cadet);
107
+ transform: translateY(-1px);
108
+ }
109
+ body.assistant-sidebar-shell .sidebar-toggle span {
110
+ display: none;
111
+ }
112
+ body.assistant-sidebar-shell .sidebar-toggle::before {
113
+ content: "\00d7";
114
+ font-size: 1.75rem;
115
+ font-weight: 700;
116
+ line-height: 1;
117
+ transform: translateY(-0.08em);
118
+ }
119
+ body.assistant-sidebar-shell.close .sidebar-toggle {
120
+ background: var(--assistant-naples-yellow);
121
+ border-color: var(--assistant-naples-yellow);
122
+ color: var(--assistant-space-cadet);
123
+ }
124
+ body.assistant-sidebar-shell.close .sidebar-toggle:hover {
125
+ background: var(--assistant-space-cadet);
126
+ border-color: var(--assistant-space-cadet);
127
+ color: var(--assistant-naples-yellow);
128
+ }
129
+ body.assistant-sidebar-shell.close .sidebar-toggle::before {
130
+ content: "\2630";
131
+ font-size: 1.25rem;
132
+ transform: none;
133
+ }
134
+ </style>
135
+ </head>
136
+ <body class="assistant-sidebar-shell">
137
+ <div id="app">Loading documentation&hellip;</div>
138
+
139
+ <script>
140
+ // Re-applies the matching Prism theme stylesheet whenever the
141
+ // docsify-darklight-theme toggle is clicked. Without this, switching to
142
+ // dark mode keeps the light Prism palette on code blocks.
143
+ const prismThemeSwitcher = function (hook) {
144
+ hook.doneEach(function () {
145
+ const toggle = document.querySelector('.docsify-darklight-theme');
146
+ if (!toggle || toggle.dataset.bound === '1') return;
147
+ toggle.dataset.bound = '1';
148
+ toggle.addEventListener('click', function () {
149
+ const link = document.getElementById('prism-theme');
150
+ if (!link) return;
151
+ const dark = link.href.includes('prism-material-dark');
152
+ link.href = dark
153
+ ? '//cdn.jsdelivr.net/npm/prism-themes/themes/prism-material-light.min.css'
154
+ : '//cdn.jsdelivr.net/npm/prism-themes/themes/prism-material-dark.min.css';
155
+ });
156
+ });
157
+ };
158
+
159
+ // Page footer rendered by docsify after each markdown body.
160
+ const pageFooter = function (hook) {
161
+ const footer = [
162
+ '<hr/>',
163
+ '<footer style="text-align:center; opacity:.7; font-size:.9em; padding:1rem 0;">',
164
+ ' <span>Assistant &middot; MIT-licensed &middot; ',
165
+ ' <a href="https://github.com/ramongr/assistant" target="_blank" rel="noopener">GitHub</a> &middot; ',
166
+ ' <a href="https://rubygems.org/gems/assistant" target="_blank" rel="noopener">RubyGems</a>',
167
+ ' </span>',
168
+ '</footer>'
169
+ ].join('');
170
+ hook.afterEach(function (html) { return html + footer; });
171
+ };
172
+
173
+ window.$docsify = {
174
+ name: '<span class="assistant-app-name" aria-label="Assistant">a</span>',
175
+ nameLink: '#/',
176
+ repo: 'ramongr/assistant',
177
+ basePath: '/assistant/',
178
+ homepage: 'index.md',
179
+ coverpage: false,
180
+ loadSidebar: true,
181
+ alias: {
182
+ '/.*/_sidebar.md': '/_sidebar.md'
183
+ },
184
+ loadNavbar: false,
185
+ auto2top: true,
186
+ subMaxLevel: 3,
187
+ maxLevel: 4,
188
+ relativePath: true,
189
+ notFoundPage: true,
190
+ executeScript: true,
191
+ themeColor: '#2f4f5a',
192
+ // Hash-routed by default (`/assistant/#/getting-started`). A
193
+ // history-mode experiment was tried but every clean URL like
194
+ // `/assistant/getting-started` returned HTTP 404 on GitHub Pages — Pages
195
+ // serves `docs/404.html` as the SPA shell for unknown paths, but always
196
+ // with a 404 status code, which breaks link previews, link checkers,
197
+ // and search crawlers. Hash routing keeps every navigable URL on a
198
+ // genuine 200 response.
199
+ search: {
200
+ maxAge: 86400000,
201
+ paths: 'auto',
202
+ placeholder: 'Search the docs …',
203
+ noData: 'No results.',
204
+ depth: 3,
205
+ hideOtherSidebarContent: false
206
+ },
207
+ copyCode: {
208
+ buttonText: 'Copy',
209
+ errorText: 'Error',
210
+ successText: 'Copied'
211
+ },
212
+ mermaidConfig: {
213
+ querySelector: '.mermaid'
214
+ },
215
+ darklightTheme: {
216
+ siteFont: 'PT Sans, system-ui, sans-serif',
217
+ defaultTheme: 'light',
218
+ codeFontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
219
+ bodyFontSize: '17px',
220
+ dark: {
221
+ accent: '#ffd166',
222
+ toggleBackground: '#22223b',
223
+ background: '#22223b',
224
+ textColor: '#f4f2f3',
225
+ codeTextColor: '#f4f2f3',
226
+ codeBackgroundColor: '#2c2c4a',
227
+ borderColor: '#3a3a5c',
228
+ blockQuoteColor: '#a07178',
229
+ highlightColor: '#4d7c8a',
230
+ sidebarSublink: '#a07178',
231
+ codeTypeColor: '#4d7c8a',
232
+ coverBackground: 'linear-gradient(to left bottom, #22223b 0%, #ffd166 100%)'
233
+ },
234
+ light: {
235
+ accent: '#2f4f5a',
236
+ toggleBackground: '#f4f2f3',
237
+ background: '#f4f2f3',
238
+ textColor: '#22223b',
239
+ codeTextColor: '#22223b',
240
+ codeBackgroundColor: '#ffffff',
241
+ borderColor: 'rgba(34,34,59,0.12)',
242
+ blockQuoteColor: '#a07178',
243
+ highlightColor: '#2f4f5a',
244
+ sidebarSublink: '#22223b',
245
+ codeTypeColor: '#2f4f5a',
246
+ coverBackground: 'linear-gradient(to left bottom, #f4f2f3 0%, #a07178 100%)'
247
+ }
248
+ },
249
+ plugins: [
250
+ prismThemeSwitcher,
251
+ pageFooter,
252
+ function (hook, vm) {
253
+ if (typeof EditOnGithubPlugin !== 'undefined') {
254
+ EditOnGithubPlugin.create(
255
+ 'https://github.com/ramongr/assistant/blob/main/docs/',
256
+ null,
257
+ 'Edit this page on GitHub'
258
+ )(hook, vm);
259
+ }
260
+ }
261
+ ]
262
+ };
263
+ </script>
264
+
265
+ <!-- Docsify core -->
266
+ <script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
267
+
268
+ <!-- Dark/light theme toggle -->
269
+ <script src="//cdn.jsdelivr.net/npm/docsify-darklight-theme@latest/dist/index.min.js"
270
+ type="text/javascript"></script>
271
+
272
+ <!-- Plugins -->
273
+ <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
274
+ <script src="//cdn.jsdelivr.net/npm/docsify-copy-code@2"></script>
275
+ <script src="//cdn.jsdelivr.net/npm/docsify-edit-on-github"></script>
276
+ <script src="//cdn.jsdelivr.net/npm/docsify-tabs@1"></script>
277
+
278
+ <!-- Mermaid (diagrams) — load the UMD bundle from /dist/, not the
279
+ package root, which jsDelivr resolves to an ES-module file that the
280
+ browser refuses to execute as a classic script. -->
281
+ <script src="//cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
282
+ <script src="//cdn.jsdelivr.net/npm/docsify-mermaid@2.0.1/dist/docsify-mermaid.js"></script>
283
+ <script>mermaid.initialize({ startOnLoad: false, theme: 'default' });</script>
284
+
285
+ <!-- Prism syntax highlighting: Ruby + autoloader for everything else -->
286
+ <script src="//cdn.jsdelivr.net/npm/prismjs/components/prism-ruby.min.js"></script>
287
+ <script src="//cdn.jsdelivr.net/npm/prismjs/components/prism-bash.min.js"></script>
288
+ <script src="//cdn.jsdelivr.net/npm/prismjs/components/prism-yaml.min.js"></script>
289
+ <script src="//cdn.jsdelivr.net/npm/prismjs/components/prism-json.min.js"></script>
290
+ <script src="//cdn.jsdelivr.net/npm/prismjs/plugins/autoloader/prism-autoloader.min.js"></script>
291
+ </body>
292
+ </html>
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Assistant">
3
+ <title>Assistant</title>
4
+ <rect width="64" height="64" rx="12" fill="#22223b"/>
5
+ <text x="32" y="44" text-anchor="middle"
6
+ font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
7
+ font-size="38" font-weight="700" fill="#ffd166">A</text>
8
+ </svg>
Binary file
data/docs/_sidebar.md ADDED
@@ -0,0 +1,29 @@
1
+ <!-- Sidebar (Docsify reads this on every route).
2
+ Order: Home, Getting started, Guides, API reference, Deprecations,
3
+ Examples. -->
4
+
5
+ - [Home](/)
6
+ - [Getting started](/getting-started.md)
7
+
8
+ - [Guides](/guides/)
9
+ - [Inputs](/guides/inputs.md)
10
+ - [Validation](/guides/validation.md)
11
+ - [Logging and results](/guides/logging-and-results.md)
12
+ - [Composing services](/guides/composing-services.md)
13
+ - [RBS and types](/guides/rbs-and-types.md)
14
+
15
+ - [API reference](/api-reference.md)
16
+ - [Deprecations](/deprecations.md)
17
+
18
+ - [Examples](/examples/)
19
+ - [Rails service](/examples/rails-service.md)
20
+ - [CLI handler](/examples/cli-handler.md)
21
+ - [Sidekiq worker](/examples/sidekiq-worker.md)
22
+ - [Composing services](/examples/composing-services.md)
23
+ - [Execute callbacks](/examples/execute-callbacks.md)
24
+ - [Instrumentation notifier](/examples/instrumentation-notifier.md)
25
+ - [RBS generator](/examples/rbs-generator.md)
26
+
27
+ - **Links**
28
+ - [GitHub](https://github.com/ramongr/assistant)
29
+ - [RubyGems](https://rubygems.org/gems/assistant)
@@ -1,8 +1,3 @@
1
- ---
2
- title: API reference
3
- nav_order: 3
4
- ---
5
-
6
1
  <!-- markdownlint-disable MD013 MD024 -->
7
2
  # API reference
8
3
 
@@ -25,13 +20,13 @@ without a major version bump.
25
20
  ## Table of contents
26
21
 
27
22
  - [`Assistant` module](#assistant-module)
28
- - [`Assistant::Service`](#assistantservice)
23
+ - [`Assistant::Service`](#assistant-service)
29
24
  - [Class methods](#class-methods)
30
25
  - [Instance methods](#instance-methods)
31
26
  - [Generated per-input methods](#generated-per-input-methods)
32
27
  - [Result shape](#result-shape)
33
- - [`Assistant::LogItem`](#assistantlogitem)
34
- - [`Assistant::LogList`](#assistantloglist)
28
+ - [`Assistant::LogItem`](#assistant-logitem)
29
+ - [`Assistant::LogList`](#assistant-loglist)
35
30
  - [Execute callbacks](#execute-callbacks)
36
31
  - [Service composition](#service-composition)
37
32
  - [Instrumentation notifier](#instrumentation-notifier)
@@ -72,7 +67,7 @@ you.
72
67
  | `Service.around_execute(&block)` | Frozen | Hook block is `instance_exec`'d with an inner block argument that yields to the next layer. |
73
68
  | `Service.input_snapshot_class -> Class` | Frozen *(new in 1.0)* | Memoised `Data.define(*declared_input_names)` class used by `#input_snapshot`. |
74
69
 
75
- > **M12 keyword-only sweep.** Every other public DSL helper (e.g.
70
+ > **Keyword-only DSL.** Every other public DSL helper (e.g.
76
71
  > `merge_logs`, all `InputBuilder` internals) takes keyword arguments
77
72
  > only. The two exemptions above — `Service.input` and
78
73
  > `Service.inputs` — keep their leading positional `name` / `names`
@@ -130,11 +125,27 @@ hash shapes:
130
125
  The status enum is exhaustively `:ok`, `:with_warnings`, `:with_errors`.
131
126
  No new status values may be added in 1.x without a deprecation cycle.
132
127
 
128
+ The status flag is derived purely from what the service logged during
129
+ `#run`:
130
+
131
+ ```mermaid
132
+ flowchart TD
133
+ Start([Service#run]) --> Errors{Any LogItem with<br/>level: :error?}
134
+ Errors -- Yes --> Failed["{ result: nil,<br/>status: :with_errors,<br/>errors: [...] }"]
135
+ Errors -- No --> Warnings{Any LogItem with<br/>level: :warning?}
136
+ Warnings -- Yes --> WithWarnings["{ result: ...,<br/>status: :with_warnings,<br/>warnings: [...] }"]
137
+ Warnings -- No --> Ok["{ result: ...,<br/>status: :ok,<br/>warnings: [] }"]
138
+ ```
139
+
140
+ `#execute` is **skipped** entirely when any declarative or custom
141
+ `#validate` check has already logged an error — see the
142
+ [Validation guide](guides/validation.md) for the full lifecycle.
143
+
133
144
  ---
134
145
 
135
146
  ## `Assistant::LogItem`
136
147
 
137
- > **Breaking change in 1.0 (M10).** `LogItem.new` now raises
148
+ > **Breaking change in 1.0.** `LogItem.new` now raises
138
149
  > `ArgumentError` when any required attribute is invalid. The
139
150
  > `#valid?` family is **retained** for introspection, but in normal
140
151
  > flows it always returns `true` after a successful `new`.
@@ -231,7 +242,7 @@ return values whose `class` is `equal?`. See
231
242
 
232
243
  ## `assistant-rbs` CLI
233
244
 
234
- Bundled executable shipped at `exe/assistant-rbs` (M11). Generates
245
+ Bundled executable shipped at `exe/assistant-rbs`. Generates
235
246
  per-class RBS signatures for `Assistant::Service` subclasses so Steep
236
247
  can type-check user code.
237
248
 
data/docs/changelog.md CHANGED
@@ -1,8 +1,3 @@
1
- ---
2
- title: Changelog
3
- nav_order: 7
4
- ---
5
-
6
1
  # Changelog
7
2
 
8
3
  The full release history is in
data/docs/deprecations.md CHANGED
@@ -1,8 +1,3 @@
1
- ---
2
- title: Deprecations
3
- nav_order: 4
4
- ---
5
-
6
1
  # Deprecations
7
2
 
8
3
  This page tracks every public symbol that is **deprecated** in a 1.x release
@@ -16,14 +11,14 @@ The deprecation policy is one full minor cycle: anything marked here in
16
11
 
17
12
  | Symbol | Replacement | Deprecated in | Removed in |
18
13
  |---------------------------------------------------------|-------------------------------------------------------------------|---------------|------------|
19
- | `Assistant::Service#valid_require_<name>?` | `Assistant::Service#valid_required_<name>?` | `1.0.0` (M9) | `2.0.0` |
20
- | `Assistant::Service#valid_require_conditional_<name>?` | `Assistant::Service#valid_required_conditional_<name>?` | `1.0.0` (M9) | `2.0.0` |
14
+ | `Assistant::Service#valid_require_<name>?` | `Assistant::Service#valid_required_<name>?` | `1.0.0` | `2.0.0` |
15
+ | `Assistant::Service#valid_require_conditional_<name>?` | `Assistant::Service#valid_required_conditional_<name>?` | `1.0.0` | `2.0.0` |
21
16
 
22
17
  ---
23
18
 
24
19
  ## `valid_require_<name>?` → `valid_required_<name>?`
25
20
 
26
- **Deprecated in**: `1.0.0` (M9). **Removed in**: `2.0.0`.
21
+ **Deprecated in**: `1.0.0`. **Removed in**: `2.0.0`.
27
22
 
28
23
  ### What changed
29
24
 
@@ -82,5 +77,5 @@ was decided in favour of Option B: the new names read better, match
82
77
  standard English, and are easier to grep for. The old names live one
83
78
  minor cycle to give downstream services an upgrade window.
84
79
 
85
- See [`docs/v1/02-features.md`](https://github.com/ramongr/assistant/blob/main/docs/v1/02-features.md) **M9** for the
80
+ See [`docs/v1/02-features.md`](https://github.com/ramongr/assistant/blob/main/docs/v1/02-features.md) for the
86
81
  implementation plan and acceptance criteria.
@@ -1,17 +1,10 @@
1
- ---
2
- title: Examples
3
- nav_order: 5
4
- has_children: true
5
- permalink: /examples/
6
- ---
1
+ # Examples <!-- {docsify-ignore-all} -->
7
2
 
8
- # Examples
9
-
10
- > **Status:** gallery scaffolding — each entry below ships with a
11
- > runnable script under `examples/<slug>/`, a writeup on this site,
12
- > and a regression test. See
13
- > [P6–P12 of the GitHub Pages plan](https://github.com/ramongr/assistant/blob/main/docs/v1/08-github-pages.md#p6p12-examples-one-pr-per-example)
14
- > for the per-example schedule.
3
+ Each entry below shows a small, real-world wiring pattern with a
4
+ callout-sized code snippet. Runnable scripts live under
5
+ [`examples/<slug>/`](https://github.com/ramongr/assistant/tree/main/examples)
6
+ and their regression tests under
7
+ [`test/examples/`](https://github.com/ramongr/assistant/tree/main/test/examples).
15
8
 
16
9
  | Example | Demonstrates |
17
10
  | --- | --- |
@@ -21,9 +14,6 @@ permalink: /examples/
21
14
  | [Composing services](composing-services.md) | Outer service uses `call_service` to chain two inner services; log timeline merging. |
22
15
  | [Execute callbacks](execute-callbacks.md) | `before_execute` audit logger; `around_execute` timing wrapper; failure cases. |
23
16
  | [Instrumentation notifier](instrumentation-notifier.md) | `Assistant.notifier=` wired to a fake `ActiveSupport::Notifications`-shaped sink. |
24
- | [RBS generator](rbs-generator.md) | Service definition → `bin/assistant-rbs --output sig` → Steep proving per-input return types. |
17
+ | [RBS generator](rbs-generator.md) | Service definition → `bin/assistant-rbs --output sig` → Steep proving generated input reader types. |
25
18
 
26
19
  Each example is intentionally small enough to be read in one sitting.
27
- Source for every script lives under
28
- [`examples/`](https://github.com/ramongr/assistant/tree/main/examples)
29
- in the repository.
@@ -1,17 +1,43 @@
1
- ---
2
- title: CLI handler
3
- parent: Examples
4
- nav_order: 2
5
- ---
6
-
7
1
  # CLI handler
8
2
 
9
- > **Status:** placeholder ships in
10
- > [P7](https://github.com/ramongr/assistant/blob/main/docs/v1/08-github-pages.md#p6p12-examples-one-pr-per-example) of
11
- > the GitHub Pages plan.
3
+ An `OptionParser`-driven script that runs a service and derives the
4
+ process exit code from `#status`:
5
+
6
+ ```ruby
7
+ #!/usr/bin/env ruby
8
+ # frozen_string_literal: true
9
+
10
+ require 'optparse'
11
+ require 'assistant'
12
+ require_relative 'create_user'
13
+
14
+ options = {}
15
+ OptionParser.new do |opts|
16
+ opts.banner = 'Usage: create_user --email EMAIL --name NAME'
17
+ opts.on('-eEMAIL', '--email=EMAIL') { |v| options[:email] = v }
18
+ opts.on('-nNAME', '--name=NAME') { |v| options[:name] = v }
19
+ end.parse!
20
+
21
+ case CreateUser.run(**options)
22
+ in { result:, status: :ok }
23
+ warn "ok: #{result.inspect}"
24
+ exit 0
25
+ in { result:, status: :with_warnings, warnings: }
26
+ warn "ok (with warnings): #{result.inspect}"
27
+ warnings.each { |w| warn " warning: #{w.message}" }
28
+ exit 0
29
+ in { errors:, status: :with_errors }
30
+ errors.each { |e| warn "error: #{e.message}" }
31
+ exit 1
32
+ end
33
+ ```
34
+
35
+ Notes:
12
36
 
13
- An `OptionParser`-driven script whose exit code derives from `#status`.
37
+ * `:ok` and `:with_warnings` both exit 0 warnings are still a
38
+ successful run. Only `:with_errors` exits 1.
39
+ * Print to `$stderr` (`warn`) so the script can be piped without log
40
+ noise leaking into stdout.
14
41
 
15
- When the runnable script under `examples/cli_handler/` lands, this
16
- page will include it verbatim via Jekyll `include_relative` so the
17
- prose stays in lockstep with the code.
42
+ > Source: [`examples/cli_handler/`](https://github.com/ramongr/assistant/tree/main/examples/cli_handler) ·
43
+ > Test: [`test/examples/cli_handler_example_test.rb`](https://github.com/ramongr/assistant/blob/main/test/examples/cli_handler_example_test.rb)
@@ -1,17 +1,49 @@
1
- ---
2
- title: Composing services
3
- parent: Examples
4
- nav_order: 4
5
- ---
6
-
7
1
  # Composing services
8
2
 
9
- > **Status:** placeholder ships in
10
- > [P9](https://github.com/ramongr/assistant/blob/main/docs/v1/08-github-pages.md#p6p12-examples-one-pr-per-example) of
11
- > the GitHub Pages plan.
3
+ `#call_service` runs a sibling service from inside `#execute` and
4
+ merges its log timeline into the outer service automatically — see the
5
+ [Composing services guide](../guides/composing-services.md) for the
6
+ full contract.
7
+
8
+ A two-step signup, where `CreateUser` looks up or creates the user
9
+ and `SendWelcomeEmail` queues the welcome:
10
+
11
+ ```ruby
12
+ class SignUpUser < Assistant::Service
13
+ input :email, type: String, required: true
14
+ input :name, type: String, required: true
15
+
16
+ def execute
17
+ user = call_service(CreateUser, email:, name:)
18
+ return if user.failure?
19
+
20
+ call_service(SendWelcomeEmail, user_id: user.result.id)
21
+
22
+ user.result
23
+ end
24
+ end
25
+ ```
26
+
27
+ What the merged timeline looks like:
28
+
29
+ ```text
30
+ SignUpUser.run(email: 'a@b.com', name: 'Alice')
31
+ # => { result: <User>, status: :with_warnings,
32
+ # warnings: [
33
+ # #<LogItem :email_normalized 'normalized to a@b.com'>, # from CreateUser
34
+ # #<LogItem :throttled 'welcome queued for 1s delay'> # from SendWelcomeEmail
35
+ # ] }
36
+ ```
37
+
38
+ Notes:
12
39
 
13
- An outer service uses `call_service` to chain two inner services with log timeline merging.
40
+ * If `CreateUser` returns `:with_errors`, `user.failure?` is true and
41
+ the early-return propagates: the outer status downgrades to
42
+ `:with_errors` automatically because the inner errors were merged
43
+ via `merge_logs`.
44
+ * Use `call_service` (not `CreateUser.run(...)`) so the inner service's
45
+ logs join the outer timeline. Calling `.run` directly silently
46
+ discards them.
14
47
 
15
- When the runnable script under `examples/composing_services/` lands, this
16
- page will include it verbatim via Jekyll `include_relative` so the
17
- prose stays in lockstep with the code.
48
+ > Source: [`examples/composing_services/`](https://github.com/ramongr/assistant/tree/main/examples/composing_services) ·
49
+ > Test: [`test/examples/composing_services_example_test.rb`](https://github.com/ramongr/assistant/blob/main/test/examples/composing_services_example_test.rb)