hashmake 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog.rdoc CHANGED
@@ -20,4 +20,15 @@ In hash_make, add support for Hash containers which contain objects of type :typ
20
20
 
21
21
  === 0.1.4 / 2013-02-06
22
22
 
23
- Add tests to verify array and hash container support.
23
+ Add tests to verify array and hash container support.
24
+
25
+ === 0.1.5 / 2013-03-04
26
+
27
+ Hash arg specs to key name.
28
+ Instead of enumerating containers (e.g CONTAINER_HASH) just use class (Hash).
29
+ Validate args in validate_arg (which could be called independently from hash_make).
30
+
31
+ === 0.1.6 / 2013-03-21
32
+
33
+ Add Hashmake::hash_makeable? for testing if a class includes Hashmake::HashMakeable.
34
+ Add #find_arg_specs and #make_hash methods to HashMakeable.
data/README.rdoc CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  * {Homepage}[https://rubygems.org/gems/hashmake]
4
4
  * {Documentation}[http://rubydoc.info/gems/hashmake/frames]
5
- * {Email}[mailto:jamestunnell at lavabit.com]
5
+ * {Email}[mailto:jamestunnell@lavabit.com]
6
6
 
7
7
  == Description
8
8
 
9
9
  Make hash-based object initialization easy!
10
10
 
11
- Provides hash_make method that can consider parameter name, type, default value, validation, requiredd/not, and container type (none/array/hash) according to the specification provided. By default it the value given with the arg key to to an instance variable of same name as parameter.
11
+ Provides hash_make method that can consider parameter name, type, default value, validation, requiredd/not, and container type (none/array/hash) according to the specification provided. The value is assigned to an instance variable matching the arg key.
12
+
13
+ There is also a make_hash method to turn an object into a representative Hash object. Any hash-makeable sub-objects can be turned into Hash objects as well.
12
14
 
13
15
  == Features
14
16
 
@@ -19,10 +21,10 @@ Provides hash_make method that can consider parameter name, type, default value,
19
21
  class MyClass
20
22
  include Hashmake::HashMakeable
21
23
 
22
- ARG_SPECS = [
23
- ArgSpec.new(:key => :x, :reqd => true, :type => Float, :validator => ->(a){ a.between?(0.0,1.0) }),
24
- ArgSpec.new(:key => :y, :reqd => false, :type => Float, :validator => ->(a){ a.between?(0.0,1.0) }, :default => 0.0),
25
- ]
24
+ ARG_SPECS = {
25
+ :x => arg_spec(:reqd => true, :type => Float, :validator => ->(a){ a.between?(0.0,1.0) }),
26
+ :y => arg_spec(:reqd => false, :type => Float, :validator => ->(a){ a.between?(0.0,1.0) }, :default => 0.0),
27
+ }
26
28
 
27
29
  attr_reader :x, :y
28
30
 
@@ -22,14 +22,12 @@ class ArgSpec
22
22
  :type => Object,
23
23
  }
24
24
 
25
- attr_reader :key, :type, :validator, :reqd, :default, :container
25
+ attr_reader :type, :validator, :reqd, :default, :container
26
26
 
27
27
  # A new instance of ArgSpec.
28
28
  #
29
- # @param [Hash] hashed_args Hash to use in initializing an instance. Required
30
- # keys are :key and :type. Optional keys are :reqd,
31
- # :validator, :container, and :default.
32
- # :key => the key used to identify a hashed arg.
29
+ # @param [Hash] hashed_args Hash to use in initializing an instance. Optional keys
30
+ # are :type, :reqd, :validator, :container, and :default.
33
31
  # :type => the type of object expected to be paired
34
32
  # with the key.
35
33
  # :reqd => If true, the arg key must be in the hash
@@ -49,7 +47,6 @@ class ArgSpec
49
47
  def initialize hashed_args
50
48
  new_args = DEFAULT_ARGS.merge(hashed_args)
51
49
 
52
- @key = new_args[:key]
53
50
  @type = new_args[:type]
54
51
  raise ArgumentError, "args[:type] #{@type} is not a Class" unless @type.is_a?(Class)
55
52
 
@@ -1,5 +1,10 @@
1
1
  module Hashmake
2
2
 
3
+ # Determine if a class includes the HashMakeable module.
4
+ def self.hash_makeable? klass
5
+ klass.included_modules.include?(HashMakeable)
6
+ end
7
+
3
8
  # This module should be included for any class that wants to be 'hash-makeable',
4
9
  # which means that a new object instance expects all its arguments to come in a
5
10
  # single Hash. See the hash_make method in this module and the ArgSpec class for
@@ -32,6 +37,30 @@ module HashMakeable
32
37
  arg_specs.each do |key, arg_spec|
33
38
  if hashed_args.has_key?(key)
34
39
  val = hashed_args[key]
40
+
41
+ if Hashmake::hash_makeable?(arg_spec.type)
42
+ # If the val is not of the right type, but is a Hash, attempt to
43
+ # make an object of the right type if it is hash-makeable
44
+ if arg_spec.container == Array && val.is_a?(Array)
45
+ val.each_index do |i|
46
+ item = val[i]
47
+ if !item.is_a?(arg_spec.type) && item.is_a?(Hash)
48
+ val[i] = arg_spec.type.new item
49
+ end
50
+ end
51
+ elsif arg_spec.container == Hash && val.is_a?(Hash)
52
+ val.each_key do |item_key|
53
+ item = val[item_key]
54
+ if !item.is_a?(arg_spec.type) && item.is_a?(Hash)
55
+ val[item_key] = arg_spec.type.new item
56
+ end
57
+ end
58
+ else
59
+ if !val.is_a?(arg_spec.type) && val.is_a?(Hash)
60
+ val = arg_spec.type.new val
61
+ end
62
+ end
63
+ end
35
64
  else
36
65
  if arg_spec.reqd
37
66
  raise ArgumentError, "hashed_args does not have required key #{key}"
@@ -51,6 +80,8 @@ module HashMakeable
51
80
  end
52
81
  end
53
82
 
83
+ # Check the given value, using the given ArgSpec object. An ArgumentError
84
+ # exception will be raised if the value is not valid.
54
85
  def validate_arg arg_spec, val
55
86
  if arg_spec.container == Array
56
87
  raise ArgumentError, "val #{val} is not an array" unless val.is_a?(Array)
@@ -73,19 +104,118 @@ module HashMakeable
73
104
 
74
105
  return true
75
106
  end
107
+
108
+ # Look in the current class for a constant that is a Hash containing (only)
109
+ # ArgSpec objects. Returns the first constant matching this criteria, or nil
110
+ # if none was found.
111
+ def find_arg_specs
112
+ self.class.constants.each do |constant|
113
+ val = self.class.const_get(constant)
114
+ if val.is_a? Hash
115
+ all_arg_specs = true
116
+ val.each do |key,value|
117
+ unless value.is_a? ArgSpec
118
+ all_arg_specs = false
119
+ break
120
+ end
121
+ end
122
+
123
+ if all_arg_specs
124
+ return val
125
+ end
126
+ end
127
+ end
128
+
129
+ return nil
130
+ end
131
+
132
+ # Produce a hash that contains 'hashed args'. Each hashed arg is intended to
133
+ # be used in initializing an object instance.
134
+ #
135
+ # @param [Enumerable] arg_specs An enumerable of ArgSpec objects. Each one
136
+ # details an arg key that might be expected in the
137
+ # args hash. This param is nil by default. If the param
138
+ # is nil, this method will attempt to locate arg specs
139
+ # using find_arg_specs.
140
+ #
141
+ def make_hash arg_specs = nil
142
+ if arg_specs.nil?
143
+ arg_specs = self.find_arg_specs
144
+ raise "No arg specs given, and no class constant that is a Hash containing only ArgSpec objects was found" if arg_specs.nil?
145
+ end
146
+
147
+ arg_specs.each do |key, arg_spec|
148
+ raise ArgumentError, "arg_specs item #{arg_spec} is not a ArgSpec" unless arg_spec.is_a?(ArgSpec)
149
+ end
150
+
151
+ hash = {}
152
+
153
+ arg_specs.each do |key, arg_spec|
154
+ sym = "@#{key}".to_sym
155
+ raise ArgumentError, "current obj #{self} does not include instance variable #{sym}" if !self.instance_variables.include?(sym)
156
+ val = self.instance_variable_get(sym)
157
+
158
+ should_assign = false
76
159
 
160
+ if arg_spec.reqd
161
+ should_assign = true
162
+ else
163
+ if arg_spec.default.is_a?(Proc)
164
+ should_assign = (val != arg_spec.default.call)
165
+ else
166
+ should_assign = (val != arg_spec.default)
167
+ end
168
+ end
169
+
170
+ if should_assign
171
+ if val.is_a?(Array) && arg_spec.container == Array
172
+ ary = val
173
+ val = []
174
+ ary.each do |item|
175
+ if Hashmake::hash_makeable? item.class
176
+ val << item.make_hash
177
+ else
178
+ val << item
179
+ end
180
+ end
181
+ elsif val.is_a?(Hash) && arg_spec.container == Hash
182
+ hsh = val
183
+ val = {}
184
+ hsh.each do |hsh_key,item|
185
+ if Hashmake::hash_makeable? item.class
186
+ val[hsh_key] = item.make_hash
187
+ else
188
+ val[hsh_key] = item
189
+ end
190
+ end
191
+ elsif Hashmake::hash_makeable?(val.class)
192
+ val = val.make_hash
193
+ end
194
+
195
+ hash[key] = val
196
+ end
197
+ end
198
+
199
+ return hash
200
+ end
201
+
77
202
  # Contains class methods to be added to a class that includes the
78
203
  # HashMakeable module.
79
204
  module ClassMethods
205
+ # Helper method to make a generic new ArgSpec object
80
206
  def arg_spec args
81
207
  ArgSpec.new args
82
208
  end
83
209
 
210
+ # Helper method to make a ArgSpec object where the container is an Array.
211
+ # Set :default to a Proc that generates an empty array.
84
212
  def arg_spec_array args
85
213
  args = { :container => Array, :default => ->(){Array.new} }.merge(args)
86
214
  ArgSpec.new args
87
215
  end
88
216
 
217
+ # Helper method to make a ArgSpec object where the container is an Hash.
218
+ # Set :default to a Proc that generates an empty hash.
89
219
  def arg_spec_hash args
90
220
  args = { :container => Hash, :default => ->(){Hash.new} }.merge(args)
91
221
  ArgSpec.new args
@@ -4,5 +4,5 @@
4
4
  #
5
5
  module Hashmake
6
6
  # hashmake version
7
- VERSION = "0.1.5"
7
+ VERSION = "0.1.6"
8
8
  end
@@ -2,17 +2,41 @@ require 'pry'
2
2
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
3
 
4
4
  describe Hashmake::HashMakeable do
5
+ class AlsoHashMakeable
6
+ include Comparable
7
+ include HashMakeable
8
+
9
+ ARG_SPECS = {
10
+ :a_number => arg_spec(:reqd => false, :type => Numeric, :default => 1.0)
11
+ }
12
+
13
+ attr_accessor :a_number
14
+
15
+ def initialize args = {}
16
+ hash_make ARG_SPECS, args
17
+ end
18
+
19
+ def <=>(other)
20
+ a_number <=> other.a_number
21
+ end
22
+ end
23
+
5
24
  class MyTestClass
6
25
  include HashMakeable
7
26
 
27
+ NON_REQD_FLOAT_DEFAULT = 0.0
28
+
8
29
  ARG_SPECS = {
9
30
  :reqd_string => arg_spec(:reqd => true, :type => String, :validator => ->(a){ a.length < 10 }),
10
- :not_reqd_float => arg_spec(:reqd => false, :type => Float, :default => 0.0, :validator => ->(a){ a.between?(0.0,1.0) }),
31
+ :not_reqd_float => arg_spec(:reqd => false, :type => Float, :default => NON_REQD_FLOAT_DEFAULT, :validator => ->(a){ a.between?(0.0,1.0) }),
11
32
  :not_reqd_array_of_float => arg_spec_array(:reqd => false, :type => Float, :validator => ->(a){ a.between?(0.0,1.0) }),
12
33
  :not_reqd_hash_of_float => arg_spec_hash(:reqd => false, :type => Float, :validator => ->(a){ a.between?(0.0,1.0) }),
34
+ :also_hash_makeable => arg_spec(:reqd => false, :type => AlsoHashMakeable, :default => ->(){ AlsoHashMakeable.new }),
35
+ :ary_of_also_hash_makeables => arg_spec_array(:reqd => false, :type => AlsoHashMakeable)
13
36
  }
14
37
 
15
- attr_reader :reqd_string, :not_reqd_float, :not_reqd_array_of_float, :not_reqd_hash_of_float
38
+ attr_accessor :not_reqd_float, :ary_of_also_hash_makeables
39
+ attr_reader :reqd_string, :not_reqd_array_of_float, :not_reqd_hash_of_float, :also_hash_makeable
16
40
 
17
41
  def initialize hashed_args = {}
18
42
  hash_make ARG_SPECS, hashed_args
@@ -104,6 +128,64 @@ describe Hashmake::HashMakeable do
104
128
  end.should raise_error(ArgumentError)
105
129
  end
106
130
  end
131
+
132
+ context 'hash-makeable arg' do
133
+ it 'should construct the hash-makeable arg from just a Hash' do
134
+ a_number = 5
135
+ a = MyTestClass.new(:reqd_string => "ok", :also_hash_makeable => { :a_number => a_number })
136
+ a.also_hash_makeable.a_number.should eq(a_number)
137
+ end
138
+ end
139
+ end
140
+
141
+ describe '#make_hash' do
142
+ before :each do
143
+ @reqd_string = "okeydoke"
144
+ @obj = MyTestClass.new :reqd_string => @reqd_string, :non_reqd_float => MyTestClass::NON_REQD_FLOAT_DEFAULT
145
+ @hash = @obj.make_hash
146
+ end
147
+
148
+ it 'should produce a Hash' do
149
+ @hash.should be_a Hash
150
+ end
151
+
152
+ it "should always include req'd values" do
153
+ @hash.should include(:reqd_string)
154
+ @hash[:reqd_string].should eq(@reqd_string)
155
+ end
156
+
157
+ it "should never include non-req'd default values" do
158
+ @hash.should_not include(:non_reqd_float)
159
+ end
107
160
 
108
- end
161
+ it "should always include non-req'd non-default values" do
162
+ @obj.not_reqd_float = 2.0
163
+ hash = @obj.make_hash
164
+ hash.should include(:not_reqd_float)
165
+ hash[:not_reqd_float].should eq(2.0)
166
+ end
167
+
168
+ it "should turn any hash-makeable objects into Hash objects" do
169
+ @obj.also_hash_makeable.a_number = 2.0
170
+ hash = @obj.make_hash
171
+ hash.should include(:also_hash_makeable)
172
+ hash[:also_hash_makeable].should be_a Hash
173
+ hash[:also_hash_makeable].should include(:a_number)
174
+ hash[:also_hash_makeable][:a_number].should eq(2.0)
175
+ end
176
+
177
+ it "should turn an array of hash-makeable objects into an array of Hash objects" do
178
+ obj2 = MyTestClass.new @obj.make_hash
179
+ @obj.ary_of_also_hash_makeables = [
180
+ AlsoHashMakeable.new(:a_number => 1),
181
+ AlsoHashMakeable.new(:a_number => 2),
182
+ AlsoHashMakeable.new(:a_number => 3),
183
+ ]
184
+ obj2 = MyTestClass.new @obj.make_hash
185
+ obj2.ary_of_also_hash_makeables.count.should be(3)
186
+ obj2.ary_of_also_hash_makeables[0].a_number.should eq(1)
187
+ obj2.ary_of_also_hash_makeables[1].a_number.should eq(2)
188
+ obj2.ary_of_also_hash_makeables[2].a_number.should eq(3)
189
+ end
190
+ end
109
191
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hashmake
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-04 00:00:00.000000000 Z
12
+ date: 2013-03-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -133,7 +133,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
133
133
  version: '0'
134
134
  segments:
135
135
  - 0
136
- hash: 144920435
136
+ hash: 77734045
137
137
  required_rubygems_version: !ruby/object:Gem::Requirement
138
138
  none: false
139
139
  requirements:
@@ -142,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
142
  version: '0'
143
143
  segments:
144
144
  - 0
145
- hash: 144920435
145
+ hash: 77734045
146
146
  requirements: []
147
147
  rubyforge_project:
148
148
  rubygems_version: 1.8.23