parsable_hash 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Code Climate](https://codeclimate.com/github/koleksiuk/parsable_hash.png)](https://codeclimate.com/github/koleksiuk/parsable_hash)
|
2
|
+
[![Build Status](https://secure.travis-ci.org/koleksiuk/parsable_hash.png)](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
|