&|What|build JSON with tdom using '''''createNodeCmd'''''|&
&|Requires|tdom 0.9.2|&
&|tags|tdom|&
&|reference|http://www.tdom.org/index.html/doc/trunk/doc/index.html|&
&|Updated|03.07.2022|&
----
** Index **
1. Challenge
2. insufficient Approach
3. Issues and Improvements
4. improved Approvements
5. Summary
6. Discussion
**Challenge**
** Challenge **
how to build a JSON like this:
======
{
"aNumber": 0.123,
"aNumberAsString": "0.123",
"aString": "this is a String",
"a key with spaces": "please take care on setting: dom setNameCheck 0",
"aArray": ["a","b","c"],
"aObject": {
"foo":"that",
"bar":"grill"
},
"aObjectArray": [
{
"foo": "objArray_01_foo",
"bar": "objArray_01_bar",
"array": ["five_01_a", "five_01_b", "five_01_c"]
},
{
"foo": "objArray_02_foo",
"bar": "objArray_02_bar",
"array": ["five_02_a", "five_02_b", "five_02_c"]
}
]
}
======
**insufficient Approach**
======
#
# --- load tdom ---
#
package require tdom 0.9 ;# current version: 0.9.2 (May, 2022)
puts "tdom: [package versions tdom]"
#
# http://www.tdom.org/index.html/file?name=tests/domjson.test&ci=release
#
#
set doc [dom createDocumentNode root]
#
puts "asXML: [$root asXML -indent 4]"
puts "asJSON: [$root asJSON -indent 4]"
#
# -- aNumber --
#
set node_Number [$doc createElement "aNumber"]
$node_Number appendChild [$doc createTextNode 0.123]
$root appendChild $node_Number
#
# -- aNumberAsString --
#
set node_StringNum [$doc createElement "aNumberAsString"]
$node_StringNum appendChild [$doc createTextNode 0.123]
$root appendChild $node_StringNum
#
# -- aString --
#
set node_String [$doc createElement "aNumberString"]
$node_String appendChild [$doc createTextNode "0.123"]
$root appendChild $node_String
#
set node_String [$doc createElement "aString"]
$node_String appendChild [$doc createTextNode "this is a String"]
$root appendChild $node_String
#
# -- a key with spaces --
#
# ... you have to disable namecheck first
dom setNameCheck 0
set node_String [$doc createElement "a key with spaces"]
# [dom setNameCheck] == 1 ... brings this error
# ERROR: Invalid tag name 'a key with spaces'
# while executing
# "$doc createElement "a key with spaces""
# invoked from within
# "set node_String [$doc createElement "a key with spaces"]"
#
$node_String appendChild [$doc createTextNode "please take care on setting: dom setNameCheck 0"]
$root appendChild $node_String
#
# -- aArray --
#
set node_Array [$doc createElement "aArray"]
$node_Array appendChild [$doc createTextNode "a"]
$node_Array appendChild [$doc createTextNode "b"]
$node_Array appendChild [$doc createTextNode "c"]
$root appendChild $node_Array
#
#
# -- aObject --
#
set node_Object [$doc createElement "aObject"]
#
set node__Label [$doc createElement "foo"]
set node__Value [$doc createTextNode "that"]
$node_Object appendChild $node__Label
$node__Label appendChild $node__Value
#
set node__Label [$doc createElement "bar"]
set node__Value [$doc createTextNode "grill"]
$node_Object appendChild $node__Label
$node__Label appendChild $node__Value
#
$root appendChild $node_Object
#
#
# -- aObjectArray --
#
set node_ObjArray [$doc createElement "aObjectArray"]
#
set node__Obj_01 [$doc createElement "obj0001"]
#
set node__Label [$doc createElement "foo"]
set node__Value [$doc createTextNode "objArray_01_foo"]
$node__Label appendChild $node__Value
$node__Obj_01 appendChild $node__Label
set node__Label [$doc createElement "bar"]
set node__Value [$doc createTextNode "objArray_01_bar"]
$node__Label appendChild $node__Value
$node__Obj_01 appendChild $node__Label
#
set node__Array [$doc createElement "array"]
$node__Array appendChild [$doc createTextNode "five_01_a"]
$node__Array appendChild [$doc createTextNode "five_01_b"]
$node__Array appendChild [$doc createTextNode "five_01_c"]
#
$node__Obj_01 appendChild $node__Array
#
#
set node__Obj_02 [$doc createElement "obj0002"]
#
set node__Label [$doc createElement "foo"]
set node__Value [$doc createTextNode "objArray_02_foo"]
$node__Label appendChild $node__Value
$node__Obj_02 appendChild $node__Label
set node__Label [$doc createElement "bar"]
set node__Value [$doc createTextNode "objArray_02_bar"]
$node__Label appendChild $node__Value
$node__Obj_02 appendChild $node__Label
#
set node__Array [$doc createElement "array" ARRAY]
$node__Array appendChild [$doc createTextNode "five_02_a"]
$node__Array appendChild [$doc createTextNode "five_02_b"]
$node__Array appendChild [$doc createTextNode "five_02_c"]
#
$node__Obj_02 appendChild $node__Array
#
$node_ObjArray appendChild $node__Obj_01
$node_ObjArray appendChild $node__Obj_02
#
#
$root appendChild $node_ObjArray
#
# in the case of mixed OBJECT and ARRAYs you have to update the specific rendering
$node_ObjArray jsonType ARRAY
$node__Obj_01 jsonType OBJECT
$node__Obj_02 jsonType OBJECT
#
puts "== \$resultJSON ===="
puts [$doc asXML -indent 4]
puts "== \$resultJSON ===="
puts [$doc asJSON -indent 4]
#
======
... output of this script:
======
tdom: 0.9.2
asXML:
asJSON: {}
== $resultJSON ====
<aNumber>0.123</aNumber>
<aNumberAsString>0.123</aNumberAsString>
<aNumberString>0.123</aNumberString>
<aString>this is a String</aString>
<a key with spaces>please take care on setting: dom setNameCheck 0</a key with spaces>
<aArray>abc</aArray>
<aObject>
<foo>that</foo>
<bar>grill</bar>
</aObject>
<aObjectArray>
<obj0001>
<foo>objArray_01_foo</foo>
<bar>objArray_01_bar</bar>
<array>five_01_afive_01_bfive_01_c</array>
</obj0001>
<obj0002>
<foo>objArray_02_foo</foo>
<bar>objArray_02_bar</bar>
<array>five_02_afive_02_bfive_02_c</array>
</obj0002>
</aObjectArray>
== $resultJSON ====
{
"aNumber": "0.123",
"aNumberAsString": "0.123",
"aNumberString": "0.123",
"aString": "this is a String",
"a key with spaces": "please take care on setting: dom setNameCheck 0",
"aArray": [
"a",
"b",
"c"
],
"aObject": {
"foo": "that",
"bar": "grill"
},
"aObjectArray": [
{
"foo": "objArray_01_foo",
"bar": "objArray_01_bar",
"array": [
"five_01_a",
"five_01_b",
"five_01_c"
]
},
{
"foo": "objArray_02_foo",
"bar": "objArray_02_bar",
"array": [
"five_02_a",
"five_02_b",
"five_02_c"
]
}
]
}
======
** Issues and Imoprovements **
*** wrong Value ***
======
this Result: "aNumber": "0.123"
expected Result: "aNumber": 0.123
======
*** complex code ***
... the given approach takes a lot of lines to create this result
** improved Approach **
use
======
dom createNodeCmd
======
to improve our code
======
#
# --- load tdom ---
#
package require tdom 0.9 ;# current version: 0.9.2 (May, 2022)
puts "tdom: [package versions tdom]"
#
# http://www.tdom.org/index.html/file?name=tests/domjson.test&ci=release
#
#
#
puts "== 01 == \$resultJSON ===="
set doc [dom createDocumentNode root]
#
puts "asXML: [$root asXML -indent 4]"
puts "asJSON: [$root asJSON -indent 4]"
#
#
# our targetJSON includes nodes with predefined names:
# "aNumber" "aString" "a key with spaces" "foo" "bar"
#
# ... lets predefine dom-commands to create these nodes
#
dom createNodeCmd -jsonType NONE elementNode aNumber
dom createNodeCmd -jsonType NONE elementNode aNumberAsString
dom createNodeCmd -jsonType NONE elementNode aString
dom createNodeCmd -jsonType NONE elementNode "a key with spaces"
dom createNodeCmd -jsonType NONE elementNode foo
dom createNodeCmd -jsonType NONE elementNode bar
dom createNodeCmd -jsonType NONE elementNode array
dom createNodeCmd -jsonType ARRAY elementNode "aArray"
#
# ... json distinguishs between
# ... Arrays ["A","B","C"] and
# ... Objects {"a": "A", "b": "B", "c":"C"}
#
dom createNodeCmd -jsonType OBJECT elementNode "aObject"
dom createNodeCmd -jsonType ARRAY elementNode "aObjectArray"
#
# ... json distinguishs between
# ... Strings "0.123"
# ... Numbers 0.123
#
dom createNodeCmd -jsonType NUMBER textNode jsonNumber
dom createNodeCmd -jsonType STRING textNode jsonString
#
#
set resultJSON [dom createDocumentNode]
#
puts "== 02 == \$resultJSON ===="
puts [$resultJSON asJSON -indent 4]
#
$resultJSON appendFromScript {
aNumber {jsonNumber 0.123}
aNumberAsString {jsonString 0.123}
aString {jsonString "this is a string"}
"a key with spaces" {jsonString "It's possible."}
aArray {
jsonString a
jsonString b
jsonString c
}
aObject {
foo {jsonString "that"}
bar {jsonString "grill"}
}
aObjectArray {
aObject {
foo {jsonString "objArray_01_foo"}
bar {jsonString "objArray_01_bar"}
aArray {
jsonString "five_01_a"
jsonString "five_01_b"
jsonString "five_01_c"
}
}
aObject {
foo {jsonString "objArray_02_foo"}
bar {jsonString "objArray_02_bar"}
aArray {
jsonString "five_02_a"
jsonString "five_02_b"
jsonString "five_02_c"
}
}
}
}
#
#
puts "== 03 == \$resultJSON ===="
puts [$resultJSON asXML -indent 4]
puts "== 03 == \$resultJSON == final =="
puts [$resultJSON asJSON -indent 4]
#
======
... output of this script:
======
tdom: 0.9.2
== 01 == $resultJSON ====
asXML:
asJSON: {}
== 02 == $resultJSON ====
{}
== 03 == $resultJSON ====
<aNumber>0.123</aNumber>
<aNumberAsString>0.123</aNumberAsString>
<aString>this is a string</aString>
<a key with spaces>It's possible.</a key with spaces>
<aArray>abc</aArray>
<aObject>
<foo>that</foo>
<bar>grill</bar>
</aObject>
<aObjectArray>
<aObject>
<foo>objArray_01_foo</foo>
<bar>objArray_01_bar</bar>
<aArray>five_01_afive_01_bfive_01_c</aArray>
</aObject>
<aObject>
<foo>objArray_02_foo</foo>
<bar>objArray_02_bar</bar>
<aArray>five_02_afive_02_bfive_02_c</aArray>
</aObject>
</aObjectArray>
== 03 == $resultJSON == final ==
{
"aNumber": 0.123,
"aNumberAsString": "0.123",
"aString": "this is a string",
"a key with spaces": "It's possible.",
"aArray": [
"a",
"b",
"c"
],
"aObject": {
"foo": "that",
"bar": "grill"
},
"aObjectArray": [
{
"foo": "objArray_01_foo",
"bar": "objArray_01_bar",
"aArray": [
"five_01_a",
"five_01_b",
"five_01_c"
]
},
{
"foo": "objArray_02_foo",
"bar": "objArray_02_bar",
"aArray": [
"five_02_a",
"five_02_b",
"five_02_c"
]
}
]
}
======
** Summary **
* enable creating a node as a number
* createNodeCmd:
** define nodes with name and type
** create your JSON like it should look like at the end
** Discussion **
... your comments
[rattleCAD] 2022-07-03 ... you may improve this by replace:
======
dom createNodeCmd -jsonType NONE elementNode aNumber
dom createNodeCmd -jsonType NONE elementNode aNumberAsString
dom createNodeCmd -jsonType NONE elementNode aString
dom createNodeCmd -jsonType NONE elementNode "a key with spaces"
dom createNodeCmd -jsonType NONE elementNode foo
dom createNodeCmd -jsonType NONE elementNode bar
dom createNodeCmd -jsonType NONE elementNode array
dom createNodeCmd -jsonType ARRAY elementNode "aArray"
dom createNodeCmd -jsonType OBJECT elementNode "aObject"
dom createNodeCmd -jsonType ARRAY elementNode "aObjectArray"
dom createNodeCmd -jsonType NUMBER textNode jsonNumber
dom createNodeCmd -jsonType STRING textNode jsonString
======
by
======
foreach {nodeName nodeType jsonType} {
aNumber elementNode NONE
aNumberAsString elementNode NONE
aString elementNode NONE
"a key with spaces" elementNode NONE
foo elementNode NONE
bar elementNode NONE
array elementNode NONE
"aArray" elementNode ARRAY
"aObject" elementNode OBJECT
"aObjectArray" elementNode ARRAY
jsonNumber textNode NUMBER
jsonString textNode STRING
} {
# dom createNodeCmd -jsonType STRING textNode jsonString
# puts " -> dom createNodeCmd -jsonType [format {%-8s %-12s %s} $jsonType $nodeType $nodeName]"
dom createNodeCmd -jsonType $jsonType $nodeType $nodeName
}
======
----
[sbron] 2022-08-18: I wasn't very satisfied with the need to hardcode the createNodeCmd commands. So I came up with a more general approach, using the [namespace unknown] functionality. While making that, it occurred to me that this also allowed for a nice way to pass in variables to be substituted:
======package require tdom 0.9.3-
namespace eval json {
namespace ensemble create -subcommands build
namespace eval eval {
namespace unknown [namespace parent]::unknown
dom createNodeCmd -jsonType OBJECT elementNode object
dom createNodeCmd -jsonType ARRAY elementNode array
dom createNodeCmd -jsonType NUMBER textNode number
dom createNodeCmd -jsonType STRING textNode string dom createNodeCmd -jsonType STRING textNode text
dom createNodeCmd -jsonType TRUE textNode true
dom createNodeCmd -jsonType FALSE textNode false
dom createNodeCmd -jsonType NULL textNode null
proc boolean {arg} {
if {$arg} {
tailcall true true
} else {
tailcall false false
}
}
}
proc unknown {cmd args} {
if {[regexp {^([a-z]+)::(.*)} $cmd -> type name]} {
if {$type in {object array}} { # GeneCreate an appropriate elementNode command
set cmd [namespace eval eval [list tdom cr::fsneatewNodeCmd \
-jsonType [string toupper $type] -tagName $name \
elementNode [{*}$argencmds]]]
} else { if {$type eq "boolean"} {
set bool [lindex $args 0]
# Booleans need to be split into tCrue and false
if {$bool} {
set type true
} else {
set type false
}
}
# Generate an appropriate elementNode command
set cmd [namespace eval eval [list tdom cr::fsneatewNodeCmd \
-jsonType NONE -tagName $name elementNode [gencmd]]]
# Adjust the args
set args [list [linsert $args 0 $type]]
} # Execute the command, then clean it up
try {
# Strangely this needs to explicitly be evaluated in eval,
# even though $cmd is in the eval namespace
return [namespace eval eval [list $cmd {*}$args]]
} finally {
rename $cmd ""
}
}
}
proc gencmd {} {
variable gencmd
return [string cat gencmd [incr gencmd]]
}
proc build {type script {data {}}} {
dom createDocumentNode json
# Make the data dict available as variables in the eval namespace
namespace eval eval [list variable {*}$data]
try {
namespace eval eval \
[list [namespace which $json] appendFromScript $script]
} finally {
# Clean up the variables from the data dict
namespace eval eval [list unset {*}[dict keys $data]]
}
$json jsonType [string toupper $type]
$json asJSON -indent 2
}
}
# Demo
set script {
number::aNumber $number
string::aNumberAsString $number
string::aString $str
boolean::aBool $bool
"string::a key with spaces" "It's possible."
array::aArray {
string $chra
string $chrb
string $chrc
}
object::aObject {
string::foo $foo
string::bar $bar
}
array::aObjectArray {
object {
string::foo objArray_01_foo
string::bar objArray_01_bar
array::aArray {
string five_01_a
string five_01_b
string five_01_c
}
}
object {
string::foo objArray_02_foo
string::bar objArray_02_bar
array::aArray {
string five_02_a
string five_02_b
string five_02_c
}
}
}
}
set data {
number 0.123
str "this is a string"
bool yes
chra a
chrb b
chrc c
foo that
bar grill
}
puts [json build object $script $data]
======