Skip to main content

loom-ruby

Ruby language expertise for idiomatic, production-quality code.

معلومات المصدر

المستودع
cosmix/loom
آخر نشاط في المصدر
٢٥ سبتمبر ٢٠٢٦ في ٠٦:٠٠
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦١
التفرعات
٤

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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.
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub