peanuts 1.0 → 2.0.7

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