peanuts 1.0
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/MIT-LICENSE +20 -0
- data/README.rdoc +104 -0
- data/Rakefile +26 -0
- data/lib/omg-peanuts.rb +1 -0
- data/lib/peanuts.rb +1 -0
- data/lib/peanuts/backend.rb +38 -0
- data/lib/peanuts/converters.rb +224 -0
- data/lib/peanuts/mappings.rb +154 -0
- data/lib/peanuts/nuts.rb +219 -0
- data/lib/peanuts/rexml.rb +98 -0
- data/test/parsing_test.rb +115 -0
- metadata +76 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Igor Gunko
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
=== Introduction
|
2
|
+
Peanuts is an library that allows for bidirectional mapping between Ruby objects and XML.
|
3
|
+
|
4
|
+
Released under the MIT license.
|
5
|
+
|
6
|
+
=== Features
|
7
|
+
- Type awareness (extensible).
|
8
|
+
- XML namespaces support.
|
9
|
+
- Pluggable backends to work with different XML APIs (REXML implemented so far).
|
10
|
+
|
11
|
+
=== Installation
|
12
|
+
gem install omg-peanuts --source http://gems.github.com
|
13
|
+
|
14
|
+
=== Usage
|
15
|
+
Please see an example below.
|
16
|
+
See also +Peanuts+, +Peanuts::ClassMethods+
|
17
|
+
|
18
|
+
=== TODO
|
19
|
+
- Inheritance
|
20
|
+
- Mixins
|
21
|
+
- More mappings?
|
22
|
+
- More types?
|
23
|
+
|
24
|
+
=== Docs
|
25
|
+
http://rdoc.info/projects/omg/peanuts
|
26
|
+
|
27
|
+
=== Bugs & such
|
28
|
+
Please report via Github issue tracking.
|
29
|
+
|
30
|
+
=== Example
|
31
|
+
class Cheezburger
|
32
|
+
include Peanuts
|
33
|
+
|
34
|
+
attribute :weight, :integer
|
35
|
+
end
|
36
|
+
|
37
|
+
class Cat
|
38
|
+
include Peanuts
|
39
|
+
|
40
|
+
namespaces :lol => 'urn:x-lol', :kthnx => 'urn:x-lol:kthnx'
|
41
|
+
|
42
|
+
root 'kitteh', :xmlns => :lol
|
43
|
+
|
44
|
+
attribute :has_tail, :boolean, :xmlname => 'has-tail', :xmlns => 'urn:x-lol:kthnx'
|
45
|
+
attribute :ears, :integer
|
46
|
+
|
47
|
+
element :ration, [:string], :xmlname => :eats, :xmlns => :kthnx
|
48
|
+
element :name, :string, :xmlns => 'urn:x-lol:kthnx'
|
49
|
+
elements :paws, :string, :xmlname => :paw
|
50
|
+
|
51
|
+
element :friends, :xmlname => :pals do # anonymous class definition follows within block
|
52
|
+
elements :names, :string, :xmlname => :friend, :xmlname => :pal
|
53
|
+
end
|
54
|
+
|
55
|
+
element :cheezburger, Cheezburger
|
56
|
+
end
|
57
|
+
|
58
|
+
xml_fragment = <<-EOS
|
59
|
+
<kitteh xmlns='urn:x-lol' xmlns:kthnx='urn:x-lol:kthnx' ears=' 2 ' kthnx:has-tail=' yes '>
|
60
|
+
<name xmlns='urn:x-lol:kthnx'>
|
61
|
+
Silly
|
62
|
+
Tom
|
63
|
+
</name>
|
64
|
+
<kthnx:eats>
|
65
|
+
tigers
|
66
|
+
lions
|
67
|
+
</kthnx:eats>
|
68
|
+
<pals>
|
69
|
+
<pal>Chrissy</pal>
|
70
|
+
<pal>Missy</pal>
|
71
|
+
<pal>Sissy</pal>
|
72
|
+
</pals>
|
73
|
+
<paw> one</paw>
|
74
|
+
<paw> two </paw>
|
75
|
+
<paw>three</paw>
|
76
|
+
<paw>four</paw>
|
77
|
+
<cheezburger weight='2' />
|
78
|
+
</kitteh>
|
79
|
+
EOS
|
80
|
+
cat = Cat.parse(xml_fragment)
|
81
|
+
|
82
|
+
assert_equal 'Silly Tom', cat.name
|
83
|
+
assert_equal %w(tigers lions), cat.ration
|
84
|
+
assert_equal ['Chrissy', 'Missy', 'Sissy'], cat.friends.names
|
85
|
+
assert_equal 2, cat.ears
|
86
|
+
assert_equal true, cat.has_tail
|
87
|
+
assert_equal %w(one two three four), cat.paws
|
88
|
+
assert_kind_of Cheezburger, cat.cheezburger
|
89
|
+
assert_equal 2, cat.cheezburger.weight
|
90
|
+
...
|
91
|
+
puts cat.build
|
92
|
+
|
93
|
+
|
94
|
+
=== See also
|
95
|
+
* http://github.com/omg/threadpool -- Thread pool implementation
|
96
|
+
* http://github.com/omg/statelogic -- A simple state machine for ActiveRecord
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
Free hint: If you liek mudkipz^W^Wfeel like generous today you can tip me at http://tipjoy.com/u/pisuka
|
101
|
+
|
102
|
+
|
103
|
+
Copyright (c) 2009 Igor Gunko, released under the MIT license
|
104
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$KCODE = 'u'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/clean'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/testtask'
|
9
|
+
|
10
|
+
Rake::GemPackageTask.new(Gem::Specification.load('peanuts.gemspec')) do |p|
|
11
|
+
p.need_tar = true
|
12
|
+
p.need_zip = true
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::RDocTask.new do |rdoc|
|
16
|
+
files =['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
|
17
|
+
rdoc.rdoc_files.add(files)
|
18
|
+
rdoc.main = "README.rdoc" # page to start on
|
19
|
+
rdoc.title = "Peanuts Documentation"
|
20
|
+
rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
|
21
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
22
|
+
end
|
23
|
+
|
24
|
+
Rake::TestTask.new do |t|
|
25
|
+
t.test_files = FileList['test/**/*.rb']
|
26
|
+
end
|
data/lib/omg-peanuts.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'peanuts'
|
data/lib/peanuts.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'peanuts/nuts'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module Peanuts
|
4
|
+
module XmlBackend
|
5
|
+
extend MonitorMixin
|
6
|
+
|
7
|
+
autoload :REXMLBackend, 'peanuts/rexml'
|
8
|
+
|
9
|
+
def self.default
|
10
|
+
synchronize do
|
11
|
+
unless defined? @@default
|
12
|
+
@@default = REXMLBackend.new
|
13
|
+
def self.default #:nodoc:
|
14
|
+
@@default
|
15
|
+
end
|
16
|
+
@@default
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.default=(backend)
|
22
|
+
@@default = backend
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.current
|
26
|
+
Thread.current[XmlBackend.name] || default
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.current=(backend)
|
30
|
+
Thread.current[XmlBackend.name] = backend
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def backend #:doc:
|
35
|
+
XmlBackend.current
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module Peanuts
|
2
|
+
autoload :Time, 'time'
|
3
|
+
autoload :BigDecimal, 'bigdecimal'
|
4
|
+
|
5
|
+
# === Currently supported types:
|
6
|
+
# string:: see +Convert_string+
|
7
|
+
# boolean:: see +Convert_boolean+, +Convert_yesno+
|
8
|
+
# numeric:: see +Convert_integer+, +Convert_decimal+, +Convert_float+
|
9
|
+
# date & time:: see +Convert_datetime+
|
10
|
+
# lists:: see +Convert_list+
|
11
|
+
module Converter
|
12
|
+
def self.lookup(type)
|
13
|
+
lookup!(type)
|
14
|
+
rescue ArgumentError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.lookup!(type)
|
19
|
+
const_get("Convert_#{type}")
|
20
|
+
rescue NameError
|
21
|
+
raise ArgumentError, "converter not found for #{type}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.create(type, options)
|
25
|
+
create!(type, options)
|
26
|
+
rescue ArgumentError
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.create!(type, options)
|
31
|
+
lookup!(type).new(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Who could have thought... a string.
|
35
|
+
#
|
36
|
+
# Specifier:: <tt>:string</tt>
|
37
|
+
#
|
38
|
+
# ==== Options:
|
39
|
+
# [<tt>:whitespace => :collapse</tt>]
|
40
|
+
# Whitespace handling behavior.
|
41
|
+
# [<tt>:trim</tt>] Trim whitespace from both ends.
|
42
|
+
# [<tt>:collapse</tt>] Collapse consecutive whitespace + trim as well.
|
43
|
+
# [<tt>:preserve</tt>] Keep'em all.
|
44
|
+
class Convert_string
|
45
|
+
def initialize(options)
|
46
|
+
@whitespace = options[:whitespace] || :collapse
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_xml(string)
|
50
|
+
string
|
51
|
+
end
|
52
|
+
|
53
|
+
def from_xml(string)
|
54
|
+
return nil unless string
|
55
|
+
string = case @whitespace
|
56
|
+
when :trim then string.gsub(/\A\s*|\s*\Z/, '')
|
57
|
+
when :preserve then string
|
58
|
+
when :collapse then string.gsub(/\s+/, ' ').gsub(/\A\s*|\s*\Z|\s*(?=\s)/, '')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# An XSD boolean.
|
64
|
+
#
|
65
|
+
# Specifier:: <tt>:boolean</tt>
|
66
|
+
#
|
67
|
+
# ==== Options:
|
68
|
+
# [<tt>:format => :true_false</tt>]
|
69
|
+
# Format variation.
|
70
|
+
# [<tt>:true_false</tt>] <tt>true/false</tt>
|
71
|
+
# [<tt>:yes_no</tt>] <tt>yes/no</tt>
|
72
|
+
# [<tt>:numeric</tt>] <tt>0/1</tt>
|
73
|
+
# In addition supports all options of +Convert_string+.
|
74
|
+
#
|
75
|
+
# See also +Convert_yesno+.
|
76
|
+
class Convert_boolean < Convert_string
|
77
|
+
def initialize(options)
|
78
|
+
super
|
79
|
+
@format = options[:format] || :truefalse
|
80
|
+
raise ArgumentError, "unrecognized format #{@format}" unless [:truefalse, :yesno, :numeric].include?(@format)
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_xml(flag)
|
84
|
+
return nil if flag.nil?
|
85
|
+
string = case @format
|
86
|
+
when :true_false then flag ? 'true' : 'false'
|
87
|
+
when :yes_no then flag ? 'yes' : 'no'
|
88
|
+
when :numeric then flag ? '0' : '1'
|
89
|
+
end
|
90
|
+
super(string)
|
91
|
+
end
|
92
|
+
|
93
|
+
def from_xml(string)
|
94
|
+
case string = super(string)
|
95
|
+
when nil then nil
|
96
|
+
when '1', 'true', 'yes' then true
|
97
|
+
when '0', 'false', 'no' then false
|
98
|
+
else
|
99
|
+
raise ArgumentError, "invalid value for boolean: #{string.inspect}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# The same as +Convert_boolean+ but with the <tt>:yes_no</tt> default format.
|
105
|
+
#
|
106
|
+
# Specifier:: <tt>:yesno</tt>
|
107
|
+
class Convert_yesno < Convert_boolean
|
108
|
+
def initialize(options)
|
109
|
+
options[:format] ||= :yes_no
|
110
|
+
super
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# An integer.
|
115
|
+
#
|
116
|
+
# Specifier:: <tt>:integer</tt>
|
117
|
+
#
|
118
|
+
# ==== Options
|
119
|
+
# Accepts all options of +Convert_string+.
|
120
|
+
class Convert_integer < Convert_string
|
121
|
+
def initialize(options)
|
122
|
+
super
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_xml(int)
|
126
|
+
super(int.to_s)
|
127
|
+
end
|
128
|
+
|
129
|
+
def from_xml(string)
|
130
|
+
(string = super(string)) && Integer(string)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# A decimal.
|
135
|
+
#
|
136
|
+
# Specifier:: <tt>:decimal</tt>
|
137
|
+
# Ruby type:: +BigDecimal+
|
138
|
+
#
|
139
|
+
# ==== Options
|
140
|
+
# Accepts all options of +Convert_string+.
|
141
|
+
class Convert_decimal < Convert_string
|
142
|
+
def initialize(options)
|
143
|
+
super
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_xml(int)
|
147
|
+
super(int && int.to_s('F'))
|
148
|
+
end
|
149
|
+
|
150
|
+
def from_xml(string)
|
151
|
+
(string = super(string)) && BigDecimal.new(string)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# A float.
|
156
|
+
#
|
157
|
+
# Specifier:: <tt>:float</tt>
|
158
|
+
#
|
159
|
+
# ==== Options
|
160
|
+
# [<tt>:precision</tt>] Floating point precision.
|
161
|
+
#
|
162
|
+
# In addition accepts all options of +Convert_string+.
|
163
|
+
class Convert_float < Convert_string
|
164
|
+
def initialize(options)
|
165
|
+
super
|
166
|
+
@precision = options[:precision]
|
167
|
+
@format = @precision ? "%f.#{@precision}" : '%f'
|
168
|
+
end
|
169
|
+
|
170
|
+
def to_xml(int)
|
171
|
+
super(int && sprintf(@format, int))
|
172
|
+
end
|
173
|
+
|
174
|
+
def from_xml(string)
|
175
|
+
(string = super(string)) && string.to_f
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# An XSD datetime.
|
180
|
+
#
|
181
|
+
# Specifier:: <tt>:datetime</tt>
|
182
|
+
# Ruby type:: +Time+
|
183
|
+
#
|
184
|
+
# ==== Options
|
185
|
+
# Accepts all options of +Convert_string+.
|
186
|
+
class Convert_datetime < Convert_string
|
187
|
+
def initialize(options)
|
188
|
+
super
|
189
|
+
@fraction_digits = options[:fraction_digits] || 0
|
190
|
+
end
|
191
|
+
|
192
|
+
def to_xml(time)
|
193
|
+
super(time && time.xmlschema(@fraction_digits))
|
194
|
+
end
|
195
|
+
|
196
|
+
def from_xml(string)
|
197
|
+
(string = super(string)) && Time.parse(string)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# An XSD whitespace-separated list.
|
202
|
+
#
|
203
|
+
# Specifier:: <tt>:list, :item_type => <em>simple type specifier</em></tt>
|
204
|
+
# Alternative specifier:: <tt>[<em>simple type specifier</em>]</tt>
|
205
|
+
# Ruby type:: +Array+ of <tt><em>simple type</em></tt>
|
206
|
+
#
|
207
|
+
# ==== Options
|
208
|
+
# All options will be passed to the underlying type converter.
|
209
|
+
class Convert_list
|
210
|
+
def initialize(options)
|
211
|
+
@item_type = options[:item_type] || :string
|
212
|
+
@item_converter = Converter.create!(@item_type, options)
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_xml(items)
|
216
|
+
items && items.map {|x| @item_converter.to_xml(x) } * ' '
|
217
|
+
end
|
218
|
+
|
219
|
+
def from_xml(string)
|
220
|
+
string && string.split.map! {|x| @item_converter.from_xml(x)}
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
require 'peanuts/backend'
|
3
|
+
require 'peanuts/converters'
|
4
|
+
|
5
|
+
module Peanuts
|
6
|
+
module Mappings
|
7
|
+
class Mapping
|
8
|
+
attr_reader :xmlname, :xmlns, :options
|
9
|
+
|
10
|
+
def initialize(xmlname, options)
|
11
|
+
@xmlname, @xmlns, @options = xmlname.to_s, options.delete(:xmlns), options
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Root < Mapping
|
16
|
+
def initialize(xmlname, options = {})
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MemberMapping < Mapping
|
22
|
+
include XmlBackend
|
23
|
+
|
24
|
+
attr_reader :name, :type, :converter
|
25
|
+
|
26
|
+
def initialize(name, type, options)
|
27
|
+
super(options.delete(:xmlname) || name, options)
|
28
|
+
case type
|
29
|
+
when Array
|
30
|
+
raise ArgumentError, "invalid value for type: #{type}" if type.length != 1
|
31
|
+
options[:item_type] = type.first
|
32
|
+
@converter = Converter.create!(:list, options)
|
33
|
+
when Class
|
34
|
+
options[:object_type] = type
|
35
|
+
else
|
36
|
+
@converter = Converter.create!(type, options)
|
37
|
+
end
|
38
|
+
@name, @setter, @type = name.to_sym, :"#{name}=", type
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_xml(nut, node)
|
42
|
+
setxml(node, get(nut))
|
43
|
+
end
|
44
|
+
|
45
|
+
def from_xml(nut, node)
|
46
|
+
set(nut, getxml(node))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def get(nut)
|
51
|
+
nut.send(@name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def set(nut, value)
|
55
|
+
nut.send(@setter, value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def toxml(value)
|
59
|
+
@converter ? @converter.to_xml(value) : value
|
60
|
+
end
|
61
|
+
|
62
|
+
def froxml(text)
|
63
|
+
@converter ? @converter.from_xml(text) : text
|
64
|
+
end
|
65
|
+
|
66
|
+
def each_element(node, &block)
|
67
|
+
node && backend.each_element(node, xmlname, xmlns, &block)
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_element(node, value = nil)
|
72
|
+
backend.add_element(node, xmlname, xmlns, value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def value(node)
|
76
|
+
backend.value(node)
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse(node)
|
80
|
+
type.parse_node(type.new, node)
|
81
|
+
end
|
82
|
+
|
83
|
+
def build(node, nut, dest_node)
|
84
|
+
nut && type.build_node(nut, dest_node)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class ElementValue < MemberMapping
|
89
|
+
private
|
90
|
+
def getxml(node)
|
91
|
+
each_element(node) {|e| return froxml(value(e)) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def setxml(node, value)
|
95
|
+
add_element(node, toxml(value))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Element < MemberMapping
|
100
|
+
private
|
101
|
+
def getxml(node)
|
102
|
+
each_element(node) {|e| return parse(e) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def setxml(node, value)
|
106
|
+
build(node, value, add_element(node))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Attribute < MemberMapping
|
111
|
+
private
|
112
|
+
def getxml(node)
|
113
|
+
froxml(backend.attribute(node, xmlname, xmlns))
|
114
|
+
end
|
115
|
+
|
116
|
+
def setxml(node, value)
|
117
|
+
backend.set_attribute(node, xmlname, xmlns, toxml(value))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class ElementValues < MemberMapping
|
122
|
+
private
|
123
|
+
def each_value(node)
|
124
|
+
each_element(node) {|x| yield froxml(value(x)) }
|
125
|
+
end
|
126
|
+
|
127
|
+
def getxml(node)
|
128
|
+
enum_for(:each_value, node).to_a
|
129
|
+
end
|
130
|
+
|
131
|
+
def setxml(node, values)
|
132
|
+
unless node
|
133
|
+
raise 'fuck'
|
134
|
+
end
|
135
|
+
values.each {|v| add_element(node, toxml(v)) } if values
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class Elements < MemberMapping
|
140
|
+
private
|
141
|
+
def each_object(node)
|
142
|
+
each_element(node) {|e| yield parse(e) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def getxml(node)
|
146
|
+
enum_for(:each_object, node)
|
147
|
+
end
|
148
|
+
|
149
|
+
def setxml(node, elements)
|
150
|
+
elements.each {|e| build(node, e, add_element(node)) } if elements
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/peanuts/nuts.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'peanuts/mappings'
|
2
|
+
|
3
|
+
module Peanuts #:nodoc:
|
4
|
+
# See also +ClassMethods+
|
5
|
+
def self.included(other) #:nodoc:
|
6
|
+
other.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
# See also +PeaNuts+.
|
10
|
+
module ClassMethods
|
11
|
+
include XmlBackend
|
12
|
+
include Mappings
|
13
|
+
|
14
|
+
# namespaces(hash) -> Hash
|
15
|
+
# namespaces -> Hash
|
16
|
+
#
|
17
|
+
# Updates and returns class-level prefix mappings.
|
18
|
+
# When given a hash of mappings merges it over current.
|
19
|
+
# When called withot arguments simply returns current mappings.
|
20
|
+
#
|
21
|
+
# === Example:
|
22
|
+
# class Cat
|
23
|
+
# include PeaNuts
|
24
|
+
# namespaces :lol => 'urn:lol', ...
|
25
|
+
# ...
|
26
|
+
# end
|
27
|
+
def namespaces(mappings = nil)
|
28
|
+
@namespaces ||= {}
|
29
|
+
mappings ? @namespaces.update(mappings) : @namespaces
|
30
|
+
end
|
31
|
+
|
32
|
+
# root(xmlname[, :xmlns => ...]) -> Mappings::Root
|
33
|
+
# root -> Mappings::Root
|
34
|
+
#
|
35
|
+
# Defines element name.
|
36
|
+
# TODO: moar details
|
37
|
+
#
|
38
|
+
# === Arguments
|
39
|
+
# [+xmlname+] Element name
|
40
|
+
# [+options+] <tt>:xmlns => <tt> Element namespace
|
41
|
+
#
|
42
|
+
# === Example:
|
43
|
+
# class Cat
|
44
|
+
# include Peanuts
|
45
|
+
# ...
|
46
|
+
# root :kitteh, :xmlns => 'urn:lol'
|
47
|
+
# ...
|
48
|
+
# end
|
49
|
+
def root(xmlname = nil, options = {})
|
50
|
+
@root = Root.new(xmlname, prepare_options(options)) if xmlname
|
51
|
+
@root ||= Root.new('root')
|
52
|
+
end
|
53
|
+
|
54
|
+
# element(name, [type[, options]]) -> Mappings::Element or Mappings::ElementValue
|
55
|
+
# element(name[, options]) { block } -> Mappings::Element
|
56
|
+
#
|
57
|
+
# Defines single-element mapping.
|
58
|
+
#
|
59
|
+
# === Arguments
|
60
|
+
# [+name+] Accessor name
|
61
|
+
# [+type+] Element type. <tt>:string</tt> assumed if omitted (see +Converter+).
|
62
|
+
# [+options+] <tt>:xmlname</tt>, <tt>:xmlns</tt>, converter options (see +Converter+).
|
63
|
+
# [+block+] An anonymous class definition.
|
64
|
+
#
|
65
|
+
# === Example:
|
66
|
+
# class Cat
|
67
|
+
# include Peanuts
|
68
|
+
# ...
|
69
|
+
# element :name, :string, :whitespace => :collapse
|
70
|
+
# element :cheeseburger, Cheeseburger, :xmlname => :cheezburger
|
71
|
+
# ...
|
72
|
+
# end
|
73
|
+
def element(name, type = :string, options = {}, &block)
|
74
|
+
type, options = prepare_args(type, options, &block)
|
75
|
+
define_accessor name
|
76
|
+
(mappings << (type.is_a?(Class) ? Element : ElementValue).new(name, type, prepare_options(options))).last
|
77
|
+
end
|
78
|
+
|
79
|
+
# elements(name, [type[, options]]) -> Mappings::Element or Mappings::ElementValue
|
80
|
+
# elements(name[, options]) { block } -> Mappings::Element
|
81
|
+
#
|
82
|
+
# Defines multiple elements mapping.
|
83
|
+
#
|
84
|
+
# === Arguments
|
85
|
+
# [+name+] Accessor name
|
86
|
+
# [+type+] Element type. <tt>:string</tt> assumed if omitted (see +Converter+).
|
87
|
+
# [+options+] <tt>:xmlname</tt>, <tt>:xmlns</tt>, converter options (see +Converter+).
|
88
|
+
# [+block+] An anonymous class definition.
|
89
|
+
#
|
90
|
+
# === Example:
|
91
|
+
# class RichCat
|
92
|
+
# include Peanuts
|
93
|
+
# ...
|
94
|
+
# elements :ration, :string, :whitespace => :collapse
|
95
|
+
# elements :cheeseburgers, Cheeseburger, :xmlname => :cheezburger
|
96
|
+
# ...
|
97
|
+
# end
|
98
|
+
def elements(name, type = :string, options = {}, &block)
|
99
|
+
type, options = prepare_args(type, options, &block)
|
100
|
+
define_accessor name
|
101
|
+
(mappings << (type.is_a?(Class) ? Elements : ElementValues).new(name, type, prepare_options(options))).last
|
102
|
+
end
|
103
|
+
|
104
|
+
# attribute(name, [type[, options]]) -> Mappings::Attribute or Mappings::AttributeValue
|
105
|
+
#
|
106
|
+
# Defines attribute mapping.
|
107
|
+
#
|
108
|
+
# === Arguments
|
109
|
+
# [+name+] Accessor name
|
110
|
+
# [+type+] Element type. <tt>:string</tt> assumed if omitted (see +Converter+).
|
111
|
+
# [+options+] <tt>:xmlname</tt>, <tt>:xmlns</tt>, converter options (see +Converter+).
|
112
|
+
#
|
113
|
+
# === Example:
|
114
|
+
# class Cat
|
115
|
+
# include Peanuts
|
116
|
+
# ...
|
117
|
+
# element :name, :string, :whitespace => :collapse
|
118
|
+
# element :cheeseburger, Cheeseburger, :xmlname => :cheezburger
|
119
|
+
# ...
|
120
|
+
# end
|
121
|
+
def attribute(name, type = :string, options = {})
|
122
|
+
define_accessor name
|
123
|
+
mappings << Attribute.new(name, type, prepare_options(options))
|
124
|
+
end
|
125
|
+
|
126
|
+
# mappings -> Array
|
127
|
+
#
|
128
|
+
# Returns all mappings defined on a class.
|
129
|
+
def mappings
|
130
|
+
@mappings ||= []
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse(source, options = {})
|
134
|
+
backend.parse(source, options) {|node| parse_node(new, node) }
|
135
|
+
end
|
136
|
+
|
137
|
+
def build(nut, result = :string, options = {})
|
138
|
+
options, result = result, :string if result.is_a?(Hash)
|
139
|
+
options[:xmlname] ||= root.xmlname
|
140
|
+
options[:xmlns_prefix] = namespaces.invert[options[:xmlns] ||= root.xmlns]
|
141
|
+
backend.build(result, options) {|node| build_node(nut, node) }
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_node(nut, node) #:nodoc:
|
145
|
+
backend.add_namespaces(node, namespaces)
|
146
|
+
callem(:to_xml, nut, node)
|
147
|
+
node
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse_node(nut, node) #:nodoc:
|
151
|
+
callem(:from_xml, nut, node)
|
152
|
+
nut
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
def prepare_args(type, options, &block)
|
157
|
+
if block_given?
|
158
|
+
options = type if type.is_a?(Hash)
|
159
|
+
type = Class.new
|
160
|
+
type.class_eval do
|
161
|
+
include Peanuts
|
162
|
+
class_eval(&block)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
return type, prepare_options(options)
|
166
|
+
end
|
167
|
+
|
168
|
+
def prepare_options(options)
|
169
|
+
ns = options[:xmlns]
|
170
|
+
if ns.is_a?(Symbol)
|
171
|
+
raise ArgumentError, "undefined prefix: #{ns}" unless options[:xmlns] = namespaces[ns]
|
172
|
+
end
|
173
|
+
options
|
174
|
+
end
|
175
|
+
|
176
|
+
def define_accessor(name)
|
177
|
+
if method_defined?(name) || method_defined?("#{name}=")
|
178
|
+
raise ArgumentError, "#{name}: name already defined or reserved"
|
179
|
+
end
|
180
|
+
attr_accessor name
|
181
|
+
end
|
182
|
+
|
183
|
+
def callem(method, *args)
|
184
|
+
mappings.each {|m| m.send(method, *args) }
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def parse(source, options = {})
|
189
|
+
backend.parse(source, options) {|node| parse_node(node) }
|
190
|
+
end
|
191
|
+
|
192
|
+
# build([options]) -> root element or string
|
193
|
+
# build([options]) -> root element or string
|
194
|
+
# build(destination[, options]) -> destination
|
195
|
+
#
|
196
|
+
# Defines attribute mapping.
|
197
|
+
#
|
198
|
+
# === Arguments
|
199
|
+
# [+destination+]
|
200
|
+
# Can be given a symbol a backend-specific object, an instance of String or IO classes.
|
201
|
+
# [<tt>:string</tt>] will return an XML string.
|
202
|
+
# [<tt>:document</tt>] will return a backend specific document object.
|
203
|
+
# [<tt>:object</tt>] will return a backend specific object. New document will be created.
|
204
|
+
# [an instance of +String+] the contents of the string will be replaced with the generated XML.
|
205
|
+
# [an instance of +IO+] the IO will be written to.
|
206
|
+
# [+options+] Backend-specific options
|
207
|
+
#
|
208
|
+
# === Example:
|
209
|
+
# cat = Cat.new
|
210
|
+
# cat.name = 'Pussy'
|
211
|
+
# puts cat.build
|
212
|
+
# ...
|
213
|
+
# doc = REXML::Document.new
|
214
|
+
# cat.build(doc)
|
215
|
+
# puts doc.to_s
|
216
|
+
def build(result = :string, options = {})
|
217
|
+
self.class.build(self, result, options)
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'peanuts/backend'
|
3
|
+
|
4
|
+
class Peanuts::XmlBackend::REXMLBackend #:nodoc:
|
5
|
+
def parse(source, options)
|
6
|
+
case source
|
7
|
+
when nil
|
8
|
+
return nil
|
9
|
+
when REXML::Document
|
10
|
+
node = source.root
|
11
|
+
when REXML::Node
|
12
|
+
node = source
|
13
|
+
when String, IO
|
14
|
+
node = REXML::Document.new(source).root
|
15
|
+
else
|
16
|
+
raise ArgumentError, 'invalid source'
|
17
|
+
end
|
18
|
+
node && yield(node)
|
19
|
+
end
|
20
|
+
|
21
|
+
def build(result, options)
|
22
|
+
case result
|
23
|
+
when :string, :document, :object, String, IO
|
24
|
+
doc = REXML::Document.new
|
25
|
+
when REXML::Document
|
26
|
+
doc = result
|
27
|
+
when REXML::Node
|
28
|
+
node, doc = result, result.document
|
29
|
+
else
|
30
|
+
raise ArgumentError, 'invalid destination'
|
31
|
+
end
|
32
|
+
node ||= doc.root
|
33
|
+
unless node
|
34
|
+
name, ns, prefix = options[:xmlname], options[:xmlns], options[:xmlns_prefix]
|
35
|
+
name, ns = "#{prefix}:#{name}", nil if prefix
|
36
|
+
node = add_element(doc, name, ns, nil)
|
37
|
+
end
|
38
|
+
|
39
|
+
yield node
|
40
|
+
|
41
|
+
case result
|
42
|
+
when :string
|
43
|
+
doc.to_s
|
44
|
+
when String
|
45
|
+
result.replace(doc.to_s)
|
46
|
+
when IO
|
47
|
+
doc.write(result)
|
48
|
+
result
|
49
|
+
when REXML::Document, :document
|
50
|
+
doc
|
51
|
+
when REXML::Node, :object
|
52
|
+
node
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_namespaces(context, namespaces)
|
57
|
+
namespaces.each {|prefix, uri| context.add_namespace(prefix.to_s, uri) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def each_element(context, name, ns, &block)
|
61
|
+
ns = context.namespace unless ns
|
62
|
+
REXML::XPath.each(context, "ns:#{name}", 'ns' => ns, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def value(node)
|
66
|
+
node.text
|
67
|
+
end
|
68
|
+
|
69
|
+
def attribute(context, name, ns)
|
70
|
+
name, ns = to_prefixed_name(context, name, ns, true)
|
71
|
+
context.attributes[name]
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_attribute(context, name, ns, text)
|
75
|
+
name, ns = to_prefixed_name(context, name, ns, true)
|
76
|
+
context.add_attribute(name, text)
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_element(context, name, ns, text)
|
80
|
+
name, ns = to_prefixed_name(context, name, ns, false)
|
81
|
+
elem = context.add_element(name)
|
82
|
+
elem.add_namespace(ns) if ns
|
83
|
+
elem.text = text if text
|
84
|
+
elem
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def to_prefixed_name(context, name, ns, prefix_required)
|
89
|
+
if ns
|
90
|
+
if prefix = context.namespaces.invert[ns]
|
91
|
+
name, ns = "#{prefix}:#{name}", nil
|
92
|
+
else
|
93
|
+
raise ArgumentError, "no prefix defined for #{ns}" if prefix_required
|
94
|
+
end
|
95
|
+
end
|
96
|
+
return name, ns
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#$:.unshift File.join(File.dirname(__FILE__),'..','lib')
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'shoulda'
|
7
|
+
require 'lib/peanuts'
|
8
|
+
|
9
|
+
|
10
|
+
class Cheezburger
|
11
|
+
include Peanuts
|
12
|
+
|
13
|
+
attribute :weight, :float
|
14
|
+
attribute :price, :decimal
|
15
|
+
end
|
16
|
+
|
17
|
+
class Cat
|
18
|
+
include Peanuts
|
19
|
+
|
20
|
+
namespaces :lol => 'urn:x-lol', :kthnx => 'urn:x-lol:kthnx'
|
21
|
+
|
22
|
+
root 'kitteh', :xmlns => :lol
|
23
|
+
|
24
|
+
attribute :has_tail, :boolean, :xmlname => 'has-tail', :xmlns => 'urn:x-lol:kthnx'
|
25
|
+
attribute :ears, :integer
|
26
|
+
|
27
|
+
element :ration, [:string], :xmlname => :eats, :xmlns => :kthnx
|
28
|
+
element :name, :string, :xmlns => 'urn:x-lol:kthnx'
|
29
|
+
elements :paws, :string, :xmlname => :paw
|
30
|
+
|
31
|
+
element :friends, :xmlname => :pals do
|
32
|
+
elements :names, :string, :xmlname => :pal
|
33
|
+
end
|
34
|
+
|
35
|
+
element :cheezburger, Cheezburger
|
36
|
+
element :moar_cheezburgers do
|
37
|
+
elements :cheezburger, Cheezburger
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class ParsingTest < Test::Unit::TestCase
|
42
|
+
def setup
|
43
|
+
@xml_fragment = <<-EOS
|
44
|
+
<kitteh xmlns='urn:x-lol' xmlns:kthnx='urn:x-lol:kthnx' ears=' 2 ' kthnx:has-tail=' yes '>
|
45
|
+
<name xmlns='urn:x-lol:kthnx'>
|
46
|
+
Silly
|
47
|
+
Tom
|
48
|
+
</name>
|
49
|
+
<kthnx:eats>
|
50
|
+
tigers
|
51
|
+
lions
|
52
|
+
</kthnx:eats>
|
53
|
+
<pals>
|
54
|
+
<pal>Chrissy</pal>
|
55
|
+
<pal>Missy</pal>
|
56
|
+
<pal>Sissy</pal>
|
57
|
+
</pals>
|
58
|
+
<paw> one</paw>
|
59
|
+
<paw> two </paw>
|
60
|
+
<paw>three</paw>
|
61
|
+
<paw>four</paw>
|
62
|
+
<cheezburger price='2.05' weight='14.5547' />
|
63
|
+
<moar_cheezburgers>
|
64
|
+
<cheezburger price='19' weight='685.940' />
|
65
|
+
<cheezburger price='7.40' weight='9356.7' />
|
66
|
+
</moar_cheezburgers>
|
67
|
+
</kitteh>
|
68
|
+
EOS
|
69
|
+
@cat = Cat.parse(@xml_fragment)
|
70
|
+
end
|
71
|
+
|
72
|
+
context "A cat" do
|
73
|
+
should 'be named Silly Tom' do
|
74
|
+
assert_equal 'Silly Tom', @cat.name
|
75
|
+
end
|
76
|
+
|
77
|
+
should 'eat tigers and lions' do
|
78
|
+
assert_equal %w(tigers lions), @cat.ration
|
79
|
+
end
|
80
|
+
|
81
|
+
should 'be a friend of Chrissy, Missy & Sissy' do
|
82
|
+
assert_equal ['Chrissy', 'Missy', 'Sissy'], @cat.friends.names
|
83
|
+
end
|
84
|
+
|
85
|
+
should 'have 2 ears' do
|
86
|
+
assert_equal 2, @cat.ears
|
87
|
+
end
|
88
|
+
|
89
|
+
should 'have a tail' do
|
90
|
+
assert @cat.has_tail
|
91
|
+
end
|
92
|
+
|
93
|
+
should 'have four paws' do
|
94
|
+
assert_equal %w(one two three four), @cat.paws
|
95
|
+
end
|
96
|
+
|
97
|
+
should 'has cheezburger' do
|
98
|
+
assert_kind_of Cheezburger, @cat.cheezburger
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'A cheezburger' do
|
103
|
+
setup do
|
104
|
+
@burger = @cat.cheezburger
|
105
|
+
end
|
106
|
+
|
107
|
+
should 'weigh 14.5547 pounds' do
|
108
|
+
assert_equal 14.5547, @burger.weight
|
109
|
+
end
|
110
|
+
|
111
|
+
should 'cost $2.05' do
|
112
|
+
assert_equal BigDecimal('2.05'), @burger.price
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: peanuts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.0"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Igor Gunko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-21 00:00:00 +03:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.0.6
|
24
|
+
version:
|
25
|
+
description: " Peanuts is an XML to Ruby and back again mapping library.\n"
|
26
|
+
email: tekmon@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.rdoc
|
33
|
+
- MIT-LICENSE
|
34
|
+
files:
|
35
|
+
- README.rdoc
|
36
|
+
- MIT-LICENSE
|
37
|
+
- Rakefile
|
38
|
+
- lib/peanuts.rb
|
39
|
+
- lib/omg-peanuts.rb
|
40
|
+
- lib/peanuts/nuts.rb
|
41
|
+
- lib/peanuts/mappings.rb
|
42
|
+
- lib/peanuts/converters.rb
|
43
|
+
- lib/peanuts/backend.rb
|
44
|
+
- lib/peanuts/rexml.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/omg/peanuts
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --line-numbers
|
52
|
+
- --main
|
53
|
+
- README.rdoc
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.5
|
72
|
+
signing_key:
|
73
|
+
specification_version: 2
|
74
|
+
summary: Making XML <-> Ruby binding easy
|
75
|
+
test_files:
|
76
|
+
- test/parsing_test.rb
|