-
Notifications
You must be signed in to change notification settings - Fork 3
/
json_tools-1.3.0.tm
433 lines (405 loc) · 13.1 KB
/
json_tools-1.3.0.tm
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# Require the rl_json extension
package require rl_json
# Load yajltcl if it exists, yajl is still the best
# way to generate dynamic json
catch { package require yajltcl }
# require typeof
# https://github.com/Dash-OS/tcl-modules/blob/master/typeof-1.0.0.tm
package require typeof
# Taken from the json tcllib package for validation
namespace eval ::json {
# Regular expression for tokenizing a JSON text (cf. http://json.org/)
# tokens consisting of a single character
::variable singleCharTokens { "{" "}" ":" "\\[" "\\]" "," }
::variable singleCharTokenRE "\[[::join $singleCharTokens {}]\]"
# quoted string tokens
::variable escapableREs { "[\\\"\\\\/bfnrt]" "u[[:xdigit:]]{4}" "." }
::variable escapedCharRE "\\\\(?:[::join $escapableREs |])"
::variable unescapedCharRE {[^\\\"]}
::variable stringRE "\"(?:$escapedCharRE|$unescapedCharRE)*\""
# as above, for validation
::variable escapableREsv { "[\\\"\\\\/bfnrt]" "u[[:xdigit:]]{4}" }
::variable escapedCharREv "\\\\(?:[::join $escapableREsv |])"
::variable stringREv "\"(?:$escapedCharREv|$unescapedCharRE)*\""
# (unquoted) words
::variable wordTokens { "true" "false" "null" }
::variable wordTokenRE [::join $wordTokens "|"]
# number tokens
# negative lookahead (?!0)[[:digit:]]+ might be more elegant, but
# would slow down tokenizing by a factor of up to 3!
::variable positiveRE {[1-9][[:digit:]]*}
::variable cardinalRE "-?(?:$positiveRE|0)"
::variable fractionRE {[.][[:digit:]]+}
::variable exponentialRE {[eE][+-]?[[:digit:]]+}
::variable numberRE "${cardinalRE}(?:$fractionRE)?(?:$exponentialRE)?"
# JSON token, and validation
::variable tokenRE "$singleCharTokenRE|$stringRE|$wordTokenRE|$numberRE"
::variable tokenREv "$singleCharTokenRE|$stringREv|$wordTokenRE|$numberRE"
# 0..n white space characters
::variable whiteSpaceRE {[[:space:]]*}
# Regular expression for validating a JSON text
::variable validJsonRE "^(?:${whiteSpaceRE}(?:$tokenREv))*${whiteSpaceRE}$"
# parser will store a yajl object globally for
# parsing json values into yajl maps.
#
# Only created when first called [json parse]
# ::variable parser {}
::namespace ensemble create -unknown [::list ::json::unknown]
::namespace export {[a-z]*}
}
# In-case new commands are added to rl_json we pass them through to the
# rl_json procedure. When handled with tailcall we should see a speed
# improvement of the handling (have yet to benchmark it).
proc ::json::unknown { ns cmd args } {
::switch -- $cmd {
default {
::return [ ::list ::rl_json::json $cmd ]
}
}
}
# tailcall the native ::rl_json::json commands into the json namespace
# since we cant just import them since ::rl_json::json is a command rather
# than a namespace.
proc ::json::rl args { ::tailcall ::rl_json::json {*}$args }
proc ::json::get args { ::tailcall ::rl_json::json get {*}$args }
proc ::json::set args { ::tailcall ::rl_json::json set {*}$args }
proc ::json::new args { ::tailcall ::rl_json::json new {*}$args }
proc ::json::json2dict args { ::tailcall ::rl_json::json get {*}$args }
proc ::json::get_typed args { ::tailcall ::rl_json::json get_typed {*}$args }
proc ::json::type args { ::tailcall ::rl_json::json type {*}$args }
proc ::json::template args { ::tailcall ::rl_json::json template {*}$args }
proc ::json::normalize args { ::tailcall ::rl_json::json normalize {*}$args }
proc ::json::unset args { ::tailcall ::rl_json::json unset {*}$args }
proc ::json::extract args { ::tailcall ::rl_json::json extract {*}$args }
proc ::json::foreach args { ::tailcall ::rl_json::json foreach {*}$args }
proc ::json::lmap args { ::tailcall ::rl_json::json lmap {*}$args }
proc ::json::pretty args { ::tailcall ::rl_json::json pretty {*}$args }
# Extends the native rl_json exists to handle the quirk it has in handling
# of an empty string ({}). Since a JSON object is valid when it is an empty
# but properly formatted json object, exists will not throw an error with this
# workaround and will perform as expected (returning false since nothing exists)
proc ::json::exists {j args} {
::switch -- $j {
{} - {{}} {
::return 0
}
default {
::try {
::tailcall ::rl_json::json exists $j {*}$args
} on error {result} {
::return 0
}
}
}
}
# Attempt to get the json value (returned as a dict) of the path. If the
# path does not exist, returns {} rather than an error.
proc ::json::get? args {
::if {[::json exists {*}$args]} {
::tailcall ::rl_json::json get {*}$args
} else {
::return
}
}
# Attempt to validate that a given value is a json object, returns bool
proc ::json::isjson v {
::tailcall ::json validate $v
}
proc ::json::validate v {
::variable validJsonRE
::return [::regexp -- $validJsonRE $v]
}
# Push local variables into the json object while optionally transforming
# the keys and/or default value should the value of the variable be {}
proc ::json::push {vname args} {
::if { $vname ne "->" } {
::upvar 1 $vname rj
}
::if { ! [::info exists rj] || $rj eq {} } {
::set rj {{}}
}
::foreach arg $args {
::set default [::lassign $arg variable name]
::upvar 1 $variable value
::if {[::info exists value]} {
::if { $name eq {} } {
::set name $variable
}
::if { $value ne {} } {
::json set rj $name [::json typed $value]
} else {
::json set rj $name [::json typed $default]
}
} else {
::throw error "$variable doesn't exist when trying to push $name into dict $var"
}
}
::return $rj
}
# Pull keys from the json object and create them as local variables in the
# callers scope. Optionally provide the variables name, the default value
# if the key was not found, and a path to the key.
# - Each element is either the name of the key or a list of $key $newName $default ...$path
# where items in the list are optional.
proc ::json::pull {vname args} {
::upvar 1 $vname check
::if { [::info exists check] } {
::set j $check
} else {
::set j $vname
}
::set rj {{}}
::foreach v $args {
::set path [::lassign $v variable name default]
::if { $name eq {} } {
::set name $variable
}
::upvar 1 $name value
::if { [::json exists $j {*}$path $variable] } {
::lassign [::json get_typed $j {*}$path $variable] value type
::set ex [::json extract $j {*}$path $variable]
::json set rj {*}$path $name $ex
} else {
::set value $default
}
}
::return $rj
}
# Works identically to [dict merge] but also validates.
proc ::json::merge {json args} {
::if { $json eq {} } { ::set json {{}} }
::foreach arg $args {
::if { ! [::json validate $arg] } {
continue
}
::json foreach { k v } $arg {
::json set json $k $v
}
}
::return $json
}
# Similar to json pull, this allows you to provide a list as the first
# argument to define the path you wish to operate from as a root.
# - Each argument may still specify the same arguments as in json pull
# except that it will operate from the given main path.
proc ::json::pullFrom {vname args} {
::set mpath [::lassign $vname var]
::upvar 1 $var check
::if { [::info exists check] } {
::set j $check
} else {
::set j $var
}
::set rj {{}}
::foreach v $args {
::set path [::lassign $v variable name default]
::if { $name eq {} } {
::set name $variable
}
::upvar 1 $name value
::if { [::json exists $j {*}$mpath $variable {*}$path ] } {
::set value [::json get $j {*}$mpath $variable {*}$path ]
::json set rj $name [::json extract $j {*}$mpath $variable {*}$path]
} elseif { $default ne {} } {
::set value $default
::json set rj $name $default
} else {
::set value {}
}
}
::return $rj
}
proc ::json::destruct args {
}
# Returns a new json object comprised of the given keys (if they existed in the
# original json object).
proc ::json::pick {var args} {
::set rj {{}}
::foreach arg $args {
::set path [::lrange $arg 0 end-1]
::set as [::lindex $arg end]
::if { [::json exists $var {*}$path $as] } {
::json set rj $as [::json extract $var {*}$path $as]
}
}
::return $rj
}
# Iterates through a json object and attempts to retrieve one of its childs
# value ($key) and assigns that as the main keys value.
# { "foo": { "v" : 2 }, "bar": { "v": 3 } }
# withKey $j v == { "foo": 2, "bar": 3 }
proc ::json::withKey { var key } {
::set rj {{}}
rl foreach {k v} $var {
::if { [::json exists $v $key] } {
::json set rj $k [::json extract $var $k $key]
}
}
::return $rj
}
# Modifies a given json object in place. The value can be a dict or an even
# number of arguments.
proc ::json::modify { vname args } {
::upvar 1 $vname rj
::if { ! [::info exists rj] } {
::set rj {{}}
}
::if { [::llength $args] == 1 } {
::set args [::lindex $args 0]
}
::dict for { k v } $args {
::json set rj $k [::json typed $v]
}
::return $rj
}
proc ::json::file2dict { file } {
::if {[::file isfile $file]} {
::set data [::string trim [::fileutil::cat $file]]
::return [::json get $data]
} else {
::throw error "File $file does not exist - cant convert from json to dict!"
}
}
# Does a "best attempt" to discover and handle the value of an item and convert it
# to a json object or value. Primitive support for properly built nested data
# structures but should not be relied upon for that. This is generally used to
# convert to a json value (example: hi -> "hi") and will first confirm the value
# is not already a json value (example: "hi" -> "hi")
#
# This is a key ingredient to allowing many of the other functions to work.
proc ::json::typed {value args} {
::if { "-map" ni $args && ! [ ::catch {::json type $value} err ] } {
::return $value
}
::set type [::typeof $value -exact]
::switch -glob -- $type {
dict {
::set obj {}
::dict for { k v } $value {
::lappend obj $k [::json typed $v -map]
}
::if { "-map" in $args } {
::return "object $obj"
}
::return [::json new object {*}$obj]
}
*array - list {
::set arr {}
::set i 0
::foreach v $value {
::set v [::json typed $v -map]
::if { $i == 0 && [::lindex $v 0] eq "array" && [::llength [::lindex $v 1]] == 2 } {
::set v [::lindex $v 1]
}
::incr i
::lappend arr $v
}
::if { "-map" in $args } {
::return "array $arr"
}
::return [::json new array {*}$arr]
}
int - double {
::if { "-map" in $args } {
::return "number [::expr {$value}]"
}
::return [::expr {$value}]
}
boolean* {
::if { "-map" in $args } {
::return "boolean [::expr {bool($value)}]"
}
::return [::expr {bool($value)}]
}
*string - default {
::if {$value eq "null"} {
::return $value
} elseif {[::string is entier -strict $value]} {
::if { "-map" in $args } {
::return "number [::expr {$value}]"
}
::return [::expr {$value}]
} elseif {[::string is double -strict $value]} {
::if { "-map" in $args } {
::return "number [::expr {$value}]"
}
::return [::expr {$value}]
} elseif {[::string is boolean -strict $value]} {
::if { "-map" in $args } {
::return "boolean [::expr {bool($value)}]"
}
::return [::expr {bool($value)}]
}
}
}
::if { "-map" in $args } {
::return "string [::json new string $value]"
}
::return [::json new string $value]
}
# Modifies an object.
# set j {{
# "foo": "bar",
# "baz": [ "foo", "bar", "qux" ]
# }}
# json object lappend j baz one
# % {{
# % "foo": "bar",
# % "baz": [ "foo", "bar", "qux", "one" ]
# % }}
proc ::json::object { what args } {
::set r {{}}
::switch -- $what {
create {
::dict for {k v} $args {
::json set r $k [::json typed $v]
}
}
lappend {
::set args [::lassign $args v k]
::upvar 1 $v j
::if { [info exists j] && [::json exists $j $k] } {
::lassign [::json get_typed $j $k] val type
::if { $type ne "array" } {
::throw error "You must use json object lappend on an array value"
}
}
::json set j $k [::json typed [::lappend val {*}$args]]
::return $j
}
}
::return $r
}
proc ::json::start {} {
::set json [::yajl create #auto]
::return $json
}
if 0 {
@ json parse $jsonValue @
| This is used to globally parse yajltcl objects.
| As of 1.6.2 there has been a bug that does not
| allow parsing an object more than once without resetting
| so we instead use a global object here that we can reset
| without worry.
}
proc ::json::parse val {
if {![::info exists ::json::parser] || $::json::parser eq {}} {
# create our parser if it doesnt exist
::set ::json::parser [::yajl create #auto]
}
::set parsed [$::json::parser parse $val]
$::json::parser reset
::return $parsed
}
proc ::json::done { json } {
::try {
::set body [$json get]
$json delete
} on error {r} {
::catch {
$json delete
}
::throw $r
}
::return $body
}