-
Notifications
You must be signed in to change notification settings - Fork 28
/
hana.rb
255 lines (204 loc) · 6.01 KB
/
hana.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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
211
212
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
# frozen_string_literal: true
module Hana
VERSION = '1.3.7'
class Pointer
include Enumerable
class Exception < StandardError
end
class FormatError < Exception
end
def initialize path
@path = Pointer.parse path
end
def each(&block); @path.each(&block); end
def eval object
Pointer.eval @path, object
end
ESC = {'^/' => '/', '^^' => '^', '~0' => '~', '~1' => '/'} # :nodoc:
def self.eval list, object
list.inject(object) { |o, part|
return nil unless o
if Array === o
raise Patch::IndexError unless part =~ /\A(?:\d|[1-9]\d+)\Z/
part = part.to_i
end
o[part]
}
end
def self.parse path
return [''] if path == '/'
return [] if path == ''
unless path.start_with? '/'
raise FormatError, "JSON Pointer should start with a slash"
end
parts = path.sub(/^\//, '').split(/(?<!\^)\//).each { |part|
part.gsub!(/\^[\/^]|~[01]/) { |m| ESC[m] }
}
parts.push("") if path[-1] == '/'
parts
end
end
class Patch
class Exception < StandardError
end
class FailedTestException < Exception
attr_accessor :path, :value
def initialize path, value
super "expected #{value} at #{path}"
@path = path
@value = value
end
end
class OutOfBoundsException < Exception
end
class ObjectOperationOnArrayException < Exception
end
class InvalidObjectOperationException < Exception
end
class IndexError < Exception
end
class MissingTargetException < Exception
end
class InvalidPath < Exception
end
def initialize is
@is = is
end
def apply doc
really_apply doc
end
private
whens = %w{ add move test replace remove copy }.map { |x| "when #{x.dump} then #{x}(ins, d)"}.join("; ")
class_eval <<~eodispatch, __FILE__, __LINE__ + 1
def really_apply doc
@is.inject(doc) { |d, ins|
case ins['op'].strip
#{whens}
else
raise Exception, "bad method `\#{ins['op']}`"
end
}
end
eodispatch
FROM = 'from' # :nodoc:
VALUE = 'value' # :nodoc:
def add ins, doc
path = get_path ins
list = Pointer.parse path
key = list.pop
dest = Pointer.eval list, doc
obj = ins.fetch VALUE
raise(MissingTargetException, "target location '#{ins['path']}' does not exist") unless dest
if key
add_op dest, key, obj
else
if doc.equal? dest
doc = obj
else
dest.replace obj
end
end
doc
end
def move ins, doc
path = get_path ins
from = Pointer.parse ins.fetch FROM
to = Pointer.parse path
from_key = from.pop
key = to.pop
src = Pointer.eval from, doc
dest = Pointer.eval to, doc
raise(MissingTargetException, "target location '#{ins['path']}' does not exist") unless dest
obj = rm_op src, from_key
add_op dest, key, obj
doc
end
def copy ins, doc
path = get_path ins
from = Pointer.parse ins.fetch FROM
to = Pointer.parse path
from_key = from.pop
key = to.pop
src = Pointer.eval from, doc
dest = Pointer.eval to, doc
if Array === src
raise Patch::ObjectOperationOnArrayException, "cannot apply non-numeric key '#{key}' to array" unless from_key =~ /\A\d+\Z/
obj = src.fetch from_key.to_i
else
begin
obj = src.fetch from_key
rescue KeyError, NoMethodError
raise Hana::Patch::MissingTargetException, "'from' location '#{ins.fetch FROM}' does not exist"
end
end
raise(MissingTargetException, "target location '#{ins['path']}' does not exist") unless dest
add_op dest, key, obj
doc
end
def test ins, doc
path = get_path ins
expected = Pointer.new(path).eval doc
unless expected == ins.fetch(VALUE)
raise FailedTestException.new(ins['path'], ins[VALUE])
end
doc
end
def replace ins, doc
path = get_path ins
list = Pointer.parse path
key = list.pop
obj = Pointer.eval list, doc
return ins.fetch VALUE unless key
rm_op obj, key
add_op obj, key, ins.fetch(VALUE)
doc
end
def remove ins, doc
path = get_path ins
list = Pointer.parse path
key = list.pop
obj = Pointer.eval list, doc
rm_op obj, key
doc
end
def get_path ins
unless ins.key?('path')
raise Hana::Patch::InvalidPath, "missing 'path' parameter"
end
unless ins['path']
raise Hana::Patch::InvalidPath, "null is not valid value for 'path'"
end
ins['path']
end
def check_index obj, key
return -1 if key == '-'
raise ObjectOperationOnArrayException, "cannot apply non-numeric key '#{key}' to array" unless key =~ /\A-?\d+\Z/
idx = key.to_i
raise OutOfBoundsException, "key '#{key}' is out of bounds for array" if idx > obj.length || idx < 0
idx
end
def add_op dest, key, obj
if Array === dest
dest.insert check_index(dest, key), obj
else
raise Patch::InvalidObjectOperationException, "cannot add key '#{key}' to non-object" unless Hash === dest
dest[key] = obj
end
end
def rm_op obj, key
if Array === obj
raise Patch::ObjectOperationOnArrayException, "cannot apply non-numeric key '#{key}' to array" unless key =~ /\A\d+\Z/
key = key.to_i
raise Patch::OutOfBoundsException, "key '#{key}' is out of bounds for array" if key >= obj.length
obj.delete_at key
else
begin
raise Patch::MissingTargetException, "key '#{key}' not found" unless obj&.key? key
obj.delete key
rescue ::NoMethodError
raise Patch::InvalidObjectOperationException, "cannot remove key '#{key}' from non-object"
end
end
end
end
end