active_fields 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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]