From b465c9c08bb67e3184baf9e264bba374ca54d5fb Mon Sep 17 00:00:00 2001 From: Albert Salim Date: Sat, 19 Sep 2020 13:02:13 +0800 Subject: [PATCH] Add Hashie::Extensions::Dash::AllowList Extends a Dash with the ability to accept only predefined values on a property. #61 --- CHANGELOG.md | 1 + README.md | 15 ++++ lib/hashie.rb | 1 + lib/hashie/extensions/dash/allow_list.rb | 88 +++++++++++++++++++ .../hashie/extensions/dash/allow_list_spec.rb | 56 ++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 lib/hashie/extensions/dash/allow_list.rb create mode 100644 spec/hashie/extensions/dash/allow_list_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index c26a19e0..bd87ce0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Any violations of this scheme are considered to be bugs. * [#523](https://github.com/hashie/hashie/pull/523): Added TOC, ensure a keep-a-changelog formatted CHANGELOG - [@dblock](https://github.com/dblock). * [#522](https://github.com/hashie/hashie/pull/522): Added eierlegende Wollmilchsau mascot graphic - [@carolineartz](https://github.com/carolineartz). +* [#530](https://github.com/hashie/hashie/pull/530): Add Hashie::Extensions::Dash::AllowList - [@caalberts](https://github.com/caalberts). * Your contribution here. ### Changed diff --git a/README.md b/README.md index 4280e7b5..21091626 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ - [PropertyTranslation](#propertytranslation) - [Mash and Rails 4 Strong Parameters](#mash-and-rails-4-strong-parameters) - [Coercion](#coercion-1) + - [AllowList](#allowlist) - [Trash](#trash) - [Clash](#clash) - [Rash](#rash) @@ -968,6 +969,20 @@ class UserHash < Hashie::Dash end ``` +### AllowList + +The `Hashie::Extensions::Dash::AllowList` mixin extends a Dash with +the ability to accept predefined values on a property. + +```ruby +class UserHash < Hashie::Dash + include Hashie::Extensions::Coercion + + property :gender, allow: %i[male female prefer_not_to_say] + property :age, allow: (0..150) +end +``` + ## Trash A Trash is a Dash that allows you to translate keys on initialization. It mixes diff --git a/lib/hashie.rb b/lib/hashie.rb index 7f88ed4a..e2e87783 100644 --- a/lib/hashie.rb +++ b/lib/hashie.rb @@ -41,6 +41,7 @@ module Dash autoload :IndifferentAccess, 'hashie/extensions/dash/indifferent_access' autoload :PropertyTranslation, 'hashie/extensions/dash/property_translation' autoload :Coercion, 'hashie/extensions/dash/coercion' + autoload :AllowList, 'hashie/extensions/dash/allow_list' end module Mash diff --git a/lib/hashie/extensions/dash/allow_list.rb b/lib/hashie/extensions/dash/allow_list.rb new file mode 100644 index 00000000..05e135c4 --- /dev/null +++ b/lib/hashie/extensions/dash/allow_list.rb @@ -0,0 +1,88 @@ +module Hashie + module Extensions + module Dash + # Extends a Dash with the ability to accept only predefined values on a property. + # + # == Example + # + # class PersonHash < Hashie::Dash + # include Hashie::Extensions::Dash::AllowedValues + # + # property :gender, values: [:male, :female, :meat_popsicle] + # property :age, values: 1..150 # a Range + # end + # + # person = PersonHash.new(gender: :male, age: 0) + # # => ArgumentError: The property 'age' must be within 1..150. + module AllowList + def self.included(base) + base.instance_variable_set(:@allow_list_for_properties, {}) + base.extend(ClassMethods) + base.include(InstanceMethods) + end + + module ClassMethods + attr_reader :allow_list_for_properties + + def inherited(klass) + super + klass.instance_variable_set(:@allow_list_for_properties, allow_list_for_properties.dup) + end + + def property(property_name, options = {}) + super + + return unless (allowed_values = options[:allow]) + + assert_allow_list_type!(allowed_values) + define_allow_list(property_name, allowed_values) + end + + private + + def assert_allow_list_type!(allowed_values) + return if supported_type?(allowed_values) + + raise ArgumentError, %(`allow` accepts an Array or a Range.) + end + + def supported_type?(allowed_values) + [::Array, ::Range].any? { |klass| allowed_values.is_a?(klass) } + end + + def define_allow_list(property_name, allowed_values) + @allow_list_for_properties[property_name] = allowed_values + end + end + + module InstanceMethods + def initialize(*) + super + + assert_allowed_property_values! + end + + private + + def assert_allowed_property_values! + self.class.allow_list_for_properties.each_key do |property| + value = send(property) + + if value && !allow_list_for_properties(property).include?(value) + fail_allowed_property_value_error!(property) + end + end + end + + def fail_allowed_property_value_error!(property) + raise ArgumentError, "The property '#{property}' is not in the allow list" + end + + def allow_list_for_properties(property) + self.class.allow_list_for_properties[property] + end + end + end + end + end +end diff --git a/spec/hashie/extensions/dash/allow_list_spec.rb b/spec/hashie/extensions/dash/allow_list_spec.rb new file mode 100644 index 00000000..995e5a80 --- /dev/null +++ b/spec/hashie/extensions/dash/allow_list_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Hashie::Extensions::Dash::AllowList do + class DashWithAllowedValues < Hashie::Dash + include Hashie::Extensions::Dash::AllowList + + property :gender, allow: %i[male female prefer_not_to_say] + property :age, allow: (0..150) + end + + it 'allows value within the allow list' do + valid_dash = DashWithAllowedValues.new(gender: :male) + expect(valid_dash.gender).to eq(:male) + end + + it 'rejects value outside the allow list' do + expect { DashWithAllowedValues.new(gender: :unicorn) } + .to raise_error(ArgumentError, %(The property 'gender' is not in the allow list)) + end + + it 'accepts a range for allow list' do + expect { DashWithAllowedValues.new(age: -1) } + .to raise_error(ArgumentError, %(The property 'age' is not in the allow list)) + end + + it 'allows property to be nil' do + expect { DashWithAllowedValues.new } + .not_to raise_error + end + + it 'rejects non array or range for allow list' do + expect do + class DashWithUnsupportedAllowedValueType < Hashie::Dash + include Hashie::Extensions::Dash::AllowList + + property :name, allow: -> { :foo } + end + end.to raise_error(ArgumentError, %(`allow` accepts an Array or a Range.)) + end + + let(:subclass) do + Class.new(DashWithAllowedValues) do + property :language, allow: %i[ruby javascript] + end + end + + it 'passes property allow list to subclasses' do + expect { subclass.new(gender: :unicorn) } + .to raise_error(ArgumentError, %(The property 'gender' is not in the allow list)) + end + + it 'allows subclass to define allow list' do + expect { subclass.new(language: :ruby) } + .not_to raise_error + end +end