active-query 0.1.3 → 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 +24 -1
- data/README.md +101 -0
- data/active-query.gemspec +0 -1
- data/lib/active_query/type_registry.rb +56 -0
- data/lib/active_query/types/base.rb +15 -0
- data/lib/active_query/types/boolean.rb +24 -0
- data/lib/active_query/types/float.rb +17 -0
- data/lib/active_query/types/integer.rb +17 -0
- data/lib/active_query/types/string.rb +15 -0
- data/lib/active_query/version.rb +1 -1
- data/lib/active_query.rb +23 -13
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bafd618669b6562ca622703d0b758b27fdc2ea64d031569a44757db21230cb71
|
|
4
|
+
data.tar.gz: 35328a7159020c14d201d2735d8f364073418383819efcaad260be8e6911a83f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f97020cced56e3f51ebb51c5c2844dafd408498d4a52b53722b542c9dfc5ae09e5838dede66601a01e0d6ccf1b8f764f1e6af791b416fd0d436103c0c494ff4a
|
|
7
|
+
data.tar.gz: 74ab0d6a7c1bb23475827832d87178112ff221f02dca5eb74b5a67703c4ec461f21e2499ed2deae15c09d609b63df7586ce6d8f5a124c8efb0e42c4bf63d0333
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-03-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `ActiveQuery::Base.registry` — a global registry tracking all classes that include `ActiveQuery::Base`, enabling programmatic discovery of query objects in an application
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Registry support for intermediary concern pattern (e.g. classes that include `ActiveQuery::Base` through an intermediate concern like `HireartQuery::Base`)
|
|
17
|
+
- Guard `infer_model` against anonymous classes with nil name
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2026-03-11
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `ActiveQuery::TypeRegistry` for extensible type validation and coercion — register custom types via `TypeRegistry.register` with a `type_class:`, custom validator lambda, or coercer lambda
|
|
23
|
+
- Built-in type classes (`Types::String`, `Types::Integer`, `Types::Float`, `Types::Boolean`) with default coercion support
|
|
24
|
+
- Per-argument `coerce:` option in argument definitions, taking priority over registry-level coercion
|
|
25
|
+
- `TypeRegistry.unregister` to disable built-in coercion and fall back to strict `is_a?` validation
|
|
26
|
+
- Integer coercion restricted to base-10 strings to prevent unexpected hex/octal conversions
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Replace `instance_of?` with `is_a?` in type validation so subclass instances (e.g. `ActiveSupport::SafeBuffer`) pass `String` type checks
|
|
30
|
+
|
|
10
31
|
## [0.1.3] - 2024-12-19
|
|
11
32
|
|
|
12
33
|
### Added
|
|
@@ -38,6 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
38
59
|
- **Resolvers**: Support complex query logic with resolver classes
|
|
39
60
|
- **Conditional Logic**: Apply scopes conditionally with `if`/`unless`
|
|
40
61
|
|
|
41
|
-
[Unreleased]: https://github.com/matiasasis/active-query/compare/v0.1
|
|
62
|
+
[Unreleased]: https://github.com/matiasasis/active-query/compare/v0.2.1...HEAD
|
|
63
|
+
[0.2.1]: https://github.com/matiasasis/active-query/compare/v0.2.0...v0.2.1
|
|
64
|
+
[0.2.0]: https://github.com/matiasasis/active-query/compare/v0.1.3...v0.2.0
|
|
42
65
|
[0.1.3]: https://github.com/matiasasis/active-query/compare/v0.0.1...v0.1.3
|
|
43
66
|
[0.0.1]: https://github.com/matiasasis/active-query/releases/tag/v0.0.1
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ ActiveQuery is a Ruby gem that helps you create clean, reusable query objects wi
|
|
|
11
11
|
- **Conditional Logic**: Apply scopes conditionally with `if` and `unless`
|
|
12
12
|
- **Resolver Pattern**: Support for complex query logic in separate classes
|
|
13
13
|
- **Custom Scopes**: Define reusable scopes within query objects
|
|
14
|
+
- **Global Registry**: Discover all query objects in your application at runtime
|
|
14
15
|
- **ActiveRecord Integration**: Works seamlessly with ActiveRecord models
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
@@ -97,6 +98,68 @@ ProductQuery.filter_products(
|
|
|
97
98
|
ProductQuery.filter_products(name: 123, price: 'invalid', quantity: true, available: 'yes')
|
|
98
99
|
```
|
|
99
100
|
|
|
101
|
+
### Custom Types & Coercion
|
|
102
|
+
|
|
103
|
+
ActiveQuery includes built-in type coercion for `String`, `Integer`, `Float`, and `Boolean`. When a value doesn't match the expected type, ActiveQuery will attempt to coerce it before validation:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# These all work — coercion converts the values automatically
|
|
107
|
+
ProductQuery.filter_products(name: :widget, price: '19.99', quantity: '10', available: 'true')
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Built-in coercion behavior:**
|
|
111
|
+
|
|
112
|
+
| Type | Coerces from | Example |
|
|
113
|
+
|------|-------------|---------|
|
|
114
|
+
| `String` | Any (via `.to_s`) | `42` → `"42"` |
|
|
115
|
+
| `Integer` | Numeric strings | `"42"` → `42` |
|
|
116
|
+
| `Float` | Numeric strings, integers | `"1.5"` → `1.5`, `42` → `42.0` |
|
|
117
|
+
| `Boolean` | `"true"`, `"1"`, `1`, `"false"`, `"0"`, `0` | `"true"` → `true` |
|
|
118
|
+
|
|
119
|
+
**Registering a custom type class:**
|
|
120
|
+
|
|
121
|
+
Create a class that inherits from `ActiveQuery::Types::Base` and implements `.valid?` and `.coerce`:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# config/initializers/active_query.rb
|
|
125
|
+
class UuidType < ActiveQuery::Types::Base
|
|
126
|
+
UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
127
|
+
|
|
128
|
+
def self.valid?(value)
|
|
129
|
+
value.is_a?(String) && value.match?(UUID_REGEX)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.coerce(value)
|
|
133
|
+
value.to_s.downcase.strip
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
ActiveQuery::TypeRegistry.register(UuidType, type_class: UuidType)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Disabling built-in coercion:**
|
|
141
|
+
|
|
142
|
+
If you prefer strict type validation without coercion, unregister the built-in type:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# config/initializers/active_query.rb
|
|
146
|
+
ActiveQuery::TypeRegistry.unregister(Integer) # now "42" won't auto-convert to 42
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
After unregistering, the type falls back to `is_a?` validation with no coercion.
|
|
150
|
+
|
|
151
|
+
**Per-argument coercion:**
|
|
152
|
+
|
|
153
|
+
You can also define coercion on individual arguments using the `coerce:` option:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
query :by_number, 'Find by number',
|
|
157
|
+
{ number: { type: Integer, coerce: ->(v) { v.to_i } } },
|
|
158
|
+
-> (number:) { scope.where(number: number) }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Per-argument `coerce:` takes priority over the global TypeRegistry coercion.
|
|
162
|
+
|
|
100
163
|
### Optional Arguments and Defaults
|
|
101
164
|
|
|
102
165
|
```ruby
|
|
@@ -264,6 +327,44 @@ UserQuery.queries
|
|
|
264
327
|
# ]
|
|
265
328
|
```
|
|
266
329
|
|
|
330
|
+
### Global Query Registry
|
|
331
|
+
|
|
332
|
+
`ActiveQuery::Base` maintains a global registry of all classes that include it, enabling programmatic discovery of every query object in your application:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
# All query objects are automatically registered
|
|
336
|
+
class UserQuery
|
|
337
|
+
include ActiveQuery::Base
|
|
338
|
+
# ...
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
class ProductQuery
|
|
342
|
+
include ActiveQuery::Base
|
|
343
|
+
# ...
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Discover all registered query objects
|
|
347
|
+
ActiveQuery::Base.registry
|
|
348
|
+
# => [UserQuery, ProductQuery]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
This also works when including `ActiveQuery::Base` through an intermediary concern:
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
module ApplicationQuery
|
|
355
|
+
extend ActiveSupport::Concern
|
|
356
|
+
include ActiveQuery::Base
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
class OrderQuery
|
|
360
|
+
include ApplicationQuery
|
|
361
|
+
# ...
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
ActiveQuery::Base.registry
|
|
365
|
+
# => [..., OrderQuery]
|
|
366
|
+
```
|
|
367
|
+
|
|
267
368
|
### Error Handling
|
|
268
369
|
|
|
269
370
|
ActiveQuery provides clear error messages for common mistakes:
|
data/active-query.gemspec
CHANGED
|
@@ -7,7 +7,6 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.version = ActiveQuery::VERSION
|
|
8
8
|
spec.authors = ["Matias Asis"]
|
|
9
9
|
spec.email = ["matiasis.90@gmail.com"]
|
|
10
|
-
|
|
11
10
|
spec.summary = "ActiveQuery is a gem that helps you to create query objects in a simple way."
|
|
12
11
|
spec.description = "ActiveQuery is a gem that helps you to create query objects in a simple way. It provides a DSL to define queries and scopes for your query object."
|
|
13
12
|
spec.homepage = "https://github.com/matiasasis/active-query"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'types/base'
|
|
4
|
+
require_relative 'types/string'
|
|
5
|
+
require_relative 'types/integer'
|
|
6
|
+
require_relative 'types/float'
|
|
7
|
+
require_relative 'types/boolean'
|
|
8
|
+
|
|
9
|
+
module ActiveQuery
|
|
10
|
+
module TypeRegistry
|
|
11
|
+
@validators = {}
|
|
12
|
+
@coercers = {}
|
|
13
|
+
@type_classes = {}
|
|
14
|
+
|
|
15
|
+
def self.register(type, validator: nil, coerce: nil, type_class: nil)
|
|
16
|
+
@validators[type] = validator if validator
|
|
17
|
+
@coercers[type] = coerce if coerce
|
|
18
|
+
@type_classes[type] = type_class if type_class
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.unregister(type)
|
|
22
|
+
@validators.delete(type)
|
|
23
|
+
@coercers.delete(type)
|
|
24
|
+
@type_classes.delete(type)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.valid?(type, value)
|
|
28
|
+
if @validators.key?(type)
|
|
29
|
+
@validators[type].call(value)
|
|
30
|
+
elsif @type_classes.key?(type)
|
|
31
|
+
@type_classes[type].valid?(value)
|
|
32
|
+
else
|
|
33
|
+
value.is_a?(type)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.coerce(type, value)
|
|
38
|
+
if @coercers.key?(type)
|
|
39
|
+
@coercers[type].call(value)
|
|
40
|
+
elsif @type_classes.key?(type)
|
|
41
|
+
@type_classes[type].coerce(value)
|
|
42
|
+
else
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.coercer?(type)
|
|
48
|
+
@coercers.key?(type) || @type_classes.key?(type)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
register(String, type_class: Types::String)
|
|
52
|
+
register(Integer, type_class: Types::Integer)
|
|
53
|
+
register(Float, type_class: Types::Float)
|
|
54
|
+
register(Types::Boolean, type_class: Types::Boolean)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveQuery
|
|
4
|
+
module Types
|
|
5
|
+
class Boolean < Base
|
|
6
|
+
def self.to_s = 'Boolean'
|
|
7
|
+
def self.inspect = 'Boolean'
|
|
8
|
+
|
|
9
|
+
TRUTHY = [true, 'true', '1', 1].freeze
|
|
10
|
+
FALSY = [false, 'false', '0', 0].freeze
|
|
11
|
+
|
|
12
|
+
def self.valid?(value)
|
|
13
|
+
value == true || value == false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.coerce(value)
|
|
17
|
+
return true if TRUTHY.include?(value)
|
|
18
|
+
return false if FALSY.include?(value)
|
|
19
|
+
|
|
20
|
+
value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveQuery
|
|
4
|
+
module Types
|
|
5
|
+
class Float < Base
|
|
6
|
+
def self.valid?(value)
|
|
7
|
+
value.is_a?(::Float)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.coerce(value)
|
|
11
|
+
::Kernel.Float(value)
|
|
12
|
+
rescue ArgumentError, TypeError
|
|
13
|
+
value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveQuery
|
|
4
|
+
module Types
|
|
5
|
+
class Integer < Base
|
|
6
|
+
def self.valid?(value)
|
|
7
|
+
value.is_a?(::Integer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.coerce(value)
|
|
11
|
+
::Kernel.Integer(value, 10)
|
|
12
|
+
rescue ArgumentError, TypeError
|
|
13
|
+
value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/active_query/version.rb
CHANGED
data/lib/active_query.rb
CHANGED
|
@@ -5,15 +5,23 @@ require 'active_support'
|
|
|
5
5
|
require 'active_support/concern'
|
|
6
6
|
require_relative 'active_query/version'
|
|
7
7
|
require_relative 'active_query/resolver'
|
|
8
|
+
require_relative 'active_query/type_registry'
|
|
8
9
|
require_relative 'active_record_relation_extensions'
|
|
9
10
|
|
|
10
11
|
module ActiveQuery
|
|
11
12
|
module Base
|
|
12
13
|
extend ::ActiveSupport::Concern
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Boolean = ActiveQuery::Types::Boolean
|
|
16
|
+
|
|
17
|
+
@registry = []
|
|
18
|
+
|
|
19
|
+
def self.registry
|
|
20
|
+
@registry
|
|
21
|
+
end
|
|
15
22
|
|
|
16
23
|
included do
|
|
24
|
+
ActiveQuery::Base.registry << self unless ActiveQuery::Base.registry.include?(self)
|
|
17
25
|
infer_model
|
|
18
26
|
@__queries = []
|
|
19
27
|
end
|
|
@@ -72,6 +80,8 @@ module ActiveQuery
|
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
def infer_model
|
|
83
|
+
return unless self.name
|
|
84
|
+
|
|
75
85
|
model_class_name = self.name.sub(/::Query$/, '').classify
|
|
76
86
|
return unless const_defined?(model_class_name)
|
|
77
87
|
|
|
@@ -139,20 +149,20 @@ module ActiveQuery
|
|
|
139
149
|
extra_params = given_args.keys - args_def.keys
|
|
140
150
|
raise ArgumentError, "Unknown params: #{extra_params}" unless extra_params.empty?
|
|
141
151
|
|
|
142
|
-
given_args.each do |
|
|
143
|
-
given_arg_config = args_def[
|
|
152
|
+
given_args.each do |given_arg_name, given_arg_value|
|
|
153
|
+
given_arg_config = args_def[given_arg_name]
|
|
144
154
|
given_arg_type = given_arg_config[:type]
|
|
145
|
-
given_arg_name = given_arg.first
|
|
146
|
-
given_arg_value = given_arg.second
|
|
147
155
|
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
if given_arg_config[:coerce]
|
|
157
|
+
given_args[given_arg_name] = given_arg_config[:coerce].call(given_arg_value)
|
|
158
|
+
given_arg_value = given_args[given_arg_name]
|
|
159
|
+
elsif ActiveQuery::TypeRegistry.coercer?(given_arg_type)
|
|
160
|
+
given_args[given_arg_name] = ActiveQuery::TypeRegistry.coerce(given_arg_type, given_arg_value)
|
|
161
|
+
given_arg_value = given_args[given_arg_name]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
unless ActiveQuery::TypeRegistry.valid?(given_arg_type, given_arg_value)
|
|
165
|
+
raise ArgumentError, ":#{given_arg_name} must be of type #{given_arg_type}"
|
|
156
166
|
end
|
|
157
167
|
end
|
|
158
168
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active-query
|
|
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
|
- Matias Asis
|
|
@@ -178,6 +178,12 @@ files:
|
|
|
178
178
|
- lib/active-query.rb
|
|
179
179
|
- lib/active_query.rb
|
|
180
180
|
- lib/active_query/resolver.rb
|
|
181
|
+
- lib/active_query/type_registry.rb
|
|
182
|
+
- lib/active_query/types/base.rb
|
|
183
|
+
- lib/active_query/types/boolean.rb
|
|
184
|
+
- lib/active_query/types/float.rb
|
|
185
|
+
- lib/active_query/types/integer.rb
|
|
186
|
+
- lib/active_query/types/string.rb
|
|
181
187
|
- lib/active_query/version.rb
|
|
182
188
|
- lib/active_record_relation_extensions.rb
|
|
183
189
|
homepage: https://github.com/matiasasis/active-query
|