summaryrefslogtreecommitdiff
path: root/lib/hashie/dash.rb
blob: 99386c484b4abe416621f829906f0808c071fa22 (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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
require 'hashie/hash'
require 'set'

module Hashie
  # A Dash is a 'defined' or 'discrete' Hash, that is, a Hash
  # that has a set of defined keys that are accessible (with
  # optional defaults) and only those keys may be set or read.
  #
  # Dashes are useful when you need to create a very simple
  # lightweight data object that needs even fewer options and
  # resources than something like a DataMapper resource.
  #
  # It is preferrable to a Struct because of the in-class
  # API for defining properties as well as per-property defaults.
  class Dash < Hash
    include PrettyInspect
    alias_method :to_s, :inspect

    # Defines a property on the Dash. Options are
    # as follows:
    #
    # * <tt>:default</tt> - Specify a default value for this property,
    #   to be returned before a value is set on the property in a new
    #   Dash.
    #
    # * <tt>:required</tt> - Specify the value as required for this
    #   property, to raise an error if a value is unset in a new or
    #   existing Dash.
    #
    def self.property(property_name, options = {})
      property_name = property_name.to_sym

      self.properties << property_name

      if options.has_key?(:default)
        self.defaults[property_name] = options[:default]
      elsif self.defaults.has_key?(property_name)
        self.defaults.delete property_name
      end

      unless instance_methods.map { |m| m.to_s }.include?("#{property_name}=")
        class_eval <<-ACCESSORS
          def #{property_name}(&block)
            self.[](#{property_name.to_s.inspect}, &block)
          end

          def #{property_name}=(value)
            self.[]=(#{property_name.to_s.inspect}, value)
          end
        ACCESSORS
      end

      if defined? @subclasses
        @subclasses.each { |klass| klass.property(property_name, options) }
      end
      required_properties << property_name if options.delete(:required)
    end

    class << self
      attr_reader :properties, :defaults
      attr_reader :required_properties
    end
    instance_variable_set('@properties', Set.new)
    instance_variable_set('@defaults', {})
    instance_variable_set('@required_properties', Set.new)

    def self.inherited(klass)
      super
      (@subclasses ||= Set.new) << klass
      klass.instance_variable_set('@properties', self.properties.dup)
      klass.instance_variable_set('@defaults', self.defaults.dup)
      klass.instance_variable_set('@required_properties', self.required_properties.dup)
    end

    # Check to see if the specified property has already been
    # defined.
    def self.property?(name)
      properties.include? name.to_sym
    end

    # Check to see if the specified property is
    # required.
    def self.required?(name)
      required_properties.include? name.to_sym
    end

    # You may initialize a Dash with an attributes hash
    # just like you would many other kinds of data objects.
    def initialize(attributes = {}, &block)
      super(&block)

      self.class.defaults.each_pair do |prop, value|
        self[prop] = if value.is_a?(Numeric)
          value
        else
          begin
            value.dup
          rescue TypeError
            value
          end
        end
      end

      initialize_attributes(attributes)
      assert_required_properties_set!
    end

    alias_method :_regular_reader, :[]
    alias_method :_regular_writer, :[]=
    private :_regular_reader, :_regular_writer

    # Retrieve a value from the Dash (will return the
    # property's default value if it hasn't been set).
    def [](property)
      assert_property_exists! property
      value = super(property.to_s)
      yield value if block_given?
      value
    end

    # Set a value on the Dash in a Hash-like way. Only works
    # on pre-existing properties.
    def []=(property, value)
      assert_property_required! property, value
      assert_property_exists! property
      super(property.to_s, value)
    end

    private

      def initialize_attributes(attributes)
        attributes.each_pair do |att, value|
          self[att] = value
        end if attributes
      end

      def assert_property_exists!(property)
        unless self.class.property?(property)
          raise NoMethodError, "The property '#{property}' is not defined for this Dash."
        end
      end

      def assert_required_properties_set!
        self.class.required_properties.each do |required_property|
          assert_property_set!(required_property)
        end
      end

      def assert_property_set!(property)
        if send(property).nil?
          raise ArgumentError, "The property '#{property}' is required for this Dash."
        end
      end

      def assert_property_required!(property, value)
        if self.class.required?(property) && value.nil?
          raise ArgumentError, "The property '#{property}' is required for this Dash."
        end
      end

  end
end