summaryrefslogtreecommitdiff
path: root/lib/hashie/extensions/dash/predefined_values.rb
blob: 2a0b7ca70de52ce705c8dd7ef36c56aefb8f1402 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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