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 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