georgepalmer-couch_foo 0.7.4 → 0.7.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/README.rdoc CHANGED
@@ -18,7 +18,7 @@ are a few minor differences to the way CouchDB works. In particular:
18
18
  * The price of index updating is paid when next accessing the index rather than the point of insertion. This can be more efficient or less depending on your application. It may make sense to use an external process to do the updating for you - see CouchFoo#find for more on this
19
19
  * On that note, occasional compacting of CouchDB is required to recover space from old versions of documents. This can be kicked off in several ways (see quick start guide)
20
20
 
21
- It is recommend that you read the quick start and performance sections in the rdoc for a full overview of differences and points to be aware of when developing.
21
+ It is recommend that you read the quick start and performance sections in the rdoc for a full overview of differences and points to be aware of when developing. The Changelog file shows the differences between gem versions and should be checked when upgrading gem versions.
22
22
 
23
23
 
24
24
  == Getting started
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
3
  :minor: 7
4
- :patch: 4
4
+ :patch: 7
data/lib/couch_foo.rb CHANGED
@@ -20,7 +20,8 @@ require 'couch_foo/associations'
20
20
  #require 'active_record/association_preload'
21
21
  #require 'active_record/aggregations'
22
22
  require 'couch_foo/timestamp'
23
- require 'active_record/calculations'
23
+ require 'couch_foo/calculations'
24
+ require 'couch_foo/serialization'
24
25
  require 'couch_foo/attribute_methods'
25
26
  require 'couch_foo/dirty'
26
27
 
@@ -40,4 +41,5 @@ CouchFoo::Base.class_eval do
40
41
  # include ActiveRecord::Aggregations
41
42
  include CouchFoo::Reflection
42
43
  include CouchFoo::Calculations
44
+ include CouchFoo::Serialization
43
45
  end
@@ -11,6 +11,7 @@ module CouchFoo
11
11
  end
12
12
 
13
13
  module ClassMethods
14
+
14
15
  # Declares a method available for all attributes with the given suffix.
15
16
  # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method
16
17
  #
@@ -32,6 +32,12 @@ module CouchFoo
32
32
  class DocumentConflict < CouchFooError
33
33
  end
34
34
 
35
+ # The types that are permitted for properties. At the moment this is just used
36
+ # to determine whether a .to_xml call should be made on the type during
37
+ # serialization but I imagine it will be used to enforce type checking as well
38
+ # at a later date
39
+ AVAILABLE_TYPES = [String, Integer, Float, DateTime, Time, Date, TrueClass, Boolean]
40
+
35
41
  # Simple class encapsulating a property
36
42
  class Property
37
43
  attr_accessor :name, :type, :default
@@ -1027,7 +1033,7 @@ module CouchFoo
1027
1033
  define_attr_method :inheritance_column, value, &block
1028
1034
  end
1029
1035
  alias :inheritance_column= :set_inheritance_column
1030
-
1036
+
1031
1037
  # Set a property for the document. These can be passed a type and options hash. If no type
1032
1038
  # is passed a #to_json method is called on the ruby object and the result stored in the
1033
1039
  # database. When it is retrieved from the database a class.from_json(json) method is called
@@ -0,0 +1,98 @@
1
+ module CouchFoo #:nodoc:
2
+ module Serialization
3
+ class Serializer #:nodoc:
4
+ attr_reader :options
5
+
6
+ def initialize(record, options = {})
7
+ @record, @options = record, options.dup
8
+ end
9
+
10
+ # To replicate the behavior in CouchFoo#attributes,
11
+ # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
12
+ # for a N level model but is set for the N+1 level models,
13
+ # then because <tt>:except</tt> is set to a default value, the second
14
+ # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
15
+ # <tt>:only</tt> is set, always delete <tt>:except</tt>.
16
+ def serializable_attribute_names
17
+ attribute_names = @record.attribute_names
18
+
19
+ if options[:only]
20
+ options.delete(:except)
21
+ attribute_names = attribute_names & Array(options[:only]).collect { |n| n.to_s }
22
+ else
23
+ options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column)
24
+ attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
25
+ end
26
+
27
+ attribute_names
28
+ end
29
+
30
+ def serializable_method_names
31
+ Array(options[:methods]).inject([]) do |method_attributes, name|
32
+ method_attributes << name if @record.respond_to?(name.to_s)
33
+ method_attributes
34
+ end
35
+ end
36
+
37
+ def serializable_names
38
+ serializable_attribute_names + serializable_method_names
39
+ end
40
+
41
+ # Add associations specified via the <tt>:includes</tt> option.
42
+ # Expects a block that takes as arguments:
43
+ # +association+ - name of the association
44
+ # +records+ - the association record(s) to be serialized
45
+ # +opts+ - options for the association records
46
+ def add_includes(&block)
47
+ if include_associations = options.delete(:include)
48
+ base_only_or_except = { :except => options[:except],
49
+ :only => options[:only] }
50
+
51
+ include_has_options = include_associations.is_a?(Hash)
52
+ associations = include_has_options ? include_associations.keys : Array(include_associations)
53
+
54
+ for association in associations
55
+ records = case @record.class.reflect_on_association(association).macro
56
+ when :has_many, :has_and_belongs_to_many
57
+ @record.send(association).to_a
58
+ when :has_one, :belongs_to
59
+ @record.send(association)
60
+ end
61
+
62
+ unless records.nil?
63
+ association_options = include_has_options ? include_associations[association] : base_only_or_except
64
+ opts = options.merge(association_options)
65
+ yield(association, records, opts)
66
+ end
67
+ end
68
+
69
+ options[:include] = include_associations
70
+ end
71
+ end
72
+
73
+ def serializable_record
74
+ returning(serializable_record = {}) do
75
+ serializable_names.each { |name| serializable_record[name] = @record.send(:read_attribute_before_type_cast, name) }
76
+ add_includes do |association, records, opts|
77
+ if records.is_a?(Enumerable)
78
+ serializable_record[association] = records.collect { |r| self.class.new(r, opts).serializable_record }
79
+ else
80
+ serializable_record[association] = self.class.new(records, opts).serializable_record
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def serialize
87
+ # overwrite to implement
88
+ end
89
+
90
+ def to_s(&block)
91
+ serialize(&block)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ require 'couch_foo/serializers/xml_serializer'
98
+ require 'couch_foo/serializers/json_serializer'
@@ -0,0 +1,81 @@
1
+ module CouchFoo #:nodoc:
2
+ module Serialization
3
+ def self.included(base)
4
+ base.cattr_accessor :include_root_in_json, :instance_writer => false
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # Returns a JSON string representing the model. Some configuration is
9
+ # available through +options+.
10
+ #
11
+ # Without any +options+, the returned JSON string will include all
12
+ # the model's attributes. For example:
13
+ #
14
+ # konata = User.find(1)
15
+ # konata.to_json
16
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
17
+ # "created_at": "2006/08/01", "awesome": true}
18
+ #
19
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
20
+ # included, and work similar to the +attributes+ method. For example:
21
+ #
22
+ # konata.to_json(:only => [ :id, :name ])
23
+ # # => {"id": 1, "name": "Konata Izumi"}
24
+ #
25
+ # konata.to_json(:except => [ :id, :created_at, :age ])
26
+ # # => {"name": "Konata Izumi", "awesome": true}
27
+ #
28
+ # To include any methods on the model, use <tt>:methods</tt>.
29
+ #
30
+ # konata.to_json(:methods => :permalink)
31
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
32
+ # "created_at": "2006/08/01", "awesome": true,
33
+ # "permalink": "1-konata-izumi"}
34
+ #
35
+ # To include associations, use <tt>:include</tt>.
36
+ #
37
+ # konata.to_json(:include => :posts)
38
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
39
+ # "created_at": "2006/08/01", "awesome": true,
40
+ # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
41
+ # {"id": 2, author_id: 1, "title": "So I was thinking"}]}
42
+ #
43
+ # 2nd level and higher order associations work as well:
44
+ #
45
+ # konata.to_json(:include => { :posts => {
46
+ # :include => { :comments => {
47
+ # :only => :body } },
48
+ # :only => :title } })
49
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
50
+ # "created_at": "2006/08/01", "awesome": true,
51
+ # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
52
+ # "title": "Welcome to the weblog"},
53
+ # {"comments": [{"body": "Don't think too hard"}],
54
+ # "title": "So I was thinking"}]}
55
+ def to_json(options = {})
56
+ if include_root_in_json
57
+ "{#{self.class.json_class_name}: #{JsonSerializer.new(self, options).to_s}}"
58
+ else
59
+ JsonSerializer.new(self, options).to_s
60
+ end
61
+ end
62
+
63
+ def from_json(json)
64
+ self.attributes = ActiveSupport::JSON.decode(json)
65
+ self
66
+ end
67
+
68
+ class JsonSerializer < CouchFoo::Serialization::Serializer #:nodoc:
69
+ def serialize
70
+ puts serializable_record.inspect
71
+ serializable_record.to_json
72
+ end
73
+ end
74
+
75
+ module ClassMethods
76
+ def json_class_name
77
+ @json_class_name ||= name.demodulize.underscore.inspect
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,347 @@
1
+ module CouchFoo #:nodoc:
2
+ module Serialization
3
+ # Builds an XML document to represent the model. Some configuration is
4
+ # available through +options+. However more complicated cases should
5
+ # override CouchFoo::Base#to_xml. This is a necessary step if you wish
6
+ # to use custom types
7
+ #
8
+ # By default the generated XML document will include the processing
9
+ # instruction and all the object's attributes. For example:
10
+ #
11
+ # <?xml version="1.0" encoding="UTF-8"?>
12
+ # <topic>
13
+ # <title>The First Topic</title>
14
+ # <author-name>David</author-name>
15
+ # <id type="integer">1</id>
16
+ # <approved type="boolean">false</approved>
17
+ # <replies-count type="integer">0</replies-count>
18
+ # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
19
+ # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
20
+ # <content>Have a nice day</content>
21
+ # <author-email-address>david@loudthinking.com</author-email-address>
22
+ # <parent-id></parent-id>
23
+ # <last-read type="date">2004-04-15</last-read>
24
+ # </topic>
25
+ #
26
+ # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
27
+ # <tt>:skip_instruct</tt>, <tt>:skip_types</tt> and <tt>:dasherize</tt>.
28
+ # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
29
+ # +attributes+ method. The default is to dasherize all column names, but you
30
+ # can disable this setting <tt>:dasherize</tt> to +false+. To not have the
31
+ # column type included in the XML output set <tt>:skip_types</tt> to +true+.
32
+ #
33
+ # For instance:
34
+ #
35
+ # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
36
+ #
37
+ # <topic>
38
+ # <title>The First Topic</title>
39
+ # <author-name>David</author-name>
40
+ # <approved type="boolean">false</approved>
41
+ # <content>Have a nice day</content>
42
+ # <author-email-address>david@loudthinking.com</author-email-address>
43
+ # <parent-id></parent-id>
44
+ # <last-read type="date">2004-04-15</last-read>
45
+ # </topic>
46
+ #
47
+ # To include first level associations use <tt>:include</tt>:
48
+ #
49
+ # firm.to_xml :include => [ :account, :clients ]
50
+ #
51
+ # <?xml version="1.0" encoding="UTF-8"?>
52
+ # <firm>
53
+ # <id type="integer">1</id>
54
+ # <rating type="integer">1</rating>
55
+ # <name>37signals</name>
56
+ # <clients type="array">
57
+ # <client>
58
+ # <rating type="integer">1</rating>
59
+ # <name>Summit</name>
60
+ # </client>
61
+ # <client>
62
+ # <rating type="integer">1</rating>
63
+ # <name>Microsoft</name>
64
+ # </client>
65
+ # </clients>
66
+ # <account>
67
+ # <id type="integer">1</id>
68
+ # <credit-limit type="integer">50</credit-limit>
69
+ # </account>
70
+ # </firm>
71
+ #
72
+ # To include deeper levels of associations pass a hash like this:
73
+ #
74
+ # firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
75
+ # <?xml version="1.0" encoding="UTF-8"?>
76
+ # <firm>
77
+ # <id type="integer">1</id>
78
+ # <rating type="integer">1</rating>
79
+ # <name>37signals</name>
80
+ # <clients type="array">
81
+ # <client>
82
+ # <rating type="integer">1</rating>
83
+ # <name>Summit</name>
84
+ # <address>
85
+ # ...
86
+ # </address>
87
+ # </client>
88
+ # <client>
89
+ # <rating type="integer">1</rating>
90
+ # <name>Microsoft</name>
91
+ # <address>
92
+ # ...
93
+ # </address>
94
+ # </client>
95
+ # </clients>
96
+ # <account>
97
+ # <id type="integer">1</id>
98
+ # <credit-limit type="integer">50</credit-limit>
99
+ # </account>
100
+ # </firm>
101
+ #
102
+ # To include any methods on the model being called use <tt>:methods</tt>:
103
+ #
104
+ # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
105
+ #
106
+ # <firm>
107
+ # # ... normal attributes as shown above ...
108
+ # <calculated-earnings>100000000000000000</calculated-earnings>
109
+ # <real-earnings>5</real-earnings>
110
+ # </firm>
111
+ #
112
+ # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
113
+ # modified version of the options hash that was given to +to_xml+:
114
+ #
115
+ # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
116
+ # firm.to_xml :procs => [ proc ]
117
+ #
118
+ # <firm>
119
+ # # ... normal attributes as shown above ...
120
+ # <abc>def</abc>
121
+ # </firm>
122
+ #
123
+ # Alternatively, you can yield the builder object as part of the +to_xml+ call:
124
+ #
125
+ # firm.to_xml do |xml|
126
+ # xml.creator do
127
+ # xml.first_name "David"
128
+ # xml.last_name "Heinemeier Hansson"
129
+ # end
130
+ # end
131
+ #
132
+ # <firm>
133
+ # # ... normal attributes as shown above ...
134
+ # <creator>
135
+ # <first_name>David</first_name>
136
+ # <last_name>Heinemeier Hansson</last_name>
137
+ # </creator>
138
+ # </firm>
139
+ #
140
+ # As noted above, you may override +to_xml+ in your CouchFoo::Base
141
+ # subclasses to have complete control about what's generated. The general
142
+ # form of doing this is:
143
+ #
144
+ # class IHaveMyOwnXML < CouchFoo::Base
145
+ # def to_xml(options = {})
146
+ # options[:indent] ||= 2
147
+ # xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
148
+ # xml.instruct! unless options[:skip_instruct]
149
+ # xml.level_one do
150
+ # xml.tag!(:second_level, 'content')
151
+ # end
152
+ # end
153
+ # end
154
+ def to_xml(options = {}, &block)
155
+ serializer = XmlSerializer.new(self, options)
156
+ block_given? ? serializer.to_s(&block) : serializer.to_s
157
+ end
158
+
159
+ def from_xml(xml)
160
+ self.attributes = Hash.from_xml(xml).values.first
161
+ self
162
+ end
163
+ end
164
+
165
+ class XmlSerializer < CouchFoo::Serialization::Serializer #:nodoc:
166
+ def builder
167
+ @builder ||= begin
168
+ options[:indent] ||= 2
169
+ builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
170
+
171
+ unless options[:skip_instruct]
172
+ builder.instruct!
173
+ options[:skip_instruct] = true
174
+ end
175
+
176
+ builder
177
+ end
178
+ end
179
+
180
+ def root
181
+ root = (options[:root] || @record.class.to_s.underscore).to_s
182
+ dasherize? ? root.dasherize : root
183
+ end
184
+
185
+ def dasherize?
186
+ !options.has_key?(:dasherize) || options[:dasherize]
187
+ end
188
+
189
+ def serializable_attributes
190
+ serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
191
+ end
192
+
193
+ def serializable_method_attributes
194
+ Array(options[:methods]).inject([]) do |method_attributes, name|
195
+ method_attributes << MethodAttribute.new(name.to_s, @record) if @record.respond_to?(name.to_s)
196
+ method_attributes
197
+ end
198
+ end
199
+
200
+ def add_attributes
201
+ (serializable_attributes + serializable_method_attributes).each do |attribute|
202
+ add_tag(attribute)
203
+ end
204
+ end
205
+
206
+ def add_procs
207
+ if procs = options.delete(:procs)
208
+ [ *procs ].each do |proc|
209
+ proc.call(options)
210
+ end
211
+ end
212
+ end
213
+
214
+ def add_tag(attribute)
215
+ builder.tag!(
216
+ dasherize? ? attribute.name.dasherize : attribute.name,
217
+ attribute.value.to_s,
218
+ attribute.decorations(!options[:skip_types])
219
+ )
220
+ end
221
+
222
+ def add_associations(association, records, opts)
223
+ if records.is_a?(Enumerable)
224
+ tag = association.to_s
225
+ tag = tag.dasherize if dasherize?
226
+ if records.empty?
227
+ builder.tag!(tag, :type => :array)
228
+ else
229
+ builder.tag!(tag, :type => :array) do
230
+ association_name = association.to_s.singularize
231
+ records.each do |record|
232
+ record.to_xml opts.merge(
233
+ :root => association_name,
234
+ :type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
235
+ )
236
+ end
237
+ end
238
+ end
239
+ else
240
+ if record = @record.send(association)
241
+ record.to_xml(opts.merge(:root => association))
242
+ end
243
+ end
244
+ end
245
+
246
+ def serialize
247
+ args = [root]
248
+ if options[:namespace]
249
+ args << {:xmlns=>options[:namespace]}
250
+ end
251
+
252
+ if options[:type]
253
+ args << {:type=>options[:type]}
254
+ end
255
+
256
+ builder.tag!(*args) do
257
+ add_attributes
258
+ procs = options.delete(:procs)
259
+ add_includes { |association, records, opts| add_associations(association, records, opts) }
260
+ options[:procs] = procs
261
+ add_procs
262
+ yield builder if block_given?
263
+ end
264
+ end
265
+
266
+ class Attribute #:nodoc:
267
+ attr_reader :name, :value, :type
268
+
269
+ def initialize(name, record)
270
+ @name, @record = name, record
271
+
272
+ @type = compute_type
273
+ @value = compute_value
274
+ end
275
+
276
+ # There is a significant speed improvement if the value
277
+ # does not need to be escaped, as <tt>tag!</tt> escapes all values
278
+ # to ensure that valid XML is generated. For known binary
279
+ # values, it is at least an order of magnitude faster to
280
+ # Base64 encode binary values and directly put them in the
281
+ # output XML than to pass the original value or the Base64
282
+ # encoded value to the <tt>tag!</tt> method. It definitely makes
283
+ # no sense to Base64 encode the value and then give it to
284
+ # <tt>tag!</tt>, since that just adds additional overhead.
285
+ def needs_encoding?
286
+ ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
287
+ end
288
+
289
+ def decorations(include_types = true)
290
+ decorations = {}
291
+
292
+ if type == :binary
293
+ decorations[:encoding] = 'base64'
294
+ end
295
+
296
+ if include_types && type != :string
297
+ decorations[:type] = type
298
+ end
299
+
300
+ if value.nil?
301
+ decorations[:nil] = true
302
+ end
303
+
304
+ decorations
305
+ end
306
+
307
+ protected
308
+ def compute_type
309
+ type = @record.class.property_types[name.to_sym]
310
+
311
+ # Hack until get these types into properties structure
312
+ if name.to_sym == :_id || name.to_sym == :_rev || name.to_sym == :ruby_class
313
+ type = String
314
+ end
315
+
316
+ case type
317
+ when Time
318
+ Datetime
319
+ when Boolean
320
+ TrueClass
321
+ else
322
+ type
323
+ end
324
+ end
325
+
326
+ def compute_value
327
+ value = @record.send(name)
328
+
329
+ # Custom types must implement .to_xml
330
+ if !CouchFoo::AVAILABLE_TYPES.include?(type)
331
+ value.to_xml unless value.nil?
332
+ elsif formatter = Hash::XML_FORMATTING[type.to_s]
333
+ value ? formatter.call(value) : nil
334
+ else
335
+ value
336
+ end
337
+ end
338
+ end
339
+
340
+ class MethodAttribute < Attribute #:nodoc:
341
+ protected
342
+ def compute_type
343
+ Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string
344
+ end
345
+ end
346
+ end
347
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: georgepalmer-couch_foo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - George Palmer
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-06 00:00:00 -08:00
12
+ date: 2009-02-09 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -64,6 +64,7 @@ files:
64
64
  - lib/couch_foo
65
65
  - lib/couch_foo/database.rb
66
66
  - lib/couch_foo/dirty.rb
67
+ - lib/couch_foo/serialization.rb
67
68
  - lib/couch_foo/associations.rb
68
69
  - lib/couch_foo/associations
69
70
  - lib/couch_foo/associations/has_one_association.rb
@@ -75,6 +76,9 @@ files:
75
76
  - lib/couch_foo/associations/has_and_belongs_to_many_association.rb
76
77
  - lib/couch_foo/observer.rb
77
78
  - lib/couch_foo/base.rb
79
+ - lib/couch_foo/serializers
80
+ - lib/couch_foo/serializers/xml_serializer.rb
81
+ - lib/couch_foo/serializers/json_serializer.rb
78
82
  - lib/couch_foo/calculations.rb
79
83
  - lib/couch_foo/view_methods.rb
80
84
  - lib/couch_foo/attribute_methods.rb