active_model-relation 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2440274ca803bb2b2d571a3c7753730c66a86a5c8e5ccba873dbd19541e236a3
4
- data.tar.gz: cab3d4c534dc3ada120d5660a029cffdd343d5869653f183434cf8fd3793da4b
3
+ metadata.gz: f0321b5b92acdc5dd26dae064782f042c36201d3f5f3a956497d0719e774aaee
4
+ data.tar.gz: f46553ea8591a73af03326c41c3ada13454f7ae9f508382bfeef1788bdbb178c
5
5
  SHA512:
6
- metadata.gz: 12fe2562f8977cb3b2ac5db4f32dce3dd886d9c4d09996e2bf8fbb98e699ac54f6cad071625cc430c4de1615a3089fdf178bdc2d9c738d1f96b2fbe58a185ff7
7
- data.tar.gz: 7cf7a6a159a8b5df87c5112644f2dc1118cf5e91b26bdf273c182e8ea4d84768291618ec4c0b47bd3b985217d26331551d9bec5a8f8e5a8c6bc59f69b00f3f7e
6
+ metadata.gz: ab15a40f58b9a295841a8740a9b4465c4a872db3d64a1d9e03b69ff6c018b5716af356cf65d64ed8b2e8d8037ee601f8eac4e5d9d33a911257a1f2db7e447152
7
+ data.tar.gz: 4f2fad7e6ed5b5f365aec277d438fb9d22c6bdce031adf3b42d22e36ee5c297eaee14d655f1c8c6fcdfda5746655838f51d4454261d146d654bc73e874e52db1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2024-08-30
3
+ ## [0.2.0] - 2024-09-16
4
+
5
+ - Rename `ActiveModel::Relation::ModelNotFound` to `ActiveModel::Relation::RecordNotFound`
6
+ - Allow creating a `ActiveModel::Relation` without passing a collection
7
+ - Don't require a `.records` class method on model classes
8
+ - Allow passing a block to `ActiveModel::Relation#find`
9
+
10
+ ## [0.1.0] - 2024-09-09
4
11
 
5
12
  - Initial release
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ActiveModel::Relation
2
2
 
3
- This library allows querying of collections of Ruby objects, with a similar interfaces to `ActiveRecord::Relation`.
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
- Afterwards you can use it (almost) like an `ActiveRecord::Relation`.
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::Relation::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
- You can also write named filter methods on the model class, after including `ActiveModel::Relation::Model`.
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).
@@ -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.public_send(name) <=> record.public_send(name)
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, records)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveModel
4
4
  class Relation
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -3,7 +3,7 @@
3
3
  require 'active_model'
4
4
 
5
5
  module ActiveModel
6
- class ModelNotFound < StandardError
6
+ class RecordNotFound < StandardError
7
7
  def initialize(message = nil, model = nil, primary_key = nil, id = nil) # rubocop:disable Metrics/ParameterLists
8
8
  @primary_key = primary_key
9
9
  @model = model
@@ -29,7 +29,7 @@ module ActiveModel
29
29
 
30
30
  delegate :each, :size, :last, to: :records
31
31
 
32
- def initialize(model, records = [])
32
+ def initialize(model, records = model.try(:records) || [])
33
33
  @model = model
34
34
  @records = records
35
35
  @where_clause = WhereClause.new
@@ -39,11 +39,13 @@ module ActiveModel
39
39
  @extending_values = []
40
40
  end
41
41
 
42
- def find(id)
42
+ def find(id = nil, &)
43
+ return records.find(id, &) if block_given?
44
+
43
45
  primary_key = model.try(:primary_key) || :id
44
46
 
45
47
  find_by(primary_key => id) ||
46
- raise(ModelNotFound.new("Couldn't find #{model} with '#{primary_key}'=#{id}", model, primary_key, id))
48
+ raise(RecordNotFound.new("Couldn't find #{model} with '#{primary_key}'=#{id}", model, primary_key, id))
47
49
  end
48
50
 
49
51
  def find_by(attributes = {})
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_model-relation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
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-09-09 00:00:00.000000000 Z
11
+ date: 2024-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -47,13 +47,13 @@ files:
47
47
  - lib/active_model/relation/version.rb
48
48
  - lib/active_model/relation/where_chain.rb
49
49
  - lib/active_model/relation/where_clause.rb
50
- homepage: https://github.com/benedikt/active_model-relation/
50
+ homepage: https://github.com/userlist/active_model-relation/
51
51
  licenses:
52
52
  - MIT
53
53
  metadata:
54
- homepage_uri: https://github.com/benedikt/active_model-relation/
55
- source_code_uri: https://github.com/benedikt/active_model-relation/
56
- changelog_uri: https://github.com/benedikt/active_model-relation/blob/main/CHANGELOG.md
54
+ homepage_uri: https://github.com/userlist/active_model-relation/
55
+ source_code_uri: https://github.com/userlist/active_model-relation/
56
+ changelog_uri: https://github.com/userlist/active_model-relation/blob/main/CHANGELOG.md
57
57
  rubygems_mfa_required: 'true'
58
58
  post_install_message:
59
59
  rdoc_options: []
@@ -73,5 +73,5 @@ requirements: []
73
73
  rubygems_version: 3.5.3
74
74
  signing_key:
75
75
  specification_version: 4
76
- summary: Query collection of ActiveModel objects like an ActiveRecord::Relation
76
+ summary: Query collections of ActiveModel objects like an ActiveRecord::Relation
77
77
  test_files: []