active_fields 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/CHANGELOG.md +27 -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 -3
  20. data/app/models/concerns/active_fields/field_concern.rb +26 -3
  21. data/db/migrate/20240229230000_create_active_fields_tables.rb +1 -1
  22. data/lib/active_fields/casters/date_time_caster.rb +1 -3
  23. data/lib/active_fields/casters/decimal_caster.rb +2 -5
  24. data/lib/active_fields/constants.rb +55 -0
  25. data/lib/active_fields/engine.rb +2 -1
  26. data/lib/active_fields/finders/array_finder.rb +112 -0
  27. data/lib/active_fields/finders/base_finder.rb +73 -0
  28. data/lib/active_fields/finders/boolean_finder.rb +20 -0
  29. data/lib/active_fields/finders/date_array_finder.rb +65 -0
  30. data/lib/active_fields/finders/date_finder.rb +32 -0
  31. data/lib/active_fields/finders/date_time_array_finder.rb +65 -0
  32. data/lib/active_fields/finders/date_time_finder.rb +32 -0
  33. data/lib/active_fields/finders/decimal_array_finder.rb +65 -0
  34. data/lib/active_fields/finders/decimal_finder.rb +32 -0
  35. data/lib/active_fields/finders/enum_array_finder.rb +41 -0
  36. data/lib/active_fields/finders/enum_finder.rb +20 -0
  37. data/lib/active_fields/finders/integer_array_finder.rb +65 -0
  38. data/lib/active_fields/finders/integer_finder.rb +32 -0
  39. data/lib/active_fields/finders/singular_finder.rb +66 -0
  40. data/lib/active_fields/finders/text_array_finder.rb +47 -0
  41. data/lib/active_fields/finders/text_finder.rb +81 -0
  42. data/lib/active_fields/has_active_fields.rb +3 -4
  43. data/lib/active_fields/registry.rb +38 -0
  44. data/lib/active_fields/version.rb +1 -1
  45. data/lib/active_fields.rb +29 -1
  46. data/lib/generators/active_fields/scaffold/scaffold_generator.rb +9 -0
  47. data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +0 -10
  48. data/lib/generators/active_fields/scaffold/templates/controllers/concerns/active_fields_controller_concern.rb +33 -0
  49. data/lib/generators/active_fields/scaffold/templates/helpers/active_fields_helper.rb +67 -0
  50. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_fields_finders_form_controller.js +59 -0
  51. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/_form.html.erb +42 -0
  52. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_array_size.html.erb +16 -0
  53. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_boolean.html.erb +21 -0
  54. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date.html.erb +16 -0
  55. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date_array.html.erb +16 -0
  56. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime.html.erb +16 -0
  57. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime_array.html.erb +16 -0
  58. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal.html.erb +16 -0
  59. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal_array.html.erb +16 -0
  60. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum.html.erb +16 -0
  61. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum_array.html.erb +16 -0
  62. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer.html.erb +16 -0
  63. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer_array.html.erb +16 -0
  64. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text.html.erb +16 -0
  65. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text_array.html.erb +16 -0
  66. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +1 -1
  67. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +1 -1
  68. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +1 -1
  69. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +2 -2
  70. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +2 -2
  71. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +2 -2
  72. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +2 -2
  73. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +1 -1
  74. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +1 -1
  75. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +1 -1
  76. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +1 -1
  77. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +1 -1
  78. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +1 -1
  79. data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +1 -1
  80. metadata +42 -10
  81. 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: 1d32fccb9d7af7eab0db169ea67d7046e09d6f8eace84e0a2c49f32574377543
4
+ data.tar.gz: 0fa74f41f48702355d9fc73f64d49389433f03e94b62e5d3e2552fcff01ec7f3
5
5
  SHA512:
6
- metadata.gz: 4cc1b2984a7912497bc1b26cec3fc793bb897cc813c76440b5a8513834acae27d5c4df5da9c5f87cf9b36ae79351cacb52ab60965195fa6b44ce6ec8fc4f3c4e
7
- data.tar.gz: 2f03d79bb28e91af86a99d36ff60a992b14ecf9c13d6d8a13eb92490790c4cd71afb11465173ba8a2a756cc24c3cf50c488a13bc8fca90d619b1079ec88fe216
6
+ metadata.gz: 6e76f8e6e24e1b65a8d16480b06ce348c0d777dae4be55ec4fd908d82b17752d69866d9e73cee2847dafa1b04800977d34048eac8f0e7efc61644ddc2f30ae07
7
+ data.tar.gz: 673b3a37fcd8f2a1e0ee0d40bb3f80192d2a24933e097aae4d76d1a34d9fb04850a2b5504d2301b70cdbf2226a43bd96fd0bda0e58314aaf6ff3f54d2b7e0237
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,4 +1,29 @@
1
1
  ## [Unreleased]
2
+ - Drop support for _Rails_ < 7.1
3
+ - Drop support for _Ruby_ < 3.1 (EOL)
4
+ - Added search functionality
5
+ - Added registry to store relationships between _Customizable_ types and _Active Field_ types
6
+ - Added notes about the necessity of disabling reloading for custom model classes and custom _Active Field_ type models
7
+ to prevent _STI_ (_Single Table Inheritance_) issues
8
+
9
+ **Breaking changes**:
10
+ - Maximum datetime precision reduced to 6 for all _Ruby_/_Rails_ versions.
11
+
12
+ While _Ruby_ allows up to 9 fractional seconds, most databases, including _PostgreSQL_, support only 6.
13
+ To ensure compatibility and prevent potential issues,
14
+ we are standardizing the precision to the minimum supported across our technology stack.
15
+
16
+ - Maximum datetime precision constant relocated.
17
+
18
+ The maximum precision value has been moved
19
+ from `ActiveFields::Casters::DateTimeCaster::MAX_PRECISION` to `ActiveFields::MAX_DATETIME_PRECISION`.
20
+
21
+ - Maximum decimal precision set to 16383 (2**14 - 1).
22
+
23
+ While _Ruby_'s `BigDecimal` class allows extremely high precision,
24
+ PostgreSQL supports a maximum of 16383 digits after the decimal point.
25
+ To ensure compatibility, we are capping the precision at this value.
26
+ The maximum precision value is now accessible via `ActiveFields::MAX_DECIMAL_PRECISION`.
2
27
 
3
28
  ## [1.1.0] - 2024-09-10
4
29
  - Added scaffold generator
@@ -16,11 +41,11 @@
16
41
  - Added datetime and datetime array field types
17
42
  - Added fields configuration DSL
18
43
  - 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.
44
+ to replace the default setter (`active_values_attributes=`) from _Rails_ nested attributes feature
20
45
 
21
46
  ## [0.2.0] - 2024-06-13
22
47
 
23
- - Rewritten as a Rails plugin!
48
+ - Rewritten as a _Rails_ plugin!
24
49
  - Custom field types support
25
50
  - Global configuration options for changing field and value classes
26
51
  - 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