parsable_hash 0.0.1
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/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +114 -0
- data/Rakefile +1 -0
- data/lib/parsable_hash.rb +49 -0
- data/lib/parsable_hash/converters/base.rb +27 -0
- data/lib/parsable_hash/converters/big_decimal.rb +13 -0
- data/lib/parsable_hash/converters/boolean.rb +11 -0
- data/lib/parsable_hash/converters/date.rb +13 -0
- data/lib/parsable_hash/converters/float.rb +11 -0
- data/lib/parsable_hash/converters/integer.rb +11 -0
- data/lib/parsable_hash/converters/null.rb +11 -0
- data/lib/parsable_hash/core_ext/hash/parse_with.rb +11 -0
- data/lib/parsable_hash/hash_strategy.rb +51 -0
- data/lib/parsable_hash/parser.rb +27 -0
- data/lib/parsable_hash/strategies.rb +44 -0
- data/lib/parsable_hash/strategy/converter_loader.rb +48 -0
- data/lib/parsable_hash/version.rb +3 -0
- data/parsable_hash.gemspec +24 -0
- data/spec/converters/big_decimal_spec.rb +16 -0
- data/spec/converters/boolean_spec.rb +21 -0
- data/spec/converters/date_spec.rb +18 -0
- data/spec/converters/float_spec.rb +15 -0
- data/spec/converters/integer_spec.rb +15 -0
- data/spec/converters/null_spec.rb +13 -0
- data/spec/dummy/module_spec.rb +121 -0
- data/spec/hash_strategy_spec.rb +50 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/strategies_spec.rb +67 -0
- data/spec/strategy/converter_loader_spec.rb +33 -0
- data/spec/support/converters/with_fallback.rb +17 -0
- data/tags +3 -0
- metadata +155 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Konrad Oleksiuk
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
[](https://codeclimate.com/github/koleksiuk/parsable_hash)
|
2
|
+
[](http://travis-ci.org/koleksiuk/parsable_hash)
|
3
|
+
|
4
|
+
# ParsableHash
|
5
|
+
|
6
|
+
Allows to parse hash values in easy way, by extending class with hash
|
7
|
+
strategies.
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'parsable_hash'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install parsable_hash
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
class FooTx
|
25
|
+
include ParsableHash
|
26
|
+
|
27
|
+
parse_strategy :transaction_strategy, :date => :date,
|
28
|
+
:amount => :big_decimal,
|
29
|
+
:id => :integer
|
30
|
+
|
31
|
+
attr_accessor :transaction
|
32
|
+
|
33
|
+
def initialize(raw_transaction = {})
|
34
|
+
self.transaction = parse_hash(raw_transaction, with: :transaction_strategy)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Fetch transaction from e.g. queue
|
39
|
+
transaction = { :date => '12-12-2013', :amount => '23.22', :id => '124564' }
|
40
|
+
|
41
|
+
tx = FooTx.new(transaction)
|
42
|
+
|
43
|
+
tx.transaction #=> { :date=>#<Date: 2013-12-12 ((2456639j,0s,0n),+0s,2299161j)>, :amount=>23.22, :id=>124564 }
|
44
|
+
|
45
|
+
## Available parsers
|
46
|
+
:big_decimal => BigDecimal
|
47
|
+
:boolean => Boolean
|
48
|
+
:date => Date
|
49
|
+
:float => Float
|
50
|
+
:integer => Integer
|
51
|
+
:date_time => DateTime
|
52
|
+
:null => default parser (if parser is missing it simply leaves value as it is)
|
53
|
+
|
54
|
+
### Parsing with modules and other features
|
55
|
+
|
56
|
+
# Using strategies from other modules / classes
|
57
|
+
module ParseStrategies
|
58
|
+
include ParsableHash
|
59
|
+
|
60
|
+
parse_strategy :transaction_strategy, :date => :date,
|
61
|
+
:amount => :big_decimal,
|
62
|
+
:id => :integer
|
63
|
+
end
|
64
|
+
|
65
|
+
class FooTx
|
66
|
+
include ParsableHash
|
67
|
+
|
68
|
+
|
69
|
+
attr_accessor :transaction
|
70
|
+
|
71
|
+
def initialize(raw_transaction = {})
|
72
|
+
self.transaction = parse_hash(
|
73
|
+
raw_transaction,
|
74
|
+
:with => :transaction_strategy,
|
75
|
+
:const => ParseStrategies # must be kind of module/class
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
## Extending with own parsers
|
82
|
+
|
83
|
+
module ParsableHash
|
84
|
+
module Converters
|
85
|
+
class MyClass < Base
|
86
|
+
private
|
87
|
+
|
88
|
+
def try_convert
|
89
|
+
::MyClass.parse_string(@value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
And then use ```:my_class``` as parse strategy for key
|
96
|
+
|
97
|
+
## Configuration
|
98
|
+
ParsableHash::Strategy.fallbacks = true # set to false if you want to raise ParsableHash::MissingStrategy if strategy doesn't exist.
|
99
|
+
# In other case it is empty strategy (you can override it by adding :default to your strategies)
|
100
|
+
|
101
|
+
## To-do
|
102
|
+
* Add support for adding default values
|
103
|
+
* Add support for configurable fallback to string if key as symbol is missing (&
|
104
|
+
vice-versa)
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
## Contributing
|
109
|
+
|
110
|
+
1. Fork it
|
111
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
112
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
113
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
114
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'parsable_hash/version'
|
2
|
+
require 'parsable_hash/strategies'
|
3
|
+
require 'parsable_hash/converters/base'
|
4
|
+
require 'parsable_hash/strategy/converter_loader'
|
5
|
+
require 'parsable_hash/hash_strategy'
|
6
|
+
require 'parsable_hash/parser'
|
7
|
+
|
8
|
+
require 'parsable_hash/core_ext/hash/parse_with'
|
9
|
+
|
10
|
+
module ParsableHash
|
11
|
+
NotDefinedError = Class.new(StandardError)
|
12
|
+
|
13
|
+
def self.included(klass)
|
14
|
+
klass.send(:include, InstanceMethods)
|
15
|
+
klass.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
def parse_hash(object, options = {})
|
20
|
+
parse_with_strategy(object, options[:with], options[:const])
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse_with_strategy(hash, strategy, const = nil)
|
26
|
+
if const && const.is_a?(Module)
|
27
|
+
raise NotDefinedError.new unless const.include?(ParsableHash)
|
28
|
+
|
29
|
+
Parser.new(hash, const.parse_strategies[strategy]).call
|
30
|
+
else
|
31
|
+
Parser.new(hash, parse_strategies[strategy]).call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_strategies
|
36
|
+
self.class.parse_strategies
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
def parse_strategy(name, options = {})
|
42
|
+
parse_strategies.add(name, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_strategies
|
46
|
+
@strategies ||= Strategies.new
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ParsableHash
|
2
|
+
module Converters
|
3
|
+
class Base
|
4
|
+
def initialize(value)
|
5
|
+
@value = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(&block)
|
9
|
+
try_convert
|
10
|
+
rescue => e
|
11
|
+
if block_given?
|
12
|
+
block.call(@value)
|
13
|
+
else
|
14
|
+
@value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def try_convert
|
21
|
+
raise NotImplementedError.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Gem.find_files("parsable_hash/converters/*.rb").delete_if {|f| f =~ /base/ }.each {|f| require f }
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Hash.class_eval do
|
2
|
+
define_method :parse_with do |object, strategy|
|
3
|
+
if object.is_a? Module
|
4
|
+
raise ParsableHash::NotDefinedError.new unless object.include?(ParsableHash)
|
5
|
+
|
6
|
+
ParsableHash::Parser.new(self, object.parse_strategies[strategy]).call
|
7
|
+
else
|
8
|
+
object.parse_hash(self, :with => strategy)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ParsableHash
|
2
|
+
class HashStrategy
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :hash
|
6
|
+
attr_accessor :origin_hash
|
7
|
+
|
8
|
+
def initialize(strategy, origin_hash = nil)
|
9
|
+
@hash = prepare(strategy)
|
10
|
+
self.origin_hash = origin_hash
|
11
|
+
|
12
|
+
clean_for_hash!(origin_hash) unless origin_hash.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(&block)
|
16
|
+
hash.each { |strategy| yield strategy }
|
17
|
+
end
|
18
|
+
|
19
|
+
def clean_for_hash!(value_hash)
|
20
|
+
@hash = clean_pairs(@hash, value_hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def prepare(hash)
|
26
|
+
hash.inject({}) do |h, (k, v)|
|
27
|
+
h[k] = if v.is_a? Hash
|
28
|
+
prepare(v)
|
29
|
+
else
|
30
|
+
Strategy::ConverterLoader.from_value(v)
|
31
|
+
end
|
32
|
+
|
33
|
+
h
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def clean_pairs(strategy_hash, value_hash)
|
38
|
+
strategy_hash.each_pair do |key, val|
|
39
|
+
if value_hash[key].is_a?(Hash)
|
40
|
+
if val.is_a?(Hash)
|
41
|
+
clean_pairs(strategy_hash[key], value_hash[key])
|
42
|
+
else
|
43
|
+
strategy_hash.delete(key)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
strategy_hash.delete(key) unless value_hash.has_key?(key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ParsableHash
|
2
|
+
class Parser
|
3
|
+
def initialize(hash, strategy)
|
4
|
+
@hash = hash
|
5
|
+
@strategy = HashStrategy.new(strategy, @hash)
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
deep_parse(@hash, @strategy.hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def deep_parse(hash, strategy)
|
14
|
+
hash.merge(strategy) do |key, val, str|
|
15
|
+
if val.is_a?(Hash) && str.is_a?(Hash)
|
16
|
+
deep_parse(val, str)
|
17
|
+
else
|
18
|
+
parse_with_strategy(val, str)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_with_strategy(value, strategy)
|
24
|
+
strategy.call(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ParsableHash
|
2
|
+
MissingStrategy = Class.new(StandardError)
|
3
|
+
|
4
|
+
class Strategies
|
5
|
+
class << self
|
6
|
+
def fallbacks
|
7
|
+
@fallbacks.nil? ? true : @fallbacks
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_writer :fallbacks
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(value = {})
|
14
|
+
strategies[:default] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](strategy)
|
18
|
+
get(strategy)
|
19
|
+
end
|
20
|
+
|
21
|
+
def []=(strategy, value)
|
22
|
+
add(strategy, value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(strategy)
|
26
|
+
_strategy = strategies[strategy.to_sym]
|
27
|
+
|
28
|
+
return _strategy unless _strategy.nil?
|
29
|
+
return strategies[:default] if _strategy.nil? && self.class.fallbacks
|
30
|
+
|
31
|
+
raise MissingStrategy.new("Strategy #{strategy} is missing!")
|
32
|
+
end
|
33
|
+
|
34
|
+
def add(strategy, value)
|
35
|
+
strategies[strategy] = value
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def strategies
|
41
|
+
@strategies ||= {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ParsableHash
|
2
|
+
module Strategy
|
3
|
+
class ConverterLoader
|
4
|
+
def self.from_value(value)
|
5
|
+
if value.is_a? Class
|
6
|
+
from_class(value)
|
7
|
+
else
|
8
|
+
from_name(value)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_name(name)
|
13
|
+
new(name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_class(klass)
|
17
|
+
new(nil, :converter => klass)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :klass
|
21
|
+
|
22
|
+
def initialize(name, options = {})
|
23
|
+
@name = name.to_s
|
24
|
+
self.klass = options.fetch(:converter) { load_from_name }
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(val)
|
28
|
+
klass.new(val).call
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def load_from_name
|
34
|
+
klass = camelize(@name)
|
35
|
+
|
36
|
+
if ParsableHash::Converters.const_defined?(klass)
|
37
|
+
ParsableHash::Converters.const_get(klass)
|
38
|
+
else
|
39
|
+
ParsableHash::Converters::Null
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def camelize(name)
|
44
|
+
name.split('_').map(&:capitalize).join
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'parsable_hash/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "parsable_hash"
|
8
|
+
spec.version = ParsableHash::VERSION
|
9
|
+
spec.authors = ["Konrad Oleksiuk"]
|
10
|
+
spec.email = ["konole@gmail.com"]
|
11
|
+
spec.description = %q{Allows to parse hash with e.g. string values to other class instances}
|
12
|
+
spec.summary = %q{ParsableHash extension for classes}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Converters::BigDecimal do
|
4
|
+
describe '#call' do
|
5
|
+
context 'when value can be parsed' do
|
6
|
+
subject { described_class.new('5.3') }
|
7
|
+
|
8
|
+
it 'should return float object' do
|
9
|
+
expect(subject.call).to eq(BigDecimal.new('5.3'))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it_should_behave_like "with fallback"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Converters::Boolean do
|
4
|
+
describe '#call' do
|
5
|
+
describe 'when value == "true"' do
|
6
|
+
subject { described_class.new('true') }
|
7
|
+
|
8
|
+
it 'should return true value' do
|
9
|
+
expect(subject.call).to eq(true)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'when value != "true"' do
|
14
|
+
subject { described_class.new('falsy') }
|
15
|
+
|
16
|
+
it 'should return false value' do
|
17
|
+
expect(subject.call).to eq(false)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Converters::Date do
|
4
|
+
describe '#call' do
|
5
|
+
context 'when value can be parsed' do
|
6
|
+
let(:date_str) { '12.12.2012' }
|
7
|
+
let(:date_obj) { Date.parse(date_str) }
|
8
|
+
|
9
|
+
subject { described_class.new('12.12.2012') }
|
10
|
+
|
11
|
+
it 'should return date object' do
|
12
|
+
expect(subject.call).to eq(date_obj)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it_should_behave_like "with fallback"
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Converters::Float do
|
4
|
+
describe '#call' do
|
5
|
+
context 'when value can be parsed' do
|
6
|
+
subject { described_class.new('5.3') }
|
7
|
+
|
8
|
+
it 'should return float object' do
|
9
|
+
expect(subject.call).to eq(5.3)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it_should_behave_like "with fallback"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Converters::Integer do
|
4
|
+
describe '#call' do
|
5
|
+
context 'when value can be parsed' do
|
6
|
+
subject { described_class.new('5') }
|
7
|
+
|
8
|
+
it 'should return integer object' do
|
9
|
+
expect(subject.call).to eq(5)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it_should_behave_like "with fallback"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Converters::Null do
|
4
|
+
describe '#call' do
|
5
|
+
[3, 'test', Object.new, {}].each do |obj|
|
6
|
+
it "should return passed object (e.g. #{obj.class}) without any change" do
|
7
|
+
converter = described_class.new(obj)
|
8
|
+
|
9
|
+
expect(converter.call).to eq(obj)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DummyParsers
|
4
|
+
include ParsableHash
|
5
|
+
|
6
|
+
parse_strategy :rands, :int => :integer, :fl => :float, :nested => { :dt => :date }
|
7
|
+
end
|
8
|
+
|
9
|
+
class Dummy
|
10
|
+
include ParsableHash
|
11
|
+
|
12
|
+
parse_strategy :random_values, :int => :integer, :fl => :float, :nested => { :dt => :date }
|
13
|
+
parse_strategy :int, :int => :integer
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
parse_hash(@options, :with => :random_values)
|
21
|
+
end
|
22
|
+
|
23
|
+
def call_with_const
|
24
|
+
parse_hash(@options, :with => :rands, :const => DummyParsers)
|
25
|
+
end
|
26
|
+
|
27
|
+
def call_with_wrong_const
|
28
|
+
parse_hash(@options, :with => :rands, :const => Object)
|
29
|
+
end
|
30
|
+
|
31
|
+
def direct_call
|
32
|
+
@options.parse_with(self, :random_values)
|
33
|
+
end
|
34
|
+
|
35
|
+
def const_call
|
36
|
+
@options.parse_with(DummyParsers, :rands)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe Dummy do
|
41
|
+
describe '#parse_hash' do
|
42
|
+
context 'when all values exist' do
|
43
|
+
let(:obj) { Dummy.new(:int => '3', :fl => '3.5', :nested => { :dt => str_date }) }
|
44
|
+
let(:str_date) { '12.12.2013' }
|
45
|
+
let(:obj_date) { Date.parse(str_date) }
|
46
|
+
|
47
|
+
after do
|
48
|
+
expect(parsed_obj_with_hash).to eq({
|
49
|
+
:int => 3,
|
50
|
+
:fl => 3.5,
|
51
|
+
:nested => { :dt => obj_date },
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'call' do
|
56
|
+
let(:parsed_obj_with_hash) { obj.call }
|
57
|
+
|
58
|
+
it 'should parse hash to values passed with given strategy' do
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'call with constant passed' do
|
63
|
+
let(:parsed_obj_with_hash) { obj.call_with_const }
|
64
|
+
|
65
|
+
it 'should parse hash to values passed with given strategy' do
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'call with wrong constant passed' do
|
72
|
+
let(:obj) { Dummy.new }
|
73
|
+
|
74
|
+
it 'should raise an error' do
|
75
|
+
expect {
|
76
|
+
obj.call_with_wrong_const
|
77
|
+
}.to raise_error(ParsableHash::NotDefinedError)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when hash value is missing' do
|
82
|
+
let(:obj) { Dummy.new(:int => '3', :nested => { :foo => 'test' }, :bar => { :test => 'foobar' }) }
|
83
|
+
|
84
|
+
let(:parsed_obj_with_hash) { obj.call }
|
85
|
+
it 'should not create new key-value pair in hash' do
|
86
|
+
expect(parsed_obj_with_hash).to eq({
|
87
|
+
:int => 3,
|
88
|
+
:nested => { :foo => 'test' },
|
89
|
+
:bar => { :test => 'foobar' },
|
90
|
+
})
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe 'hash#parse_with' do
|
96
|
+
let(:str_date) { '12.12.2013' }
|
97
|
+
let(:obj_date) { Date.parse(str_date) }
|
98
|
+
let(:obj) { Dummy.new(:int => '3', :fl => '3.5', :nested => { :dt => str_date }) }
|
99
|
+
|
100
|
+
after do
|
101
|
+
expect(subject).to eq({
|
102
|
+
:int => 3,
|
103
|
+
:fl => 3.5,
|
104
|
+
:nested => { :dt => obj_date },
|
105
|
+
})
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'with self' do
|
109
|
+
subject { obj.direct_call }
|
110
|
+
it 'should parse hash to values passed with given strategy' do
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'with class / module' do
|
115
|
+
subject { obj.const_call }
|
116
|
+
it 'should parse hash to values passed with given strategy' do
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::HashStrategy do
|
4
|
+
|
5
|
+
let(:integer_klass) { double('integer_klass') }
|
6
|
+
let(:float_klass) { double('float_klass') }
|
7
|
+
let(:object_klass) { double('object_klass') }
|
8
|
+
|
9
|
+
context 'when all values are symbols' do
|
10
|
+
before do
|
11
|
+
ParsableHash::Strategy::ConverterLoader.stub(:from_name).with(:integer) { integer_klass }
|
12
|
+
ParsableHash::Strategy::ConverterLoader.stub(:from_name).with(:float) { float_klass }
|
13
|
+
end
|
14
|
+
|
15
|
+
subject { described_class.new(:one => :integer, :two => :float) }
|
16
|
+
|
17
|
+
it 'should replace them with callable converters' do
|
18
|
+
expect(subject.hash).to eq({
|
19
|
+
:one => integer_klass,
|
20
|
+
:two => float_klass,
|
21
|
+
})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when there are direct class passed' do
|
26
|
+
before do
|
27
|
+
ParsableHash::Strategy::ConverterLoader.stub(:from_name).with(:integer) { integer_klass }
|
28
|
+
ParsableHash::Strategy::ConverterLoader.stub(:from_class).with(Object) { object_klass }
|
29
|
+
end
|
30
|
+
|
31
|
+
subject { described_class.new(:one => :integer, :two => Object) }
|
32
|
+
|
33
|
+
it 'should leave them as they are' do
|
34
|
+
expect(subject.hash).to eq({
|
35
|
+
:one => integer_klass,
|
36
|
+
:two => object_klass,
|
37
|
+
})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'cleaning up strategy hash' do
|
42
|
+
it 'should be possible with passing value hash into initializer' do
|
43
|
+
pending
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should be possible by invoking directly #clean_for_hash!' do
|
47
|
+
pending
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require './lib/parsable_hash'
|
3
|
+
|
4
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
5
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
6
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
7
|
+
# loaded once.
|
8
|
+
#
|
9
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
10
|
+
Gem.find_files("support/**/*.rb").each {|f| require f }
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
14
|
+
config.run_all_when_everything_filtered = true
|
15
|
+
config.filter_run :focus
|
16
|
+
|
17
|
+
# Run specs in random order to surface order dependencies. If you find an
|
18
|
+
# order dependency and want to debug it, you can fix the order by providing
|
19
|
+
# the seed, which is printed after each run.
|
20
|
+
# --seed 1234
|
21
|
+
config.order = 'random'
|
22
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Strategies do
|
4
|
+
let(:strategies) { described_class.new }
|
5
|
+
let(:foo_strategy) { { :bar => :integer } }
|
6
|
+
|
7
|
+
describe 'on initialize' do
|
8
|
+
it 'should have at least default strategy' do
|
9
|
+
expect(strategies[:default]).to eq({})
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'accessing strategry' do
|
14
|
+
|
15
|
+
before { strategies.add(:new, foo_strategy) }
|
16
|
+
|
17
|
+
it 'should return strategy with array accessor' do
|
18
|
+
expect(strategies[:new]).to eq(foo_strategy)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#get' do
|
22
|
+
context 'for string' do
|
23
|
+
it 'should return strategy' do
|
24
|
+
expect(strategies.get('new')).to eq(foo_strategy)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'for symbol' do
|
29
|
+
it 'should return strategy' do
|
30
|
+
expect(strategies.get(:new)).to eq(foo_strategy)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'setting strategy' do
|
37
|
+
it 'should allow to set strategy with array accessor' do
|
38
|
+
strategies[:new] = foo_strategy
|
39
|
+
expect(strategies.get(:new)).to eq(foo_strategy)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should allow to set strategy with #add method' do
|
43
|
+
strategies.add(:new, foo_strategy)
|
44
|
+
expect(strategies.get(:new)).to eq(foo_strategy)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
describe 'dealing with missing strategies' do
|
50
|
+
context 'by default' do
|
51
|
+
it 'should return default strategy' do
|
52
|
+
expect(strategies[:foobar]).to eq({})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when set fallbacks to false' do
|
57
|
+
before { ParsableHash::Strategies.fallbacks = false }
|
58
|
+
after { ParsableHash::Strategies.fallbacks = true }
|
59
|
+
|
60
|
+
it 'should raise an error' do
|
61
|
+
expect {
|
62
|
+
strategies[:foobar]
|
63
|
+
}.to raise_error(ParsableHash::MissingStrategy, "Strategy foobar is missing!")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ParsableHash::Strategy::ConverterLoader do
|
4
|
+
let(:klass) { double('my_converter') }
|
5
|
+
|
6
|
+
before do
|
7
|
+
ParsableHash::Converters.stub(:const_defined?).with('MyConverter') { true }
|
8
|
+
ParsableHash::Converters.stub(:const_get).with('MyConverter') { klass }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'coverter loading' do
|
12
|
+
subject { described_class.new(:my_converter) }
|
13
|
+
|
14
|
+
it 'should load converter class from name' do
|
15
|
+
expect(subject.klass).to eq(klass)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'calling converter' do
|
20
|
+
before do
|
21
|
+
klass.stub(:new).with('my_val').and_return(inst = double('converter_inst', :call => :my_val))
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should allow to load converter by passed symbol' do
|
25
|
+
expect(described_class.new(:my_converter).call('my_val')).to eq(:my_val)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
it 'should allow to initialize loader with given converter' do
|
31
|
+
expect(described_class.from_class(klass).klass).to eq(klass)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
shared_examples "with fallback" do
|
2
|
+
context 'when value can not be parsed' do
|
3
|
+
subject { described_class.new([1,2]) }
|
4
|
+
|
5
|
+
describe 'and block is given' do
|
6
|
+
it 'should return value of called block' do
|
7
|
+
expect(subject.call {|x| x.count }).to eq(2)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'and block is missing' do
|
12
|
+
it 'should return unchanged value' do
|
13
|
+
expect(subject.call).to eq([1,2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/tags
ADDED
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: parsable_hash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Konrad Oleksiuk
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2014-01-04 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: bundler
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 9
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 3
|
33
|
+
version: "1.3"
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: rspec
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
description: Allows to parse hash with e.g. string values to other class instances
|
65
|
+
email:
|
66
|
+
- konole@gmail.com
|
67
|
+
executables: []
|
68
|
+
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files: []
|
72
|
+
|
73
|
+
files:
|
74
|
+
- .gitignore
|
75
|
+
- .rspec
|
76
|
+
- .travis.yml
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- lib/parsable_hash.rb
|
82
|
+
- lib/parsable_hash/converters/base.rb
|
83
|
+
- lib/parsable_hash/converters/big_decimal.rb
|
84
|
+
- lib/parsable_hash/converters/boolean.rb
|
85
|
+
- lib/parsable_hash/converters/date.rb
|
86
|
+
- lib/parsable_hash/converters/float.rb
|
87
|
+
- lib/parsable_hash/converters/integer.rb
|
88
|
+
- lib/parsable_hash/converters/null.rb
|
89
|
+
- lib/parsable_hash/core_ext/hash/parse_with.rb
|
90
|
+
- lib/parsable_hash/hash_strategy.rb
|
91
|
+
- lib/parsable_hash/parser.rb
|
92
|
+
- lib/parsable_hash/strategies.rb
|
93
|
+
- lib/parsable_hash/strategy/converter_loader.rb
|
94
|
+
- lib/parsable_hash/version.rb
|
95
|
+
- parsable_hash.gemspec
|
96
|
+
- spec/converters/big_decimal_spec.rb
|
97
|
+
- spec/converters/boolean_spec.rb
|
98
|
+
- spec/converters/date_spec.rb
|
99
|
+
- spec/converters/float_spec.rb
|
100
|
+
- spec/converters/integer_spec.rb
|
101
|
+
- spec/converters/null_spec.rb
|
102
|
+
- spec/dummy/module_spec.rb
|
103
|
+
- spec/hash_strategy_spec.rb
|
104
|
+
- spec/spec_helper.rb
|
105
|
+
- spec/strategies_spec.rb
|
106
|
+
- spec/strategy/converter_loader_spec.rb
|
107
|
+
- spec/support/converters/with_fallback.rb
|
108
|
+
- tags
|
109
|
+
has_rdoc: true
|
110
|
+
homepage: ""
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
hash: 3
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
version: "0"
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
hash: 3
|
133
|
+
segments:
|
134
|
+
- 0
|
135
|
+
version: "0"
|
136
|
+
requirements: []
|
137
|
+
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.6.2
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: ParsableHash extension for classes
|
143
|
+
test_files:
|
144
|
+
- spec/converters/big_decimal_spec.rb
|
145
|
+
- spec/converters/boolean_spec.rb
|
146
|
+
- spec/converters/date_spec.rb
|
147
|
+
- spec/converters/float_spec.rb
|
148
|
+
- spec/converters/integer_spec.rb
|
149
|
+
- spec/converters/null_spec.rb
|
150
|
+
- spec/dummy/module_spec.rb
|
151
|
+
- spec/hash_strategy_spec.rb
|
152
|
+
- spec/spec_helper.rb
|
153
|
+
- spec/strategies_spec.rb
|
154
|
+
- spec/strategy/converter_loader_spec.rb
|
155
|
+
- spec/support/converters/with_fallback.rb
|