omg-peanuts 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +104 -0
- data/Rakefile +26 -0
- data/lib/omg-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/lib/peanuts.rb +1 -0
- data/test/parsing_test.rb +115 -0
- metadata +74 -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'
|
@@ -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
|
data/lib/peanuts.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'peanuts/nuts'
|
@@ -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,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: omg-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 -07: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.
|
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
|
+
post_install_message:
|
48
|
+
rdoc_options:
|
49
|
+
- --line-numbers
|
50
|
+
- --main
|
51
|
+
- README.rdoc
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.2.0
|
70
|
+
signing_key:
|
71
|
+
specification_version: 2
|
72
|
+
summary: Making XML <-> Ruby binding easy
|
73
|
+
test_files:
|
74
|
+
- test/parsing_test.rb
|