zuzu-rust 0.6.0

Rust implementation of ZuzuScript
Documentation
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
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
=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) ] );
}