Class: Path::Transaction

Inherits:
Object
  • Object
show all
Defined in:
lib/path/transaction.rb

Overview

Path Transactions are a convenient way to apply selections and deletions to complex data structures without having to know what state the data will be in after each operation.

data = [
  {:name => "Jamie", :id => "12345"},
  {:name => "Adam",  :id => "54321"},
  {:name => "Kari",  :id => "12345"},
  {:name => "Grant", :id => "12345"},
  {:name => "Tory",  :id => "12345"},
]

# Select all element names, delete the one at index 2,
# and move the element with the value "Tory" to the same path but
# with the key renamed to "boo"
Transaction.run data do |t|
  t.select "*/name"
  t.move "**=Tory" => "%%/boo"
  t.delete "2"
end

# => [
#  {:name => "Jamie"},
#  {:name => "Adam"},
#  {:name => "Grant"},
#  {"boo" => "Tory"},
# ]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ Transaction

Create a new Transaction instance with a the data object to perform operations on.



48
49
50
51
52
53
54
# File 'lib/path/transaction.rb', line 48

def initialize data
  @data     = data
  @new_data = nil
  @actions  = []

  @make_array = {}
end

Instance Attribute Details

#actionsObject

Returns the value of attribute actions.



42
43
44
# File 'lib/path/transaction.rb', line 42

def actions
  @actions
end

Class Method Details

.run(data, opts = {}, &block) ⇒ Object

Create new Transaction instance and run it with a block. Equivalent to:

Transaction.new(data).run(opts)


37
38
39
# File 'lib/path/transaction.rb', line 37

def self.run data, opts={}, &block
  new(data).run opts, &block
end

Instance Method Details

#ary_or_hash?(obj) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


268
269
270
# File 'lib/path/transaction.rb', line 268

def ary_or_hash? obj # :nodoc:
  Array === obj || Hash === obj
end

#ary_to_hash(ary) ⇒ Object

:nodoc:



278
279
280
281
282
# File 'lib/path/transaction.rb', line 278

def ary_to_hash ary # :nodoc:
  hash = {}
  ary.each_with_index{|val, i| hash[i] = val}
  hash
end

#child_ary_or_hash?(obj, key) ⇒ Boolean

Returns:

  • (Boolean)


273
274
275
# File 'lib/path/transaction.rb', line 273

def child_ary_or_hash? obj, key
  ary_or_hash?(obj[key]) rescue false
end

#clearObject

Clears the queued actions and cache.



293
294
295
296
297
# File 'lib/path/transaction.rb', line 293

def clear
  @new_data = nil
  @actions.clear
  @make_array.clear
end

#delete(*paths) ⇒ Object

Queues path deletes for transaction.



315
316
317
# File 'lib/path/transaction.rb', line 315

def delete *paths
  @actions << [:delete, paths]
end

#force_assign_paths(data, path_val_hash) ⇒ Object

:nodoc:



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/path/transaction.rb', line 213

def force_assign_paths data, path_val_hash # :nodoc:
  return data if path_val_hash.empty?
  @new_data = (data.dup rescue [])

  path_val_hash.each do |path, value|
    curr_data     = data
    new_curr_data = @new_data
    prev_data     = nil
    prev_key      = nil
    prev_path     = []

    path.each_with_index do |key, i|
      if Array === new_curr_data
        new_curr_data          = ary_to_hash new_curr_data
        prev_data[prev_key]    = new_curr_data if prev_data
        @new_data              = new_curr_data if i == 0
        @make_array[prev_path] = true          if i == 0
      end

      last      = i == path.length - 1
      prev_path = path[0..(i-1)] if i > 0
      curr_path = path[0..i]
      next_key  = path[i+1]

      # new_curr_data is a hash from here on

      @make_array.delete prev_path unless is_integer?(key)

      new_curr_data[key] = value and break if last

      if ary_or_hash?(curr_data) && child_ary_or_hash?(curr_data, key)
        new_curr_data[key] ||= curr_data[key]

      elsif !ary_or_hash?(new_curr_data[key])
        new_curr_data[key] = is_integer?(next_key) ? [] : {}
      end

      @make_array[curr_path] = true if Array === new_curr_data[key]

      prev_key      = key
      prev_data     = new_curr_data
      new_curr_data = new_curr_data[key]
      curr_data     = curr_data[key] if ary_or_hash?(curr_data) rescue nil
    end
  end

  @new_data
end

#hash_to_ary(hash) ⇒ Object

:nodoc:



285
286
287
# File 'lib/path/transaction.rb', line 285

def hash_to_ary hash # :nodoc:
  hash.keys.sort.map{|k| hash[k] }
end

#is_integer?(item) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


263
264
265
# File 'lib/path/transaction.rb', line 263

def is_integer? item # :nodoc:
  item.to_s.to_i.to_s == item.to_s
end

#map(path_maps) ⇒ Object

Queues path mapping for transaction. Mapping a path will only keep the mapped values, completely replacing the original data structure.

t.map "my/path/1..4/key" => "new_path/%1/key",
      "other/path/*"     => "moved/%1"


338
339
340
# File 'lib/path/transaction.rb', line 338

def map path_maps
  @actions << [:map, Array(path_maps)]
end

#move(path_maps) ⇒ Object

Queues path moving for transaction. Moving a path will attempt to keep the original data structure and only affect the given paths. Empty hashes or arrays after a move are deleted.

t.move "my/path/1..4/key" => "new_path/%1/key",
       "other/path/*"     => "moved/%1"


327
328
329
# File 'lib/path/transaction.rb', line 327

def move path_maps
  @actions << [:move, Array(path_maps)]
end

#remake_arrays(new_data, except_modified = false) ⇒ Object

:nodoc:



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/path/transaction.rb', line 84

def remake_arrays new_data, except_modified=false # :nodoc:
  remake_paths = @make_array.keys.sort{|p1, p2| p2.length <=> p1.length}

  remake_paths.each do |path_arr|
    key  = path_arr.last
    obj = Path.data_at_path path_arr[0..-2], new_data

    next unless obj && Hash === obj[key]

    if except_modified
      data_at_path = Path.data_at_path(path_arr, @data)
      next if !data_at_path || obj[key].length != data_at_path.length
    end

    obj[key] = hash_to_ary obj[key]
  end

  new_data = hash_to_ary new_data if
    (remake_paths.last == [] || Array === @data && Hash === new_data) &&
    (!except_modified || @data.length == new_data.length)

  new_data
end

#remap_make_arrays(new_path, old_path) ⇒ Object

:nodoc:



109
110
111
112
113
114
115
116
117
# File 'lib/path/transaction.rb', line 109

def remap_make_arrays new_path, old_path # :nodoc:
  @make_array[new_path] = true and return if @make_array[old_path]

  @make_array.keys.each do |path|
    if path[0...old_path.length] == old_path
      path[0...old_path.length] = new_path
    end
  end
end

#results(opts = {}) ⇒ Object

Returns the results of the transaction operations. To keep the original indicies of modified arrays, and return them as hashes, pass the :keep_indicies => true option.



73
74
75
76
77
78
79
80
81
# File 'lib/path/transaction.rb', line 73

def results opts={}
  new_data = @data

  @actions.each do |type, paths|
    new_data = send("transaction_#{type}", new_data, *paths)
  end

  remake_arrays new_data, opts[:keep_indicies]
end

#run(opts = {}) {|_self| ... } ⇒ Object

Run operations as a transaction. See Transaction#results for supported options.

Yields:

  • (_self)

Yield Parameters:



61
62
63
64
65
# File 'lib/path/transaction.rb', line 61

def run opts={}, &block
  clear
  yield self if block_given?
  results opts
end

#select(*paths) ⇒ Object

Queues path selects for transaction.



303
304
305
306
307
308
309
# File 'lib/path/transaction.rb', line 303

def select *paths
  if @actions.last && @actions.last[0] == :select
    @actions.last[1].concat paths
  else
    @actions << [:select, paths]
  end
end

#transaction(data, data_paths, create_empty = false) ⇒ Object

:nodoc:



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/path/transaction.rb', line 164

def transaction data, data_paths, create_empty=false # :nodoc:
  data_paths = data_paths.compact
  return @new_data || data if data_paths.empty?

  @new_data = create_empty ? Hash.new : data.dup

  if Array === @new_data
    @new_data = ary_to_hash @new_data
  end

  data_paths.each do |data_path|
    # If data_path is an array, the second element is the path where the value
    # should be mapped to.
    data_path, target_path = data_path

    Path.find data_path, data do |obj, k, path|
      curr_data     = data
      new_curr_data = @new_data

      path.each_with_index do |key, i|
        break unless new_curr_data

        if i == path.length - 1
          tpath = path.make_path target_path if target_path
          yield new_curr_data, curr_data, key, path, tpath if block_given?

        else
          if create_empty
            new_curr_data[key] ||= Hash.new

          elsif new_curr_data[key] == curr_data[key]
            new_curr_data[key] = Array === new_curr_data[key] ?
                                  ary_to_hash(curr_data[key]) :
                                  curr_data[key].dup
          end

          @make_array[path[0..i]] = true if Array === curr_data[key]

          new_curr_data = new_curr_data[key]
          curr_data     = curr_data[key]
        end
      end
    end
  end

  @new_data
end

#transaction_delete(data, *data_paths) ⇒ Object

:nodoc:



129
130
131
132
133
# File 'lib/path/transaction.rb', line 129

def transaction_delete data, *data_paths # :nodoc:
  transaction data, data_paths do |new_curr_data, curr_data, key|
    new_curr_data.delete key
  end
end

#transaction_map(data, *path_pairs) ⇒ Object

:nodoc:



150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/path/transaction.rb', line 150

def transaction_map data, *path_pairs # :nodoc:
  return data if path_pairs.empty?
  path_val_hash = {}

  transaction data, path_pairs do |sdata, cdata, key, path, tpath|
    tpath ||= path
    path_val_hash[tpath] = sdata.delete key
    remap_make_arrays(tpath, path)
  end

  force_assign_paths [], path_val_hash
end

#transaction_move(data, *path_pairs) ⇒ Object

:nodoc:



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/path/transaction.rb', line 136

def transaction_move data, *path_pairs # :nodoc:
  return data if path_pairs.empty?
  path_val_hash = {}

  new_data =
    transaction data, path_pairs do |sdata, cdata, key, path, tpath|
      path_val_hash[tpath] = sdata.delete key
      remap_make_arrays(tpath, path)
    end

  force_assign_paths new_data, path_val_hash
end

#transaction_select(data, *data_paths) ⇒ Object

:nodoc:



120
121
122
123
124
125
126
# File 'lib/path/transaction.rb', line 120

def transaction_select data, *data_paths # :nodoc:
  return data if data_paths.empty?

  transaction data, data_paths, true do |sdata, cdata, key, path, tpath|
    sdata[key] = cdata[key]
  end
end