hashmake 0.1.5 → 0.1.6

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/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