nori 0.2.4 → 1.0.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.

@@ -1,12 +1,33 @@
1
- == 0.2.4 (2011-06-21)
1
+ == 1.0.0 (2011-06-20)
2
2
 
3
- * Fix: Make sure to always load both StringWithAttributes and StringIOFile
4
- to prevent NameError's.
3
+ * Notice: As of v1.0.0, Nori will follow [Semantic Versioning](http://semver.org).
5
4
 
6
- == 0.2.3 (2011-05-26)
5
+ * Feature: Added somewhat advanced typecasting:
7
6
 
8
- * Fix: Use extended core classes StringWithAttributes and StringIOFile instead of
9
- creating singletons to prevent serialization problems.
7
+ What this means:
8
+
9
+ * "true" and "false" are converted to TrueClass and FalseClass
10
+ * Strings matching an xs:time, xs:date and xs:dateTime are converted
11
+ to Time, Date and DateTime objects.
12
+
13
+ You can disable this feature via:
14
+
15
+ Nori.advanced_typecasting = false
16
+
17
+ * Feature: Added an option to strip the namespaces from every tag.
18
+ This feature might raise problems and is therefore disabled by default.
19
+
20
+ Nori.strip_namespaces = true
21
+
22
+ * Feature: Added an option to specify a custom formula to convert tags.
23
+ Here's an example:
24
+
25
+ Nori.configure do |config|
26
+ config.convert_tags_to { |tag| tag.snake_case.to_sym }
27
+ end
28
+
29
+ xml = '<userResponse><accountStatus>active</accountStatus></userResponse>'
30
+ parse(xml).should == { :user_response => { :account_status => "active" }
10
31
 
11
32
  == 0.2.2 (2011-05-16)
12
33
 
data/README.md CHANGED
@@ -19,3 +19,46 @@ Nori.parser = :nokogiri
19
19
 
20
20
  Make sure Nokogiri is in your LOAD_PATH when parsing XML, because Nori tries to load it
21
21
  when it's needed.
22
+
23
+
24
+ Typecasting
25
+ -----------
26
+
27
+ Besides regular typecasting, Nori features somewhat "advanced" typecasting:
28
+
29
+ * "true" and "false" String values are converted to `TrueClass` and `FalseClass`.
30
+ * String values matching xs:time, xs:date and xs:dateTime are converted
31
+ to `Time`, `Date` and `DateTime` objects.
32
+
33
+ You can disable this feature via:
34
+
35
+ ``` ruby
36
+ Nori.advanced_typecasting = false
37
+ ```
38
+
39
+
40
+ Namespaces
41
+ ----------
42
+
43
+ Nori can strip the namespaces from your XML tags. This feature might raise
44
+ problems and is therefore disabled by default. Enable it via:
45
+
46
+ ``` ruby
47
+ Nori.strip_namespaces = true
48
+ ```
49
+
50
+
51
+ XML tags -> Hash keys
52
+ ---------------------
53
+
54
+ Nori lets you specify a custom formula to convert XML tags to Hash keys.
55
+ Let me give you an example:
56
+
57
+ ``` ruby
58
+ Nori.configure do |config|
59
+ config.convert_tags_to { |tag| tag.snake_case.to_sym }
60
+ end
61
+
62
+ xml = '<userResponse><accountStatus>active</accountStatus></userResponse>'
63
+ Nori.parse(xml) # => { :user_response => { :account_status => "active" }
64
+ ```
@@ -1,19 +1,58 @@
1
1
  require "nori/version"
2
2
  require "nori/core_ext"
3
3
  require "nori/parser"
4
- require "nori/xml_utility_node"
5
4
 
6
5
  module Nori
6
+ extend self
7
7
 
8
8
  # Translates the given +xml+ to a Hash. Accepts an optional +parser+ to use.
9
- def self.parse(xml, parser = nil)
9
+ def parse(xml, parser = nil)
10
10
  return {} if xml.blank?
11
11
  Parser.parse xml, parser
12
12
  end
13
13
 
14
14
  # Sets the +parser+ to use.
15
- def self.parser=(parser)
15
+ def parser=(parser)
16
16
  Parser.use = parser
17
17
  end
18
18
 
19
+ # Yields +self+ for configuration.
20
+ def configure
21
+ yield self
22
+ end
23
+
24
+ # Sets whether to use advanced typecasting.
25
+ attr_writer :advanced_typecasting
26
+
27
+ # Returns whether to use advanced typecasting.
28
+ # Defaults to +true+.
29
+ def advanced_typecasting?
30
+ @advanced_typecasting != false
31
+ end
32
+
33
+ # Sets whether to strip namespaces.
34
+ attr_writer :strip_namespaces
35
+
36
+ # Returns whether to strip namespaces.
37
+ # Defaults to +false+.
38
+ def strip_namespaces?
39
+ @strip_namespaces
40
+ end
41
+
42
+ # Expects a +block+ which receives a tag to convert.
43
+ # Accepts +nil+ for a reset to the default behavior of not converting tags.
44
+ def convert_tags_to(reset = nil, &block)
45
+ @convert_tag = reset || block
46
+ end
47
+
48
+ # Transforms a given +tag+ using the specified conversion formula.
49
+ def convert_tag(tag)
50
+ @convert_tag.call(tag)
51
+ end
52
+
53
+ # Returns whether to convert tags.
54
+ def convert_tags?
55
+ @convert_tag
56
+ end
57
+
19
58
  end
@@ -59,7 +59,7 @@ module Nori
59
59
  # #=> 'one="1" two="TWO"'
60
60
  def to_xml_attributes
61
61
  map do |k, v|
62
- %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
62
+ %{#{k.to_s.snakecase.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
63
63
  end.join(' ')
64
64
  end
65
65
 
@@ -2,11 +2,17 @@ module Nori
2
2
  module CoreExt
3
3
  module String
4
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)
5
+ # Returns the String in snake_case.
6
+ def snakecase
7
+ str = dup
8
+ str.gsub! /::/, '/'
9
+ str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2'
10
+ str.gsub! /([a-z\d])([A-Z])/, '\1_\2'
11
+ str.tr! ".", "_"
12
+ str.tr! "-", "_"
13
+ str.downcase!
14
+ str
15
+ end unless method_defined?(:snakecase)
10
16
 
11
17
  end
12
18
  end
@@ -1,4 +1,5 @@
1
1
  require "nokogiri"
2
+ require "nori/xml_utility_node"
2
3
 
3
4
  module Nori
4
5
  module Parser
@@ -1,4 +1,5 @@
1
1
  require "rexml/parsers/baseparser"
2
+ require "nori/xml_utility_node"
2
3
 
3
4
  module Nori
4
5
  module Parser
@@ -1,5 +1,5 @@
1
1
  module Nori
2
2
 
3
- VERSION = "0.2.4"
3
+ VERSION = "1.0.0"
4
4
 
5
5
  end
@@ -18,6 +18,15 @@ module Nori
18
18
  # underlying parser.
19
19
  class XMLUtilityNode
20
20
 
21
+ # Simple xs:time Regexp.
22
+ XS_TIME = /\d{2}:\d{2}:\d{2}/
23
+
24
+ # Simple xs:date Regexp.
25
+ XS_DATE = /\d{4}-\d{2}-\d{2}/
26
+
27
+ # Simple xs:dateTime Regexp.
28
+ XS_DATE_TIME = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
29
+
21
30
  def self.typecasts
22
31
  @@typecasts
23
32
  end
@@ -56,20 +65,23 @@ module Nori
56
65
  [ key, unnormalize_xml_entities(value) ]
57
66
  end.flatten]
58
67
 
59
- @name = name.tr("-", "_")
68
+ @name = name.tr("-", "_")
69
+ @name = @name.split(":").last if Nori.strip_namespaces?
70
+ @name = Nori.convert_tag(@name) if Nori.convert_tags?
71
+
60
72
  # leave the type alone if we don't know what it is
61
- @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
73
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
62
74
 
63
- @nil_element = false
75
+ @nil_element = false
64
76
  attributes.keys.each do |key|
65
77
  if result = /^((.*):)?nil$/.match(key)
66
78
  @nil_element = attributes.delete(key) == "true"
67
79
  attributes.delete("xmlns:#{result[2]}") if result[1]
68
80
  end
69
81
  end
70
- @attributes = undasherize_keys(attributes)
71
- @children = []
72
- @text = false
82
+ @attributes = undasherize_keys(attributes)
83
+ @children = []
84
+ @text = false
73
85
  end
74
86
 
75
87
  attr_accessor :name, :attributes, :children, :type
@@ -96,10 +108,13 @@ module Nori
96
108
 
97
109
  if @text
98
110
  t = typecast_value unnormalize_xml_entities(inner_html)
111
+ t = advanced_typecasting(t) if t.is_a?(String) && Nori.advanced_typecasting?
112
+
99
113
  if t.is_a?(String)
100
114
  t = StringWithAttributes.new(t)
101
115
  t.attributes = attributes
102
116
  end
117
+
103
118
  return { name => t }
104
119
  else
105
120
  #change repeating groups into an array
@@ -167,6 +182,20 @@ module Nori
167
182
  proc.nil? ? value : proc.call(value)
168
183
  end
169
184
 
185
+ def advanced_typecasting(value)
186
+ split = value.split
187
+ return value if split.size > 1
188
+
189
+ case split.first
190
+ when "true" then true
191
+ when "false" then false
192
+ when XS_DATE_TIME then DateTime.parse(value)
193
+ when XS_DATE then Date.parse(value)
194
+ when XS_TIME then Time.parse(value)
195
+ else value
196
+ end
197
+ end
198
+
170
199
  # Take keys of the form foo-bar and convert them to foo_bar
171
200
  def undasherize_keys(params)
172
201
  params.keys.each do |key, value|
@@ -2,31 +2,31 @@ require "spec_helper"
2
2
 
3
3
  describe String do
4
4
 
5
- describe "#snake_case" do
5
+ describe "#snakecase" do
6
6
  it "lowercases one word CamelCase" do
7
- "Merb".snake_case.should == "merb"
7
+ "Merb".snakecase.should == "merb"
8
8
  end
9
9
 
10
- it "makes one underscore snake_case two word CamelCase" do
11
- "MerbCore".snake_case.should == "merb_core"
10
+ it "makes one underscore snakecase two word CamelCase" do
11
+ "MerbCore".snakecase.should == "merb_core"
12
12
  end
13
13
 
14
14
  it "handles CamelCase with more than 2 words" do
15
- "SoYouWantContributeToMerbCore".snake_case.should == "so_you_want_contribute_to_merb_core"
15
+ "SoYouWantContributeToMerbCore".snakecase.should == "so_you_want_contribute_to_merb_core"
16
16
  end
17
17
 
18
18
  it "handles CamelCase with more than 2 capital letter in a row" do
19
- "CNN".snake_case.should == "cnn"
20
- "CNNNews".snake_case.should == "cnn_news"
21
- "HeadlineCNNNews".snake_case.should == "headline_cnn_news"
19
+ "CNN".snakecase.should == "cnn"
20
+ "CNNNews".snakecase.should == "cnn_news"
21
+ "HeadlineCNNNews".snakecase.should == "headline_cnn_news"
22
22
  end
23
23
 
24
24
  it "does NOT change one word lowercase" do
25
- "merb".snake_case.should == "merb"
25
+ "merb".snakecase.should == "merb"
26
26
  end
27
27
 
28
28
  it "leaves snake_case as is" do
29
- "merb_core".snake_case.should == "merb_core"
29
+ "merb_core".snakecase.should == "merb_core"
30
30
  end
31
31
  end
32
32
 
@@ -78,12 +78,82 @@ describe Nori do
78
78
  it "should prefix attributes with an @-sign to avoid problems with overwritten values" do
79
79
  xml =<<-XML
80
80
  <multiRef id="id1">
81
- <approved>true</approved>
81
+ <login>grep</login>
82
82
  <id>76737</id>
83
83
  </multiRef>
84
84
  XML
85
85
 
86
- parse(xml)["multiRef"].should == { "approved" => "true", "@id" => "id1", "id" => "76737" }
86
+ parse(xml)["multiRef"].should == { "login" => "grep", "@id" => "id1", "id" => "76737" }
87
+ end
88
+
89
+ context "without advanced typecasting" do
90
+ around do |example|
91
+ Nori.advanced_typecasting = false
92
+ example.run
93
+ Nori.advanced_typecasting = true
94
+ end
95
+
96
+ it "should not transform 'true'" do
97
+ parse("<value>true</value>")["value"].should == "true"
98
+ end
99
+
100
+ it "should not transform 'false'" do
101
+ parse("<value>false</value>")["value"].should == "false"
102
+ end
103
+
104
+ it "should not transform Strings matching the xs:time format" do
105
+ parse("<value>09:33:55Z</value>")["value"].should == "09:33:55Z"
106
+ end
107
+
108
+ it "should not transform Strings matching the xs:date format" do
109
+ parse("<value>1955-04-18-05:00</value>")["value"].should == "1955-04-18-05:00"
110
+ end
111
+
112
+ it "should not transform Strings matching the xs:dateTime format" do
113
+ parse("<value>1955-04-18T11:22:33-05:00</value>")["value"].should == "1955-04-18T11:22:33-05:00"
114
+ end
115
+ end
116
+
117
+ context "with advanced typecasting" do
118
+ around do |example|
119
+ Nori.advanced_typecasting = true
120
+ example.run
121
+ Nori.advanced_typecasting = false
122
+ end
123
+
124
+ it "should transform 'true' to TrueClass" do
125
+ parse("<value>true</value>")["value"].should == true
126
+ end
127
+
128
+ it "should transform 'false' to FalseClass" do
129
+ parse("<value>false</value>")["value"].should == false
130
+ end
131
+
132
+ it "should transform Strings matching the xs:time format to Time objects" do
133
+ parse("<value>09:33:55Z</value>")["value"].should == Time.parse("09:33:55Z")
134
+ end
135
+
136
+ it "should transform Strings matching the xs:date format to Date objects" do
137
+ parse("<value>1955-04-18-05:00</value>")["value"].should == Date.parse("1955-04-18-05:00")
138
+ end
139
+
140
+ it "should transform Strings matching the xs:dateTime format to DateTime objects" do
141
+ parse("<value>1955-04-18T11:22:33-05:00</value>")["value"].should ==
142
+ DateTime.parse("1955-04-18T11:22:33-05:00")
143
+ end
144
+
145
+ it "should not transform Strings containing an xs:time String and more" do
146
+ parse("<value>09:33:55Z is a time</value>")["value"].should == "09:33:55Z is a time"
147
+ end
148
+
149
+ it "should not transform Strings containing an xs:date String and more" do
150
+ parse("<value>1955-04-18-05:00 is a date</value>")["value"].should == "1955-04-18-05:00 is a date"
151
+ end
152
+
153
+ it "should not transform Strings containing an xs:dateTime String and more" do
154
+ parse("<value>1955-04-18T11:22:33-05:00 is a dateTime</value>")["value"].should ==
155
+ "1955-04-18T11:22:33-05:00 is a dateTime"
156
+ end
87
157
  end
88
158
 
89
159
  context "Parsing xml with text and attributes" do
@@ -191,6 +261,46 @@ describe Nori do
191
261
  parse(xml)['tag_1'].keys.should include('@attr_1')
192
262
  end
193
263
 
264
+ context "with strip_namespaces set to true" do
265
+ around do |example|
266
+ Nori.strip_namespaces = true
267
+ example.run
268
+ Nori.strip_namespaces = false
269
+ end
270
+
271
+ it "should strip the namespace from every tag" do
272
+ xml = '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"></soap:Envelope>'
273
+ parse(xml).should have_key("Envelope")
274
+ end
275
+
276
+ it "converts namespaced entries to array elements" do
277
+ xml = <<-XML
278
+ <history
279
+ xmlns:ns10="http://ns10.example.com"
280
+ xmlns:ns11="http://ns10.example.com">
281
+ <ns10:case><ns10:name>a_name</ns10:name></ns10:case>
282
+ <ns11:case><ns11:name>another_name</ns11:name></ns11:case>
283
+ </history>
284
+ XML
285
+
286
+ expected_case = [{ "name" => "a_name" }, { "name" => "another_name" }]
287
+ parse(xml)["history"]["case"].should == expected_case
288
+ end
289
+ end
290
+
291
+ context "with convert_tags_to set to a custom formula" do
292
+ around do |example|
293
+ Nori.convert_tags_to { |tag| tag.snakecase.to_sym }
294
+ example.run
295
+ Nori.convert_tags_to(nil)
296
+ end
297
+
298
+ it "transforms the tags to snakecase Symbols" do
299
+ xml = '<userResponse><accountStatus>active</accountStatus></userResponse>'
300
+ parse(xml).should == { :user_response => { :account_status => "active" } }
301
+ end
302
+ end
303
+
194
304
  it "should render nested content correctly" do
195
305
  xml = "<root><tag1>Tag1 Content <em><strong>This is strong</strong></em></tag1></root>"
196
306
  parse(xml)['root']['tag1'].should == "Tag1 Content <em><strong>This is strong</strong></em>"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nori
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
+ - 1
7
8
  - 0
8
- - 2
9
- - 4
10
- version: 0.2.4
9
+ - 0
10
+ version: 1.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Daniel Harrington
@@ -17,8 +17,7 @@ autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
19
 
20
- date: 2011-06-21 00:00:00 +02:00
21
- default_executable:
20
+ date: 2011-06-20 00:00:00 Z
22
21
  dependencies:
23
22
  - !ruby/object:Gem::Dependency
24
23
  name: nokogiri
@@ -104,7 +103,6 @@ files:
104
103
  - spec/nori/nori_spec.rb
105
104
  - spec/nori/parser_spec.rb
106
105
  - spec/spec_helper.rb
107
- has_rdoc: true
108
106
  homepage: http://github.com/rubiii/nori
109
107
  licenses: []
110
108
 
@@ -134,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
132
  requirements: []
135
133
 
136
134
  rubyforge_project: nori
137
- rubygems_version: 1.4.1
135
+ rubygems_version: 1.8.5
138
136
  signing_key:
139
137
  specification_version: 3
140
138
  summary: XML to Hash translator