rubino-agent 0.3.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# Errors, exceptions & type checking
|
|
2
|
+
|
|
3
|
+
Modern Ruby (3.2–3.4) error handling, plus optional static typing with RBS and Sorbet. Dense, idiomatic, do/don't.
|
|
4
|
+
|
|
5
|
+
## The exception hierarchy
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Exception
|
|
9
|
+
├── NoMemoryError, SystemExit, SignalException, ScriptError, ... # do NOT rescue these
|
|
10
|
+
└── StandardError # rescue THIS
|
|
11
|
+
├── ArgumentError, TypeError, KeyError, IndexError, NameError
|
|
12
|
+
├── RuntimeError # default class for `raise "msg"`
|
|
13
|
+
├── IOError, Errno::*
|
|
14
|
+
└── your custom errors (subclass StandardError)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`rescue` with no class rescues `StandardError`, **not** `Exception`. Rescuing `Exception` swallows `SignalException` (Ctrl-C), `SystemExit` (`exit`), and `NoMemoryError` — breaking process control and hiding fatal bugs.
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# WRONG — catches Ctrl-C, exit, and out-of-memory; nearly always a bug
|
|
21
|
+
begin
|
|
22
|
+
do_work
|
|
23
|
+
rescue Exception => e
|
|
24
|
+
log(e)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# RIGHT — bare rescue defaults to StandardError
|
|
28
|
+
begin
|
|
29
|
+
do_work
|
|
30
|
+
rescue => e # == rescue StandardError => e
|
|
31
|
+
log(e)
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Only rescue what you can handle. Catch specific classes when you can do something specific; let everything else propagate.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
begin
|
|
39
|
+
parse(payload)
|
|
40
|
+
rescue JSON::ParserError => e # specific: we know how to recover
|
|
41
|
+
fallback
|
|
42
|
+
rescue KeyError => e # specific: missing field
|
|
43
|
+
report_missing(e.key)
|
|
44
|
+
end
|
|
45
|
+
# Anything else bubbles up — good.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Raising well
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
raise "boom" # RuntimeError, message "boom"
|
|
52
|
+
raise ArgumentError, "name required" # class + message — the common form
|
|
53
|
+
raise ArgumentError.new("name required")
|
|
54
|
+
raise MyError.new(code: 42) # custom class with structured data
|
|
55
|
+
raise # re-raise the current exception (inside rescue)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Prefer `raise Class, "message"` over constructing the instance unless you need to pass extra constructor args. Always raise a *class*, never a string-only `raise` for library code where callers may want to rescue a specific type.
|
|
59
|
+
|
|
60
|
+
`fail` is an alias of `raise`. Some style guides used `fail` for the first raise and `raise` for re-raises; today **prefer `raise` everywhere** for consistency (RuboCop's default `Style/SignalException` enforces `raise`).
|
|
61
|
+
|
|
62
|
+
## Custom error classes & a per-library base error
|
|
63
|
+
|
|
64
|
+
Give every library/app a single base error so callers can `rescue MyLib::Error` to catch *anything* from it. Subclass `StandardError`, never `Exception`.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
module Billing
|
|
68
|
+
class Error < StandardError; end # base — one per library
|
|
69
|
+
|
|
70
|
+
class PaymentDeclined < Error
|
|
71
|
+
attr_reader :code, :gateway_ref
|
|
72
|
+
|
|
73
|
+
def initialize(code:, gateway_ref:, message: nil)
|
|
74
|
+
@code = code
|
|
75
|
+
@gateway_ref = gateway_ref
|
|
76
|
+
super(message || "Payment declined (#{code})")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class RateLimited < Error
|
|
81
|
+
attr_reader :retry_after
|
|
82
|
+
def initialize(retry_after)
|
|
83
|
+
@retry_after = retry_after
|
|
84
|
+
super("Rate limited; retry after #{retry_after}s")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Caller can be coarse or fine-grained:
|
|
90
|
+
begin
|
|
91
|
+
Billing.charge(card)
|
|
92
|
+
rescue Billing::PaymentDeclined => e
|
|
93
|
+
notify_user(e.code)
|
|
94
|
+
rescue Billing::Error => e # catch-all for this library only
|
|
95
|
+
retry_later(e)
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Rules for custom errors:
|
|
100
|
+
- Always call `super(message)` so `#message`/`#to_s` work.
|
|
101
|
+
- Expose structured data via `attr_reader`, not by stuffing it into the message string.
|
|
102
|
+
- Keep the hierarchy shallow (base + a handful of leaf classes).
|
|
103
|
+
|
|
104
|
+
## begin / rescue / else / ensure / retry
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
begin
|
|
108
|
+
result = risky
|
|
109
|
+
rescue SomeError => e
|
|
110
|
+
handle(e) # runs only on error
|
|
111
|
+
else
|
|
112
|
+
use(result) # runs only when NO exception was raised
|
|
113
|
+
ensure
|
|
114
|
+
cleanup # ALWAYS runs (success, error, return, or break)
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `else` holds the "happy path" code that must *not* be guarded by the rescue. Keeps the `begin` block to just the risky call.
|
|
119
|
+
- `ensure` always runs — use it for cleanup. Do **not** `return` from `ensure`; it silently swallows the in-flight exception/return value.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# WRONG — return in ensure eats the exception
|
|
123
|
+
def f
|
|
124
|
+
raise "x"
|
|
125
|
+
ensure
|
|
126
|
+
return 1 # caller gets 1, exception vanishes. Never do this.
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Method-level rescue (no explicit begin)
|
|
131
|
+
|
|
132
|
+
`def`, blocks, and `do...end` have implicit begin/ensure scopes:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
def fetch
|
|
136
|
+
api_call
|
|
137
|
+
rescue Timeout::Error => e
|
|
138
|
+
retry_or_raise(e)
|
|
139
|
+
ensure
|
|
140
|
+
close_connection
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### retry with a backoff cap
|
|
145
|
+
|
|
146
|
+
`retry` re-runs the `begin` block. **Always cap attempts** or you get infinite loops. Add backoff (ideally with jitter) for network calls.
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
def with_retries(max: 3, base: 0.5)
|
|
150
|
+
attempts = 0
|
|
151
|
+
begin
|
|
152
|
+
yield
|
|
153
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
154
|
+
attempts += 1
|
|
155
|
+
raise if attempts >= max # give up — re-raise last error
|
|
156
|
+
sleep(base * (2 ** (attempts - 1)) + rand * 0.1) # exp backoff + jitter
|
|
157
|
+
retry
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
with_retries(max: 4) { http.get(url) }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Do **not** `retry` on programmer errors (`ArgumentError`, `NoMethodError`) — they won't fix themselves. Only retry transient failures.
|
|
165
|
+
|
|
166
|
+
## Rescue modifier & inline-rescue pitfalls
|
|
167
|
+
|
|
168
|
+
The one-line `expr rescue fallback` form catches **`StandardError`** and discards the exception object. It's a blunt instrument.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
value = Integer(str) rescue 0 # ok-ish: narrow, intentional fallback
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Pitfalls:
|
|
175
|
+
- It hides *every* `StandardError`, not just the one you expect (e.g. a typo `NameError` becomes `0`).
|
|
176
|
+
- You can't inspect the error.
|
|
177
|
+
- It's easy to over-scope.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# WRONG — masks bugs; if `parse` raises NoMethodError you silently get nil
|
|
181
|
+
data = parse(payload) rescue nil
|
|
182
|
+
|
|
183
|
+
# RIGHT — name the error you actually expect
|
|
184
|
+
data =
|
|
185
|
+
begin
|
|
186
|
+
parse(payload)
|
|
187
|
+
rescue JSON::ParserError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
For "give me nil on a known failure", prefer purpose-built methods: `Integer(s, exception: false)`, `Float(s, exception: false)`, `hash.dig`, `Array.fetch` with default, etc.
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
Integer("x", exception: false) # => nil, no rescue needed
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Inspecting the rescued exception
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
rescue => e
|
|
202
|
+
e.message # the message string
|
|
203
|
+
e.class # e.g. KeyError
|
|
204
|
+
e.backtrace # Array<String>
|
|
205
|
+
e.full_message # formatted message + backtrace + cause chain (great for logs)
|
|
206
|
+
e.cause # the exception that was in flight when this one was raised
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`e.full_message(highlight: false)` gives plain text suited for log files; `highlight: true` (default on a TTY) adds ANSI color.
|
|
210
|
+
|
|
211
|
+
## Cause chaining (`Exception#cause`)
|
|
212
|
+
|
|
213
|
+
When you `raise` *inside* a `rescue`, Ruby automatically sets the new exception's `#cause` to the one being handled — preserving the original. Do **not** manually thread the original through unless you want to override it.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
def load_config
|
|
217
|
+
YAML.safe_load_file(path)
|
|
218
|
+
rescue Psych::SyntaxError => e
|
|
219
|
+
raise ConfigError, "config #{path} is invalid"
|
|
220
|
+
# e becomes the implicit cause; full_message shows both
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`full_message` then prints:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
ConfigError: config /etc/app.yml is invalid
|
|
228
|
+
...
|
|
229
|
+
caused by: Psych::SyntaxError: (...) ...
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Override or suppress the implicit cause explicitly:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
raise ConfigError, "bad config", cause: nil # drop the chain
|
|
236
|
+
raise ConfigError.new("bad config"), cause: original # set a specific cause
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Wrap low-level errors in your library's error type while keeping the cause, so callers get a stable interface and you don't lose the root cause:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
rescue PG::Error => e
|
|
243
|
+
raise Repo::DatabaseError, "query failed" # cause = the PG error, preserved
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Resource cleanup: prefer auto-closing blocks over manual ensure
|
|
247
|
+
|
|
248
|
+
If an API offers a block form that closes/releases automatically, use it. Reach for `ensure` only when there's no block form.
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
# RIGHT — block form closes the file even on exception
|
|
252
|
+
File.open(path, "r") do |f|
|
|
253
|
+
process(f)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Manual equivalent — only when no block form exists
|
|
257
|
+
f = acquire_resource
|
|
258
|
+
begin
|
|
259
|
+
process(f)
|
|
260
|
+
ensure
|
|
261
|
+
f.release # runs on success and on error
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Common block-closing APIs: `File.open`, `Tempfile.create`, `Net::HTTP.start`, `Mutex#synchronize`, `connection_pool.with`, `ActiveRecord::Base.transaction` (rolls back on raise). Don't hand-roll `ensure` when one of these fits.
|
|
266
|
+
|
|
267
|
+
## Structured error data & custom messages
|
|
268
|
+
|
|
269
|
+
Put machine-readable detail in attributes; keep the message human-readable.
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
class ValidationError < StandardError
|
|
273
|
+
attr_reader :errors # e.g. { email: ["is invalid"], age: ["too low"] }
|
|
274
|
+
|
|
275
|
+
def initialize(errors)
|
|
276
|
+
@errors = errors
|
|
277
|
+
super("Validation failed: #{errors.keys.join(', ')}")
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
begin
|
|
282
|
+
validate!(form)
|
|
283
|
+
rescue ValidationError => e
|
|
284
|
+
render json: e.errors, status: :unprocessable_entity
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
This is cleaner than parsing strings out of `e.message`, and it survives i18n of the message.
|
|
289
|
+
|
|
290
|
+
## Exceptions vs Result objects
|
|
291
|
+
|
|
292
|
+
Use **exceptions** for genuinely exceptional / unexpected conditions and for cross-cutting failures you want to bubble up (DB down, bug, programmer error). Use a **Result object** when failure is an expected, *modeled* outcome that the caller must branch on (validation, "user not found", payment declined in a flow). Don't use exceptions for ordinary control flow — they're slow on the raise path and obscure intent.
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
# Exception: unexpected
|
|
296
|
+
raise Repo::DatabaseError if conn.dead?
|
|
297
|
+
|
|
298
|
+
# Result: expected branch the caller handles
|
|
299
|
+
result = ChargeCard.call(card)
|
|
300
|
+
if result.success?
|
|
301
|
+
render :receipt
|
|
302
|
+
else
|
|
303
|
+
render :declined, locals: { reason: result.error }
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Result/Either object design, `.success?`/`.failure?` shapes, and the `dry-monads` approach are covered in **See references/oo-design.md**. Testing that code raises (`raise_error` matcher, etc.) is in **See references/testing.md**.
|
|
308
|
+
|
|
309
|
+
## Warnings & deprecations
|
|
310
|
+
|
|
311
|
+
`warn` writes to `$stderr` (suppressed by `-W0` / `$VERBOSE = nil`). Use it for non-fatal advisories.
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
warn "[MyLib] #{old} is deprecated; use #{new}", category: :deprecated
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`category: :deprecated` (Ruby 3.0+) lets users filter: `Warning[:deprecated] = false` silences deprecation warnings globally. Gate noisy ones behind it.
|
|
318
|
+
|
|
319
|
+
Intercept/route warnings (e.g. to your logger, or to fail tests on warnings) via the `Warning` module:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
module Warning
|
|
323
|
+
def self.warn(msg, category: nil)
|
|
324
|
+
Rails.logger.warn(msg) # or: raise in test env to surface them
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Rails: prefer `ActiveSupport::Deprecation` instances for library-style deprecations so behavior, horizon, and silencing are configurable:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
DEPRECATOR = ActiveSupport::Deprecation.new("2.0", "MyGem")
|
|
333
|
+
def old_api(*) = DEPRECATOR.warn("old_api is deprecated; use new_api") || new_api(*)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
# Type checking (optional)
|
|
339
|
+
|
|
340
|
+
Ruby is dynamically typed; static typing is **opt-in** and additive. Two ecosystems: **RBS + Steep** (official, separate sig files) and **Sorbet** (inline `sig`, runtime + static). They don't mix per-file; pick one per project.
|
|
341
|
+
|
|
342
|
+
## RBS + Steep
|
|
343
|
+
|
|
344
|
+
RBS is Ruby's standard type-signature language. Signatures live in separate `.rbs` files (typically under `sig/`). `steep` is the type checker; `rbs collection` manages third-party signatures.
|
|
345
|
+
|
|
346
|
+
```rbs
|
|
347
|
+
# sig/billing.rbs
|
|
348
|
+
module Billing
|
|
349
|
+
class PaymentDeclined < StandardError
|
|
350
|
+
attr_reader code: Integer
|
|
351
|
+
attr_reader gateway_ref: String
|
|
352
|
+
def initialize: (code: Integer, gateway_ref: String, ?message: String?) -> void
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def self.charge: (Card) -> Result
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# Steepfile
|
|
361
|
+
target :app do
|
|
362
|
+
signature "sig"
|
|
363
|
+
check "lib"
|
|
364
|
+
# library "json", "logger" # pull in stdlib sigs
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
gem install rbs steep
|
|
370
|
+
rbs collection init # creates rbs_collection.yaml (gem_rbs_collection)
|
|
371
|
+
rbs collection install # vendors third-party .rbs into .gem_rbs_collection
|
|
372
|
+
steep check # type-check the project
|
|
373
|
+
rbs prototype rb lib/x.rb # scaffold an initial .rbs from existing code
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Pros: official, no runtime cost, no source pollution, gradual. Cons: signatures live apart from code (can drift), tooling/editor support less mature than Sorbet, inference is weaker.
|
|
377
|
+
|
|
378
|
+
## Inline RBS comments (Ruby 3.x)
|
|
379
|
+
|
|
380
|
+
Ruby 3.x (with RBS 3.x / Steep) supports type annotations as **special comments** next to the code, so signatures sit beside implementation without a separate file:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
# @rbs name: String
|
|
384
|
+
# @rbs return: Integer
|
|
385
|
+
def length_of(name)
|
|
386
|
+
name.length
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
xs = [] #: Array[String]
|
|
390
|
+
config = fetch #: Config
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
This is a pragmatic middle ground: keeps types near code while staying valid Ruby (they're comments). Steep reads them. Good for incremental adoption.
|
|
394
|
+
|
|
395
|
+
## Sorbet
|
|
396
|
+
|
|
397
|
+
Sorbet uses inline `sig` blocks plus `T.*` helpers, and checks both **statically** (`srb tc`) and **at runtime** (the `sig` enforces types when the method runs, raising `TypeError` on violation).
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
# typed: true
|
|
401
|
+
require "sorbet-runtime"
|
|
402
|
+
|
|
403
|
+
class Box
|
|
404
|
+
extend T::Sig
|
|
405
|
+
|
|
406
|
+
sig { params(value: Integer).returns(String) }
|
|
407
|
+
def label(value)
|
|
408
|
+
"##{value}"
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
sig { void }
|
|
412
|
+
def initialize
|
|
413
|
+
@items = T.let([], T::Array[String]) # declare ivar type
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
gem install sorbet sorbet-runtime
|
|
420
|
+
srb init # generates sorbet/ + RBI files for gems
|
|
421
|
+
srb tc # static type check
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- `# typed: false | true | strict | strong` sigil per file controls strictness.
|
|
425
|
+
- `T.let`, `T.cast`, `T.must` (assert non-nil), `T.nilable(X)`, `T.any(A, B)`, `T.untyped`.
|
|
426
|
+
- RBI files (`.rbi`) describe gems/Rails; `tapioca` generates them.
|
|
427
|
+
|
|
428
|
+
Pros: mature IDE support, runtime enforcement catches violations in tests, strong inference. Cons: runtime overhead from `sig` checks, source is more verbose / Sorbet-specific, RBI maintenance, `T.untyped` escape hatches erode guarantees.
|
|
429
|
+
|
|
430
|
+
## When typing pays off — tradeoffs
|
|
431
|
+
|
|
432
|
+
Worth it:
|
|
433
|
+
- Large/long-lived codebases and libraries with many callers (signatures = enforced docs).
|
|
434
|
+
- Public gem APIs — ship `.rbs` so consumers get checking.
|
|
435
|
+
- Refactors across big surfaces; catching `nil` and arity errors before runtime.
|
|
436
|
+
|
|
437
|
+
Skip / defer:
|
|
438
|
+
- Small scripts, spikes, short-lived code — overhead outweighs benefit.
|
|
439
|
+
- Highly metaprogrammed code (`method_missing`, dynamic `define_method`) — hard to type; needs manual sigs and often `T.untyped`. See references/metaprogramming.md.
|
|
440
|
+
|
|
441
|
+
Guidance: start `# typed: false`/loose, type the **boundaries** (public methods, models, service `#call`) first, tighten incrementally. Don't let `T.untyped`/`T.must` proliferate — each one is an unchecked hole. Types complement tests; they don't replace them (See references/testing.md).
|
|
442
|
+
|
|
443
|
+
## Quick checklist
|
|
444
|
+
|
|
445
|
+
- `rescue` (bare) == `rescue StandardError`. **Never `rescue Exception`** in normal code.
|
|
446
|
+
- Rescue the **most specific** class you can actually handle; let the rest propagate.
|
|
447
|
+
- `raise Class, "message"` is the default form; raise classes, not strings, in libraries.
|
|
448
|
+
- Give each library one base error subclassing `StandardError`; keep the hierarchy shallow.
|
|
449
|
+
- Put structured detail in `attr_reader`s; keep `#message` human-readable.
|
|
450
|
+
- Always `super(message)` in custom error `initialize`.
|
|
451
|
+
- Cap `retry` with a max-attempt count and exponential backoff + jitter; only retry transient errors.
|
|
452
|
+
- Never `return` from `ensure`. Use `else` for the happy path.
|
|
453
|
+
- Prefer block-closing APIs (`File.open { }`, `transaction { }`) over manual `ensure`.
|
|
454
|
+
- Avoid `expr rescue fallback` for anything but narrow, intentional fallbacks; prefer `Integer(s, exception: false)` & friends.
|
|
455
|
+
- Re-raising inside `rescue` sets `#cause` automatically — wrap low-level errors in your type without losing the root.
|
|
456
|
+
- Log with `e.full_message`; it includes the cause chain.
|
|
457
|
+
- Use `fail`? No — prefer `raise` everywhere.
|
|
458
|
+
- Exceptions for the unexpected; Result objects for expected, branchable failures (See references/oo-design.md).
|
|
459
|
+
- `warn(..., category: :deprecated)`; route via `Warning.warn`; use `ActiveSupport::Deprecation` in Rails.
|
|
460
|
+
- Typing: RBS+Steep (official, separate sigs, inline `#:` comments) or Sorbet (`sig`, runtime+static). Type boundaries first; minimize `T.untyped`.
|