risa 1.0.0 → 1.0.2
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 +4 -4
- data/CHANGELOG.md +8 -2
- data/README.md +49 -49
- data/lib/risa/version.rb +1 -1
- data/lib/risa.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c8013dc8881f7cb09ede1757d465eac8902f24efd9d7d7028ff8d7c08bb6563
|
|
4
|
+
data.tar.gz: c8754eae69616d994f370003c43e91e05b31654c08c6ec3c3549dc6e8ca07da9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15a92f5133c83ce7d4ea7e4544a6aa31f14699be0667b5ca1df762501866838ec4d02da33dfa652e09dd1a3731263c3d119de2d073c233a3c62ecf6b749bba6d
|
|
7
|
+
data.tar.gz: 1a88eb4c601cc3e573536cc0d6030e8a2d8328c98d488dc9d9ca06b05c77229c661094a6299c3f4722ab969c260af809e77b9d243eb4497b2314a013943dee2d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
# Changelog
|
|
2
3
|
|
|
3
4
|
All notable changes to this project will be documented in this file.
|
|
@@ -7,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
|
11
|
+
## [1.0.2] - 2025-11-09
|
|
12
|
+
|
|
13
|
+
### 💥 Changed
|
|
14
|
+
* Updated the `rs()` helper for the more idiomatic `all()` helper
|
|
15
|
+
|
|
10
16
|
## [1.0.1] - 2025-11-01
|
|
11
17
|
|
|
12
18
|
### 🐛 Fixed
|
|
@@ -118,7 +124,7 @@ This is the first stable 1.0 release! It introduces auto-loading and finalizes t
|
|
|
118
124
|
|
|
119
125
|
### 🐛 Fixed
|
|
120
126
|
* Fixed a `NoMethodError` when using `.order` on a key where some records had a `nil` value. Nils are now always sorted last by default.
|
|
121
|
-
* `
|
|
127
|
+
* `all(:model).first` on an empty dataset now correctly returns `nil` instead of raising an error.
|
|
122
128
|
|
|
123
129
|
## [0.1.0] - 2025-04-10
|
|
124
130
|
|
|
@@ -126,7 +132,7 @@ This is the first stable 1.0 release! It introduces auto-loading and finalizes t
|
|
|
126
132
|
* **Initial Release!**
|
|
127
133
|
* `Risa.define(:model_name)` block.
|
|
128
134
|
* `from_array([...])` data source.
|
|
129
|
-
* Global `
|
|
135
|
+
* Global `all()` helper for easy querying.
|
|
130
136
|
* Basic `.where(key: value)` for exact matching.
|
|
131
137
|
* `.order(:key)` for ascending sort.
|
|
132
138
|
* Query execution methods: `.to_a`, `.first`, `.last`, `.each`.
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Risa.define :posts do
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Query it like you always wished you could:
|
|
30
|
-
|
|
30
|
+
all(:posts).where(published: true).order(:created_at, desc: true).first[:title]
|
|
31
31
|
# => "Hello World"
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -60,7 +60,7 @@ Risa.define :posts do
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Query like you always wanted to:
|
|
63
|
-
popular_ruby_posts =
|
|
63
|
+
popular_ruby_posts = all(:posts)
|
|
64
64
|
.where(tags: { contains: 'ruby' })
|
|
65
65
|
.where(views: { greater_than: 1000 })
|
|
66
66
|
.order(:views, desc: true)
|
|
@@ -69,7 +69,7 @@ popular_ruby_posts = rs(:posts)
|
|
|
69
69
|
puts popular_ruby_posts.first[:title] # => "Why I Love Ruby"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
That global `
|
|
72
|
+
That global `all()` helper? It's there because life's too short to type `Risa.query()` every time.
|
|
73
73
|
|
|
74
74
|
---
|
|
75
75
|
|
|
@@ -152,8 +152,8 @@ Risa.define :posts do
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Now your queries read like English:
|
|
155
|
-
trending_ruby_posts =
|
|
156
|
-
featured_content =
|
|
155
|
+
trending_ruby_posts = all(:posts).published.tagged('ruby').popular.recent(5)
|
|
156
|
+
featured_content = all(:posts).published.featured.recent
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
Scopes are chainable, composable, and parameterizable. They're basically custom query methods that don't suck.
|
|
@@ -167,7 +167,7 @@ Risa provides a fluent, chainable API for filtering and sorting your data. Queri
|
|
|
167
167
|
### Basic Queries (The Classics)
|
|
168
168
|
|
|
169
169
|
```ruby
|
|
170
|
-
posts =
|
|
170
|
+
posts = all(:posts) # Get the query builder for :posts
|
|
171
171
|
|
|
172
172
|
# Get results
|
|
173
173
|
posts.to_a # Get all matching records as an array of Risa::InstanceWrapper objects
|
|
@@ -188,10 +188,10 @@ Chain multiple `.where` calls or provide multiple conditions in a hash to combin
|
|
|
188
188
|
|
|
189
189
|
```ruby
|
|
190
190
|
# Find published posts tagged 'ruby'
|
|
191
|
-
|
|
191
|
+
all(:posts).where(published: true).where(tags: { contains: 'ruby' })
|
|
192
192
|
|
|
193
193
|
# Equivalent using a single hash
|
|
194
|
-
|
|
194
|
+
all(:posts).where(published: true, tags: { contains: 'ruby' })
|
|
195
195
|
```
|
|
196
196
|
|
|
197
197
|
### OR Logic (`or_where`)
|
|
@@ -200,7 +200,7 @@ Use `.or_where` to add conditions combined with `OR`.
|
|
|
200
200
|
|
|
201
201
|
```ruby
|
|
202
202
|
# Find posts that are featured OR have more than 1000 views
|
|
203
|
-
|
|
203
|
+
all(:posts).where(featured: true).or_where(views: { greater_than: 1000 })
|
|
204
204
|
# => WHERE featured = true OR views > 1000
|
|
205
205
|
```
|
|
206
206
|
|
|
@@ -210,23 +210,23 @@ Use blocks with `where` and `or_where` to create nested logical groups. Conditio
|
|
|
210
210
|
|
|
211
211
|
```ruby
|
|
212
212
|
# Find posts where (author_id = 1 AND published = true)
|
|
213
|
-
|
|
213
|
+
all(:posts).where do |q|
|
|
214
214
|
q.where(author_id: 1).where(published: true)
|
|
215
215
|
end
|
|
216
216
|
|
|
217
217
|
# Find posts where (author_id = 1 AND (published = true OR featured = true))
|
|
218
|
-
|
|
218
|
+
all(:posts).where(author_id: 1).where do |q|
|
|
219
219
|
q.where(published: true).or_where(featured: true)
|
|
220
220
|
end
|
|
221
221
|
# => WHERE author_id = 1 AND (published = true OR featured = true)
|
|
222
222
|
|
|
223
223
|
# Find posts where (author_id = 1 AND published = true) OR (views > 1000)
|
|
224
|
-
|
|
224
|
+
all(:posts).where { |q| q.where(author_id: 1).where(published: true) }
|
|
225
225
|
.or_where(views: { greater_than: 1000 })
|
|
226
226
|
# => WHERE (author_id = 1 AND published = true) OR views > 1000
|
|
227
227
|
|
|
228
228
|
# Find posts where (author_id = 1 AND published = true) OR (author_id = 2 AND featured = true)
|
|
229
|
-
|
|
229
|
+
all(:posts).where { |q| q.where(author_id: 1).where(published: true) }
|
|
230
230
|
.or_where { |q| q.where(author_id: 2).where(featured: true) }
|
|
231
231
|
# => WHERE (author_id = 1 AND published = true) OR (author_id = 2 AND featured = true)
|
|
232
232
|
```
|
|
@@ -237,32 +237,32 @@ Use hash conditions within `where` or `or_where` for powerful comparisons and ne
|
|
|
237
237
|
|
|
238
238
|
```ruby
|
|
239
239
|
# Text searches
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
all(:posts).where(title: { contains: 'Ruby' })
|
|
241
|
+
all(:posts).where(title: { starts_with: 'How to' })
|
|
242
|
+
all(:posts).where(title: { ends_with: '101' })
|
|
243
243
|
|
|
244
244
|
# Numeric comparisons
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
all(:posts).where(views: { greater_than: 1000 })
|
|
246
|
+
all(:posts).where(views: { less_than_or_equal: 500 })
|
|
247
|
+
all(:posts).where(score: { from: 7.5, to: 9.0 }) # Inclusive range
|
|
248
|
+
all(:posts).where(views: 100..500) # Ruby Range works too
|
|
249
249
|
|
|
250
250
|
# Existence and emptiness
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
all(:posts).where(featured_image: { exists: true }) # Key is present and not nil
|
|
252
|
+
all(:posts).where(featured_image: { exists: false }) # Key is missing or nil
|
|
253
|
+
all(:posts).where(tags: { empty: false }) # Not nil, not '', not []
|
|
254
|
+
all(:posts).where(tags: { empty: true }) # Is nil, '', or []
|
|
255
255
|
|
|
256
256
|
# Array / Set operations
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
all(:posts).where(id: { in: [1, 3, 5] }) # Value is one of these
|
|
258
|
+
all(:posts).where(id: [1, 3, 5]) # Shortcut for :in
|
|
259
|
+
all(:posts).where(status: { not_in: ['draft', 'archived'] }) # Value is NOT one of these
|
|
260
|
+
all(:posts).where(tags: ['ruby', 'web']) # Exact array match (order matters)
|
|
261
261
|
|
|
262
262
|
# Negation
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
all(:posts).where(published: { not: true }) # Value is not true (false or nil)
|
|
264
|
+
all(:posts).where(title: { not: 'Hello' }) # Value is not 'Hello'
|
|
265
|
+
all(:posts).where(views: { not: nil }) # Same as { exists: true }
|
|
266
266
|
```
|
|
267
267
|
|
|
268
268
|
### Ordering (Nil-Safe and Type-Aware)
|
|
@@ -271,13 +271,13 @@ Sort your results using `.order`. Nils are always sorted last.
|
|
|
271
271
|
|
|
272
272
|
```ruby
|
|
273
273
|
# Ascending (default)
|
|
274
|
-
|
|
274
|
+
all(:posts).order(:published_at)
|
|
275
275
|
|
|
276
276
|
# Descending
|
|
277
|
-
|
|
277
|
+
all(:posts).order(:views, desc: true)
|
|
278
278
|
|
|
279
279
|
# Strings sort naturally
|
|
280
|
-
|
|
280
|
+
all(:posts).order(:title)
|
|
281
281
|
```
|
|
282
282
|
|
|
283
283
|
Mixed types? No problem. Risa handles the type coercion so you don't have to think about it during sorting.
|
|
@@ -287,11 +287,11 @@ Mixed types? No problem. Risa handles the type coercion so you don't have to thi
|
|
|
287
287
|
|
|
288
288
|
```ruby
|
|
289
289
|
# Classic pagination
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
all(:posts).limit(10) # First 10
|
|
291
|
+
all(:posts).offset(20).limit(10) # Items 21-30
|
|
292
292
|
|
|
293
293
|
# Modern pagination with metadata
|
|
294
|
-
pages =
|
|
294
|
+
pages = all(:posts).order(:created_at, desc: true).paginate(per_page: 5)
|
|
295
295
|
|
|
296
296
|
page = pages.first
|
|
297
297
|
page.items # Array of posts for this page
|
|
@@ -318,7 +318,7 @@ The Page object has everything you need for pagination UI without any mental mat
|
|
|
318
318
|
Results aren't plain hashes—they're immutable wrappers that feel like hashes but prevent accidents:
|
|
319
319
|
|
|
320
320
|
```ruby
|
|
321
|
-
post =
|
|
321
|
+
post = all(:posts).first
|
|
322
322
|
|
|
323
323
|
# Access like a hash (symbol or string keys both work)
|
|
324
324
|
post[:title] # => "Hello World"
|
|
@@ -353,7 +353,7 @@ Risa.reload
|
|
|
353
353
|
Risa.defined_models # => [:posts, :users, :tags]
|
|
354
354
|
|
|
355
355
|
# Use the explicit API when you need it
|
|
356
|
-
Risa.query(:posts).where(...) # Same as
|
|
356
|
+
Risa.query(:posts).where(...) # Same as all(:posts).where(...)
|
|
357
357
|
```
|
|
358
358
|
|
|
359
359
|
Error messages are actually helpful:
|
|
@@ -416,9 +416,9 @@ end
|
|
|
416
416
|
Access related data using simple dot notation on your `Risa::InstanceWrapper` objects.
|
|
417
417
|
|
|
418
418
|
```ruby
|
|
419
|
-
alice =
|
|
420
|
-
post =
|
|
421
|
-
profile =
|
|
419
|
+
alice = all(:authors).find_by(id: 1)
|
|
420
|
+
post = all(:posts).find_by(id: 101)
|
|
421
|
+
profile = all(:profiles).find_by(profile_id: 201)
|
|
422
422
|
|
|
423
423
|
# Belongs To (returns InstanceWrapper or nil)
|
|
424
424
|
author_name = post.author.name
|
|
@@ -459,11 +459,11 @@ Risa.define :posts do
|
|
|
459
459
|
belongs_to :creator, class_name: :users, foreign_key: :creator_id, primary_key: :user_pk
|
|
460
460
|
end
|
|
461
461
|
|
|
462
|
-
admin =
|
|
462
|
+
admin = all(:users).first
|
|
463
463
|
admin_article_title = admin.articles.first.title
|
|
464
464
|
# => "Admin Post"
|
|
465
465
|
|
|
466
|
-
post =
|
|
466
|
+
post = all(:posts).find_by(id: 105)
|
|
467
467
|
creator_name = post.creator.username
|
|
468
468
|
# => "admin"
|
|
469
469
|
```
|
|
@@ -498,11 +498,11 @@ Risa.define :post_tags do # The join collection
|
|
|
498
498
|
end
|
|
499
499
|
|
|
500
500
|
# Usage:
|
|
501
|
-
post =
|
|
501
|
+
post = all(:posts).find_by(id: 101)
|
|
502
502
|
tag_names = post.tags.map { |t| t.name }
|
|
503
503
|
# => ["ruby", "web"]
|
|
504
504
|
|
|
505
|
-
tag =
|
|
505
|
+
tag = all(:tags).find_by(name: 'ruby')
|
|
506
506
|
post_titles = tag.posts.map { |p| p.title }
|
|
507
507
|
# => ["Intro to Risa", "Advanced Ruby"]
|
|
508
508
|
|
|
@@ -561,7 +561,7 @@ end
|
|
|
561
561
|
Presenter methods are automatically available directly on the `InstanceWrapper` objects returned by your queries.
|
|
562
562
|
|
|
563
563
|
```ruby
|
|
564
|
-
post =
|
|
564
|
+
post = all(:posts).first
|
|
565
565
|
|
|
566
566
|
# Access hash data keys
|
|
567
567
|
puts post.id # => 1
|
|
@@ -601,8 +601,8 @@ require 'hr'
|
|
|
601
601
|
Risa.load_from('data') # Loads all .rb files in data/
|
|
602
602
|
|
|
603
603
|
# Now all collections are available
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
all(:users).where(active: true)
|
|
605
|
+
all(:posts).order(:created_at)
|
|
606
606
|
```
|
|
607
607
|
|
|
608
608
|
**Development mode reloading:**
|
data/lib/risa/version.rb
CHANGED
data/lib/risa.rb
CHANGED