object_patch 0.8.1
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.
- 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
|
+
|