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 CHANGED
@@ -2,3 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ README.html
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
@@ -11,6 +11,10 @@ task :console do
11
11
  IRB.start
12
12
  end
13
13
 
14
+ task :markdown do
15
+ exec "bluecloth README > README.html"
16
+ end
17
+
14
18
  Rake::TestTask.new do |t|
15
19
  t.libs << "test"
16
20
  t.test_files = FileList['test/*_test.rb']
@@ -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 even schema definitions to take the pain out of sanity checking your input.}
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
@@ -1,3 +1,3 @@
1
1
  module Hashblock
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -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
@@ -0,0 +1,4 @@
1
+ require 'shoulda'
2
+ require 'pp'
3
+
4
+ require 'hashblock'
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: hashblock
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.1
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: shoulda
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
- 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 even schema definitions to take the pain out of sanity checking your input.
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