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 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
@@ -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
@@ -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