active_model-relation 0.1.0 → 0.2.1
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 +12 -1
- data/README.md +213 -6
- data/lib/active_model/relation/order_clause.rb +3 -7
- data/lib/active_model/relation/querying.rb +1 -5
- data/lib/active_model/relation/railtie.rb +13 -0
- data/lib/active_model/relation/version.rb +1 -1
- data/lib/active_model/relation.rb +19 -7
- metadata +16 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 317f4bd9c497d254fabc65f125c743badd36fe4f91e41876bd40819f77703e01
|
4
|
+
data.tar.gz: 937df0db69de0e3983531fae564b0cefa8efbb3cc9d9aac499238a601d9e6503
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f147ee5f25e5b5cd8570dae9d0117c4cbbf015491d8820ae07cf6cca831075d3c9a70f5cc0ec0bf7a15dd522bd691103fa12ee9b6e9b3dcc9a12484ca0761df3
|
7
|
+
data.tar.gz: d61a00b7833c030be0ca14d814f8aeb8e0f23d913e071dd068a09fac050106ca1f3c46cd80b8dea597c21632397fec4d0ccd276cfb7193b29e09b73d1b2ebe68
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
|
3
|
+
- Treat `ActiveModel::RecordNotFound` like `ActiveRecord::RecordNotFound` in `ActionDispatch`
|
4
|
+
- Properly type cast values when they are defined as attribute
|
5
|
+
- Ensure that there is always at least an empty array of records
|
6
|
+
|
7
|
+
## [0.2.0] - 2024-09-16
|
8
|
+
|
9
|
+
- Rename `ActiveModel::ModelNotFound` to `ActiveModel::RecordNotFound`
|
10
|
+
- Allow creating a `ActiveModel::Relation` without passing a collection
|
11
|
+
- Don't require a `.records` class method on model classes
|
12
|
+
- Allow passing a block to `ActiveModel::Relation#find`
|
13
|
+
|
14
|
+
## [0.1.0] - 2024-09-09
|
4
15
|
|
5
16
|
- Initial release
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ActiveModel::Relation
|
2
2
|
|
3
|
-
|
3
|
+
Query a collection of ActiveModel objects like an ActiveRecord::Relation.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -14,6 +14,8 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
+
### Initialization
|
18
|
+
|
17
19
|
Create a new relation by passing the model class and a collection:
|
18
20
|
|
19
21
|
```ruby
|
@@ -25,16 +27,144 @@ relation = ActiveModel::Relation.new(Project, [
|
|
25
27
|
])
|
26
28
|
```
|
27
29
|
|
28
|
-
|
30
|
+
As an alternative, it's also possible to create a collection for a model without explicitly passing a collection.
|
31
|
+
In this case, the library will attempt to call `Project.records` to get the default collection. If the method doesn't exist or returns `nil`, the collection will default to an empty array.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Project
|
35
|
+
def self.records
|
36
|
+
[
|
37
|
+
Project.new(id: 1, state: 'draft', priority: 1),
|
38
|
+
Project.new(id: 2, state: 'running', priority: 2),
|
39
|
+
Project.new(id: 3, state: 'completed', priority: 3),
|
40
|
+
Project.new(id: 4, state: 'completed', priority: 1)
|
41
|
+
]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
relation = ActiveModel::Relation.new(Project)
|
46
|
+
```
|
47
|
+
|
48
|
+
### Querying
|
49
|
+
|
50
|
+
An `ActiveModel::Relation` can be queried almost exactly like an `ActiveRecord::Relation`.
|
51
|
+
|
52
|
+
#### `#find`
|
53
|
+
|
54
|
+
You can look up a record by it's primary key, using the `find` method. If no record is found, it will raise a `ActiveModel::RecordNotFound` error.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
project = relation.find(1)
|
58
|
+
```
|
59
|
+
|
60
|
+
By default, `ActiveModel::Relation` will assume `:id` as the primary key. You can customize this behavior by setting a `primary_key` on the model class.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class Project
|
64
|
+
def self.primary_key = :identifier
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
When passed a block, the `find` method will behave like `Enumerable#find`.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
project = relation.find { |p| p.id == 1 }
|
72
|
+
```
|
73
|
+
|
74
|
+
#### `#find_by`
|
75
|
+
|
76
|
+
To look up a record based on a set of arbitary attributes, you can use `find_by`. It accepts the same arguments as `#where` and will return the first matching record.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
project = relation.find_by(state: 'draft')
|
80
|
+
```
|
81
|
+
|
82
|
+
#### `#where`
|
83
|
+
|
84
|
+
To filter a relation, you can use `where` and pass a set of attributes and the expected values. This method will return a new `ActiveModel::Relation` that only returns the matching records, so it's possible to chain multiple calls. The filtering will only happen when actually accessing records.
|
29
85
|
|
30
86
|
```ruby
|
31
87
|
relation.where(state: 'completed')
|
32
|
-
relation.offset(3)
|
33
|
-
relation.limit(2)
|
34
|
-
relation.order(priority: :asc, state: :desc)
|
35
88
|
```
|
36
89
|
|
37
|
-
|
90
|
+
The following two lines will return the same filtered results:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
relation.where(state: 'completed', priority: 1)
|
94
|
+
relation.where(state: 'completed').where(priority: 1)
|
95
|
+
```
|
96
|
+
|
97
|
+
To allow for more advanced filtering, `#where` allows filtering using a block. This works similar to `Enumerable#select`, but will return a new `ActiveModel::Relation` instead of an already filtered array.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
relation.where { |p| p.state == 'completed' && p.priority == 1 }
|
101
|
+
```
|
102
|
+
|
103
|
+
#### `#where.not`
|
104
|
+
|
105
|
+
Similar to `#where`, the `#where.not` chain allows you to filter a relation. It will also return a new `ActiveModel::Relation` with that returns only the matching records.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
relation.where.not(state: 'draft')
|
109
|
+
```
|
110
|
+
|
111
|
+
To allow for more advanced filtering, `#where.not` allows filtering using a block. This works similar to `Enumerable#reject`, but will return a new `ActiveModel::Relation` instead of an already filtered array.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
relation.where.not { |p| p.state == 'draft' && p.priority == 1 }
|
115
|
+
```
|
116
|
+
|
117
|
+
### Sorting
|
118
|
+
|
119
|
+
It is possible to sort an `ActiveModel::Relation` by a given set of attribute names. Sorting will be applied after filtering, but before limits and offsets.
|
120
|
+
|
121
|
+
#### `#order`
|
122
|
+
|
123
|
+
To sort by a single attribute in ascending order, you can just pass the attribute name to the `order` method.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
relation.order(:priority)
|
127
|
+
```
|
128
|
+
|
129
|
+
To specify the sort direction, you can pass a hash with the attribute name as key and either `:asc`, or `:desc` as value.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
relation.order(priorty: :desc)
|
133
|
+
```
|
134
|
+
|
135
|
+
To order by multiple attributes, you can pass them in the order of specificity you want.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
relation.order(:state, :priority)
|
139
|
+
```
|
140
|
+
|
141
|
+
For multiple attributes, it's also possible to specify the direction.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
relation.order(state: :desc, priority: :asc)
|
145
|
+
```
|
146
|
+
|
147
|
+
### Limiting and offsets
|
148
|
+
|
149
|
+
#### `#limit`
|
150
|
+
|
151
|
+
To limit the amount of records returned in the collection, you can call `limit` on the relation. It will return a new `ActiveModel::Relation` that only returns the given limit of records, allowing you to chain multiple other calls. The limit will only be applied when actually accessing the records later on.
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
relation.limit(10)
|
155
|
+
```
|
156
|
+
|
157
|
+
#### `#offset`
|
158
|
+
|
159
|
+
To skip a certain number of records in the collection, you can use `offset` on the relation. It will return a new `ActiveModel::Relation` that skips the given number of records at the beginning. The offset will only be applied when actually accessing the records later on.
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
relation.offset(20)
|
163
|
+
```
|
164
|
+
|
165
|
+
### Scopes
|
166
|
+
|
167
|
+
After including `ActiveModel::Relation::Model`, the library also supports calling class methods defined on the model class as part of the relation.
|
38
168
|
|
39
169
|
```ruby
|
40
170
|
class Project
|
@@ -52,6 +182,71 @@ class Project
|
|
52
182
|
end
|
53
183
|
```
|
54
184
|
|
185
|
+
Given the example above, you can now create relations like you're used to from `ActiveRecord::Relation`.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
projects = Project.all
|
189
|
+
completed_projects = all_projects.completed
|
190
|
+
important_projects = all_projects.where(priority: 1)
|
191
|
+
```
|
192
|
+
|
193
|
+
### Spawning
|
194
|
+
|
195
|
+
It's possilbe to create new versions of a `ActiveModel::Relation` that only includes certain aspects of the `ActiveModel::Relation` it is based on. It's currently possible to customize the following aspects: `:where`, `:limit`, `:offset`.
|
196
|
+
|
197
|
+
#### `#except`
|
198
|
+
|
199
|
+
To create a new `ActiveModel::Relation` without certain aspects, you can use `except` and pass a list of aspects, you'd like to exclude from the newly created instance. The following example will create a new `ActiveModel::Relation` without any previously defined limit or offset.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
relation.except(:limit, :offset)
|
203
|
+
```
|
204
|
+
#### `#only`
|
205
|
+
|
206
|
+
Similar to `except`, the `only` method will return a new instance of the `ActiveModel::Relation` it is based on but with only the passed list of aspects applied to it.
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
relation.only(:where)
|
210
|
+
```
|
211
|
+
|
212
|
+
### Extending relations
|
213
|
+
|
214
|
+
#### `#extending`
|
215
|
+
|
216
|
+
In order to add additional methods to a relation, you can use `extending`. You can either pass a list of modules that will be included in this particular instance, or a block defining additional methods.
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
module Pagination
|
220
|
+
def page_size = 25
|
221
|
+
|
222
|
+
def page(page)
|
223
|
+
limit(page_size).offset(page.to_i * page_size)
|
224
|
+
end
|
225
|
+
|
226
|
+
def total_count
|
227
|
+
except(:limit, :offset).count
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
relation.extending(Pagination)
|
232
|
+
```
|
233
|
+
|
234
|
+
The following example is equivalent to the example above:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
relation.extending do
|
238
|
+
def page_size = 25
|
239
|
+
|
240
|
+
def page(page)
|
241
|
+
limit(page_size).offset(page.to_i * page_size)
|
242
|
+
end
|
243
|
+
|
244
|
+
def total_count
|
245
|
+
except(:limit, :offset).count
|
246
|
+
end
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
55
250
|
## Development
|
56
251
|
|
57
252
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -62,6 +257,10 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
62
257
|
|
63
258
|
Bug reports and pull requests are welcome on GitHub at https://github.com/userlist/active_model-relation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/userlist/active_model-relation/blob/main/CODE_OF_CONDUCT.md).
|
64
259
|
|
260
|
+
## Acknowledgements
|
261
|
+
|
262
|
+
This library is _heavily_ inspired by [`ActiveRecord::Relation`](https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation.rb) and uses similar patterns and implementations in various parts.
|
263
|
+
|
65
264
|
## License
|
66
265
|
|
67
266
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -69,3 +268,11 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
69
268
|
## Code of Conduct
|
70
269
|
|
71
270
|
Everyone interacting in the ActiveModel::Relation project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/userlist/active_model-relation/blob/main/CODE_OF_CONDUCT.md).
|
271
|
+
|
272
|
+
## What is Userlist?
|
273
|
+
|
274
|
+
[](https://userlist.com/)
|
275
|
+
|
276
|
+
[Userlist](https://userlist.com/) allows you to onboard and engage your SaaS users with targeted behavior-based campaigns using email or in-app messages.
|
277
|
+
|
278
|
+
Userlist was started in 2017 as an alternative to bulky enterprise messaging tools. We believe that running SaaS products should be more enjoyable. Learn more [about us](https://userlist.com/about-us/).
|
@@ -10,20 +10,16 @@ module ActiveModel
|
|
10
10
|
@name = name
|
11
11
|
end
|
12
12
|
|
13
|
-
def call(_, _)
|
14
|
-
0
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
class Ascending < OrderExpression
|
19
13
|
def call(record, other)
|
20
14
|
record.public_send(name) <=> other.public_send(name)
|
21
15
|
end
|
22
16
|
end
|
23
17
|
|
18
|
+
class Ascending < OrderExpression; end
|
19
|
+
|
24
20
|
class Descending < OrderExpression
|
25
21
|
def call(record, other)
|
26
|
-
other
|
22
|
+
super(other, record)
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
@@ -9,11 +9,7 @@ module ActiveModel
|
|
9
9
|
delegate :where, :find, :find_by, :offset, :limit, :first, :last, to: :all
|
10
10
|
|
11
11
|
def all
|
12
|
-
current_scope || ActiveModel::Relation.new(self
|
13
|
-
end
|
14
|
-
|
15
|
-
def records
|
16
|
-
raise NotImplementedError
|
12
|
+
current_scope || ActiveModel::Relation.new(self)
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_model'
|
4
|
+
require_relative 'relation/railtie' if defined?(Rails::Railtie)
|
4
5
|
|
5
6
|
module ActiveModel
|
6
|
-
class
|
7
|
+
class RecordNotFound < StandardError
|
7
8
|
def initialize(message = nil, model = nil, primary_key = nil, id = nil) # rubocop:disable Metrics/ParameterLists
|
8
9
|
@primary_key = primary_key
|
9
10
|
@model = model
|
@@ -29,9 +30,9 @@ module ActiveModel
|
|
29
30
|
|
30
31
|
delegate :each, :size, :last, to: :records
|
31
32
|
|
32
|
-
def initialize(model, records =
|
33
|
+
def initialize(model, records = model.try(:records))
|
33
34
|
@model = model
|
34
|
-
@records = records
|
35
|
+
@records = records || []
|
35
36
|
@where_clause = WhereClause.new
|
36
37
|
@order_clause = OrderClause.new
|
37
38
|
@offset_value = nil
|
@@ -39,15 +40,17 @@ module ActiveModel
|
|
39
40
|
@extending_values = []
|
40
41
|
end
|
41
42
|
|
42
|
-
def find(id)
|
43
|
+
def find(id = nil, &)
|
44
|
+
return records.find(id, &) if block_given?
|
45
|
+
|
43
46
|
primary_key = model.try(:primary_key) || :id
|
44
47
|
|
45
48
|
find_by(primary_key => id) ||
|
46
|
-
raise(
|
49
|
+
raise(RecordNotFound.new("Couldn't find #{model} with '#{primary_key}'=#{id}", model, primary_key, id))
|
47
50
|
end
|
48
51
|
|
49
52
|
def find_by(attributes = {})
|
50
|
-
where_clause = self.where_clause + WhereClause.from_hash(attributes)
|
53
|
+
where_clause = self.where_clause + WhereClause.from_hash(type_cast_values(attributes))
|
51
54
|
|
52
55
|
records.find(&where_clause)
|
53
56
|
end
|
@@ -59,7 +62,7 @@ module ActiveModel
|
|
59
62
|
def where!(attributes = {}, &)
|
60
63
|
return WhereChain.new(spawn) unless attributes.any? || block_given?
|
61
64
|
|
62
|
-
self.where_clause += WhereClause.build(attributes, &)
|
65
|
+
self.where_clause += WhereClause.build(type_cast_values(attributes), &)
|
63
66
|
self
|
64
67
|
end
|
65
68
|
|
@@ -178,5 +181,14 @@ module ActiveModel
|
|
178
181
|
relation.limit_value = values[:limit]
|
179
182
|
end
|
180
183
|
end
|
184
|
+
|
185
|
+
def type_cast_values(attributes)
|
186
|
+
attributes.to_h do |key, value|
|
187
|
+
type = model.try(:type_for_attribute, key)
|
188
|
+
value = type.cast(value) if type
|
189
|
+
|
190
|
+
[key, value]
|
191
|
+
end
|
192
|
+
end
|
181
193
|
end
|
182
194
|
end
|
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_model-relation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benedikt Deicke
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '7.2'
|
20
|
+
- - "~>"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '8.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '7.2'
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8.0'
|
27
33
|
description: This library allows querying of collections of Ruby objects, with a similar
|
28
34
|
interface to ActiveRecord::Relation.
|
29
35
|
email:
|
@@ -43,17 +49,18 @@ files:
|
|
43
49
|
- lib/active_model/relation/model.rb
|
44
50
|
- lib/active_model/relation/order_clause.rb
|
45
51
|
- lib/active_model/relation/querying.rb
|
52
|
+
- lib/active_model/relation/railtie.rb
|
46
53
|
- lib/active_model/relation/scoping.rb
|
47
54
|
- lib/active_model/relation/version.rb
|
48
55
|
- lib/active_model/relation/where_chain.rb
|
49
56
|
- lib/active_model/relation/where_clause.rb
|
50
|
-
homepage: https://github.com/
|
57
|
+
homepage: https://github.com/userlist/active_model-relation/
|
51
58
|
licenses:
|
52
59
|
- MIT
|
53
60
|
metadata:
|
54
|
-
homepage_uri: https://github.com/
|
55
|
-
source_code_uri: https://github.com/
|
56
|
-
changelog_uri: https://github.com/
|
61
|
+
homepage_uri: https://github.com/userlist/active_model-relation/
|
62
|
+
source_code_uri: https://github.com/userlist/active_model-relation/
|
63
|
+
changelog_uri: https://github.com/userlist/active_model-relation/blob/main/CHANGELOG.md
|
57
64
|
rubygems_mfa_required: 'true'
|
58
65
|
post_install_message:
|
59
66
|
rdoc_options: []
|
@@ -73,5 +80,5 @@ requirements: []
|
|
73
80
|
rubygems_version: 3.5.3
|
74
81
|
signing_key:
|
75
82
|
specification_version: 4
|
76
|
-
summary: Query
|
83
|
+
summary: Query collections of ActiveModel objects like an ActiveRecord::Relation
|
77
84
|
test_files: []
|