rubydojo 0.1.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.
@@ -0,0 +1,736 @@
1
+ module Rubydojo
2
+ class Lesson
3
+ attr_reader :id, :title, :level, :description, :explanation, :code_template, :validation_code, :hint
4
+
5
+ def initialize(id:, title:, level:, description:, explanation:, code_template:, validation_code:, hint:)
6
+ @id = id
7
+ @title = title
8
+ @level = level
9
+ @description = description
10
+ @explanation = explanation
11
+ @code_template = code_template
12
+ @validation_code = validation_code
13
+ @hint = hint
14
+ end
15
+
16
+ def self.all
17
+ @all ||= LOADED_LESSONS
18
+ end
19
+
20
+ def self.find(id)
21
+ all.find { |l| l.id.to_s == id.to_s }
22
+ end
23
+
24
+ def self.next_lesson(current_id)
25
+ current_index = all.index { |l| l.id.to_s == current_id.to_s }
26
+ return nil unless current_index && current_index < all.size - 1
27
+ all[current_index + 1]
28
+ end
29
+
30
+ LOADED_LESSONS = [
31
+ new(
32
+ id: "variables",
33
+ title: "Variables & Constants",
34
+ level: "Level 1: Ruby Essentials",
35
+ description: "Understand the backbone of Ruby storage: local variables, constants, and basic methods.",
36
+ explanation: <<~MARKDOWN,
37
+ # Variables & Constants in Ruby
38
+
39
+ In Ruby, variables are dynamically typed. This means you do not need to declare their data types (like `int` or `String`). Ruby determines the type at runtime.
40
+
41
+ Here are **5 detailed points** you must master regarding variables, constants, and scope in Ruby:
42
+
43
+ ### 1. Local Variables & Naming Conventions
44
+ Local variables are the most common variable type. They are defined without a keyword (unlike `var` or `let` in JS) and must start with a lowercase letter or an underscore `_`.
45
+ * **Naming Convention**: Ruby uses `snake_case` for local variables.
46
+ * **Scope**: A local variable's scope is strictly bounded by the local context in which it is defined: a method body, block, class definition, or loop. It does *not* leak outside these scopes.
47
+ ```ruby
48
+ user_age = 25
49
+ _temp_val = "cached"
50
+ ```
51
+
52
+ ### 2. Instance Variables (`@variables`)
53
+ Instance variables represent the **state** of an object. They are prefixed with a single `@` sign.
54
+ * **Scope**: They are accessible across all instance methods of a class instance.
55
+ * **Defaults**: If you reference an uninitialized instance variable, Ruby returns `nil` instead of raising an error!
56
+ * **Rails Context**: In Rails, controllers set instance variables (e.g. `@users = User.all`) which are then automatically shared with your view files (like `.html.erb`).
57
+ ```ruby
58
+ class User
59
+ def set_name(name)
60
+ @name = name # Accessible anywhere in this object instance
61
+ end
62
+ end
63
+ ```
64
+
65
+ ### 3. Class Variables (`@@variables`)
66
+ Class variables are prefixed with two `@` signs.
67
+ * **Scope**: They are shared across the class itself, all instances of the class, and all subclasses of the class.
68
+ * **Warning**: Because they are shared across subclass hierarchies, a change in a subclass will modify the value in the parent class. In modern Ruby, they are generally avoided in favor of class instance variables (`@variable` defined directly inside the class definition).
69
+ ```ruby
70
+ class Application
71
+ @@connection_count = 0 # Shared across all subclasses and instances
72
+ end
73
+ ```
74
+
75
+ ### 4. Global Variables (`$variables`)
76
+ Global variables are prefixed with a dollar sign `$`.
77
+ * **Scope**: They are accessible from anywhere in the entire Ruby program, regardless of class or method boundaries.
78
+ * **Best Practice**: Use them very sparingly. Global mutable state makes code hard to test and debug. The most common use cases are standard system variables provided by Ruby, like `$stdout` (current output stream) or `$PROGRAM_NAME`.
79
+ ```ruby
80
+ $system_mode = :production # Accessible globally
81
+ ```
82
+
83
+ ### 5. Constants & the Constant Warning System
84
+ Constants start with a capital letter and are typically written in `ALL_UPPERCASE`.
85
+ * **Reassignment**: Unlike other languages, Ruby does *not* prevent you from modifying constants. If you change a constant, Ruby will execute the assignment but emit a warning in the console: `warning: already initialized constant...`.
86
+ * **Lookup**: Constants are lexically looked up through the nesting of classes and modules.
87
+ ```ruby
88
+ PI = 3.14159
89
+ PI = 3.0 # Works, but warns "warning: already initialized constant PI"
90
+ ```
91
+
92
+ ---
93
+
94
+ ### Let's Practice!
95
+ Your goal is to:
96
+ 1. Define a local variable `age` and set it to `25`.
97
+ 2. Define a constant `PLANET` and set it to `"Earth"`.
98
+ 3. Define a method `greet(name)` that returns `"Hello, #{name}!"`. (Note: Ruby methods implicitly return the last evaluated expression!)
99
+ MARKDOWN
100
+ code_template: <<~RUBY,
101
+ # 1. Define local variable age below
102
+ age =
103
+
104
+ # 2. Define constant PLANET below
105
+ PLANET =
106
+
107
+ # 3. Define method greet(name) below
108
+ def greet(name)
109
+ # Your code here
110
+ end
111
+ RUBY
112
+ validation_code: <<~RUBY,
113
+ raise "Local variable 'age' must be defined" unless binding.local_variable_defined?(:age)
114
+ user_age = binding.local_variable_get(:age)
115
+ raise "Variable 'age' should be 25, got \#{user_age.inspect}" unless user_age == 25
116
+
117
+ raise "Constant 'PLANET' must be defined" unless self.class.const_defined?(:PLANET) || binding.eval("defined?(PLANET)")
118
+ planet_val = binding.eval("PLANET")
119
+ raise "Constant 'PLANET' should be 'Earth', got \#{planet_val.inspect}" unless planet_val == "Earth"
120
+
121
+ raise "Method 'greet' is not defined" unless respond_to?(:greet)
122
+ greet_test = greet("Jayesh")
123
+ raise "Method 'greet' should return 'Hello, Jayesh!', got \#{greet_test.inspect}" unless greet_test == "Hello, Jayesh!"
124
+ RUBY
125
+ hint: "Make sure you use double quotes for strings, and remember that methods in Ruby implicitly return the last statement."
126
+ ),
127
+
128
+ new(
129
+ id: "arrays",
130
+ title: "Collections & Arrays",
131
+ level: "Level 2: Collections",
132
+ description: "Master the structure of Ruby lists (Arrays) and how to query, append, and slice them.",
133
+ explanation: <<~MARKDOWN,
134
+ # Collections & Arrays in Ruby
135
+
136
+ Arrays are ordered, integer-indexed collections of any object. In Ruby, arrays are highly dynamic and can hold heterogeneous elements.
137
+
138
+ Here are **5 detailed points** explaining how arrays operate in Ruby:
139
+
140
+ ### 1. Dynamic Resizing & Heterogeneous Contents
141
+ Unlike compiled languages where arrays have fixed sizes and strictly typed contents (e.g. only integers), Ruby arrays grow and shrink dynamically. A single array can hold strings, floats, classes, and nested arrays simultaneously.
142
+ ```ruby
143
+ mixed = [1, "two", 3.0, [4, 5]] # Fully valid!
144
+ ```
145
+
146
+ ### 2. Indexing and Negative Offsets
147
+ Array indexes start at `0`.
148
+ * **Negative Offsets**: You can access elements from the end of the array using negative integers. `-1` is the last element, `-2` is the second-to-last, and so on.
149
+ * **Safety**: If you try to access an index out of bounds (e.g. index 100 on a 3-element array), Ruby safely returns `nil` instead of raising an index-out-of-bounds exception!
150
+ ```ruby
151
+ letters = ["a", "b", "c"]
152
+ letters[0] # => "a"
153
+ letters[-1] # => "c" (last element)
154
+ letters[100] # => nil (no exception)
155
+ ```
156
+
157
+ ### 3. Appending and Mutating (The Shovel Operator)
158
+ To append elements to an array, you can use the shovel operator `<<` or `.push`. The shovel operator is a standard Ruby idiom.
159
+ * **Mutations**: Methods like `push`, `pop`, `shift` (removes first), and `unshift` (adds to front) modify the original array in place.
160
+ ```ruby
161
+ nums = [1, 2]
162
+ nums << 3 # nums becomes [1, 2, 3]
163
+ nums.push(4) # nums becomes [1, 2, 3, 4]
164
+ nums.pop # Returns 4, nums becomes [1, 2, 3]
165
+ ```
166
+
167
+ ### 4. Slicing with Ranges and Lengths
168
+ You can retrieve sub-sections of arrays easily:
169
+ * **Range slice**: `arr[start..end]` (inclusive range) or `arr[start...end]` (exclusive range).
170
+ * **Offset slice**: `arr[start, length]` returns `length` elements starting from `start`.
171
+ ```ruby
172
+ chars = %w[a b c d e] # => ["a", "b", "c", "d", "e"]
173
+ chars[1..3] # => ["b", "c", "d"]
174
+ chars[2, 2] # => ["c", "d"]
175
+ ```
176
+
177
+ ### 5. Array Arithmetic and Set Operations
178
+ Ruby arrays implement arithmetic operators like `+`, `-`, and `&` (intersection):
179
+ * **Concatenation (`+`)**: Combines two arrays.
180
+ * **Difference (`-`)**: Returns a copy of the first array removing elements present in the second.
181
+ * **Intersection (`&`)**: Returns elements common to both arrays (without duplicates).
182
+ ```ruby
183
+ [1, 2] + [2, 3] # => [1, 2, 2, 3]
184
+ [1, 2, 3] - [2] # => [1, 3]
185
+ [1, 2] & [2, 3] # => [2]
186
+ ```
187
+
188
+ ---
189
+
190
+ ### Let's Practice!
191
+ Your goal is to:
192
+ 1. Implement `first_and_last(arr)` which returns a new array containing only the first and last elements of the input array.
193
+ 2. Implement `add_element(arr, el)` which appends `el` to the array `arr` and returns the updated array.
194
+ MARKDOWN
195
+ code_template: <<~RUBY,
196
+ def first_and_last(arr)
197
+ # Return a new array with the first and last element of 'arr'
198
+ end
199
+
200
+ def add_element(arr, el)
201
+ # Append 'el' to array 'arr' and return it
202
+ end
203
+ RUBY
204
+ validation_code: <<~RUBY,
205
+ raise "Method 'first_and_last' is not defined" unless respond_to?(:first_and_last)
206
+ test_arr1 = [10, 20, 30, 40]
207
+ res1 = first_and_last(test_arr1)
208
+ raise "first_and_last([10, 20, 30, 40]) should return [10, 40], got \#{res1.inspect}" unless res1 == [10, 40]
209
+
210
+ raise "Method 'add_element' is not defined" unless respond_to?(:add_element)
211
+ test_arr2 = ["a", "b"]
212
+ res2 = add_element(test_arr2, "c")
213
+ raise "add_element(['a', 'b'], 'c') should return ['a', 'b', 'c'], got \#{res2.inspect}" unless res2 == ["a", "b", "c"]
214
+ RUBY
215
+ hint: "You can access the first element with arr[0] or arr.first, and the last with arr[-1] or arr.last. Use << to append."
216
+ ),
217
+
218
+ new(
219
+ id: "iterators",
220
+ title: "Enumerables & Iterators",
221
+ level: "Level 2: Collections",
222
+ description: "Learn how to use blocks to iterate, map, select, and reduce collections in a powerful way.",
223
+ explanation: <<~MARKDOWN,
224
+ # Enumerables & Iterators in Ruby
225
+
226
+ The `Enumerable` mixin is one of Ruby's crown jewels. It provides a suite of traversal and search methods. Any class that implements the `each` method can mix in `Enumerable` to get these powers for free.
227
+
228
+ Here are **5 detailed points** on using Enumerables like a senior Rubyist:
229
+
230
+ ### 1. Side Effects vs Transformation (`each` vs `map`)
231
+ * **`each`**: Used when you want to execute code for its *side effects* (like saving to a database, printing, or modifying global state). It returns the **original** array.
232
+ * **`map` (or `collect`)**: Used when you want to *transform* every element. It yields each item to the block and returns a **new array** containing the block's return values.
233
+ ```ruby
234
+ [1, 2].each { |x| puts x } # Prints 1, 2. Returns [1, 2]
235
+ [1, 2].map { |x| x * 2 } # Returns [2, 4]
236
+ ```
237
+
238
+ ### 2. Filtering Collections (`select` vs `reject`)
239
+ * **`select` (or `find_all`)**: Filters a collection by keeping elements that evaluate to `true` inside the block.
240
+ * **`reject`**: The inverse of `select`. It discards elements that evaluate to `true` (keeping the false/nil ones).
241
+ ```ruby
242
+ numbers = [1, 2, 3, 4]
243
+ numbers.select { |n| n.even? } # => [2, 4]
244
+ numbers.reject { |n| n.even? } # => [1, 3]
245
+ ```
246
+
247
+ ### 3. Searching Collections (`find` / `detect`)
248
+ If you only need a single element, use `find` (aliased as `detect`). It runs through the array and returns the **first** element that makes the block true, then stops searching immediately!
249
+ ```ruby
250
+ [1, 3, 4, 5].find { |n| n.even? } # => 4
251
+ ```
252
+
253
+ ### 4. Reduction & Aggregation (`reduce` / `inject`)
254
+ `reduce` (aliased as `inject`) combines all elements of a collection by applying a binary operation, passed as a block or symbol.
255
+ * **How it works**: It maintains an accumulator. For each element, it yields the accumulator and the element to the block, updating the accumulator with the block's return value.
256
+ ```ruby
257
+ # Sum array (starting accumulator at 0)
258
+ sum = [1, 2, 3].reduce(0) { |acc, n| acc + n } # => 6
259
+
260
+ # Shortcut passing symbol
261
+ sum = [1, 2, 3].reduce(:+) # => 6
262
+ ```
263
+
264
+ ### 5. The Symbol-to-Proc Shorthand (`&:method`)
265
+ When you want to call a single method on every element, you can use the shorthand symbol-to-proc syntax `&:method_name`.
266
+ * **How it works**: Under the hood, this converts the symbol to a block that sends the method call to each yielded object.
267
+ ```ruby
268
+ # Long form
269
+ %w[alice bob].map { |name| name.upcase } # => ["ALICE", "BOB"]
270
+ # Short form
271
+ %w[alice bob].map(&:upcase) # => ["ALICE", "BOB"]
272
+ ```
273
+
274
+ ---
275
+
276
+ ### Let's Practice!
277
+ Your goal is to:
278
+ 1. Implement `double_numbers(numbers)` to return a new array with all numbers doubled, using `.map`.
279
+ 2. Implement `filter_even(numbers)` to return only the even numbers, using `.select`.
280
+ MARKDOWN
281
+ code_template: <<~RUBY,
282
+ def double_numbers(numbers)
283
+ # Use map to double all elements
284
+ end
285
+
286
+ def filter_even(numbers)
287
+ # Use select to filter even elements
288
+ end
289
+ RUBY
290
+ validation_code: <<~RUBY,
291
+ raise "Method 'double_numbers' is not defined" unless respond_to?(:double_numbers)
292
+ res1 = double_numbers([2, 5, 10])
293
+ raise "double_numbers([2, 5, 10]) should return [4, 10, 20], got \#{res1.inspect}" unless res1 == [4, 10, 20]
294
+
295
+ raise "Method 'filter_even' is not defined" unless respond_to?(:filter_even)
296
+ res2 = filter_even([1, 2, 3, 4, 5, 6])
297
+ raise "filter_even([1, 2, 3, 4, 5, 6]) should return [2, 4, 6], got \#{res2.inspect}" unless res2 == [2, 4, 6]
298
+ RUBY
299
+ hint: "Remember to call map on the numbers array inside double_numbers: `numbers.map { |n| ... }`."
300
+ ),
301
+
302
+ new(
303
+ id: "classes",
304
+ title: "Object-Oriented Ruby",
305
+ level: "Level 3: OOP",
306
+ description: "Understand classes, instance variables, initializers, and attribute readers/writers.",
307
+ explanation: <<~MARKDOWN,
308
+ # Object-Oriented Ruby
309
+
310
+ Ruby is a pure Object-Oriented language: **everything** is an object. Numbers (like `5`), strings, nil, and classes themselves are all fully instantiated objects.
311
+
312
+ Here are **5 detailed points** explaining OOP architecture in Ruby:
313
+
314
+ ### 1. Classes & Instantiation (`.new` & `initialize`)
315
+ To create objects, you write a blueprint using the `class` keyword.
316
+ * **Instantiating**: You call `Class.new` (e.g. `User.new`), which allocates memory and automatically triggers the constructor method `initialize`.
317
+ * **Arguments**: Any arguments passed to `.new` are passed directly to `initialize`.
318
+ ```ruby
319
+ class User
320
+ def initialize(username)
321
+ @username = username
322
+ end
323
+ end
324
+ ```
325
+
326
+ ### 2. Encapsulation & Instance Variable Scope
327
+ In Ruby, instance variables (`@variables`) are **strictly encapsulated** (private by default). They cannot be accessed or modified from outside the object instance directly. To access them, you must write getter and setter methods.
328
+ ```ruby
329
+ class User
330
+ def name; @name; end # Getter
331
+ def name=(val); @name = val; end # Setter
332
+ end
333
+ ```
334
+
335
+ ### 3. Attribute Accessor Shortcuts (`attr_reader`, `attr_writer`, `attr_accessor`)
336
+ Writing getters and setters is tedious. Ruby provides class macros to generate them dynamically:
337
+ - `attr_reader :name` generates the getter `def name; @name; end`.
338
+ - `attr_writer :name` generates the setter `def name=(val); @name = val; end`.
339
+ - `attr_accessor :name` generates both getter and setter.
340
+ ```ruby
341
+ class User
342
+ attr_accessor :name, :age # Creates getters and setters for both
343
+ end
344
+ ```
345
+
346
+ ### 4. Single Inheritance & Overriding (`super`)
347
+ Ruby supports **single inheritance**. A class can inherit state and behavior from a single parent class using the `<` symbol.
348
+ * **`super`**: You can override a parent method and call the parent's version using the `super` keyword.
349
+ ```ruby
350
+ class Admin < User
351
+ def initialize(name, role)
352
+ super(name) # Calls User#initialize
353
+ @role = role
354
+ end
355
+ end
356
+ ```
357
+
358
+ ### 5. Polymorphism & Duck Typing
359
+ Ruby does not use interfaces or strict static typing. It uses **Duck Typing**: *"If it walks like a duck and quacks like a duck, we treat it as a duck."*
360
+ * **Message Passing**: You can call any method on any object as long as that object responds to that method at runtime. This keeps object relationships highly flexible.
361
+ ```ruby
362
+ # We don't care about the type; we only care that 'logger' responds to 'log'
363
+ def save_data(data, logger)
364
+ logger.log("Saving data...") if logger.respond_to?(:log)
365
+ end
366
+ ```
367
+
368
+ ---
369
+
370
+ ### Let's Practice!
371
+ Your goal is to:
372
+ 1. Create a `Developer` class.
373
+ 2. Give it `attr_accessor` for `name` and `role`.
374
+ 3. Implement an `initialize(name, role)` method to assign these.
375
+ 4. Implement a `greet` method that returns the string `"Hello, I am [name] and I work as a [role]!"`.
376
+ MARKDOWN
377
+ code_template: <<~RUBY,
378
+ class Developer
379
+ # Write your class definition here
380
+ end
381
+ RUBY
382
+ validation_code: <<~RUBY,
383
+ raise "Class Developer is not defined" unless defined?(Developer) && Developer.is_a?(Class)
384
+ dev = Developer.new("Jayesh", "Rails Intern")
385
+ raise "Developer name is not readable" unless dev.respond_to?(:name)
386
+ raise "Developer name is not writable" unless dev.respond_to?(:name=)
387
+ raise "Developer role is not readable" unless dev.respond_to?(:role)
388
+
389
+ raise "Developer name should be 'Jayesh', got \#{dev.name.inspect}" unless dev.name == "Jayesh"
390
+ raise "Developer role should be 'Rails Intern', got \#{dev.role.inspect}" unless dev.role == "Rails Intern"
391
+
392
+ dev.name = "Alex"
393
+ raise "greet method not defined" unless dev.respond_to?(:greet)
394
+ greeting = dev.greet
395
+ expected = "Hello, I am Alex and I work as a Rails Intern!"
396
+ raise "greet should return '\#{expected}', got \#{greeting.inspect}" unless greeting == expected
397
+ RUBY
398
+ hint: "Don't forget to use `attr_accessor :name, :role` at the top of your class, and interpolate variables in your greeting: `\"Hello, I am \#{name}...\"`."
399
+ ),
400
+
401
+ new(
402
+ id: "blocks_procs_lambdas",
403
+ title: "Blocks, Procs & Lambdas",
404
+ level: "Level 3: OOP & Functions",
405
+ description: "Understand closures in Ruby: how blocks yield code, and the differences between Procs and Lambdas.",
406
+ explanation: <<~MARKDOWN,
407
+ # Blocks, Procs & Lambdas
408
+
409
+ Ruby is famous for its powerful implementation of closures. Let's look at how they work and how they differ from each other.
410
+
411
+ Here are **5 detailed points** to master Ruby closures:
412
+
413
+ ### 1. What is a Block? (Anonymous Closures)
414
+ A block is a chunk of code wrapped in `do...end` or curly braces `{...}`. Blocks are not objects; they cannot be assigned to variables. They are passed to methods implicitly as arguments.
415
+ ```ruby
416
+ [1, 2].each { |x| puts x } # The block is { |x| puts x }
417
+ ```
418
+
419
+ ### 2. Yielding Control (`yield` and `block_given?`)
420
+ Inside a method, you can execute a block using the `yield` keyword.
421
+ * **Yield Params**: Any arguments passed to `yield` are forwarded to the block parameters.
422
+ * **Safety**: If you use `yield` without passing a block, Ruby raises a `LocalJumpError`. Check for a block first using `block_given?`.
423
+ ```ruby
424
+ def run_twice
425
+ if block_given?
426
+ yield("First")
427
+ yield("Second")
428
+ end
429
+ end
430
+ run_twice { |step| puts "\#{step} run" }
431
+ ```
432
+
433
+ ### 3. Procs (First-Class Block Objects)
434
+ A `Proc` (short for procedure) is a block of code saved into an object.
435
+ * **First-Class**: Because they are objects, you can save Procs in variables, store them in arrays, and pass them as method arguments.
436
+ * **Calling**: You execute a Proc using the `.call` method or the square bracket shortcut `[]`.
437
+ ```ruby
438
+ say_hi = Proc.new { |name| "Hi \#{name}" }
439
+ say_hi.call("Alex") # => "Hi Alex"
440
+ say_hi["Alex"] # => "Hi Alex"
441
+ ```
442
+
443
+ ### 4. Lambdas (Strict Procs)
444
+ Lambdas are a sub-species of Procs defined using `lambda { ... }` or the stabby lambda syntax `->(args) { ... }`.
445
+ * **Strict Arguments**: Lambdas check the number of arguments passed to them. If you pass the wrong number, it raises `ArgumentError`. Procs ignore extra arguments and bind missing ones to `nil`.
446
+ ```ruby
447
+ my_lambda = ->(a, b) { a + b }
448
+ my_lambda.call(1) # Raises ArgumentError!
449
+
450
+ my_proc = Proc.new { |a, b| a.to_i + b.to_i }
451
+ my_proc.call(1) # Works (b becomes nil, converted to 0)
452
+ ```
453
+
454
+ ### 5. Control Flow: Return Behavior
455
+ The most critical difference between a Proc and a Lambda is how the `return` keyword behaves:
456
+ * **Lambdas**: A `return` inside a lambda returns control out of the **lambda itself** (acting like a normal function return).
457
+ * **Procs**: A `return` inside a Proc returns from the **method/scope that defined the Proc**. If defined outside a method, it raises a `LocalJumpError`.
458
+ ```ruby
459
+ def proc_test
460
+ p = Proc.new { return "proc exit" }
461
+ p.call
462
+ "method exit" # This line is NEVER reached!
463
+ end
464
+
465
+ def lambda_test
466
+ l = -> { return "lambda exit" }
467
+ l.call
468
+ "method exit" # This line IS reached!
469
+ end
470
+ ```
471
+
472
+ ---
473
+
474
+ ### Let's Practice!
475
+ Your goal is to:
476
+ 1. Implement a method `run_twice` that yields to a block exactly twice, but *only* if a block is given.
477
+ 2. Create a Lambda called `format_usd` (using `->` or `lambda`) that takes a number and returns a string starting with `$` and displaying 2 decimal places (e.g. `25` -> `"$25.00"`, `5.5` -> `"$5.50"`).
478
+ MARKDOWN
479
+ code_template: <<~RUBY,
480
+ def run_twice
481
+ # Yield to block twice if given
482
+ end
483
+
484
+ # Define format_usd lambda below
485
+ format_usd =
486
+ RUBY
487
+ validation_code: <<~RUBY,
488
+ raise "Method 'run_twice' is not defined" unless respond_to?(:run_twice)
489
+ counter = 0
490
+ run_twice { counter += 1 }
491
+ raise "run_twice should execute the block twice, counted: \#{counter}" unless counter == 2
492
+
493
+ # Test if run_twice handles no block given case
494
+ begin
495
+ run_twice
496
+ rescue LocalJumpError => e
497
+ raise "run_twice should check block_given? before yielding, otherwise it raises LocalJumpError!"
498
+ end
499
+
500
+ raise "format_usd is not defined" unless binding.local_variable_defined?(:format_usd) || binding.eval("defined?(format_usd)")
501
+ fmt = binding.eval("format_usd")
502
+ raise "format_usd should be a Lambda or Proc" unless fmt.is_a?(Proc)
503
+
504
+ # Check return format
505
+ res1 = fmt.call(10)
506
+ raise "format_usd(10) should return '$10.00', got \#{res1.inspect}" unless res1 == "$10.00"
507
+ res2 = fmt.call(4.5)
508
+ raise "format_usd(4.5) should return '$4.50', got \#{res2.inspect}" unless res2 == "$4.50"
509
+ RUBY
510
+ hint: "To format a float with 2 decimal places in Ruby, use string formatting: `'%.2f' % value`."
511
+ ),
512
+
513
+ new(
514
+ id: "metaprogramming",
515
+ title: "Metaprogramming Basics",
516
+ level: "Level 4: Advanced Ruby",
517
+ description: "Learn Ruby's magical powers: calling methods dynamically using send, and defining them using define_method.",
518
+ explanation: <<~MARKDOWN,
519
+ # Metaprogramming Basics in Ruby
520
+
521
+ Metaprogramming is writing code that writes code dynamically at runtime. It is the core magic behind Rails features like dynamic model scopes, association helpers, and controllers.
522
+
523
+ Here are **5 detailed points** on the mechanics of Ruby metaprogramming:
524
+
525
+ ### 1. Dynamic Method Dispatch (`send`)
526
+ In Ruby, calling a method is actually "sending a message" to an object. You can send a message dynamically by passing the method's name as a symbol or string to `.send`.
527
+ * **Bypassing encapsulation**: `.send` bypasses private access control, allowing you to call private methods! If you want to respect private methods, use `.public_send`.
528
+ ```ruby
529
+ user = User.new("Bob")
530
+ method_name = :name
531
+ user.send(method_name) # Equivalent to user.name
532
+ ```
533
+
534
+ ### 2. Dynamic Methods (`define_method`)
535
+ You can define methods programmatically using `define_method` inside a class body.
536
+ * **Scope**: It takes a method name symbol and a block which is executed whenever the defined method is called.
537
+ * **Rails Context**: Rails uses this to create active record attribute getters/setters based on database columns at startup.
538
+ ```ruby
539
+ class Device
540
+ [:on, :off].each do |status|
541
+ define_method("turn_\#{status}") do
542
+ "Device is \#{status.to_s.upcase}"
543
+ end
544
+ end
545
+ end
546
+ ```
547
+
548
+ ### 3. Dynamic Introspection (`respond_to?`)
549
+ Because Ruby is highly dynamic, objects can gain and lose methods at runtime.
550
+ * **Safety**: Before sending a dynamic message, check if the object supports it using `.respond_to?(method_name_symbol)`.
551
+ ```ruby
552
+ device = Device.new
553
+ device.respond_to?(:turn_on) # => true
554
+ ```
555
+
556
+ ### 4. Intercepting Undefined Messages (`method_missing`)
557
+ If an object receives a message it doesn't recognize, Ruby calls its `method_missing` method. By overriding this, you can catch undefined calls and handle them dynamically.
558
+ * **Rails Context**: This is how dynamic finders like `User.find_by_name_and_email` are implemented! Rails intercepts the call, parses the method name, and runs the query.
559
+ ```ruby
560
+ class CatchAll
561
+ def method_missing(method_name, *args, &block)
562
+ "You tried to call \#{method_name} with \#{args}"
563
+ end
564
+
565
+ # Good practice: Always override respond_to_missing? when overriding method_missing
566
+ def respond_to_missing?(method_name, include_private = false)
567
+ true
568
+ end
569
+ end
570
+ ```
571
+
572
+ ### 5. Open Classes (Monkey Patching)
573
+ In Ruby, class definitions are not closed. You can reopen any existing class (even standard libraries like `String` or `Integer`) at runtime and inject new methods.
574
+ * **Warning**: While powerful, this can lead to conflicts if two libraries attempt to define the same method name.
575
+ ```ruby
576
+ class String
577
+ def loud
578
+ self.upcase + "!!!"
579
+ end
580
+ end
581
+ "hello".loud # => "HELLO!!!"
582
+ ```
583
+
584
+ ---
585
+
586
+ ### Let's Practice!
587
+ Your goal is to:
588
+ 1. Complete the `SmartDevice` class by dynamically defining two methods: `turn_on` (returns `"Device is ON"`) and `turn_off` (returns `"Device is OFF"`), using `define_method` inside a loop.
589
+ 2. Complete the `call_turn_on(device)` method to call the `turn_on` method on the `device` object dynamically using `send`.
590
+ MARKDOWN
591
+ code_template: <<~RUBY,
592
+ class SmartDevice
593
+ # Loop over status values and dynamically define turn_on and turn_off
594
+ [:on, :off].each do |status|
595
+ # define_method(...) do ... end
596
+ end
597
+ end
598
+
599
+ def call_turn_on(device)
600
+ # Call 'turn_on' dynamically using send
601
+ end
602
+ RUBY
603
+ validation_code: <<~RUBY,
604
+ raise "Class SmartDevice is not defined" unless defined?(SmartDevice) && SmartDevice.is_a?(Class)
605
+ device = SmartDevice.new
606
+ raise "turn_on method should be defined on SmartDevice" unless device.respond_to?(:turn_on)
607
+ raise "turn_off method should be defined on SmartDevice" unless device.respond_to?(:turn_off)
608
+
609
+ val_on = device.turn_on
610
+ val_off = device.turn_off
611
+ raise "turn_on should return 'Device is ON', got \#{val_on.inspect}" unless val_on == "Device is ON"
612
+ raise "turn_off should return 'Device is OFF', got \#{val_off.inspect}" unless val_off == "Device is OFF"
613
+
614
+ raise "Method 'call_turn_on' is not defined" unless respond_to?(:call_turn_on)
615
+ res = call_turn_on(device)
616
+ raise "call_turn_on(device) should return 'Device is ON', got \#{res.inspect}" unless res == "Device is ON"
617
+ RUBY
618
+ hint: "Inside define_method, status will be :on or :off. You can convert it to string and format it: `'Device is ' + status.to_s.upcase`."
619
+ ),
620
+
621
+ new(
622
+ id: "rails_idioms",
623
+ title: "Rails Ruby Idioms & ActiveSupport",
624
+ level: "Level 5: Ruby in Rails",
625
+ description: "Learn how Rails extends Ruby under the hood and master core ActiveSupport helpers like blank?, presence, and Safe Navigation.",
626
+ explanation: <<~MARKDOWN,
627
+ # Rails Ruby Idioms & ActiveSupport
628
+
629
+ ActiveSupport is the component of Ruby on Rails that extends the Ruby standard library with convenient methods, string transformations, and helper tools.
630
+
631
+ Here are **5 detailed points** explaining the most common Ruby idioms inside Rails:
632
+
633
+ ### 1. Object Evaluation: `.blank?` vs `.present?`
634
+ Rails extends `Object` with presence query methods to replace complex checks:
635
+ * **`.blank?`**: Returns `true` if an object is `nil`, `false`, an empty string, an empty array/hash, or a string containing only whitespace.
636
+ * **`.present?`**: The exact opposite of `.blank?`.
637
+ ```ruby
638
+ # Without Rails:
639
+ if name.nil? || name.strip.empty?
640
+ # With Rails:
641
+ if name.blank?
642
+ ```
643
+
644
+ ### 2. Streamlining Defaults using `.presence`
645
+ The `.presence` helper returns the object itself if it is `.present?`, otherwise it returns `nil`. This is highly useful for cleaning params and setting clean fallbacks using the `||` operator.
646
+ ```ruby
647
+ # Without .presence:
648
+ display_name = params[:name].present? ? params[:name] : "Guest"
649
+
650
+ # With .presence:
651
+ display_name = params[:name].presence || "Guest"
652
+ ```
653
+
654
+ ### 3. Safe Navigation Operator (`&.`)
655
+ Introduced in Ruby 2.3, the Safe Navigation Operator `&.` prevents your code from throwing a `NoMethodError: undefined method ... for nil:NilClass` when querying properties on objects that might be `nil`.
656
+ * **How it works**: If the receiver is `nil`, it skips the method call and returns `nil` safely.
657
+ ```ruby
658
+ # Without Safe Navigation (crashes if user is nil):
659
+ user.profile.name
660
+
661
+ # With Safe Navigation (safely returns nil if user or profile is nil):
662
+ user&.profile&.name
663
+ ```
664
+
665
+ ### 4. ActiveSupport String Inflections
666
+ Rails extends the `String` class with inflection helpers to handle grammatical pluralization and code-style formatting:
667
+ * **Pluralization**: `.pluralize`, `.singularize`.
668
+ * **Naming Transformations**: `.camelize`, `.underscore`, `.classify`.
669
+ ```ruby
670
+ "developer".pluralize # => "developers"
671
+ "admin_user".camelize # => "AdminUser"
672
+ "AdminUser".underscore # => "admin_user"
673
+ ```
674
+
675
+ ### 5. Declaring Method Delegation (`delegate`)
676
+ Rails provides a class macro `delegate` to implement the Law of Demeter, allowing you to delegate method calls from one object to an associated object automatically.
677
+ * **Cleaner calls**: Keeps code clean by avoiding long chaining like `user.profile.zipcode` in favor of `user.zipcode`.
678
+ ```ruby
679
+ class User < ApplicationRecord
680
+ has_one :profile
681
+ # Automatically defines User#zipcode to fetch it from profile
682
+ delegate :zipcode, to: :profile, allow_nil: true
683
+ end
684
+ ```
685
+
686
+ ---
687
+
688
+ ### Let's Practice!
689
+ Your goal is to:
690
+ 1. Implement `clean_params(params)` that returns the parameter hash itself if it is `.present?`, otherwise returns `nil`. (Hint: use `.presence`).
691
+ 2. Implement `safe_user_name(user)` that safely queries a user object:
692
+ - If the user is present and has a name (which is not blank), return the name converted to uppercase.
693
+ - If the user is nil or their name is blank/nil, return `"ANONYMOUS"`.
694
+ - Use safe navigation `&.` and `.blank?` to write clean, crash-proof code.
695
+ MARKDOWN
696
+ code_template: <<~RUBY,
697
+ def clean_params(params)
698
+ # Use ActiveSupport's .presence helper
699
+ end
700
+
701
+ def safe_user_name(user)
702
+ # Safely return user's uppercase name or "ANONYMOUS"
703
+ end
704
+ RUBY
705
+ validation_code: <<~RUBY,
706
+ raise "Method 'clean_params' is not defined" unless respond_to?(:clean_params)
707
+ raise "clean_params should return nil for empty hash" unless clean_params({}).nil?
708
+ raise "clean_params should return nil for nil" unless clean_params(nil).nil?
709
+ raise "clean_params should return the parameters if present" unless clean_params({ name: "Jayesh" }) == { name: "Jayesh" }
710
+
711
+ raise "Method 'safe_user_name' is not defined" unless respond_to?(:safe_user_name)
712
+
713
+ # Create mock user struct
714
+ UserMock = Struct.new(:name)
715
+ u1 = UserMock.new("Jayesh")
716
+ u2 = UserMock.new("")
717
+ u3 = UserMock.new(nil)
718
+ u4 = nil
719
+
720
+ res1 = safe_user_name(u1)
721
+ raise "safe_user_name(u1) should return 'JAYESH', got \#{res1.inspect}" unless res1 == "JAYESH"
722
+
723
+ res2 = safe_user_name(u2)
724
+ raise "safe_user_name(u2) should return 'ANONYMOUS', got \#{res2.inspect}" unless res2 == "ANONYMOUS"
725
+
726
+ res3 = safe_user_name(u3)
727
+ raise "safe_user_name(u3) should return 'ANONYMOUS', got \#{res3.inspect}" unless res3 == "ANONYMOUS"
728
+
729
+ res4 = safe_user_name(u4)
730
+ raise "safe_user_name(u4) should return 'ANONYMOUS', got \#{res4.inspect}" unless res4 == "ANONYMOUS"
731
+ RUBY
732
+ hint: "Remember: `user&.name` returns nil safely if user is nil. You can check if the name is blank with `&.blank?`."
733
+ )
734
+ ]
735
+ end
736
+ end