Skip to main content

ruby

Use when writing or refactoring plain Ruby outside Rails — scripts, CLIs, gems, libraries: Enumerable chains and blocks, module mixins and value objects, Bundler/gemspec packaging, whether metaprogramming earns its keep, Minitest or RSpec, Ruby 3.4/4.0 frozen-string hygiene. NOT Rails, ActiveRecord, or ActiveSupport idioms (that is `rails`).

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

المستودع
ericrisco/rsc-harness
آخر نشاط في المصدر
٢٩ يوليو ٢٠٢٦ في ٢٢:٤٨
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٤٢
التفرعات
١١

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

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

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

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

مستكشف الملفات
6 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
ruby
description
Use when writing or refactoring plain Ruby outside Rails — scripts, CLIs, gems, libraries: Enumerable chains and blocks, module mixins and value objects, Bundler/gemspec packaging, whether metaprogramming earns its keep, Minitest or RSpec, Ruby 3.4/4.0 frozen-string hygiene. NOT Rails, ActiveRecord, or ActiveSupport idioms (that is `rails`).
tags
["ruby","gems","bundler","metaprogramming","rspec","minitest","enumerable"]
recommends
["rails","testing-web","secure-coding"]
profiles
[]
origin
risco
# Idiomatic Ruby, the language Write Ruby a senior Rubyist would sign off on: expression-oriented, block-driven, leaning on `Enumerable` and `Comparable` instead of hand-rolled loops, with `frozen_string_literal` hygiene and a real `Gemfile`/`.gemspec` when you package. This is **Ruby the language and its non-Rails ecosystem** — blocks, modules, gems, metaprogramming, Minitest/RSpec. Delegate: Rails apps, ActiveRecord models, controllers, migrations, views, Hotwire — anything that loads Rails or uses ActiveSupport/ActiveRecord idioms — to [`rails`](../rails/SKILL.md); the same task in another language to [`python`](../python/SKILL.md), [`go`](../go/SKILL.md), [`rust`](../rust/SKILL.md), [`elixir`](../elixir/SKILL.md), [`php`](../php/SKILL.md) or [`typescript`](../typescript/SKILL.md); the CI pipeline that runs your Ruby tests to [`github-actions`](../github-actions/SKILL.md) — this skill writes the tests, not the workflow. Targets **Ruby 4.0** (released 2025-12-25) on **3.4** semantics: Prism is the default parser, string literals warn on mutation without a magic comment, `it` is an implicit block param, and `Set` is now a core class. ## Mental model Internalize these before writing a line: 1. **Everything is an expression and returns a value.** `if`, `case`, blocks, method bodies all yield a value — name and return it instead of threading a flag. 2. **Everything is an object receiving messages.** `5.times`, `nil.to_a`, `"x".freeze`. There are no primitives to special-case. 3. **Reach for `Enumerable` before you write a loop.** A manual index loop in Ruby is almost always a `map`/`select`/`reduce` you have not spotted yet. 4. **Prefer returning new values to mutating in place.** Mutation is allowed but it is the exception you justify, not the default. 5. **Freeze your string literals.** New files start with `# frozen_string_literal: true`. 6. **Dynamism is the trap, not the feature.** Metaprogramming is a scalpel; if a plain method, a `Struct`, or a `Data` does the job, use that. ## Blocks, procs, lambdas, yield Blocks are the spine of Ruby. A method takes a block implicitly via `yield`, or explicitly with `&block` when it must store or forward it. ```ruby # frozen_string_literal: true # Implicit: yield runs the caller's block and returns its value. def with_timing start = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = yield [result, Process.clock_gettime(Process::CLOCK_MONOTONIC) - start] end # Explicit: capture the block as a Proc to forward or store it. def retrying(times, &block) attempts = 0 begin block.call rescue StandardError (attempts += 1) < times ? retry : raise end end ``` `proc` vs `lambda` differ in two ways that bite: | Aspect | `proc` / block | `lambda` / `->` | |--------|----------------|-----------------| | Arity | Lenient — extra args dropped, missing become `nil` | Strict — wrong count raises `ArgumentError` | | `return` | Returns from the enclosing method (can surprise) | Returns from the lambda only | Rule: use a **lambda** for a function-like value you call yourself; use a **block/proc** for the "do this with each element" pattern. For cheap accumulation prefer `each_with_object` (returns the seed) or `reduce`; use `tap` (returns the receiver) for side effects and `then` for piping a value through a transform. ```ruby # Bad: index loop building a result by mutation. result = [] (0...items.length).each { |i| result << items[i].upcase if items[i].size > 3 } # Good: intent reads top-to-bottom, no index, no pre-seeded array. result = items.select { |s| s.size > 3 }.map(&:upcase) ``` ## Enumerable & Comparable Most loops map to one named method. Pick by **intent**, not by habit: | You want to… | Use | |---------------------------------------|------------------------------| | Transform every element | `map` | | Keep / drop by predicate | `select` / `reject` | | Fold into one value | `reduce` / `sum` / `min_by` | | Build a Hash or accumulate into a seed| `each_with_object` | | Count occurrences | `tally` | | Bucket by a key | `group_by` | | First match | `find` (`detect`) | | Pair index with element | `each_with_index` | Make your own classes first-class collections by including `Enumerable` and defining `each`; get `<`, `>`, `between?`, `clamp`, `sort` for free by including `Comparable` and defining `<=>`. ```ruby # frozen_string_literal: true class Playlist include Enumerable def initialize(tracks) = @tracks = tracks def each(&) = @tracks.each(&) # one method unlocks map/select/sort/... end class Version include Comparable attr_reader :major, :minor def initialize(major, minor) @major = major @minor = minor end def <=>(other) = [major, minor] <=> [other.major, other.minor] end ``` ## Modules & composition Ruby favors composition through mixins over deep inheritance. - `include M` — adds `M`'s instance methods to instances (the default mixin). - `prepend M` — inserts `M` *before* the class in the lookup chain, so `M`'s methods can wrap the originals via `super` (cleaner than aliasing for decoration). - `extend M` — adds `M`'s methods as *class/singleton* methods. Rule: reach for a **module mixin** when the same behavior is shared across unrelated classes; reserve inheritance for a genuine is-a hierarchy. Namespace with `module Foo` to avoid global constant clashes. Need to patch a class without polluting it globally? Use a **refinement** (`refine … using`) — a scoped, reversible scalpel — never a bare monkey-patch. Full refinement scoping and `prepend`-wrapping in [references/metaprogramming.md](references/metaprogramming.md). ## Value objects: Data vs Struct ```ruby # frozen_string_literal: true Point = Data.define(:x, :y) # immutable value object (Ruby 3.2+) origin = Point.new(x: 0, y: 0) # no setters; with(x: 1) returns a copy Cursor = Struct.new(:x, :y) # mutable; has setters and is Enumerable ``` | Need | Reach for | |--------------------------------------------|---------------| | Immutable value with equality + `with` | `Data.define` | | Mutable record, or array/positional access | `Struct.new` | Default to `Data` for value semantics; pick `Struct` only when you genuinely need mutation or positional/`to_a` behavior. ## Metaprogramming, sparingly Decision rule, in order: **could a plain method, a `Struct`, or a `Data` do this?** If yes, do that. Only when the method *names themselves* are dynamic (driven by data or config) does metaprogramming earn its keep. - Prefer `define_method` (defines real, introspectable methods) over `method_missing` (intercepts at call time, slower, invisible to `respond_to?` and tooling). - If you *must* use `method_missing`, you **must** also define `respond_to_missing?` — otherwise `respond_to?`, `method`, and duck-typing all lie about your object. - Never `eval` a string built from user input. ```ruby # Good: dynamic method names, but real methods that introspect and document themselves. class Config def initialize(**data) = @data = data %i[host port timeout].each do |key| define_method(key) { @data[key] } define_method("#{key}=") { |value| @data[key] = value } end end ``` Building a real DSL, `instance_eval`/`class_eval`, and the full `method_missing` + `respond_to_missing?` pairing live in [references/metaprogramming.md](references/metaprogramming.md). ## Gems & Bundler Bundler is the universal dependency manager. `bundle init` creates a `Gemfile`; `bundle install` resolves and writes `Gemfile.lock`; run project binaries with `bundle exec` so they use the locked versions. - Use the **pessimistic constraint** `~> 2.3` (allows `2.x >= 2.3`, blocks `3.0`) for libraries; pin tighter only when forced. - **Commit `Gemfile.lock` for applications** (reproducible installs); for a *library* gem, do not commit the lock — let consumers resolve. - Scaffold a gem with `bundle gem NAME`, which generates the `lib/`, `test/`/`spec/`, `.gemspec`, and Rakefile layout. ```ruby # my_gem.gemspec Gem::Specification.new do |spec| spec.name = "my_gem" spec.version = MyGem::VERSION # SemVer: MAJOR.MINOR.PATCH spec.summary = "One-line description." spec.authors = ["You"] spec.files = Dir["lib/**/*.rb"] spec.required_ruby_version = ">= 3.4" spec.add_dependency "thor", "~> 1.3" end ``` Full `bundle gem` tree, publishing flow (`gem build` / `gem push`), and Rake test tasks are in [references/gems-and-testing.md](references/gems-and-testing.md). ## Ruby 4.0 / 3.4 hygiene ```ruby # frozen_string_literal: true # without it, 3.4+ warns under -W:deprecated on mutation nums.map { it * 2 } # `it` is the implicit single block param (3.4+) seen = Set.new([1, 2, 3]) # Set is a core class in 4.0 — no `require "set"` ``` - The magic comment freezes every string literal in the file; mutating one then raises `FrozenError`. This is the cause of most "my string froze after upgrading" reports. - `it` reads cleaner than `_1` for one-arg blocks. - Ruby 4.0 ships **ZJIT** (a new JIT written in Rust, successor to YJIT) and removed `Ractor.yield`/`Ractor#take` in favor of `Ractor::Port`. ## Testing Both ship a clean path; choose by ceremony tolerance: | Framework | Pick when… | |-----------|-----------| | **Minitest** | You want the default — ships with Ruby, tiny surface, `assert`-style or spec-style, fast. | | **RSpec** | You want the expressive DSL and large ecosystem (`describe`/`context`/`it`/`expect`, `let`, mocks). | ```ruby # test/tally_test.rb (Minitest) require "minitest/autorun" require "tally" class TallyTest < Minitest::Test def test_counts_words assert_equal({ "a" => 2, "b" => 1 }, Tally.count("a b a")) end end ``` ```ruby # spec/tally_spec.rb (RSpec) RSpec.describe Tally do subject(:counts) { described_class.count("a b a") } it { is_expected.to eq("a" => 2, "b" => 1) } end ``` Run via `bundle exec rake test` (Minitest) or `bundle exec rspec` (RSpec). Fuller examples with `let`/`subject`/`context` are in [references/gems-and-testing.md](references/gems-and-testing.md). ## Linting & typing (optional layers) - **Linter:** Standard (zero-config, "no bikeshadding" wrapper) when you want one less argument; RuboCop (`.rubocop.yml`, every cop opt-in/out) when the team wants control. - **Types:** two coexisting systems — **RBS** (official signatures in separate `.rbs` files, checked by Steep) and **Sorbet** (inline `sig` blocks, with experimental inline-RBS-comment support). Add types only on a library's public surface or a hot, bug-prone core — not on throwaway scripts. RuboCop vs Standard config sits in [references/gems-and-testing.md](references/gems-and-testing.md). ## Anti-patterns | Anti-pattern | Why it bites | Do instead | |--------------|--------------|------------| | Monkey-patching a core class globally | Action-at-a-distance; collides with gems and future Ruby | A refinement (scoped) or a wrapper/helper | | `method_missing` without `respond_to_missing?` | `respond_to?`/`method`/duck-typing all lie | Pair them, or prefer `define_method` | | Mutating a frozen string literal | `FrozenError` after the magic comment | `dup` before mutating, or build a new string | | `for x in coll` loops | Leaks the loop var; un-idiomatic | `coll.each`/`map`/`select` |
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub