object_patch 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.gitmodules +4 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/.yardopts +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +77 -0
- data/Rakefile +18 -0
- data/TODO.md +7 -0
- data/lib/object_patch.rb +46 -0
- data/lib/object_patch/exceptions.rb +51 -0
- data/lib/object_patch/generator.rb +114 -0
- data/lib/object_patch/operation_factory.rb +25 -0
- data/lib/object_patch/operations.rb +79 -0
- data/lib/object_patch/operations/add.rb +57 -0
- data/lib/object_patch/operations/copy.rb +69 -0
- data/lib/object_patch/operations/move.rb +63 -0
- data/lib/object_patch/operations/remove.rb +49 -0
- data/lib/object_patch/operations/replace.rb +59 -0
- data/lib/object_patch/operations/test.rb +47 -0
- data/lib/object_patch/pointer.rb +90 -0
- data/lib/object_patch/version.rb +4 -0
- data/object_patch.gemspec +28 -0
- data/spec/fixtures/tenderlove_tests.json +36 -0
- data/spec/helpers/json_test_loader.rb +19 -0
- data/spec/object_patch/fixture_tests_spec.rb +62 -0
- data/spec/object_patch/object_patch_spec.rb +9 -0
- data/spec/object_patch/pointer_spec.rb +52 -0
- data/spec/spec_helper.rb +24 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1d001241ee49b3b29f9a8a04bd8aa4ea32720175
|
4
|
+
data.tar.gz: 4dd6636e8c29378d7038cacf6972697ef1d69f71
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 69fecfe300ec106c18761625fa0e0b059dc69b1f2e62a2d5e9b1b0fe3620e6b57146bb6d153d5448b0c013c06af895a178f13f02ea70d000b87f85b1187cc2dd
|
7
|
+
data.tar.gz: a8759ce823f1e00a6b43bf15aab0714d76a2f33c364bb3a94f921cc5c02904c3c50fa01792ce348730fa736b249a4b1a6db0f4522e977edac039a1720e8c29e2
|
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sam Stelfox
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# ObjectPatch
|
2
|
+
|
3
|
+
Please note this project isn't complete in either the generation or application
|
4
|
+
of patches.
|
5
|
+
|
6
|
+
ObjectPatch is a pure ruby implementation of [RFC6902
|
7
|
+
(JSON::Patch)](http://tools.ietf.org/rfc/rfc6902.txt) for standard hashes,
|
8
|
+
arrays, and scalar types. This will both generate patches as well as apply
|
9
|
+
them.
|
10
|
+
|
11
|
+
Rather than restricting end-users to the native JSON library, ObjectPatch only
|
12
|
+
operates on pure Ruby objects. These objects can be converted to a proper JSON
|
13
|
+
encoding using the standard JSON library or any other JSON compliant encoder.
|
14
|
+
|
15
|
+
The application of patches is based on
|
16
|
+
[Hana](http://github.com/tenderlove/hana). I greatly respect tenderlove but
|
17
|
+
disagreed with pieces of his implementation. I could have chosen to make pull
|
18
|
+
requests but since I was going to be extending the scope of the project I
|
19
|
+
decided to create my own project. At the very least it seemed like a fun
|
20
|
+
project to attempt.
|
21
|
+
|
22
|
+
The generation of patches attempts to do so in a way to minimize the size of
|
23
|
+
the patch, this is particularily difficult in arrays where the deletion of a
|
24
|
+
single element at the beginning may be hard to distinguish from the changing of
|
25
|
+
multiple values and the removal of the last.
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
Add this line to your application's Gemfile:
|
30
|
+
|
31
|
+
gem 'object_patch'
|
32
|
+
|
33
|
+
And then execute:
|
34
|
+
|
35
|
+
$ bundle
|
36
|
+
|
37
|
+
Or install it yourself as:
|
38
|
+
|
39
|
+
$ gem install object_patch
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
TODO: Write usage instructions here
|
44
|
+
|
45
|
+
## RFC6901
|
46
|
+
|
47
|
+
There is one thing that I implemented within this RFC that is either an
|
48
|
+
extension or a violation of the RFC depending on your view of it. The violation
|
49
|
+
specifically is I have allowed negative array addressing (ie -1 is the last
|
50
|
+
element, -2 the second to last, &c). The absolute value of the index can't be
|
51
|
+
greater than or equal to the the length of the array.
|
52
|
+
|
53
|
+
## RFC6902
|
54
|
+
|
55
|
+
One thing to note is that while referencing [RFC6902
|
56
|
+
(JSON::Patch)](http://tools.ietf.org/rfc/rfc6902.txt) it came to my attention
|
57
|
+
that the published RFC was missing a section that was part of the accepted
|
58
|
+
revision (specifically the appendix). The revision of the document that was
|
59
|
+
accepted by the IETF can be [found
|
60
|
+
here](http://tools.ietf.org/id/draft-ietf-appsawg-json-patch-10.txt). This was
|
61
|
+
gleaned from the public history of review of the RFC which is [available
|
62
|
+
here](https://datatracker.ietf.org/doc/rfc6902/history/).
|
63
|
+
|
64
|
+
I referenced the draft version 10 for any information available in the appendix
|
65
|
+
and the published version when the information was available there.
|
66
|
+
|
67
|
+
## Contributing
|
68
|
+
|
69
|
+
1. Fork it
|
70
|
+
2. Create your feature branch off of the current `develop` head (`git checkout
|
71
|
+
-b my-new-feature origin/develop`)
|
72
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
73
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
74
|
+
5. Create new Pull Request
|
75
|
+
|
76
|
+
I'll respond to all pull requests within two weeks, hopefully in under one.
|
77
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
task :environment do
|
10
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
11
|
+
require 'object_patch'
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Run a pry session with the gem code loaded"
|
15
|
+
task :console => :environment do
|
16
|
+
require 'pry'
|
17
|
+
pry
|
18
|
+
end
|
data/TODO.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
|
2
|
+
* Apparently string reuse is better done through constants to prevent them
|
3
|
+
from being recreated all the time such as accessing common hash keys, think
|
4
|
+
'path', 'from', 'value', 'op'.
|
5
|
+
* The big one: Generation of patches based on the difference of two provided
|
6
|
+
hashes.
|
7
|
+
|
data/lib/object_patch.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
require "object_patch/exceptions"
|
4
|
+
require "object_patch/generator"
|
5
|
+
require "object_patch/operation_factory"
|
6
|
+
require "object_patch/operations"
|
7
|
+
require "object_patch/pointer"
|
8
|
+
require "object_patch/version"
|
9
|
+
|
10
|
+
module ObjectPatch # :nodoc:
|
11
|
+
|
12
|
+
# Applies a series of patches to a source document. It is important to note
|
13
|
+
# that this will return the changed document but will also modify the original
|
14
|
+
# document that got passed in, using dup also isn't enough to prevent this
|
15
|
+
# modification as any subvalues within a hash or an array will not by
|
16
|
+
# duplicated. A deep duplication by marshalling the object, encoding and
|
17
|
+
# decoding as JSON, or using a recursive function to duplicate all of an
|
18
|
+
# object's contents would prevent your source from being modified.
|
19
|
+
#
|
20
|
+
# The JSON Patch RFC specifies that the original document shouldn't be
|
21
|
+
# modified unless the whole series of patches is able to be applied
|
22
|
+
# successfully.
|
23
|
+
#
|
24
|
+
# @param [Object] source The source document that will be modified.
|
25
|
+
# @param [Array<Hash<String => String>>] patches An array of JSON patch
|
26
|
+
# operations that should be applied to the source document.
|
27
|
+
# @return [Object] The modified source document.
|
28
|
+
def apply(source, patches)
|
29
|
+
patches.inject(source) do |src, patch|
|
30
|
+
OperationFactory.build(patch).apply(src)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Proxies the generation of a new set of patches to the Generator.
|
35
|
+
#
|
36
|
+
# @see [Generator#generate]
|
37
|
+
# @param [Object] source The source document
|
38
|
+
# @param [Object] target The target document we'll generate the
|
39
|
+
# differences for.
|
40
|
+
# @return [Array<Hash>]
|
41
|
+
def generate(source, target)
|
42
|
+
Generator.generate(source, target)
|
43
|
+
end
|
44
|
+
|
45
|
+
module_function :apply, :generate
|
46
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch
|
3
|
+
# This is a parent exception for anything that is going to get raised by this
|
4
|
+
# gem, allowing users of this code to catch all the subclasses.
|
5
|
+
BaseException = Class.new(StandardError)
|
6
|
+
|
7
|
+
# Raised when the index value that was attempted to be accessed isn't a
|
8
|
+
# numeric identifier or '-' (the special index value defined in JSON Pointer).
|
9
|
+
InvalidIndexError = Class.new(BaseException)
|
10
|
+
|
11
|
+
# An exception that gets raised when a patch contains an invalid operation.
|
12
|
+
InvalidOperation = Class.new(BaseException)
|
13
|
+
|
14
|
+
# An exception that gets raised when an operation takes place on a missing
|
15
|
+
# hash key, or a missing hash key is somewhere along the path.
|
16
|
+
MissingTargetException = Class.new(BaseException)
|
17
|
+
|
18
|
+
# Raised when a non-integer value is attempted to be used to access some part
|
19
|
+
# of an array.
|
20
|
+
ObjectOperationOnArrayException = Class.new(BaseException)
|
21
|
+
|
22
|
+
# When an integer value outside the available range of an array is used to
|
23
|
+
# access an array this will get raised.
|
24
|
+
OutOfBoundsException = Class.new(BaseException)
|
25
|
+
|
26
|
+
# When the path provided attempts to cross a scalar value, this exception will
|
27
|
+
# be raised.
|
28
|
+
TraverseScalarException = Class.new(BaseException)
|
29
|
+
|
30
|
+
# An exception that will get raised when a test operation fails after being
|
31
|
+
# applied to a document.
|
32
|
+
#
|
33
|
+
# @attr [String] path A JSON pointer string representing the location to be
|
34
|
+
# checked.
|
35
|
+
# @attr [Object] value The value that failed the comparison check.
|
36
|
+
class FailedTestException < BaseException
|
37
|
+
attr_accessor :path, :value
|
38
|
+
|
39
|
+
# Formats the exception message with the relevant information before
|
40
|
+
# actually raising the error.
|
41
|
+
#
|
42
|
+
# @param [String] path
|
43
|
+
# @param [Object] value
|
44
|
+
# @return [void]
|
45
|
+
def initialize(path, value)
|
46
|
+
super("Expected #{value} at #{path}")
|
47
|
+
@path, @value = path, value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,114 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch
|
3
|
+
|
4
|
+
# This module handles the generation of patches between two objects.
|
5
|
+
module Generator
|
6
|
+
|
7
|
+
# Generate a series of patch operations that describe the changes from the
|
8
|
+
# source object to the target object.
|
9
|
+
#
|
10
|
+
# @todo This doesn't do anything yet...
|
11
|
+
# @param [Object] source The source document
|
12
|
+
# @param [Object] target The target document we'll generate the
|
13
|
+
# differences for.
|
14
|
+
# @return [Array<Hash>]
|
15
|
+
def generate(source, target, current_path = [])
|
16
|
+
if source.class != target.class
|
17
|
+
# The simplest of cases is that we have incompatible types, in which
|
18
|
+
# case we'll full replace the root. This of course could be optimized
|
19
|
+
# for situations where the root as moved to a child element but that is
|
20
|
+
# harder to detect...
|
21
|
+
return [Operations::Replace.new("path" => Pointer.encode(current_path), "value" => target)]
|
22
|
+
end
|
23
|
+
|
24
|
+
case source.class.to_s
|
25
|
+
when "Hash"
|
26
|
+
return hash_compare(source, target, current_path)
|
27
|
+
when "Array"
|
28
|
+
return array_compare(source, target, current_path)
|
29
|
+
else
|
30
|
+
# A scaler value
|
31
|
+
return [] if source == target
|
32
|
+
return [Operations::Replace.new("path" => Pointer.encode(current_path), "value" => target)]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def hash_compare(src_hash, tgt_hash, current_path)
|
37
|
+
operations = []
|
38
|
+
|
39
|
+
# Keys to remove
|
40
|
+
(src_hash.keys - tgt_hash.keys).each do |k|
|
41
|
+
path = Pointer.encode(current_path + Array(k))
|
42
|
+
operations.push(
|
43
|
+
Operations::Test.new("path" => path, "value" => src_hash[k]),
|
44
|
+
Operations::Remove.new("path" => path)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Missing keys to add
|
49
|
+
(tgt_hash.keys - src_hash.keys).each do |k|
|
50
|
+
path = Pointer.encode(current_path + Array(k))
|
51
|
+
operations.push(
|
52
|
+
Operations::Add.new("path" => path, "value" => tgt_hash[k])
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# When both hashes share the same key we need to go deeper...
|
57
|
+
(src_hash.keys & tgt_hash.keys).each do |k|
|
58
|
+
operations += generate(src_hash[k], tgt_hash[k], current_path + Array(k))
|
59
|
+
end
|
60
|
+
|
61
|
+
operations
|
62
|
+
end
|
63
|
+
|
64
|
+
def array_compare(src_ary, tgt_ary, current_path)
|
65
|
+
operations = []
|
66
|
+
|
67
|
+
if src_ary.size > tgt_ary.size
|
68
|
+
# We'll need to remove some elements
|
69
|
+
base_size = tgt_ary.size
|
70
|
+
src_ary[base_size..-1].each_with_index do |itm, idx|
|
71
|
+
path = Pointer.encode(current_path + Array(base_size + idx))
|
72
|
+
operations.push(
|
73
|
+
Operations::Test.new("path" => path, "value" => src_ary[base_size + idx]),
|
74
|
+
Operations::Remove.new("path" => path)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
elsif src_ary.size < tgt_ary.size
|
78
|
+
# We'll need to add some elements
|
79
|
+
base_size = tgt_ary.size
|
80
|
+
src_ary[base_size..-1].each_with_index do |itm, idx|
|
81
|
+
path = Pointer.encode(current_path + Array(base_size + idx))
|
82
|
+
operations.push(
|
83
|
+
Operations::Add.new("path" => path, "value" => tgt_ary[base_size + idx]),
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Compare the existing values in the array
|
89
|
+
smallest_length = (src_ary.size > tgt_ary.size) ? tgt_ary.size : src_ary.size
|
90
|
+
smallest_length.times do |n|
|
91
|
+
# Handle arrays and hashes in a special way as their values are
|
92
|
+
# potentially not going to be able to hold up to a comparison.
|
93
|
+
if src_ary[n].is_a?(Array) || src_ary[n].is_a?(Hash) || tgt_ary[n].is_a?(Array) || tgt_ary[n].is_a?(Hash)
|
94
|
+
operations.push(*generate(src_ary[n], tgt_ary[n], current_path + Array(n)))
|
95
|
+
next
|
96
|
+
end
|
97
|
+
|
98
|
+
# We've gotten the complicated cases out, this is a simple test and
|
99
|
+
# replace.
|
100
|
+
unless src_ary[n] == tgt_ary[n]
|
101
|
+
path = Pointer.encode(current_path + Array(n))
|
102
|
+
operations.push(
|
103
|
+
Operations::Test.new("path" => path, "value" => src_ary[n]),
|
104
|
+
Operations::Replace.new("path" => path, "value" => tgt_ary[n]),
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
operations
|
110
|
+
end
|
111
|
+
|
112
|
+
module_function :array_compare, :generate, :hash_compare
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch
|
3
|
+
|
4
|
+
# A factory module used to build the appropriate operation objects.
|
5
|
+
module OperationFactory
|
6
|
+
|
7
|
+
# Build an operation object that matches the operation type and makes the
|
8
|
+
# appropriate information available to them.
|
9
|
+
#
|
10
|
+
# @param [Hash] patch
|
11
|
+
# @return [Object] One of the operations classes.
|
12
|
+
def build(patch)
|
13
|
+
operations = ObjectPatch::Operations.constants.map { |c| c.to_s.downcase }
|
14
|
+
|
15
|
+
unless operations.include?(patch['op'])
|
16
|
+
raise InvalidOperation, "Invalid operation: `#{patch['op']}`"
|
17
|
+
end
|
18
|
+
|
19
|
+
Operations.const_get(patch['op'].capitalize.to_sym).new(patch)
|
20
|
+
end
|
21
|
+
|
22
|
+
module_function :build
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
require "object_patch/operations/add"
|
3
|
+
require "object_patch/operations/copy"
|
4
|
+
require "object_patch/operations/move"
|
5
|
+
require "object_patch/operations/remove"
|
6
|
+
require "object_patch/operations/replace"
|
7
|
+
require "object_patch/operations/test"
|
8
|
+
|
9
|
+
module ObjectPatch
|
10
|
+
|
11
|
+
# These operations take advantage of the fact that Pointer#eval returns the
|
12
|
+
# same object (obj.object_id match) and thus any changes made to the
|
13
|
+
# extracted object will be reflected in the original deeply nested object.
|
14
|
+
module Operations
|
15
|
+
|
16
|
+
# Add a value at the provided key within the provided object. This will
|
17
|
+
# behave differently depending on whether we're processing a hash or an
|
18
|
+
# array as the target destination.
|
19
|
+
#
|
20
|
+
# It is important to note that this behaves by adjusting the state of the
|
21
|
+
# provided object. It does not return the new object itself!
|
22
|
+
#
|
23
|
+
# @param [Array, Hash] target_obj The object that will have the value added.
|
24
|
+
# @param [Fixnum,String] key The index / key where the new value will be
|
25
|
+
# inserted.
|
26
|
+
# @param [Object] new_value The value to insert at the specified location.
|
27
|
+
# @return [Object] The value that was added.
|
28
|
+
def add_op(target_obj, key, new_value)
|
29
|
+
if target_obj.is_a?(Array)
|
30
|
+
target_obj.insert(check_array_index(key, target_obj.size), new_value)
|
31
|
+
else
|
32
|
+
target_obj[key] = new_value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Validates that the array index provided falls within the acceptable range
|
37
|
+
# or in the event we have received the special '-' index defined in the
|
38
|
+
# JSON Pointer RFC we treat it as the last element.
|
39
|
+
#
|
40
|
+
# @param [String,Fixnum] index The index value to validate
|
41
|
+
# @param [Fixnum] array_size The size of the array this index will be used
|
42
|
+
# within (Used for bounds checking).
|
43
|
+
# @return [Fixnum] Valid index
|
44
|
+
def check_array_index(index, array_size)
|
45
|
+
return -1 if index == "-"
|
46
|
+
raise ObjectOperationOnArrayException unless index =~ /\A-?\d+\Z/
|
47
|
+
|
48
|
+
index = index.to_i
|
49
|
+
|
50
|
+
# There is a bug in the IETF tests that require us to allow patches to
|
51
|
+
# set a value at the end of the array. The final '<=' should actually be
|
52
|
+
# a '<'.
|
53
|
+
raise OutOfBoundsException unless (0 <= index && index <= array_size)
|
54
|
+
|
55
|
+
index
|
56
|
+
end
|
57
|
+
|
58
|
+
# Remove a hash key or index from the provided object.
|
59
|
+
#
|
60
|
+
# It is important to note that this behaves by adjusting the state of the
|
61
|
+
# provided object. It does not return the new object itself!
|
62
|
+
#
|
63
|
+
# @param [Array, Hash] target_obj The object that will have the value
|
64
|
+
# removed.
|
65
|
+
# @return [Object] The deleted object.
|
66
|
+
def rm_op(target_obj, key)
|
67
|
+
if target_obj.is_a?(Array)
|
68
|
+
raise InvalidIndexError unless key =~ /\A\d+\Z/
|
69
|
+
target_obj.delete_at(check_array_index(key, target_obj.size))
|
70
|
+
else
|
71
|
+
raise(MissingTargetException, key) unless target_obj.has_key?(key)
|
72
|
+
target_obj.delete(key)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module_function :add_op, :check_array_index, :rm_op
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|