nori 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of nori might be problematic. Click here for more details.
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +36 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +21 -0
- data/Rakefile +16 -0
- data/benchmark/benchmark.rb +19 -0
- data/benchmark/soap_response.xml +266 -0
- data/lib/nori.rb +18 -0
- data/lib/nori/core_ext.rb +3 -0
- data/lib/nori/core_ext/hash.rb +70 -0
- data/lib/nori/core_ext/object.rb +13 -0
- data/lib/nori/core_ext/string.rb +15 -0
- data/lib/nori/parser.rb +51 -0
- data/lib/nori/parser/nokogiri.rb +46 -0
- data/lib/nori/parser/rexml.rb +39 -0
- data/lib/nori/version.rb +5 -0
- data/lib/nori/xml_utility_node.rb +186 -0
- data/nori.gemspec +24 -0
- data/spec/nori/core_ext/hash_spec.rb +60 -0
- data/spec/nori/core_ext/object_spec.rb +15 -0
- data/spec/nori/core_ext/string_spec.rb +33 -0
- data/spec/nori/nori_spec.rb +506 -0
- data/spec/nori/parser_spec.rb +37 -0
- data/spec/spec_helper.rb +2 -0
- metadata +145 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module Nori
|
2
|
+
module CoreExt
|
3
|
+
module String
|
4
|
+
|
5
|
+
def snake_case
|
6
|
+
return self.downcase if self =~ /^[A-Z]+$/
|
7
|
+
self.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
|
8
|
+
$+.downcase
|
9
|
+
end unless method_defined?(:snake_case)
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
String.send :include, Nori::CoreExt::String
|
data/lib/nori/parser.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Nori
|
2
|
+
|
3
|
+
# = Nori::Parser
|
4
|
+
#
|
5
|
+
# Manages the parser classes. Currently supports:
|
6
|
+
#
|
7
|
+
# * REXML
|
8
|
+
# * Nokogiri
|
9
|
+
module Parser
|
10
|
+
|
11
|
+
# The default parser.
|
12
|
+
DEFAULT = :rexml
|
13
|
+
|
14
|
+
# List of available parsers.
|
15
|
+
PARSERS = { :rexml => "REXML", :nokogiri => "Nokogiri" }
|
16
|
+
|
17
|
+
# Returns the parser to use. Defaults to <tt>Nori::Parser::REXML</tt>.
|
18
|
+
def self.use
|
19
|
+
@use ||= DEFAULT
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets the +parser+ to use. Raises an +ArgumentError+ unless the +parser+ exists.
|
23
|
+
def self.use=(parser)
|
24
|
+
validate_parser! parser
|
25
|
+
@use = parser
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the parsed +xml+ using the parser to use. Raises an +ArgumentError+
|
29
|
+
# unless the optional or default +parser+ exists.
|
30
|
+
def self.parse(xml, parser = nil)
|
31
|
+
load_parser(parser).parse xml
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Raises an +ArgumentError+ unless the +parser+ exists.
|
37
|
+
def self.validate_parser!(parser)
|
38
|
+
raise ArgumentError, "Invalid Nori parser: #{parser}" unless PARSERS[parser]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Requires and returns the +parser+ to use.
|
42
|
+
def self.load_parser(parser)
|
43
|
+
parser ||= use
|
44
|
+
validate_parser! parser
|
45
|
+
|
46
|
+
require "nori/parser/#{parser}"
|
47
|
+
const_get PARSERS[parser]
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require "nori/xml_utility_node"
|
3
|
+
|
4
|
+
module Nori
|
5
|
+
module Parser
|
6
|
+
|
7
|
+
# = Nori::Parser::Nokogiri
|
8
|
+
#
|
9
|
+
# Nokogiri SAX parser.
|
10
|
+
module Nokogiri
|
11
|
+
|
12
|
+
class Document < ::Nokogiri::XML::SAX::Document
|
13
|
+
|
14
|
+
def stack
|
15
|
+
@stack ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def start_element(name, attrs = [])
|
19
|
+
stack.push Nori::XMLUtilityNode.new(name, Hash[*attrs.flatten])
|
20
|
+
end
|
21
|
+
|
22
|
+
def end_element(name)
|
23
|
+
if stack.size > 1
|
24
|
+
last = stack.pop
|
25
|
+
stack.last.add_node last
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def characters(string)
|
30
|
+
stack.last.add_node(string) unless string.strip.length == 0 || stack.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
alias cdata_block characters
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.parse(xml)
|
38
|
+
document = Document.new
|
39
|
+
parser = ::Nokogiri::XML::SAX::Parser.new document
|
40
|
+
parser.parse xml
|
41
|
+
document.stack.length > 0 ? document.stack.pop.to_hash : {}
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "rexml/parsers/baseparser"
|
2
|
+
require "nori/xml_utility_node"
|
3
|
+
|
4
|
+
module Nori
|
5
|
+
module Parser
|
6
|
+
|
7
|
+
# = Nori::Parser::REXML
|
8
|
+
#
|
9
|
+
# REXML pull parser.
|
10
|
+
module REXML
|
11
|
+
|
12
|
+
def self.parse(xml)
|
13
|
+
stack = []
|
14
|
+
parser = ::REXML::Parsers::BaseParser.new(xml)
|
15
|
+
|
16
|
+
while true
|
17
|
+
event = parser.pull
|
18
|
+
case event[0]
|
19
|
+
when :end_document
|
20
|
+
break
|
21
|
+
when :end_doctype, :start_doctype
|
22
|
+
# do nothing
|
23
|
+
when :start_element
|
24
|
+
stack.push Nori::XMLUtilityNode.new(event[1], event[2])
|
25
|
+
when :end_element
|
26
|
+
if stack.size > 1
|
27
|
+
temp = stack.pop
|
28
|
+
stack.last.add_node(temp)
|
29
|
+
end
|
30
|
+
when :text, :cdata
|
31
|
+
stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
stack.length > 0 ? stack.pop.to_hash : {}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/nori/version.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
require "rexml/text"
|
2
|
+
require "date"
|
3
|
+
require "time"
|
4
|
+
require "yaml"
|
5
|
+
require "bigdecimal"
|
6
|
+
|
7
|
+
module Nori
|
8
|
+
|
9
|
+
# This is a slighly modified version of the XMLUtilityNode from
|
10
|
+
# http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
|
11
|
+
#
|
12
|
+
# John Nunemaker:
|
13
|
+
# It's mainly just adding vowels, as I ht cd wth n vwls :)
|
14
|
+
# This represents the hard part of the work, all I did was change the
|
15
|
+
# underlying parser.
|
16
|
+
class XMLUtilityNode
|
17
|
+
|
18
|
+
def self.typecasts
|
19
|
+
@@typecasts
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.typecasts=(obj)
|
23
|
+
@@typecasts = obj
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.available_typecasts
|
27
|
+
@@available_typecasts
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.available_typecasts=(obj)
|
31
|
+
@@available_typecasts = obj
|
32
|
+
end
|
33
|
+
|
34
|
+
self.typecasts = {}
|
35
|
+
self.typecasts["integer"] = lambda { |v| v.nil? ? nil : v.to_i }
|
36
|
+
self.typecasts["boolean"] = lambda { |v| v.nil? ? nil : (v.strip != "false") }
|
37
|
+
self.typecasts["datetime"] = lambda { |v| v.nil? ? nil : Time.parse(v).utc }
|
38
|
+
self.typecasts["date"] = lambda { |v| v.nil? ? nil : Date.parse(v) }
|
39
|
+
self.typecasts["dateTime"] = lambda { |v| v.nil? ? nil : Time.parse(v).utc }
|
40
|
+
self.typecasts["decimal"] = lambda { |v| v.nil? ? nil : BigDecimal(v.to_s) }
|
41
|
+
self.typecasts["double"] = lambda { |v| v.nil? ? nil : v.to_f }
|
42
|
+
self.typecasts["float"] = lambda { |v| v.nil? ? nil : v.to_f }
|
43
|
+
self.typecasts["symbol"] = lambda { |v| v.nil? ? nil : v.to_sym }
|
44
|
+
self.typecasts["string"] = lambda { |v| v.to_s }
|
45
|
+
self.typecasts["yaml"] = lambda { |v| v.nil? ? nil : YAML.load(v) }
|
46
|
+
self.typecasts["base64Binary"] = lambda { |v| v.unpack('m').first }
|
47
|
+
|
48
|
+
self.available_typecasts = self.typecasts.keys
|
49
|
+
|
50
|
+
def initialize(name, normalized_attributes = {})
|
51
|
+
# unnormalize attribute values
|
52
|
+
attributes = Hash[* normalized_attributes.map do |key, value|
|
53
|
+
[ key, unnormalize_xml_entities(value) ]
|
54
|
+
end.flatten]
|
55
|
+
|
56
|
+
@name = name.tr("-", "_")
|
57
|
+
# leave the type alone if we don't know what it is
|
58
|
+
@type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
|
59
|
+
|
60
|
+
@nil_element = attributes.delete("nil") == "true"
|
61
|
+
@attributes = undasherize_keys(attributes)
|
62
|
+
@children = []
|
63
|
+
@text = false
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_accessor :name, :attributes, :children, :type
|
67
|
+
|
68
|
+
def add_node(node)
|
69
|
+
@text = true if node.is_a? String
|
70
|
+
@children << node
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_hash
|
74
|
+
if @type == "file"
|
75
|
+
f = StringIO.new((@children.first || '').unpack('m').first)
|
76
|
+
class << f
|
77
|
+
attr_accessor :original_filename, :content_type
|
78
|
+
end
|
79
|
+
f.original_filename = attributes['name'] || 'untitled'
|
80
|
+
f.content_type = attributes['content_type'] || 'application/octet-stream'
|
81
|
+
return {name => f}
|
82
|
+
end
|
83
|
+
|
84
|
+
if @text
|
85
|
+
t = typecast_value( unnormalize_xml_entities( inner_html ) )
|
86
|
+
t.class.send(:attr_accessor, :attributes)
|
87
|
+
t.attributes = attributes
|
88
|
+
return { name => t }
|
89
|
+
else
|
90
|
+
#change repeating groups into an array
|
91
|
+
groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
|
92
|
+
|
93
|
+
out = nil
|
94
|
+
if @type == "array"
|
95
|
+
out = []
|
96
|
+
groups.each do |k, v|
|
97
|
+
if v.size == 1
|
98
|
+
out << v.first.to_hash.entries.first.last
|
99
|
+
else
|
100
|
+
out << v.map{|e| e.to_hash[k]}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
out = out.flatten
|
104
|
+
|
105
|
+
else # If Hash
|
106
|
+
out = {}
|
107
|
+
groups.each do |k,v|
|
108
|
+
if v.size == 1
|
109
|
+
out.merge!(v.first)
|
110
|
+
else
|
111
|
+
out.merge!( k => v.map{|e| e.to_hash[k]})
|
112
|
+
end
|
113
|
+
end
|
114
|
+
out.merge! attributes unless attributes.empty?
|
115
|
+
out = out.empty? ? nil : out
|
116
|
+
end
|
117
|
+
|
118
|
+
if @type && out.nil?
|
119
|
+
{ name => typecast_value(out) }
|
120
|
+
else
|
121
|
+
{ name => out }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Typecasts a value based upon its type. For instance, if
|
127
|
+
# +node+ has #type == "integer",
|
128
|
+
# {{[node.typecast_value("12") #=> 12]}}
|
129
|
+
#
|
130
|
+
# @param value<String> The value that is being typecast.
|
131
|
+
#
|
132
|
+
# @details [:type options]
|
133
|
+
# "integer"::
|
134
|
+
# converts +value+ to an integer with #to_i
|
135
|
+
# "boolean"::
|
136
|
+
# checks whether +value+, after removing spaces, is the literal
|
137
|
+
# "true"
|
138
|
+
# "datetime"::
|
139
|
+
# Parses +value+ using Time.parse, and returns a UTC Time
|
140
|
+
# "date"::
|
141
|
+
# Parses +value+ using Date.parse
|
142
|
+
#
|
143
|
+
# @return <Integer, TrueClass, FalseClass, Time, Date, Object>
|
144
|
+
# The result of typecasting +value+.
|
145
|
+
#
|
146
|
+
# @note
|
147
|
+
# If +self+ does not have a "type" key, or if it's not one of the
|
148
|
+
# options specified above, the raw +value+ will be returned.
|
149
|
+
def typecast_value(value)
|
150
|
+
return value unless @type
|
151
|
+
proc = self.class.typecasts[@type]
|
152
|
+
proc.nil? ? value : proc.call(value)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Take keys of the form foo-bar and convert them to foo_bar
|
156
|
+
def undasherize_keys(params)
|
157
|
+
params.keys.each do |key, value|
|
158
|
+
params[key.tr("-", "_")] = params.delete(key)
|
159
|
+
end
|
160
|
+
params
|
161
|
+
end
|
162
|
+
|
163
|
+
# Get the inner_html of the REXML node.
|
164
|
+
def inner_html
|
165
|
+
@children.join
|
166
|
+
end
|
167
|
+
|
168
|
+
# Converts the node into a readable HTML node.
|
169
|
+
#
|
170
|
+
# @return <String> The HTML node in text form.
|
171
|
+
def to_html
|
172
|
+
attributes.merge!(:type => @type ) if @type
|
173
|
+
"<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
|
174
|
+
end
|
175
|
+
|
176
|
+
alias to_s to_html
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
# TODO: replace REXML
|
181
|
+
def unnormalize_xml_entities value
|
182
|
+
REXML::Text.unnormalize(value)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
data/nori.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "nori/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "nori"
|
7
|
+
s.version = Nori::VERSION
|
8
|
+
s.authors = ["Daniel Harrington", "John Nunemaker", "Wynn Netherland"]
|
9
|
+
s.email = "me@rubiii.com"
|
10
|
+
s.homepage = "http://github.com/rubiii/nori"
|
11
|
+
s.summary = "XML to Hash translator"
|
12
|
+
s.description = s.summary
|
13
|
+
|
14
|
+
s.rubyforge_project = "nori"
|
15
|
+
|
16
|
+
s.add_development_dependency "nokogiri", ">= 1.4.0"
|
17
|
+
s.add_development_dependency "rspec", "~> 2.4.0"
|
18
|
+
s.add_development_dependency "autotest"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Hash do
|
4
|
+
|
5
|
+
describe "#to_params" do
|
6
|
+
|
7
|
+
{
|
8
|
+
{ "foo" => "bar", "baz" => "bat" } => "foo=bar&baz=bat",
|
9
|
+
{ "foo" => [ "bar", "baz" ] } => "foo[]=bar&foo[]=baz",
|
10
|
+
{ "foo" => [ {"bar" => "1"}, {"bar" => 2} ] } => "foo[][bar]=1&foo[][bar]=2",
|
11
|
+
{ "foo" => { "bar" => [ {"baz" => 1}, {"baz" => "2"} ] } } => "foo[bar][][baz]=1&foo[bar][][baz]=2",
|
12
|
+
{ "foo" => {"1" => "bar", "2" => "baz"} } => "foo[1]=bar&foo[2]=baz"
|
13
|
+
}.each do |hash, params|
|
14
|
+
it "should covert hash: #{hash.inspect} to params: #{params.inspect}" do
|
15
|
+
hash.to_params.split('&').sort.should == params.split('&').sort
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should not leave a trailing &" do
|
20
|
+
{
|
21
|
+
:name => 'Bob',
|
22
|
+
:address => {
|
23
|
+
:street => '111 Ruby Ave.',
|
24
|
+
:city => 'Ruby Central',
|
25
|
+
:phones => ['111-111-1111', '222-222-2222']
|
26
|
+
}
|
27
|
+
}.to_params.should_not =~ /&$/
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should URL encode unsafe characters" do
|
31
|
+
{:q => "?&\" +"}.to_params.should == "q=%3F%26%22%20%2B"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#normalize_param" do
|
36
|
+
it "should have specs"
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#to_xml_attributes" do
|
40
|
+
|
41
|
+
it "should turn the hash into xml attributes" do
|
42
|
+
attrs = { :one => "ONE", "two" => "TWO" }.to_xml_attributes
|
43
|
+
attrs.should =~ /one="ONE"/m
|
44
|
+
attrs.should =~ /two="TWO"/m
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should preserve _ in hash keys" do
|
48
|
+
attrs = {
|
49
|
+
:some_long_attribute => "with short value",
|
50
|
+
:crash => :burn,
|
51
|
+
:merb => "uses extlib"
|
52
|
+
}.to_xml_attributes
|
53
|
+
|
54
|
+
attrs.should =~ /some_long_attribute="with short value"/
|
55
|
+
attrs.should =~ /merb="uses extlib"/
|
56
|
+
attrs.should =~ /crash="burn"/
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|