Tcl library designed to declaratively parse, reshape, and transform complex dictionary structures. Source at https://github.com/ch32n/DictTransform
DictTransform lets you define a set of rules in a Tcl array. These rules specify how to parse, filter, reshape, and transform an input dictionary into a desired output structure. It's particularly useful for processing structured data from APIs.
The library works by dynamically generating parsing procedures based on definitions.
::dicttr::create_interface array_name ?-staticvars {Var1 Val1 Var2 Val2 ...}?
This command creates procedures for all the definitions specified in the array variable name. The name of that array becomes a namespace containing the defined commands.
-staticvars creates mapping for variables, which can be referenced in 'command' definition.
get Extracts specified keys and their values from the source dictionary. This is the primary command for pulling data into your result and supports set of transformations on the values.
get {
{<key-path-list> ?->
alias <new-name>
include <1|0>
default <default-value>
required <1|0>
map {<from1 to1 from2 to2 ...>}
regexp {<pattern> <index>}
script {<script-body>}?
}
{...}
}from Descends into a nested dictionary at the specified <key-path-list> and applies a new set of rules to it.
from {<key-path-list> -> include <1|0> alias <new-name>} {
<definition>
}key Reshapes a list of dictionaries into a key-value pair, using a specified value from within each item as its new key.
key ({<key-path-list> -> index <num>} | @arrayindex@)foreachkey Iterates over every key-value pair in the current dictionary context and applies a set of rules (<definition>) to each one.
foreachkey {
<definition>
}flatten Takes the dictionary value at <key-path-list> and merges its contents into the current dictionary level.
flatten <list of key-path-list>
transform Executes a pre-defined transformation procedure on the current dictionary before any other rules in the current block are applied. This allows for multi-pass transformations.
transform <transform-proc-list>
variable Extracts a value from the source dictionary at <key-path-list> and stores it in a temporary variable named <variable-name>. This variable can then be referenced in subsequent command blocks as @<variable-name>@.
variable <variable-name> from <key-path-list>
command Executes an arbitrary Tcl command that is expected to return a dictionary. The result is placed under a new key in the output, and a new set of rules is applied to it. Any variables defined with the variable command can be used within the Tcl command string.
command <new-key-name> <tcl-command> {
<definition>
}filter Applies a set of conditions to the current dictionary. If the conditions are not met, processing of the current item is halted. It's possible to have multiple filters.
filter {
{
<key-path-list> ->
regexp <pattern>
include <1|0>
script {<script-body>}
}
{...}
}return Immediately halts processing for the current definition block and returns the dictionary in its current state. This is useful for fetching data with command without applying further parsing.
transform -> flatten -> filter -> return -> get -> from -> foreachkey -> command
DictTransform supports templating to help you reuse common blocks of configuration. The library pre-processes your entire configuration to find and replace all template placeholders before building the final transformation procedures.
A template can include other templates, and they will all be resolved correctly before the final transformation procedures are created. A template is defined like any other rule, but its name must be enclosed in percent signs (%) — this signals to the library that it is a reusable block and not a top-level transformation command.
Template definition
set array_name(%template_name%) {
<tranform definition>
}Template usage
set array_name(tranfrom) {
%template_name%
}Get examples
set getExample(ParseEx1) {
get {x z}
}
set getExample(ParseEx2) {
get {
{x -> alias xnew}
{a -> default 3}
{b -> include 0}
}
}
set getExample(ParseEx3) {
get {
{b -> required 1}
}
}
set getExample(ParseEx4) {
get {
{x -> map {val1 newVal1 val2 newVal2} default newVal3}
{y -> regexp { {val(\d+)} 1}}
{z -> script {
string map {val3 newVal3} %v
}}
}
}
set getExample(ParseEx5) {
get {
{x y}
{a b -> alias newb}
}
}
::dicttr::create_interface getExample
set dict1 {x 1 y 1 z 2}
set dict2 {x {y 1 z 2} a {b 3 c 4}}
set dict3 {x val1 y val2 z val3}
# returns {x 1 y 1 z 2}
getExample::ParseEx1 $dict1
# returns {xnew 1 a 1}
getExample::ParseEx2 $dict1
# raises error 'Error: Field 'b' is required but not found'
getExample::ParseEx3 $dict3
# returns {x newVal1 y 2 z newVal3}
getExample::ParseEx4 $dict3
# returns {x {y 1} newb 3}
getExample::ParseEx5 $dict2From examples
set fromExample(ParseEx1) {
from x {
get y
}
from {a -> alias aNew} {
get c
}
from {k i -> alias t} {
get j
}
}
set fromExample(ParseEx2) {
from objects {
key name
get {value option}
}
}
set fromExample(ParseEx3) {
from {objects -> alias newObjectsKey} {
key name
get {value option}
}
from otherObjects {
key name
get {value option}
}
from {noSuchObjects -> include 0} {
key name
get {value option}
}
}
::dicttr::create_interface fromExample
set dict1 {
x {y 1 z 2}
a {b 3 c 4}
k {
i {j 6}
g {j 8}
}
}
set dict2 {
objects {
{name name1 value value1 option option1}
{name name2 value value2 option option2}
}
}
# returns {x {y 1} aNew {c 4} t {j 6}}
fromExample::ParseEx1 $dict1
# returns {objects {name1 {value value1 option option1} name2 {value value2 option option2}}}
fromExample::ParseEx2 $dict2
# returns {newObjectsKey {name1 {value value1 option option1} name2 {value value2 option option2}} otherObjects {}}
# otherObjects is included as empty key and noSuchObjects is omitted
fromExample::ParseEx3 $dict2Key examples
set keyExample(ParseEx1) {
from objects {
key {name -> index 1}
get {value}
}
}
set keyExample(ParseEx2) {
from objects {
key @arrayindex@
get {name value}
}
}
set keyExample(ParseEx3) {
from objects {
key {name namekey}
get {value}
}
}
::dicttr::create_interface keyExample
set dict1 {
objects {
{name {name1 name2} value value1 option option1}
{name {name3 name4} value value2 option option2}
}
}
set dict2 {
objects {
{name {namekey nameval1} value value1 option option1}
{name {namekey nameval2} value value2 option option2}
}
}
# returns {objects {name2 {value value1} name4 {value value2}}}
keyExample::ParseEx1 $dict1
# returns {objects {0 {name {name1 name2} value value1} 1 {name {name3 name4} value value2}}}
keyExample::ParseEx2 $dict1
# returns {objects {nameval1 {value value1} nameval2 {value value2}}}
keyExample::ParseEx3 $dict2Foreachkey examples
set forEachKeyExample(ParseEx1) {
from objects {
foreachkey {
get {value}
}
}
}
::dicttr::create_interface forEachKeyExample
set dict1 {
objects {
obj1 {value val1 name name1}
obj2 {value val2 name name1}
obj3 {value val3 name name1}
}
}
# returns {objects {obj1 {value val1} obj2 {value val2} obj3 {value val3}}}
forEachKeyExample::ParseEx1 $dict1Flatten examples
set flattenExample(ParseEx1) {
from data {
flatten objcets
return
}
}
set flattenExample(ParseEx2) {
from data {
flatten {objcets values}
get {time moreObjectsKey moreValuesKey}
}
}
::dicttr::create_interface flattenExample
set dict1 {
data {
time timeVal
date dateVal
objcets {
moreObjectsKey {
{id 1 name nameVal1}
{id 2 name nameVal2}
}
}
values {
moreValuesKey {
{id 1 value Val1}
{id 2 value Val2}
}
}
}
}
# returns
set returnData {
data {
time timeVal
date dateVal
moreObjectsKey {
{id 1 name nameVal1}
{id 2 name nameVal2}}
values {
moreValuesKey {
{id 1 value Val1}
{id 2 value Val2}
}
}
}
}
flattenExample::ParseEx1 $dict1
# returns
set returnData {
data {
time timeVal
moreObjectsKey {
{id 1 name nameVal1}
{id 2 name nameVal2}
}
moreValuesKey {
{id 1 value Val1}
{id 2 value Val2}
}
}
}
flattenExample::ParseEx2 $dict1Filter examples
set filterExample(ParseEx1) {
from data {
from objcets {
filter {
{type -> regexp type[12]}
}
key id
get {id type}
}
}
}
set filterExample(ParseEx2) {
from data {
from objcets {
filter {
{type -> regexp type[12]}
{name -> regexp name2 include 0}
}
key id
get {name type}
}
}
}
set filterExample(ParseEx3) {
from data {
from objcets {
filter {
{id -> script {
if {%v < 3} {
return 1
}
return 0
}}
}
key id
get {name type}
}
}
}
::dicttr::create_interface filterExample
set dict1 {
data {
date dateVal
objcets {
{id 1 type type1 name name1}
{id 2 type type2 name name2}
{id 3 type type3 name name3}
}
}
}
# returns {data {objcets {1 {id 1 type type1} 2 {id 2 type type2}}}}
filterExample::ParseEx1 $dict1
# returns {data {objcets {1 {name name1 type type1}}}}
filterExample::ParseEx2 $dict1
# returns {data {objcets {1 {name name1 type type1} 2 {name name2 type type2}}}}
filterExample::ParseEx3 $dict1Command examples
proc device_rest_api_stub {} {
set Result [dict create devices [dict create]]
for {set DevId 1} {$DevId < 4} {incr DevId} {
set TmpDict [dict create]
dict set TmpDict device_id $DevId
dict set TmpDict device_name name${DevId}
dict lappend Result devices $TmpDict
}
return $Result
}
# returns {devices {{device_id 1 name name1} {device_id 2 name name2} {device_id 3 name name3}}}
proc device_interface_rest_api_stub {DevId} {
set Result [dict create interface [dict create]]
for {set IntId 1} {$IntId < 4} {incr IntId} {
set TmpDict [dict create]
dict set TmpDict device_id $DevId
dict set TmpDict int_id $IntId
dict set TmpDict int_name name${IntId}
dict lappend Result interface $TmpDict
}
return $Result
}
# returns {interface {{device_id 1 int_id 1 int_name name1} {device_id 1 int_id 2 int_name name2} {device_id 1 int_id 3 int_name name3}}}
set commandExample(ParseEx1) {
command my_devices_key device_rest_api_stub {
from devices {
key device_id
get device_name
}
}
}
set commandExample(ParseEx2) {
command my_devices_key device_rest_api_stub {
from devices {
key device_id
get device_name
variable DevId from device_id
command my_interface_key {device_interface_rest_api_stub @DevId@} {
from interface {
key int_id
get {device_id int_name}
}
}
}
}
}
::dicttr::create_interface commandExample
#returns {my_devices_key {devices {1 {device_name name1} 2 {device_name name2} 3 {device_name name3}}}}
commandExample::ParseEx1
commandExample::ParseEx2
#result
set result {
my_devices_key {
devices {
1 {
device_name name1
my_interface_key {
interface {
1 {
device_id 1
int_name name1
}
2 {
device_id 1
int_name name2
}
3 {
device_id 1
int_name name3
}
}
}
}
2 {
device_name name2
my_interface_key {
interface {
1 {
device_id 2
int_name name1
}
2 {
device_id 2
int_name name2
}
3 {
device_id 2
int_name name3
}
}
}
}
3 {
device_name name3
my_interface_key {
interface {
1 {
device_id 3
int_name name1
}
2 {
device_id 3
int_name name2
}
3 {
device_id 3
int_name name3
}
}
}
}
}
}Transform examples
set tranfromExample(transformEx1) {
from data {
get {x a}
}
}
set tranfromExample(ParseEx1) {
transform transformEx1
get {
{data a -> alias new_key}
}
}
::dicttr::create_interface tranfromExample
set dict1 {
data {
x {
y 1
z 2
}
a {
b 3
c 4
}
}
otherdata {
q 1
}
}
# result {new_key {b 3 c 4}}
tranfromExample::ParseEx1 $dict1
Template examples
set templateExample(%templateEx1%) {
get {
{x y}
{a b}
}
}
set templateExample(ParseEx1) {
from data {
%templateEx1%
}
}
::dicttr::create_interface templateExample
set dict1 {
data {
x {
y 1
z 2
}
a {
b 3
c 4
}
}
otherdata {
q 1
}
}
#returns {data {x {y 1} a {b 3}}}
templateExample::ParseEx1 $dict1Complex example
package require dicttransform
set junosParse(%tmplt_bgp-rib_pre%) {
from bgp-rib {
key {name -> index {0 1}}
from name {get {data}}
from total-prefix-count {get {data}}
from active-prefix-count {get {data}}
from received-prefix-count {get {data}}
from accepted-prefix-count {get {data}}
from suppressed-prefix-count {get {data}}
}
}
set junosParse(%tmplt_bgp-rib%) {
from bgp-rib {
foreachkey {
get {
{name data -> alias name}
{total-prefix-count data -> alias total-prefix-count}
{active-prefix-count data -> alias active-prefix-count}
{received-prefix-count data -> alias received-prefix-count}
{accepted-prefix-count data -> alias accepted-prefix-count}
{suppressed-prefix-count data -> alias suppressed-prefix-count}
}
}
}
}
set junosParse(bgp_peer_pre) {
from bgp-information {
from bgp-peer {
key {peer-id -> index {0 1}}
from peer-address {get {data}}
from peer-as {get {data}}
from local-address {get {data}}
from local-as {get {data}}
from description {get {data}}
from peer-group {get {data}}
from peer-cfg-rti {get {data}}
from peer-fwd-rti {get {data}}
from peer-type {get {data}}
from peer-state {get {data}}
from peer-id {get {data}}
from local-id {get {data}}
from nlri-type-peer {get {data}}
%tmplt_bgp-rib_pre%
}
from attributes {
return
}
}
}
set junosParse(bgp_peer) {
transform bgp_peer_pre
from {bgp-information bgp-peer -> alias bgp-peer} {
foreachkey {
get {
{peer-address data -> alias peer-address}
{peer-as data -> alias peer-as}
{local-address data -> alias local-address}
{local-as data -> alias local-as}
{description data -> alias description}
{peer-group data -> alias peer-group}
{peer-cfg-rti data -> alias vrf}
{peer-fwd-rti data -> alias peer-fwd-rti}
{peer-type data -> alias peer-type}
{peer-state data -> alias peer-state}
{peer-id data -> alias peer-id}
{nlri-type-peer data -> alias nlri-type-peer}
}
%tmplt_bgp-rib%
}
}
}
dicttr::create_interface junosParse
set junosData {
bgp-information {
{
bgp-peer {
{
attributes {junos:style detail}
peer-address {{data 168.168.1.1+13071}}
peer-as {{data 65002}}
local-address {{data 168.168.1.20+179}}
local-as {{data 65001}}
description {{data {### CLIENT_EBGP ###}}}
peer-group {{data EBGP-PEER-CLIENT}}
peer-cfg-rti {{data INET}}
peer-fwd-rti {{data INET}}
peer-type {{data External}}
peer-state {{data Established}}
peer-flags {{data {Sync RSync}}}
rsync-flags {{data SocketReplicated}}
last-state {{data EstabSync}}
last-event {{data RsyncAck}}
last-error {{data Cease}}
bgp-option-information {
{
export-policy {{data pol-peer-CLIENT-export}}
import-policy {{data pol-peer-CLIENT-import}}
bgp-options {{data {RemovePrivateAS LogUpDown PeerAS Refresh}}}
bgp-options2 {{}}
bgp-options-extended {{data GracefulShutdownRcv}}
bgp-options-extended2 {{}}
holdtime {{data 90}}
preference {{data 170}}
gshut-recv-local-preference {{data 0}}
}
}
flap-count {{data 2}}
last-flap-event {{data InterfaceAddrDeleted}}
last-unreplicate-event {{data {Peer went down}}}
recv-ebgp-origin-validation-state {{data Reject}}
bgp-error {
{
name {{data Cease}}
send-count {{data 2}}
receive-count {{data 0}}
}
}
peer-id {{data 192.168.30.1}}
local-id {{data 10.201.254.1}}
active-holdtime {{data 90}}
keepalive-interval {{data 30}}
group-index {{data 60}}
peer-index {{data 0}}
snmp-index {{data 160}}
bgp-peer-iosession {
{
iosession-thread-name {{data bgpio-0}}
iosession-state {{data Enabled}}
}
}
bgp-bfd {
{
bfd-configuration-state {{data disabled}}
bfd-operational-state {{data down}}
}
}
local-interface-name {{data xe-7/1/16.0}}
local-interface-index {{data 1525}}
peer-restart-nlri-configured {{data inet-unicast}}
nlri-type-peer {{data inet-unicast}}
nlri-type-session {{data inet-unicast}}
peer-refresh-capability {{data 2}}
peer-stale-route-time-configured {{data 300}}
peer-no-restart {{data null}}
peer-restart-nlri-negotiated {{}}
peer-end-of-rib-received {{data inet-unicast}}
peer-end-of-rib-sent {{data inet-unicast}}
peer-end-of-rib-scheduled {{}}
peer-no-helper {{data null}}
peer-no-llgr-helper {{data null}}
peer-4byte-as-capability-advertised {{data 20545}}
peer-addpath-not-supported {{data null}}
bgp-rib {
{
attributes {junos:style detail}
name {{data INET.inet.0}}
rib-bit {{data 180001}}
bgp-rib-state {{data {BGP restart is complete}}}
vpn-rib-state {{data {VPN restart is complete}}}
send-state {{data {in sync}}}
active-prefix-count {{data 10}}
received-prefix-count {{data 10}}
accepted-prefix-count {{data 10}}
suppressed-prefix-count {{data 0}}
advertised-prefix-count {{data 194}}
}
}
last-received {{data 13}}
last-sent {{data 19}}
last-checked {{data 7390919}}
input-messages {{data 268719}}
input-updates {{data 30}}
input-refreshes {{data 0}}
input-octets {{data 5106716}}
output-messages {{data 253216}}
output-updates {{data 494}}
output-refreshes {{data 0}}
output-octets {{data 4825389}}
bgp-output-queue {
{
number {{data 23}}
count {{data 0}}
table-name {{data INET.inet.0}}
rib-adv-nlri {{data inet-unicast}}
}
}
}
}
}
}
}
junosParse::bgp_peer $junosData
#Result
set Result {
bgp-peer {
192.168.30.1 {
peer-address 168.168.1.1+13071
peer-as 65002
local-address 168.168.1.20+179
local-as 65001
description {### CLIENT_EBGP ###}
peer-group EBGP-PEER-CLIENT
vrf INET
peer-fwd-rti INET
peer-type External
peer-state Established
peer-id 192.168.30.1
nlri-type-peer inet-unicast
bgp-rib {
INET.inet.0 {
name INET.inet.0
total-prefix-count {}
active-prefix-count 10
received-prefix-count 10
accepted-prefix-count 10
suppressed-prefix-count 0
}
}
}
}
}