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,339 @@
|
|
|
1
|
+
# Metaprogramming & DSLs
|
|
2
|
+
|
|
3
|
+
Metaprogramming is Ruby code that defines or alters code at runtime. It is powerful and abused often. **Default to plain Ruby.** Reach for these tools only when they delete real, repeated duplication that no normal abstraction (method, module, value object) can. Every dynamic method must be **documented and tested** — it is invisible to `grep`, LSP, and the next reader.
|
|
4
|
+
|
|
5
|
+
See `references/oo-design.md` for when a value/service object beats a DSL, and `references/language-idioms.md` for blocks/procs/Enumerable basics this file assumes.
|
|
6
|
+
|
|
7
|
+
## The object model
|
|
8
|
+
|
|
9
|
+
Three facts drive everything below:
|
|
10
|
+
|
|
11
|
+
1. **Classes are objects** (instances of `Class`). `class Foo; end` is sugar for assigning a `Class` instance to constant `Foo`.
|
|
12
|
+
2. **Every object has a singleton class** (a.k.a. eigenclass / metaclass) holding methods unique to that one object. "Class methods" are just instance methods on the class's singleton class.
|
|
13
|
+
3. **Method lookup walks `ancestors`** left to right: singleton class → prepended modules → the class → included modules → superclass (recursively) → `BasicObject`.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
class A; end
|
|
17
|
+
module M; end
|
|
18
|
+
class B < A; include M; end
|
|
19
|
+
|
|
20
|
+
B.ancestors # => [B, M, A, Object, Kernel, BasicObject]
|
|
21
|
+
B.singleton_class.ancestors.first(3)
|
|
22
|
+
# => [#<Class:B>, #<Class:A>, #<Class:Object>] (class-method lookup chain)
|
|
23
|
+
|
|
24
|
+
obj = Object.new
|
|
25
|
+
def obj.greet = "hi" # defines on obj's singleton class
|
|
26
|
+
obj.singleton_class.instance_methods(false) # => [:greet]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### include / prepend / extend + super
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
module Logged
|
|
33
|
+
def save
|
|
34
|
+
puts "before"
|
|
35
|
+
r = super # calls the next save in ancestors
|
|
36
|
+
puts "after"
|
|
37
|
+
r
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Doc
|
|
42
|
+
prepend Logged # Logged sits BEFORE Doc -> its #save wins, super hits Doc#save
|
|
43
|
+
def save = :saved
|
|
44
|
+
end
|
|
45
|
+
Doc.new.save # before / after / => :saved
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `include M` — inserts `M` **after** the class in ancestors (instance methods, overridable by the class).
|
|
49
|
+
- `prepend M` — inserts `M` **before** the class; ideal for wrapping/decorating an existing method via `super` (the modern replacement for `alias_method` chains).
|
|
50
|
+
- `extend M` — adds `M`'s methods to a single object's singleton class. `obj.extend(M)`; at class level `extend M` makes M's methods class methods.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# WRONG (old pattern): alias-method chaining is fragile and order-dependent
|
|
54
|
+
alias_method :save_without_log, :save
|
|
55
|
+
def save; log; save_without_log; end
|
|
56
|
+
|
|
57
|
+
# RIGHT: prepend a module and call super
|
|
58
|
+
prepend Logged
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## define_method — dynamic method generation
|
|
62
|
+
|
|
63
|
+
Use when you'd otherwise copy-paste near-identical methods. The block is a closure (captures surrounding scope), unlike `def`.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class Settings
|
|
67
|
+
%i[host port timeout].each do |name|
|
|
68
|
+
define_method(name) { @config[name] }
|
|
69
|
+
define_method("#{name}=") { |v| @config[name] = v }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Prefer `define_method` over `class_eval("def ...")` with string interpolation: it is faster to define, safer (no string injection), and shows in backtraces. Only use string `class_eval` if you need the absolute fastest *call-time* method and have profiled it (see `references/performance.md`).
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Acceptable string form when call-time speed is proven-critical & names are trusted constants:
|
|
78
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
79
|
+
def #{name}; @#{name}; end
|
|
80
|
+
RUBY
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Always pass `__FILE__, __LINE__ + 1` to string evals so backtraces point at the real source.
|
|
84
|
+
|
|
85
|
+
## method_missing — always paired with respond_to_missing?
|
|
86
|
+
|
|
87
|
+
`method_missing` is the fallback when lookup fails. **Never define it without `respond_to_missing?`** — otherwise `respond_to?`, `method()`, `Symbol#to_proc`, and duck-typing checks lie about the object.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class DynamicConfig
|
|
91
|
+
def initialize(data = {}) = @data = data
|
|
92
|
+
|
|
93
|
+
def method_missing(name, *args)
|
|
94
|
+
key = name.to_s.chomp("=")
|
|
95
|
+
if name.to_s.end_with?("=")
|
|
96
|
+
@data[key] = args.first
|
|
97
|
+
elsif @data.key?(key)
|
|
98
|
+
@data[key]
|
|
99
|
+
else
|
|
100
|
+
super # let Ruby raise a proper NoMethodError
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def respond_to_missing?(name, include_private = false)
|
|
105
|
+
@data.key?(name.to_s.chomp("=")) || super
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Rules: always call `super` for the unhandled case (gives a correct `NoMethodError` with `did_you_mean`); keep the matching logic identical in both methods.
|
|
111
|
+
|
|
112
|
+
**Costs:** every missing call walks the *entire* ancestor chain before reaching `method_missing`, so it is far slower than a real method, and it defeats tooling/autocomplete. Prefer defining real methods up front:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# BETTER than method_missing when the key set is known: define them once
|
|
116
|
+
class Config
|
|
117
|
+
def self.attribute(name)
|
|
118
|
+
define_method(name) { @data[name] }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A common upgrade is to **define the method on first use** inside `method_missing`, so later calls hit a real method (define-on-miss):
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
def method_missing(name, *args, &blk)
|
|
127
|
+
if dynamic?(name)
|
|
128
|
+
self.class.define_method(name) { @data[name] } # define once
|
|
129
|
+
send(name)
|
|
130
|
+
else
|
|
131
|
+
super
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## send / public_send
|
|
137
|
+
|
|
138
|
+
`send` invokes a method by name, **including private methods**. `public_send` respects visibility — use it unless you specifically need private access.
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
public_send(action) # respects private/protected — safe default
|
|
142
|
+
send(:internal_helper) # only when you intentionally bypass privacy
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Security:** never pass unsanitized user input to `send`/`public_send` — it lets a caller invoke arbitrary methods (`destroy`, `system`, ...). Allowlist first.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# WRONG — arbitrary method invocation
|
|
149
|
+
record.public_send(params[:field])
|
|
150
|
+
|
|
151
|
+
# RIGHT — allowlist
|
|
152
|
+
ALLOWED = %w[name email created_at].freeze
|
|
153
|
+
record.public_send(field) if ALLOWED.include?(field)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
See `references/security.md`.
|
|
157
|
+
|
|
158
|
+
## Instance variables & singleton methods reflectively
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
obj.instance_variable_get(:@name) # read; returns nil if unset (no warning)
|
|
162
|
+
obj.instance_variable_set(:@name, "Ada")
|
|
163
|
+
obj.instance_variables # => [:@name]
|
|
164
|
+
|
|
165
|
+
obj.define_singleton_method(:shout) { @name.upcase } # method on this object only
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Use sparingly — reaching into another object's ivars breaks encapsulation. It is legitimate inside serializers, test setup, and the object's own metaprogramming.
|
|
169
|
+
|
|
170
|
+
## Module / Class hooks
|
|
171
|
+
|
|
172
|
+
These callbacks fire when modules/classes are used, enabling DSLs that inject both instance and class behavior.
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
module Trackable
|
|
176
|
+
def self.included(base) # fires on `include Trackable`
|
|
177
|
+
base.extend(ClassMethods) # add class-level methods
|
|
178
|
+
base.class_eval { @records = [] }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
module ClassMethods
|
|
182
|
+
def all = @records
|
|
183
|
+
def track(r) = (@records << r)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def save = self.class.track(self)
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Hooks: `included(base)`, `extended(obj)`, `prepended(base)`, `inherited(subclass)` (subclass registration), and `method_added` / `method_removed`. In Rails, prefer `ActiveSupport::Concern` which formalizes the include+extend+`included do` pattern — see `references/rails.md`.
|
|
191
|
+
|
|
192
|
+
### Anonymous classes/modules
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
klass = Class.new(StandardError) # anonymous, assign to a const to name it
|
|
196
|
+
NotFound = Class.new(StandardError) # now named "NotFound"
|
|
197
|
+
|
|
198
|
+
mod = Module.new do
|
|
199
|
+
define_method(:tag) { "x" }
|
|
200
|
+
end
|
|
201
|
+
Object.include(mod) # generate behavior at runtime
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`Class.new(Super) { ... }` and `Module.new { ... }` are the runtime-construction primitives behind many DSLs and factories.
|
|
205
|
+
|
|
206
|
+
## class_eval / instance_eval / instance_exec
|
|
207
|
+
|
|
208
|
+
- `Klass.class_eval { def foo; end }` — runs in **class context**; `def`/`define_method` add **instance** methods. Reopens a class given only a reference.
|
|
209
|
+
- `obj.instance_eval { @ivar }` — runs with `self = obj`; `def` here defines a **singleton** method. Reads/writes the object's ivars.
|
|
210
|
+
- `instance_exec(*args) { |x| ... }` — like `instance_eval` but passes arguments into the block. Essential for DSL blocks that need outside data.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
config.instance_exec(env) { |e| @url = e.fetch("URL") }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Note the asymmetry: inside `class_eval`, `def` makes instance methods but `define_method` is needed to capture closures; inside `instance_eval`, `def` makes singleton methods.
|
|
217
|
+
|
|
218
|
+
### binding
|
|
219
|
+
|
|
220
|
+
`binding` captures the local scope (variables, `self`) as a `Binding` object — used by templating (ERB) and debuggers (`binding.irb`, the `debug` gem's `binding.break`; see `references/tooling.md`).
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
require "erb"
|
|
224
|
+
name = "Ada"
|
|
225
|
+
ERB.new("Hi <%= name %>").result(binding)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Internal DSLs
|
|
229
|
+
|
|
230
|
+
Two clean styles. Prefer **explicit `define_method`/declared macros** over `method_missing` DSLs — they are greppable, autocompletable, and fail loudly on typos.
|
|
231
|
+
|
|
232
|
+
### Class-macro DSL (declarative, preferred)
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
class Mapper
|
|
236
|
+
def self.field(name, from:)
|
|
237
|
+
fields[name] = from
|
|
238
|
+
define_method(name) { @data[from] }
|
|
239
|
+
end
|
|
240
|
+
def self.fields = @fields ||= {}
|
|
241
|
+
|
|
242
|
+
def initialize(data) = @data = data
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
class UserMapper < Mapper
|
|
246
|
+
field :email, from: "email_address" # reads as configuration
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Block/builder DSL with instance_eval
|
|
251
|
+
|
|
252
|
+
Good for nested config. Beware: inside `instance_eval`, the block can't see the caller's methods/ivars (self is swapped) — pass needed data via `instance_exec`, and document that gotcha.
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
class RouteBuilder
|
|
256
|
+
def self.draw(&blk)
|
|
257
|
+
b = new
|
|
258
|
+
b.instance_eval(&blk)
|
|
259
|
+
b.routes
|
|
260
|
+
end
|
|
261
|
+
def initialize = @routes = []
|
|
262
|
+
def get(path, to:) = @routes << [:get, path, to]
|
|
263
|
+
attr_reader :routes
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
RouteBuilder.draw do
|
|
267
|
+
get "/health", to: "system#health"
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### method_missing DSL (use only when keys are open-ended)
|
|
272
|
+
|
|
273
|
+
Justified when the vocabulary is genuinely unbounded (e.g. a builder for arbitrary HTML tags). Otherwise prefer macros above.
|
|
274
|
+
|
|
275
|
+
## Introspection
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
String.instance_methods(false) # methods defined directly on String
|
|
279
|
+
obj.methods - Object.instance_methods # what this object adds
|
|
280
|
+
obj.method(:foo).source_location # ["file.rb", 12] — find dynamic defs
|
|
281
|
+
obj.respond_to?(:foo)
|
|
282
|
+
Foo.const_get(:Bar) # resolve "Foo::Bar" dynamically
|
|
283
|
+
Object.const_get("A::B::C") # const_get follows :: in a string
|
|
284
|
+
Foo.instance_method(:foo).parameters # [[:req, :x], [:key, :y]]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`source_location` is the best tool for *finding* where a dynamic method was defined — make sure your generators produce a usable one.
|
|
288
|
+
|
|
289
|
+
### ObjectSpace caveats
|
|
290
|
+
|
|
291
|
+
`ObjectSpace.each_object(SomeClass)` enumerates live instances but is **slow, GC-dependent, and disabled/limited on JRuby & TruffleRuby**. Use only for debugging/diagnostics, never in production logic. `ObjectSpace.count_objects` and `memsize_of` are useful in profiling (see `references/performance.md`).
|
|
292
|
+
|
|
293
|
+
## TracePoint
|
|
294
|
+
|
|
295
|
+
`TracePoint` hooks runtime events (`:call`, `:line`, `:raise`, `:class`). For diagnostics/instrumentation only — it is expensive and global.
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
tp = TracePoint.new(:call) { |t| puts "#{t.defined_class}##{t.method_id}" }
|
|
299
|
+
tp.enable { run_something } # active only inside the block
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Never ship TracePoint in a hot path. It powers debuggers and coverage tools, not application logic.
|
|
303
|
+
|
|
304
|
+
## Refinements vs monkey-patching
|
|
305
|
+
|
|
306
|
+
**Monkey-patching** (reopening a class globally) is a last resort: it is action-at-a-distance, can break other gems, and is invisible. If you must, do it in a clearly named file, only add (never silently override) behavior, and consider `prepend` so the original is reachable via `super`.
|
|
307
|
+
|
|
308
|
+
**Refinements** scope a patch lexically — active only in files that `using` them. Safer than global patches but have sharp edges (no dynamic dispatch into refined methods from `send` in some versions, ignored by metaprogramming, lexical-only).
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
module StringExt
|
|
312
|
+
refine String do
|
|
313
|
+
def shout = upcase + "!"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# in another file:
|
|
318
|
+
using StringExt # active only for the rest of THIS file (lexical scope)
|
|
319
|
+
"hi".shout # => "HI!"
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Guidance:
|
|
323
|
+
- **First choice:** add a method to *your own* class/module, or a helper/service object — no patching at all.
|
|
324
|
+
- **Acceptable:** a refinement for a small, localized extension of a core class within your own code.
|
|
325
|
+
- **Avoid:** global monkey-patches of stdlib/core or third-party gem internals. If unavoidable, isolate and test heavily.
|
|
326
|
+
|
|
327
|
+
## Quick checklist
|
|
328
|
+
|
|
329
|
+
- Prefer plain Ruby; use metaprogramming only to remove real, repeated duplication.
|
|
330
|
+
- Document and write tests for every dynamically defined method; ensure `source_location` works.
|
|
331
|
+
- `method_missing` ⇒ always define `respond_to_missing?` too, and `super` for the unhandled case.
|
|
332
|
+
- Prefer declarative `define_method`/class-macros over `method_missing` DSLs (greppable, autocompletable).
|
|
333
|
+
- Use `define_method` over string `class_eval`; if you must use string eval, pass `__FILE__, __LINE__ + 1`.
|
|
334
|
+
- Use `prepend` + `super` instead of `alias_method` chains to wrap methods.
|
|
335
|
+
- Use `public_send` by default; `send` only to intentionally bypass privacy.
|
|
336
|
+
- Never pass user input to `send`/`public_send`/`const_get` without an allowlist.
|
|
337
|
+
- Use `prepend`/refinements over global monkey-patching; never override core/gem internals globally.
|
|
338
|
+
- `ObjectSpace.each_object` and `TracePoint` are diagnostics-only — keep them out of production paths.
|
|
339
|
+
- In Rails, reach for `ActiveSupport::Concern` instead of hand-rolled `included`/`extend` plumbing.
|