sequel 3.12.1 → 3.13.0

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.
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