Skip to content

Commit

Permalink
Merge pull request #47 from lotus/attributes
Browse files Browse the repository at this point in the history
Utils::Attributes
  • Loading branch information
jodosha committed Dec 4, 2014
2 parents 2a30ed5 + b77d2a7 commit a15ec9e
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 0 deletions.
106 changes: 106 additions & 0 deletions 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
25 changes: 25 additions & 0 deletions lib/lotus/utils/hash.rb
Expand Up @@ -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
Expand Down
120 changes: 120 additions & 0 deletions 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
18 changes: 18 additions & 0 deletions test/hash_test.rb
Expand Up @@ -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
Expand Down

0 comments on commit a15ec9e

Please sign in to comment.