peanuts 1.0

Sign up to get free protection for your applications and to get access to all the features.
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