Skip to main content

loom-ruby

Ruby language expertise for idiomatic, production-quality code.

Datos de origen

Repositorio
cosmix/loom
Última actividad en el origen
25 de septiembre de 2026 a las 06:00
Idioma detectado de SKILL.md
inglés
Estrellas
61
Forks
4

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
loom-ruby
description
Ruby language expertise for idiomatic, production-quality code.
triggers
["ruby","rb","bundler","gem","gemfile","gemspec","rake","rails","activerecord","activejob","rspec","minitest","rubocop","standardrb","sorbet","rbs","sidekiq","puma","sinatra","yjit"]
# Ruby Language Expertise ## Overview Idiomatic, production-grade Ruby 3.x: Bundler, the Ruby 3 language additions, blocks and closures, Rails, and RSpec/Minitest. Assumes competence; the value is in what is easy to get wrong (keyword-argument separation, `proc` return semantics, `||=` on falsy values, N+1 queries, unsafe deserialization, the GVL) and in writing tests that loom's `rspec` and `minitest` adapters can select one at a time. ## Tooling Preserve the repository's version manager and lockfile. The Ruby version lives in `.ruby-version` (read by rbenv, chruby, asdf, mise) and in the `Gemfile`'s `ruby` directive. **Bundler** resolves dependencies and isolates the load path. | Task | Command | | --- | --- | | Add a dependency | `bundle add faraday` (edits `Gemfile`, resolves, updates `Gemfile.lock`) | | Add a dev/test dependency | `bundle add rspec --group "development, test"` | | Install from the lock | `bundle install`; set `BUNDLE_FROZEN=true` in CI so drift fails the build | | Update one gem | `bundle update --conservative faraday` (shared dependencies stay pinned) | | Run inside the bundle | `bundle exec rspec`, or the binstubs `bin/rails`, `bin/rspec` | | Known CVEs in the lock | `bundle exec bundler-audit check --update` | | Deploy platforms | `bundle lock --add-platform x86_64-linux` | - `bundle exec` activates exactly the locked versions; a bare `rspec` or `rake` may load a different installed version. `require "bundler/setup"` does the same from inside a script. - A gem declares runtime dependencies in its `.gemspec`; its `Gemfile` holds `gemspec` plus development tools. **Format and lint:** RuboCop (`rubocop -a` applies safe corrections; `-A` adds unsafe ones, so review that diff) or Standard (`standardrb --fix`, RuboCop with a fixed config). Rails 7.2+ generates `bin/rubocop` (rubocop-rails-omakase) and `bin/brakeman`. Plugins: `rubocop-rspec`, `rubocop-performance`, `rubocop-rails`. Disable a cop for a range with `# rubocop:disable Style/Foo` ... `# rubocop:enable Style/Foo`; never disable all cops for a file. **Types (gradual, optional):** RBS signatures in `sig/*.rbs` checked by Steep, or Sorbet (`# typed: strict`, `sig { params(id: Integer).returns(String) }`, `srb tc`). Follow whichever the project uses; never add a second one. **The gate:** ```bash bundle exec rubocop # or: bundle exec standardrb bundle exec srb tc # Sorbet projects only (Steep: bundle exec steep check) bundle exec rspec # or: bin/rails test, bundle exec rake test bundle exec brakeman --no-pager # Rails apps: static security scan ``` ## Ruby 3 Language Features `.ruby-version` bounds what you may use. | Version | Feature | | --- | --- | | 3.0 | Keyword arguments separated from positional hashes; `case/in` pattern matching; endless methods `def sq(x) = x * x`; Ractor (experimental); fiber scheduler interface | | 3.1 | Hash shorthand `{x:, y:}`; anonymous block forwarding `def f(&) = g(&)`; YJIT; Psych 4, so `YAML.load` is safe by default | | 3.2 | `Data.define`; anonymous `*` and `**` forwarding; `Struct.new` accepts keyword arguments without `keyword_init: true`; `Fiber[]` storage | | 3.3 | Prism parser shipped; YJIT speedups; M:N thread scheduler (opt-in) | | 3.4 | `it` as the implicit block parameter; Prism is the default parser; string literals are "chilled" (mutation warns under `-W:deprecated`); `Hash#inspect` prints `{name: "x"}` | **Keyword argument separation (3.0).** A hash passed positionally no longer becomes keywords, and keywords no longer collapse into a trailing positional hash. Code that relied on the 2.x conversion raises `ArgumentError`. Splat explicitly, and delegate with `...`. ```ruby def connect(host, port: 5432, ssl: false) = open_socket(host, port, ssl) opts = { port: 6543, ssl: true } connect("db", opts) # ArgumentError on 3.x: opts is a second positional argument connect("db", **opts) # keywords def with_logging(...) = connect(...) # forwards positionals, keywords and the block ``` **Pattern matching.** `case/in` destructures arrays, hashes and any object implementing `deconstruct`/`deconstruct_keys`. With no matching branch and no `else` it raises `NoMatchingPatternError`, so an unhandled shape fails loudly. `expr => pattern` binds or raises; `expr in pattern` returns a boolean. ```ruby case response in { status: 200, body: { items: [_, *] => items } } process(items) in { status: 404 } nil in { status: 500.. => code } raise UpstreamError, "upstream returned #{code}" end config => { database: { host: String => host } } # binds host; a missing key raises NoMatchingPatternKeyError, a non-String host NoMatchingPatternError ``` Hash patterns match a subset of keys (`**nil` forbids extra keys); array patterns match the whole array unless a `*rest` is present. Pin a local with `^expected`, an expression with `^(expr)`. **Value objects with `Data.define` (3.2).** Immutable, keyword or positional construction, value equality, `with` for modified copies, pattern-matchable. Use it where older code used `Struct` for values that should never change; keep `Struct` for the rare case where mutability is the point. ```ruby Point = Data.define(:x, :y) do def +(other) = with(x: x + other.x, y: y + other.y) end origin = Point.new(x: 0, y: 0) moved = origin.with(y: 5) # origin is unchanged Point.new(1) # ArgumentError: missing keyword: :y ``` **Frozen string literals.** Put `# frozen_string_literal: true` at the top of every file (RuboCop enforces it): literals are frozen and deduplicated, which saves allocations and turns accidental mutation into `FrozenError`. Build strings from `+""` or `String.new(capacity: n)`; `str.dup` returns an unfrozen copy. ## Blocks, Procs and Lambdas | | Block / `proc {}` | `lambda {}` / `->(x) {}` | | --- | --- | --- | | Arity | lenient: missing arguments are `nil`, extras dropped, a lone array auto-splats | strict: `ArgumentError` on mismatch | | `return` | returns from the method that defined the block | returns from the lambda | | `next` | ends this call of the block, with a value | same as `return` | - A method takes a block implicitly (`yield`, `block_given?`) or explicitly (`&block`, which allocates a `Proc`; the anonymous `&` of 3.1 forwards without naming it). - Return an `Enumerator` when no block is given, so callers can chain: `return enum_for(__method__) unless block_given?`. - `&:name` calls `Symbol#to_proc`; `&method(:parse)` passes an existing method. - Blocks capture local variables by reference: a block stored and called later sees the current value and can reassign it. - `it` (3.4) and `_1` suit one-line blocks; name the parameter when a block spans lines. - ⚠ `return` inside a `proc` that outlives its defining method raises `LocalJumpError`. Store callbacks as lambdas. ```ruby def each_batch(size: 100) return enum_for(__method__, size:) unless block_given? offset = 0 loop do rows = fetch(offset:, limit: size) break if rows.empty? yield rows offset += size end end each_batch.with_index { |rows, i| logger.info("batch #{i}: #{rows.size} rows") } present = ->(value) { value.is_a?(String) && !value.empty? } names.select(&present) # a lambda passed as the block ids = users.map { it.id } # 3.4 ``` ## Errors and Exceptions - Bare `rescue` and `rescue => e` catch `StandardError` only. Never `rescue Exception`: it also swallows `Interrupt`, `SystemExit` and `SignalException`, so Ctrl-C and `exit` stop working. - Give a library one base error and a hierarchy under it. Callers rescue the base for everything or a subclass for one case. - Raising inside a `rescue` sets `cause` automatically, so the original stays reachable as `e.cause`. - Keep the `begin` body to the call that can fail. A method body is an implicit `begin`: `def x ... rescue ... ensure ... end`. - `ensure` runs on every exit path. A `return` inside `ensure` discards the in-flight exception; never write one. - `retry` re-runs the `begin` body; always bound it with a counter. - ⚠ `Timeout.timeout` raises into the block from another thread at an arbitrary point, which can leave connections, locks and files half-updated. Use the library's own timeouts (`Net::HTTP#read_timeout`, driver `connect_timeout`, database `statement_timeout`). ```ruby module Spool class Error < StandardError; end class SymlinkError < Error; end class FetchError < Error; end end def fetch_with_retry(url, attempts: 3) tries = 0 begin tries += 1 http_get(url) rescue Net::OpenTimeout, Errno::ECONNRESET retry if tries < attempts raise Spool::FetchError, "fetch failed after #{tries} attempts: #{url}" # original is #cause end end ``` ## Concurrency - **The GVL** lets one thread run Ruby code at a time (per Ractor). Threads help IO-bound work because blocking IO and `sleep` release the GVL; they give no speedup for CPU-bound Ruby. For CPU parallelism use processes (Puma workers, Sidekiq processes, `Process.fork`) or a native extension. - **Puma** runs `workers` (processes) times `threads`. Class-level and global mutable state is shared by every thread in a worker: memoized class instance variables, `@@cache ||= {}`, a constant holding a mutable hash. Guard with `Mutex#synchronize`, use `Concurrent::Map` from concurrent-ruby, or keep the state per request. - **Fibers:** under a fiber scheduler (the `async` gem, Falcon) stdlib IO, `sleep` and `Net::HTTP` yield instead of blocking, so one thread serves many IO-bound tasks. - **Ractors** give real parallelism with isolated objects, but remain experimental and most gems are not Ractor-safe. Keep them out of application code. - **Background jobs** go through ActiveJob backends (Sidekiq, Solid Queue, GoodJob). Arguments must be serializable primitives or GlobalID records, and every backend retries, so jobs must be idempotent. ```ruby class RateCache def initialize @lock = Mutex.new @rates = {} end def fetch(currency) @lock.synchronize { @rates[currency] ||= load_rate(currency) } # check and set under one lock end end ``` ## Rails Essentials - **Autoloading (Zeitwerk):** paths must match constant names (`app/services/spool/writer.rb` defines `Spool::Writer`). Never `require` an autoloaded file; `bin/rails zeitwerk:check` validates the tree. - **N+1 queries:** `includes` lets Rails choose, `preload` runs separate queries, `eager_load` uses one `LEFT JOIN` (needed to filter on the association). `strict_loading` on a model, association or relation raises on lazy loads; the `bullet` gem reports them in development. - **Large tables:** `find_each` / `in_batches`; `Model.all.each` loads every row into memory. - **Strong parameters:** `params.expect(user: [:name, :email])` (Rails 8) or `params.require(:user).permit(:name, :email)`. Never `permit!`, and never pass raw `params` to `create`/`update`. - **Callbacks:** keep them to the model's own invariants (normalization, derived columns). `after_save` runs inside the transaction, so an email or job triggered there can run before the commit or for a row that is rolled back; use `after_commit` / `after_create_commit` or a service object for side effects. - **Bypasses:** `update_column(s)`, `update_all`, `insert_all` and `delete_all` skip validations and callbacks. `validates :email, uniqueness: true` alone races; back it with a unique index. - **Transactions:** `ActiveRecord::Base.transaction do ... end` rolls back on any exception; `raise ActiveRecord::Rollback` rolls back without propagating. - **Secrets:** `bin/rails credentials:edit` (encrypted) or environment variables; never plaintext in the repository. - **Rails 7.1+:** `normalizes`, `generates_token_for`, `authenticate_by` (timing-safe credential lookup). Rails 8 adds the authentication generator and Solid Queue/Cache/Cable defaults. ```ruby class Order < ApplicationRecord belongs_to :customer has_many :line_items, dependent: :destroy normalizes :reference, with: ->(ref) { ref.strip.upcase } validates :reference, presence: true after_create_commit -> { OrderMailer.confirmation(self).deliver_later } scope :recent, -> { where(created_at: 7.days.ago..) } end Order.recent.includes(:customer, :line_items).find_each do |order| export(order) # no per-row customer or line-item queries end ``` ## Testing **RSpec.** ```ruby # spec/spool/writer_spec.rb RSpec.describe "Spool::Writer" do subject(:writer) { Spool::Writer.new(root:) } let(:root) { Pathname(Dir.mktmpdir) } after { FileUtils.remove_entry(root) } it "writes the payload into the spool directory" do writer.write("job-1", "payload") expect(root.join("job-1").read).to eq("payload") end it "rejects a symlinked spool directory" do target = Pathname(Dir.mktmpdir) link = root.join("spool") File.symlink(target, link) expect { Spool::Writer.new(root: link).write("job-1", "payload") } .to raise_error(Spool::SymlinkError, /symlink/) expect(target.join("job-1")).not_to exist ensure FileUtils.remove_entry(target) if target end end ``` - `let` is lazy and memoized per example; `let!` runs before every example. Prefer `let`; a `let!` that nothing reads is hidden setup. - Verified doubles (`instance_double(Spool::Store, put: true)`) fail when the stubbed method does not exist on the real class or is called with the wrong arity; a plain `double` checks nothing. - Always name the class in `raise_error`: a bare `raise_error` passes on the `NoMethodError` from a typo. - `aggregate_failures` reports every failed expectation in an example instead of stopping at the first. - Run in random order (`config.order = :random` plus `Kernel.srand config.seed`); reproduce with `--seed N`; `rspec --bisect` finds the minimal order-dependent pair.
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub