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.
- 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
|
[![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
|
-
|
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]
|