hashie-pre 2.0.0.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +36 -0
- data/Guardfile +5 -0
- data/LICENSE +20 -0
- data/README.markdown +232 -0
- data/Rakefile +13 -0
- data/VERSION +1 -0
- data/hashie.gemspec +22 -0
- data/lib/hashie.rb +23 -0
- data/lib/hashie/clash.rb +86 -0
- data/lib/hashie/dash.rb +150 -0
- data/lib/hashie/extensions/coercion.rb +101 -0
- data/lib/hashie/extensions/deep_merge.rb +7 -0
- data/lib/hashie/extensions/indifferent_access.rb +110 -0
- data/lib/hashie/extensions/key_conversion.rb +52 -0
- data/lib/hashie/extensions/merge_initializer.rb +24 -0
- data/lib/hashie/extensions/method_access.rb +124 -0
- data/lib/hashie/extensions/structure.rb +47 -0
- data/lib/hashie/hash.rb +31 -0
- data/lib/hashie/hash_extensions.rb +49 -0
- data/lib/hashie/mash.rb +216 -0
- data/lib/hashie/trash.rb +77 -0
- data/lib/hashie/version.rb +3 -0
- data/spec/hashie/clash_spec.rb +42 -0
- data/spec/hashie/dash_spec.rb +215 -0
- data/spec/hashie/extensions/coercion_spec.rb +70 -0
- data/spec/hashie/extensions/indifferent_access_spec.rb +66 -0
- data/spec/hashie/extensions/key_conversion_spec.rb +66 -0
- data/spec/hashie/extensions/merge_initializer_spec.rb +20 -0
- data/spec/hashie/extensions/method_access_spec.rb +112 -0
- data/spec/hashie/hash_spec.rb +22 -0
- data/spec/hashie/mash_spec.rb +305 -0
- data/spec/hashie/trash_spec.rb +139 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +12 -0
- metadata +181 -0
data/lib/hashie/dash.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'hashie/hash'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Hashie
|
5
|
+
# A Dash is a 'defined' or 'discrete' Hash, that is, a Hash
|
6
|
+
# that has a set of defined keys that are accessible (with
|
7
|
+
# optional defaults) and only those keys may be set or read.
|
8
|
+
#
|
9
|
+
# Dashes are useful when you need to create a very simple
|
10
|
+
# lightweight data object that needs even fewer options and
|
11
|
+
# resources than something like a DataMapper resource.
|
12
|
+
#
|
13
|
+
# It is preferrable to a Struct because of the in-class
|
14
|
+
# API for defining properties as well as per-property defaults.
|
15
|
+
class Dash < Hashie::Hash
|
16
|
+
include Hashie::PrettyInspect
|
17
|
+
alias_method :to_s, :inspect
|
18
|
+
|
19
|
+
# Defines a property on the Dash. Options are
|
20
|
+
# as follows:
|
21
|
+
#
|
22
|
+
# * <tt>:default</tt> - Specify a default value for this property,
|
23
|
+
# to be returned before a value is set on the property in a new
|
24
|
+
# Dash.
|
25
|
+
#
|
26
|
+
# * <tt>:required</tt> - Specify the value as required for this
|
27
|
+
# property, to raise an error if a value is unset in a new or
|
28
|
+
# existing Dash.
|
29
|
+
#
|
30
|
+
def self.property(property_name, options = {})
|
31
|
+
property_name = property_name.to_sym
|
32
|
+
|
33
|
+
self.properties << property_name
|
34
|
+
|
35
|
+
if options.has_key?(:default)
|
36
|
+
self.defaults[property_name] = options[:default]
|
37
|
+
elsif self.defaults.has_key?(property_name)
|
38
|
+
self.defaults.delete property_name
|
39
|
+
end
|
40
|
+
|
41
|
+
unless instance_methods.map { |m| m.to_s }.include?("#{property_name}=")
|
42
|
+
class_eval <<-ACCESSORS
|
43
|
+
def #{property_name}(&block)
|
44
|
+
self.[](#{property_name.to_s.inspect}, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def #{property_name}=(value)
|
48
|
+
self.[]=(#{property_name.to_s.inspect}, value)
|
49
|
+
end
|
50
|
+
ACCESSORS
|
51
|
+
end
|
52
|
+
|
53
|
+
if defined? @subclasses
|
54
|
+
@subclasses.each { |klass| klass.property(property_name, options) }
|
55
|
+
end
|
56
|
+
required_properties << property_name if options.delete(:required)
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
attr_reader :properties, :defaults
|
61
|
+
attr_reader :required_properties
|
62
|
+
end
|
63
|
+
instance_variable_set('@properties', Set.new)
|
64
|
+
instance_variable_set('@defaults', {})
|
65
|
+
instance_variable_set('@required_properties', Set.new)
|
66
|
+
|
67
|
+
def self.inherited(klass)
|
68
|
+
super
|
69
|
+
(@subclasses ||= Set.new) << klass
|
70
|
+
klass.instance_variable_set('@properties', self.properties.dup)
|
71
|
+
klass.instance_variable_set('@defaults', self.defaults.dup)
|
72
|
+
klass.instance_variable_set('@required_properties', self.required_properties.dup)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check to see if the specified property has already been
|
76
|
+
# defined.
|
77
|
+
def self.property?(name)
|
78
|
+
properties.include? name.to_sym
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check to see if the specified property is
|
82
|
+
# required.
|
83
|
+
def self.required?(name)
|
84
|
+
required_properties.include? name.to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
# You may initialize a Dash with an attributes hash
|
88
|
+
# just like you would many other kinds of data objects.
|
89
|
+
def initialize(attributes = {}, &block)
|
90
|
+
super(&block)
|
91
|
+
|
92
|
+
self.class.defaults.each_pair do |prop, value|
|
93
|
+
self[prop] = value
|
94
|
+
end
|
95
|
+
|
96
|
+
attributes.each_pair do |att, value|
|
97
|
+
self[att] = value
|
98
|
+
end if attributes
|
99
|
+
assert_required_properties_set!
|
100
|
+
end
|
101
|
+
|
102
|
+
alias_method :_regular_reader, :[]
|
103
|
+
alias_method :_regular_writer, :[]=
|
104
|
+
private :_regular_reader, :_regular_writer
|
105
|
+
|
106
|
+
# Retrieve a value from the Dash (will return the
|
107
|
+
# property's default value if it hasn't been set).
|
108
|
+
def [](property)
|
109
|
+
assert_property_exists! property
|
110
|
+
value = super(property.to_s)
|
111
|
+
yield value if block_given?
|
112
|
+
value
|
113
|
+
end
|
114
|
+
|
115
|
+
# Set a value on the Dash in a Hash-like way. Only works
|
116
|
+
# on pre-existing properties.
|
117
|
+
def []=(property, value)
|
118
|
+
assert_property_required! property, value
|
119
|
+
assert_property_exists! property
|
120
|
+
super(property.to_s, value)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def assert_property_exists!(property)
|
126
|
+
unless self.class.property?(property)
|
127
|
+
raise NoMethodError, "The property '#{property}' is not defined for this Dash."
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def assert_required_properties_set!
|
132
|
+
self.class.required_properties.each do |required_property|
|
133
|
+
assert_property_set!(required_property)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def assert_property_set!(property)
|
138
|
+
if send(property).nil?
|
139
|
+
raise ArgumentError, "The property '#{property}' is required for this Dash."
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def assert_property_required!(property, value)
|
144
|
+
if self.class.required?(property) && value.nil?
|
145
|
+
raise ArgumentError, "The property '#{property}' is required for this Dash."
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module Coercion
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
def []=(key, value)
|
11
|
+
into = self.class.key_coercion(key) || self.class.value_coercion(value)
|
12
|
+
|
13
|
+
if value && into
|
14
|
+
if into.respond_to?(:coerce)
|
15
|
+
value = into.coerce(value)
|
16
|
+
else
|
17
|
+
value = into.new(value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
super(key, value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
# Set up a coercion rule such that any time the specified
|
27
|
+
# key is set it will be coerced into the specified class.
|
28
|
+
# Coercion will occur by first attempting to call Class.coerce
|
29
|
+
# and then by calling Class.new with the value as an argument
|
30
|
+
# in either case.
|
31
|
+
#
|
32
|
+
# @param [Object] key the key you would like to be coerced.
|
33
|
+
# @param [Class] into the class into which you want the key coerced.
|
34
|
+
#
|
35
|
+
# @example Coerce a "user" subhash into a User object
|
36
|
+
# class Tweet < Hash
|
37
|
+
# include Hashie::Extensions::Coercion
|
38
|
+
# coerce_key :user, User
|
39
|
+
# end
|
40
|
+
def coerce_key(key, into)
|
41
|
+
(@key_coercions ||= {})[key] = into
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a hash of any existing key coercions.
|
45
|
+
def key_coercions
|
46
|
+
@key_coercions || {}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the specific key coercion for the specified key,
|
50
|
+
# if one exists.
|
51
|
+
def key_coercion(key)
|
52
|
+
key_coercions[key]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Set up a coercion rule such that any time a value of the
|
56
|
+
# specified type is set it will be coerced into the specified
|
57
|
+
# class.
|
58
|
+
#
|
59
|
+
# @param [Class] from the type you would like coerced.
|
60
|
+
# @param [Class] into the class into which you would like the value coerced.
|
61
|
+
# @option options [Boolean] :strict (true) whether use exact source class only or include ancestors
|
62
|
+
#
|
63
|
+
# @example Coerce all hashes into this special type of hash
|
64
|
+
# class SpecialHash < Hash
|
65
|
+
# include Hashie::Extensions::Coercion
|
66
|
+
# coerce_value Hash, SpecialHash
|
67
|
+
#
|
68
|
+
# def initialize(hash = {})
|
69
|
+
# super
|
70
|
+
# hash.each_pair do |k,v|
|
71
|
+
# self[k] = v
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
def coerce_value(from, into, options = {})
|
76
|
+
options = {:strict => true}.merge(options)
|
77
|
+
|
78
|
+
if options[:strict]
|
79
|
+
(@strict_value_coercions ||= {})[from] = into
|
80
|
+
else
|
81
|
+
while from.superclass && from.superclass != Object
|
82
|
+
(@lenient_value_coercions ||= {})[from] = into
|
83
|
+
from = from.superclass
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return all value coercions that have the :strict rule as true.
|
89
|
+
def strict_value_coercions; @strict_value_coercions || {} end
|
90
|
+
# Return all value coercions that have the :strict rule as false.
|
91
|
+
def lenient_value_coercions; @value_coercions || {} end
|
92
|
+
|
93
|
+
# Fetch the value coercion, if any, for the specified object.
|
94
|
+
def value_coercion(value)
|
95
|
+
from = value.class
|
96
|
+
strict_value_coercions[from] || lenient_value_coercions[from]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
# IndifferentAccess gives you the ability to not care
|
4
|
+
# whether your hash has string or symbol keys. Made famous
|
5
|
+
# in Rails for accessing query and POST parameters, this
|
6
|
+
# is a handy tool for making sure your hash has maximum
|
7
|
+
# utility.
|
8
|
+
#
|
9
|
+
# One unique feature of this mixin is that it will recursively
|
10
|
+
# inject itself into sub-hash instances without modifying
|
11
|
+
# the actual class of the sub-hash.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# class MyHash < Hash
|
15
|
+
# include Hashie::Extensions::MergeInitializer
|
16
|
+
# include Hashie::Extensions::IndifferentAccess
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# h = MyHash.new(:foo => 'bar', 'baz' => 'blip')
|
20
|
+
# h['foo'] # => 'bar'
|
21
|
+
# h[:foo] # => 'bar'
|
22
|
+
# h[:baz] # => 'blip'
|
23
|
+
# h['baz'] # => 'blip'
|
24
|
+
#
|
25
|
+
module IndifferentAccess
|
26
|
+
def self.included(base)
|
27
|
+
base.class_eval do
|
28
|
+
alias_method :regular_writer, :[]=
|
29
|
+
alias_method :[]=, :indifferent_writer
|
30
|
+
%w(default update fetch delete key? values_at).each do |m|
|
31
|
+
alias_method "regular_#{m}", m
|
32
|
+
alias_method m, "indifferent_#{m}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# This will inject indifferent access into an instance of
|
38
|
+
# a hash without modifying the actual class. This is what
|
39
|
+
# allows IndifferentAccess to spread to sub-hashes.
|
40
|
+
def self.inject!(hash)
|
41
|
+
(class << hash; self; end).send :include, Hashie::Extensions::IndifferentAccess
|
42
|
+
hash.convert!
|
43
|
+
end
|
44
|
+
|
45
|
+
# Injects indifferent access into a duplicate of the hash
|
46
|
+
# provided. See #inject!
|
47
|
+
def self.inject(hash)
|
48
|
+
inject!(hash.dup)
|
49
|
+
end
|
50
|
+
|
51
|
+
def convert_key(key)
|
52
|
+
key.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
# Iterates through the keys and values, reconverting them to
|
56
|
+
# their proper indifferent state. Used when IndifferentAccess
|
57
|
+
# is injecting itself into member hashes.
|
58
|
+
def convert!
|
59
|
+
keys.each do |k|
|
60
|
+
regular_writer convert_key(k), convert_value(self.regular_delete(k))
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def convert_value(value)
|
66
|
+
if hash_lacking_indifference?(value)
|
67
|
+
Hashie::Extensions::IndifferentAccess.inject(value.dup)
|
68
|
+
elsif value.is_a?(::Array)
|
69
|
+
value.dup.replace(value.map { |e| convert_value(e) })
|
70
|
+
else
|
71
|
+
value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def indifferent_default(key = nil)
|
76
|
+
return self[convert_key(key)] if key?(key)
|
77
|
+
regular_default(key)
|
78
|
+
end
|
79
|
+
|
80
|
+
def indifferent_update(other_hash)
|
81
|
+
return regular_update(other_hash) if hash_with_indifference?(other_hash)
|
82
|
+
other_hash.each_pair do |k,v|
|
83
|
+
self[k] = v
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def indifferent_writer(key, value); regular_writer convert_key(key), convert_value(value) end
|
88
|
+
def indifferent_fetch(key, *args); regular_fetch convert_key(key), *args end
|
89
|
+
def indifferent_delete(key); regular_delete convert_key(key) end
|
90
|
+
def indifferent_key?(key); regular_key? convert_key(key) end
|
91
|
+
def indifferent_values_at(*indices); indices.map{|i| self[i] } end
|
92
|
+
|
93
|
+
def indifferent_access?; true end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
def hash_lacking_indifference?(other)
|
98
|
+
other.is_a?(::Hash) &&
|
99
|
+
!(other.respond_to?(:indifferent_access?) &&
|
100
|
+
other.indifferent_access?)
|
101
|
+
end
|
102
|
+
|
103
|
+
def hash_with_indifference?(other)
|
104
|
+
other.is_a?(::Hash) &&
|
105
|
+
other.respond_to?(:indifferent_access?) &&
|
106
|
+
other.indifferent_access?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module StringifyKeys
|
4
|
+
# Convert all keys in the hash to strings.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# test = {:abc => 'def'}
|
8
|
+
# test.stringify_keys!
|
9
|
+
# test # => {'abc' => 'def'}
|
10
|
+
def stringify_keys!
|
11
|
+
keys.each do |k|
|
12
|
+
self[k.to_s] = self.delete(k)
|
13
|
+
end
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return a new hash with all keys converted
|
18
|
+
# to strings.
|
19
|
+
def stringify_keys
|
20
|
+
dup.stringify_keys!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module SymbolizeKeys
|
25
|
+
# Convert all keys in the hash to strings.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# test = {'abc' => 'def'}
|
29
|
+
# test.symbolize_keys!
|
30
|
+
# test # => {:abc => 'def'}
|
31
|
+
def symbolize_keys!
|
32
|
+
keys.each do |k|
|
33
|
+
self[k.to_sym] = self.delete(k)
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return a new hash with all keys converted
|
39
|
+
# to symbols.
|
40
|
+
def symbolize_keys
|
41
|
+
dup.symbolize_keys!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module KeyConversion
|
46
|
+
def self.included(base)
|
47
|
+
base.send :include, SymbolizeKeys
|
48
|
+
base.send :include, StringifyKeys
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
# The MergeInitializer is a super-simple mixin that allows
|
4
|
+
# you to initialize a subclass of Hash with another Hash
|
5
|
+
# to give you faster startup time for Hash subclasses. Note
|
6
|
+
# that you can still provide a default value as a second
|
7
|
+
# argument to the initializer.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class MyHash < Hash
|
11
|
+
# include Hashie::Extensions::MergeInitializer
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# h = MyHash.new(:abc => 'def')
|
15
|
+
# h[:abc] # => 'def'
|
16
|
+
#
|
17
|
+
module MergeInitializer
|
18
|
+
def initialize(hash = {}, default = nil, &block)
|
19
|
+
default ? super(default) : super(&block)
|
20
|
+
update(hash)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|