badgerhash 0.0.1

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/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Badgerhash
2
+ [![Build Status](https://travis-ci.org/gfmurphy/badgerhash.svg?branch=master)](https://travis-ci.org/gfmurphy/badgerhash)
3
+ [![Code Climate](https://codeclimate.com/github/gfmurphy/badgerhash.png)](https://codeclimate.com/github/gfmurphy/badgerhash)
4
+
5
+ Convert XML to a Ruby Hash using the BadgerFish convention: http://badgerfish.ning.com/
6
+
7
+ The resulting Hash can be easily converted to JSON using a JSON library.
8
+
9
+ The reference implementation of the generators use REXML for parsing the given
10
+ XML. REXML is included in the Ruby Standard Library so no depencies are
11
+ introduced. Nevertheless, this library provides interfaces that allow using
12
+ alternate parsers if performance is a concern.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'badgerhash'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install badgerhash
27
+
28
+ ## Usage
29
+
30
+ ### Generating a BadgerFish Hash Using a Stream Parser
31
+
32
+ ```ruby
33
+ require "badgerhash"
34
+ require "stringio"
35
+
36
+ xml = StringIO.new("<alice>bob</alice>")
37
+ xml_stream = Badgerhash::XmlStream.create(xml)
38
+
39
+ puts xml_stream.to_badgerfish.inspect
40
+ ```
41
+
42
+ ## Contributing
43
+
44
+ 1. Fork it ( http://github.com/gfmurphy/badgerhash/fork )
45
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
46
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
47
+ 4. Push to the branch (`git push origin my-new-feature`)
48
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "yard"
3
+
4
+ YARD::Rake::YardocTask.new do |t|
5
+ t.files = ['lib/**/*.rb']
6
+ 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 'badgerhash/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "badgerhash"
8
+ spec.version = Badgerhash::VERSION
9
+ spec.authors = ["George F. Murphy"]
10
+ spec.email = ["gmurphy@epublishing.com"]
11
+ spec.summary = "Parse XML as IO into hash using badgerfish convention."
12
+ spec.homepage = "http://github.com/gfmurphy/badgerhash"
13
+ spec.license = "GNU LGPL"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.required_ruby_version = ">= 1.9.3"
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.5"
23
+ spec.add_development_dependency "rake", "~> 10.1.1"
24
+ end
data/lib/badgerhash.rb ADDED
@@ -0,0 +1,31 @@
1
+ require "badgerhash/handlers/sax_handler"
2
+ require "badgerhash/parsers/rexml"
3
+ require "badgerhash/version"
4
+ require "badgerhash/xml_stream"
5
+
6
+ # Convert XML to Ruby Hash using Badgerfish convention: http://badgerfish.ning.com/
7
+ # @api public
8
+ module Badgerhash
9
+ # Class of the default sax parser implementation
10
+ DEFAULT_SAX_PARSER = Parsers::REXML::SaxDocument
11
+
12
+ # Set the sax parser for the module
13
+ #
14
+ # @param parser [Class] a parser that supports the required interface
15
+ # @see Parsers::REXML::SaxDocument for the reference implementation
16
+ # @example
17
+ # Badgerhash.sax_parser = MyFastParser => MyFastParser
18
+ # @return [Class] the new parser class
19
+ def self.sax_parser=(parser)
20
+ @sax_parser = parser
21
+ end
22
+
23
+ # The current sax parser implementation
24
+ #
25
+ # @example
26
+ # Badgerhash.sax_parser => Parsers::REXML::SaxDocument
27
+ # @return [Class] the current sax parser implementation
28
+ def self.sax_parser
29
+ @sax_parser || DEFAULT_SAX_PARSER
30
+ end
31
+ end
@@ -0,0 +1,95 @@
1
+ module Badgerhash
2
+ module Handlers
3
+ # SaxHandler that is passed to a sax parser implementation
4
+ # @api public
5
+ class SaxHandler
6
+ attr_reader :node
7
+
8
+ # Initialize the SaxHandler
9
+ #
10
+ # @param node [Hash] Used to preinitialzie the internal node.
11
+ # @api private
12
+ def initialize(node={})
13
+ @parents = []
14
+ @node = node
15
+ end
16
+
17
+ # Sends message to handler that a new element was encountered in
18
+ # stream
19
+ #
20
+ # @param name [String] the name of the new element
21
+ #
22
+ # @example
23
+ # handler.start_element "foo" => handler
24
+ # @return [SaxHandler]
25
+ def start_element(name)
26
+ name = name.to_s
27
+ element = node_namespaces
28
+
29
+ @node[name] = case @node[name]
30
+ when nil
31
+ element
32
+ when Hash
33
+ [@node[name], element]
34
+ else
35
+ @node[name] << element
36
+ end
37
+
38
+ @parents << @node
39
+ @node = element
40
+ self
41
+ end
42
+
43
+ # Sends message to handler that an attribute was encountered in
44
+ # the stream
45
+ #
46
+ # @param name [String] the name of the attribute
47
+ # @param value [String] the attribute's value
48
+ # @example
49
+ # handler.attr "foo", "bar" => handler
50
+ # @return [SaxHandler]
51
+ def attr(name, value)
52
+ if name =~ /^xmlns(:?(.*))/i
53
+ key = $2.to_s.length > 0 ? $2 : "$"
54
+ (@node["@xmlns"] ||= {})[key] = value
55
+ else
56
+ @node["@#{name}"] = value
57
+ end
58
+ self
59
+ end
60
+
61
+ # Sends a message to handler that a text node or cdata section
62
+ # was found in the stream
63
+ #
64
+ # @param value [String] the text encountered
65
+ # @example
66
+ # handler.text "this is my text node" => handler
67
+ # @return [SaxHandler]
68
+ def text(value)
69
+ value = value.to_s.strip
70
+ @node["$"] = value unless value.empty?
71
+ self
72
+ end
73
+
74
+ alias :cdata :text
75
+
76
+ # Sends a message to the handler that the end of an element has
77
+ # been found in the stream
78
+ #
79
+ # @param name [String] the name of the element
80
+ # @example
81
+ # handler.end_element "foo" => handler
82
+ # @return [SaxHandler]
83
+ def end_element(name)
84
+ @node = @parents.pop || {}
85
+ self
86
+ end
87
+
88
+ private
89
+ def node_namespaces
90
+ ns = @node.fetch("@xmlns", {})
91
+ ns.size > 0 ? { "@xmlns" => ns } : {}
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,49 @@
1
+ require "forwardable"
2
+ require "rexml/document"
3
+ require "rexml/streamlistener"
4
+
5
+ module Badgerhash
6
+ module Parsers
7
+ module REXML
8
+ # SaxDocument provides an implmentation of the required
9
+ # public interface for a parser that is to be used when parsing
10
+ # an XmlStream. An implementation is required to provide a parse method
11
+ # that accepts a Badgerhash::Handler::SaxHandler and an IO object.
12
+ # When parsing the given IO stream, it must send messages to the handler
13
+ # in order to build the correct Hash. This also serves as a reference
14
+ # implementation for other sax parsers.
15
+ #
16
+ # @api private
17
+ class SaxDocument
18
+ include ::REXML::StreamListener
19
+ extend Forwardable
20
+
21
+ def_delegators :@handler, :text, :cdata
22
+ def_delegator :@handler, :end_element, :tag_end
23
+
24
+ # Parse the given stream and update the handler.
25
+ #
26
+ # @param handler [Badgerhash::Handler::SaxHandler] parsing handler
27
+ # @param io [IO] the XML to be parsed.
28
+ # @return void
29
+ def self.parse(handler, io)
30
+ ::REXML::Document.parse_stream(io, new(handler))
31
+ end
32
+
33
+ # Initialize the parser.
34
+ #
35
+ # @param handler [Badgerhash::Handlers::SaxHandler]
36
+ def initialize(handler)
37
+ @handler = handler
38
+ end
39
+
40
+ def tag_start(name, attributes=[])
41
+ @handler.start_element name
42
+ Array(attributes).each do |attr|
43
+ @handler.attr(*attr)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ module Badgerhash
2
+ # The String containing the current version of the library.
3
+ # @api public
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,40 @@
1
+ module Badgerhash
2
+ # Converts XML as IO to a Badgerfish Hash using a stream parser
3
+ class XmlStream
4
+ # Create a properly initialized Badgerhash::XmlStream object
5
+ #
6
+ # @param io [IO] an object containing the XML to be parsed
7
+ # @example
8
+ # io = StringIO.new("<alice>bob</alice>")
9
+ # Badgerhash::XmlStream.create(io)
10
+ # @return [Badgerhash::XmlStream] the XmlStream
11
+ # @api public
12
+ def self.create(io)
13
+ new(Handlers::SaxHandler.new, Badgerhash.sax_parser, io)
14
+ end
15
+
16
+ # The Badgerfish representation of the XML
17
+ #
18
+ # @example
19
+ # io = StringIO.new("<alice>bob</alice>")
20
+ # Badgerhash::XmlStream.create(io).to_badgerfish => {"alice" => {"$" => "bob" }}
21
+ # @return [Hash] the Badgerfish hash
22
+ # @api public
23
+ def to_badgerfish
24
+ @parser.parse(@handler, @io)
25
+ @handler.node.dup
26
+ end
27
+
28
+ # Initialize an XmlStream object.
29
+ #
30
+ # @param handler [Badgerhash::Handlers::SaxHandler] the handler
31
+ # @param parser [Class] a parser conforming to the required interface.
32
+ # @param io [IO] object containing the XML to be parsed.
33
+ # @api private
34
+ def initialize(handler, parser, io)
35
+ @handler = handler
36
+ @parser = parser
37
+ @io = io
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,134 @@
1
+ require File.expand_path "../../../spec_helper.rb", __FILE__
2
+
3
+ module Badgerhash
4
+ module Handlers
5
+ describe SaxHandler do
6
+ subject(:handler) { SaxHandler.new }
7
+
8
+ describe "#start_element" do
9
+ it "sets the current node to new hash" do
10
+ handler.start_element "foo"
11
+ expect(handler.node).to eq({})
12
+ end
13
+
14
+ context "when no element present with name" do
15
+ subject(:handler) { SaxHandler.new }
16
+ it "adds the node name and empty hash" do
17
+ handler.start_element("foo").end_element("foo")
18
+ expect(handler.node).to eq({"foo" => {}})
19
+ end
20
+ end
21
+
22
+ context "when one element present with name" do
23
+ subject(:handler) { SaxHandler.new({"foo" => { "$" => "bar" }}) }
24
+ it "add the new node and old node to an Array" do
25
+ handler.start_element("foo").end_element("foo")
26
+ expect(handler.node)
27
+ .to eq({"foo" => [{"\$" => "bar"}, {}]})
28
+ end
29
+ end
30
+
31
+ context "when more than one element present with name" do
32
+ subject(:handler) { SaxHandler.new({ "foo" => [{ "\$" => "bar" },
33
+ { "\$" => "baz" }] }) }
34
+ it "adds the new node to the Array" do
35
+ handler.start_element("foo").end_element("foo")
36
+ expect(handler.node)
37
+ .to eq({"foo" => [{"\$" => "bar"}, {"\$" => "baz"}, {}]})
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "#attr" do
43
+ it "adds attribute as property that begins with '@'" do
44
+ handler.start_element("foo")
45
+ .attr("george", "bar")
46
+ .end_element("foo")
47
+ expect(handler.node).to eq("foo" => {"@george"=> "bar"})
48
+ end
49
+
50
+ context "with parent namespaces" do
51
+ before do
52
+ handler.start_element("foo")
53
+ .text("test")
54
+ .attr("xmlns", "http://foo.com/namespace")
55
+ .attr("xmlns:charlie", "http://foo.com/charlie")
56
+ .start_element("charlie:bar")
57
+ .text("test2")
58
+ .end_element("charlie:bar")
59
+ end
60
+
61
+ it "adds parents namespaces" do
62
+ expect(handler.node["charlie:bar"]["@xmlns"]["$"])
63
+ .to eq("http://foo.com/namespace")
64
+ expect(handler.node["charlie:bar"]["@xmlns"]["charlie"])
65
+ .to eq("http://foo.com/charlie")
66
+ end
67
+ end
68
+
69
+ context "when namespace" do
70
+ it "stores main namespace in @xmlns $ property" do
71
+ handler.start_element("foo")
72
+ .attr("xmlns", "http://foo.com/namespace")
73
+ .end_element("foo")
74
+ expect(handler.node)
75
+ .to eq("foo" => {"@xmlns" => {"$" => "http://foo.com/namespace"}})
76
+ end
77
+
78
+ it "stores additional namespaces in named property" do
79
+ handler.start_element("foo")
80
+ .attr("xmlns:george", "http://foo.com/george")
81
+ .end_element("foo")
82
+ expect(handler.node)
83
+ .to eq("foo" => {"@xmlns" => {"george" => "http://foo.com/george"}})
84
+ end
85
+ end
86
+ end
87
+
88
+ describe "#text" do
89
+ it "sets the $ key in the hash with the text value" do
90
+ handler.text "foo"
91
+ expect(handler.node).to eq({"\$" => "foo"})
92
+ end
93
+
94
+ it "compresses leading whitespace" do
95
+ handler.text "\nfoo"
96
+ expect(handler.node).to eq({"\$" => "foo"})
97
+ end
98
+
99
+ it "compresses trailing whitespace" do
100
+ handler.text "foo\n"
101
+ expect(handler.node).to eq({"\$" => "foo"})
102
+ end
103
+ end
104
+
105
+ describe "#cdata" do
106
+ it "sets the $ key in the hash with the cdata section" do
107
+ handler.cdata "foo"
108
+ expect(handler.node).to eq({"\$" => "foo"})
109
+ end
110
+ end
111
+
112
+ describe "#end_element" do
113
+ context "with parent tag on stack" do
114
+ before do
115
+ handler.start_element "foo"
116
+ end
117
+
118
+ it "closes the current node" do
119
+ handler.end_element "foo"
120
+ expect(handler.node).to eq({"foo" => {}})
121
+ end
122
+ end
123
+
124
+ context "when no parent tag on stack" do
125
+ it "sets node to empty hash" do
126
+ handler.end_element "foo"
127
+ expect(handler.node).to eq({})
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+
@@ -0,0 +1,60 @@
1
+ require File.expand_path "../../../spec_helper.rb", __FILE__
2
+
3
+ module Badgerhash
4
+ module Parsers
5
+ module REXML
6
+ describe SaxDocument do
7
+ let(:handler) { double(:handler) }
8
+ subject(:document) { SaxDocument.new(handler) }
9
+
10
+ describe ".parse" do
11
+ let(:parser) { double(:parser) }
12
+ let(:io) { double(:io) }
13
+
14
+ it "delegates to the REXML sax parser" do
15
+ expect(::REXML::Document).to receive(:parse_stream)
16
+ SaxDocument.parse(handler, io)
17
+ end
18
+ end
19
+
20
+ describe "#tag_end" do
21
+ it "delegates to handler" do
22
+ expect(handler).to receive(:end_element).with("foo")
23
+ document.tag_end("foo")
24
+ end
25
+ end
26
+
27
+ describe "#cdata" do
28
+ it "delegates to handler's text method" do
29
+ expect(handler).to receive(:cdata).with "foo"
30
+ document.cdata "foo"
31
+ end
32
+ end
33
+
34
+ describe "#text" do
35
+ it "delegates to the handler's text method" do
36
+ expect(handler).to receive(:text).with "foo"
37
+ document.text "foo"
38
+ end
39
+ end
40
+
41
+ describe "#tag_start" do
42
+ let(:attributes) { [["one", '1'], ["two", '2']] }
43
+
44
+ before do
45
+ expect(handler).to receive(:start_element).with "foo"
46
+ end
47
+
48
+ it "delegates to handler's start_element_method" do
49
+ document.tag_start "foo"
50
+ end
51
+
52
+ it "delegates attributes to handler's attr method" do
53
+ expect(handler).to receive(:attr).exactly(attributes.size).times
54
+ document.tag_start "foo", attributes
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end