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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +633 -0
- data/app/models/active_fields/field/boolean.rb +10 -3
- data/app/models/active_fields/field/date.rb +10 -4
- data/app/models/active_fields/field/date_array.rb +12 -18
- data/app/models/active_fields/field/date_time.rb +77 -0
- data/app/models/active_fields/field/date_time_array.rb +61 -0
- data/app/models/active_fields/field/decimal.rb +35 -6
- data/app/models/active_fields/field/decimal_array.rb +28 -11
- data/app/models/active_fields/field/enum.rb +28 -9
- data/app/models/active_fields/field/enum_array.rb +29 -22
- data/app/models/active_fields/field/integer.rb +10 -4
- data/app/models/active_fields/field/integer_array.rb +12 -8
- data/app/models/active_fields/field/text.rb +10 -4
- data/app/models/active_fields/field/text_array.rb +14 -8
- data/app/models/concerns/active_fields/customizable_concern.rb +60 -51
- data/app/models/concerns/active_fields/field_array_concern.rb +25 -0
- data/app/models/concerns/active_fields/field_concern.rb +45 -24
- data/app/models/concerns/active_fields/value_concern.rb +32 -4
- data/db/migrate/20240229230000_create_active_fields_tables.rb +2 -2
- data/lib/active_fields/casters/base_caster.rb +8 -2
- data/lib/active_fields/casters/date_caster.rb +8 -6
- data/lib/active_fields/casters/date_time_array_caster.rb +19 -0
- data/lib/active_fields/casters/date_time_caster.rb +43 -0
- data/lib/active_fields/casters/decimal_caster.rb +10 -2
- data/lib/active_fields/casters/enum_array_caster.rb +13 -1
- data/lib/active_fields/casters/enum_caster.rb +15 -1
- data/lib/active_fields/casters/integer_caster.rb +2 -2
- data/lib/active_fields/config.rb +12 -1
- data/lib/active_fields/customizable_config.rb +1 -1
- data/lib/active_fields/has_active_fields.rb +1 -1
- data/lib/active_fields/validators/base_validator.rb +7 -3
- data/lib/active_fields/validators/boolean_validator.rb +2 -2
- data/lib/active_fields/validators/date_array_validator.rb +3 -3
- data/lib/active_fields/validators/date_time_array_validator.rb +26 -0
- data/lib/active_fields/validators/date_time_validator.rb +19 -0
- data/lib/active_fields/validators/date_validator.rb +3 -3
- data/lib/active_fields/validators/decimal_array_validator.rb +2 -2
- data/lib/active_fields/validators/decimal_validator.rb +2 -2
- data/lib/active_fields/validators/enum_array_validator.rb +3 -3
- data/lib/active_fields/validators/enum_validator.rb +3 -3
- data/lib/active_fields/validators/integer_array_validator.rb +2 -2
- data/lib/active_fields/validators/integer_validator.rb +2 -2
- data/lib/active_fields/validators/text_array_validator.rb +2 -2
- data/lib/active_fields/validators/text_validator.rb +2 -2
- data/lib/active_fields/version.rb +1 -1
- data/lib/active_fields.rb +4 -0
- data/lib/generators/active_fields/install/USAGE +5 -0
- data/lib/generators/active_fields/install/install_generator.rb +29 -0
- metadata +11 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e668baeb2df705293feec23b2948fd6ca762bd3e5f6671aec273579133cfb368
|
4
|
+
data.tar.gz: a7eac6f5cb2a46e9d246e2d3d8023a9e1995446d92d9c2cf93ab1100f265d283
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
-
|
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
|
[](https://rubygems.org/gems/active_fields)
|
4
4
|
[](https://rubygems.org/gems/active_fields)
|
5
5
|
[](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
|
-
|
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
|
-
|
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]
|