=encoding utf8
=head1 NAME
std/data/kdl/json - JSON-in-KDL structure conversion.
=head1 SYNOPSIS
from std/data/kdl import KDL;
from std/data/kdl/json import kdl_to_json, json_to_kdl;
let kdl_doc := ( new KDL() ).decode( """- foo=1 bar=#true""" );
let data := kdl_to_json(kdl_doc);
let roundtrip := json_to_kdl(data);
=head1 IMPLEMENTATION SUPPORT
This module is supported by all implementations of ZuzuScript.
=head1 DESCRIPTION
This module implements the JSON-in-KDL (JiK) mapping for parsed Zuzu
objects. C<kdl_to_json> accepts a C<KDLDocument> or C<KDLNode> and
returns native Zuzu data structures. C<json_to_kdl> accepts native
JSON-like data structures and returns a C<KDLDocument>.
C<kdl_to_json(value, pairlists: true)> maps object-like JiK nodes to
C<PairList> values instead of C<Dict> values, preserving key order and
duplicate keys.
=head1 EXPORTS
=head2 Functions
=over
=item C<< kdl_to_json(value, ... PairList opts) >>
Parameters: C<value> is a C<KDLDocument>, C<KDLNode>, or compatible KDL
value and C<opts> may include C<pairlists>. Returns: value. Converts
JSON-in-KDL structures into native JSON-like ZuzuScript data.
=item C<< json_to_kdl(value) >>
Parameters: C<value> is a JSON-like ZuzuScript value. Returns:
C<KDLDocument>. Converts native JSON-like data into JSON-in-KDL nodes.
=back
=head1 COPYRIGHT AND LICENCE
B<< std/data/kdl/json >> is copyright Toby Inkster.
It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.
=cut
from std/data/kdl import KDLDocument, KDLNode, KDLValue;
from std/data/kdl/xml import xml_to_kdl;
from std/time import Time;
function _jik_has_props ( KDLNode node ) {
return node.props().to_Array().length() > 0;
}
function _jik_literal_value ( value ) {
die "JSON-in-KDL literal must be a KDLValue"
if not( value instanceof KDLValue );
if ( value.is_number() and value.kind() ≡ "string" ) {
die "JSON-in-KDL does not support non-finite KDL number keywords";
}
return value.native_value();
}
function _jik_is_time_native ( value ) {
return value instanceof Time;
}
function _jik_pad2 ( value ) {
let text := "" _ value;
return length text < 2 ? "0" _ text : text;
}
function _jik_pad4 ( value ) {
let text := "" _ value;
while ( length text < 4 ) {
text := "0" _ text;
}
return text;
}
function _jik_time_text ( value ) {
return _jik_pad4( value.year() )
_ "-" _ _jik_pad2( value.mon() )
_ "-" _ _jik_pad2( value.day_of_month() )
_ "T" _ _jik_pad2( value.hour() )
_ ":" _ _jik_pad2( value.min() )
_ ":" _ _jik_pad2( value.sec() );
}
function _jik_value ( value ) {
if ( value instanceof KDLValue ) {
return value;
}
if ( value ≡ null ) {
return new KDLValue( type: "null", value: null );
}
if ( value instanceof Boolean ) {
return new KDLValue( type: "boolean", value: value );
}
if ( value instanceof Number ) {
return new KDLValue( type: "number", kind: "float", value: value );
}
if ( value instanceof String or value instanceof BinaryString ) {
return new KDLValue( type: "string", value: "" _ value );
}
if ( _jik_is_time_native(value) ) {
return new KDLValue(
type: "string",
value: _jik_time_text(value),
type_annotation: "date-time",
);
}
return new KDLValue( type: typeof value, value: value );
}
function _jik_is_array_native ( value ) {
return value instanceof Array or value instanceof Set or value instanceof Bag;
}
function _jik_is_object_native ( value ) {
return value instanceof Dict or value instanceof PairList;
}
function _jik_is_kdl_native ( value ) {
return value instanceof KDLDocument or value instanceof KDLNode;
}
function _jik_is_xml_native ( value ) {
try {
if ( value can nodeType ) {
return true if value.nodeType() ≢ null;
}
}
catch {
}
try {
return false if not( value can documentElement );
value.documentElement();
return true;
}
catch {
}
return false;
}
function _jik_is_opaque_literal_native ( value ) {
return not _jik_is_array_native(value)
and not _jik_is_object_native(value)
and not _jik_is_kdl_native(value)
and not _jik_is_xml_native(value);
}
function _jik_is_literal_native ( value ) {
return value ≡ null
or value instanceof Boolean
or value instanceof Number
or value instanceof String
or value instanceof BinaryString
or _jik_is_time_native(value)
or value instanceof KDLValue
or _jik_is_opaque_literal_native(value);
}
function _jik_sorted_array ( value ) {
if ( value instanceof Set or value instanceof Bag ) {
return value.sortstr();
}
return value;
}
function _jik_pairs ( obj ) {
let out := [];
if ( obj instanceof PairList ) {
for ( let p in obj.to_Array() ) {
out.push( p{pair} );
}
return out;
}
for ( let key in obj.sorted_keys() ) {
out.push( [ key, obj.get(key) ] );
}
return out;
}
function _jik_native_to_node;
function _jik_native_to_nodes;
function _jik_xml_nodes ( value ) {
return xml_to_kdl(value).nodes();
}
function _jik_kdl_nodes ( value ) {
if ( value instanceof KDLDocument ) {
return value.nodes();
}
return [ value ];
}
function _jik_structural_nodes ( value, String name := "-" ) {
let nodes := _jik_is_kdl_native(value)
? _jik_kdl_nodes(value)
: _jik_xml_nodes(value);
if ( name ≡ "-" ) {
return nodes.length() = 0 ? [ new KDLNode( name: name ) ] : nodes;
}
return [ new KDLNode( name: name, children: nodes ) ];
}
function _jik_make_array_node ( value, String name := "-" ) {
let items := _jik_sorted_array(value);
let all_literals := true;
for ( let item in items ) {
all_literals := false unless _jik_is_literal_native(item);
}
let annotate := items.length() < 2;
if ( all_literals ) {
return new KDLNode(
name: name,
type_annotation: annotate ? "array" : null,
args: items.map( fn item -> _jik_value(item) ),
);
}
let children := [];
for ( let item in items ) {
for ( let child in _jik_native_to_nodes( item, "-" ) ) {
children.push(child);
}
}
return new KDLNode(
name: name,
type_annotation: annotate ? "array" : null,
children: children,
);
}
function _jik_object_needs_annotation ( Array pairs, Boolean pairlist ) {
if ( pairs.length() = 0 ) {
return true;
}
if ( pairlist ) {
return pairs.all( fn pair -> pair[0] ≡ "-" );
}
return pairs.length() = 1 and pairs[0][0] ≡ "-"
and not _jik_is_literal_native( pairs[0][1] );
}
function _jik_make_object_node ( value, String name := "-" ) {
let pairs := _jik_pairs(value);
let pairlist := value instanceof PairList;
let props := new PairList();
let children := [];
if ( pairlist ) {
for ( let pair in pairs ) {
for ( let child in _jik_native_to_nodes( pair[1], pair[0] ) ) {
children.push(child);
}
}
}
else {
for ( let pair in pairs ) {
if ( _jik_is_literal_native( pair[1] ) ) {
props.add( pair[0], _jik_value( pair[1] ) );
}
else {
for ( let child in _jik_native_to_nodes( pair[1], pair[0] ) ) {
children.push(child);
}
}
}
}
return new KDLNode(
name: name,
type_annotation: _jik_object_needs_annotation( pairs, pairlist )
? "object"
: null,
props: props,
children: children,
);
}
function _jik_native_to_node ( value, String name := "-" ) {
if ( _jik_is_literal_native(value) ) {
return new KDLNode( name: name, args: [ _jik_value(value) ] );
}
if ( _jik_is_array_native(value) ) {
return _jik_make_array_node( value, name );
}
if ( _jik_is_object_native(value) ) {
return _jik_make_object_node( value, name );
}
if ( _jik_is_kdl_native(value) or _jik_is_xml_native(value) ) {
let nodes := _jik_structural_nodes( value, name );
return nodes.length() = 1 ? nodes[0] : new KDLNode(
name: name,
children: nodes,
);
}
die `Cannot convert ${typeof value} to JSON-in-KDL`;
}
function _jik_native_to_nodes ( value, String name := "-" ) {
if ( _jik_is_kdl_native(value) or _jik_is_xml_native(value) ) {
return _jik_structural_nodes( value, name );
}
return [ _jik_native_to_node( value, name ) ];
}
function _jik_node_has_only_dash_children ( KDLNode node ) {
return node.children().all( fn child -> child.name() ≡ "-" );
}
function _jik_node_kind ( KDLNode node ) {
let annotation := node.type_annotation();
if ( annotation ≡ "array" or annotation ≡ "object" ) {
return annotation;
}
if ( _jik_has_props(node) ) {
return "object";
}
if ( node.children().length() > 0 ) {
if ( _jik_node_has_only_dash_children(node) ) {
return "array";
}
return "object";
}
if ( node.args().length() = 1 ) {
return "literal";
}
if ( node.args().length() > 1 ) {
return "array";
}
die "Empty JSON-in-KDL node must be annotated as array or object";
}
function _jik_node_to_native;
function _jik_array_from_node ( KDLNode node, PairList opts ) {
die "JSON-in-KDL array node cannot contain properties"
if _jik_has_props(node);
die "Empty JSON-in-KDL array node must be annotated as array"
if node.args().length() = 0
and node.children().length() = 0
and node.type_annotation() ≢ "array";
die "JSON-in-KDL array child nodes must be named '-'"
unless _jik_node_has_only_dash_children(node);
let out := [];
for ( let arg in node.args() ) {
out.push( _jik_literal_value(arg) );
}
for ( let child in node.children() ) {
out.push( _jik_node_to_native( child, opts ) );
}
return out;
}
function _jik_store_object_item ( obj, String key, value, Boolean pairlists ) {
if ( pairlists ) {
obj.add( key, value );
return;
}
die `Duplicate JSON-in-KDL object key '${key}'` if obj.exists(key);
obj.set( key, value );
}
function _jik_object_from_node ( KDLNode node, PairList opts ) {
die "JSON-in-KDL object node cannot contain unnamed arguments"
if node.args().length() > 0;
die "Empty JSON-in-KDL object node must be annotated as object"
if not _jik_has_props(node)
and node.children().length() = 0
and node.type_annotation() ≢ "object";
let pairlists := opts.get( "pairlists", false );
let out := pairlists ? new PairList() : {};
for ( let pair in node.props().to_Array() ) {
let kv := pair{pair};
_jik_store_object_item(
out,
kv[0],
_jik_literal_value( kv[1] ),
pairlists,
);
}
for ( let child in node.children() ) {
_jik_store_object_item(
out,
child.name(),
_jik_node_to_native( child, opts ),
pairlists,
);
}
return out;
}
function _jik_node_to_native ( KDLNode node, PairList opts ) {
let kind := _jik_node_kind(node);
if ( kind ≡ "literal" ) {
die "JSON-in-KDL literal node must contain one argument only"
if node.args().length() ≢ 1
or _jik_has_props(node)
or node.children().length() > 0;
return _jik_literal_value( node.args()[0] );
}
if ( kind ≡ "array" ) {
return _jik_array_from_node( node, opts );
}
if ( kind ≡ "object" ) {
return _jik_object_from_node( node, opts );
}
die `Unknown JSON-in-KDL node kind '${kind}'`;
}
function kdl_to_json ( value, ... PairList opts ) {
if ( value instanceof KDLNode ) {
return _jik_node_to_native( value, opts );
}
if ( value instanceof KDLDocument ) {
die "JSON-in-KDL document must contain exactly one top-level node"
if value.nodes().length() ≢ 1;
return _jik_node_to_native( value.nodes()[0], opts );
}
die "kdl_to_json expects a KDLDocument or KDLNode";
}
function json_to_kdl ( value ) {
if ( value instanceof KDLDocument ) {
return value;
}
if ( value instanceof KDLNode ) {
return value;
}
return new KDLDocument( nodes: [ _jik_native_to_node(value) ] );
}
std/data/kdl/json
Standard Library source code
JSON-in-KDL structure conversion.
Module
- Name
std/data/kdl/json- Area
- Standard Library
- Source
modules/std/data/kdl/json.zzm