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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 426f4c764e93fcdacda7b354b9f4597ced451648ac4a0e56052f9cfc239f6d3f
4
- data.tar.gz: b8e7db1ca8f99f4851a469feb412539c93a41e66f2d9d676ff619197dacf53a9
3
+ metadata.gz: bafd618669b6562ca622703d0b758b27fdc2ea64d031569a44757db21230cb71
4
+ data.tar.gz: 35328a7159020c14d201d2735d8f364073418383819efcaad260be8e6911a83f
5
5
  SHA512:
6
- metadata.gz: 5437dc8c0c0e0ca61883a52799bc927c5bc710d708192be2fb1bd661c2f638e188d842be6098f3692d9fbd515a5659cdf881d684d49425ec9212e8d0b0d119b6
7
- data.tar.gz: 7352b91b01bfa4fe0e96a9d6ad026f6a8ad6e1182d372e879c4eb0fdf453f2f410cd3f0016426b4a22ff9e42dd1f5cd6a45d21269340d20d89c87173142889f7
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.3...HEAD
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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveQuery
4
+ module Types
5
+ class Base
6
+ def self.valid?(_value)
7
+ raise NotImplementedError, "#{name} must implement .valid?"
8
+ end
9
+
10
+ def self.coerce(value)
11
+ value
12
+ end
13
+ end
14
+ end
15
+ 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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveQuery
4
+ module Types
5
+ class String < Base
6
+ def self.valid?(value)
7
+ value.is_a?(::String)
8
+ end
9
+
10
+ def self.coerce(value)
11
+ value.to_s
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveQuery
4
- VERSION = '0.1.3'
4
+ VERSION = '0.2.1'
5
5
  end
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
- class Boolean; end
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 |given_arg|
143
- given_arg_config = args_def[given_arg.first]
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 given_arg_type == ActiveQuery::Base::Boolean
149
- unless given_arg_value == true || given_arg_value == false
150
- raise ArgumentError, ":#{given_arg_name} must be of type Boolean"
151
- end
152
- else
153
- unless given_arg_value.instance_of?(given_arg_type)
154
- raise ArgumentError, ":#{given_arg_name} must be of type #{given_arg_type}"
155
- end
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.3
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