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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +25 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/rubydojo/application.css +738 -0
- data/app/controllers/rubydojo/application_controller.rb +4 -0
- data/app/controllers/rubydojo/dashboard_controller.rb +13 -0
- data/app/controllers/rubydojo/lessons_controller.rb +99 -0
- data/app/helpers/rubydojo/application_helper.rb +4 -0
- data/app/models/rubydojo/application_record.rb +5 -0
- data/app/views/layouts/rubydojo/application.html.erb +61 -0
- data/app/views/rubydojo/dashboard/index.html.erb +56 -0
- data/app/views/rubydojo/lessons/show.html.erb +220 -0
- data/config/routes.rb +7 -0
- data/lib/rubydojo/engine.rb +5 -0
- data/lib/rubydojo/lessons.rb +736 -0
- data/lib/rubydojo/version.rb +3 -0
- data/lib/rubydojo.rb +7 -0
- data/lib/tasks/rubydojo_tasks.rake +4 -0
- metadata +82 -0
|
@@ -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
|