hashblock 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README +46 -0
- data/Rakefile +4 -0
- data/hashblock.gemspec +2 -1
- data/lib/hashblock/block_evaluator.rb +79 -0
- data/lib/hashblock/parser.rb +80 -0
- data/lib/hashblock/version.rb +1 -1
- data/test/parser_test.rb +135 -0
- data/test/test_helper.rb +4 -0
- metadata +22 -5
data/.gitignore
CHANGED
data/README
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
Hashblock (tentative name) is a simple gem that converts free form blocks into hashes.
|
2
|
+
|
3
|
+
Here's how to use it
|
4
|
+
--------------------
|
5
|
+
|
6
|
+
-> parser = Hashblock::Parser.new
|
7
|
+
=> #<Hashblock::Parser ...>
|
8
|
+
-> to_parse = Proc.new do
|
9
|
+
foo :bar
|
10
|
+
ping :pong
|
11
|
+
nest_this do
|
12
|
+
abra :cadabra
|
13
|
+
end
|
14
|
+
end
|
15
|
+
=> #<Proc:...>
|
16
|
+
-> parser.parse(to_parse)
|
17
|
+
=> {:foo=>:bar, :ping=>:pong, :nest_this=>{:abra=>:cadabra}}
|
18
|
+
|
19
|
+
This may be useful if you're creating an acts\_as\_* type plugin, for example. Say you want your users to invoke your plugin thusly:
|
20
|
+
|
21
|
+
acts_as_whizbang do
|
22
|
+
config do
|
23
|
+
whiz :bang
|
24
|
+
end
|
25
|
+
other_config :foo
|
26
|
+
end
|
27
|
+
|
28
|
+
Youll want to handle that like this:
|
29
|
+
|
30
|
+
def acts_as_whizbang(&block)
|
31
|
+
@config = Hashblock::Parser.new.parse(block)
|
32
|
+
end
|
33
|
+
|
34
|
+
Now, suppose you want the user to also be able to supply options via a Hash:
|
35
|
+
|
36
|
+
def acts_as_whizbang(hash, &block)
|
37
|
+
if block_given?
|
38
|
+
@config = Hashblock::Parser.new.parse(block)
|
39
|
+
else
|
40
|
+
@config = hash
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
This allows users of your plugin to configure acts\_as\_whizbang via a hash, too:
|
45
|
+
|
46
|
+
acts_as_whizbang :config => { :whiz => :bang }, :other_config => :foo
|
data/Rakefile
CHANGED
data/hashblock.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.email = ["nathanladd@gmail.com"]
|
11
11
|
s.homepage = "http://github.com/ntl"
|
12
12
|
s.summary = %q{Hashblock converts ruby hashes and blocks to friendly config objects}
|
13
|
-
s.description = %q{Sometimes you'll want to allow the user to configure a plugin or some other object via a ruby block where options are specified through method calls. Hashblock is a tool which converts either a hash or a block into a plain old ruby object that is simple to access and modify. Hashblock supports deep nesting and
|
13
|
+
s.description = %q{Sometimes you'll want to allow the user to configure a plugin or some other object via a ruby block where options are specified through method calls. Hashblock is a tool which converts either a hash or a block into a plain old ruby object that is simple to access and modify. Hashblock supports deep nesting and collision handling (since a block can contain multiple invokations of the same method)}
|
14
14
|
|
15
15
|
s.rubyforge_project = "hashblock"
|
16
16
|
|
@@ -19,5 +19,6 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
|
+
s.add_development_dependency "bluecloth"
|
22
23
|
s.add_development_dependency "shoulda"
|
23
24
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Hashblock::BlockEvaluator
|
2
|
+
MERGE_STRATEGIES = [:array, :exception, :first_wins, :last_wins]
|
3
|
+
|
4
|
+
class DuplicateProperty < StandardError ; end
|
5
|
+
|
6
|
+
def method_missing(sym, *args, &block)
|
7
|
+
#
|
8
|
+
# Setter method, looks like one of the following:
|
9
|
+
#
|
10
|
+
# some_property value
|
11
|
+
# self.some_property = value
|
12
|
+
if args.size == 1
|
13
|
+
if block_given?
|
14
|
+
raise ArgumentError, "Setters may not have blocks supplied"
|
15
|
+
end
|
16
|
+
__set(sym.to_s.chomp("="), args.first)
|
17
|
+
|
18
|
+
#
|
19
|
+
# Nested block, convert this to a hash and graft it on to self. Looks
|
20
|
+
# like this:
|
21
|
+
#
|
22
|
+
# some_property do
|
23
|
+
# some_nested_property true
|
24
|
+
# end
|
25
|
+
elsif args.empty? and block_given?
|
26
|
+
inner = self.class.new(block, @merge_strategy)
|
27
|
+
__set(sym, inner.to_hash)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(block, merge_strategy)
|
32
|
+
unless block.is_a?(Proc)
|
33
|
+
raise ArgumentError, "Must supply a Proc, not a `#{block.class}'"
|
34
|
+
end
|
35
|
+
|
36
|
+
@merge_strategy = merge_strategy
|
37
|
+
@properties = {}
|
38
|
+
|
39
|
+
self.instance_eval(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_hash
|
43
|
+
@properties
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def __merge_property(property_name, existing_value, new_value)
|
49
|
+
case @merge_strategy
|
50
|
+
when :array then
|
51
|
+
if existing_value.instance_variable_get(:@is_merged_property_array)
|
52
|
+
existing_value.push new_value
|
53
|
+
else
|
54
|
+
[existing_value, new_value].tap do |merged_property_array|
|
55
|
+
merged_property_array.instance_variable_set(:@is_merged_property_array,
|
56
|
+
true)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
when :exception then
|
60
|
+
raise DuplicateProperty, "Supplied multiple values for property "\
|
61
|
+
"`#{property_name}'; consider using a different merge strategy or "\
|
62
|
+
"fixing your input"
|
63
|
+
when :first_wins then existing_value
|
64
|
+
when :last_wins then new_value
|
65
|
+
else raise "Uh oh, @merge_strategy has been clobbered"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def __set(property_name, value)
|
70
|
+
property_name = property_name.to_sym
|
71
|
+
|
72
|
+
if @properties[property_name]
|
73
|
+
@properties[property_name] = __merge_property(property_name,
|
74
|
+
@properties[property_name], value)
|
75
|
+
else
|
76
|
+
@properties[property_name] = value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class Hashblock::Parser
|
2
|
+
DEFAULTS = {
|
3
|
+
:allow_nil => false,
|
4
|
+
:merge_strategy => :exception
|
5
|
+
}
|
6
|
+
|
7
|
+
attr_reader *DEFAULTS.keys
|
8
|
+
|
9
|
+
def allow_nil?
|
10
|
+
self.allow_nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
@options = {}
|
15
|
+
|
16
|
+
if args.last.is_a? Hash
|
17
|
+
args.pop.each do |key, value|
|
18
|
+
@options[key.to_sym] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
sanitize_options!
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse(hash_or_block, merge_strategy = nil)
|
26
|
+
merge_strategy ||= self.merge_strategy
|
27
|
+
|
28
|
+
if hash_or_block.nil? and allow_nil?
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
|
32
|
+
hash = case hash_or_block
|
33
|
+
when Proc then self.class.hashify_block(hash_or_block, merge_strategy)
|
34
|
+
when Hash then hash_or_block
|
35
|
+
else
|
36
|
+
class_list = [Proc, Hash]
|
37
|
+
if allow_nil?
|
38
|
+
class_list.unshift(NilClass)
|
39
|
+
end
|
40
|
+
|
41
|
+
raise ArgumentError, "Cannot parse a `#{hash_or_block.class}'; not one "\
|
42
|
+
"of #{class_list.map(&:to_s).join(', ')}"
|
43
|
+
end
|
44
|
+
|
45
|
+
hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.hashify_block(block, merge_strategy = DEFAULTS[:merge_strategy])
|
49
|
+
Hashblock::BlockEvaluator.new(block, merge_strategy).to_hash
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def sanitize_options!
|
55
|
+
unless @options.is_a?(Hash)
|
56
|
+
raise ArgumentError, "Must supply :defaults as a Hash, not a "\
|
57
|
+
"`#{@defaults.class}'"
|
58
|
+
end
|
59
|
+
|
60
|
+
DEFAULTS.each do |property_name, value|
|
61
|
+
@options[property_name] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
invalid_options = (@options.keys - DEFAULTS.keys).map(&:to_s)
|
65
|
+
unless invalid_options.empty?
|
66
|
+
raise ArgumentError, "Invalid option(s): #{invalid_options.join(', ')}"
|
67
|
+
end
|
68
|
+
|
69
|
+
@options.each do |option_name, value|
|
70
|
+
instance_variable_set("@#{option_name}", value)
|
71
|
+
end
|
72
|
+
|
73
|
+
valid_strategies = Hashblock::BlockEvaluator::MERGE_STRATEGIES
|
74
|
+
unless valid_strategies.include? merge_strategy
|
75
|
+
strategy_list = valid_strategies.map(&:to_s).join(", ")
|
76
|
+
raise ArgumentError, "Invalid block to hash merge strategy "\
|
77
|
+
"`#{merge_strategy}'; valid strategies are #{strategy_list}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/hashblock/version.rb
CHANGED
data/test/parser_test.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ParserTest < Test::Unit::TestCase
|
4
|
+
include Hashblock
|
5
|
+
|
6
|
+
context "Parser.new" do
|
7
|
+
should "not exception when passed zero arguments" do
|
8
|
+
assert_nothing_raised do
|
9
|
+
Parser.new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
should "not exception when passed one hash argument" do
|
13
|
+
assert_nothing_raised do
|
14
|
+
Parser.new {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "Parser#parse" do
|
20
|
+
setup do
|
21
|
+
@parser = Parser.new
|
22
|
+
|
23
|
+
@as_hash = { :foo => :bar, :ping => { :pong => [:abra, :cadabra]} }
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when passed a Hash" do
|
27
|
+
setup do
|
28
|
+
@returns = @parser.parse(@as_hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
should "return a deep copy of original hash" do
|
32
|
+
assert_instance_of Hash, @returns
|
33
|
+
assert_equal @as_hash, @returns
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when passed a block" do
|
38
|
+
setup do
|
39
|
+
@as_block = Proc.new do
|
40
|
+
foo :bar
|
41
|
+
ping do
|
42
|
+
self.pong = [:abra, :cadabra]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@returns = @parser.parse(@as_block)
|
46
|
+
end
|
47
|
+
|
48
|
+
should "return a deep copy of original hash" do
|
49
|
+
assert_instance_of Hash, @returns
|
50
|
+
assert_equal @as_hash, @returns
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when passed a block with collisions" do
|
55
|
+
setup do
|
56
|
+
@as_block = Proc.new do
|
57
|
+
inner do
|
58
|
+
foo :bar
|
59
|
+
foo [:biz]
|
60
|
+
foo :baz
|
61
|
+
end
|
62
|
+
self.foo = :bar
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
should "raise when merge strategy is :exception" do
|
67
|
+
assert_raises BlockEvaluator::DuplicateProperty do
|
68
|
+
@parser.parse(@as_block, :exception)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
should "select :bar when merge strategy is :first_wins" do
|
72
|
+
assert_equal({
|
73
|
+
:inner => {
|
74
|
+
:foo => :bar
|
75
|
+
},
|
76
|
+
:foo => :bar
|
77
|
+
}, @parser.parse(@as_block, :first_wins))
|
78
|
+
end
|
79
|
+
should "select :baz when merge strategy is :last_wins" do
|
80
|
+
assert_equal({
|
81
|
+
:inner => {
|
82
|
+
:foo => :baz
|
83
|
+
},
|
84
|
+
:foo => :bar
|
85
|
+
}, @parser.parse(@as_block, :last_wins))
|
86
|
+
end
|
87
|
+
should "create an array when merge strategy is :array" do
|
88
|
+
assert_equal({
|
89
|
+
:inner => {
|
90
|
+
:foo => [:bar, [:biz], :baz]
|
91
|
+
},
|
92
|
+
:foo => :bar
|
93
|
+
}, @parser.parse(@as_block, :array))
|
94
|
+
end
|
95
|
+
|
96
|
+
context "whose first value is an array" do
|
97
|
+
setup do
|
98
|
+
@as_block = Proc.new do
|
99
|
+
inner do
|
100
|
+
foo [:bar]
|
101
|
+
foo :baz
|
102
|
+
end
|
103
|
+
foo :bar
|
104
|
+
end
|
105
|
+
end
|
106
|
+
should "preserve the array when merging into :array" do
|
107
|
+
assert_equal({
|
108
|
+
:inner => { :foo => [[:bar], :baz] },
|
109
|
+
:foo => :bar
|
110
|
+
}, @parser.parse(@as_block, :array))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "when passed a block with accessors on the single argument" do
|
116
|
+
setup do
|
117
|
+
@as_block = Proc.new do |o|
|
118
|
+
o.inner do |x|
|
119
|
+
x.foo = :bar
|
120
|
+
o.ping = :pong
|
121
|
+
end
|
122
|
+
abra :cadabra
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
should "injects the properties correctly" do
|
127
|
+
assert_equal({
|
128
|
+
:inner => { :foo => :bar },
|
129
|
+
:ping => :pong,
|
130
|
+
:abra => :cadabra
|
131
|
+
}, @parser.parse(@as_block))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: hashblock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0
|
5
|
+
version: 0.1.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Nathan Ladd
|
@@ -13,7 +13,7 @@ cert_chain: []
|
|
13
13
|
date: 2011-07-26 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
16
|
+
name: bluecloth
|
17
17
|
prerelease: false
|
18
18
|
requirement: &id001 !ruby/object:Gem::Requirement
|
19
19
|
none: false
|
@@ -23,7 +23,18 @@ dependencies:
|
|
23
23
|
version: "0"
|
24
24
|
type: :development
|
25
25
|
version_requirements: *id001
|
26
|
-
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: shoulda
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "0"
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id002
|
37
|
+
description: Sometimes you'll want to allow the user to configure a plugin or some other object via a ruby block where options are specified through method calls. Hashblock is a tool which converts either a hash or a block into a plain old ruby object that is simple to access and modify. Hashblock supports deep nesting and collision handling (since a block can contain multiple invokations of the same method)
|
27
38
|
email:
|
28
39
|
- nathanladd@gmail.com
|
29
40
|
executables: []
|
@@ -35,10 +46,15 @@ extra_rdoc_files: []
|
|
35
46
|
files:
|
36
47
|
- .gitignore
|
37
48
|
- Gemfile
|
49
|
+
- README
|
38
50
|
- Rakefile
|
39
51
|
- hashblock.gemspec
|
40
52
|
- lib/hashblock.rb
|
53
|
+
- lib/hashblock/block_evaluator.rb
|
54
|
+
- lib/hashblock/parser.rb
|
41
55
|
- lib/hashblock/version.rb
|
56
|
+
- test/parser_test.rb
|
57
|
+
- test/test_helper.rb
|
42
58
|
homepage: http://github.com/ntl
|
43
59
|
licenses: []
|
44
60
|
|
@@ -66,5 +82,6 @@ rubygems_version: 1.8.6
|
|
66
82
|
signing_key:
|
67
83
|
specification_version: 3
|
68
84
|
summary: Hashblock converts ruby hashes and blocks to friendly config objects
|
69
|
-
test_files:
|
70
|
-
|
85
|
+
test_files:
|
86
|
+
- test/parser_test.rb
|
87
|
+
- test/test_helper.rb
|