ruby-path 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +23 -0
- data/.gemtest +0 -0
- data/History.rdoc +6 -0
- data/Manifest.txt +14 -0
- data/README.rdoc +96 -0
- data/Rakefile +14 -0
- data/lib/path.rb +340 -0
- data/lib/path/core_ext.rb +106 -0
- data/lib/path/match.rb +130 -0
- data/lib/path/matcher.rb +193 -0
- data/lib/path/transaction.rb +341 -0
- data/test/test_core_ext.rb +73 -0
- data/test/test_helper.rb +3 -0
- data/test/test_path.rb +316 -0
- data/test/test_path_match.rb +105 -0
- data/test/test_path_matcher.rb +371 -0
- metadata +103 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
##
|
2
|
+
# Data manipulation and retrieval methods for Array and Hash classes.
|
3
|
+
|
4
|
+
module Path::DataExt
|
5
|
+
|
6
|
+
##
|
7
|
+
# Checks if the given path exists and returns the first matching path
|
8
|
+
# as an array of keys. Returns false if no path is found.
|
9
|
+
|
10
|
+
def has_path? path
|
11
|
+
Path.find path, self do |d,k,p|
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
##
|
20
|
+
# Looks for data at paths matching path. Returns a hash of
|
21
|
+
# path array => data value pairs.
|
22
|
+
#
|
23
|
+
# If given a block will pass the parent data structure, the key
|
24
|
+
# or index of the item at given path, and the full path
|
25
|
+
# as an array of keys for each found path.
|
26
|
+
#
|
27
|
+
# data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
28
|
+
# data.find_data "**/foo" do |parent, key, path|
|
29
|
+
# p path
|
30
|
+
# p parent[key]
|
31
|
+
# puts "---"
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # outputs:
|
35
|
+
# # [:foo]
|
36
|
+
# # "bar"
|
37
|
+
# # ---
|
38
|
+
# # [:foobar, 2, :foo]
|
39
|
+
# # "other bar"
|
40
|
+
# # ---
|
41
|
+
#
|
42
|
+
# # returns:
|
43
|
+
# # {[:foo] => "bar", [:foobar, 2, :foo] => "other bar"}
|
44
|
+
|
45
|
+
def find_data path, &block
|
46
|
+
Path.find path, self, &block
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
##
|
51
|
+
# Finds and replaces the value of any match with the given new value.
|
52
|
+
# Returns true if matches were replaced, otherwise false.
|
53
|
+
#
|
54
|
+
# data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
55
|
+
# data.replace_at_path "**=*bar", "BAR"
|
56
|
+
# #=> true
|
57
|
+
#
|
58
|
+
# data
|
59
|
+
# #=> {:foo => "BAR", :foobar => [:a, :b, {:foo => "BAR"}, :c]}
|
60
|
+
#
|
61
|
+
# Note: Specifying a limit will allow only "limit" number of items to be
|
62
|
+
# set but may yield unpredictible results for non-ordered Hashes.
|
63
|
+
# It's also important to realize that arrays are modified starting with
|
64
|
+
# the last index, going down.
|
65
|
+
|
66
|
+
def replace_at_path path, value, limit=nil
|
67
|
+
count = 0
|
68
|
+
|
69
|
+
Path.find path, self do |data, key, path_arr|
|
70
|
+
count = count.next
|
71
|
+
data[key] = value
|
72
|
+
|
73
|
+
return true if limit && count >= limit
|
74
|
+
end
|
75
|
+
|
76
|
+
return count > 0
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
##
|
81
|
+
# Similar to DataExt#replace_at_path but deletes found items.
|
82
|
+
# Returns a hash of path/value pairs of deleted items.
|
83
|
+
#
|
84
|
+
# data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
85
|
+
# data.replace_at_path "**=*bar", "BAR"
|
86
|
+
# #=> {[:foo] => "bar", [:foobar, 2, :foo] => "other bar"}
|
87
|
+
|
88
|
+
def delete_at_path path, limit=nil
|
89
|
+
count = 0
|
90
|
+
out = {}
|
91
|
+
|
92
|
+
Path.find path, self do |data, key, path_arr|
|
93
|
+
count = count.next
|
94
|
+
out[path_arr] = data[key]
|
95
|
+
|
96
|
+
data.respond_to(:delete_at) ? data.delete_at(key) : data.delete(key)
|
97
|
+
|
98
|
+
return true if limit && count >= limit
|
99
|
+
end
|
100
|
+
|
101
|
+
return count > 0
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
Array.send :include, Path::DataExt
|
106
|
+
Hash.send :include, Path::DataExt
|
data/lib/path/match.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
##
|
2
|
+
# Represents the single match of a relative path against a data set.
|
3
|
+
|
4
|
+
class Path::Match < Array
|
5
|
+
|
6
|
+
attr_accessor :matches, :splat
|
7
|
+
|
8
|
+
def initialize *args
|
9
|
+
@matches = []
|
10
|
+
@splat = []
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def [] selector
|
16
|
+
path_match = super
|
17
|
+
|
18
|
+
if self.class === path_match
|
19
|
+
path_match.matches = @matches.dup
|
20
|
+
path_match.splat = @splat.map{|key, sp| [key, sp.dup]}
|
21
|
+
end
|
22
|
+
|
23
|
+
path_match
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def append_splat id, key # :nodoc:
|
28
|
+
if @splat[-1] && @splat[-1][0] == id
|
29
|
+
@splat[-1][1] << key
|
30
|
+
else
|
31
|
+
@splat << [id, [key]]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def dup # :nodoc:
|
37
|
+
path_match = super
|
38
|
+
path_match.matches = @matches.dup
|
39
|
+
path_match.splat = @splat.map{|key, sp| [key, sp.dup]}
|
40
|
+
path_match
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def append_match_for str, path # :nodoc:
|
45
|
+
match = @matches[str.to_i-1]
|
46
|
+
if match && !(String === match) && path[-1].empty?
|
47
|
+
path[-1] = match
|
48
|
+
else
|
49
|
+
path[-1] << match.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
##
|
55
|
+
# Builds a path array by replacing %n and %% values with matches and splat.
|
56
|
+
#
|
57
|
+
# matches = Path.find_in "**/foo=bar", data
|
58
|
+
# # [["path", "to", "foo"]]
|
59
|
+
#
|
60
|
+
# matches.first.make_path "root/%%/foo"
|
61
|
+
# # ["root", "path", "to", "foo"]
|
62
|
+
#
|
63
|
+
# matches = Path.find_in "path/*/(foo)=bar", data
|
64
|
+
# # [["path", "to", "foo"]]
|
65
|
+
#
|
66
|
+
# matches.first.make_path "root/%1/%2"
|
67
|
+
# # ["root", "to", "foo"]
|
68
|
+
|
69
|
+
def make_path path_map, regex_opts=nil, &block
|
70
|
+
tmpsplat = @splat.dup
|
71
|
+
path = []
|
72
|
+
escape = false
|
73
|
+
replace = false
|
74
|
+
new_item = true
|
75
|
+
rindex = ""
|
76
|
+
|
77
|
+
path_map.to_s.chars do |chr|
|
78
|
+
case chr
|
79
|
+
when Path::ECH
|
80
|
+
escape = true
|
81
|
+
|
82
|
+
when Path::DCH
|
83
|
+
new_item = true
|
84
|
+
|
85
|
+
when Path::RCH
|
86
|
+
if replace
|
87
|
+
if rindex.empty?
|
88
|
+
unless tmpsplat.empty?
|
89
|
+
items = tmpsplat.shift[1].dup
|
90
|
+
if new_item
|
91
|
+
new_item = false
|
92
|
+
else
|
93
|
+
path[-1] = path[-1].dup << items.shift
|
94
|
+
end
|
95
|
+
path.concat items
|
96
|
+
end
|
97
|
+
replace = false
|
98
|
+
else
|
99
|
+
append_match_for(rindex, path)
|
100
|
+
rindex = ""
|
101
|
+
end
|
102
|
+
|
103
|
+
next
|
104
|
+
else
|
105
|
+
replace = true
|
106
|
+
end
|
107
|
+
end and next unless escape
|
108
|
+
|
109
|
+
if replace
|
110
|
+
if new_item && !rindex.empty? || chr.to_i.to_s != chr || escape
|
111
|
+
append_match_for(rindex, path) unless rindex.empty?
|
112
|
+
rindex = ""
|
113
|
+
replace = false
|
114
|
+
else
|
115
|
+
rindex << chr
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
path << "" if new_item
|
120
|
+
path.last << chr unless replace
|
121
|
+
|
122
|
+
new_item = false
|
123
|
+
escape = false
|
124
|
+
end
|
125
|
+
|
126
|
+
append_match_for(rindex, path) unless rindex.empty?
|
127
|
+
|
128
|
+
path
|
129
|
+
end
|
130
|
+
end
|
data/lib/path/matcher.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
##
|
2
|
+
# Path::Matcher is representation of a single node of a relative path used
|
3
|
+
# to find values in a data set.
|
4
|
+
|
5
|
+
class Path::Matcher
|
6
|
+
|
7
|
+
# Used as path item value to match any key or value.
|
8
|
+
module ANY_VALUE; end
|
9
|
+
|
10
|
+
# Shortcut characters that require modification before being turned into
|
11
|
+
# a matcher.
|
12
|
+
SUFF_CHARS = Regexp.escape "*?"
|
13
|
+
|
14
|
+
# All special path characters.
|
15
|
+
PATH_CHARS = Regexp.escape("()|") << SUFF_CHARS
|
16
|
+
|
17
|
+
# Path chars that get regexp escaped.
|
18
|
+
RESC_CHARS = "*?()|/"
|
19
|
+
|
20
|
+
# Matcher for Range path item.
|
21
|
+
RANGE_MATCHER = %r{^(\-?\d+)(\.{2,3})(\-?\d+)$}
|
22
|
+
|
23
|
+
# Matcher for index,length path item.
|
24
|
+
ILEN_MATCHER = %r{^(\-?\d+),(\-?\d+)$}
|
25
|
+
|
26
|
+
# Matcher allowing any value to be matched.
|
27
|
+
ANYVAL_MATCHER = /^(\?*\*+\?*)*$/
|
28
|
+
|
29
|
+
# Matcher to assert if any unescaped special chars are in a path item.
|
30
|
+
PATH_CHAR_MATCHER = /(^|[^#{Path::RECH}])([#{PATH_CHARS}])/
|
31
|
+
|
32
|
+
|
33
|
+
attr_reader :key, :value, :regex_opts
|
34
|
+
attr_accessor :recursive
|
35
|
+
|
36
|
+
##
|
37
|
+
# New instance of Matcher. Options supported:
|
38
|
+
# :key:: String - The path item key to match.
|
39
|
+
# :value:: String - The path item value to match.
|
40
|
+
# :recursive:: Boolean - Look for path item recursively.
|
41
|
+
# :regex_opts:: Fixnum - representing the Regexp options.
|
42
|
+
|
43
|
+
def initialize opts={}
|
44
|
+
@regex_opts = opts[:regex_opts]
|
45
|
+
@recursive = !!opts[:recursive]
|
46
|
+
|
47
|
+
@key = nil
|
48
|
+
@key = parse_node opts[:key] if
|
49
|
+
opts[:key] && !opts[:key].to_s.empty?
|
50
|
+
|
51
|
+
@value = nil
|
52
|
+
@value = parse_node opts[:value] if
|
53
|
+
opts[:value] && !opts[:value].to_s.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def == other # :nodoc:
|
58
|
+
self.class == other.class &&
|
59
|
+
@key == other.key &&
|
60
|
+
@value == other.value &&
|
61
|
+
@regex_opts == other.regex_opts
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
##
|
66
|
+
# Universal iterator for Hash and Array like objects.
|
67
|
+
# The data argument must either respond to both :each_with_index
|
68
|
+
# and :length, or respond to :has_key? and :each yielding a key/value pair.
|
69
|
+
|
70
|
+
def each_data_item data, &block
|
71
|
+
if data.respond_to?(:has_key?) && data.respond_to?(:each)
|
72
|
+
data.each(&block)
|
73
|
+
|
74
|
+
elsif data.respond_to?(:each_with_index) && data.respond_to?(:length)
|
75
|
+
# We need to iterate through the array this way
|
76
|
+
# in case items in it get deleted.
|
77
|
+
(data.length - 1).downto(0) do |i|
|
78
|
+
block.call i, data[i]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
##
|
85
|
+
# Finds data with the given key and value matcher, optionally recursive.
|
86
|
+
# Yields data, key and path Array when block is given.
|
87
|
+
# Returns an Array of path arrays.
|
88
|
+
|
89
|
+
def find_in data, path=nil, &block
|
90
|
+
return [] unless Array === data || Hash === data
|
91
|
+
|
92
|
+
paths = []
|
93
|
+
path ||= Path::Match.new
|
94
|
+
path = Path::Match.new path if path.class == Array
|
95
|
+
|
96
|
+
each_data_item data do |key, value|
|
97
|
+
c_path = path.dup << key
|
98
|
+
|
99
|
+
found, kmatch = match_node(@key, key) if @key
|
100
|
+
found, vmatch = match_node(@value, value) if @value && (!@key || found)
|
101
|
+
|
102
|
+
c_path.append_splat self, key if @recursive
|
103
|
+
|
104
|
+
if found
|
105
|
+
c_path.matches.concat kmatch.to_a
|
106
|
+
c_path.matches.concat vmatch.to_a
|
107
|
+
|
108
|
+
f_path = c_path.dup
|
109
|
+
f_path.splat[-1][-1].pop if @key && !f_path.splat.empty?
|
110
|
+
|
111
|
+
yield data, key, f_path if block_given?
|
112
|
+
paths << f_path
|
113
|
+
end
|
114
|
+
|
115
|
+
paths.concat find_in(data[key], c_path, &block) if @recursive
|
116
|
+
end
|
117
|
+
|
118
|
+
paths
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
##
|
123
|
+
# Check if data key or value is a match for nested data searches.
|
124
|
+
# Returns an array with a boolean expressing if the value matched the node,
|
125
|
+
# and the matches found.
|
126
|
+
|
127
|
+
def match_node node, value
|
128
|
+
return if ANY_VALUE != node &&
|
129
|
+
(Array === value || Hash === value)
|
130
|
+
|
131
|
+
if node.class == value.class
|
132
|
+
node == value
|
133
|
+
|
134
|
+
elsif Regexp === node
|
135
|
+
match = node.match value.to_s
|
136
|
+
return false unless match
|
137
|
+
match = match.size > 1 ? match[1..-1] : match.to_a
|
138
|
+
[true, match]
|
139
|
+
|
140
|
+
elsif Range === node
|
141
|
+
stat = node.include? value.to_i
|
142
|
+
match = [value.to_i] if stat
|
143
|
+
[stat, match]
|
144
|
+
|
145
|
+
elsif ANY_VALUE == node
|
146
|
+
[true, [value]]
|
147
|
+
|
148
|
+
else
|
149
|
+
value.to_s == node.to_s
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
##
|
155
|
+
# Decide whether to make path item matcher a regex, range, array, or string.
|
156
|
+
|
157
|
+
def parse_node str
|
158
|
+
case str
|
159
|
+
when nil, ANYVAL_MATCHER
|
160
|
+
ANY_VALUE
|
161
|
+
|
162
|
+
when RANGE_MATCHER
|
163
|
+
Range.new $1.to_i, $3.to_i, ($2 == "...")
|
164
|
+
|
165
|
+
when ILEN_MATCHER
|
166
|
+
Range.new $1.to_i, ($1.to_i + $2.to_i), true
|
167
|
+
|
168
|
+
when String
|
169
|
+
if @regex_opts || str =~ PATH_CHAR_MATCHER
|
170
|
+
|
171
|
+
# Remove extra suffix characters
|
172
|
+
str.gsub! %r{(^|[^#{Path::RECH}])(\*+\?*)}, '\1*'
|
173
|
+
str.gsub! %r{(^|[^#{Path::RECH}])\*+}, '\1*'
|
174
|
+
|
175
|
+
str = Regexp.escape str
|
176
|
+
|
177
|
+
# Remove escaping from special path characters
|
178
|
+
str.gsub! %r{#{Path::RECH}([#{PATH_CHARS}])}, '\1'
|
179
|
+
str.gsub! %r{#{Path::RECH}([#{RESC_CHARS}])}, '\1'
|
180
|
+
str.gsub! %r{(^|[^#{Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
|
181
|
+
str.gsub! %r{(^|[^\.#{Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
|
182
|
+
|
183
|
+
Regexp.new "\\A(?:#{str})\\Z", @regex_opts
|
184
|
+
|
185
|
+
else
|
186
|
+
str.gsub %r{#{Path::RECH}([^#{Path::RECH}]|$)}, '\1'
|
187
|
+
end
|
188
|
+
|
189
|
+
else
|
190
|
+
str
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,341 @@
|
|
1
|
+
##
|
2
|
+
# Path Transactions are a convenient way to apply selections and deletions
|
3
|
+
# to complex data structures without having to know what state the data will
|
4
|
+
# be in after each operation.
|
5
|
+
#
|
6
|
+
# data = [
|
7
|
+
# {:name => "Jamie", :id => "12345"},
|
8
|
+
# {:name => "Adam", :id => "54321"},
|
9
|
+
# {:name => "Kari", :id => "12345"},
|
10
|
+
# {:name => "Grant", :id => "12345"},
|
11
|
+
# {:name => "Tory", :id => "12345"},
|
12
|
+
# ]
|
13
|
+
#
|
14
|
+
# # Select all element names, delete the one at index 2,
|
15
|
+
# # and move the element with the value "Tory" to the same path but
|
16
|
+
# # with the key renamed to "boo"
|
17
|
+
# Transaction.run data do |t|
|
18
|
+
# t.select "*/name"
|
19
|
+
# t.move "**=Tory" => "%%/boo"
|
20
|
+
# t.delete "2"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # => [
|
24
|
+
# # {:name => "Jamie"},
|
25
|
+
# # {:name => "Adam"},
|
26
|
+
# # {:name => "Grant"},
|
27
|
+
# # {"boo" => "Tory"},
|
28
|
+
# # ]
|
29
|
+
|
30
|
+
class Path::Transaction
|
31
|
+
|
32
|
+
##
|
33
|
+
# Create new Transaction instance and run it with a block.
|
34
|
+
# Equivalent to:
|
35
|
+
# Transaction.new(data).run(opts)
|
36
|
+
|
37
|
+
def self.run data, opts={}, &block
|
38
|
+
new(data).run opts, &block
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
attr_accessor :actions
|
43
|
+
|
44
|
+
##
|
45
|
+
# Create a new Transaction instance with a the data object to perform
|
46
|
+
# operations on.
|
47
|
+
|
48
|
+
def initialize data
|
49
|
+
@data = data
|
50
|
+
@new_data = nil
|
51
|
+
@actions = []
|
52
|
+
|
53
|
+
@make_array = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
##
|
58
|
+
# Run operations as a transaction.
|
59
|
+
# See Transaction#results for supported options.
|
60
|
+
|
61
|
+
def run opts={}, &block
|
62
|
+
clear
|
63
|
+
yield self if block_given?
|
64
|
+
results opts
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
##
|
69
|
+
# Returns the results of the transaction operations.
|
70
|
+
# To keep the original indicies of modified arrays, and return them as hashes,
|
71
|
+
# pass the :keep_indicies => true option.
|
72
|
+
|
73
|
+
def results opts={}
|
74
|
+
new_data = @data
|
75
|
+
|
76
|
+
@actions.each do |type, paths|
|
77
|
+
new_data = send("transaction_#{type}", new_data, *paths)
|
78
|
+
end
|
79
|
+
|
80
|
+
remake_arrays new_data, opts[:keep_indicies]
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def remake_arrays new_data, except_modified=false # :nodoc:
|
85
|
+
remake_paths = @make_array.keys.sort{|p1, p2| p2.length <=> p1.length}
|
86
|
+
|
87
|
+
remake_paths.each do |path_arr|
|
88
|
+
key = path_arr.last
|
89
|
+
obj = Path.data_at_path path_arr[0..-2], new_data
|
90
|
+
|
91
|
+
next unless obj && Hash === obj[key]
|
92
|
+
|
93
|
+
if except_modified
|
94
|
+
data_at_path = Path.data_at_path(path_arr, @data)
|
95
|
+
next if !data_at_path || obj[key].length != data_at_path.length
|
96
|
+
end
|
97
|
+
|
98
|
+
obj[key] = hash_to_ary obj[key]
|
99
|
+
end
|
100
|
+
|
101
|
+
new_data = hash_to_ary new_data if
|
102
|
+
(remake_paths.last == [] || Array === @data && Hash === new_data) &&
|
103
|
+
(!except_modified || @data.length == new_data.length)
|
104
|
+
|
105
|
+
new_data
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def remap_make_arrays new_path, old_path # :nodoc:
|
110
|
+
@make_array[new_path] = true and return if @make_array[old_path]
|
111
|
+
|
112
|
+
@make_array.keys.each do |path|
|
113
|
+
if path[0...old_path.length] == old_path
|
114
|
+
path[0...old_path.length] = new_path
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def transaction_select data, *data_paths # :nodoc:
|
121
|
+
return data if data_paths.empty?
|
122
|
+
|
123
|
+
transaction data, data_paths, true do |sdata, cdata, key, path, tpath|
|
124
|
+
sdata[key] = cdata[key]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
def transaction_delete data, *data_paths # :nodoc:
|
130
|
+
transaction data, data_paths do |new_curr_data, curr_data, key|
|
131
|
+
new_curr_data.delete key
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def transaction_move data, *path_pairs # :nodoc:
|
137
|
+
return data if path_pairs.empty?
|
138
|
+
path_val_hash = {}
|
139
|
+
|
140
|
+
new_data =
|
141
|
+
transaction data, path_pairs do |sdata, cdata, key, path, tpath|
|
142
|
+
path_val_hash[tpath] = sdata.delete key
|
143
|
+
remap_make_arrays(tpath, path)
|
144
|
+
end
|
145
|
+
|
146
|
+
force_assign_paths new_data, path_val_hash
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
def transaction_map data, *path_pairs # :nodoc:
|
151
|
+
return data if path_pairs.empty?
|
152
|
+
path_val_hash = {}
|
153
|
+
|
154
|
+
transaction data, path_pairs do |sdata, cdata, key, path, tpath|
|
155
|
+
tpath ||= path
|
156
|
+
path_val_hash[tpath] = sdata.delete key
|
157
|
+
remap_make_arrays(tpath, path)
|
158
|
+
end
|
159
|
+
|
160
|
+
force_assign_paths [], path_val_hash
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
def transaction data, data_paths, create_empty=false # :nodoc:
|
165
|
+
data_paths = data_paths.compact
|
166
|
+
return @new_data || data if data_paths.empty?
|
167
|
+
|
168
|
+
@new_data = create_empty ? Hash.new : data.dup
|
169
|
+
|
170
|
+
if Array === @new_data
|
171
|
+
@new_data = ary_to_hash @new_data
|
172
|
+
end
|
173
|
+
|
174
|
+
data_paths.each do |data_path|
|
175
|
+
# If data_path is an array, the second element is the path where the value
|
176
|
+
# should be mapped to.
|
177
|
+
data_path, target_path = data_path
|
178
|
+
|
179
|
+
Path.find data_path, data do |obj, k, path|
|
180
|
+
curr_data = data
|
181
|
+
new_curr_data = @new_data
|
182
|
+
|
183
|
+
path.each_with_index do |key, i|
|
184
|
+
break unless new_curr_data
|
185
|
+
|
186
|
+
if i == path.length - 1
|
187
|
+
tpath = path.make_path target_path if target_path
|
188
|
+
yield new_curr_data, curr_data, key, path, tpath if block_given?
|
189
|
+
|
190
|
+
else
|
191
|
+
if create_empty
|
192
|
+
new_curr_data[key] ||= Hash.new
|
193
|
+
|
194
|
+
elsif new_curr_data[key] == curr_data[key]
|
195
|
+
new_curr_data[key] = Array === new_curr_data[key] ?
|
196
|
+
ary_to_hash(curr_data[key]) :
|
197
|
+
curr_data[key].dup
|
198
|
+
end
|
199
|
+
|
200
|
+
@make_array[path[0..i]] = true if Array === curr_data[key]
|
201
|
+
|
202
|
+
new_curr_data = new_curr_data[key]
|
203
|
+
curr_data = curr_data[key]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
@new_data
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
def force_assign_paths data, path_val_hash # :nodoc:
|
214
|
+
return data if path_val_hash.empty?
|
215
|
+
@new_data = (data.dup rescue [])
|
216
|
+
|
217
|
+
path_val_hash.each do |path, value|
|
218
|
+
curr_data = data
|
219
|
+
new_curr_data = @new_data
|
220
|
+
prev_data = nil
|
221
|
+
prev_key = nil
|
222
|
+
prev_path = []
|
223
|
+
|
224
|
+
path.each_with_index do |key, i|
|
225
|
+
if Array === new_curr_data
|
226
|
+
new_curr_data = ary_to_hash new_curr_data
|
227
|
+
prev_data[prev_key] = new_curr_data if prev_data
|
228
|
+
@new_data = new_curr_data if i == 0
|
229
|
+
@make_array[prev_path] = true if i == 0
|
230
|
+
end
|
231
|
+
|
232
|
+
last = i == path.length - 1
|
233
|
+
prev_path = path[0..(i-1)] if i > 0
|
234
|
+
curr_path = path[0..i]
|
235
|
+
next_key = path[i+1]
|
236
|
+
|
237
|
+
# new_curr_data is a hash from here on
|
238
|
+
|
239
|
+
@make_array.delete prev_path unless is_integer?(key)
|
240
|
+
|
241
|
+
new_curr_data[key] = value and break if last
|
242
|
+
|
243
|
+
if ary_or_hash?(curr_data) && child_ary_or_hash?(curr_data, key)
|
244
|
+
new_curr_data[key] ||= curr_data[key]
|
245
|
+
|
246
|
+
elsif !ary_or_hash?(new_curr_data[key])
|
247
|
+
new_curr_data[key] = is_integer?(next_key) ? [] : {}
|
248
|
+
end
|
249
|
+
|
250
|
+
@make_array[curr_path] = true if Array === new_curr_data[key]
|
251
|
+
|
252
|
+
prev_key = key
|
253
|
+
prev_data = new_curr_data
|
254
|
+
new_curr_data = new_curr_data[key]
|
255
|
+
curr_data = curr_data[key] if ary_or_hash?(curr_data) rescue nil
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
@new_data
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
def is_integer? item # :nodoc:
|
264
|
+
item.to_s.to_i.to_s == item.to_s
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
def ary_or_hash? obj # :nodoc:
|
269
|
+
Array === obj || Hash === obj
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
def child_ary_or_hash? obj, key
|
274
|
+
ary_or_hash?(obj[key]) rescue false
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
def ary_to_hash ary # :nodoc:
|
279
|
+
hash = {}
|
280
|
+
ary.each_with_index{|val, i| hash[i] = val}
|
281
|
+
hash
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
def hash_to_ary hash # :nodoc:
|
286
|
+
hash.keys.sort.map{|k| hash[k] }
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
##
|
291
|
+
# Clears the queued actions and cache.
|
292
|
+
|
293
|
+
def clear
|
294
|
+
@new_data = nil
|
295
|
+
@actions.clear
|
296
|
+
@make_array.clear
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
##
|
301
|
+
# Queues path selects for transaction.
|
302
|
+
|
303
|
+
def select *paths
|
304
|
+
if @actions.last && @actions.last[0] == :select
|
305
|
+
@actions.last[1].concat paths
|
306
|
+
else
|
307
|
+
@actions << [:select, paths]
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
##
|
313
|
+
# Queues path deletes for transaction.
|
314
|
+
|
315
|
+
def delete *paths
|
316
|
+
@actions << [:delete, paths]
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
##
|
321
|
+
# Queues path moving for transaction. Moving a path will attempt to
|
322
|
+
# keep the original data structure and only affect the given paths.
|
323
|
+
# Empty hashes or arrays after a move are deleted.
|
324
|
+
# t.move "my/path/1..4/key" => "new_path/%1/key",
|
325
|
+
# "other/path/*" => "moved/%1"
|
326
|
+
|
327
|
+
def move path_maps
|
328
|
+
@actions << [:move, Array(path_maps)]
|
329
|
+
end
|
330
|
+
|
331
|
+
|
332
|
+
##
|
333
|
+
# Queues path mapping for transaction. Mapping a path will only keep the
|
334
|
+
# mapped values, completely replacing the original data structure.
|
335
|
+
# t.map "my/path/1..4/key" => "new_path/%1/key",
|
336
|
+
# "other/path/*" => "moved/%1"
|
337
|
+
|
338
|
+
def map path_maps
|
339
|
+
@actions << [:map, Array(path_maps)]
|
340
|
+
end
|
341
|
+
end
|