active_fields 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -1
  3. data/README.md +633 -0
  4. data/app/models/active_fields/field/boolean.rb +10 -3
  5. data/app/models/active_fields/field/date.rb +10 -4
  6. data/app/models/active_fields/field/date_array.rb +12 -18
  7. data/app/models/active_fields/field/date_time.rb +77 -0
  8. data/app/models/active_fields/field/date_time_array.rb +61 -0
  9. data/app/models/active_fields/field/decimal.rb +35 -6
  10. data/app/models/active_fields/field/decimal_array.rb +28 -11
  11. data/app/models/active_fields/field/enum.rb +28 -9
  12. data/app/models/active_fields/field/enum_array.rb +29 -22
  13. data/app/models/active_fields/field/integer.rb +10 -4
  14. data/app/models/active_fields/field/integer_array.rb +12 -8
  15. data/app/models/active_fields/field/text.rb +10 -4
  16. data/app/models/active_fields/field/text_array.rb +14 -8
  17. data/app/models/concerns/active_fields/customizable_concern.rb +60 -51
  18. data/app/models/concerns/active_fields/field_array_concern.rb +25 -0
  19. data/app/models/concerns/active_fields/field_concern.rb +45 -24
  20. data/app/models/concerns/active_fields/value_concern.rb +32 -4
  21. data/db/migrate/20240229230000_create_active_fields_tables.rb +2 -2
  22. data/lib/active_fields/casters/base_caster.rb +8 -2
  23. data/lib/active_fields/casters/date_caster.rb +8 -6
  24. data/lib/active_fields/casters/date_time_array_caster.rb +19 -0
  25. data/lib/active_fields/casters/date_time_caster.rb +43 -0
  26. data/lib/active_fields/casters/decimal_caster.rb +10 -2
  27. data/lib/active_fields/casters/enum_array_caster.rb +13 -1
  28. data/lib/active_fields/casters/enum_caster.rb +15 -1
  29. data/lib/active_fields/casters/integer_caster.rb +2 -2
  30. data/lib/active_fields/config.rb +12 -1
  31. data/lib/active_fields/customizable_config.rb +1 -1
  32. data/lib/active_fields/has_active_fields.rb +1 -1
  33. data/lib/active_fields/validators/base_validator.rb +7 -3
  34. data/lib/active_fields/validators/boolean_validator.rb +2 -2
  35. data/lib/active_fields/validators/date_array_validator.rb +3 -3
  36. data/lib/active_fields/validators/date_time_array_validator.rb +26 -0
  37. data/lib/active_fields/validators/date_time_validator.rb +19 -0
  38. data/lib/active_fields/validators/date_validator.rb +3 -3
  39. data/lib/active_fields/validators/decimal_array_validator.rb +2 -2
  40. data/lib/active_fields/validators/decimal_validator.rb +2 -2
  41. data/lib/active_fields/validators/enum_array_validator.rb +3 -3
  42. data/lib/active_fields/validators/enum_validator.rb +3 -3
  43. data/lib/active_fields/validators/integer_array_validator.rb +2 -2
  44. data/lib/active_fields/validators/integer_validator.rb +2 -2
  45. data/lib/active_fields/validators/text_array_validator.rb +2 -2
  46. data/lib/active_fields/validators/text_validator.rb +2 -2
  47. data/lib/active_fields/version.rb +1 -1
  48. data/lib/active_fields.rb +4 -0
  49. data/lib/generators/active_fields/install/USAGE +5 -0
  50. data/lib/generators/active_fields/install/install_generator.rb +29 -0
  51. metadata +11 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aee118ed966e5912d28dcf8cb2d1db0cdd68d85f7d4fc3bcdf267cc5629c7ff3
4
- data.tar.gz: 99009dea7a5f940034263a85f5f20f76b84911772677dfc69fe97380bd308563
3
+ metadata.gz: e668baeb2df705293feec23b2948fd6ca762bd3e5f6671aec273579133cfb368
4
+ data.tar.gz: a7eac6f5cb2a46e9d246e2d3d8023a9e1995446d92d9c2cf93ab1100f265d283
5
5
  SHA512:
6
- metadata.gz: ca324370030b6028c6a579e4782f37d010e6e01dff7d75054220302e6a36e9aaf35acb30488513f723eadef183ba8fda590890db7bd5f654517ceb03ddfd2a82
7
- data.tar.gz: df6501f049664003e9d02571c6f6652b8eaacf92718038e98eea1daae7ee08ddba92a028f737fac5d08042a410f06445ef996bcf418844099d4003d568a53ed5
6
+ metadata.gz: 1c7007b25907d9f0a25b5ef76bb4b031772a0cdf859243676c242fb57f457dab768b5f629760b68661a3b00aa60601b0bf856911a816313a36a53b74206dbb4b
7
+ data.tar.gz: 83595ef579ea24057de81a0f5b07563eaf77a0e8e2a78da3b30bbf4f0312246c6ca237590f77d279c990c9765f982e8293958b1df73d92996e2f63dc567990c8
data/CHANGELOG.md CHANGED
@@ -1,8 +1,20 @@
1
1
  ## [Unreleased]
2
+ - Precision configuration for decimal fields
3
+ - Added array field types mix-in `ActiveFields::FieldArrayConcern`
4
+ - Fixed enum types behavior for blank values
5
+ - Dummy app
6
+ - Enhanced values storage format
7
+ - Do not implicitly create _Active Values_ when saving an _Active Field_ or _Customizable_
8
+ - Utilize _ActiveRecord_'s nested attributes feature in _Customizable_ models to manage associated _Active Values_
9
+ - Serialize decimals as strings as _ActiveRecord_ does for JSON columns
10
+ - Added datetime and datetime array field types
11
+ - Added fields configuration DSL
12
+ - Introduced new _Customizable_ setter for _Active Values_ (`active_fields_attributes=`) with a more convenient syntax
13
+ to replace the default setter (`active_values_attributes=`) from Rails nested attributes feature.
2
14
 
3
15
  ## [0.2.0] - 2024-06-13
4
16
 
5
- - Rewrited as a Rails plugin!
17
+ - Rewritten as a Rails plugin!
6
18
  - Custom field types support
7
19
  - Global configuration options for changing field and value classes
8
20
  - Per-model configuration option for enabling specific field types only
data/README.md CHANGED
@@ -3,3 +3,636 @@
3
3
  [![Gem Version](https://img.shields.io/gem/v/active_fields?color=blue&label=version)](https://rubygems.org/gems/active_fields)
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
+
7
+ **ActiveFields** is a Rails plugin that implements the Entity-Attribute-Value (EAV) pattern,
8
+ enabling the addition of custom fields to any model at runtime without requiring changes to the database schema.
9
+
10
+ ## Key Concepts
11
+
12
+ - **Active Field**: A record with the definition of a custom field.
13
+ - **Active Value**: A record that stores the value of an _Active Field_ for a specific _Customizable_.
14
+ - **Customizable**: A record that has custom fields.
15
+
16
+ ## Models Structure
17
+
18
+ ```mermaid
19
+ classDiagram
20
+ ActiveValue "*" --> "1" ActiveField
21
+ ActiveValue "*" --> "1" Customizable
22
+
23
+ class ActiveField {
24
+ + string name
25
+ + string type
26
+ + string customizable_type
27
+ + json default_value
28
+ + json options
29
+ }
30
+ class ActiveValue {
31
+ + json value
32
+ }
33
+ class Customizable {
34
+ // This is your model
35
+ }
36
+ ```
37
+
38
+ All values are stored in a JSON (jsonb) field, which is a highly flexible column type capable of storing various data types,
39
+ such as booleans, strings, numbers, arrays, etc.
40
+
41
+ ## Installation
42
+
43
+ 1. Install the gem and add it to your application's Gemfile by running:
44
+
45
+ ```shell
46
+ bundle add active_fields
47
+ ```
48
+
49
+ 2. Run install generator, then run migrations:
50
+
51
+ ```shell
52
+ bin/rails generate active_fields:install
53
+ bin/rails db:migrate
54
+ ```
55
+
56
+ 3. Add the `has_active_fields` method to any models where you want to enable custom fields:
57
+
58
+ ```ruby
59
+ class Author < ApplicationRecord
60
+ has_active_fields
61
+ end
62
+ ```
63
+
64
+ 4. Implement the necessary code to work with _Active Fields_.
65
+
66
+ This plugin provides a convenient API and helpers, allowing you to write code that meets your specific needs
67
+ without being forced to use predefined implementations that is hard to extend.
68
+
69
+ Generally, you should:
70
+ - Implement a controller and UI for managing _Active Fields_.
71
+ - Add inputs for _Active Values_ in _Customizable_ forms and permit their params in the controller.
72
+
73
+ To set _Active Values_ for your _Customizable_, use the `active_fields_attributes=` method,
74
+ that integrates with Rails `fields_for` to generate appropriate form fields.
75
+ Alternatively, the alias `active_fields=` can be used in contexts without `fields_for`, such as APIs.
76
+
77
+ To prepare a collection of _Active Values_ for use with the `fields_for` builder,
78
+ call the `initialize_active_values` method.
79
+
80
+ **Note:** By default, Rails form fields insert an empty string into array (multiple) parameters.
81
+ You’ll need to handle the removal of these empty strings.
82
+
83
+ ```ruby
84
+ # app/controllers/posts_controller.rb
85
+ # ...
86
+
87
+ def new
88
+ @post = Post.new
89
+ @post.initialize_active_values
90
+ end
91
+
92
+ def edit
93
+ @post.initialize_active_values
94
+ end
95
+
96
+ def post_params
97
+ permitted_params = params.require(:post).permit(
98
+ # ...
99
+ active_fields_attributes: [:name, :value, :_destroy, value: []],
100
+ )
101
+ permitted_params[:active_fields_attributes]&.each do |_index, value_attrs|
102
+ value_attrs[:value] = compact_array_param(value_attrs[:value]) if value_attrs[:value].is_a?(Array)
103
+ end
104
+
105
+ permitted_params
106
+ end
107
+
108
+ def compact_array_param(value)
109
+ if value.first == ""
110
+ value[1..-1]
111
+ else
112
+ value
113
+ end
114
+ end
115
+ ```
116
+
117
+ ```erb
118
+ # app/views/posts/_form.html.erb
119
+ # ...
120
+
121
+ <%= form.fields_for :active_fields, post.active_values.sort_by(&:active_field_id), include_id: false do |active_fields_form| %>
122
+ <%= active_fields_form.hidden_field :name %>
123
+ # Render appropriate Active Value input and (optionally) destroy flag here
124
+ <% end %>
125
+
126
+ # ...
127
+ ```
128
+
129
+ You can find a detailed [example](https://github.com/lassoid/active_fields/blob/main/spec/dummy)
130
+ of how to implement this in a full-stack Rails application.
131
+ Feel free to explore the source code and run it locally:
132
+
133
+ ```shell
134
+ spec/dummy/bin/setup
135
+ bin/rails s
136
+ ```
137
+
138
+ ## Field Types
139
+
140
+ The plugin comes with a structured set of _Active Fields_ types:
141
+
142
+ ```mermaid
143
+ classDiagram
144
+ class ActiveField {
145
+ + string name
146
+ + string type
147
+ + string customizable_type
148
+ }
149
+ class Boolean {
150
+ + boolean default_value
151
+ + boolean required
152
+ + boolean nullable
153
+ }
154
+ class Date {
155
+ + date default_value
156
+ + boolean required
157
+ + date min
158
+ + date max
159
+ }
160
+ class DateArray {
161
+ + array~date~ default_value
162
+ + date min
163
+ + date max
164
+ + integer min_size
165
+ + integer max_size
166
+ }
167
+ class DateTime {
168
+ + datetime default_value
169
+ + boolean required
170
+ + datetime min
171
+ + datetime max
172
+ + integer precision
173
+ }
174
+ class DateTimeArray {
175
+ + array~datetime~ default_value
176
+ + datetime min
177
+ + datetime max
178
+ + integer precision
179
+ + integer min_size
180
+ + integer max_size
181
+ }
182
+ class Decimal {
183
+ + decimal default_value
184
+ + boolean required
185
+ + decimal min
186
+ + decimal max
187
+ + integer precision
188
+ }
189
+ class DecimalArray {
190
+ + array~decimal~ default_value
191
+ + decimal min
192
+ + decimal max
193
+ + integer precision
194
+ + integer min_size
195
+ + integer max_size
196
+ }
197
+ class Enum {
198
+ + string default_value
199
+ + boolean required
200
+ + array~string~ allowed_values
201
+ }
202
+ class EnumArray {
203
+ + array~string~ default_value
204
+ + array~string~ allowed_values
205
+ + integer min_size
206
+ + integer max_size
207
+ }
208
+ class Integer {
209
+ + integer default_value
210
+ + boolean required
211
+ + integer min
212
+ + integer max
213
+ }
214
+ class IntegerArray {
215
+ + array~integer~ default_value
216
+ + integer min
217
+ + integer max
218
+ + integer min_size
219
+ + integer max_size
220
+ }
221
+ class Text {
222
+ + string default_value
223
+ + boolean required
224
+ + integer min_length
225
+ + integer max_length
226
+ }
227
+ class TextArray {
228
+ + array~string~ default_value
229
+ + integer min_length
230
+ + integer max_length
231
+ + integer min_size
232
+ + integer max_size
233
+ }
234
+
235
+ ActiveField <|-- Boolean
236
+ ActiveField <|-- Date
237
+ ActiveField <|-- DateArray
238
+ ActiveField <|-- DateTime
239
+ ActiveField <|-- DateTimeArray
240
+ ActiveField <|-- Decimal
241
+ ActiveField <|-- DecimalArray
242
+ ActiveField <|-- Enum
243
+ ActiveField <|-- EnumArray
244
+ ActiveField <|-- Integer
245
+ ActiveField <|-- IntegerArray
246
+ ActiveField <|-- Text
247
+ ActiveField <|-- TextArray
248
+ ```
249
+
250
+ ### Fields Base Attributes
251
+ - `name`(`string`)
252
+ - `type`(`string`)
253
+ - `customizable_type`(`string`)
254
+ - `default_value` (`json`)
255
+
256
+ ### Field Types Summary
257
+
258
+ All _Active Field_ model names start with `ActiveFields::Field`.
259
+ We replace it with `**` for conciseness.
260
+
261
+ | Active Field model | Type name | Attributes | Options |
262
+ |---------------------------------|------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
263
+ | `**::Boolean` | `boolean` | `default_value`<br>(`boolean` or `nil`) | `required`(`boolean`) - the value must not be `false`<br>`nullable`(`boolean`) - the value could be `nil` |
264
+ | `**::Date` | `date` | `default_value`<br>(`date` or `nil`) | `required`(`boolean`) - the value must not be `nil`<br>`min`(`date`) - minimum value allowed<br>`max`(`date`) - maximum value allowed |
265
+ | `**::DateArray` | `date_array` | `default_value`<br>(`array[date]`) | `min`(`date`) - minimum value allowed, for each element<br>`max`(`date`) - maximum value allowed, for each element<br>`min_size`(`integer`) - minimum value size<br>`max_size`(`integer`) - maximum value size |
266
+ | `**::DateTime` | `datetime` | `default_value`<br>(`datetime` or `nil`) | `required`(`boolean`) - the value must not be `nil`<br>`min`(`datetime`) - minimum value allowed<br>`max`(`datetime`) - maximum value allowed<br>`precision`(`integer`) - the number of digits in fractional seconds |
267
+ | `**::DateTimeArray` | `datetime_array` | `default_value`<br>(`array[datetime]`) | `min`(`datetime`) - minimum value allowed, for each element<br>`max`(`datetime`) - maximum value allowed, for each element<br>`precision`(`integer`) - the number of digits in fractional seconds, for each element<br>`min_size`(`integer`) - minimum value size<br>`max_size`(`integer`) - maximum value size |
268
+ | `**::Decimal` | `decimal` | `default_value`<br>(`decimal` or `nil`) | `required`(`boolean`) - the value must not be `nil`<br>`min`(`decimal`) - minimum value allowed<br>`max`(`decimal`) - maximum value allowed<br>`precision`(`integer`) - the number of digits after the decimal point |
269
+ | `**::DecimalArray` | `decimal_array` | `default_value`<br>(`array[decimal]`) | `min`(`decimal`) - minimum value allowed, for each element<br>`max`(`decimal`) - maximum value allowed, for each element<br>`precision`(`integer`) - the number of digits after the decimal point, for each element<br>`min_size`(`integer`) - minimum value size<br>`max_size`(`integer`) - maximum value size |
270
+ | `**::Enum` | `enum` | `default_value`<br>(`string` or `nil`) | `required`(`boolean`) - the value must not be `nil`<br>**\***`allowed_values`(`array[string]`) - a list of allowed values |
271
+ | `**::EnumArray` | `enum_array` | `default_value`<br>(`array[string]`) | **\***`allowed_values`(`array[string]`) - a list of allowed values<br>`min_size`(`integer`) - minimum value size<br>`max_size`(`integer`) - maximum value size |
272
+ | `**::Integer` | `integer` | `default_value`<br>(`integer` or `nil`) | `required`(`boolean`) - the value must not be `nil`<br>`min`(`integer`) - minimum value allowed<br>`max`(`integer`) - maximum value allowed |
273
+ | `**::IntegerArray` | `integer_array` | `default_value`<br>(`array[integer]`) | `min`(`integer`) - minimum value allowed, for each element<br>`max`(`integer`) - maximum value allowed, for each element<br>`min_size`(`integer`) - minimum value size<br>`max_size`(`integer`) - maximum value size |
274
+ | `**::Text` | `text` | `default_value`<br>(`string` or `nil`) | `required`(`boolean`) - the value must not be `nil`<br>`min_length`(`integer`) - minimum value length allowed<br>`max_length`(`integer`) - maximum value length allowed |
275
+ | `**::TextArray` | `text_array` | `default_value`<br>(`array[string]`) | `min_length`(`integer`) - minimum value length allowed, for each element<br>`max_length`(`integer`) - maximum value length allowed, for each element<br>`min_size`(`integer`) - minimum value size<br>`max_size`(`integer`) - maximum value size |
276
+ | _Your custom class can be here_ | _..._ | _..._ | _..._ |
277
+
278
+ **Note:** Options marked with **\*** are mandatory.
279
+
280
+ ## Configuration
281
+
282
+ ### Limiting Field Types for a Customizable
283
+
284
+ You can restrict the allowed _Active Field_ types for a _Customizable_ by passing _type names_ to the `types` argument in the `has_active_fields` method:
285
+
286
+ ```ruby
287
+ class Post < ApplicationRecord
288
+ has_active_fields types: %i[boolean date_array integer your_custom_field_type_name]
289
+ # ...
290
+ end
291
+ ```
292
+
293
+ Attempting to save an _Active Field_ with a disallowed type will result in a validation error:
294
+
295
+ ```ruby
296
+ active_field = ActiveFields::Field::Date.new(name: "date", customizable_type: "Post")
297
+ active_field.valid? #=> false
298
+ active_field.errors.messages #=> {:customizable_type=>["is not included in the list"]}
299
+ ```
300
+
301
+ ### Customizing Internal Model Classes
302
+
303
+ You can extend the functionality of _Active Fields_ and _Active Values_ by changing their classes.
304
+ By default, _Active Fields_ inherit from `ActiveFields::Field::Base` (utilizing STI),
305
+ and _Active Values_ class is `ActiveFields::Value`.
306
+ You should include the mix-ins `ActiveFields::FieldConcern` and `ActiveFields::ValueConcern`
307
+ in your custom models to add the necessary functionality.
308
+
309
+ ```ruby
310
+ # config/initializers/active_fields.rb
311
+ ActiveFields.configure do |config|
312
+ config.field_base_class_name = "CustomField"
313
+ config.value_class_name = "CustomValue"
314
+ end
315
+
316
+ # app/models/custom_field.rb
317
+ class CustomField < ApplicationRecord
318
+ self.table_name = "active_fields" # Ensure the model uses the correct table
319
+
320
+ include ActiveFields::FieldConcern
321
+
322
+ # Your custom code to extend Active Fields
323
+ def label = name.titleize
324
+ # ...
325
+ end
326
+
327
+ # app/models/custom_value.rb
328
+ class CustomValue < ApplicationRecord
329
+ self.table_name = "active_fields_values" # Ensure the model uses the correct table
330
+
331
+ include ActiveFields::ValueConcern
332
+
333
+ # Your custom code to extend Active Values
334
+ def label = active_field.label
335
+ # ...
336
+ end
337
+ ```
338
+
339
+ ### Adding Custom Field Types
340
+
341
+ To add a custom _Active Field_ type, create a subclass of the `ActiveFields.config.field_base_class`,
342
+ register it in the global configuration and configure the field by calling `acts_as_active_field`.
343
+
344
+ ```ruby
345
+ # config/initializers/active_fields.rb
346
+ ActiveFields.configure do |config|
347
+ # The first argument - field type name, the second - field class name
348
+ config.register_field :ip, "IpField"
349
+ end
350
+
351
+ # app/models/ip_field.rb
352
+ class IpField < ActiveFields.config.field_base_class
353
+ # Configure the field
354
+ acts_as_active_field(
355
+ validator: {
356
+ class_name: "IpValidator",
357
+ options: -> { { required: required? } }, # options that will be passed to the validator
358
+ },
359
+ caster: {
360
+ class_name: "IpCaster",
361
+ options: -> { { strip: strip? } }, # options that will be passed to the caster
362
+ },
363
+ )
364
+
365
+ # Store specific attributes in `options`
366
+ store_accessor :options, :required, :strip
367
+
368
+ # You can use built-in casters to cast your options
369
+ %i[required strip].each do |column|
370
+ define_method(column) do
371
+ ActiveFields::Casters::BooleanCaster.new.deserialize(super())
372
+ end
373
+
374
+ define_method(:"#{column}?") do
375
+ !!public_send(column)
376
+ end
377
+
378
+ define_method(:"#{column}=") do |other|
379
+ super(ActiveFields::Casters::BooleanCaster.new.serialize(other))
380
+ end
381
+ end
382
+
383
+ private
384
+
385
+ # This method allows you to assign default values to your options.
386
+ # It is automatically executed within the `after_initialize` callback.
387
+ def set_defaults
388
+ self.required ||= false
389
+ self.strip ||= true
390
+ end
391
+ end
392
+ ```
393
+
394
+ To create an array _Active Field_ type, pass the `array: true` option to `acts_as_active_field`.
395
+ This will add `min_size` and `max_size` options, as well as some important internal methods such as `array?`.
396
+
397
+ ```ruby
398
+ # config/initializers/active_fields.rb
399
+ ActiveFields.configure do |config|
400
+ config.register_field :ip_array, "IpArrayField"
401
+ end
402
+
403
+ # app/models/ip_array_field.rb
404
+ class IpArrayField < ActiveFields.config.field_base_class
405
+ acts_as_active_field(
406
+ array: true,
407
+ validator: {
408
+ class_name: "IpArrayValidator",
409
+ options: -> { { min_size: min_size, max_size: max_size } },
410
+ },
411
+ caster: {
412
+ class_name: "IpArrayCaster",
413
+ },
414
+ )
415
+ # ...
416
+ end
417
+ ```
418
+
419
+ For each custom _Active Field_ type, you must define a **validator** and a **caster**:
420
+
421
+ #### Validator
422
+
423
+ Create a class that inherits from `ActiveFields::Validators::BaseValidator` and implements the `perform_validation` method.
424
+ This method is responsible for validating `active_field.default_value` and `active_value.value`, and adding any errors to the `errors` set.
425
+ These errors will then propagate to the corresponding record.
426
+ Each error should match the arguments format of the _ActiveModel_ `errors.add` method.
427
+
428
+ ```ruby
429
+ # lib/ip_validator.rb (or anywhere you want)
430
+ class IpValidator < ActiveFields::Validators::BaseValidator
431
+ private
432
+
433
+ def perform_validation(value)
434
+ if value.nil?
435
+ if options[:required]
436
+ errors << :required # type only
437
+ end
438
+ elsif value.is_a?(String)
439
+ unless value.match?(Resolv::IPv4::Regex)
440
+ errors << [:invalid, message: "doesn't match the IPv4 format"] # type with options
441
+ end
442
+ else
443
+ errors << :invalid
444
+ end
445
+ end
446
+ end
447
+ ```
448
+
449
+ #### Caster
450
+
451
+ Create a class that inherits from `ActiveFields::Casters::BaseCaster`
452
+ and implements methods `serialize` (used when setting a value) and `deserialize` (used when retrieving a value).
453
+ These methods handle the conversion of `active_field.default_value` and `active_value.value`.
454
+
455
+ ```ruby
456
+ # lib/ip_caster.rb (or anywhere you want)
457
+ class IpCaster < ActiveFields::Casters::BaseCaster
458
+ def serialize(value)
459
+ value = value&.to_s
460
+ value = value&.strip if options[:strip]
461
+
462
+ value
463
+ end
464
+
465
+ def deserialize(value)
466
+ value = value&.to_s
467
+ value = value&.strip if options[:strip]
468
+
469
+ value
470
+ end
471
+ end
472
+ ```
473
+
474
+ ### Localization (I18n)
475
+
476
+ The built-in _validators_ primarily use _Rails_ default error types.
477
+ However, there are some custom error types that you’ll need to handle in your locale files:
478
+ - `size_too_short` (args: `count`): Triggered when the size of an array _Active Field_ value is smaller than the allowed minimum.
479
+ - `size_too_long` (args: `count`): Triggered when the size of an array _Active Field_ value exceeds the allowed maximum.
480
+ - `duplicate`: Triggered when an enum array _Active Field_ contains duplicate elements.
481
+
482
+ For an example, refer to the [locale file](https://github.com/lassoid/active_fields/blob/main/spec/dummy/config/locales/en.yml).
483
+
484
+ ## Current Restrictions
485
+
486
+ 1. Only _PostgreSQL_ is fully supported.
487
+
488
+ The gem is tested exclusively with _PostgreSQL_. Support for other databases is not guaranteed.
489
+
490
+ However, you can give it a try! :)
491
+
492
+ 2. Updating some _Active Fields_ options may be unsafe.
493
+
494
+ This could cause existing _Active Values_ to become invalid,
495
+ leading to the associated _Customizables_ also becoming invalid,
496
+ which could potentially result in update failures.
497
+
498
+ ## API Overview
499
+
500
+ ### Fields API
501
+
502
+ ```ruby
503
+ active_field = ActiveFields::Field::Boolean.take
504
+
505
+ # Associations:
506
+ active_field.active_values # `has_many` association with Active Values associated with this Active Field
507
+
508
+ # Attributes:
509
+ active_field.type # Class name of this Active Field (utilizing STI)
510
+ active_field.customizable_type # Name of the Customizable model this Active Field is registered to
511
+ active_field.name # Identifier of this Active Field, it should be unique in scope of customizable_type
512
+ active_field.default_value_meta # JSON column declaring the default value. Consider using `default_value` instead
513
+ active_field.options # A hash (json) containing type-specific attributes for this Active Field
514
+
515
+ # Methods:
516
+ active_field.default_value # Default value for all Active Values associated with this Active Field
517
+ active_field.array? # Returns whether the Active Field type is an array
518
+ active_field.value_validator_class # Class used for values validation
519
+ active_field.value_validator # Validator object that performs values validation
520
+ active_field.value_caster_class # Class used for values casting
521
+ active_field.value_caster # Caster object that performs values casting
522
+ active_field.customizable_model # Customizable model class
523
+ active_field.type_name # Identifier of the type of this Active Field (instead of class name)
524
+
525
+ # Scopes:
526
+ ActiveFields::Field::Boolean.for("Author") # Collection of Active Fields registered for the specified Customizable type
527
+ ```
528
+
529
+ ### Values API
530
+
531
+ ```ruby
532
+ active_value = ActiveFields::Value.take
533
+
534
+ # Associations:
535
+ active_value.active_field # `belongs_to` association with the associated Active Field
536
+ active_value.customizable # `belongs_to` association with the associated Customizable
537
+
538
+ # Attributes:
539
+ active_value.value_meta # JSON column declaring the value. Consider using `value` instead
540
+
541
+ # Methods:
542
+ active_value.value # The value of this Active Value
543
+ active_value.name # Name of the associated Active Field
544
+ ```
545
+
546
+ ### Customizable API
547
+
548
+ ```ruby
549
+ customizable = Author.take
550
+
551
+ # Associations:
552
+ customizable.active_values # `has_many` association with Active Values linked to this Customizable
553
+
554
+ # Methods:
555
+ customizable.active_fields # Collection of Active Fields registered for this record
556
+
557
+ # Create, update or destroy Active Values.
558
+ customizable.active_fields_attributes = [
559
+ { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
560
+ { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
561
+ { name: "date", _destroy: true }, # destroy (symbol keys)
562
+ { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
563
+ permitted_params, # params could be passed, but they must be permitted
564
+ ]
565
+
566
+ # Alias of `#active_fields_attributes=`.
567
+ customizable.active_fields = [
568
+ { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
569
+ { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
570
+ { name: "date", _destroy: true }, # destroy (symbol keys)
571
+ { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
572
+ permitted_params, # params could be passed, but they must be permitted
573
+ ]
574
+
575
+ # Create, update or destroy Active Values.
576
+ # Implemented by `accepts_nested_attributes_for`.
577
+ # Please use `active_fields_attributes=`/`active_fields=` instead.
578
+ customizable.active_values_attributes = attributes
579
+
580
+ # Build an Active Value, if it doesn't exist, with the default value for each Active Field.
581
+ # This method is useful with `fields_for`, allowing you to pass the collection as an argument to render new Active Values:
582
+ # `form.fields_for :active_values, customizable.active_values`.
583
+ customizable.initialize_active_values
584
+ ```
585
+
586
+ ### Global Config
587
+
588
+ ```ruby
589
+ ActiveFields.config # Access the plugin's global configuration
590
+ ActiveFields.config.fields # Registered Active Fields types (type_name => field_class)
591
+ ActiveFields.config.field_base_class # Base class for all Active Fields
592
+ ActiveFields.config.field_base_class_name # Name of the Active Fields base class
593
+ ActiveFields.config.value_class # Active Values class
594
+ ActiveFields.config.value_class_name # Name of the Active Values class
595
+ ActiveFields.config.field_base_class_changed? # Check if the Active Fields base class has changed
596
+ ActiveFields.config.value_class_changed? # Check if the Active Values class has changed
597
+ ActiveFields.config.type_names # Registered Active Fields type names
598
+ ActiveFields.config.type_class_names # Registered Active Fields class names
599
+ ActiveFields.config.register_field(:ip, "IpField") # Register a custom Active Field type
600
+ ```
601
+
602
+ ### Customizable Config
603
+
604
+ ```ruby
605
+ customizable_model = Author
606
+ customizable_model.active_fields_config # Access the Customizable's configuration
607
+ customizable_model.active_fields_config.customizable_model # The Customizable model itself
608
+ customizable_model.active_fields_config.types # Allowed Active Field types (e.g., `[:boolean]`)
609
+ customizable_model.active_fields_config.types_class_names # Allowed Active Field class names (e.g., `[ActiveFields::Field::Boolean]`)
610
+ ```
611
+
612
+ ## Development
613
+
614
+ After checking out the repo, run `spec/dummy/bin/setup` to setup the environment.
615
+ Then, run `bin/rspec` to run the tests.
616
+ You can also run `bin/rubocop` to lint the source code,
617
+ `bin/rails c` for an interactive prompt that will allow you to experiment
618
+ and `bin/rails s` to start the Dummy app with plugin already enabled and configured.
619
+
620
+ To install this gem onto your local machine, run `bin/rake install`.
621
+ To release a new version, update the version number in `version.rb`, and then run `bin/rake release`,
622
+ which will create a git tag for the version, push git commits and the created tag,
623
+ and push the `.gem` file to [rubygems.org](https://rubygems.org).
624
+
625
+ ## Contributing
626
+
627
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lassoid/active_fields.
628
+ This project is intended to be a safe, welcoming space for collaboration, and contributors
629
+ are expected to adhere to the [code of conduct](https://github.com/lassoid/active_fields/blob/main/CODE_OF_CONDUCT.md).
630
+
631
+ ## License
632
+
633
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
634
+
635
+ ## Code of Conduct
636
+
637
+ Everyone interacting in the ActiveFields project's codebases, issue trackers, chat rooms and mailing lists
638
+ is expected to follow the [code of conduct](https://github.com/lassoid/active_fields/blob/main/CODE_OF_CONDUCT.md).
@@ -3,10 +3,17 @@
3
3
  module ActiveFields
4
4
  module Field
5
5
  class Boolean < ActiveFields.config.field_base_class
6
- store_accessor :options, :required, :nullable
6
+ acts_as_active_field(
7
+ validator: {
8
+ class_name: "ActiveFields::Validators::BooleanValidator",
9
+ options: -> { { required: required?, nullable: nullable? } },
10
+ },
11
+ caster: {
12
+ class_name: "ActiveFields::Casters::BooleanCaster",
13
+ },
14
+ )
7
15
 
8
- # attribute :required, :boolean, default: false
9
- # attribute :nullable, :boolean, default: false
16
+ store_accessor :options, :required, :nullable
10
17
 
11
18
  validates :required, exclusion: [nil]
12
19
  validates :nullable, exclusion: [nil]