From c46a0e1d2fcda5e7a9b520dff27d6b2fa9dd8268 Mon Sep 17 00:00:00 2001 From: Kirill Usanov <48535087+Exterm1nate@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:34:22 +0300 Subject: [PATCH] Add registry to store relationships between Active Fields and Customizables (#20) --- CHANGELOG.md | 1 + README.md | 25 ++++++----- .../active_fields/customizable_concern.rb | 13 +++++- .../concerns/active_fields/field_concern.rb | 19 ++++++++- lib/active_fields.rb | 4 +- lib/active_fields/customizable_config.rb | 24 ----------- lib/active_fields/has_active_fields.rb | 7 ++-- lib/active_fields/registry.rb | 38 +++++++++++++++++ .../active_fields/forms/_boolean.html.erb | 2 +- .../views/active_fields/forms/_date.html.erb | 2 +- .../active_fields/forms/_date_array.html.erb | 2 +- .../active_fields/forms/_datetime.html.erb | 2 +- .../forms/_datetime_array.html.erb | 2 +- .../active_fields/forms/_decimal.html.erb | 2 +- .../forms/_decimal_array.html.erb | 2 +- .../views/active_fields/forms/_enum.html.erb | 2 +- .../active_fields/forms/_enum_array.html.erb | 2 +- .../active_fields/forms/_integer.html.erb | 2 +- .../forms/_integer_array.html.erb | 2 +- .../views/active_fields/forms/_text.html.erb | 2 +- .../active_fields/forms/_text_array.html.erb | 2 +- .../active_fields/forms/_boolean.html.erb | 2 +- .../views/active_fields/forms/_date.html.erb | 2 +- .../active_fields/forms/_date_array.html.erb | 2 +- .../active_fields/forms/_datetime.html.erb | 2 +- .../forms/_datetime_array.html.erb | 2 +- .../active_fields/forms/_decimal.html.erb | 2 +- .../forms/_decimal_array.html.erb | 2 +- .../views/active_fields/forms/_enum.html.erb | 2 +- .../active_fields/forms/_enum_array.html.erb | 2 +- .../active_fields/forms/_integer.html.erb | 2 +- .../forms/_integer_array.html.erb | 2 +- .../views/active_fields/forms/_ip.html.erb | 2 +- .../active_fields/forms/_ip_array.html.erb | 2 +- .../views/active_fields/forms/_text.html.erb | 2 +- .../active_fields/forms/_text_array.html.erb | 2 +- .../active_fields/customizable_config_spec.rb | 41 ------------------- spec/models/author_spec.rb | 18 ++++++-- spec/models/group_spec.rb | 6 --- spec/models/post_spec.rb | 18 ++++++-- spec/support/shared_examples/active_field.rb | 14 +++---- spec/support/test_methods.rb | 4 +- 42 files changed, 151 insertions(+), 137 deletions(-) delete mode 100644 lib/active_fields/customizable_config.rb create mode 100644 lib/active_fields/registry.rb delete mode 100644 spec/lib/active_fields/customizable_config_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0c8b0..07e2cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index e053af7..800b613 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [](https://rubygems.org/gems/active_fields) [](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 @@ -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. @@ -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. @@ -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 @@ -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) ``` @@ -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 @@ -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 = [ @@ -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 diff --git a/app/models/concerns/active_fields/customizable_concern.rb b/app/models/concerns/active_fields/customizable_concern.rb index 47852cc..9d29876 100644 --- a/app/models/concerns/active_fields/customizable_concern.rb +++ b/app/models/concerns/active_fields/customizable_concern.rb @@ -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 diff --git a/app/models/concerns/active_fields/field_concern.rb b/app/models/concerns/active_fields/field_concern.rb index 9c6082f..cdb982c 100644 --- a/app/models/concerns/active_fields/field_concern.rb +++ b/app/models/concerns/active_fields/field_concern.rb @@ -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 @@ -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 diff --git a/lib/active_fields.rb b/lib/active_fields.rb index 901f917..f336c6a 100644 --- a/lib/active_fields.rb +++ b/lib/active_fields.rb @@ -18,7 +18,7 @@ def eager_load! eager_autoload do autoload :Config - autoload :CustomizableConfig + autoload :Registry autoload :HasActiveFields end @@ -95,5 +95,7 @@ def config end alias_method :configure, :config + + def registry = Registry.instance end end diff --git a/lib/active_fields/customizable_config.rb b/lib/active_fields/customizable_config.rb deleted file mode 100644 index 7f2b92e..0000000 --- a/lib/active_fields/customizable_config.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module ActiveFields - class CustomizableConfig - attr_reader :customizable_model, :types - - def initialize(customizable_model) - @customizable_model = customizable_model - end - - def types=(value) - invalid_types = value - ActiveFields.config.type_names - if invalid_types.any? - raise ArgumentError, "undefined ActiveFields types provided for #{customizable_model}: #{invalid_types}" - end - - @types = value - end - - def types_class_names - ActiveFields.config.fields.values_at(*types) - end - end -end diff --git a/lib/active_fields/has_active_fields.rb b/lib/active_fields/has_active_fields.rb index 4433ded..6e27589 100644 --- a/lib/active_fields/has_active_fields.rb +++ b/lib/active_fields/has_active_fields.rb @@ -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 diff --git a/lib/active_fields/registry.rb b/lib/active_fields/registry.rb new file mode 100644 index 0000000..ac11df8 --- /dev/null +++ b/lib/active_fields/registry.rb @@ -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 diff --git a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb index 28c1620..b9a037f 100644 --- a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +++ b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb @@ -24,7 +24,7 @@