From 8750f87171bf1ed0cc4346f42ab5be0be1ea7d12 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Mon, 24 Nov 2014 16:30:52 +0100 Subject: [PATCH 1/2] Implemented Utils::Hash#stringify! --- lib/lotus/utils/hash.rb | 25 +++++++++++++++++++++++++ test/hash_test.rb | 18 ++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lib/lotus/utils/hash.rb b/lib/lotus/utils/hash.rb index 494fc7bd..39b9d3e9 100644 --- a/lib/lotus/utils/hash.rb +++ b/lib/lotus/utils/hash.rb @@ -57,6 +57,31 @@ def symbolize! self end + # Convert in-place all the keys to Symbol instances, nested hashes are converted too. + # + # @return [Hash] self + # + # @since x.x.x + # + # @example + # require 'lotus/utils/hash' + # + # hash = Lotus::Utils::Hash.new a: 23, b: { c: ['x','y','z'] } + # hash.stringify! + # + # hash.keys # => [:a, :b] + # hash.inspect # => {"a"=>23, "b"=>{"c"=>["x", "y", "z"]}} + def stringify! + keys.each do |k| + v = delete(k) + v = Hash.new(v).stringify! if v.is_a?(::Hash) + + self[k.to_s] = v + end + + self + end + # Return a deep copy of the current Lotus::Utils::Hash # # @return [Hash] a deep duplicated self diff --git a/test/hash_test.rb b/test/hash_test.rb index fae4976d..39ad4bd8 100644 --- a/test/hash_test.rb +++ b/test/hash_test.rb @@ -42,6 +42,24 @@ end end + describe '#stringify!' do + it 'covert keys to strings' do + hash = Lotus::Utils::Hash.new(fub: 'baz') + hash.stringify! + + hash[:fub].must_be_nil + hash['fub'].must_equal('baz') + end + + it 'stringifies nested hashes' do + hash = Lotus::Utils::Hash.new(nested: {key: 'value'}) + hash.stringify! + + hash['nested'].must_be_kind_of Lotus::Utils::Hash + hash['nested']['key'].must_equal('value') + end + end + describe '#deep_dup' do it 'returns an instance of Utils::Hash' do duped = Lotus::Utils::Hash.new('foo' => 'bar').deep_dup From b77d2a7f9fb1c204c406aa83080196c958c87159 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Thu, 4 Dec 2014 10:45:44 +0100 Subject: [PATCH 2/2] Introducing Utils::Attributes --- lib/lotus/utils/attributes.rb | 106 ++++++++++++++++++++++++++++++ test/attributes_test.rb | 120 ++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 lib/lotus/utils/attributes.rb create mode 100644 test/attributes_test.rb diff --git a/lib/lotus/utils/attributes.rb b/lib/lotus/utils/attributes.rb new file mode 100644 index 00000000..e4a5029a --- /dev/null +++ b/lib/lotus/utils/attributes.rb @@ -0,0 +1,106 @@ +require 'lotus/utils/hash' + +module Lotus + module Utils + # A set of attributes. + # + # It internally stores the data as a Hash. + # + # All the operations convert keys to strings. + # This strategy avoids memory attacks due to Symbol abuses when parsing + # untrusted input. + # + # At the same time, this allows to get/set data with the original key or + # with the string representation. See the examples below. + # + # @since x.x.x + class Attributes + # Initialize a set of attributes + # All the keys of the given Hash are recursively converted to strings. + # + # @param hash [#to_h] a Hash or any object that implements #to_h + # + # @return [Lotus::Utils::Attributes] self + # + # @since x.x.x + # + # @example + # require 'lotus/utils/attributes' + # + # attributes = Lotus::Utils::Attributes.new(a: 1, b: { 2 => [3, 4] } }) + # attributes.to_h # => { "a" => 1, "b" => { "2" => [3, 4] } } + def initialize(hash = {}) + @attributes = Utils::Hash.new(hash, &nil).stringify! + end + + # Get the value associated with the given attribute + # + # @param attribute [#to_s] a String or any object that implements #to_s + # + # @return [Object,NilClass] the associated value, if present + # + # @since x.x.x + # + # @example + # require 'lotus/utils/attributes' + # + # attributes = Lotus::Utils::Attributes.new(a: 1, 'b' => 2, 23 => 'foo') + # + # attributes.get(:a) # => 1 + # attributes.get('a') # => 1 + # + # attributes.get(:b) # => 2 + # attributes.get('b') # => 2 + # + # attributes.get(23) # => "foo" + # attributes.get('23') # => "foo" + # + # attributes.get(:unknown) # => nil + # attributes.get('unknown') # => nil + def get(attribute) + @attributes[attribute.to_s] + end + + # Set the given value for the given attribute + # + # @param attribute [#to_s] a String or any object that implements #to_s + # @param value [Object] any value + # + # @return [NilClass] + # + # @since x.x.x + # + # @example + # require 'lotus/utils/attributes' + # + # attributes = Lotus::Utils::Attributes.new + # + # attributes.set(:a, 1) + # attributes.get(:a) # => 1 + # attributes.get('a') # => 1 + # + # attributes.set('b', 2) + # attributes.get(:b) # => 2 + # attributes.get('b') # => 2 + # + # attributes.set(23, 'foo') + # attributes.get(23) # => "foo" + # attributes.get('23') # => "foo" + def set(attribute, value) + @attributes[attribute.to_s] = value + nil + end + + # Returns a deep duplicated copy of the attributes as a Hash + # + # @return [Lotus::Utils::Hash] + # + # @since x.x.x + # + # @see Lotus::Utils::Hash + def to_h + @attributes.deep_dup + end + end + end +end diff --git a/test/attributes_test.rb b/test/attributes_test.rb new file mode 100644 index 00000000..dbfddcaf --- /dev/null +++ b/test/attributes_test.rb @@ -0,0 +1,120 @@ +require 'test_helper' +require 'bigdecimal' +require 'lotus/utils/attributes' + +describe Lotus::Utils::Attributes do + describe '#initialize' do + before do + class AttributesSet + def to_h + {a: 1} + end + end + end + + after do + Object.__send__(:remove_const, :AttributesSet) + end + + it 'accepts an object that implements #to_h' do + attributes = Lotus::Utils::Attributes.new(AttributesSet.new) + attributes.to_h.must_equal({'a' => 1}) + end + + it "ignores hash default" do + attributes = Lotus::Utils::Attributes.new{|h,k| h[k] = [] } + attributes.get('uknown').must_be_nil + end + + it 'recursively stringify keys' do + attributes = Lotus::Utils::Attributes.new({a: 1, b: { 2 => [3, 4] }}) + attributes.to_h.must_equal({'a'=>1, 'b'=>{'2'=>[3,4]}}) + end + end + + describe '#get' do + it 'returns value associated to the given key (string)' do + attributes = Lotus::Utils::Attributes.new('foo' => 'bar') + attributes.get('foo').must_equal 'bar' + attributes.get(:foo).must_equal 'bar' + end + + it 'returns value associated to the given key (symbol)' do + attributes = Lotus::Utils::Attributes.new(foo: 'bar') + attributes.get(:foo).must_equal 'bar' + attributes.get('foo').must_equal 'bar' + end + + it 'returns value associated to the given key (number)' do + attributes = Lotus::Utils::Attributes.new( 23 => 'foo') + attributes.get(23).must_equal 'foo' + attributes.get('23').must_equal 'foo' + end + + it 'correctly handles Ruby falsey' do + attributes = Lotus::Utils::Attributes.new('foo' => false) + attributes.get(:foo).must_equal false + attributes.get('foo').must_equal false + + attributes = Lotus::Utils::Attributes.new(foo: false) + attributes.get(:foo).must_equal false + end + + it 'ignores hash default' do + attributes = Lotus::Utils::Attributes.new{|h,k| h[k] = [] } + attributes.get('foo').must_be_nil + attributes.get(:foo).must_be_nil + end + + it 'overrides clashing keys' do + attributes = Lotus::Utils::Attributes.new('foo' => 'bar', foo: 'baz') + attributes.get('foo').must_equal 'baz' + attributes.get(:foo).must_equal 'baz' + end + end + + describe '#set' do + it 'is a void operation' do + Lotus::Utils::Attributes.new.set('foo', 11).must_be_nil + end + + it 'sets a value (string)' do + attributes = Lotus::Utils::Attributes.new + attributes.set('foo', 'bar') + + attributes.get('foo').must_equal 'bar' + attributes.get(:foo).must_equal 'bar' + end + + it 'sets a value (symbol)' do + attributes = Lotus::Utils::Attributes.new + attributes.set(:foo, 'bar') + + attributes.get('foo').must_equal 'bar' + attributes.get(:foo).must_equal 'bar' + end + + it 'sets a value (number)' do + attributes = Lotus::Utils::Attributes.new + attributes.set(23, 'bar') + + attributes.get(23).must_equal 'bar' + attributes.get('23').must_equal 'bar' + end + end + + describe '#to_h' do + it 'returns a ::Hash' do + attributes = Lotus::Utils::Attributes.new(foo: 'bar') + attributes.to_h.must_equal({'foo' => 'bar'}) + end + + it 'prevents information escape' do + actual = Lotus::Utils::Attributes.new({'a' => 1}) + hash = actual.to_h + hash.merge!('b' => 2) + + actual.get('b').must_be_nil + end + end +end