Skip to content

Commit

Permalink
Add registry to store relationships between Active Fields and Customi…
Browse files Browse the repository at this point in the history
…zables (#20)
  • Loading branch information
Exterm1nate authored Feb 22, 2025
1 parent de5172d commit c46a0e1
Show file tree
Hide file tree
Showing 42 changed files with 151 additions and 137 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- Drop support for _Rails_ < 7.1
- Drop support for _Ruby_ < 3.1 (EOL)
- Added search functionality
- Added registry to store relationships between _Customizable_ types and _Active Field_ types

**Breaking changes**:
- Maximum datetime precision reduced to 6 for all _Ruby_/_Rails_ versions.
Expand Down
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Gem downloads count](https://img.shields.io/gem/dt/active_fields)](https://rubygems.org/gems/active_fields)
[![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)

**ActiveFields** is a Rails plugin that implements the Entity-Attribute-Value (EAV) pattern,
**ActiveFields** is a _Rails_ plugin that implements the _Entity-Attribute-Value (EAV)_ pattern,
enabling the addition of custom fields to any model at runtime without requiring changes to the database schema.

## Key Concepts
Expand Down Expand Up @@ -75,8 +75,6 @@ such as booleans, strings, numbers, arrays, etc.
This command generates a controller, routes, views for managing _Active Fields_,
along with form inputs for _Active Values_, search form and some useful helper methods that will be used in next steps.

**Note:** Don't forget to add available _Customizable_ types in generated _Active Fields_ forms.
**Note:** The array field helper and search form use _Stimulus_ for interactivity.
If your app doesn't already include _Stimulus_, you can [easily add it](https://github.com/hotwired/stimulus-rails).
Alternatively, if you prefer not to use _Stimulus_, you should implement your own JavaScript code.
Expand Down Expand Up @@ -122,7 +120,7 @@ such as booleans, strings, numbers, arrays, etc.
```
**Note:** Here we use the `active_fields_attributes=` method (as a permitted parameter),
that integrates well with Rails `fields_for` to generate appropriate form fields.
that integrates well with _Rails_ `fields_for` to generate appropriate form fields.
Alternatively, the alias `active_fields=` can be used in contexts without `fields_for`, such as API controllers.
**Note:** `compact_array_param` is a helper method, that was added by scaffold generator.
Expand Down Expand Up @@ -157,7 +155,7 @@ fill in _Active Values_ within _Customizable_ forms
and search _Customizables_ using their index actions.

You can also explore the [Demo app](https://github.com/lassoid/active_fields/blob/main/spec/dummy)
where the plugin is fully integrated into a full-stack Rails application.
where the plugin is fully integrated into a full-stack _Rails_ application.
Feel free to explore the source code and run it locally:

```shell
Expand Down Expand Up @@ -819,13 +817,13 @@ Once defined, every _Active Value_ of this type will support the specified searc
```ruby
# Find customizables
Author.where_active_values([
{ name: "main_ip", operator: "=", value: "127.0.0.1" },
{ name: "main_ip", operator: "eq", value: "127.0.0.1" },
{ n: "all_ips", op: "#>=", v: 5 },
{ name: "all_ips", operator: "|=", value: "0.0.0.0" },
])
# Find Active Values
IpFinder.new(active_field: ip_active_field).search(op: "=", value: "127.0.0.1")
IpFinder.new(active_field: ip_active_field).search(op: "eq", value: "127.0.0.1")
IpArrayFinder.new(active_field: ip_array_active_field).search(op: "#>=", value: 5)
```
Expand Down Expand Up @@ -877,6 +875,7 @@ active_field.value_caster_class # Class used for values casting
active_field.value_caster # Caster object that performs values casting
active_field.customizable_model # Customizable model class
active_field.type_name # Identifier of the type of this Active Field (instead of class name)
active_field.available_customizable_types # Available Customizable types for this Active Field
# Scopes:
ActiveFields::Field::Boolean.for("Post") # Collection of Active Fields registered for the specified Customizable type
Expand Down Expand Up @@ -910,6 +909,8 @@ customizable.active_values # `has_many` association with Active Values linked to
# Methods:
customizable.active_fields # Collection of Active Fields registered for this record
Post.active_fields # Collection of Active Fields registered for this model
Post.allowed_field_type_names # Active Fields type names allowed for this Customizable model
Post.allowed_field_class_names # Active Fields class names allowed for this Customizable model
# Create, update or destroy Active Values.
customizable.active_fields_attributes = [
Expand Down Expand Up @@ -966,14 +967,12 @@ ActiveFields.config.type_class_names # Registered Active Fields class names
ActiveFields.config.register_field(:ip, "IpField") # Register a custom Active Field type
```
### Customizable Config
### Registry
```ruby
customizable_model = Post
customizable_model.active_fields_config # Access the Customizable's configuration
customizable_model.active_fields_config.customizable_model # The Customizable model itself
customizable_model.active_fields_config.types # Allowed Active Field types (e.g., `[:boolean]`)
customizable_model.active_fields_config.types_class_names # Allowed Active Field class names (e.g., `[ActiveFields::Field::Boolean]`)
ActiveFields.registry.add(:boolean, "Post") # Stores relation between Active Field type and customizable type. Please do not use directly.
ActiveFields.registry.customizable_types_for(:boolean) # Returns Customizable types that allow provided Active Field type name
ActiveFields.registry.field_type_names_for("Post") # Returns Active Field type names, allowed for given Customizable type
```
## Development
Expand Down
13 changes: 12 additions & 1 deletion app/models/concerns/active_fields/customizable_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,19 @@ module CustomizableConcern
end

class_methods do
# Collection of active fields registered for this customizable
def active_fields
ActiveFields.config.field_base_class.for(model_name.name)
ActiveFields.config.field_base_class.for(name)
end

# Returns field type names allowed for this customizable model.
def allowed_field_type_names
ActiveFields.registry.field_type_names_for(name).to_a
end

# Returns field class names allowed for this customizable model.
def allowed_field_class_names
ActiveFields.config.fields.values_at(*allowed_field_type_names)
end
end

Expand Down
19 changes: 17 additions & 2 deletions app/models/concerns/active_fields/field_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ def type_name
ActiveFields.config.fields.key(type)
end

# Returns customizable types that allow this field type.
#
# Notes:
# - The customizable model must be loaded to appear in this list.
# Relationships between customizable models and field types are established in the `has_active_fields` method,
# which is typically called within the customizable model.
# If eager loading is enabled, there should be no issues.
# However, if eager loading is disabled (common in development),
# the list will remain incomplete until all customizable models are loaded.
# - Code reloading may behave incorrectly at times.
# Restart your application if you make changes to the allowed types list in `has_active_fields`.
def available_customizable_types
ActiveFields.registry.customizable_types_for(type_name).to_a
end

private

def validate_default_value
Expand All @@ -107,8 +122,8 @@ def validate_default_value
end

def validate_customizable_model_allows_type
allowed_types = customizable_model&.active_fields_config&.types || []
return true if allowed_types.include?(type_name)
allowed_type_names = ActiveFields.registry.field_type_names_for(customizable_type).to_a
return true if allowed_type_names.include?(type_name)

errors.add(:customizable_type, :inclusion)
false
Expand Down
4 changes: 3 additions & 1 deletion lib/active_fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def eager_load!

eager_autoload do
autoload :Config
autoload :CustomizableConfig
autoload :Registry
autoload :HasActiveFields
end

Expand Down Expand Up @@ -95,5 +95,7 @@ def config
end

alias_method :configure, :config

def registry = Registry.instance
end
end
24 changes: 0 additions & 24 deletions lib/active_fields/customizable_config.rb

This file was deleted.

7 changes: 3 additions & 4 deletions lib/active_fields/has_active_fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ module HasActiveFields
extend ActiveSupport::Concern

class_methods do
attr_reader :active_fields_config

def has_active_fields(types: ActiveFields.config.type_names)
include CustomizableConcern

@active_fields_config = CustomizableConfig.new(self)
@active_fields_config.types = types
types.each do |field_type|
ActiveFields.registry.add(field_type, name)
end
end
end
end
Expand Down
38 changes: 38 additions & 0 deletions lib/active_fields/registry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module ActiveFields
# Storage for configured relations between active fields and customizable models
class Registry
include Singleton

def initialize
@customizables = {}
@fields = {}
end

# Add relation between active field type and customizable type
def add(field_type_name, customizable_type)
if ActiveFields.config.type_names.exclude?(field_type_name)
raise ArgumentError, "undefined ActiveFields type provided for #{customizable_type}: #{field_type_name}"
end

@customizables[field_type_name] ||= Set.new
@customizables[field_type_name] << customizable_type

@fields[customizable_type] ||= Set.new
@fields[customizable_type] << field_type_name

nil
end

# Returns customizable types that allow provided active field type name
def customizable_types_for(field_type_name)
@customizables[field_type_name]
end

# Returns active field type names, allowed for given customizable type
def field_type_names_for(customizable_type)
@fields[customizable_type]
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<div>
<%= f.label :customizable_type %>
<%= f.select :customizable_type, ["TODO: add allowed customizable model names"], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div>
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/app/views/active_fields/forms/_boolean.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<div class="form-input">
<%= f.label :customizable_type %>
<%= f.select :customizable_type, %w[Author Post], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div class="form-input">
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/app/views/active_fields/forms/_date.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<div class="form-input">
<%= f.label :customizable_type %>
<%= f.select :customizable_type, %w[Author], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div class="form-input">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<div class="form-input">
<%= f.label :customizable_type %>
<%= f.select :customizable_type, %w[Author Post], {}, { disabled: active_field.persisted? } %>
<%= f.select :customizable_type, active_field.available_customizable_types, {}, { disabled: active_field.persisted? } %>
</div>

<div class="form-input">
Expand Down
Loading

0 comments on commit c46a0e1

Please sign in to comment.