crack 0.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of crack might be problematic. Click here for more details.

data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 John Nunemaker
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ = crack
2
+
3
+ Really simple JSON and XML parsing, ripped from Merb and Rails. The XML parser is ripped from Merb and the JSON parser is ripped from Rails. I take no credit, just packaged them for all to enjoy and easily use.
4
+
5
+ = usage
6
+
7
+ gem 'crack'
8
+ require 'crack' # for xml and json
9
+ require 'crack/json' # for just json
10
+ require 'crack/xml' # for just xml
11
+
12
+ = examples
13
+
14
+ Crack::XML.parse("<tag>This is the contents</tag>")
15
+ # => {'tag' => 'This is the contents'}
16
+
17
+ Crack::JSON.parse('{"tag":"This is the contents"}')
18
+ # => {'tag' => 'This is the contents'}
19
+
20
+ == Copyright
21
+
22
+ Copyright (c) 2009 John Nunemaker. See LICENSE for details.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 1
4
+ :major: 0
@@ -0,0 +1,7 @@
1
+ module Crack
2
+ class ParseError < StandardError; end
3
+ end
4
+
5
+ require 'crack/core_extensions'
6
+ require 'crack/json'
7
+ require 'crack/xml'
@@ -0,0 +1,126 @@
1
+ class Object #:nodoc:
2
+ # @return <TrueClass, FalseClass>
3
+ #
4
+ # @example [].blank? #=> true
5
+ # @example [1].blank? #=> false
6
+ # @example [nil].blank? #=> false
7
+ #
8
+ # Returns true if the object is nil or empty (if applicable)
9
+ def blank?
10
+ nil? || (respond_to?(:empty?) && empty?)
11
+ end unless method_defined?(:blank?)
12
+ end # class Object
13
+
14
+ class Numeric #:nodoc:
15
+ # @return <TrueClass, FalseClass>
16
+ #
17
+ # Numerics can't be blank
18
+ def blank?
19
+ false
20
+ end unless method_defined?(:blank?)
21
+ end # class Numeric
22
+
23
+ class NilClass #:nodoc:
24
+ # @return <TrueClass, FalseClass>
25
+ #
26
+ # Nils are always blank
27
+ def blank?
28
+ true
29
+ end unless method_defined?(:blank?)
30
+ end # class NilClass
31
+
32
+ class TrueClass #:nodoc:
33
+ # @return <TrueClass, FalseClass>
34
+ #
35
+ # True is not blank.
36
+ def blank?
37
+ false
38
+ end unless method_defined?(:blank?)
39
+ end # class TrueClass
40
+
41
+ class FalseClass #:nodoc:
42
+ # False is always blank.
43
+ def blank?
44
+ true
45
+ end unless method_defined?(:blank?)
46
+ end # class FalseClass
47
+
48
+ class String #:nodoc:
49
+ # @example "".blank? #=> true
50
+ # @example " ".blank? #=> true
51
+ # @example " hey ho ".blank? #=> false
52
+ #
53
+ # @return <TrueClass, FalseClass>
54
+ #
55
+ # Strips out whitespace then tests if the string is empty.
56
+ def blank?
57
+ strip.empty?
58
+ end unless method_defined?(:blank?)
59
+
60
+ def snake_case
61
+ return self.downcase if self =~ /^[A-Z]+$/
62
+ self.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
63
+ return $+.downcase
64
+ end unless method_defined?(:snake_case)
65
+ end # class String
66
+
67
+ class Hash #:nodoc:
68
+ # @return <String> This hash as a query string
69
+ #
70
+ # @example
71
+ # { :name => "Bob",
72
+ # :address => {
73
+ # :street => '111 Ruby Ave.',
74
+ # :city => 'Ruby Central',
75
+ # :phones => ['111-111-1111', '222-222-2222']
76
+ # }
77
+ # }.to_params
78
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
79
+ def to_params
80
+ params = self.map { |k,v| normalize_param(k,v) }.join
81
+ params.chop! # trailing &
82
+ params
83
+ end
84
+
85
+ # @param key<Object> The key for the param.
86
+ # @param value<Object> The value for the param.
87
+ #
88
+ # @return <String> This key value pair as a param
89
+ #
90
+ # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
91
+ def normalize_param(key, value)
92
+ param = ''
93
+ stack = []
94
+
95
+ if value.is_a?(Array)
96
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
97
+ elsif value.is_a?(Hash)
98
+ stack << [key,value]
99
+ else
100
+ param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
101
+ end
102
+
103
+ stack.each do |parent, hash|
104
+ hash.each do |key, value|
105
+ if value.is_a?(Hash)
106
+ stack << ["#{parent}[#{key}]", value]
107
+ else
108
+ param << normalize_param("#{parent}[#{key}]", value)
109
+ end
110
+ end
111
+ end
112
+
113
+ param
114
+ end
115
+
116
+ # @return <String> The hash as attributes for an XML tag.
117
+ #
118
+ # @example
119
+ # { :one => 1, "two"=>"TWO" }.to_xml_attributes
120
+ # #=> 'one="1" two="TWO"'
121
+ def to_xml_attributes
122
+ map do |k,v|
123
+ %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
124
+ end.join(' ')
125
+ end
126
+ end
@@ -0,0 +1,66 @@
1
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
2
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5
+
6
+ require 'yaml'
7
+ require 'strscan'
8
+
9
+ module Crack
10
+ class JSON
11
+ def self.parse(json)
12
+ YAML.load(unescape(convert_json_to_yaml(json)))
13
+ rescue ArgumentError => e
14
+ raise ParseError, "Invalid JSON string"
15
+ end
16
+
17
+ protected
18
+ def self.unescape(str)
19
+ str.gsub(/\\u([0-9a-f]{4})/) { [$1.hex].pack("U") }
20
+ end
21
+
22
+ # matches YAML-formatted dates
23
+ DATE_REGEX = /^\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?$/
24
+
25
+ # Ensure that ":" and "," are always followed by a space
26
+ def self.convert_json_to_yaml(json) #:nodoc:
27
+ scanner, quoting, marks, pos, times = StringScanner.new(json), false, [], nil, []
28
+ while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
29
+ case char = scanner[1]
30
+ when '"', "'"
31
+ if !quoting
32
+ quoting = char
33
+ pos = scanner.pos
34
+ elsif quoting == char
35
+ if json[pos..scanner.pos-2] =~ DATE_REGEX
36
+ # found a date, track the exact positions of the quotes so we can remove them later.
37
+ # oh, and increment them for each current mark, each one is an extra padded space that bumps
38
+ # the position in the final YAML output
39
+ total_marks = marks.size
40
+ times << pos+total_marks << scanner.pos+total_marks
41
+ end
42
+ quoting = false
43
+ end
44
+ when ":",","
45
+ marks << scanner.pos - 1 unless quoting
46
+ end
47
+ end
48
+
49
+ if marks.empty?
50
+ json.gsub(/\\\//, '/')
51
+ else
52
+ left_pos = [-1].push(*marks)
53
+ right_pos = marks << json.length
54
+ output = []
55
+ left_pos.each_with_index do |left, i|
56
+ output << json[left.succ..right_pos[i]]
57
+ end
58
+ output = output * " "
59
+
60
+ times.each { |i| output[i-1] = ' ' }
61
+ output.gsub!(/\\\//, '/')
62
+ output
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,211 @@
1
+ require 'rexml/parsers/streamparser'
2
+ require 'rexml/parsers/baseparser'
3
+ require 'rexml/light/node'
4
+ require 'date'
5
+ require 'time'
6
+ require 'yaml'
7
+ require 'bigdecimal'
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
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
12
+ # This represents the hard part of the work, all I did was change the
13
+ # underlying parser.
14
+ class REXMLUtilityNode #:nodoc:
15
+ attr_accessor :name, :attributes, :children, :type
16
+
17
+ def self.typecasts
18
+ @@typecasts
19
+ end
20
+
21
+ def self.typecasts=(obj)
22
+ @@typecasts = obj
23
+ end
24
+
25
+ def self.available_typecasts
26
+ @@available_typecasts
27
+ end
28
+
29
+ def self.available_typecasts=(obj)
30
+ @@available_typecasts = obj
31
+ end
32
+
33
+ self.typecasts = {}
34
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
35
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
36
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
37
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
38
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
39
+ self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)}
40
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
41
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
42
+ self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym}
43
+ self.typecasts["string"] = lambda{|v| v.to_s}
44
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
45
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
46
+
47
+ self.available_typecasts = self.typecasts.keys
48
+
49
+ def initialize(name, attributes = {})
50
+ @name = name.tr("-", "_")
51
+ # leave the type alone if we don't know what it is
52
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
53
+
54
+ @nil_element = attributes.delete("nil") == "true"
55
+ @attributes = undasherize_keys(attributes)
56
+ @children = []
57
+ @text = false
58
+ end
59
+
60
+ def add_node(node)
61
+ @text = true if node.is_a? String
62
+ @children << node
63
+ end
64
+
65
+ def to_hash
66
+ if @type == "file"
67
+ f = StringIO.new((@children.first || '').unpack('m').first)
68
+ class << f
69
+ attr_accessor :original_filename, :content_type
70
+ end
71
+ f.original_filename = attributes['name'] || 'untitled'
72
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
73
+ return {name => f}
74
+ end
75
+
76
+ if @text
77
+ return { name => typecast_value( translate_xml_entities( inner_html ) ) }
78
+ else
79
+ #change repeating groups into an array
80
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
81
+
82
+ out = nil
83
+ if @type == "array"
84
+ out = []
85
+ groups.each do |k, v|
86
+ if v.size == 1
87
+ out << v.first.to_hash.entries.first.last
88
+ else
89
+ out << v.map{|e| e.to_hash[k]}
90
+ end
91
+ end
92
+ out = out.flatten
93
+
94
+ else # If Hash
95
+ out = {}
96
+ groups.each do |k,v|
97
+ if v.size == 1
98
+ out.merge!(v.first)
99
+ else
100
+ out.merge!( k => v.map{|e| e.to_hash[k]})
101
+ end
102
+ end
103
+ out.merge! attributes unless attributes.empty?
104
+ out = out.empty? ? nil : out
105
+ end
106
+
107
+ if @type && out.nil?
108
+ { name => typecast_value(out) }
109
+ else
110
+ { name => out }
111
+ end
112
+ end
113
+ end
114
+
115
+ # Typecasts a value based upon its type. For instance, if
116
+ # +node+ has #type == "integer",
117
+ # {{[node.typecast_value("12") #=> 12]}}
118
+ #
119
+ # @param value<String> The value that is being typecast.
120
+ #
121
+ # @details [:type options]
122
+ # "integer"::
123
+ # converts +value+ to an integer with #to_i
124
+ # "boolean"::
125
+ # checks whether +value+, after removing spaces, is the literal
126
+ # "true"
127
+ # "datetime"::
128
+ # Parses +value+ using Time.parse, and returns a UTC Time
129
+ # "date"::
130
+ # Parses +value+ using Date.parse
131
+ #
132
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
133
+ # The result of typecasting +value+.
134
+ #
135
+ # @note
136
+ # If +self+ does not have a "type" key, or if it's not one of the
137
+ # options specified above, the raw +value+ will be returned.
138
+ def typecast_value(value)
139
+ return value unless @type
140
+ proc = self.class.typecasts[@type]
141
+ proc.nil? ? value : proc.call(value)
142
+ end
143
+
144
+ # Convert basic XML entities into their literal values.
145
+ #
146
+ # @param value<#gsub> An XML fragment.
147
+ #
148
+ # @return <#gsub> The XML fragment after converting entities.
149
+ def translate_xml_entities(value)
150
+ value.gsub(/&lt;/, "<").
151
+ gsub(/&gt;/, ">").
152
+ gsub(/&quot;/, '"').
153
+ gsub(/&apos;/, "'").
154
+ gsub(/&amp;/, "&")
155
+ end
156
+
157
+ # Take keys of the form foo-bar and convert them to foo_bar
158
+ def undasherize_keys(params)
159
+ params.keys.each do |key, value|
160
+ params[key.tr("-", "_")] = params.delete(key)
161
+ end
162
+ params
163
+ end
164
+
165
+ # Get the inner_html of the REXML node.
166
+ def inner_html
167
+ @children.join
168
+ end
169
+
170
+ # Converts the node into a readable HTML node.
171
+ #
172
+ # @return <String> The HTML node in text form.
173
+ def to_html
174
+ attributes.merge!(:type => @type ) if @type
175
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
176
+ end
177
+
178
+ # @alias #to_html #to_s
179
+ def to_s
180
+ to_html
181
+ end
182
+ end
183
+
184
+ module Crack
185
+ class XML
186
+ def self.parse(xml)
187
+ stack = []
188
+ parser = REXML::Parsers::BaseParser.new(xml)
189
+
190
+ while true
191
+ event = parser.pull
192
+ case event[0]
193
+ when :end_document
194
+ break
195
+ when :end_doctype, :start_doctype
196
+ # do nothing
197
+ when :start_element
198
+ stack.push REXMLUtilityNode.new(event[1], event[2])
199
+ when :end_element
200
+ if stack.size > 1
201
+ temp = stack.pop
202
+ stack.last.add_node(temp)
203
+ end
204
+ when :text, :cdata
205
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty?
206
+ end
207
+ end
208
+ stack.length > 0 ? stack.pop.to_hash : {}
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,4 @@
1
+ require 'test_helper'
2
+
3
+ # class CrackTest < Test::Unit::TestCase
4
+ # end
@@ -0,0 +1,42 @@
1
+ require 'test_helper'
2
+
3
+ class JsonTest < Test::Unit::TestCase
4
+ TESTS = {
5
+ %q({"data": "G\u00fcnter"}) => {"data" => "Günter"},
6
+ %q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
7
+ %q({returnTo:{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
8
+ %q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}},
9
+ %q({"returnTo":{"\/categories":1}}) => {"returnTo" => {"/categories" => 1}},
10
+ %({"returnTo":[1,"a"]}) => {"returnTo" => [1, "a"]},
11
+ %({"returnTo":[1,"\\"a\\",", "b"]}) => {"returnTo" => [1, "\"a\",", "b"]},
12
+ %({a: "'", "b": "5,000"}) => {"a" => "'", "b" => "5,000"},
13
+ %({a: "a's, b's and c's", "b": "5,000"}) => {"a" => "a's, b's and c's", "b" => "5,000"},
14
+ %({a: "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
15
+ %({a: "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
16
+ # no time zone
17
+ %({a: "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
18
+ %([]) => [],
19
+ %({}) => {},
20
+ %(1) => 1,
21
+ %("") => "",
22
+ %("\\"") => "\"",
23
+ %(null) => nil,
24
+ %(true) => true,
25
+ %(false) => false,
26
+ %q("http:\/\/test.host\/posts\/1") => "http://test.host/posts/1"
27
+ }
28
+
29
+ TESTS.each do |json, expected|
30
+ should "should decode json (#{json})" do
31
+ lambda {
32
+ Crack::JSON.parse(json).should == expected
33
+ }.should_not raise_error
34
+ end
35
+ end
36
+
37
+ should "should raise error for failed decoding" do
38
+ lambda {
39
+ Crack::JSON.parse(%({: 1}))
40
+ }.should raise_error(Crack::ParseError)
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ gem 'jnunemaker-matchy', '>= 0.4.0'
5
+ require 'matchy'
6
+
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
9
+ require 'crack'
10
+
11
+ class Test::Unit::TestCase
12
+ end
@@ -0,0 +1,451 @@
1
+ require 'test_helper'
2
+
3
+ class XmlTest < Test::Unit::TestCase
4
+ should "should transform a simple tag with content" do
5
+ xml = "<tag>This is the contents</tag>"
6
+ Crack::XML.parse(xml).should == { 'tag' => 'This is the contents' }
7
+ end
8
+
9
+ should "should work with cdata tags" do
10
+ xml = <<-END
11
+ <tag>
12
+ <![CDATA[
13
+ text inside cdata
14
+ ]]>
15
+ </tag>
16
+ END
17
+ Crack::XML.parse(xml)["tag"].strip.should == "text inside cdata"
18
+ end
19
+
20
+ should "should transform a simple tag with attributes" do
21
+ xml = "<tag attr1='1' attr2='2'></tag>"
22
+ hash = { 'tag' => { 'attr1' => '1', 'attr2' => '2' } }
23
+ Crack::XML.parse(xml).should == hash
24
+ end
25
+
26
+ should "should transform repeating siblings into an array" do
27
+ xml =<<-XML
28
+ <opt>
29
+ <user login="grep" fullname="Gary R Epstein" />
30
+ <user login="stty" fullname="Simon T Tyson" />
31
+ </opt>
32
+ XML
33
+
34
+ Crack::XML.parse(xml)['opt']['user'].class.should == Array
35
+
36
+ hash = {
37
+ 'opt' => {
38
+ 'user' => [{
39
+ 'login' => 'grep',
40
+ 'fullname' => 'Gary R Epstein'
41
+ },{
42
+ 'login' => 'stty',
43
+ 'fullname' => 'Simon T Tyson'
44
+ }]
45
+ }
46
+ }
47
+
48
+ Crack::XML.parse(xml).should == hash
49
+ end
50
+
51
+ should "should not transform non-repeating siblings into an array" do
52
+ xml =<<-XML
53
+ <opt>
54
+ <user login="grep" fullname="Gary R Epstein" />
55
+ </opt>
56
+ XML
57
+
58
+ Crack::XML.parse(xml)['opt']['user'].class.should == Hash
59
+
60
+ hash = {
61
+ 'opt' => {
62
+ 'user' => {
63
+ 'login' => 'grep',
64
+ 'fullname' => 'Gary R Epstein'
65
+ }
66
+ }
67
+ }
68
+
69
+ Crack::XML.parse(xml).should == hash
70
+ end
71
+
72
+ should "should typecast an integer" do
73
+ xml = "<tag type='integer'>10</tag>"
74
+ Crack::XML.parse(xml)['tag'].should == 10
75
+ end
76
+
77
+ should "should typecast a true boolean" do
78
+ xml = "<tag type='boolean'>true</tag>"
79
+ Crack::XML.parse(xml)['tag'].should be(true)
80
+ end
81
+
82
+ should "should typecast a false boolean" do
83
+ ["false"].each do |w|
84
+ Crack::XML.parse("<tag type='boolean'>#{w}</tag>")['tag'].should be(false)
85
+ end
86
+ end
87
+
88
+ should "should typecast a datetime" do
89
+ xml = "<tag type='datetime'>2007-12-31 10:32</tag>"
90
+ Crack::XML.parse(xml)['tag'].should == Time.parse( '2007-12-31 10:32' ).utc
91
+ end
92
+
93
+ should "should typecast a date" do
94
+ xml = "<tag type='date'>2007-12-31</tag>"
95
+ Crack::XML.parse(xml)['tag'].should == Date.parse('2007-12-31')
96
+ end
97
+
98
+ should "should unescape html entities" do
99
+ values = {
100
+ "<" => "&lt;",
101
+ ">" => "&gt;",
102
+ '"' => "&quot;",
103
+ "'" => "&apos;",
104
+ "&" => "&amp;"
105
+ }
106
+ values.each do |k,v|
107
+ xml = "<tag>Some content #{v}</tag>"
108
+ Crack::XML.parse(xml)['tag'].should =~ Regexp.new(k)
109
+ end
110
+ end
111
+
112
+ should "should undasherize keys as tags" do
113
+ xml = "<tag-1>Stuff</tag-1>"
114
+ Crack::XML.parse(xml).keys.should include( 'tag_1' )
115
+ end
116
+
117
+ should "should undasherize keys as attributes" do
118
+ xml = "<tag1 attr-1='1'></tag1>"
119
+ Crack::XML.parse(xml)['tag1'].keys.should include( 'attr_1')
120
+ end
121
+
122
+ should "should undasherize keys as tags and attributes" do
123
+ xml = "<tag-1 attr-1='1'></tag-1>"
124
+ Crack::XML.parse(xml).keys.should include( 'tag_1' )
125
+ Crack::XML.parse(xml)['tag_1'].keys.should include( 'attr_1')
126
+ end
127
+
128
+ should "should render nested content correctly" do
129
+ xml = "<root><tag1>Tag1 Content <em><strong>This is strong</strong></em></tag1></root>"
130
+ Crack::XML.parse(xml)['root']['tag1'].should == "Tag1 Content <em><strong>This is strong</strong></em>"
131
+ end
132
+
133
+ should "should render nested content with splshould text nodes correctly" do
134
+ xml = "<root>Tag1 Content<em>Stuff</em> Hi There</root>"
135
+ Crack::XML.parse(xml)['root'].should == "Tag1 Content<em>Stuff</em> Hi There"
136
+ end
137
+
138
+ should "should ignore attributes when a child is a text node" do
139
+ xml = "<root attr1='1'>Stuff</root>"
140
+ Crack::XML.parse(xml).should == { "root" => "Stuff" }
141
+ end
142
+
143
+ should "should ignore attributes when any child is a text node" do
144
+ xml = "<root attr1='1'>Stuff <em>in italics</em></root>"
145
+ Crack::XML.parse(xml).should == { "root" => "Stuff <em>in italics</em>" }
146
+ end
147
+
148
+ should "should correctly transform multiple children" do
149
+ xml = <<-XML
150
+ <user gender='m'>
151
+ <age type='integer'>35</age>
152
+ <name>Home Simpson</name>
153
+ <dob type='date'>1988-01-01</dob>
154
+ <joined-at type='datetime'>2000-04-28 23:01</joined-at>
155
+ <is-cool type='boolean'>true</is-cool>
156
+ </user>
157
+ XML
158
+
159
+ hash = {
160
+ "user" => {
161
+ "gender" => "m",
162
+ "age" => 35,
163
+ "name" => "Home Simpson",
164
+ "dob" => Date.parse('1988-01-01'),
165
+ "joined_at" => Time.parse("2000-04-28 23:01"),
166
+ "is_cool" => true
167
+ }
168
+ }
169
+
170
+ Crack::XML.parse(xml).should == hash
171
+ end
172
+
173
+ should "should properly handle nil values (ActiveSupport Compatible)" do
174
+ topic_xml = <<-EOT
175
+ <topic>
176
+ <title></title>
177
+ <id type="integer"></id>
178
+ <approved type="boolean"></approved>
179
+ <written-on type="date"></written-on>
180
+ <viewed-at type="datetime"></viewed-at>
181
+ <content type="yaml"></content>
182
+ <parent-id></parent-id>
183
+ </topic>
184
+ EOT
185
+
186
+ expected_topic_hash = {
187
+ 'title' => nil,
188
+ 'id' => nil,
189
+ 'approved' => nil,
190
+ 'written_on' => nil,
191
+ 'viewed_at' => nil,
192
+ 'content' => nil,
193
+ 'parent_id' => nil
194
+ }
195
+ Crack::XML.parse(topic_xml)["topic"].should == expected_topic_hash
196
+ end
197
+
198
+ should "should handle a single record from xml (ActiveSupport Compatible)" do
199
+ topic_xml = <<-EOT
200
+ <topic>
201
+ <title>The First Topic</title>
202
+ <author-name>David</author-name>
203
+ <id type="integer">1</id>
204
+ <approved type="boolean"> true </approved>
205
+ <replies-count type="integer">0</replies-count>
206
+ <replies-close-in type="integer">2592000000</replies-close-in>
207
+ <written-on type="date">2003-07-16</written-on>
208
+ <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
209
+ <content type="yaml">--- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n should_have_underscores: true\n</content>
210
+ <author-email-address>david@loudthinking.com</author-email-address>
211
+ <parent-id></parent-id>
212
+ <ad-revenue type="decimal">1.5</ad-revenue>
213
+ <optimum-viewing-angle type="float">135</optimum-viewing-angle>
214
+ <resident type="symbol">yes</resident>
215
+ </topic>
216
+ EOT
217
+
218
+ expected_topic_hash = {
219
+ 'title' => "The First Topic",
220
+ 'author_name' => "David",
221
+ 'id' => 1,
222
+ 'approved' => true,
223
+ 'replies_count' => 0,
224
+ 'replies_close_in' => 2592000000,
225
+ 'written_on' => Date.new(2003, 7, 16),
226
+ 'viewed_at' => Time.utc(2003, 7, 16, 9, 28),
227
+ # Changed this line where the key is :message. The yaml specifies this as a symbol, and who am I to change what you specify
228
+ # The line in ActiveSupport is
229
+ # 'content' => { 'message' => "Have a nice day", 1 => "should be an integer", "array" => [{ "should-have-dashes" => true, "should_have_underscores" => true }] },
230
+ 'content' => { :message => "Have a nice day", 1 => "should be an integer", "array" => [{ "should-have-dashes" => true, "should_have_underscores" => true }] },
231
+ 'author_email_address' => "david@loudthinking.com",
232
+ 'parent_id' => nil,
233
+ 'ad_revenue' => BigDecimal("1.50"),
234
+ 'optimum_viewing_angle' => 135.0,
235
+ 'resident' => :yes
236
+ }
237
+
238
+ Crack::XML.parse(topic_xml)["topic"].each do |k,v|
239
+ v.should == expected_topic_hash[k]
240
+ end
241
+ end
242
+
243
+ should "should handle multiple records (ActiveSupport Compatible)" do
244
+ topics_xml = <<-EOT
245
+ <topics type="array">
246
+ <topic>
247
+ <title>The First Topic</title>
248
+ <author-name>David</author-name>
249
+ <id type="integer">1</id>
250
+ <approved type="boolean">false</approved>
251
+ <replies-count type="integer">0</replies-count>
252
+ <replies-close-in type="integer">2592000000</replies-close-in>
253
+ <written-on type="date">2003-07-16</written-on>
254
+ <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
255
+ <content>Have a nice day</content>
256
+ <author-email-address>david@loudthinking.com</author-email-address>
257
+ <parent-id nil="true"></parent-id>
258
+ </topic>
259
+ <topic>
260
+ <title>The Second Topic</title>
261
+ <author-name>Jason</author-name>
262
+ <id type="integer">1</id>
263
+ <approved type="boolean">false</approved>
264
+ <replies-count type="integer">0</replies-count>
265
+ <replies-close-in type="integer">2592000000</replies-close-in>
266
+ <written-on type="date">2003-07-16</written-on>
267
+ <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
268
+ <content>Have a nice day</content>
269
+ <author-email-address>david@loudthinking.com</author-email-address>
270
+ <parent-id></parent-id>
271
+ </topic>
272
+ </topics>
273
+ EOT
274
+
275
+ expected_topic_hash = {
276
+ 'title' => "The First Topic",
277
+ 'author_name' => "David",
278
+ 'id' => 1,
279
+ 'approved' => false,
280
+ 'replies_count' => 0,
281
+ 'replies_close_in' => 2592000000,
282
+ 'written_on' => Date.new(2003, 7, 16),
283
+ 'viewed_at' => Time.utc(2003, 7, 16, 9, 28),
284
+ 'content' => "Have a nice day",
285
+ 'author_email_address' => "david@loudthinking.com",
286
+ 'parent_id' => nil
287
+ }
288
+ # puts Crack::XML.parse(topics_xml)['topics'].first.inspect
289
+ Crack::XML.parse(topics_xml)["topics"].first.each do |k,v|
290
+ v.should == expected_topic_hash[k]
291
+ end
292
+ end
293
+
294
+ should "should handle a single record from_xml with attributes other than type (ActiveSupport Compatible)" do
295
+ topic_xml = <<-EOT
296
+ <rsp stat="ok">
297
+ <photos page="1" pages="1" perpage="100" total="16">
298
+ <photo id="175756086" owner="55569174@N00" secret="0279bf37a1" server="76" title="Colored Pencil PhotoBooth Fun" ispublic="1" isfriend="0" isfamily="0"/>
299
+ </photos>
300
+ </rsp>
301
+ EOT
302
+
303
+ expected_topic_hash = {
304
+ 'id' => "175756086",
305
+ 'owner' => "55569174@N00",
306
+ 'secret' => "0279bf37a1",
307
+ 'server' => "76",
308
+ 'title' => "Colored Pencil PhotoBooth Fun",
309
+ 'ispublic' => "1",
310
+ 'isfriend' => "0",
311
+ 'isfamily' => "0",
312
+ }
313
+ Crack::XML.parse(topic_xml)["rsp"]["photos"]["photo"].each do |k,v|
314
+ v.should == expected_topic_hash[k]
315
+ end
316
+ end
317
+
318
+ should "should handle an emtpy array (ActiveSupport Compatible)" do
319
+ blog_xml = <<-XML
320
+ <blog>
321
+ <posts type="array"></posts>
322
+ </blog>
323
+ XML
324
+ expected_blog_hash = {"blog" => {"posts" => []}}
325
+ Crack::XML.parse(blog_xml).should == expected_blog_hash
326
+ end
327
+
328
+ should "should handle empty array with whitespace from xml (ActiveSupport Compatible)" do
329
+ blog_xml = <<-XML
330
+ <blog>
331
+ <posts type="array">
332
+ </posts>
333
+ </blog>
334
+ XML
335
+ expected_blog_hash = {"blog" => {"posts" => []}}
336
+ Crack::XML.parse(blog_xml).should == expected_blog_hash
337
+ end
338
+
339
+ should "should handle array with one entry from_xml (ActiveSupport Compatible)" do
340
+ blog_xml = <<-XML
341
+ <blog>
342
+ <posts type="array">
343
+ <post>a post</post>
344
+ </posts>
345
+ </blog>
346
+ XML
347
+ expected_blog_hash = {"blog" => {"posts" => ["a post"]}}
348
+ Crack::XML.parse(blog_xml).should == expected_blog_hash
349
+ end
350
+
351
+ should "should handle array with multiple entries from xml (ActiveSupport Compatible)" do
352
+ blog_xml = <<-XML
353
+ <blog>
354
+ <posts type="array">
355
+ <post>a post</post>
356
+ <post>another post</post>
357
+ </posts>
358
+ </blog>
359
+ XML
360
+ expected_blog_hash = {"blog" => {"posts" => ["a post", "another post"]}}
361
+ Crack::XML.parse(blog_xml).should == expected_blog_hash
362
+ end
363
+
364
+ should "should handle file types (ActiveSupport Compatible)" do
365
+ blog_xml = <<-XML
366
+ <blog>
367
+ <logo type="file" name="logo.png" content_type="image/png">
368
+ </logo>
369
+ </blog>
370
+ XML
371
+ hash = Crack::XML.parse(blog_xml)
372
+ hash.keys.should include('blog')
373
+ hash['blog'].keys.should include('logo')
374
+
375
+ file = hash['blog']['logo']
376
+ file.original_filename.should == 'logo.png'
377
+ file.content_type.should == 'image/png'
378
+ end
379
+
380
+ should "should handle file from xml with defaults (ActiveSupport Compatible)" do
381
+ blog_xml = <<-XML
382
+ <blog>
383
+ <logo type="file">
384
+ </logo>
385
+ </blog>
386
+ XML
387
+ file = Crack::XML.parse(blog_xml)['blog']['logo']
388
+ file.original_filename.should == 'untitled'
389
+ file.content_type.should == 'application/octet-stream'
390
+ end
391
+
392
+ should "should handle xsd like types from xml (ActiveSupport Compatible)" do
393
+ bacon_xml = <<-EOT
394
+ <bacon>
395
+ <weight type="double">0.5</weight>
396
+ <price type="decimal">12.50</price>
397
+ <chunky type="boolean"> 1 </chunky>
398
+ <expires-at type="dateTime">2007-12-25T12:34:56+0000</expires-at>
399
+ <notes type="string"></notes>
400
+ <illustration type="base64Binary">YmFiZS5wbmc=</illustration>
401
+ </bacon>
402
+ EOT
403
+
404
+ expected_bacon_hash = {
405
+ 'weight' => 0.5,
406
+ 'chunky' => true,
407
+ 'price' => BigDecimal("12.50"),
408
+ 'expires_at' => Time.utc(2007,12,25,12,34,56),
409
+ 'notes' => "",
410
+ 'illustration' => "babe.png"
411
+ }
412
+
413
+ Crack::XML.parse(bacon_xml)["bacon"].should == expected_bacon_hash
414
+ end
415
+
416
+ should "should let type trickle through when unknown (ActiveSupport Compatible)" do
417
+ product_xml = <<-EOT
418
+ <product>
419
+ <weight type="double">0.5</weight>
420
+ <image type="ProductImage"><filename>image.gif</filename></image>
421
+
422
+ </product>
423
+ EOT
424
+
425
+ expected_product_hash = {
426
+ 'weight' => 0.5,
427
+ 'image' => {'type' => 'ProductImage', 'filename' => 'image.gif' },
428
+ }
429
+
430
+ Crack::XML.parse(product_xml)["product"].should == expected_product_hash
431
+ end
432
+
433
+ should "should handle unescaping from xml (ActiveResource Compatible)" do
434
+ xml_string = '<person><bare-string>First &amp; Last Name</bare-string><pre-escaped-string>First &amp;amp; Last Name</pre-escaped-string></person>'
435
+ expected_hash = {
436
+ 'bare_string' => 'First & Last Name',
437
+ 'pre_escaped_string' => 'First &amp; Last Name'
438
+ }
439
+
440
+ Crack::XML.parse(xml_string)['person'].should == expected_hash
441
+ end
442
+
443
+ should "handle an empty xml string" do
444
+ Crack::XML.parse('').should == {}
445
+ end
446
+
447
+ # As returned in the response body by the unfuddle XML API when creating objects
448
+ should "handle an xml string containing a single space" do
449
+ Crack::XML.parse(' ').should == {}
450
+ end
451
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - John Nunemaker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-02 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: nunemaker@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - LICENSE
25
+ files:
26
+ - README.rdoc
27
+ - VERSION.yml
28
+ - lib/crack
29
+ - lib/crack/core_extensions.rb
30
+ - lib/crack/json.rb
31
+ - lib/crack/xml.rb
32
+ - lib/crack.rb
33
+ - test/crack_test.rb
34
+ - test/json_test.rb
35
+ - test/test_helper.rb
36
+ - test/xml_test.rb
37
+ - LICENSE
38
+ has_rdoc: true
39
+ homepage: http://github.com/jnunemaker/crack
40
+ post_install_message:
41
+ rdoc_options:
42
+ - --inline-source
43
+ - --charset=UTF-8
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project: crack
61
+ rubygems_version: 1.3.1
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: Really simple JSON and XML parsing, ripped from Merb and Rails.
65
+ test_files: []
66
+