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,465 @@
|
|
|
1
|
+
# Ruby language & core idioms
|
|
2
|
+
|
|
3
|
+
Modern Ruby (3.2–3.4) fundamentals. Dense, idiomatic, do/don't. For metaprogramming see `references/metaprogramming.md`; OO design see `references/oo-design.md`; exceptions see `references/errors-and-types.md`; concurrency see `references/concurrency.md`.
|
|
4
|
+
|
|
5
|
+
## Truthiness & nil
|
|
6
|
+
|
|
7
|
+
Only `false` and `nil` are falsey. `0`, `""`, `[]`, `{}` are all **truthy**.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
if count # true even when count == 0
|
|
11
|
+
if count.positive? # what you usually meant
|
|
12
|
+
if list.any? # not `if list` — empty array is truthy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Distinguish "missing" from "falsey value". `||` collapses both `nil` and `false`; use `nil?` / `key?` when `false` is a legitimate value.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
flag = opts[:enabled] || true # WRONG: explicit false becomes true
|
|
19
|
+
flag = opts.fetch(:enabled, true) # RIGHT: only defaults when key absent
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Safe navigation `&.`
|
|
23
|
+
|
|
24
|
+
`&.` short-circuits on `nil` only. It does **not** guard against other return values.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
user&.address&.city # nil if any link is nil
|
|
28
|
+
config&.fetch(:host) # still raises if config present but key missing
|
|
29
|
+
|
|
30
|
+
# DON'T chain &. to paper over a design smell (Law of Demeter — see oo-design.md).
|
|
31
|
+
# DON'T mix with ||: `a &. b || c` parses as `(a&.b) || c` — usually fine, but be explicit.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`&.` differs from `try` (Rails): `&.` calls the method and raises NoMethodError if the receiver is non-nil but doesn't respond; `try` swallows that. Prefer `&.`.
|
|
35
|
+
|
|
36
|
+
### `||=`, `&&=`
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
@cache ||= compute # memoize (NOT thread-safe — see performance.md / concurrency.md)
|
|
40
|
+
h[:k] ||= [] # default-then-append; for hashes prefer Hash.new { |h,k| h[k] = [] }
|
|
41
|
+
config &&= config.dup # reassign only if already truthy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`||=` on a falsey-but-valid value recomputes every time. If `compute` can return `false`/`nil`, memoize with a sentinel:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
@result = compute unless defined?(@result)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Symbols vs strings
|
|
51
|
+
|
|
52
|
+
Symbols are immutable, interned, identity-comparable — use them as **identifiers/keys**. Strings are for **data/text**.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
status == :active # state/identifier -> symbol
|
|
56
|
+
record.name == "Acme" # human data -> string
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Don't `to_sym` untrusted/unbounded input in long-lived processes (symbols from user input are GC'd since 2.2, but still avoid for clarity/security — see security.md).
|
|
60
|
+
|
|
61
|
+
### Frozen string literals
|
|
62
|
+
|
|
63
|
+
Put this **magic comment** on line 1 of every file (it must be the first line, or after `#!`):
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# frozen_string_literal: true
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
All string literals in the file become frozen → fewer allocations (see performance.md), and accidental mutation raises. When you need a mutable buffer, allocate explicitly:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
buf = +"" # unary + = dup'd, mutable string
|
|
73
|
+
buf << "a" << "b"
|
|
74
|
+
name = -"active" # unary - = frozen/deduplicated
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### String building
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# DON'T build with repeated + in a loop (O(n²) allocations)
|
|
81
|
+
out = ""; items.each { |i| out = out + i.to_s } # BAD
|
|
82
|
+
|
|
83
|
+
# DO use << (mutating) or join
|
|
84
|
+
out = +""; items.each { |i| out << i.to_s }
|
|
85
|
+
out = items.map(&:to_s).join(", ")
|
|
86
|
+
out = "#{name} (#{count})" # interpolation > concatenation
|
|
87
|
+
out = format("%.2f%%", pct) # format/sprintf for padding/precision
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`<<` mutates left operand; `+` allocates a new string. Heredocs with `<<~` strip leading indentation:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
sql = <<~SQL
|
|
94
|
+
SELECT *
|
|
95
|
+
FROM users
|
|
96
|
+
SQL
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Method arguments
|
|
100
|
+
|
|
101
|
+
### Keyword arguments (Ruby 3.x keeps them fully separate from positionals)
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
def connect(host:, port: 5432, **opts) # host required, port optional, rest in opts
|
|
105
|
+
...
|
|
106
|
+
end
|
|
107
|
+
connect(host: "db", timeout: 5) # timeout lands in opts
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- `name:` (no default) = required keyword.
|
|
111
|
+
- `name: default` = optional.
|
|
112
|
+
- `**opts` = collect extra keywords; `**nil` forbids any keywords.
|
|
113
|
+
- Prefer keyword args when a method takes 3+ params or any boolean flag (avoids mystery `true, false, nil` call sites).
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
render(partial, true, false) # DON'T: unreadable
|
|
117
|
+
render(partial, layout: true, cache: false) # DO
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Splatting a hash into keywords needs explicit `**` in 3.x:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
opts = { host: "db", port: 5432 }
|
|
124
|
+
connect(**opts) # required; bare `connect(opts)` raises ArgumentError
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Argument forwarding `...`
|
|
128
|
+
|
|
129
|
+
Forward all args (positional, keyword, block, and 3.2+ anonymous splats) verbatim:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
def log_and_call(...)
|
|
133
|
+
logger.info("calling")
|
|
134
|
+
target.call(...)
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Ruby 3.2 also allows anonymous `*`, `**`, `&` forwarding:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
def wrap(*, **, &) = inner(*, **, &)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Blocks, procs, lambdas
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
[1,2,3].each { |n| puts n } # block — not an object, passed implicitly
|
|
148
|
+
sq = ->(n) { n * n } # lambda (stabby)
|
|
149
|
+
pr = proc { |n| n * n } # proc
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Return semantics (the key difference)
|
|
153
|
+
|
|
154
|
+
- **lambda**: `return` returns from the lambda; strict arity.
|
|
155
|
+
- **proc / block**: `return` returns from the **enclosing method**; lenient arity (missing args → nil, extra ignored).
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
def find_first(list)
|
|
159
|
+
list.each { |x| return x if x.positive? } # block return -> exits find_first ✔
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def m
|
|
163
|
+
p = proc { return 42 }
|
|
164
|
+
p.call # returns from m
|
|
165
|
+
99 # never reached
|
|
166
|
+
end
|
|
167
|
+
# DON'T store a proc and call it later expecting local return — it LocalJumpErrors once m has returned.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Prefer lambdas for stored callables (predictable return + arity checking).
|
|
171
|
+
|
|
172
|
+
### yield, block_given?, &block
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
def each_pair
|
|
176
|
+
return enum_for(:each_pair) unless block_given? # return Enumerator if no block
|
|
177
|
+
yield :a, 1
|
|
178
|
+
yield :b, 2
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def with_capture(&block) # capture only when you must pass it on / store it
|
|
182
|
+
block.call(self)
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`yield` is faster than `&block` + `block.call` (no Proc allocation). Capture with `&block` only when forwarding or storing it.
|
|
187
|
+
|
|
188
|
+
### Symbol#to_proc and method references
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
%w[a b].map(&:upcase) # &:sym -> ->(x){ x.upcase }
|
|
192
|
+
[1,-2].map(&:abs)
|
|
193
|
+
nums.map(&method(:format_row)) # method object as block
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Enumerable toolbox — pick the right tool
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
map # transform 1:1
|
|
200
|
+
flat_map # map then flatten one level (map+flatten -> flat_map)
|
|
201
|
+
filter_map # map + compact + select in one pass (3.x); great for "transform then drop nils"
|
|
202
|
+
select / filter # keep matching reject # drop matching
|
|
203
|
+
each_with_object # build a collection; returns the object (not the block value)
|
|
204
|
+
reduce / inject # fold to single value
|
|
205
|
+
sum # numeric/string fold (sum(0.0) to start as float)
|
|
206
|
+
tally # frequency Hash {elem => count}
|
|
207
|
+
group_by # Hash {key => [elems]}
|
|
208
|
+
partition # [matching, non_matching]
|
|
209
|
+
chunk_while / slice_when # split into runs by adjacent-pair predicate
|
|
210
|
+
each_slice(n) # fixed-size batches each_cons(n) # sliding windows
|
|
211
|
+
zip # interleave/pair parallel collections
|
|
212
|
+
min_by/max_by/sort_by/minmax_by # by derived key (computed once via Schwartzian)
|
|
213
|
+
find / detect # first match
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# filter_map vs map+compact
|
|
218
|
+
emails = users.filter_map { |u| u.email if u.active? } # one pass ✔
|
|
219
|
+
emails = users.map { |u| u.email if u.active? }.compact # two passes ✗
|
|
220
|
+
|
|
221
|
+
# each_with_object vs reduce for building
|
|
222
|
+
index = items.each_with_object({}) { |i, h| h[i.id] = i } # clean
|
|
223
|
+
index = items.reduce({}) { |h, i| h[i.id] = i; h } # must return h — error-prone
|
|
224
|
+
|
|
225
|
+
# counting -> tally, not manual hash
|
|
226
|
+
%w[a b a].tally # {"a"=>2, "b"=>1}
|
|
227
|
+
|
|
228
|
+
# grouped sums
|
|
229
|
+
totals = orders.group_by(&:user_id).transform_values { |os| os.sum(&:amount) }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`sort_by`/`max_by` compute the key once per element — prefer over `sort { |a,b| f(a) <=> f(b) }` when the key is expensive. Use `sort`/`<=>` block only for multi-key or mixed-direction sorts:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
people.sort_by { |p| [p.last, p.first] } # multi-key ascending
|
|
236
|
+
people.sort_by { |p| [-p.age, p.name] } # age desc, name asc (negate numeric key)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Lazy enumerators (large/infinite/streaming)
|
|
240
|
+
|
|
241
|
+
`lazy` defers and pipelines per-element — no giant intermediate arrays. Essential for files and infinite ranges.
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
(1..Float::INFINITY).lazy.select(&:even?).first(5) # [2,4,6,8,10]
|
|
245
|
+
|
|
246
|
+
File.foreach("huge.log").lazy
|
|
247
|
+
.map(&:chomp)
|
|
248
|
+
.select { |l| l.include?("ERROR") }
|
|
249
|
+
.first(100) # stops reading after 100 matches
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Call a terminal op (`first`, `to_a`, `force`, `each`) to materialize. DON'T `.lazy` short in-memory arrays — overhead outweighs benefit.
|
|
253
|
+
|
|
254
|
+
## Comparable & Enumerable mixins on your own classes
|
|
255
|
+
|
|
256
|
+
Define `<=>` (returns -1/0/1/nil) and include `Comparable` to get `< <= == > >= between? clamp`:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
class Version
|
|
260
|
+
include Comparable
|
|
261
|
+
attr_reader :parts
|
|
262
|
+
def initialize(str) = @parts = str.split(".").map(&:to_i)
|
|
263
|
+
def <=>(other) = parts <=> other.parts # Array#<=> compares elementwise
|
|
264
|
+
end
|
|
265
|
+
Version.new("1.2.0") < Version.new("1.10.0") # => true
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Define `each` and include `Enumerable` to get the whole toolbox above:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
class Roster
|
|
272
|
+
include Enumerable
|
|
273
|
+
def initialize(members) = @members = members
|
|
274
|
+
def each(&block) = @members.each(&block) # yield each element
|
|
275
|
+
end
|
|
276
|
+
Roster.new(people).map(&:name).sort # map/select/sort_by all work now
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Pattern matching (`case/in`)
|
|
280
|
+
|
|
281
|
+
Structural matching with binding. `in` raises `NoMatchingPatternError` if nothing matches (use `else` to handle). Use `case/in` for shape; `case/when` for simple equality.
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
case response
|
|
285
|
+
in { status: 200, body: } # binds body
|
|
286
|
+
body
|
|
287
|
+
in { status: 404 }
|
|
288
|
+
raise NotFound
|
|
289
|
+
in { status: Integer => code } if code >= 500
|
|
290
|
+
retry_later(code)
|
|
291
|
+
else
|
|
292
|
+
raise "unexpected"
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Array, find, and alternative patterns:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
case command
|
|
300
|
+
in [:move, Integer => x, Integer => y] # array pattern, type + bind
|
|
301
|
+
move(x, y)
|
|
302
|
+
in [:say, *words] # splat captures rest
|
|
303
|
+
say(words.join(" "))
|
|
304
|
+
in [*, {error:}, *] # find pattern: locate elem anywhere
|
|
305
|
+
fail error
|
|
306
|
+
in :start | :resume # alternative pattern
|
|
307
|
+
begin!
|
|
308
|
+
end
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
One-line `=>` (rightward assignment / destructuring) and `in` as a boolean test:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
config => { host:, port: } # raises if no match; binds host, port
|
|
315
|
+
record in { id: Integer } # => true/false, no raise
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### deconstruct / deconstruct_keys
|
|
319
|
+
|
|
320
|
+
Make your objects matchable. Array patterns call `deconstruct`; hash patterns call `deconstruct_keys(keys)`. `Data` and `Struct` implement both automatically.
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
Point = Data.define(:x, :y)
|
|
324
|
+
case Point.new(1, 2)
|
|
325
|
+
in [x, y] then ... # via deconstruct
|
|
326
|
+
in { x:, y: } then ... # via deconstruct_keys
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Struct vs Data.define
|
|
331
|
+
|
|
332
|
+
Use **`Data.define`** (Ruby 3.2+) for immutable value objects — the modern default. Use `Struct` only when you need mutability or backward compat.
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
Point = Data.define(:x, :y) do
|
|
336
|
+
def dist = Math.hypot(x, y)
|
|
337
|
+
end
|
|
338
|
+
p = Point.new(x: 1, y: 2) # or Point.new(1, 2)
|
|
339
|
+
p.with(y: 9) # returns a NEW Point (copy-with-change)
|
|
340
|
+
p.x = 0 # NoMethodError — frozen, no setters ✔
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
- `Data`: immutable, no setters, value `==`, `deconstruct`/`deconstruct_keys`, `#with`. Ideal for DTOs/value objects (see oo-design.md).
|
|
344
|
+
- `Struct`: mutable (`s.x = 1`), positional **or** `keyword_init: true`. Pick one and be consistent.
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
Mutable = Struct.new(:x, :y, keyword_init: true) # always pass keyword_init explicitly
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
DON'T use `OpenStruct` — slow, defeats method-missing safety, allocations galore.
|
|
351
|
+
|
|
352
|
+
## Hash idioms
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
h.fetch(:k) # raises KeyError if missing — use when key MUST exist
|
|
356
|
+
h.fetch(:k, default) # default value
|
|
357
|
+
h.fetch(:k) { expensive } # block form — default computed only if needed
|
|
358
|
+
h[:k] # returns nil if missing (can't tell missing from nil value)
|
|
359
|
+
|
|
360
|
+
h.dig(:a, :b, :c) # safe nested access; nil if any level missing
|
|
361
|
+
data.dig(:users, 0, :name) # works across Hash/Array
|
|
362
|
+
|
|
363
|
+
Hash.new(0) # default value 0 (SHARED — don't use mutable default!)
|
|
364
|
+
Hash.new { |hash, key| hash[key] = [] } # default BLOCK — fresh array per key ✔
|
|
365
|
+
|
|
366
|
+
counts = Hash.new(0); words.each { |w| counts[w] += 1 } # or just words.tally
|
|
367
|
+
|
|
368
|
+
h.transform_values { |v| v * 2 }
|
|
369
|
+
h.transform_keys(&:to_sym)
|
|
370
|
+
h.filter_map { |k, v| [k, v] if v } # works on hashes too
|
|
371
|
+
h.slice(:a, :b) / h.except(:c)
|
|
372
|
+
h1.merge(h2) { |key, old, new| old + new } # block resolves conflicts
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
Hash.new([]).tap { |h| h[:a] << 1 } # BUG: every key shares ONE array
|
|
377
|
+
Hash.new { |h,k| h[k] = [] } # correct
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Syntax niceties
|
|
381
|
+
|
|
382
|
+
### Endless methods (Ruby 3.0+)
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
def square(n) = n * n
|
|
386
|
+
def full_name = "#{first} #{last}"
|
|
387
|
+
def active? = status == :active
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Use for true one-liners (no `begin`/multi-statement). Keep the `= expr` on one logical line.
|
|
391
|
+
|
|
392
|
+
### Numbered & `it` block params
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
[1,2,3].map { _1 * 2 } # _1.._9 numbered params (2.7+)
|
|
396
|
+
pairs.each { puts "#{_1}=#{_2}" }
|
|
397
|
+
[1,2,3].map { it * 2 } # `it` = single implicit param (Ruby 3.4)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Prefer named params for anything non-trivial or nested (you can't use `_1` across nesting levels cleanly). `it`/`_1` shine for short single-arg blocks.
|
|
401
|
+
|
|
402
|
+
### Range tricks
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
(1..5) # inclusive (1...5) # exclusive end
|
|
406
|
+
(1..) # beginless/endless ranges
|
|
407
|
+
arr[2..] # from index 2 to end
|
|
408
|
+
arr[..3] # up to index 3
|
|
409
|
+
("a".."e").to_a
|
|
410
|
+
(1..10).step(2).to_a
|
|
411
|
+
case score
|
|
412
|
+
in 90.. then "A" # endless range in pattern
|
|
413
|
+
in 80...90 then "B"
|
|
414
|
+
end
|
|
415
|
+
(Time.now..).cover?(t) # ranges as predicates via cover?/include?
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Numbers & money
|
|
419
|
+
|
|
420
|
+
`Float` is binary floating point — **never** use it for money or exact decimals.
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
0.1 + 0.2 == 0.3 # => false (0.30000000000000004)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Use `BigDecimal` (from `require "bigdecimal"`/`"bigdecimal/util"`) for currency:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
require "bigdecimal"
|
|
430
|
+
require "bigdecimal/util"
|
|
431
|
+
|
|
432
|
+
price = "19.99".to_d # BigDecimal, exact
|
|
433
|
+
total = price * 3 # 59.97 exact
|
|
434
|
+
BigDecimal("0.1") + BigDecimal("0.2") == BigDecimal("0.3") # => true
|
|
435
|
+
|
|
436
|
+
# DON'T construct BigDecimal from a Float — you inherit the float error:
|
|
437
|
+
BigDecimal(0.1, 10) # BAD-ish; use the string: BigDecimal("0.1")
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Better: store money as **integer cents** and format on display. `Rational` gives exact fractions; `Integer` is arbitrary precision (no overflow). Divide carefully:
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
7 / 2 # => 3 (integer division, truncates)
|
|
444
|
+
7.0 / 2 # => 3.5
|
|
445
|
+
7.fdiv(2) # => 3.5 (explicit float division)
|
|
446
|
+
Rational(7, 2) # => (7/2) exact
|
|
447
|
+
(0.30 * 100).round # float rounding lands you in trouble; round BigDecimal/cents instead
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
For rounding modes use `BigDecimal#round(2, :half_up)` etc.; default Ruby `Float#round` is round-half-to-even-ish — be explicit for financial math.
|
|
451
|
+
|
|
452
|
+
## Quick checklist
|
|
453
|
+
|
|
454
|
+
- Add `# frozen_string_literal: true` to line 1 of every file; build with `<<`/`join`, allocate mutable with `+""`.
|
|
455
|
+
- Only `nil`/`false` are falsey; use `fetch` (not `||`) when `false`/`nil` is a valid value.
|
|
456
|
+
- Symbols = identifiers/keys; strings = data. Don't `to_sym` untrusted input.
|
|
457
|
+
- Keyword args for 3+ params or any boolean flag; splat hashes with `**`; forward with `...`.
|
|
458
|
+
- Lambdas for stored callables (strict arity, local `return`); `yield` over `&block` unless you must capture.
|
|
459
|
+
- `filter_map`, `each_with_object`, `tally`, `group_by`, `sort_by`/`min_by`/`max_by` over manual loops; `.lazy` for huge/infinite/streamed data.
|
|
460
|
+
- `include Comparable` (+`<=>`) and `include Enumerable` (+`each`) to enrich your own classes.
|
|
461
|
+
- `case/in` for structure; bind with `{ key: }`, `=>`, guards; `else` to avoid NoMatchingPatternError.
|
|
462
|
+
- `Data.define` for immutable value objects; `Struct` only when mutable; never `OpenStruct`.
|
|
463
|
+
- `fetch`/`dig`/`transform_values`; `Hash.new { |h,k| h[k] = [] }` (block, not shared mutable default).
|
|
464
|
+
- Endless `def x = ...` for one-liners; `it`/`_1` for short single-arg blocks.
|
|
465
|
+
- Money: `BigDecimal("...")` from strings or integer cents — never `Float`; `fdiv`/`Rational` to avoid integer-division surprises.
|