sequel 3.12.1 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (150) hide show
  1. data/CHANGELOG +42 -0
  2. data/README.rdoc +137 -118
  3. data/Rakefile +21 -66
  4. data/doc/active_record.rdoc +9 -9
  5. data/doc/advanced_associations.rdoc +59 -188
  6. data/doc/association_basics.rdoc +15 -2
  7. data/doc/cheat_sheet.rdoc +38 -33
  8. data/doc/dataset_filtering.rdoc +16 -7
  9. data/doc/prepared_statements.rdoc +7 -7
  10. data/doc/querying.rdoc +5 -4
  11. data/doc/release_notes/3.13.0.txt +210 -0
  12. data/doc/sharding.rdoc +1 -1
  13. data/doc/sql.rdoc +5 -5
  14. data/doc/validations.rdoc +11 -11
  15. data/lib/sequel/adapters/ado.rb +1 -1
  16. data/lib/sequel/adapters/do.rb +3 -3
  17. data/lib/sequel/adapters/firebird.rb +3 -3
  18. data/lib/sequel/adapters/jdbc/h2.rb +39 -0
  19. data/lib/sequel/adapters/jdbc/mysql.rb +5 -0
  20. data/lib/sequel/adapters/jdbc/oracle.rb +3 -3
  21. data/lib/sequel/adapters/mysql.rb +7 -4
  22. data/lib/sequel/adapters/oracle.rb +3 -3
  23. data/lib/sequel/adapters/shared/mssql.rb +10 -1
  24. data/lib/sequel/adapters/shared/mysql.rb +63 -0
  25. data/lib/sequel/adapters/shared/postgres.rb +61 -3
  26. data/lib/sequel/adapters/sqlite.rb +105 -18
  27. data/lib/sequel/connection_pool.rb +31 -30
  28. data/lib/sequel/core.rb +58 -58
  29. data/lib/sequel/core_sql.rb +52 -43
  30. data/lib/sequel/database/misc.rb +11 -0
  31. data/lib/sequel/database/query.rb +55 -17
  32. data/lib/sequel/dataset/actions.rb +2 -1
  33. data/lib/sequel/dataset/query.rb +2 -3
  34. data/lib/sequel/dataset/sql.rb +24 -11
  35. data/lib/sequel/extensions/schema_dumper.rb +1 -1
  36. data/lib/sequel/metaprogramming.rb +4 -0
  37. data/lib/sequel/model.rb +37 -19
  38. data/lib/sequel/model/associations.rb +33 -25
  39. data/lib/sequel/model/base.rb +2 -2
  40. data/lib/sequel/model/plugins.rb +7 -2
  41. data/lib/sequel/plugins/active_model.rb +1 -1
  42. data/lib/sequel/plugins/association_pks.rb +2 -2
  43. data/lib/sequel/plugins/association_proxies.rb +1 -1
  44. data/lib/sequel/plugins/boolean_readers.rb +2 -2
  45. data/lib/sequel/plugins/class_table_inheritance.rb +10 -2
  46. data/lib/sequel/plugins/identity_map.rb +3 -3
  47. data/lib/sequel/plugins/instance_hooks.rb +1 -1
  48. data/lib/sequel/plugins/json_serializer.rb +212 -0
  49. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  50. data/lib/sequel/plugins/list.rb +174 -0
  51. data/lib/sequel/plugins/many_through_many.rb +2 -2
  52. data/lib/sequel/plugins/rcte_tree.rb +6 -7
  53. data/lib/sequel/plugins/tree.rb +118 -0
  54. data/lib/sequel/plugins/xml_serializer.rb +321 -0
  55. data/lib/sequel/sql.rb +315 -206
  56. data/lib/sequel/timezones.rb +40 -17
  57. data/lib/sequel/version.rb +8 -2
  58. data/spec/adapters/firebird_spec.rb +2 -2
  59. data/spec/adapters/informix_spec.rb +1 -1
  60. data/spec/adapters/mssql_spec.rb +2 -2
  61. data/spec/adapters/mysql_spec.rb +2 -2
  62. data/spec/adapters/oracle_spec.rb +1 -1
  63. data/spec/adapters/postgres_spec.rb +36 -6
  64. data/spec/adapters/spec_helper.rb +2 -2
  65. data/spec/adapters/sqlite_spec.rb +1 -1
  66. data/spec/core/connection_pool_spec.rb +3 -3
  67. data/spec/core/core_sql_spec.rb +31 -13
  68. data/spec/core/database_spec.rb +39 -2
  69. data/spec/core/dataset_spec.rb +24 -12
  70. data/spec/core/expression_filters_spec.rb +5 -1
  71. data/spec/core/object_graph_spec.rb +1 -1
  72. data/spec/core/schema_generator_spec.rb +1 -1
  73. data/spec/core/schema_spec.rb +1 -1
  74. data/spec/core/spec_helper.rb +1 -1
  75. data/spec/core/version_spec.rb +1 -1
  76. data/spec/extensions/active_model_spec.rb +82 -67
  77. data/spec/extensions/association_dependencies_spec.rb +1 -1
  78. data/spec/extensions/association_pks_spec.rb +1 -1
  79. data/spec/extensions/association_proxies_spec.rb +1 -1
  80. data/spec/extensions/blank_spec.rb +1 -1
  81. data/spec/extensions/boolean_readers_spec.rb +1 -1
  82. data/spec/extensions/caching_spec.rb +1 -1
  83. data/spec/extensions/class_table_inheritance_spec.rb +3 -2
  84. data/spec/extensions/composition_spec.rb +2 -5
  85. data/spec/extensions/force_encoding_spec.rb +3 -1
  86. data/spec/extensions/hook_class_methods_spec.rb +1 -1
  87. data/spec/extensions/identity_map_spec.rb +1 -1
  88. data/spec/extensions/inflector_spec.rb +1 -1
  89. data/spec/extensions/instance_filters_spec.rb +1 -1
  90. data/spec/extensions/instance_hooks_spec.rb +1 -1
  91. data/spec/extensions/json_serializer_spec.rb +154 -0
  92. data/spec/extensions/lazy_attributes_spec.rb +1 -2
  93. data/spec/extensions/list_spec.rb +251 -0
  94. data/spec/extensions/looser_typecasting_spec.rb +1 -1
  95. data/spec/extensions/many_through_many_spec.rb +3 -3
  96. data/spec/extensions/migration_spec.rb +1 -1
  97. data/spec/extensions/named_timezones_spec.rb +5 -6
  98. data/spec/extensions/nested_attributes_spec.rb +1 -1
  99. data/spec/extensions/optimistic_locking_spec.rb +1 -1
  100. data/spec/extensions/pagination_spec.rb +1 -1
  101. data/spec/extensions/pretty_table_spec.rb +1 -1
  102. data/spec/extensions/query_spec.rb +1 -1
  103. data/spec/extensions/rcte_tree_spec.rb +1 -1
  104. data/spec/extensions/schema_dumper_spec.rb +3 -2
  105. data/spec/extensions/schema_spec.rb +1 -1
  106. data/spec/extensions/serialization_spec.rb +6 -2
  107. data/spec/extensions/sharding_spec.rb +1 -1
  108. data/spec/extensions/single_table_inheritance_spec.rb +1 -1
  109. data/spec/extensions/skip_create_refresh_spec.rb +1 -1
  110. data/spec/extensions/spec_helper.rb +7 -3
  111. data/spec/extensions/sql_expr_spec.rb +1 -1
  112. data/spec/extensions/string_date_time_spec.rb +1 -1
  113. data/spec/extensions/string_stripper_spec.rb +1 -1
  114. data/spec/extensions/subclasses_spec.rb +1 -1
  115. data/spec/extensions/tactical_eager_loading_spec.rb +1 -1
  116. data/spec/extensions/thread_local_timezones_spec.rb +1 -1
  117. data/spec/extensions/timestamps_spec.rb +1 -1
  118. data/spec/extensions/touch_spec.rb +1 -1
  119. data/spec/extensions/tree_spec.rb +119 -0
  120. data/spec/extensions/typecast_on_load_spec.rb +1 -1
  121. data/spec/extensions/update_primary_key_spec.rb +1 -1
  122. data/spec/extensions/validation_class_methods_spec.rb +1 -1
  123. data/spec/extensions/validation_helpers_spec.rb +1 -1
  124. data/spec/extensions/xml_serializer_spec.rb +142 -0
  125. data/spec/integration/associations_test.rb +1 -1
  126. data/spec/integration/database_test.rb +1 -1
  127. data/spec/integration/dataset_test.rb +29 -14
  128. data/spec/integration/eager_loader_test.rb +1 -1
  129. data/spec/integration/migrator_test.rb +1 -1
  130. data/spec/integration/model_test.rb +1 -1
  131. data/spec/integration/plugin_test.rb +316 -1
  132. data/spec/integration/prepared_statement_test.rb +1 -1
  133. data/spec/integration/schema_test.rb +8 -8
  134. data/spec/integration/spec_helper.rb +1 -1
  135. data/spec/integration/timezone_test.rb +1 -1
  136. data/spec/integration/transaction_test.rb +35 -20
  137. data/spec/integration/type_test.rb +1 -1
  138. data/spec/model/association_reflection_spec.rb +1 -1
  139. data/spec/model/associations_spec.rb +49 -34
  140. data/spec/model/base_spec.rb +1 -1
  141. data/spec/model/dataset_methods_spec.rb +4 -4
  142. data/spec/model/eager_loading_spec.rb +1 -1
  143. data/spec/model/hooks_spec.rb +1 -1
  144. data/spec/model/inflector_spec.rb +1 -1
  145. data/spec/model/model_spec.rb +7 -1
  146. data/spec/model/plugins_spec.rb +1 -1
  147. data/spec/model/record_spec.rb +1 -3
  148. data/spec/model/spec_helper.rb +2 -2
  149. data/spec/model/validations_spec.rb +1 -1
  150. metadata +29 -5
@@ -70,7 +70,7 @@ module Sequel
70
70
  # the attribute for the current object.
71
71
  def lazy_attribute_lookup(a)
72
72
  primary_key = model.primary_key
73
- model.select(*(Array(primary_key) + [a])).filter(primary_key=>::Sequel::SQL::SQLArray.new(retrieved_with.map{|o| o.pk})).all if model.identity_map && retrieved_with
73
+ model.select(*(Array(primary_key) + [a])).filter(primary_key=>retrieved_with.map{|o| o.pk}).all if model.identity_map && retrieved_with
74
74
  values[a] = this.select(a).first[a] unless values.include?(a)
75
75
  values[a]
76
76
  end
@@ -0,0 +1,174 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The list plugin allows for model instances to be part of an ordered list,
4
+ # based on a position field in the database. It can either consider all
5
+ # rows in the table as being from the same list, or you can specify scopes
6
+ # so that multiple lists can be kept in the same table.
7
+ #
8
+ # Basic Example:
9
+ #
10
+ # class Item < Sequel::Model(:items)
11
+ # plugin :list # will use :position field for position
12
+ # plugin :list, :field=>:pos # will use :pos field for position
13
+ # end
14
+ #
15
+ # item = Item[1]
16
+ #
17
+ # # Get the next or previous item in the list
18
+ #
19
+ # item.next
20
+ # item.prev
21
+ #
22
+ # # Modify the item's position, which may require modifying other items in
23
+ # # the same list
24
+ #
25
+ # item.move_to(3)
26
+ # item.move_to_top
27
+ # item.move_to_bottom
28
+ # item.move_up
29
+ # item.move_down
30
+ #
31
+ # You can provide a <tt>:scope</tt> option to scope the list. This option
32
+ # can be a symbol or array of symbols specifying column name(s), or a proc
33
+ # that accepts a model instance and returns a dataset representing the list
34
+ # the object is in.
35
+ #
36
+ # For example, if each item has a +user_id+ field, and you want every user
37
+ # to have their own list:
38
+ #
39
+ # Item.plugin :list, :scope=>:user_id
40
+ #
41
+ # Note that using this plugin modifies the order of the model's dataset to
42
+ # sort by the position and scope fields.
43
+ #
44
+ # Also note that unlike ruby arrays, the list plugin assumes that the
45
+ # first entry in the list has position 1, not position 0.
46
+ #
47
+ # Copyright (c) 2007-2010 Sharon Rosner, Wayne E. Seguin, Aman Gupta, Adrian Madrid, Jeremy Evans
48
+ module List
49
+ # Set the +position_field+ and +scope_proc+ attributes for the model,
50
+ # using the <tt>:field</tt> and <tt>:scope</tt> options, respectively.
51
+ # The <tt>:scope</tt> option can be a symbol, array of symbols, or a proc that
52
+ # accepts a model instance and returns a dataset representing the list.
53
+ # Also, modify the model dataset's order to order by the position and scope fields.
54
+ def self.configure(model, opts = {})
55
+ model.position_field = opts[:field] || :position
56
+ model.dataset = model.dataset.order_prepend(model.position_field)
57
+
58
+ model.scope_proc = case scope = opts[:scope]
59
+ when Symbol
60
+ model.dataset = model.dataset.order_prepend(scope)
61
+ proc{|obj| obj.model.filter(scope=>obj.send(scope))}
62
+ when Array
63
+ model.dataset = model.dataset.order_prepend(*scope)
64
+ proc{|obj| obj.model.filter(scope.map{|s| [s, obj.send(s)]})}
65
+ else
66
+ scope
67
+ end
68
+ end
69
+
70
+ module ClassMethods
71
+ # The column name holding the position in the list, as a symbol.
72
+ attr_accessor :position_field
73
+
74
+ # A proc that scopes the dataset, so that there can be multiple positions
75
+ # in the list, but the positions are unique with the scoped dataset. This
76
+ # proc should accept an instance and return a dataset representing the list.
77
+ attr_accessor :scope_proc
78
+
79
+ # Copy the +position_field+ and +scope_proc+ to the subclass.
80
+ def inherited(subclass)
81
+ super
82
+ subclass.position_field = position_field
83
+ subclass.scope_proc = scope_proc
84
+ end
85
+ end
86
+
87
+ module InstanceMethods
88
+ # The model object at the given position in the list containing this instance.
89
+ def at_position(p)
90
+ list_dataset.first(position_field => p)
91
+ end
92
+
93
+ # Find the last position in the list containing this instance.
94
+ def last_position
95
+ list_dataset.max(position_field).to_i
96
+ end
97
+
98
+ # A dataset that represents the list containing this instance.
99
+ def list_dataset
100
+ model.scope_proc ? model.scope_proc.call(self) : model.dataset
101
+ end
102
+
103
+ # Move this instance down the given number of places in the list,
104
+ # or 1 place if no argument is specified.
105
+ def move_down(n = 1)
106
+ move_to(position_value + n)
107
+ end
108
+
109
+ # Move this instance to the given place in the list. Raises an
110
+ # exception if target is less than 1 or greater than the last position in the list.
111
+ def move_to(target, lp = nil)
112
+ current = position_value
113
+ if target != current
114
+ checked_transaction do
115
+ ds = list_dataset
116
+ op, ds = if target < current
117
+ raise(Sequel::Error, "Moving too far up (target = #{target})") if target < 1
118
+ [:+, ds.filter(position_field=>target...current)]
119
+ else
120
+ lp ||= last_position
121
+ raise(Sequel::Error, "Moving too far down (target = #{target}, last_position = #{lp})") if target > lp
122
+ [:-, ds.filter(position_field=>(current + 1)..target)]
123
+ end
124
+ ds.update(position_field => Sequel::SQL::NumericExpression.new(op, position_field, 1))
125
+ update(position_field => target)
126
+ end
127
+ end
128
+ self
129
+ end
130
+
131
+ # Move this instance to the bottom (last position) of the list.
132
+ def move_to_bottom
133
+ lp = last_position
134
+ move_to(lp, lp)
135
+ end
136
+
137
+ # Move this instance to the top (first position, position 1) of the list.
138
+ def move_to_top
139
+ move_to(1)
140
+ end
141
+
142
+ # Move this instance the given number of places up in the list, or 1 place
143
+ # if no argument is specified.
144
+ def move_up(n = 1)
145
+ move_to(position_value - n)
146
+ end
147
+
148
+ # The model instance the given number of places below this model instance
149
+ # in the list, or 1 place below if no argument is given.
150
+ def next(n = 1)
151
+ n == 0 ? self : at_position(position_value + n)
152
+ end
153
+
154
+ # The value of the model's position field for this instance.
155
+ def position_value
156
+ send(position_field)
157
+ end
158
+
159
+ # The model instance the given number of places below this model instance
160
+ # in the list, or 1 place below if no argument is given.
161
+ def prev(n = 1)
162
+ self.next(n * -1)
163
+ end
164
+
165
+ private
166
+
167
+ # The model's position field, an instance method for ease of use.
168
+ def position_field
169
+ model.position_field
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,6 +1,6 @@
1
1
  module Sequel
2
2
  module Plugins
3
- # The many_through_many plugin allow you to create a association to multiple objects using multiple join tables.
3
+ # The many_through_many plugin allow you to create an association to multiple objects using multiple join tables.
4
4
  # For example, assume the following associations:
5
5
  #
6
6
  # Artist.many_to_many :albums
@@ -185,7 +185,7 @@ module Sequel
185
185
  ds = opts.associated_class
186
186
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
187
187
  ft = opts[:final_reverse_edge]
188
- conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, SQL::SQLArray.new(h.keys)]] : [[left_key, h.keys]]
188
+ conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, h.keys]] : [[left_key, h.keys]]
189
189
  ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + conds, :table_alias=>ft[:alias])
190
190
  model.eager_loading_dataset(opts, ds, Array(opts.select), eo[:associations], eo).all do |assoc_record|
191
191
  hash_key = if uses_lcks
@@ -95,19 +95,18 @@ module Sequel
95
95
  # Create the appropriate parent, children, ancestors, and descendants
96
96
  # associations for the model.
97
97
  def self.apply(model, opts={})
98
+ model.plugin :tree, opts
99
+
98
100
  opts = opts.dup
99
101
  opts[:class] = model
102
+ opts[:methods_module] = Module.new
103
+ model.send(:include, opts[:methods_module])
100
104
 
101
105
  key = opts[:key] ||= :parent_id
102
106
  prkey = opts[:primary_key] ||= model.primary_key
103
107
 
104
- par = opts.merge(opts.fetch(:parent, {}))
105
- parent = par.fetch(:name, :parent)
106
- model.many_to_one parent, par
107
-
108
- chi = opts.merge(opts.fetch(:children, {}))
109
- childrena = chi.fetch(:name, :children)
110
- model.one_to_many childrena, chi
108
+ parent = opts.merge(opts.fetch(:parent, {})).fetch(:name, :parent)
109
+ childrena = opts.merge(opts.fetch(:children, {})).fetch(:name, :children)
111
110
 
112
111
  ka = opts[:key_alias] ||= :x_root_x
113
112
  t = opts[:cte_name] ||= :t
@@ -0,0 +1,118 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The Tree plugin adds additional associations and methods that allow you to
4
+ # treat a Model as a tree.
5
+ #
6
+ # A column for holding the parent key is required and is :parent_id by default.
7
+ # This may be overridden by passing column name via :key
8
+ #
9
+ # Optionally, a column to control order of nodes returned can be specified
10
+ # by passing column name via :order.
11
+ #
12
+ # Examples:
13
+ #
14
+ # class Node < Sequel::Model
15
+ # plugin :tree
16
+ # end
17
+ #
18
+ # class Node < Sequel::Model
19
+ # plugin :tree, :key=>:parentid, :order=>:position
20
+ # end
21
+ module Tree
22
+ # Create parent and children associations. Any options
23
+ # specified are passed to both associations. You can
24
+ # specify options to use for the parent association
25
+ # using a :parent option, and options to use for the
26
+ # children association using a :children option.
27
+ def self.apply(model, opts={})
28
+ opts = opts.dup
29
+ opts[:class] = model
30
+
31
+ model.instance_eval do
32
+ @parent_column = (opts[:key] ||= :parent_id)
33
+ @tree_order = opts[:order]
34
+ end
35
+
36
+ par = opts.merge(opts.fetch(:parent, {}))
37
+ parent = par.fetch(:name, :parent)
38
+ model.many_to_one parent, par
39
+
40
+ chi = opts.merge(opts.fetch(:children, {}))
41
+ children = chi.fetch(:name, :children)
42
+ model.one_to_many children, chi
43
+ end
44
+
45
+ module ClassMethods
46
+ # The column symbol or array of column symbols on which to order the tree.
47
+ attr_accessor :tree_order
48
+
49
+ # The symbol for the column containing the value pointing to the
50
+ # parent of the leaf.
51
+ attr_accessor :parent_column
52
+
53
+ # Copy the +parent_column+ and +order_column+ to the subclass.
54
+ def inherited(subclass)
55
+ super
56
+ subclass.parent_column = parent_column
57
+ subclass.tree_order = tree_order
58
+ end
59
+
60
+ # Returns list of all root nodes (those with no parent nodes).
61
+ #
62
+ # TreeClass.roots # => [root1, root2]
63
+ def roots
64
+ roots_dataset.all
65
+ end
66
+
67
+ # Returns the dataset for retrieval of all root nodes
68
+ #
69
+ # TreeClass.roots_dataset => Sequel#Dataset
70
+ def roots_dataset
71
+ ds = filter(parent_column => nil)
72
+ ds = ds.order(*tree_order) if tree_order
73
+ ds
74
+ end
75
+ end
76
+
77
+ module InstanceMethods
78
+ # Returns list of ancestors, starting from parent until root.
79
+ #
80
+ # subchild1.ancestors # => [child1, root]
81
+ def ancestors
82
+ node, nodes = self, []
83
+ nodes << node = node.parent while node.parent
84
+ nodes
85
+ end
86
+
87
+ # Returns list of ancestors, starting from parent until root.
88
+ #
89
+ # subchild1.ancestors # => [child1, root]
90
+ def descendants
91
+ nodes = self.children.dup
92
+ nodes.each{|child| nodes.concat(child.descendants)}
93
+ nodes
94
+ end
95
+
96
+ # Returns the root node of the tree that this node descends from
97
+ # This node is returned if it is a root node itself.
98
+ def root
99
+ ancestors.last || self
100
+ end
101
+
102
+ # Returns all siblings and a reference to the current node.
103
+ #
104
+ # subchild1.self_and_siblings # => [subchild1, subchild2]
105
+ def self_and_siblings
106
+ parent ? parent.children : model.roots
107
+ end
108
+
109
+ # Returns all siblings of the current node.
110
+ #
111
+ # subchild1.siblings # => [subchild2]
112
+ def siblings
113
+ self_and_siblings - [self]
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,321 @@
1
+ module Sequel
2
+ tsk_require 'nokogiri'
3
+
4
+ module Plugins
5
+ # The xml_serializer plugin handles serializing entire Sequel::Model
6
+ # objects to XML, and deserializing XML into a single Sequel::Model
7
+ # object or an array of Sequel::Model objects. It requires the
8
+ # nokogiri library.
9
+ #
10
+ # Basic Example:
11
+ #
12
+ # album = Album[1]
13
+ # puts album.to_xml
14
+ # # Output:
15
+ # <?xml version="1.0"?>
16
+ # <album>
17
+ # <id>1</id>
18
+ # <name>RF</name>
19
+ # <artist_id>2</artist_id>
20
+ # </album>
21
+ #
22
+ # You can provide options to control the XML output:
23
+ #
24
+ # puts album.to_xml(:only=>:name)
25
+ # puts album.to_xml(:except=>[:id, :artist_id])
26
+ # # Output:
27
+ # <?xml version="1.0"?>
28
+ # <album>
29
+ # <name>RF</name>
30
+ # </album>
31
+ #
32
+ # album.to_xml(:include=>:artist)
33
+ # # Output:
34
+ # <?xml version="1.0"?>
35
+ # <album>
36
+ # <id>1</id>
37
+ # <name>RF</name>
38
+ # <artist_id>2</artist_id>
39
+ # <artist>
40
+ # <id>2</id>
41
+ # <name>YJM</name>
42
+ # </artist>
43
+ # </album>
44
+ #
45
+ # You can use a hash value with <tt>:include</tt> to pass options
46
+ # to associations:
47
+ #
48
+ # album.to_json(:include=>{:artist=>{:only=>:name}})
49
+ # # Output:
50
+ # <?xml version="1.0"?>
51
+ # <album>
52
+ # <id>1</id>
53
+ # <name>RF</name>
54
+ # <artist_id>2</artist_id>
55
+ # <artist>
56
+ # <name>YJM</name>
57
+ # </artist>
58
+ # </album>
59
+ #
60
+ # In addition to creating XML, this plugin also enables Sequel::Model
61
+ # objects to be created by parsing XML:
62
+ #
63
+ # xml = album.to_xml
64
+ # album = Album.from_xml(xml)
65
+ #
66
+ # In addition, you can update existing model objects directly from XML
67
+ # using +from_xml+:
68
+ #
69
+ # album.from_xml(xml)
70
+ #
71
+ # Additionally, +to_xml+ also exists as a class and dataset method, both
72
+ # of which return all objects in the dataset:
73
+ #
74
+ # Album.to_xml
75
+ # Album.filter(:artist_id=>1).to_xml(:include=>:tags)
76
+ #
77
+ # Such XML can be loaded back into an array of Sequel::Model objects using
78
+ # +array_from_xml+:
79
+ #
80
+ # Album.array_from_xml(Album.to_xml) # same as Album.all
81
+ #
82
+ # Usage:
83
+ #
84
+ # # Add XML output capability to all model subclass instances (called before loading subclasses)
85
+ # Sequel::Model.plugin :xml_serializer
86
+ #
87
+ # # Add XML output capability to Album class instances
88
+ # Album.plugin :xml_serializer
89
+ module XmlSerializer
90
+ module ClassMethods
91
+ # Proc that camelizes the input string, used for the :camelize option
92
+ CAMELIZE = proc{|s| s.camelize}
93
+
94
+ # Proc that dasherizes the input string, used for the :dasherize option
95
+ DASHERIZE = proc{|s| s.dasherize}
96
+
97
+ # Proc that returns the input string as is, used if
98
+ # no :name_proc, :dasherize, or :camelize option is used.
99
+ IDENTITY = proc{|s| s}
100
+
101
+ # Proc that underscores the input string, used for the :underscore option
102
+ UNDERSCORE = proc{|s| s.underscore}
103
+
104
+ # Return an array of instances of this class based on
105
+ # the provided XML.
106
+ def array_from_xml(xml, opts={})
107
+ Nokogiri::XML(xml).children.first.children.reject{|c| c.is_a?(Nokogiri::XML::Text)}.map{|c| from_xml_node(c, opts)}
108
+ end
109
+
110
+ # Return an instance of this class based on the provided
111
+ # XML.
112
+ def from_xml(xml, opts={})
113
+ from_xml_node(Nokogiri::XML(xml).children.first, opts)
114
+ end
115
+
116
+ # Return an instance of this class based on the given
117
+ # XML node, which should be Nokogiri::XML::Node instance.
118
+ # This should probably not be used directly by user code.
119
+ def from_xml_node(parent, opts={})
120
+ new.from_xml_node(parent, opts)
121
+ end
122
+
123
+ # Call the dataset +to_xml+ method.
124
+ def to_xml(opts={})
125
+ dataset.to_xml(opts)
126
+ end
127
+
128
+ # Return an appropriate Nokogiri::XML::Builder instance
129
+ # used to create the XML. This should probably not be used
130
+ # directly by user code.
131
+ def xml_builder(opts={})
132
+ if opts[:builder]
133
+ opts[:builder]
134
+ else
135
+ builder_opts = if opts[:builder_opts]
136
+ opts[:builder_opts]
137
+ else
138
+ {}
139
+ end
140
+ builder_opts[:encoding] = opts[:encoding] if opts.has_key?(:encoding)
141
+ Nokogiri::XML::Builder.new(builder_opts)
142
+ end
143
+ end
144
+
145
+ # Return a proc (or any other object that responds to []),
146
+ # used for formatting XML tag names when serializing to XML.
147
+ # This should probably not be used directly by user code.
148
+ def xml_deserialize_name_proc(opts={})
149
+ if opts[:name_proc]
150
+ opts[:name_proc]
151
+ elsif opts[:underscore]
152
+ UNDERSCORE
153
+ else
154
+ IDENTITY
155
+ end
156
+ end
157
+
158
+ # Return a proc (or any other object that responds to []),
159
+ # used for formatting XML tag names when serializing to XML.
160
+ # This should probably not be used directly by user code.
161
+ def xml_serialize_name_proc(opts={})
162
+ pr = if opts[:name_proc]
163
+ opts[:name_proc]
164
+ elsif opts[:dasherize]
165
+ DASHERIZE
166
+ elsif opts[:camelize]
167
+ CAMELIZE
168
+ else
169
+ IDENTITY
170
+ end
171
+ proc{|s| "#{pr[s]}_"}
172
+ end
173
+ end
174
+
175
+ module InstanceMethods
176
+ # Update the contents of this instance based on the given XML.
177
+ # Accepts the following options:
178
+ #
179
+ # :name_proc :: Proc or Hash that accepts a string and returns
180
+ # a string, used to convert tag names to column or
181
+ # association names.
182
+ # :underscore :: Sets the :name_proc option to one that calls +underscore+
183
+ # on the input string. Requires that you load the inflector
184
+ # extension or another library that adds String#underscore.
185
+ def from_xml(xml, opts={})
186
+ from_xml_node(Nokogiri::XML(xml).children.first, opts)
187
+ end
188
+
189
+ # Update the contents of this instance based on the given
190
+ # XML node, which should be a Nokogiri::XML::Node instance.
191
+ def from_xml_node(parent, opts={})
192
+ cols = model.columns.map{|x| x.to_s}
193
+ assocs = {}
194
+ model.associations.map{|x| assocs[x.to_s] = model.association_reflection(x)}
195
+ meths = send(:setter_methods, nil, nil)
196
+ name_proc = model.xml_deserialize_name_proc(opts)
197
+ parent.children.each do |node|
198
+ next if node.is_a?(Nokogiri::XML::Text)
199
+ k = name_proc[node.name]
200
+ if ar = assocs[k]
201
+ klass = ar.associated_class
202
+ associations[k.to_sym] = if ar.returns_array?
203
+ node.children.reject{|c| c.is_a?(Nokogiri::XML::Text)}.map{|c| klass.from_xml_node(c)}
204
+ else
205
+ klass.from_xml_node(node)
206
+ end
207
+ elsif cols.include?(k)
208
+ self[k.to_sym] = node.children.first.to_s
209
+ elsif meths.include?("#{k}=")
210
+ send("#{k}=", node.children.first.to_s)
211
+ else
212
+ raise Error, "Entry in XML not an association or column and no setter method exists: #{k}"
213
+ end
214
+ end
215
+ self
216
+ end
217
+
218
+ # Return a string in XML format. If a block is given, yields the XML
219
+ # builder object so you can add additional XML tags.
220
+ # Accepts the following options:
221
+ #
222
+ # :builder :: The builder instance used to build the XML,
223
+ # which should be an instance of Nokogiri::XML::Node. This
224
+ # is necessary if you are serializing entire object graphs,
225
+ # like associated objects.
226
+ # :builder_opts :: Options to pass to the Nokogiri::XML::Builder
227
+ # initializer, if the :builder option is not provided.
228
+ # :camelize:: Sets the :name_proc option to one that calls +camelize+
229
+ # on the input string. Requires that you load the inflector
230
+ # extension or another library that adds String#camelize.
231
+ # :dasherize :: Sets the :name_proc option to one that calls +dasherize+
232
+ # on the input string. Requires that you load the inflector
233
+ # extension or another library that adds String#dasherize.
234
+ # :encoding :: The encoding to use for the XML output, passed
235
+ # to the Nokogiri::XML::Builder initializer.
236
+ # :except :: Symbol or Array of Symbols of columns not
237
+ # to include in the XML output.
238
+ # :include :: Symbol, Array of Symbols, or a Hash with
239
+ # Symbol keys and Hash values specifying
240
+ # associations or other non-column attributes
241
+ # to include in the XML output. Using a nested
242
+ # hash, you can pass options to associations
243
+ # to affect the XML used for associated objects.
244
+ # :name_proc :: Proc or Hash that accepts a string and returns
245
+ # a string, used to format tag names.
246
+ # :only :: Symbol or Array of Symbols of columns to only
247
+ # include in the JSON output, ignoring all other
248
+ # columns.
249
+ # :root_name :: The base name to use for the XML tag that
250
+ # contains the data for this instance. This will
251
+ # be the name of the root node if you are only serializing
252
+ # a single object, but not if you are serializing
253
+ # an array of objects using Model.to_xml or Dataset#to_xml.
254
+ # :types :: Set to true to include type information for
255
+ # all of the columns, pulled from the db_schema.
256
+ def to_xml(opts={})
257
+ vals = values
258
+ types = opts[:types]
259
+ inc = opts[:include]
260
+
261
+ cols = if only = opts[:only]
262
+ Array(only)
263
+ else
264
+ vals.keys - Array(opts[:except])
265
+ end
266
+
267
+ name_proc = model.xml_serialize_name_proc(opts)
268
+ x = model.xml_builder(opts)
269
+ x.send(name_proc[opts.fetch(:root_name, model.send(:underscore, model.name)).to_s]) do |x1|
270
+ cols.each do |c|
271
+ x1.send(name_proc[c.to_s], vals[c], types ? {:type=>db_schema.fetch(c, {})[:type]} : {})
272
+ end
273
+ if inc.is_a?(Hash)
274
+ inc.each{|k, v| to_xml_include(x1, k, v)}
275
+ else
276
+ Array(inc).each{|i| to_xml_include(x1, i)}
277
+ end
278
+ yield x1 if block_given?
279
+ end
280
+ x.to_xml
281
+ end
282
+
283
+ private
284
+
285
+ # Handle associated objects and virtual attributes when creating
286
+ # the xml.
287
+ def to_xml_include(node, i, opts={})
288
+ name_proc = model.xml_serialize_name_proc(opts)
289
+ objs = send(i)
290
+ if objs.is_a?(Array) && objs.all?{|x| x.is_a?(Sequel::Model)}
291
+ node.send(name_proc[i.to_s]) do |x2|
292
+ objs.each{|obj| obj.to_xml(opts.merge(:builder=>x2))}
293
+ end
294
+ elsif objs.is_a?(Sequel::Model)
295
+ objs.to_xml(opts.merge(:builder=>node, :root_name=>i))
296
+ else
297
+ node.send(name_proc[i.to_s], objs)
298
+ end
299
+ end
300
+ end
301
+
302
+ module DatasetMethods
303
+ # Return an XML string containing all model objects specified with
304
+ # this dataset. Takes all of the options available to Model#to_xml,
305
+ # as well as the :array_root_name option for specifying the name of
306
+ # the root node that contains the nodes for all of the instances.
307
+ def to_xml(opts={})
308
+ raise(Sequel::Error, "Dataset#to_xml") unless row_proc
309
+ x = model.xml_builder(opts)
310
+ name_proc = model.xml_serialize_name_proc(opts)
311
+ x.send(name_proc[opts.fetch(:array_root_name, model.send(:pluralize, model.send(:underscore, model.name))).to_s]) do |x1|
312
+ all.each do |obj|
313
+ obj.to_xml(opts.merge(:builder=>x1))
314
+ end
315
+ end
316
+ x.to_xml
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end