Skip to content

Commit

Permalink
Add Hashie::Extensions::Dash::AllowList
Browse files Browse the repository at this point in the history
Extends a Dash with the ability to
accept only predefined values on a property.

#61
  • Loading branch information
caalberts committed Sep 19, 2020
1 parent b24d6dc commit d9b11e1
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -14,7 +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).
* Your contribution here.
* [#530](https://github.com/hashie/hashie/pull/530): Add Hashie::Extensions::Dash::AllowList - [@caalberts](https://github.com/caalberts).

### Changed

Expand Down
15 changes: 15 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/hashie.rb
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions 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
54 changes: 54 additions & 0 deletions spec/hashie/extensions/dash/allow_list_spec.rb
@@ -0,0 +1,54 @@
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

class DashWithAllowedValuesSubClass < DashWithAllowedValues
property :language, allow: %i[ruby javascript]
end

it 'passes property allow list to subclasses' do
expect { DashWithAllowedValuesSubClass.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 { DashWithAllowedValuesSubClass.new(language: :ruby) }
.not_to raise_error
end
end

0 comments on commit d9b11e1

Please sign in to comment.