hashblock 0.0.1 → 0.1.0
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 +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
|