sequel 3.12.1 → 3.13.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +42 -0
- data/README.rdoc +137 -118
- data/Rakefile +21 -66
- data/doc/active_record.rdoc +9 -9
- data/doc/advanced_associations.rdoc +59 -188
- data/doc/association_basics.rdoc +15 -2
- data/doc/cheat_sheet.rdoc +38 -33
- data/doc/dataset_filtering.rdoc +16 -7
- data/doc/prepared_statements.rdoc +7 -7
- data/doc/querying.rdoc +5 -4
- data/doc/release_notes/3.13.0.txt +210 -0
- data/doc/sharding.rdoc +1 -1
- data/doc/sql.rdoc +5 -5
- data/doc/validations.rdoc +11 -11
- data/lib/sequel/adapters/ado.rb +1 -1
- data/lib/sequel/adapters/do.rb +3 -3
- data/lib/sequel/adapters/firebird.rb +3 -3
- data/lib/sequel/adapters/jdbc/h2.rb +39 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +5 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +3 -3
- data/lib/sequel/adapters/mysql.rb +7 -4
- data/lib/sequel/adapters/oracle.rb +3 -3
- data/lib/sequel/adapters/shared/mssql.rb +10 -1
- data/lib/sequel/adapters/shared/mysql.rb +63 -0
- data/lib/sequel/adapters/shared/postgres.rb +61 -3
- data/lib/sequel/adapters/sqlite.rb +105 -18
- data/lib/sequel/connection_pool.rb +31 -30
- data/lib/sequel/core.rb +58 -58
- data/lib/sequel/core_sql.rb +52 -43
- data/lib/sequel/database/misc.rb +11 -0
- data/lib/sequel/database/query.rb +55 -17
- data/lib/sequel/dataset/actions.rb +2 -1
- data/lib/sequel/dataset/query.rb +2 -3
- data/lib/sequel/dataset/sql.rb +24 -11
- data/lib/sequel/extensions/schema_dumper.rb +1 -1
- data/lib/sequel/metaprogramming.rb +4 -0
- data/lib/sequel/model.rb +37 -19
- data/lib/sequel/model/associations.rb +33 -25
- data/lib/sequel/model/base.rb +2 -2
- data/lib/sequel/model/plugins.rb +7 -2
- data/lib/sequel/plugins/active_model.rb +1 -1
- data/lib/sequel/plugins/association_pks.rb +2 -2
- data/lib/sequel/plugins/association_proxies.rb +1 -1
- data/lib/sequel/plugins/boolean_readers.rb +2 -2
- data/lib/sequel/plugins/class_table_inheritance.rb +10 -2
- data/lib/sequel/plugins/identity_map.rb +3 -3
- data/lib/sequel/plugins/instance_hooks.rb +1 -1
- data/lib/sequel/plugins/json_serializer.rb +212 -0
- data/lib/sequel/plugins/lazy_attributes.rb +1 -1
- data/lib/sequel/plugins/list.rb +174 -0
- data/lib/sequel/plugins/many_through_many.rb +2 -2
- data/lib/sequel/plugins/rcte_tree.rb +6 -7
- data/lib/sequel/plugins/tree.rb +118 -0
- data/lib/sequel/plugins/xml_serializer.rb +321 -0
- data/lib/sequel/sql.rb +315 -206
- data/lib/sequel/timezones.rb +40 -17
- data/lib/sequel/version.rb +8 -2
- data/spec/adapters/firebird_spec.rb +2 -2
- data/spec/adapters/informix_spec.rb +1 -1
- data/spec/adapters/mssql_spec.rb +2 -2
- data/spec/adapters/mysql_spec.rb +2 -2
- data/spec/adapters/oracle_spec.rb +1 -1
- data/spec/adapters/postgres_spec.rb +36 -6
- data/spec/adapters/spec_helper.rb +2 -2
- data/spec/adapters/sqlite_spec.rb +1 -1
- data/spec/core/connection_pool_spec.rb +3 -3
- data/spec/core/core_sql_spec.rb +31 -13
- data/spec/core/database_spec.rb +39 -2
- data/spec/core/dataset_spec.rb +24 -12
- data/spec/core/expression_filters_spec.rb +5 -1
- data/spec/core/object_graph_spec.rb +1 -1
- data/spec/core/schema_generator_spec.rb +1 -1
- data/spec/core/schema_spec.rb +1 -1
- data/spec/core/spec_helper.rb +1 -1
- data/spec/core/version_spec.rb +1 -1
- data/spec/extensions/active_model_spec.rb +82 -67
- data/spec/extensions/association_dependencies_spec.rb +1 -1
- data/spec/extensions/association_pks_spec.rb +1 -1
- data/spec/extensions/association_proxies_spec.rb +1 -1
- data/spec/extensions/blank_spec.rb +1 -1
- data/spec/extensions/boolean_readers_spec.rb +1 -1
- data/spec/extensions/caching_spec.rb +1 -1
- data/spec/extensions/class_table_inheritance_spec.rb +3 -2
- data/spec/extensions/composition_spec.rb +2 -5
- data/spec/extensions/force_encoding_spec.rb +3 -1
- data/spec/extensions/hook_class_methods_spec.rb +1 -1
- data/spec/extensions/identity_map_spec.rb +1 -1
- data/spec/extensions/inflector_spec.rb +1 -1
- data/spec/extensions/instance_filters_spec.rb +1 -1
- data/spec/extensions/instance_hooks_spec.rb +1 -1
- data/spec/extensions/json_serializer_spec.rb +154 -0
- data/spec/extensions/lazy_attributes_spec.rb +1 -2
- data/spec/extensions/list_spec.rb +251 -0
- data/spec/extensions/looser_typecasting_spec.rb +1 -1
- data/spec/extensions/many_through_many_spec.rb +3 -3
- data/spec/extensions/migration_spec.rb +1 -1
- data/spec/extensions/named_timezones_spec.rb +5 -6
- data/spec/extensions/nested_attributes_spec.rb +1 -1
- data/spec/extensions/optimistic_locking_spec.rb +1 -1
- data/spec/extensions/pagination_spec.rb +1 -1
- data/spec/extensions/pretty_table_spec.rb +1 -1
- data/spec/extensions/query_spec.rb +1 -1
- data/spec/extensions/rcte_tree_spec.rb +1 -1
- data/spec/extensions/schema_dumper_spec.rb +3 -2
- data/spec/extensions/schema_spec.rb +1 -1
- data/spec/extensions/serialization_spec.rb +6 -2
- data/spec/extensions/sharding_spec.rb +1 -1
- data/spec/extensions/single_table_inheritance_spec.rb +1 -1
- data/spec/extensions/skip_create_refresh_spec.rb +1 -1
- data/spec/extensions/spec_helper.rb +7 -3
- data/spec/extensions/sql_expr_spec.rb +1 -1
- data/spec/extensions/string_date_time_spec.rb +1 -1
- data/spec/extensions/string_stripper_spec.rb +1 -1
- data/spec/extensions/subclasses_spec.rb +1 -1
- data/spec/extensions/tactical_eager_loading_spec.rb +1 -1
- data/spec/extensions/thread_local_timezones_spec.rb +1 -1
- data/spec/extensions/timestamps_spec.rb +1 -1
- data/spec/extensions/touch_spec.rb +1 -1
- data/spec/extensions/tree_spec.rb +119 -0
- data/spec/extensions/typecast_on_load_spec.rb +1 -1
- data/spec/extensions/update_primary_key_spec.rb +1 -1
- data/spec/extensions/validation_class_methods_spec.rb +1 -1
- data/spec/extensions/validation_helpers_spec.rb +1 -1
- data/spec/extensions/xml_serializer_spec.rb +142 -0
- data/spec/integration/associations_test.rb +1 -1
- data/spec/integration/database_test.rb +1 -1
- data/spec/integration/dataset_test.rb +29 -14
- data/spec/integration/eager_loader_test.rb +1 -1
- data/spec/integration/migrator_test.rb +1 -1
- data/spec/integration/model_test.rb +1 -1
- data/spec/integration/plugin_test.rb +316 -1
- data/spec/integration/prepared_statement_test.rb +1 -1
- data/spec/integration/schema_test.rb +8 -8
- data/spec/integration/spec_helper.rb +1 -1
- data/spec/integration/timezone_test.rb +1 -1
- data/spec/integration/transaction_test.rb +35 -20
- data/spec/integration/type_test.rb +1 -1
- data/spec/model/association_reflection_spec.rb +1 -1
- data/spec/model/associations_spec.rb +49 -34
- data/spec/model/base_spec.rb +1 -1
- data/spec/model/dataset_methods_spec.rb +4 -4
- data/spec/model/eager_loading_spec.rb +1 -1
- data/spec/model/hooks_spec.rb +1 -1
- data/spec/model/inflector_spec.rb +1 -1
- data/spec/model/model_spec.rb +7 -1
- data/spec/model/plugins_spec.rb +1 -1
- data/spec/model/record_spec.rb +1 -3
- data/spec/model/spec_helper.rb +2 -2
- data/spec/model/validations_spec.rb +1 -1
- 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
|
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
|
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)},
|
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
|
-
|
105
|
-
|
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
|