diff --git a/lib/subroutine.rb b/lib/subroutine.rb index b7ef1bc..78c0ce2 100644 --- a/lib/subroutine.rb +++ b/lib/subroutine.rb @@ -14,8 +14,18 @@ require "subroutine/fields" require "subroutine/op" +require "logger" + module Subroutine + def self.logger + @logger + end + + def self.logger=(logger) + @logger = logger + end + # Used by polymorphic association fields to resolve the class name to a ruby class def self.constantize_polymorphic_class_name(class_name) return @constantize_polymorphic_class_name.call(class_name) if defined?(@constantize_polymorphic_class_name) @@ -38,6 +48,18 @@ def self.include_defaults_in_params? false end + def self.field_redefinition_behavior + @field_redefinition_behavior ||= :warn + end + + def self.field_redefinition_behavior=(symbol) + symbol = symbol.to_sym + possible = %i[error warn ignore] + raise ArgumentError, "#{symbol} must be one of #{possible.inspect}" unless possible.include?(symbol) + + @field_redefinition_behavior = symbol + end + def self.inheritable_field_options=(opts) @inheritable_field_options = opts.map(&:to_sym) end diff --git a/lib/subroutine/association_fields.rb b/lib/subroutine/association_fields.rb index d3fd110..1817a47 100644 --- a/lib/subroutine/association_fields.rb +++ b/lib/subroutine/association_fields.rb @@ -66,6 +66,8 @@ def field_with_association(field_name, options = {}) if options[:type]&.to_sym == :association config = ::Subroutine::AssociationFields::Configuration.new(field_name, options) + field_without_association(config.as, config) + if config.polymorphic? field config.foreign_type_method, config.build_foreign_type_field else @@ -77,8 +79,6 @@ def field_with_association(field_name, options = {}) end field config.foreign_key_method, config.build_foreign_key_field - - field_without_association(config.as, config) else field_without_association(field_name, options) end diff --git a/lib/subroutine/association_fields/configuration.rb b/lib/subroutine/association_fields/configuration.rb index 017e9bb..f534610 100644 --- a/lib/subroutine/association_fields/configuration.rb +++ b/lib/subroutine/association_fields/configuration.rb @@ -85,6 +85,7 @@ def build_child_field(name, opts = {}) child_opts = inheritable_options child_opts.merge!(opts) child_opts[:association_name] = as + child_opts[:allow_override] = true ComponentConfiguration.new(name, child_opts) end diff --git a/lib/subroutine/fields.rb b/lib/subroutine/fields.rb index 893ebb4..5b94abd 100644 --- a/lib/subroutine/fields.rb +++ b/lib/subroutine/fields.rb @@ -7,6 +7,8 @@ module Subroutine module Fields + DuplicateFieldError = Class.new(StandardError) + extend ActiveSupport::Concern def self.allowed_input_classes @@ -30,6 +32,16 @@ def self.action_controller_params_loaded? module ClassMethods def field(field_name, options = {}) + + if field_configurations.key?(field_name.to_sym) && !options[:allow_override] + case Subroutine.field_redefinition_behavior + when :error + raise DuplicateFieldError, "[subroutine] #{self} redefined `#{field_name}`. Add `allow_override: true` to reconfigure the field." + when :warn + Subroutine.logger&.warn("[subroutine] #{self} redefines `#{field_name}`. Add `allow_override: true` to silence this warning.\nCalled from: #{caller.join("\n")}") + end + end + config = ::Subroutine::Fields::Configuration.from(field_name, options) config.validate! diff --git a/lib/subroutine/version.rb b/lib/subroutine/version.rb index dfe7176..585ceb6 100644 --- a/lib/subroutine/version.rb +++ b/lib/subroutine/version.rb @@ -2,10 +2,10 @@ module Subroutine - MAJOR = 4 - MINOR = 5 + MAJOR = 5 + MINOR = 0 PATCH = 0 - PRE = nil + PRE = "alpha2" VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".") diff --git a/test/subroutine/fields_test.rb b/test/subroutine/fields_test.rb index dbde7ec..6869b27 100644 --- a/test/subroutine/fields_test.rb +++ b/test/subroutine/fields_test.rb @@ -214,5 +214,32 @@ def test_group_fields_are_not_mutated_by_subclasses assert_equal(%i[food], MutationChild.fields_by_group[:four_letter].sort) end + def test_redefinition_of_field__error + Subroutine.stubs(:field_redefinition_behavior).returns(:error) + Subroutine.stubs(:logger).returns(mock) + Subroutine.logger.expects(:warn).never + assert_raises Subroutine::Fields::DuplicateFieldError do + Whatever.send(:field, :foo, type: :string, default: "foo") + end + end + + def test_redefinition_of_field__warn + Subroutine.stubs(:field_redefinition_behavior).returns(:warn) + Subroutine.stubs(:logger).returns(mock) + Subroutine.logger.expects(:warn).with(includes("[subroutine] #{Whatever} redefines `foo`. Add `allow_override: true` to silence this warning.")).once + Whatever.send(:field, :foo, type: :string, default: "foo") + end + + def test_redefinition_of_field__ignore + Subroutine.stubs(:field_redefinition_behavior).returns(:ignore) + Subroutine.stubs(:logger).returns(mock) + Subroutine.logger.expects(:warn).never + + foo_config1 = Whatever.field_configurations[:foo] + Whatever.send(:field, :foo, type: :string, default: "foo") + foo_config2 = Whatever.field_configurations[:foo] + refute_equal foo_config1.object_id, foo_config2.object_id + end + end end diff --git a/test/support/ops.rb b/test/support/ops.rb index f5a2bc4..ff83eb3 100644 --- a/test/support/ops.rb +++ b/test/support/ops.rb @@ -154,7 +154,7 @@ class OnlyFooBarOp < ::Subroutine::Op class InheritedDefaultsOp < ::DefaultsOp - field :bar, default: "barstool", allow_overwrite: true + field :bar, default: "barstool", allow_override: true end @@ -354,7 +354,7 @@ class SimpleAssociationWithStringIdOp < ::OpWithAssociation class UnscopedSimpleAssociationOp < ::OpWithAssociation - association :user, unscoped: true, allow_overwrite: true + association :user, unscoped: true, allow_override: true end