diff --git a/lib/permittable.rb b/lib/permittable.rb index 745f905..41a9953 100644 --- a/lib/permittable.rb +++ b/lib/permittable.rb @@ -1621,7 +1621,11 @@ def authored_nil!(field, opt) def validate_message!(field) spec = field[:message] return if spec.nil? - return if spec.is_a?(String) + + if spec.is_a?(String) + field[:message] = freeze_authored(spec) + return + end valid_hash = spec.is_a?(Hash) && !spec.empty? && spec.all? { |code, text| (code.is_a?(Symbol) || code.is_a?(String)) && text.is_a?(String) } @@ -1630,7 +1634,7 @@ def validate_message!(field) "or a Hash of violation code => String (e.g. { missing: \"is required\" })" end - field[:message] = spec.transform_keys(&:to_sym).freeze + field[:message] = freeze_authored(spec.transform_keys(&:to_sym)) end end diff --git a/spec/field_group_spec.rb b/spec/field_group_spec.rb index ceb7111..f1c1071 100644 --- a/spec/field_group_spec.rb +++ b/spec/field_group_spec.rb @@ -196,5 +196,19 @@ def controller(klass, params: {}, action: "create") permittable_class { permit_params(:create) { use group } } expect(Permittable.filter_parameter_registry.include?("ssn")).to be(true) end + + it "freezes a shared field's message: so mutating one contract's violation can't corrupt another's" do + group = Permittable.fields { required :age, :integer, message: { missing: "age please" } } + contract_a = Permittable::Contract.define { use group } + contract_b = Permittable::Contract.define { use group } + + msg = contract_a.call({}).violations.first[:message] + expect(msg).to eq("age please") + expect { msg << " NOW" }.to raise_error(FrozenError) + + msg_b = contract_b.call({}).violations.first[:message] + expect(msg_b).to eq("age please") + expect(msg_b).to equal(msg) # same shared object — proves it stayed intact, not just re-fetched + end end end