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.

@@ -0,0 +1,13 @@
1
+ module Nori
2
+ module CoreExt
3
+ module Object
4
+
5
+ def blank?
6
+ respond_to?(:empty?) ? empty? : !self
7
+ end unless method_defined?(:blank?)
8
+
9
+ end
10
+ end
11
+ end
12
+
13
+ Object.send :include, Nori::CoreExt::Object
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ module Nori
2
+
3
+ VERSION = "0.2.0"
4
+
5
+ end
@@ -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
@@ -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