peanuts 1.0 → 2.0.7

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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2008 Igor Gunko
1
+ Copyright (c) 2009 Igor Gunko
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -1,4 +1,11 @@
1
1
  === Introduction
2
+ *This is currently undergoing some rewrite. 2.x branch will introduce minor api incompatibilities.*
3
+
4
+ +Upd:+ Ok, the thing is out and the example below has been updated for 2.x, but the rdocs are
5
+ largely out of date and I'm not sure whether I should update them since it seems like nobody's
6
+ using this lib anyway.
7
+
8
+
2
9
  Peanuts is an library that allows for bidirectional mapping between Ruby objects and XML.
3
10
 
4
11
  Released under the MIT license.
@@ -9,7 +16,8 @@ Released under the MIT license.
9
16
  - Pluggable backends to work with different XML APIs (REXML implemented so far).
10
17
 
11
18
  === Installation
12
- gem install omg-peanuts --source http://gems.github.com
19
+ Beta 2.x (recommended) and stable 1.x available from Gemcutter
20
+ gem install peanuts --source http://gemcutter.org
13
21
 
14
22
  === Usage
15
23
  Please see an example below.
@@ -31,7 +39,24 @@ Please report via Github issue tracking.
31
39
  class Cheezburger
32
40
  include Peanuts
33
41
 
34
- attribute :weight, :integer
42
+ attribute :weight, :float
43
+ attribute :price, :decimal
44
+
45
+ def initialize(weight = nil, price = nil)
46
+ @weight, @price = weight, price
47
+ end
48
+
49
+ def eql?(other)
50
+ other && weight == other.weight && price == other.price
51
+ end
52
+
53
+ alias == eql?
54
+ end
55
+
56
+ class Paws
57
+ include Peanuts
58
+
59
+ elements :paws, :name => :paw, :ns => 'urn:x-lol'
35
60
  end
36
61
 
37
62
  class Cat
@@ -39,20 +64,24 @@ Please report via Github issue tracking.
39
64
 
40
65
  namespaces :lol => 'urn:x-lol', :kthnx => 'urn:x-lol:kthnx'
41
66
 
42
- root 'kitteh', :xmlns => :lol
67
+ root 'kitteh', :ns => 'urn:x-lol'
43
68
 
44
- attribute :has_tail, :boolean, :xmlname => 'has-tail', :xmlns => 'urn:x-lol:kthnx'
69
+ attribute :has_tail?, :boolean, :name => 'has-tail', :ns => :kthnx
45
70
  attribute :ears, :integer
46
71
 
47
- element :ration, [:string], :xmlname => :eats, :xmlns => :kthnx
48
- element :name, :string, :xmlns => 'urn:x-lol:kthnx'
49
- elements :paws, :string, :xmlname => :paw
72
+ element :ration, [:string], :name => :eats, :ns => :kthnx
73
+ element :name, :ns => 'urn:x-lol:kthnx'
50
74
 
51
- element :friends, :xmlname => :pals do # anonymous class definition follows within block
52
- elements :names, :string, :xmlname => :friend, :xmlname => :pal
75
+ shallow :paws, Paws
76
+
77
+ shallow :pals, :ns => :kthnx do
78
+ elements :friends, :name => :pal
53
79
  end
54
80
 
55
81
  element :cheezburger, Cheezburger
82
+ element :moar_cheezburgers do
83
+ elements :cheezburger, Cheezburger
84
+ end
56
85
  end
57
86
 
58
87
  xml_fragment = <<-EOS
@@ -65,19 +94,26 @@ Please report via Github issue tracking.
65
94
  tigers
66
95
  lions
67
96
  </kthnx:eats>
68
- <pals>
97
+ <kthnx:pals>
69
98
  <pal>Chrissy</pal>
70
99
  <pal>Missy</pal>
71
100
  <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' />
101
+ </kthnx:pals>
102
+ <paws>
103
+ <paw> one</paw>
104
+ <paw> two </paw>
105
+ <paw>three</paw>
106
+ <paw>four</paw>
107
+ </paws>
108
+ <cheezburger price='2.05' weight='14.5547' />
109
+ <moar_cheezburgers>
110
+ <cheezburger price='19' weight='685.940' />
111
+ <cheezburger price='7.40' weight='9356.7' />
112
+ </moar_cheezburgers>
78
113
  </kitteh>
79
114
  EOS
80
- cat = Cat.parse(xml_fragment)
115
+ cat = Cat.restore_from(xml_fragment)
116
+ cheezburger = cat.cheezburger
81
117
 
82
118
  assert_equal 'Silly Tom', cat.name
83
119
  assert_equal %w(tigers lions), cat.ration
@@ -88,17 +124,12 @@ Please report via Github issue tracking.
88
124
  assert_kind_of Cheezburger, cat.cheezburger
89
125
  assert_equal 2, cat.cheezburger.weight
90
126
  ...
91
- puts cat.build
127
+ puts cat.save_to(:string)
92
128
 
93
129
 
94
130
  === 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
-
131
+ * http://github.com/omg/unrest -- REST-client
132
+ * http://github.com/omg/statelogic -- Simple state machine for ActiveRecord
102
133
 
103
134
  Copyright (c) 2009 Igor Gunko, released under the MIT license
104
135
 
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake'
5
5
  require 'rake/clean'
6
6
  require 'rake/gempackagetask'
7
7
  require 'rake/rdoctask'
8
- require 'rake/testtask'
8
+ require 'spec/rake/spectask'
9
9
 
10
10
  Rake::GemPackageTask.new(Gem::Specification.load('peanuts.gemspec')) do |p|
11
11
  p.need_tar = true
@@ -18,9 +18,12 @@ Rake::RDocTask.new do |rdoc|
18
18
  rdoc.main = "README.rdoc" # page to start on
19
19
  rdoc.title = "Peanuts Documentation"
20
20
  rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
21
- rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.options << '--line-numbers'
22
22
  end
23
23
 
24
- Rake::TestTask.new do |t|
25
- t.test_files = FileList['test/**/*.rb']
24
+ desc 'Run specs'
25
+ task :test => :spec
26
+
27
+ Spec::Rake::SpecTask.new do |t|
28
+ t.spec_files = FileList['spec/**/*.rb']
26
29
  end
@@ -8,7 +8,7 @@ module Peanuts
8
8
  # numeric:: see +Convert_integer+, +Convert_decimal+, +Convert_float+
9
9
  # date & time:: see +Convert_datetime+
10
10
  # lists:: see +Convert_list+
11
- module Converter
11
+ class Converter
12
12
  def self.lookup(type)
13
13
  lookup!(type)
14
14
  rescue ArgumentError
@@ -16,9 +16,11 @@ module Peanuts
16
16
  end
17
17
 
18
18
  def self.lookup!(type)
19
- const_get("Convert_#{type}")
20
- rescue NameError
21
- raise ArgumentError, "converter not found for #{type}"
19
+ begin
20
+ const_get("Convert_#{type}")
21
+ rescue NameError
22
+ raise ArgumentError, "converter not found for #{type}"
23
+ end
22
24
  end
23
25
 
24
26
  def self.create(type, options)
@@ -28,11 +30,16 @@ module Peanuts
28
30
  end
29
31
 
30
32
  def self.create!(type, options)
31
- lookup!(type).new(options)
33
+ case type
34
+ when Symbol
35
+ lookup!(type)
36
+ else
37
+ type
38
+ end.new(options)
32
39
  end
33
40
 
34
41
  # Who could have thought... a string.
35
- #
42
+ #
36
43
  # Specifier:: <tt>:string</tt>
37
44
  #
38
45
  # ==== Options:
@@ -41,7 +48,7 @@ module Peanuts
41
48
  # [<tt>:trim</tt>] Trim whitespace from both ends.
42
49
  # [<tt>:collapse</tt>] Collapse consecutive whitespace + trim as well.
43
50
  # [<tt>:preserve</tt>] Keep'em all.
44
- class Convert_string
51
+ class Convert_string < Converter
45
52
  def initialize(options)
46
53
  @whitespace = options[:whitespace] || :collapse
47
54
  end
@@ -83,8 +90,8 @@ module Peanuts
83
90
  def to_xml(flag)
84
91
  return nil if flag.nil?
85
92
  string = case @format
86
- when :true_false then flag ? 'true' : 'false'
87
- when :yes_no then flag ? 'yes' : 'no'
93
+ when :true_false, :truefalse then flag ? 'true' : 'false'
94
+ when :yes_no, :yesno then flag ? 'yes' : 'no'
88
95
  when :numeric then flag ? '0' : '1'
89
96
  end
90
97
  super(string)
@@ -114,7 +121,7 @@ module Peanuts
114
121
  # An integer.
115
122
  #
116
123
  # Specifier:: <tt>:integer</tt>
117
- #
124
+ #
118
125
  # ==== Options
119
126
  # Accepts all options of +Convert_string+.
120
127
  class Convert_integer < Convert_string
@@ -135,7 +142,7 @@ module Peanuts
135
142
  #
136
143
  # Specifier:: <tt>:decimal</tt>
137
144
  # Ruby type:: +BigDecimal+
138
- #
145
+ #
139
146
  # ==== Options
140
147
  # Accepts all options of +Convert_string+.
141
148
  class Convert_decimal < Convert_string
@@ -180,7 +187,7 @@ module Peanuts
180
187
  #
181
188
  # Specifier:: <tt>:datetime</tt>
182
189
  # Ruby type:: +Time+
183
- #
190
+ #
184
191
  # ==== Options
185
192
  # Accepts all options of +Convert_string+.
186
193
  class Convert_datetime < Convert_string
@@ -206,7 +213,7 @@ module Peanuts
206
213
  #
207
214
  # ==== Options
208
215
  # All options will be passed to the underlying type converter.
209
- class Convert_list
216
+ class Convert_list < Converter
210
217
  def initialize(options)
211
218
  @item_type = options[:item_type] || :string
212
219
  @item_converter = Converter.create!(@item_type, options)
@@ -0,0 +1,253 @@
1
+ require 'peanuts/xml'
2
+ require 'peanuts/mappings'
3
+ require 'peanuts/mapper'
4
+
5
+ module Peanuts #:nodoc:
6
+ # See also +MappableType+
7
+ module MappableObject
8
+ def self.included(other) #:nodoc:
9
+ MappableType.init(other)
10
+ end
11
+
12
+ def from_xml(source, options = {})
13
+ source = XML::Reader.new(source, options) unless source.is_a?(XML::Reader)
14
+ e = source.find_element
15
+ e && self.class.mapper.read(self, source)
16
+ end
17
+
18
+ # save_to(:string|:document[, options]) -> new_string|new_document
19
+ # save_to(string|iolike|document[, options]) -> string|iolike|document
20
+ #
21
+ # Defines attribute mapping.
22
+ #
23
+ # [+options+] Backend-specific options
24
+ #
25
+ # === Example:
26
+ # cat = Cat.new
27
+ # cat.name = 'Pussy'
28
+ # puts cat.save_to(:string)
29
+ # ...
30
+ # doc = LibXML::XML::Document.new
31
+ # cat.save_to(doc)
32
+ # puts doc.to_s
33
+ def to_xml(dest = :string, options = {})
34
+ dest = XML::Writer.new(dest, options) unless dest.is_a?(XML::Writer)
35
+ self.class.mapper.write(self, dest)
36
+ dest.result
37
+ end
38
+ end
39
+
40
+ # See also +MappableObject+.
41
+ module MappableType
42
+ include Mappings
43
+
44
+ def self.init(cls, ns_context = nil, default_ns = nil, &block) #:nodoc:
45
+ cls.instance_eval do
46
+ extend MappableType
47
+ @mapper = Mapper.new(ns_context, default_ns)
48
+ instance_eval(&block) if block_given?
49
+ end
50
+ cls
51
+ end
52
+
53
+ # mapper -> Mapper
54
+ #
55
+ # Returns the mapper for the class.
56
+ attr_reader :mapper
57
+
58
+ # namespaces(hash) -> Hash
59
+ # namespaces -> Hash
60
+ #
61
+ # Updates and returns class-level prefix mappings.
62
+ # When given a hash of mappings merges it over current.
63
+ # When called withot arguments simply returns current mappings.
64
+ #
65
+ # === Example:
66
+ # class Cat
67
+ # include Peanuts
68
+ # namespaces :lol => 'urn:lol', ...
69
+ # ...
70
+ # end
71
+ def namespaces(*args)
72
+ case args.size
73
+ when 0
74
+ mapper.namespaces
75
+ when 1
76
+ if args.first.is_a?(Hash)
77
+ mapper.namespaces.update(args.first)
78
+ else
79
+ mapper.default_ns = args.first
80
+ end
81
+ when 2
82
+ mapper.default_ns = args.first
83
+ mapper.namespaces.update(args[1])
84
+ else
85
+ raise ArgumentError, 'bad arguments'
86
+ end
87
+ end
88
+
89
+ # root(local_name[, :ns => ...]) -> Mappings::Root
90
+ # root -> Mappings::Root
91
+ #
92
+ # Defines element name.
93
+ # TODO: moar details
94
+ #
95
+ # [+local_name+] Element name
96
+ # [+options+] <tt>:ns => 'uri'|:prefix</tt> Element namespace
97
+ #
98
+ # === Example:
99
+ # class Cat
100
+ # include Peanuts
101
+ # ...
102
+ # root :kitteh, :ns => 'urn:lol'
103
+ # ...
104
+ # end
105
+ def root(local_name = nil, options = {})
106
+ mapper.root = Root.new(local_name, prepare_options(:root, options)) if local_name
107
+ mapper.root
108
+ end
109
+
110
+ # element(name[, type][, options]) -> mapping_object
111
+ # element(name[, options]) { block } -> mapping_object
112
+ #
113
+ # Defines single-element mapping.
114
+ #
115
+ # [+name+] Accessor name
116
+ # [+type+] Element type. <tt>:string</tt> assumed if omitted (see +Converter+).
117
+ # [+options+] <tt>name</tt>, <tt>:ns</tt>, converter options (see +Converter+).
118
+ # [+block+] An anonymous class definition.
119
+ #
120
+ # === Example:
121
+ # class Cat
122
+ # include Peanuts
123
+ # ...
124
+ # element :name, :whitespace => :collapse
125
+ # element :ears, :integer
126
+ # element :cheeseburger, Cheeseburger, :name => :cheezburger
127
+ # ...
128
+ # end
129
+ def element(name, *args, &block)
130
+ add_mapping(:element, name, *args, &block)
131
+ end
132
+
133
+ # shallow_element(name, type[, options]) -> mapping_object
134
+ # shallow_element(name[, options]) { block } -> mapping_object
135
+ #
136
+ # Defines single-element shallow mapping.
137
+ #
138
+ # [+name+] Accessor name
139
+ # [+type+] Element type. Either this or _block_ is required.
140
+ # [+options+] <tt>:name</tt>, <tt>:ns</tt>, converter options (see +Converter+).
141
+ # [+block+] An anonymous class definition.
142
+ #
143
+ # === Example:
144
+ # class Cat
145
+ # include Peanuts
146
+ # ...
147
+ # shallow :friends do
148
+ # element :friends, :name => :friend
149
+ # end
150
+ # shallow :cheeseburger, Cheeseburger, :name => :cheezburger
151
+ # ...
152
+ # end
153
+ def shallow_element(name, *args, &block)
154
+ add_mapping(:shallow_element, name, *args, &block)
155
+ end
156
+
157
+ alias shallow shallow_element
158
+
159
+ # elements(name[, type][, options]) -> mapping_object
160
+ # elements(name[, options]) { block } -> mapping_object
161
+ #
162
+ # Defines multiple elements mapping.
163
+ #
164
+ # [+name+] Accessor name
165
+ # [+type+] Element type. <tt>:string</tt> assumed if omitted (see +Converter+).
166
+ # [+options+] <tt>name</tt>, <tt>:ns</tt>, converter options (see +Converter+).
167
+ # [+block+] An anonymous class definition.
168
+ #
169
+ # === Example:
170
+ # class RichCat
171
+ # include Peanuts
172
+ # ...
173
+ # elements :ration, :string, :whitespace => :collapse
174
+ # elements :cheeseburgers, Cheeseburger, :name => :cheezburger
175
+ # ...
176
+ # end
177
+ def elements(name, *args, &block)
178
+ add_mapping(:elements, name, *args, &block)
179
+ end
180
+
181
+ # attribute(name[, type][, options]) -> mapping_object
182
+ #
183
+ # Defines attribute mapping.
184
+ #
185
+ # [+name+] Accessor name
186
+ # [+type+] Element type. <tt>:string</tt> assumed if omitted (see +Converter+).
187
+ # [+options+] <tt>name</tt>, <tt>:ns</tt>, converter options (see +Converter+).
188
+ #
189
+ # === Example:
190
+ # class Cat
191
+ # include Peanuts
192
+ # ...
193
+ # element :name, :string, :whitespace => :collapse
194
+ # element :cheeseburger, Cheeseburger, :name => :cheezburger
195
+ # ...
196
+ # end
197
+ def attribute(name, *args)
198
+ add_mapping(:attribute, name, *args)
199
+ end
200
+
201
+ def schema(schema = nil)
202
+ mapper.schema = schema if schema
203
+ mapper.schema
204
+ end
205
+
206
+ def from_xml(source, options = {})
207
+ new.from_xml(source, options)
208
+ end
209
+
210
+ private
211
+ def object_type?(type)
212
+ type.is_a?(Class) && !(type < Converter)
213
+ end
214
+
215
+ def add_mapping(node, name, *args, &block)
216
+ type, options = *args
217
+ type, options = (block ? Class.new : :string), type if type.nil? || type.is_a?(Hash)
218
+
219
+ object_type = object_type?(type)
220
+ options = prepare_options(node, options || {})
221
+
222
+ mapper << m = case node
223
+ when :element
224
+ options.delete(:shallow) ? ShallowElement : (object_type ? Element : ElementValue)
225
+ when :elements
226
+ object_type ? Elements : ElementValues
227
+ when :attribute
228
+ Attribute
229
+ when :shallow_element
230
+ ShallowElement
231
+ end.new(name, type, options)
232
+
233
+ raise ArgumentError, 'bad type for shallow element' if !object_type && m.is_a?(ShallowElement)
234
+
235
+ default_ns = m.prefix ? mapper.default_ns : m.namespace_uri
236
+ if object_type && !type.is_a?(MappableType)
237
+ raise ArgumentError, 'block is required' unless block
238
+ MappableType.init(type, mapper.namespaces, default_ns, &block)
239
+ end
240
+ m.define_accessors(self)
241
+ m
242
+ end
243
+
244
+ def prepare_options(node, options)
245
+ ns = options.fetch(:ns) {|k| node == :attribute ? nil : options[k] = mapper.default_ns }
246
+ if ns.is_a?(Symbol)
247
+ raise ArgumentError, "undefined prefix: #{ns}" unless options[:ns] = mapper.namespaces[ns]
248
+ options[:prefix] = ns unless options.include?(:prefix)
249
+ end
250
+ options
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,95 @@
1
+ require 'enumerator'
2
+
3
+ module Peanuts
4
+ class Mapper
5
+ include Enumerable
6
+
7
+ attr_reader :root, :namespaces, :ns_context
8
+ attr_accessor :default_ns, :schema
9
+
10
+ def initialize(ns_context = nil, default_ns = nil)
11
+ @ns_context, @default_ns = ns_context, default_ns
12
+ @mappings, @footprints = [], {}
13
+ @namespaces = ns_context ? Hash.new {|h, k| ns_context[k] || raise(IndexError) } : {}
14
+ end
15
+
16
+ def root=(root)
17
+ raise 'root already defined' if @root
18
+ # TODO raise 'root in nested scopes not supported' if nested?
19
+ @default_ns = root.namespace_uri unless root.prefix
20
+ @root = root
21
+ end
22
+
23
+ def each(&block)
24
+ @mappings.each(&block)
25
+ end
26
+
27
+ def <<(mapping)
28
+ fp = MappingFootprint.new(mapping)
29
+ raise "mapping already defined for #{fp}" if @footprints.include?(fp)
30
+ @mappings << (@footprints[fp] = mapping)
31
+ end
32
+
33
+ def define_accessors(type)
34
+ each {|m| m.define_accessors(type) }
35
+ end
36
+
37
+ def read(nut, reader)
38
+ rdfp = ReaderFootprint.new(reader)
39
+ reader.each do
40
+ m = @footprints[rdfp]
41
+ m.read(nut, reader) if m
42
+ end
43
+ nut
44
+ end
45
+
46
+ def write(nut, writer)
47
+ @root.write(writer) do |w|
48
+ w.write_namespaces('' => default_ns) if default_ns
49
+ w.write_namespaces(namespaces)
50
+ write_children(nut, w)
51
+ end
52
+ nil
53
+ end
54
+
55
+ def write_children(nut, writer)
56
+ each {|m| m.write(nut, writer) } if nut
57
+ nil
58
+ end
59
+
60
+ def clear(nut)
61
+ each {|m| m.clear(nut) }
62
+ end
63
+
64
+ private
65
+ class Footprint #:nodoc:
66
+ extend Forwardable
67
+
68
+ def_delegators :@obj, :node_type, :local_name, :namespace_uri
69
+
70
+ def initialize(obj)
71
+ @obj = obj
72
+ end
73
+
74
+ def hash
75
+ node_type.hash ^ local_name.hash ^ namespace_uri.hash
76
+ end
77
+
78
+ def to_s
79
+ "#{node_type}(#{local_name}, #{namespace_uri})"
80
+ end
81
+ end
82
+
83
+ class MappingFootprint < Footprint #:nodoc:
84
+ def eql?(other)
85
+ self.equal?(other) || other && @obj.matches?(other)
86
+ end
87
+ end
88
+
89
+ class ReaderFootprint < Footprint #:nodoc:
90
+ def eql?(mappingfp)
91
+ mappingfp.eql?(@obj)
92
+ end
93
+ end
94
+ end
95
+ end