active_fields 1.1.0 → 2.0.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/CHANGELOG.md +32 -2
  4. data/README.md +411 -38
  5. data/app/models/active_fields/field/boolean.rb +3 -0
  6. data/app/models/active_fields/field/date.rb +3 -0
  7. data/app/models/active_fields/field/date_array.rb +3 -0
  8. data/app/models/active_fields/field/date_time.rb +4 -1
  9. data/app/models/active_fields/field/date_time_array.rb +4 -1
  10. data/app/models/active_fields/field/decimal.rb +6 -1
  11. data/app/models/active_fields/field/decimal_array.rb +6 -1
  12. data/app/models/active_fields/field/enum.rb +3 -0
  13. data/app/models/active_fields/field/enum_array.rb +3 -0
  14. data/app/models/active_fields/field/integer.rb +3 -0
  15. data/app/models/active_fields/field/integer_array.rb +3 -0
  16. data/app/models/active_fields/field/text.rb +3 -0
  17. data/app/models/active_fields/field/text_array.rb +3 -0
  18. data/app/models/active_fields/field.rb +5 -0
  19. data/app/models/concerns/active_fields/customizable_concern.rb +89 -5
  20. data/app/models/concerns/active_fields/field_concern.rb +26 -5
  21. data/app/models/concerns/active_fields/value_concern.rb +0 -2
  22. data/db/migrate/20240229230000_create_active_fields_tables.rb +1 -1
  23. data/lib/active_fields/casters/date_time_caster.rb +1 -3
  24. data/lib/active_fields/casters/decimal_caster.rb +2 -5
  25. data/lib/active_fields/constants.rb +55 -0
  26. data/lib/active_fields/engine.rb +2 -1
  27. data/lib/active_fields/finders/array_finder.rb +112 -0
  28. data/lib/active_fields/finders/base_finder.rb +73 -0
  29. data/lib/active_fields/finders/boolean_finder.rb +20 -0
  30. data/lib/active_fields/finders/date_array_finder.rb +65 -0
  31. data/lib/active_fields/finders/date_finder.rb +32 -0
  32. data/lib/active_fields/finders/date_time_array_finder.rb +65 -0
  33. data/lib/active_fields/finders/date_time_finder.rb +32 -0
  34. data/lib/active_fields/finders/decimal_array_finder.rb +65 -0
  35. data/lib/active_fields/finders/decimal_finder.rb +32 -0
  36. data/lib/active_fields/finders/enum_array_finder.rb +41 -0
  37. data/lib/active_fields/finders/enum_finder.rb +20 -0
  38. data/lib/active_fields/finders/integer_array_finder.rb +65 -0
  39. data/lib/active_fields/finders/integer_finder.rb +32 -0
  40. data/lib/active_fields/finders/singular_finder.rb +66 -0
  41. data/lib/active_fields/finders/text_array_finder.rb +47 -0
  42. data/lib/active_fields/finders/text_finder.rb +81 -0
  43. data/lib/active_fields/has_active_fields.rb +3 -4
  44. data/lib/active_fields/registry.rb +38 -0
  45. data/lib/active_fields/version.rb +1 -1
  46. data/lib/active_fields.rb +29 -1
  47. data/lib/generators/active_fields/scaffold/scaffold_generator.rb +9 -0
  48. data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +0 -10
  49. data/lib/generators/active_fields/scaffold/templates/controllers/concerns/active_fields_controller_concern.rb +33 -0
  50. data/lib/generators/active_fields/scaffold/templates/helpers/active_fields_helper.rb +67 -0
  51. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_fields_finders_form_controller.js +59 -0
  52. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/_form.html.erb +42 -0
  53. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_array_size.html.erb +16 -0
  54. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_boolean.html.erb +21 -0
  55. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date.html.erb +16 -0
  56. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date_array.html.erb +16 -0
  57. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime.html.erb +16 -0
  58. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime_array.html.erb +16 -0
  59. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal.html.erb +16 -0
  60. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal_array.html.erb +16 -0
  61. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum.html.erb +16 -0
  62. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum_array.html.erb +16 -0
  63. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer.html.erb +16 -0
  64. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer_array.html.erb +16 -0
  65. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text.html.erb +16 -0
  66. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text_array.html.erb +16 -0
  67. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +1 -1
  68. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +1 -1
  69. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +1 -1
  70. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +2 -2
  71. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +2 -2
  72. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +2 -2
  73. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +2 -2
  74. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +1 -1
  75. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +1 -1
  76. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +1 -1
  77. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +1 -1
  78. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +1 -1
  79. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +1 -1
  80. data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +1 -1
  81. metadata +42 -10
  82. data/lib/active_fields/customizable_config.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd75dc9f715c95766a238fd268c8174c7920e2b64ddce086f4bd7eed9ea2c36a
4
- data.tar.gz: 74ba8eaf11a578067d0c45730a59aa60dc23363b9e5ffcce5bfca7795ee12da3
3
+ metadata.gz: 6c94bb84092249c9db341f69ec79233da8440b8ef74c00475c5c8acb6e42a290
4
+ data.tar.gz: 7653a7859137a53d465ccc2398f055f064105949f0a3be7e1e7fc5f3636ee240
5
5
  SHA512:
6
- metadata.gz: 4cc1b2984a7912497bc1b26cec3fc793bb897cc813c76440b5a8513834acae27d5c4df5da9c5f87cf9b36ae79351cacb52ab60965195fa6b44ce6ec8fc4f3c4e
7
- data.tar.gz: 2f03d79bb28e91af86a99d36ff60a992b14ecf9c13d6d8a13eb92490790c4cd71afb11465173ba8a2a756cc24c3cf50c488a13bc8fca90d619b1079ec88fe216
6
+ metadata.gz: 57eecfc4cb3cf040dfcc2daf59a804b33ea5b3f9744cde6931c8fa0f9777eeeb067d3895b33ebbf927c5b74c66290f00546986d472d038c11df700d8092631d7
7
+ data.tar.gz: 1e923ede58bd193079334b3fcb9e193fdd61b56d6d73ffdf977a626618d2e308787641c8194ebc806113141b9dd66a3e5fbea106c0d0ded699c866a10a6b8d0b
data/.rubocop.yml CHANGED
@@ -1,17 +1,15 @@
1
- require:
2
- # - rubocop-factory_bot
1
+ plugins:
3
2
  - rubocop-performance
4
3
  - rubocop-rails
5
4
  - rubocop-rake
6
5
  - rubocop-rspec
7
- # - rubocop-rspec_rails
8
6
 
9
7
  inherit_gem:
10
8
  rubocop-shopify: rubocop.yml
11
9
 
12
10
  AllCops:
13
11
  TargetRubyVersion: 3.3
14
- TargetRailsVersion: 7.2
12
+ TargetRailsVersion: 8.0
15
13
  NewCops: enable
16
14
  Exclude:
17
15
  - "spec/dummy/db/schema.rb"
@@ -31,6 +29,9 @@ Style/WordArray:
31
29
  Style/MethodCallWithArgsParentheses:
32
30
  Enabled: false
33
31
 
32
+ Style/SafeNavigationChainLength:
33
+ Enabled: false
34
+
34
35
  Metrics/BlockLength:
35
36
  Exclude:
36
37
  - "spec/**/*"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  ## [Unreleased]
2
2
 
3
+ # [2.0.1] - 2025-04-09
4
+ - Fixed search with `nil` operator
5
+
6
+ ## [2.0.0] - 2025-02-22
7
+ - Drop support for _Rails_ < 7.1
8
+ - Drop support for _Ruby_ < 3.1 (EOL)
9
+ - Added search functionality
10
+ - Added registry to store relationships between _Customizable_ types and _Active Field_ types
11
+ - Added notes about the necessity of disabling reloading for custom model classes and custom _Active Field_ type models
12
+ to prevent _STI_ (_Single Table Inheritance_) issues
13
+
14
+ **Breaking changes**:
15
+ - Maximum datetime precision reduced to 6 for all _Ruby_/_Rails_ versions.
16
+
17
+ While _Ruby_ allows up to 9 fractional seconds, most databases, including _PostgreSQL_, support only 6.
18
+ To ensure compatibility and prevent potential issues,
19
+ we are standardizing the precision to the minimum supported across our technology stack.
20
+
21
+ - Maximum datetime precision constant relocated.
22
+
23
+ The maximum precision value has been moved
24
+ from `ActiveFields::Casters::DateTimeCaster::MAX_PRECISION` to `ActiveFields::MAX_DATETIME_PRECISION`.
25
+
26
+ - Maximum decimal precision set to 16383 (2**14 - 1).
27
+
28
+ While _Ruby_'s `BigDecimal` class allows extremely high precision,
29
+ PostgreSQL supports a maximum of 16383 digits after the decimal point.
30
+ To ensure compatibility, we are capping the precision at this value.
31
+ The maximum precision value is now accessible via `ActiveFields::MAX_DECIMAL_PRECISION`.
32
+
3
33
  ## [1.1.0] - 2024-09-10
4
34
  - Added scaffold generator
5
35
  - Disabled models reloading to prevent STI issues
@@ -16,11 +46,11 @@
16
46
  - Added datetime and datetime array field types
17
47
  - Added fields configuration DSL
18
48
  - Introduced new _Customizable_ setter for _Active Values_ (`active_fields_attributes=`) with a more convenient syntax
19
- to replace the default setter (`active_values_attributes=`) from Rails nested attributes feature.
49
+ to replace the default setter (`active_values_attributes=`) from _Rails_ nested attributes feature
20
50
 
21
51
  ## [0.2.0] - 2024-06-13
22
52
 
23
- - Rewritten as a Rails plugin!
53
+ - Rewritten as a _Rails_ plugin!
24
54
  - Custom field types support
25
55
  - Global configuration options for changing field and value classes
26
56
  - Per-model configuration option for enabling specific field types only
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Gem downloads count](https://img.shields.io/gem/dt/active_fields)](https://rubygems.org/gems/active_fields)
5
5
  [![Github Actions CI](https://github.com/lassoid/active_fields/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/lassoid/active_fields/actions/workflows/main.yml)
6
6
 
7
- **ActiveFields** is a Rails plugin that implements the Entity-Attribute-Value (EAV) pattern,
7
+ **ActiveFields** is a _Rails_ plugin that implements the _Entity-Attribute-Value_ (_EAV_) pattern,
8
8
  enabling the addition of custom fields to any model at runtime without requiring changes to the database schema.
9
9
 
10
10
  ## Key Concepts
@@ -72,12 +72,10 @@ such as booleans, strings, numbers, arrays, etc.
72
72
  bin/rails generate active_fields:scaffold
73
73
  ```
74
74
 
75
- This command generates a controller, routes and views for managing _Active Fields_,
76
- along with form inputs for _Active Values_ and some useful helper methods.
75
+ This command generates a controller, routes, views for managing _Active Fields_,
76
+ along with form inputs for _Active Values_, search form and some useful helper methods that will be used in next steps.
77
77
 
78
- **Note:** Don't forget to add available _Customizable_ types in generated _Active Fields_ forms.
79
-
80
- **Note:** The array field helper uses _Stimulus_ for interactivity.
78
+ **Note:** The array field helper and search form use _Stimulus_ for interactivity.
81
79
  If your app doesn't already include _Stimulus_, you can [easily add it](https://github.com/hotwired/stimulus-rails).
82
80
  Alternatively, if you prefer not to use _Stimulus_, you should implement your own JavaScript code.
83
81
 
@@ -102,7 +100,7 @@ such as booleans, strings, numbers, arrays, etc.
102
100
  # ...
103
101
  ```
104
102
 
105
- Finally, permit the _Active Fields_ attributes in your _Customizables_ controllers:
103
+ Permit the _Active Fields_ attributes in your _Customizables_ controllers:
106
104
 
107
105
  ```ruby
108
106
  # app/controllers/posts_controller.rb
@@ -119,34 +117,52 @@ such as booleans, strings, numbers, arrays, etc.
119
117
 
120
118
  permitted_params
121
119
  end
122
-
123
- # Removes an empty string from the beginning of the array parameter
124
- def compact_array_param(value)
125
- if value.first == ""
126
- value[1..-1]
127
- else
128
- value
129
- end
130
- end
131
120
  ```
132
121
 
133
122
  **Note:** Here we use the `active_fields_attributes=` method (as a permitted parameter),
134
- that integrates well with Rails `fields_for` to generate appropriate form fields.
123
+ that integrates well with _Rails_ `fields_for` to generate appropriate form fields.
135
124
  Alternatively, the alias `active_fields=` can be used in contexts without `fields_for`, such as API controllers.
136
125
 
137
- That's it!
138
- You can now add _Active Fields_ to _Customizables_ at `http://localhost:3000/active_fields`
139
- and fill in _Active Values_ within _Customizable_ forms.
126
+ **Note:** `compact_array_param` is a helper method, that was added by scaffold generator.
127
+ It removes an empty string from the beginning of the array parameter.
140
128
 
141
- You can also explore the [Demo app](https://github.com/lassoid/active_fields/blob/main/spec/dummy)
142
- where the plugin is fully integrated into a full-stack Rails application.
143
- Feel free to explore the source code and run it locally:
129
+ 6. Use the `where_active_fields` query method to filter records and add a search form in _Customizables_ index actions.
144
130
 
145
- ```shell
146
- spec/dummy/bin/setup
147
- bin/rails s
131
+ ```ruby
132
+ # app/controllers/posts_controller.rb
133
+ # ...
134
+
135
+ def index
136
+ @posts = Post.where_active_fields(active_fields_finders_params)
137
+ end
148
138
  ```
149
139
 
140
+ **Note:** `active_fields_finders_params` is a helper method, that was added by scaffold generator.
141
+ It permits params from search form.
142
+
143
+ ```erb
144
+ # app/views/posts/index.html.erb
145
+ # ...
146
+
147
+ <%= render_active_fields_finders_form(active_fields: Post.active_fields, url: posts_path) %>
148
+
149
+ # ...
150
+ ```
151
+
152
+ That's it!
153
+ You can now add _Active Fields_ to _Customizables_ at `http://localhost:3000/active_fields`,
154
+ fill in _Active Values_ within _Customizable_ forms
155
+ and search _Customizables_ using their index actions.
156
+
157
+ You can also explore the [Demo app](https://github.com/lassoid/active_fields/blob/main/spec/dummy)
158
+ where the plugin is fully integrated into a full-stack _Rails_ application.
159
+ Feel free to explore the source code and run it locally:
160
+
161
+ ```shell
162
+ spec/dummy/bin/setup
163
+ bin/rails s
164
+ ```
165
+
150
166
  ## Field Types
151
167
 
152
168
  The plugin comes with a structured set of _Active Fields_ types:
@@ -289,6 +305,238 @@ We replace it with `**` for conciseness.
289
305
 
290
306
  **Note:** Options marked with **\*** are mandatory.
291
307
 
308
+ ### Search Functionality
309
+
310
+ **Note:** This feature is compatible with _PostgreSQL_ 17 and above.
311
+
312
+ The gem provides a built-in search capability. Like _Rails_ nested attributes functionality, it accepts the following argument types:
313
+
314
+ - An array of hashes.
315
+
316
+ ```ruby
317
+ Post.where_active_fields(
318
+ [
319
+ { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
320
+ { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
321
+ { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
322
+ ],
323
+ )
324
+ ```
325
+
326
+ - A hash of hashes (typically generated by _Rails_ `fields_for` form helper).
327
+
328
+ ```ruby
329
+ Post.where_active_fields(
330
+ {
331
+ "0" => { name: "integer_array", operator: "any_gteq", value: 5 },
332
+ "1" => { "name" => "text", operator: "=", "value" => "Lasso" },
333
+ "2" => { n: "boolean", op: "!=", v: false },
334
+ },
335
+ )
336
+ ```
337
+
338
+ - Permitted parameters (can contain either an array of hashes or a hash of hashes).
339
+
340
+ ```ruby
341
+ Post.where_active_fields(permitted_params)
342
+ ```
343
+
344
+ Key details:
345
+ - `n`/`name` argument must specify the name of an _Active Field_.
346
+ - `v`/`value` argument will be automatically cast to the appropriate type.
347
+ - `op`/`operator` argument can contain either _operation_ or _operator_.
348
+
349
+ Supported _operations_ and _operators_ for each _Active Field_ type are listed below.
350
+
351
+ #### Boolean
352
+
353
+ | Operation | Operator | Description |
354
+ |------------|----------|-----------------------------|
355
+ | `eq` | `=` | Value is equal to given |
356
+ | `not_eq` | `!=` | Value is not equal to given |
357
+
358
+ #### Date
359
+
360
+ | Operation | Operator | Description |
361
+ |------------|----------|-----------------------------------------|
362
+ | `eq` | `=` | Value is equal to given |
363
+ | `not_eq` | `!=` | Value is not equal to given |
364
+ | `gt` | `>` | Value is greater than given |
365
+ | `gteq` | `>=` | Value is greater than or equal to given |
366
+ | `lt` | `<` | Value is less than given |
367
+ | `lteq` | `<=` | Value is less than or equal to given |
368
+
369
+ #### DateArray
370
+
371
+ | Operation | Operator | Description |
372
+ |---------------|----------|----------------------------------------------------------------|
373
+ | `include` | `\|=` | Array value includes given element |
374
+ | `not_include` | `!\|=` | Array value doesn't include given element |
375
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
376
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
377
+ | `any_lt` | `\|<` | Array value contains an element less than given |
378
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
379
+ | `all_gt` | `&>` | All elements of array value are greater than given |
380
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
381
+ | `all_lt` | `&<` | All elements of array value are less than given |
382
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
383
+ | `size_eq` | `#=` | Array value size is equal to given |
384
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
385
+ | `size_gt` | `#>` | Array value size is greater than given |
386
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
387
+ | `size_lt` | `#<` | Array value size is less than given |
388
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
389
+
390
+ #### DateTime
391
+
392
+ | Operation | Operator | Description |
393
+ |------------|----------|-----------------------------------------|
394
+ | `eq` | `=` | Value is equal to given |
395
+ | `not_eq` | `!=` | Value is not equal to given |
396
+ | `gt` | `>` | Value is greater than given |
397
+ | `gteq` | `>=` | Value is greater than or equal to given |
398
+ | `lt` | `<` | Value is less than given |
399
+ | `lteq` | `<=` | Value is greater than or equal to given |
400
+
401
+ #### DateTimeArray
402
+
403
+ | Operation | Operator | Description |
404
+ |---------------|----------|----------------------------------------------------------------|
405
+ | `include` | `\|=` | Array value includes given element |
406
+ | `not_include` | `!\|=` | Array value doesn't include given element |
407
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
408
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
409
+ | `any_lt` | `\|<` | Array value contains an element less than given |
410
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
411
+ | `all_gt` | `&>` | All elements of array value are greater than given |
412
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
413
+ | `all_lt` | `&<` | All elements of array value are less than given |
414
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
415
+ | `size_eq` | `#=` | Array value size is equal to given |
416
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
417
+ | `size_gt` | `#>` | Array value size is greater than given |
418
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
419
+ | `size_lt` | `#<` | Array value size is less than given |
420
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
421
+
422
+ #### Decimal
423
+
424
+ | Operation | Operator | Description |
425
+ |------------|----------|-----------------------------------------|
426
+ | `eq` | `=` | Value is equal to given |
427
+ | `not_eq` | `!=` | Value is not equal to given |
428
+ | `gt` | `>` | Value is greater than given |
429
+ | `gteq` | `>=` | Value is greater than or equal to given |
430
+ | `lt` | `<` | Value is less than given |
431
+ | `lteq` | `<=` | Value is greater than or equal to given |
432
+
433
+ #### DecimalArray
434
+
435
+ | Operation | Operator | Description |
436
+ |---------------|----------|----------------------------------------------------------------|
437
+ | `include` | `\|=` | Array value includes given element |
438
+ | `not_include` | `!\|=` | Array value doesn't include given element |
439
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
440
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
441
+ | `any_lt` | `\|<` | Array value contains an element less than given |
442
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
443
+ | `all_gt` | `&>` | All elements of array value are greater than given |
444
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
445
+ | `all_lt` | `&<` | All elements of array value are less than given |
446
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
447
+ | `size_eq` | `#=` | Array value size is equal to given |
448
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
449
+ | `size_gt` | `#>` | Array value size is greater than given |
450
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
451
+ | `size_lt` | `#<` | Array value size is less than given |
452
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
453
+
454
+ #### Enum
455
+
456
+ | Operation | Operator | Description |
457
+ |------------|----------|-----------------------------|
458
+ | `eq` | `=` | Value is equal to given |
459
+ | `not_eq` | `!=` | Value is not equal to given |
460
+
461
+ #### EnumArray
462
+
463
+ | Operation | Operator | Description |
464
+ |---------------|----------|----------------------------------------------------|
465
+ | `include` | `\|=` | Array value includes given element |
466
+ | `not_include` | `!\|=` | Array value doesn't include given element |
467
+ | `size_eq` | `#=` | Array value size is equal to given |
468
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
469
+ | `size_gt` | `#>` | Array value size is greater than given |
470
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
471
+ | `size_lt` | `#<` | Array value size is less than given |
472
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
473
+
474
+ #### Integer
475
+
476
+ | Operation | Operator | Description |
477
+ |------------|----------|-----------------------------------------|
478
+ | `eq` | `=` | Value is equal to given |
479
+ | `not_eq` | `!=` | Value is not equal to given |
480
+ | `gt` | `>` | Value is greater than given |
481
+ | `gteq` | `>=` | Value is greater than or equal to given |
482
+ | `lt` | `<` | Value is less than given |
483
+ | `lteq` | `<=` | Value is greater than or equal to given |
484
+
485
+ #### IntegerArray
486
+
487
+ | Operation | Operator | Description |
488
+ |---------------|----------|----------------------------------------------------------------|
489
+ | `include` | `\|=` | Array value includes given element |
490
+ | `not_include` | `!\|=` | Array value doesn't include given element |
491
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
492
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
493
+ | `any_lt` | `\|<` | Array value contains an element less than given |
494
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
495
+ | `all_gt` | `&>` | All elements of array value are greater than given |
496
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
497
+ | `all_lt` | `&<` | All elements of array value are less than given |
498
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
499
+ | `size_eq` | `#=` | Array value size is equal to given |
500
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
501
+ | `size_gt` | `#>` | Array value size is greater than given |
502
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
503
+ | `size_lt` | `#<` | Array value size is less than given |
504
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
505
+
506
+ #### Text
507
+
508
+ | Operation | Operator | Description |
509
+ |-------------------|----------|-------------------------------------------------------------|
510
+ | `eq` | `=` | Value is equal to given |
511
+ | `not_eq` | `!=` | Value is not equal to given |
512
+ | `start_with` | `^` | Value starts with given substring |
513
+ | `end_with` | `$` | Value ends with given substring |
514
+ | `contain` | `~` | Value contains given substring |
515
+ | `not_start_with` | `!^` | Value doesn't start with given substring |
516
+ | `not_end_with` | `!$` | Value doesn't end with given substring |
517
+ | `not_contain` | `!~` | Value doesn't contain given substring |
518
+ | `istart_with` | `^*` | Value starts with given substring (case-insensitive) |
519
+ | `iend_with` | `$*` | Value ends with given substring (case-insensitive) |
520
+ | `icontain` | `~*` | Value contains given substring (case-insensitive) |
521
+ | `not_istart_with` | `!^*` | Value doesn't start with given substring (case-insensitive) |
522
+ | `not_iend_with` | `!$*` | Value doesn't end with given substring (case-insensitive) |
523
+ | `not_icontain` | `!~*` | Value doesn't contain given substring (case-insensitive) |
524
+
525
+ #### TextArray
526
+
527
+ | Operation | Operator | Description |
528
+ |------------------|----------|-------------------------------------------------------------|
529
+ | `include` | `\|=` | Array value includes given element |
530
+ | `not_include` | `!\|=` | Array value doesn't include given element |
531
+ | `any_start_with` | `\|^` | Array value contains an element starts with given substring |
532
+ | `all_start_with` | `&^` | All elements of array value starts with given substring |
533
+ | `size_eq` | `#=` | Array value size is equal to given |
534
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
535
+ | `size_gt` | `#>` | Array value size is greater than given |
536
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
537
+ | `size_lt` | `#<` | Array value size is less than given |
538
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
539
+
292
540
  ## Configuration
293
541
 
294
542
  ### Limiting Field Types for a Customizable
@@ -348,6 +596,25 @@ class CustomValue < ApplicationRecord
348
596
  end
349
597
  ```
350
598
 
599
+ **Note:** To avoid _STI_ (_Single Table Inheritance_) issues in environments with code reloading (`config.enable_reloading = true`),
600
+ you should ensure that your custom model classes, along with all their superclasses and mix-ins, are non-reloadable.
601
+ Follow these steps:
602
+ - Move your custom model classes to a separate folder, such as `app/models/active_fields`.
603
+ - If your custom model classes subclass `ApplicationRecord` (or other reloadable class) or mix-in reloadable modules,
604
+ move those superclasses and modules to another folder, such as `app/models/core`.
605
+ - After organizing your files, add the following code to your `config/application.rb`:
606
+ ```ruby
607
+ # Disable custom models reloading to avoid STI issues.
608
+ custom_models_dir = "#{root}/app/models/active_fields"
609
+ models_core_dir = "#{root}/app/models/core"
610
+ Rails.autoloaders.main.ignore(custom_models_dir, models_core_dir)
611
+ Rails.autoloaders.once.collapse(custom_models_dir, models_core_dir)
612
+ config.autoload_once_paths += [custom_models_dir, models_core_dir]
613
+ config.eager_load_paths += [custom_models_dir, models_core_dir]
614
+ ```
615
+ This configuration disables namespaces for these folders
616
+ and adds them to `autoload_once_paths`, ensuring they are not reloaded.
617
+
351
618
  ### Adding Custom Field Types
352
619
 
353
620
  To add a custom _Active Field_ type, create a subclass of the `ActiveFields.config.field_base_class`,
@@ -372,6 +639,9 @@ class IpField < ActiveFields.config.field_base_class
372
639
  class_name: "IpCaster",
373
640
  options: -> { { strip: strip? } }, # options that will be passed to the caster
374
641
  },
642
+ finder: { # Optional
643
+ class_name: "IpFinder",
644
+ },
375
645
  )
376
646
 
377
647
  # Store specific attributes in `options`
@@ -423,12 +693,18 @@ class IpArrayField < ActiveFields.config.field_base_class
423
693
  caster: {
424
694
  class_name: "IpArrayCaster",
425
695
  },
696
+ finder: { # Optional
697
+ class_name: "IpArrayFinder",
698
+ },
426
699
  )
427
700
  # ...
428
701
  end
429
702
  ```
430
703
 
431
- For each custom _Active Field_ type, you must define a **validator** and a **caster**:
704
+ **Note:** Similar to custom model classes, you should disable code reloading for custom _Active Field_ type models.
705
+ Place them in the `app/models/active_fields` folder too.
706
+
707
+ For each custom _Active Field_ type, you must define a **validator**, a **caster** and optionally a **finder**:
432
708
 
433
709
  #### Validator
434
710
 
@@ -483,6 +759,96 @@ class IpCaster < ActiveFields::Casters::BaseCaster
483
759
  end
484
760
  ```
485
761
 
762
+ #### Finder
763
+
764
+ To create your custom finder, you should define a class that inherits from one of the following base classes:
765
+ - `ActiveFields::Finders::SingularFinder` - for singular values,
766
+ - `ActiveFields::Finders::ArrayFinder` - for array values,
767
+ - `ActiveFields::Finders::BaseCaster` - if you don’t need built-in helper methods.
768
+
769
+ Finder classes include a DSL for defining search operations and provide helper methods to simplify query building.
770
+ Explore the source code to discover all these methods.
771
+
772
+ ```ruby
773
+ # lib/ip_finder.rb (or anywhere you want)
774
+ class IpFinder < ActiveFields::Finders::SingularFinder
775
+ operation :eq, operator: "=" do |value|
776
+ scope.where(eq(casted_value_field("text"), cast(value)))
777
+ # Equivalent to:
778
+ # if value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(NilClass)
779
+ # scope.where("CAST(active_fields_values.value_meta ->> 'const' AS text) IS ?)", cast(value))
780
+ # else
781
+ # scope.where("CAST(active_fields_values.value_meta ->> 'const' AS text) = ?)", cast(value))
782
+ # end
783
+ end
784
+ operation :not_eq, operator: "!=" do |value|
785
+ scope.where(not_eq(casted_value_field("text"), cast(value)))
786
+ end
787
+
788
+ def cast(value)
789
+ IpCaster.new.deserialize(value)
790
+ end
791
+ end
792
+
793
+ # lib/ip_array_finder.rb (or anywhere you want)
794
+ class IpArrayFinder < ActiveFields::Finders::ArrayFinder
795
+ operation :include, operator: "|=" do |value|
796
+ scope.where(value_match_any("==", cast(value)))
797
+ # Equivalent to:
798
+ # scope.where("jsonb_path_exists(active_fields_values.value_meta -> 'const', ?, ?)", "$[*] ? (@ == $value)", { value: cast(value) }.to_json)
799
+ end
800
+ operation :not_include, operator: "!|=" do |value|
801
+ scope.where.not(value_match_any("==", cast(value)))
802
+ end
803
+ operation :size_eq, operator: "#=" do |value|
804
+ scope.where(value_size_eq(value))
805
+ # Equivalent to:
806
+ # scope.where("jsonb_array_length(active_fields_values.value_meta -> 'const') = ?", value&.to_i)
807
+ end
808
+ operation :size_not_eq, operator: "#!=" do |value|
809
+ scope.where(value_size_not_eq(value))
810
+ end
811
+ operation :size_gt, operator: "#>" do |value|
812
+ scope.where(value_size_gt(value))
813
+ end
814
+ operation :size_gteq, operator: "#>=" do |value|
815
+ scope.where(value_size_gteq(value))
816
+ end
817
+ operation :size_lt, operator: "#<" do |value|
818
+ scope.where(value_size_lt(value))
819
+ end
820
+ operation :size_lteq, operator: "#<=" do |value|
821
+ scope.where(value_size_lteq(value))
822
+ end
823
+
824
+ private
825
+
826
+ def cast(value)
827
+ caster = IpCaster.new
828
+ caster.serialize(caster.deserialize(value))
829
+ end
830
+
831
+ # This method must be defined to utilize the `value_match_any` and `value_match_all` helper methods in your class.
832
+ # It should return a valid JSONPath expression for use in PostgreSQL jsonb query functions.
833
+ def jsonpath(operator) = "$[*] ? (@ #{operator} $value)"
834
+ end
835
+ ```
836
+
837
+ Once defined, every _Active Value_ of this type will support the specified search operations!
838
+
839
+ ```ruby
840
+ # Find customizables
841
+ Author.where_active_fields([
842
+ { name: "main_ip", operator: "eq", value: "127.0.0.1" },
843
+ { n: "all_ips", op: "#>=", v: 5 },
844
+ { name: "all_ips", operator: "|=", value: "0.0.0.0" },
845
+ ])
846
+
847
+ # Find Active Values
848
+ IpFinder.new(active_field: ip_active_field).search(op: "eq", value: "127.0.0.1")
849
+ IpArrayFinder.new(active_field: ip_array_active_field).search(op: "#>=", value: 5)
850
+ ```
851
+
486
852
  ### Localization (I18n)
487
853
 
488
854
  The built-in _validators_ primarily use _Rails_ default error types.
@@ -495,20 +861,16 @@ For an example, refer to the [locale file](https://github.com/lassoid/active_fie
495
861
 
496
862
  ## Current Restrictions
497
863
 
498
- 1. Only _PostgreSQL_ is fully supported.
864
+ 1. Only _PostgreSQL_ 17+ is fully supported.
499
865
 
500
866
  The gem is tested exclusively with _PostgreSQL_. Support for other databases is not guaranteed.
501
867
 
502
- However, you can give it a try! :)
503
-
504
868
  2. Updating some _Active Fields_ options may be unsafe.
505
869
 
506
870
  This could cause existing _Active Values_ to become invalid,
507
871
  leading to the associated _Customizables_ also becoming invalid,
508
872
  which could potentially result in update failures.
509
873
 
510
- 3. Only _Zeitwerk_ autoloading mode is supported.
511
-
512
874
  ## API Overview
513
875
 
514
876
  ### Fields API
@@ -535,6 +897,7 @@ active_field.value_caster_class # Class used for values casting
535
897
  active_field.value_caster # Caster object that performs values casting
536
898
  active_field.customizable_model # Customizable model class
537
899
  active_field.type_name # Identifier of the type of this Active Field (instead of class name)
900
+ active_field.available_customizable_types # Available Customizable types for this Active Field
538
901
 
539
902
  # Scopes:
540
903
  ActiveFields::Field::Boolean.for("Post") # Collection of Active Fields registered for the specified Customizable type
@@ -567,6 +930,9 @@ customizable.active_values # `has_many` association with Active Values linked to
567
930
 
568
931
  # Methods:
569
932
  customizable.active_fields # Collection of Active Fields registered for this record
933
+ Post.active_fields # Collection of Active Fields registered for this model
934
+ Post.allowed_active_fields_type_names # Active Fields type names allowed for this Customizable model
935
+ Post.allowed_active_fields_class_names # Active Fields class names allowed for this Customizable model
570
936
 
571
937
  # Create, update or destroy Active Values.
572
938
  customizable.active_fields_attributes = [
@@ -596,6 +962,15 @@ customizable.active_values_attributes = attributes
596
962
  # This method is useful with `fields_for`, allowing you to pass the collection as an argument to render new Active Values:
597
963
  # `form.fields_for :active_fields, customizable.initialize_active_values`.
598
964
  customizable.initialize_active_values
965
+
966
+ # Query Customizables by Active Values.
967
+ Post.where_active_fields(
968
+ [
969
+ { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
970
+ { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
971
+ { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
972
+ ],
973
+ )
599
974
  ```
600
975
 
601
976
  ### Global Config
@@ -614,14 +989,12 @@ ActiveFields.config.type_class_names # Registered Active Fields class names
614
989
  ActiveFields.config.register_field(:ip, "IpField") # Register a custom Active Field type
615
990
  ```
616
991
 
617
- ### Customizable Config
992
+ ### Registry
618
993
 
619
994
  ```ruby
620
- customizable_model = Post
621
- customizable_model.active_fields_config # Access the Customizable's configuration
622
- customizable_model.active_fields_config.customizable_model # The Customizable model itself
623
- customizable_model.active_fields_config.types # Allowed Active Field types (e.g., `[:boolean]`)
624
- customizable_model.active_fields_config.types_class_names # Allowed Active Field class names (e.g., `[ActiveFields::Field::Boolean]`)
995
+ ActiveFields.registry.add(:boolean, "Post") # Stores relation between Active Field type and customizable type. Please do not use directly.
996
+ ActiveFields.registry.customizable_types_for(:boolean) # Returns Customizable types that allow provided Active Field type name
997
+ ActiveFields.registry.field_type_names_for("Post") # Returns Active Field type names, allowed for given Customizable type
625
998
  ```
626
999
 
627
1000
  ## Development
@@ -11,6 +11,9 @@ module ActiveFields
11
11
  caster: {
12
12
  class_name: "ActiveFields::Casters::BooleanCaster",
13
13
  },
14
+ finder: {
15
+ class_name: "ActiveFields::Finders::BooleanFinder",
16
+ },
14
17
  )
15
18
 
16
19
  store_accessor :options, :required, :nullable