Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hashie::Extensions::Dash::PredefinedValues #530

Merged
merged 1 commit into from Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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): Added Hashie::Extensions::Dash::PredefinedValues - [@caalberts](https://github.com/caalberts).
* Your contribution here.

### 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)
- [PredefinedValues](#predefinedvalues)
- [Trash](#trash)
- [Clash](#clash)
- [Rash](#rash)
Expand Down Expand Up @@ -968,6 +969,20 @@ class UserHash < Hashie::Dash
end
```

### PredefinedValues

The `Hashie::Extensions::Dash::PredefinedValues` mixin extends a Dash with
the ability to accept predefined values on a property.

```ruby
class UserHash < Hashie::Dash
include Hashie::Extensions::PredefinedValues

property :gender, values: %i[male female prefer_not_to_say]
property :age, values: (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 :PredefinedValues, 'hashie/extensions/dash/predefined_values'
end

module Mash
Expand Down
88 changes: 88 additions & 0 deletions lib/hashie/extensions/dash/predefined_values.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::PredefinedValues
#
# property :gender, values: [:male, :female, :prefer_not_to_say]
# property :age, values: (0..150) # a Range
# end
#
# person = PersonHash.new(gender: :male, age: -1)
# # => ArgumentError: The value '-1' is not accepted for property 'age'
module PredefinedValues
def self.included(base)
base.instance_variable_set(:@values_for_properties, {})
base.extend(ClassMethods)
base.include(InstanceMethods)
end

module ClassMethods
attr_reader :values_for_properties

def inherited(klass)
super
klass.instance_variable_set(:@values_for_properties, values_for_properties.dup)
end

def property(property_name, options = {})
super

return unless (predefined_values = options[:values])

assert_predefined_values!(predefined_values)
set_predefined_values(property_name, predefined_values)
end

private

def assert_predefined_values!(predefined_values)
return if supported_type?(predefined_values)

raise ArgumentError, %(`values` accepts an Array or a Range.)
end

def supported_type?(predefined_values)
[::Array, ::Range].any? { |klass| predefined_values.is_a?(klass) }
end

def set_predefined_values(property_name, predefined_values)
@values_for_properties[property_name] = predefined_values
end
end

module InstanceMethods
def initialize(*)
super

assert_property_values!
end

private

def assert_property_values!
self.class.values_for_properties.each_key do |property|
value = send(property)

if value && !values_for_properties(property).include?(value)
fail_property_value_error!(property)
end
end
end

def fail_property_value_error!(property)
raise ArgumentError, "Invalid value for property '#{property}'"
end

def values_for_properties(property)
self.class.values_for_properties[property]
end
end
end
end
end
end
58 changes: 58 additions & 0 deletions spec/hashie/extensions/dash/predefined_values_spec.rb
@@ -0,0 +1,58 @@
require 'spec_helper'

describe Hashie::Extensions::Dash::PredefinedValues do
let(:extended_dash) do
Class.new(Hashie::Dash) do
include Hashie::Extensions::Dash::PredefinedValues

property :gender, values: %i[male female prefer_not_to_say]
property :age, values: (0..150)
end
end

it 'allows value within the predefined list' do
valid_dash = extended_dash.new(gender: :male)
expect(valid_dash.gender).to eq(:male)
end

it 'rejects value outside the predefined list' do
expect { extended_dash.new(gender: :unicorn) }
.to raise_error(ArgumentError, %(Invalid value for property 'gender'))
end

it 'accepts a range for predefined list' do
expect { extended_dash.new(age: -1) }
.to raise_error(ArgumentError, %(Invalid value for property 'age'))
end

it 'allows property to be nil' do
expect { extended_dash.new }
.not_to raise_error
end

it 'rejects non array or range for predefined list' do
expect do
class DashWithUnsupportedValueType < Hashie::Dash
include Hashie::Extensions::Dash::PredefinedValues

property :name, values: -> { :foo }
end
end.to raise_error(ArgumentError, %(`values` accepts an Array or a Range.))
end

let(:subclass) do
Class.new(extended_dash) do
property :language, values: %i[ruby javascript]
end
end

it 'passes property predefined list to subclasses' do
expect { subclass.new(gender: :unicorn) }
.to raise_error(ArgumentError, %(Invalid value for property 'gender'))
end

it 'allows subclass to define predefined list' do
expect { subclass.new(language: :ruby) }
.not_to raise_error
end
end