fix indentation code to honour deep indents
[scpubgit/TenDotTcl.git] / tcvJSON.tcl
CommitLineData
867f23e8 1######################################################################
2### ###
3### Copyright (c) 2009 Taylor Christopher Venable ###
4### Made available under the Simplified BSD License. ###
5### ###
6######################################################################
7
8# CVS Path: /Programs/Libraries/Tcl/tcvJSON/tcvJSON.tcl
9# Last Change: $Date$
10# Revision: $Revision$
11
12## The parser part of this code is derived from that found in Tcllib, and which
13## bears this copyright information:
14##
15## Copyright 2006 ActiveState Software Inc.
16##
17## See also the accompanying license.terms file which describes the rules for
18## licensing and distributing Tcllib code.
19
20package provide tcvJSON 1.0
21
22namespace eval tcvJSON {}
23
24# This namespace is called tcvJSON because json is already in tcllib and other
25# programs which use this module may need to access that alternative json. The
26# problem with tcllib's json is that it doesn't offer a way to write JSON, only
27# to read it. The inherent difficulty here is that most Tcl values can be
28# simultaneously representable by more than one type: most values can be shown
29# as strings. Examples: "quick brown fox" is both a string and a list of three
30# elements. Similarly, "true" is both a boolean (as interpreted by [expr]) and
31# a string. So how do we encode such things? Well we punt on it and force
32# data to be added to JSON via a special interface. This produces an internal
33# representation of tuples {TYPE VALUE} where TYPE indicates how to show the
34# data. For example, a list could be
35#
36# {list {str "quick brown fox"} {list {num 1} {num 2}}}
37#
38# --> ["quick brown fox", [1, 2]]
39#
40# In this scheme, objects are represented as follows:
41#
42# {obj "foo" {str "bar"} "tcl" {num 8.6}}
43#
44# --> {"foo" : "bar",
45# "tcl" : 8.6}
46#
47# Because keys in objects can only be strings, there's no need to tag them as
48# such. Thus, an object is a list of key-value pairs.
49
50proc tcvJSON::create {type} {
51 switch -- $type {
52 {list} {
53 return {list}
54 }
55
56 {obj} -
57 {object} {
58 return {obj}
59 }
60 }
61}
62
63# Synopsis: add! ?-type TYPE? JSONNAME VALUE
64# Appends the VALUE (possibly explicitly qualified to be of the given TYPE) to
65# the end of a JSON list stored in the caller in the variable JSONNAME.
66# Signals an error if the value of JSONNAME is not a JSON list.
67
68proc tcvJSON::add! {args} {
69 set location ""
70 set value ""
71
72 set offset 0
73
74 if {[lindex $args 0] eq "-type"} {
75 set type [lindex $args 1]
76 set offset 2
77 }
78
79 set location [lindex $args [expr {$offset + 0}]]
80 set value [lindex $args [expr {$offset + 1}]]
81
82 if {![info exists type]} {
83 if {[string is double -strict $value]} {
84 set type "num"
85 } elseif {$value eq "true" || $value eq "false"} {
86 set type "bool"
87 } elseif {$value eq "null"} {
88 set type "null"
89 } else {
90 set type "str"
91 }
92 } elseif {$type eq "auto"} {
93 set type [lindex $value 0]
94 if {$type ne "list" && $type ne "obj"} {
95 set value [lindex $value 1]
96 } else {
97 set value [lrange $value 1 end]
98 }
99 }
100
101 upvar $location json
102
103 if {[lindex $json 0] ne "list"} {
104 error "can only \"add\" to lists: received $json"
105 }
106
107 if {$type eq "null"} {
108 lappend json "null"
109 } else {
110 if {$type eq "list" || $type eq "obj"} {
111 lappend json [list $type {*}$value]
112 } else {
113 lappend json [list $type $value]
114 }
115 }
116}
117
118# Synopsis: put! ?-type TYPE? JSONNAME KEY VALUE
119# Adds the relationship KEY → VALUE (possibly explicitly qualified to be of the
120# given TYPE) into the JSON object stored in the caller in the variable
121# JSONNAME. Signals an error if the value of JSONNAME is not a JSON object.
122# KEY is treated as a string, both internally and for encoding.
123
124proc tcvJSON::put! {args} {
125 set location ""
126
127 set key ""
128 set value ""
129
130 set offset 0
131
132 if {[lindex $args 0] eq "-type"} {
133 set type [lindex $args 1]
134 set offset 2
135 }
136
137 set location [lindex $args [expr {$offset + 0}]]
138 set key [lindex $args [expr {$offset + 1}]]
139 set value [lindex $args [expr {$offset + 2}]]
140
141 if {![info exists type]} {
142 if {[string is double -strict $value]} {
143 set type "num"
144 } elseif {$value eq "true" || $value eq "false"} {
145 set type "bool"
146 } elseif {$value eq "null"} {
147 set type "null"
148 } else {
149 set type "str"
150 }
151 } elseif {$type eq "auto"} {
152 set type [lindex $value 0]
153 if {$type ne "list" && $type ne "obj"} {
154 set value [lindex $value 1]
155 } else {
156 set value [lrange $value 1 end]
157 }
158 }
159
160 upvar $location json
161
162 if {[lindex $json 0] ne "obj"} {
163 error "can only \"put\" to objects: received $json"
164 }
165
166 for {set i 1} {$i < [llength $json]} {incr i 2} {
167 if {[lindex $json $i] eq $key} {
168 set json [lreplace $json $i $i+1]
169 }
170 }
171
172 lappend json $key
173 if {$type eq "null"} {
174 lappend json null
175 } else {
176 if {$type eq "list" || $type eq "obj"} {
177 lappend json [list $type {*}$value]
178 } else {
179 lappend json [list $type $value]
180 }
181 }
182}
183
184# Synopsis: unparse JSONTHING
185# Encodes / writes / prints / unparses some kind of JSON composite or scalar
186# value JSONTHING into a string representation which can be sent over the wire
187# or written to a file.
188
189proc tcvJSON::unparse {thing args} {
190 set output ""
191 set type [lindex $thing 0]
192 set indent 2
193 set indentIncr 2
194
195 if {[llength $args] > 0} {
196 set indent [lindex $args 0]
c2a818a9 197 if {[llength $args] > 1} {
198 set indentIncr [lindex $args 1]
199 } else {
200 set indentIncr $indent
201 }
867f23e8 202 }
203
204 switch -- $type {
205 {list} {
206 append output "\[\n[string repeat " " $indent]"
207 set tmp {}
208 foreach element [lrange $thing 1 end] {
c2a818a9 209 lappend tmp [unparse $element [expr {$indent + $indentIncr}] $indentIncr]
867f23e8 210 }
211 append output [join $tmp ",\n[string repeat " " $indent]"]
212 append output "\n[string repeat " " [expr {$indent - $indentIncr}]]\]"
213 }
214
215 {obj} {
216 append output "\{\n[string repeat " " $indent]"
217 set tmp {}
218 for {set i 1} {$i < [llength $thing]} {incr i} {
219 set key [lindex $thing $i]
220 set value [lindex $thing [incr i]]
c2a818a9 221 lappend tmp "\"$key\": [unparse $value [expr {$indent + $indentIncr}] $indentIncr]"
867f23e8 222 }
223 append output [join $tmp ",\n[string repeat " " $indent]"]
224 append output "\n[string repeat " " [expr {$indent - $indentIncr}]]\}"
225 }
226
227 {str} {
228 append output "\"[lindex $thing 1]\""
229 }
230
231 {bool} -
232 {num} {
233 append output "[lindex $thing 1]"
234 }
235
236 {null} {
237 append output "null"
238 }
239
240 {default} {
241 error "unknown type \"$type\""
242 }
243 }
244 return $output
245}
246
247proc tcvJSON::write {args} {
248 if {[llength $args] == 1} {
249 set channel stdout
250 set jsonName [lindex $args 0]
251 } elseif {[llength $args] == 2} {
252 set channel [lindex $args 0]
253 set jsonName [lindex $args 1]
254 } else {
255 error "wrong # args: expected \"write ?channel? jsonName\""
256 }
257
258 upvar $jsonName json
259 puts $channel [unparse $json]
260}
261
262# Shamelessly lifted from Tcllib's json::getc proc.
263
264proc tcvJSON::getc {{txtvar txt}} {
265 # pop single char off the front of the text
266 upvar 1 $txtvar txt
267 if {$txt eq ""} {
268 return -code error "unexpected end of text"
269 }
270
271 set c [string index $txt 0]
272 set txt [string range $txt 1 end]
273 return $c
274}
275
276proc tcvJSON::parse {txt} {
277 return [Parse]
278}
279
280# Modified from Tcllib's json::_json2dict proc.
281
282proc tcvJSON::Parse {{txtvar txt}} {
283 upvar 1 $txtvar txt
284
285 set state TOP
286 set current {}
287
288 set txt [string trimleft $txt]
289 while {$txt ne ""} {
290 set c [string index $txt 0]
291
292 # skip whitespace
293 while {[string is space $c]} {
294 getc
295 set c [string index $txt 0]
296 }
297
298 if {$c eq "\{"} {
299 # object
300 switch -- $state {
301 TOP {
302 # This is the toplevel object.
303 getc
304 set state OBJECT
305 set current [create obj]
306 }
307 VALUE {
308 # We are inside an object looking at the value, which is another object.
309 put! -type auto current $name [Parse]
310 set state COMMA
311 }
312 LIST {
313 # We are inside a list and the next element is an object.
314 add! -type auto current [Parse]
315 set state COMMA
316 }
317 default {
318 return -code error "unexpected open brace in $state mode"
319 }
320 }
321 } elseif {$c eq "\}"} {
322 getc
323 if {$state ne "OBJECT" && $state ne "COMMA"} {
324 return -code error "unexpected close brace in $state mode"
325 }
326 return $current
327 } elseif {$c eq ":"} {
328 # name separator
329 getc
330
331 if {$state eq "COLON"} {
332 set state VALUE
333 } else {
334 return -code error "unexpected colon in $state mode"
335 }
336 } elseif {$c eq ","} {
337 # element separator
338 if {$state eq "COMMA"} {
339 getc
340 if {[lindex $current 0] eq "list"} {
341 set state LIST
342 } elseif {[lindex $current 0] eq "obj"} {
343 set state OBJECT
344 }
345 } else {
346 return -code error "unexpected comma in $state mode"
347 }
348 } elseif {$c eq "\""} {
349 # string
350 # capture quoted string with backslash sequences
351 set reStr {(?:(?:\")(?:[^\\\"]*(?:\\.[^\\\"]*)*)(?:\"))}
352 set string ""
353 if {![regexp $reStr $txt string]} {
354 set txt [string replace $txt 32 end ...]
355 return -code error "invalid formatted string in $txt"
356 }
357 set txt [string range $txt [string length $string] end]
358 # chop off outer ""s and substitute backslashes
359 # This does more than the RFC-specified backslash sequences,
360 # but it does cover them all
361 set string [subst -nocommand -novariable \
362 [string range $string 1 end-1]]
363
364 switch -- $state {
365 TOP {
366 return $string
367 }
368 OBJECT {
369 set name $string
370 set state COLON
371 }
372 LIST {
373 add! -type str current $string
374 set state COMMA
375 }
376 VALUE {
377 put! -type str current $name $string
378 unset name
379 set state COMMA
380 }
381 }
382 } elseif {$c eq "\["} {
383 # JSON array == Tcl list
384 switch -- $state {
385 TOP {
386 getc
387 set current [create list]
388 set state LIST
389 }
390 LIST {
391 add! -type auto current [Parse]
392 set state COMMA
393 }
394 VALUE {
395 put! -type auto current $name [Parse]
396 set state COMMA
397 }
398 default {
399 return -code error "unexpected open bracket in $state mode"
400 }
401 }
402 } elseif {$c eq "\]"} {
403 # end of list
404 getc
405 return $current
406 } elseif {[string match {[-0-9]} $c]} {
407 # one last check for a number, no leading zeros allowed,
408 # but it may be 0.xxx
409 string is double -failindex last $txt
410 if {$last > 0} {
411 set num [string range $txt 0 [expr {$last - 1}]]
412 set txt [string range $txt $last end]
413
414 switch -- $state {
415 TOP {
416 return $num
417 }
418 LIST {
419 add! -type num current $num
420 set state COMMA
421 }
422 VALUE {
423 put! -type num current $name $num
424 set state COMMA
425 }
426 default {
427 getc
428 return -code error "unexpected number '$c' in $state mode"
429 }
430 }
431 } else {
432 getc
433 return -code error "unexpected '$c' in $state mode"
434 }
435 } elseif {[string match {[ftn]} $c]
436 && [regexp {^(true|false|null)} $txt val]} {
437 # bare word value: true | false | null
438 set txt [string range $txt [string length $val] end]
439
440 switch -- $state {
441 TOP {
442 return $val
443 }
444 LIST {
445 add! current $val
446 set state COMMA
447 }
448 VALUE {
449 put! current $name $val
450 set state COMMA
451 }
452 default {
453 getc
454 return -code error "unexpected '$c' in $state mode"
455 }
456 }
457 } else {
458 # error, incorrect format or unexpected end of text
459 return -code error "unexpected '$c' in $state mode"
460 }
461 }
462}
463
464# Synopsis: objForEach KEY VALUE OBJ SCRIPT
465# Iterates through the key/value pairs in the supplied JSON object OBJ and sets
466# KEY and VALUE in the environment of SCRIPT, then running SCRIPT at the
467# caller's level.
468
469proc tcvJSON::objForEach {k v obj script} {
470 if {[lindex $obj 0] ne "obj"} {
471 error "this is not an object"
472 }
473
474 for {set i 1} {$i < [llength $obj]} {incr i 2} {
475 uplevel [list set $k [lindex $obj $i]]
476 uplevel [list set $v [lindex $obj $i+1]]
477 uplevel $script
478 }
479
480 if {[llength $obj] > 1} {
481 # Clean up after ourselves.
482 uplevel [list unset $k]
483 uplevel [list unset $v]
484 }
485}
486
487# Synopsis: exists? JSON THING
488# Indicates whether THING exists within JSON. If JSON is an object, then we
489# treat THING like a key. If JSON is a list, we treat THING like an element.
490# It is an error for the value of JSON to be a non-composite type.
491
492proc tcvJSON::exists? {json thing} {
493 if {[lindex $json 0] eq "obj"} {
494 set increment 2
495 } elseif {[lindex $json 0] eq "list"} {
496 set increment 1
497 } else {
498 error "not a composite type"
499 }
500 for {set i 1} {$i < [llength $json]} {incr i $increment} {
501 if {[lindex $json $i] == $thing} {
502 return 1
503 }
504 }
505 return 0
506}
507
508# Synopsis: listForEach ELT LIST SCRIPT
509# Iterates through all the elements in the supplied JSON list LIST and sets ELT
510# appropriately in the environment of SCRIPT, then running SCRIPT at the
511# caller's level.
512
513proc tcvJSON::listForEach {e lst script} {
514 if {[lindex $lst 0] ne "list"} {
515 error "this is not a list"
516 }
517
518 for {set i 1} {$i < [llength $lst]} {incr i} {
519 uplevel [list set $e [lindex $obj $i]]
520 uplevel $script
521 }
522
523 # Don't leave the variables set.
524 uplevel [list unset $e]
525}