active-query 0.1.3 → 0.2.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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +62 -0
- 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 +14 -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: 5636b341a804f0c3b2fb42b02b57ac502655c70c1cdc37c7e955004012375181
|
|
4
|
+
data.tar.gz: 4e48fbc0f6877a05d963aca4aa04bac4cf2691111cc9857381428b3ff9998f22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8c12b1ebe7b7317af9e2201d3cc6a6edc8cda475a2967e175f2e14605f273d87e1fe514045c3597396c7b5291ef83d6470212d41048af0c3a7ed7179b48df39
|
|
7
|
+
data.tar.gz: ed6ba6312dcf12f8f83594ce2d0557466d495a7c901e351eff1490a641a8afcb9dda96f6885ae935e722ffca3738285b03cf2339380fc1a0c5edf451c8330b51
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-03-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `ActiveQuery::TypeRegistry` for extensible type validation and coercion — register custom types via `TypeRegistry.register` with a `type_class:`, custom validator lambda, or coercer lambda
|
|
14
|
+
- Built-in type classes (`Types::String`, `Types::Integer`, `Types::Float`, `Types::Boolean`) with default coercion support
|
|
15
|
+
- Per-argument `coerce:` option in argument definitions, taking priority over registry-level coercion
|
|
16
|
+
- `TypeRegistry.unregister` to disable built-in coercion and fall back to strict `is_a?` validation
|
|
17
|
+
- Integer coercion restricted to base-10 strings to prevent unexpected hex/octal conversions
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Replace `instance_of?` with `is_a?` in type validation so subclass instances (e.g. `ActiveSupport::SafeBuffer`) pass `String` type checks
|
|
21
|
+
|
|
10
22
|
## [0.1.3] - 2024-12-19
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -38,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
38
50
|
- **Resolvers**: Support complex query logic with resolver classes
|
|
39
51
|
- **Conditional Logic**: Apply scopes conditionally with `if`/`unless`
|
|
40
52
|
|
|
41
|
-
[Unreleased]: https://github.com/matiasasis/active-query/compare/v0.
|
|
53
|
+
[Unreleased]: https://github.com/matiasasis/active-query/compare/v0.2.0...HEAD
|
|
54
|
+
[0.2.0]: https://github.com/matiasasis/active-query/compare/v0.1.3...v0.2.0
|
|
42
55
|
[0.1.3]: https://github.com/matiasasis/active-query/compare/v0.0.1...v0.1.3
|
|
43
56
|
[0.0.1]: https://github.com/matiasasis/active-query/releases/tag/v0.0.1
|
data/README.md
CHANGED
|
@@ -97,6 +97,68 @@ ProductQuery.filter_products(
|
|
|
97
97
|
ProductQuery.filter_products(name: 123, price: 'invalid', quantity: true, available: 'yes')
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
### Custom Types & Coercion
|
|
101
|
+
|
|
102
|
+
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:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# These all work — coercion converts the values automatically
|
|
106
|
+
ProductQuery.filter_products(name: :widget, price: '19.99', quantity: '10', available: 'true')
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Built-in coercion behavior:**
|
|
110
|
+
|
|
111
|
+
| Type | Coerces from | Example |
|
|
112
|
+
|------|-------------|---------|
|
|
113
|
+
| `String` | Any (via `.to_s`) | `42` → `"42"` |
|
|
114
|
+
| `Integer` | Numeric strings | `"42"` → `42` |
|
|
115
|
+
| `Float` | Numeric strings, integers | `"1.5"` → `1.5`, `42` → `42.0` |
|
|
116
|
+
| `Boolean` | `"true"`, `"1"`, `1`, `"false"`, `"0"`, `0` | `"true"` → `true` |
|
|
117
|
+
|
|
118
|
+
**Registering a custom type class:**
|
|
119
|
+
|
|
120
|
+
Create a class that inherits from `ActiveQuery::Types::Base` and implements `.valid?` and `.coerce`:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# config/initializers/active_query.rb
|
|
124
|
+
class UuidType < ActiveQuery::Types::Base
|
|
125
|
+
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
|
|
126
|
+
|
|
127
|
+
def self.valid?(value)
|
|
128
|
+
value.is_a?(String) && value.match?(UUID_REGEX)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.coerce(value)
|
|
132
|
+
value.to_s.downcase.strip
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
ActiveQuery::TypeRegistry.register(UuidType, type_class: UuidType)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Disabling built-in coercion:**
|
|
140
|
+
|
|
141
|
+
If you prefer strict type validation without coercion, unregister the built-in type:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# config/initializers/active_query.rb
|
|
145
|
+
ActiveQuery::TypeRegistry.unregister(Integer) # now "42" won't auto-convert to 42
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
After unregistering, the type falls back to `is_a?` validation with no coercion.
|
|
149
|
+
|
|
150
|
+
**Per-argument coercion:**
|
|
151
|
+
|
|
152
|
+
You can also define coercion on individual arguments using the `coerce:` option:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
query :by_number, 'Find by number',
|
|
156
|
+
{ number: { type: Integer, coerce: ->(v) { v.to_i } } },
|
|
157
|
+
-> (number:) { scope.where(number: number) }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Per-argument `coerce:` takes priority over the global TypeRegistry coercion.
|
|
161
|
+
|
|
100
162
|
### Optional Arguments and Defaults
|
|
101
163
|
|
|
102
164
|
```ruby
|
|
@@ -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,13 +5,14 @@ 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
|
|
15
16
|
|
|
16
17
|
included do
|
|
17
18
|
infer_model
|
|
@@ -139,20 +140,20 @@ module ActiveQuery
|
|
|
139
140
|
extra_params = given_args.keys - args_def.keys
|
|
140
141
|
raise ArgumentError, "Unknown params: #{extra_params}" unless extra_params.empty?
|
|
141
142
|
|
|
142
|
-
given_args.each do |
|
|
143
|
-
given_arg_config = args_def[
|
|
143
|
+
given_args.each do |given_arg_name, given_arg_value|
|
|
144
|
+
given_arg_config = args_def[given_arg_name]
|
|
144
145
|
given_arg_type = given_arg_config[:type]
|
|
145
|
-
given_arg_name = given_arg.first
|
|
146
|
-
given_arg_value = given_arg.second
|
|
147
146
|
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
if given_arg_config[:coerce]
|
|
148
|
+
given_args[given_arg_name] = given_arg_config[:coerce].call(given_arg_value)
|
|
149
|
+
given_arg_value = given_args[given_arg_name]
|
|
150
|
+
elsif ActiveQuery::TypeRegistry.coercer?(given_arg_type)
|
|
151
|
+
given_args[given_arg_name] = ActiveQuery::TypeRegistry.coerce(given_arg_type, given_arg_value)
|
|
152
|
+
given_arg_value = given_args[given_arg_name]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
unless ActiveQuery::TypeRegistry.valid?(given_arg_type, given_arg_value)
|
|
156
|
+
raise ArgumentError, ":#{given_arg_name} must be of type #{given_arg_type}"
|
|
156
157
|
end
|
|
157
158
|
end
|
|
158
159
|
|
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.
|
|
4
|
+
version: 0.2.0
|
|
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
|