Skip to content

Commit

Permalink
Value search (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
Exterm1nate authored Feb 19, 2025
1 parent 8778b9a commit de5172d
Show file tree
Hide file tree
Showing 120 changed files with 6,532 additions and 130 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- "3.3.5"
services:
postgres:
image: postgres:16
image: postgres:17
env:
POSTGRES_DB: active_fields_dummy_test
PGUSER: postgres # Fixes 'FATAL: role "root" does not exist'
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ inherit_gem:

AllCops:
TargetRubyVersion: 3.3
TargetRailsVersion: 7.2
TargetRailsVersion: 8.0
NewCops: enable
Exclude:
- "spec/dummy/db/schema.rb"
Expand Down
11 changes: 6 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [Unreleased]
- Drop support for Rails < 7 (EOL)
- Drop support for Ruby < 3.1 (EOL)
- Drop support for _Rails_ < 7.1
- Drop support for _Ruby_ < 3.1 (EOL)
- Added search functionality

**Breaking changes**:
- Maximum datetime precision reduced to 6 for all _Ruby_/_Rails_ versions.
Expand All @@ -14,7 +15,7 @@
The maximum precision value has been moved
from `ActiveFields::Casters::DateTimeCaster::MAX_PRECISION` to `ActiveFields::MAX_DATETIME_PRECISION`.

- Maximum decimal precision set to to 16383 (2**14 - 1).
- Maximum decimal precision set to 16383 (2**14 - 1).

While _Ruby_'s `BigDecimal` class allows extremely high precision,
PostgreSQL supports a maximum of 16383 digits after the decimal point.
Expand All @@ -37,11 +38,11 @@
- Added datetime and datetime array field types
- Added fields configuration DSL
- Introduced new _Customizable_ setter for _Active Values_ (`active_fields_attributes=`) with a more convenient syntax
to replace the default setter (`active_values_attributes=`) from Rails nested attributes feature.
to replace the default setter (`active_values_attributes=`) from _Rails_ nested attributes feature

## [0.2.0] - 2024-06-13

- Rewritten as a Rails plugin!
- Rewritten as a _Rails_ plugin!
- Custom field types support
- Global configuration options for changing field and value classes
- Per-model configuration option for enabling specific field types only
Expand Down
406 changes: 380 additions & 26 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion active_fields.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

# Dependencies
spec.add_dependency "rails", ">= 7.0"
spec.add_dependency "rails", ">= 7.1"

spec.add_development_dependency "factory_bot"
spec.add_development_dependency "pg"
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/boolean.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Boolean < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::BooleanCaster",
},
finder: {
class_name: "ActiveFields::Finders::BooleanFinder",
},
)

store_accessor :options, :required, :nullable
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/date.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Date < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::DateCaster",
},
finder: {
class_name: "ActiveFields::Finders::DateFinder",
},
)

store_accessor :options, :required, :min, :max
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/date_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class DateArray < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::DateArrayCaster",
},
finder: {
class_name: "ActiveFields::Finders::DateArrayFinder",
},
)

store_accessor :options, :min, :max
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/date_time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class DateTime < ActiveFields.config.field_base_class
class_name: "ActiveFields::Casters::DateTimeCaster",
options: -> { { precision: precision } },
},
finder: {
class_name: "ActiveFields::Finders::DateTimeFinder",
},
)

store_accessor :options, :required, :min, :max, :precision
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/date_time_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class DateTimeArray < ActiveFields.config.field_base_class
class_name: "ActiveFields::Casters::DateTimeArrayCaster",
options: -> { { precision: precision } },
},
finder: {
class_name: "ActiveFields::Finders::DateTimeArrayFinder",
},
)

store_accessor :options, :min, :max, :precision
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/decimal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Decimal < ActiveFields.config.field_base_class
class_name: "ActiveFields::Casters::DecimalCaster",
options: -> { { precision: precision } },
},
finder: {
class_name: "ActiveFields::Finders::DecimalFinder",
},
)

store_accessor :options, :required, :min, :max, :precision
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/decimal_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class DecimalArray < ActiveFields.config.field_base_class
class_name: "ActiveFields::Casters::DecimalArrayCaster",
options: -> { { precision: precision } },
},
finder: {
class_name: "ActiveFields::Finders::DecimalArrayFinder",
},
)

store_accessor :options, :min, :max, :precision
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Enum < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::EnumCaster",
},
finder: {
class_name: "ActiveFields::Finders::EnumFinder",
},
)

store_accessor :options, :required, :allowed_values
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/enum_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class EnumArray < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::EnumArrayCaster",
},
finder: {
class_name: "ActiveFields::Finders::EnumArrayFinder",
},
)

store_accessor :options, :allowed_values
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/integer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Integer < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::IntegerCaster",
},
finder: {
class_name: "ActiveFields::Finders::IntegerFinder",
},
)

store_accessor :options, :required, :min, :max
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/integer_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class IntegerArray < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::IntegerArrayCaster",
},
finder: {
class_name: "ActiveFields::Finders::IntegerArrayFinder",
},
)

store_accessor :options, :min, :max
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Text < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::TextCaster",
},
finder: {
class_name: "ActiveFields::Finders::TextFinder",
},
)

store_accessor :options, :required, :min_length, :max_length
Expand Down
3 changes: 3 additions & 0 deletions app/models/active_fields/field/text_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class TextArray < ActiveFields.config.field_base_class
caster: {
class_name: "ActiveFields::Casters::TextArrayCaster",
},
finder: {
class_name: "ActiveFields::Finders::TextArrayFinder",
},
)

store_accessor :options, :min_length, :max_length
Expand Down
81 changes: 78 additions & 3 deletions app/models/concerns/active_fields/customizable_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,84 @@ module CustomizableConcern
dependent: :destroy
# rubocop:enable Rails/ReflectionClassName

# Searches customizables by active_values.
#
# Accepts an Array of Hashes (symbol/string keys),
# a Hash of Hashes generated from HTTP/HTML parameters
# or permitted params.
# Each element should contain:
# - <tt>:n</tt> or <tt>:name</tt> key matching the active_field record name;
# - <tt>:op</tt> or <tt>:operator</tt> key specifying search operation or operator;
# - <tt>:v</tt> or <tt>:value</tt> key specifying search value.
#
# Example:
#
# # Array of hashes
# CustomizableModel.where_active_values(
# [
# { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
# { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
# { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
# ],
# )
#
# # Hash of hashes generated from HTTP/HTML parameters
# CustomizableModel.where_active_values(
# {
# "0" => { name: "integer_array", operator: "any_gteq", value: 5 },
# "1" => { "name" => "text", operator: "=", "value" => "Lasso" },
# "2" => { n: "boolean", op: "!=", v: false },
# },
# )
#
# # Params (must be permitted)
# CustomizableModel.where_active_values(permitted_params)
scope :where_active_values, ->(filters) do
filters = filters.to_h if filters.respond_to?(:permitted?)

unless filters.is_a?(Array) || filters.is_a?(Hash)
raise ArgumentError, "Hash or Array expected for `where_active_values`, got #{filters.class.name}"
end

# Handle `fields_for` params
filters = filters.values if filters.is_a?(Hash)

active_fields_by_name = active_fields.index_by(&:name)

filters.inject(self) do |scope, filter|
filter = filter.to_h if filter.respond_to?(:permitted?)
filter = filter.with_indifferent_access

active_field = active_fields_by_name[filter[:n] || filter[:name]]
next scope if active_field.nil?
next scope if active_field.value_finder.nil?

active_values = active_field.value_finder.search(
op: filter[:op] || filter[:operator],
value: filter[:v] || filter[:value],
)
next scope if active_values.nil?

scope.where(id: active_values.select(:customizable_id))
end
end

accepts_nested_attributes_for :active_values, allow_destroy: true
end

def active_fields
ActiveFields.config.field_base_class.for(model_name.name)
class_methods do
def active_fields
ActiveFields.config.field_base_class.for(model_name.name)
end
end

delegate :active_fields, to: :class

# Assigns the given attributes to the active_values association.
#
# Accepts an Array of Hashes (symbol/string keys) or permitted params.
# Accepts an Array of Hashes (symbol/string keys),
# a Hash of Hashes generated from HTTP/HTML parameters
# or permitted params.
# Each element should contain a <tt>:name</tt> key matching an existing active_field record.
# Element with a <tt>:value</tt> key will create an active_value if it doesn't exist
# or update an existing active_value, with the provided value.
Expand All @@ -40,6 +108,13 @@ def active_fields
# { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
# permitted_params, # params could be passed, but they must be permitted
# ]
#
# customizable.active_fields_attributes = {
# "0" => { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
# "1" => { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
# "2" => { name: "date", _destroy: true }, # destroy (symbol keys)
# "3" => { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
# }
def active_fields_attributes=(attributes)
attributes = attributes.to_h if attributes.respond_to?(:permitted?)

Expand Down
10 changes: 9 additions & 1 deletion app/models/concerns/active_fields/field_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module FieldConcern
end

class_methods do
def acts_as_active_field(array: false, validator:, caster:)
def acts_as_active_field(array: false, validator:, caster:, finder: {})
include FieldArrayConcern if array

define_method(:array?) { array }
Expand Down Expand Up @@ -62,6 +62,14 @@ def acts_as_active_field(array: false, validator:, caster:)
end
value_caster_class.new(**options)
end

define_method(:value_finder_class) do
@value_finder_class ||= finder[:class_name]&.constantize
end

define_method(:value_finder) do
value_finder_class&.new(active_field: self)
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion db/migrate/20240229230000_create_active_fields_tables.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class CreateActiveFieldsTables < ActiveRecord::Migration[7.0]
class CreateActiveFieldsTables < ActiveRecord::Migration[7.1]
def change
create_table :active_fields do |t|
t.string :name, null: false
Expand Down
25 changes: 25 additions & 0 deletions lib/active_fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def eager_load!
super
Casters.eager_load!
Validators.eager_load!
Finders.eager_load!
end
end

Expand Down Expand Up @@ -63,6 +64,30 @@ module Validators
end
end

module Finders
extend ActiveSupport::Autoload

eager_autoload do
autoload :BaseFinder
autoload :SingularFinder
autoload :ArrayFinder

autoload :BooleanFinder
autoload :DateFinder
autoload :DateArrayFinder
autoload :DateTimeFinder
autoload :DateTimeArrayFinder
autoload :DecimalFinder
autoload :DecimalArrayFinder
autoload :EnumFinder
autoload :EnumArrayFinder
autoload :IntegerFinder
autoload :IntegerArrayFinder
autoload :TextFinder
autoload :TextArrayFinder
end
end

class << self
def config
yield Config.instance if block_given?
Expand Down
Loading

0 comments on commit de5172d

Please sign in to comment.