zuzu-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +113 -0
  3. package/bin/zuzu +17 -0
  4. package/bin/zuzu-build-browser-bundle +57 -0
  5. package/bin/zuzu-generate-browser-stdlib +584 -0
  6. package/bin/zuzu-js +23 -0
  7. package/bin/zuzu-js-compile +152 -0
  8. package/bin/zuzu-js-electron +19 -0
  9. package/dist/zuzu-browser-worker.js +45574 -0
  10. package/dist/zuzu-browser.js +45362 -0
  11. package/lib/browser-bundle-entry.js +160 -0
  12. package/lib/browser-gui-renderer.js +387 -0
  13. package/lib/browser-runtime.js +167 -0
  14. package/lib/browser-worker-entry.js +413 -0
  15. package/lib/browser-ztests/runner.html +103 -0
  16. package/lib/browser-ztests/runner.js +369 -0
  17. package/lib/cli.js +350 -0
  18. package/lib/collections.js +367 -0
  19. package/lib/compiler.js +303 -0
  20. package/lib/electron/launcher.js +70 -0
  21. package/lib/electron/main.js +956 -0
  22. package/lib/electron/preload.js +80 -0
  23. package/lib/electron/renderer.html +122 -0
  24. package/lib/electron/renderer.js +24 -0
  25. package/lib/execution-metadata.js +18 -0
  26. package/lib/gui/dom-renderer.js +778 -0
  27. package/lib/host/browser-host.js +278 -0
  28. package/lib/host/capabilities.js +47 -0
  29. package/lib/host/electron-host.js +15 -0
  30. package/lib/host/node-host.js +74 -0
  31. package/lib/paths.js +150 -0
  32. package/lib/runtime-entrypoints.js +60 -0
  33. package/lib/runtime-helpers.js +886 -0
  34. package/lib/runtime.js +3529 -0
  35. package/lib/tap.js +37 -0
  36. package/lib/transpiler-new/ast.js +23 -0
  37. package/lib/transpiler-new/codegen.js +2455 -0
  38. package/lib/transpiler-new/errors.js +28 -0
  39. package/lib/transpiler-new/index.js +26 -0
  40. package/lib/transpiler-new/lexer.js +834 -0
  41. package/lib/transpiler-new/parser.js +2332 -0
  42. package/lib/transpiler-new/validate-bindings.js +326 -0
  43. package/lib/transpiler-utils.js +95 -0
  44. package/lib/transpiler.js +33 -0
  45. package/lib/zuzu.js +53 -0
  46. package/modules/javascript.js +193 -0
  47. package/modules/std/archive.js +603 -0
  48. package/modules/std/clib.js +338 -0
  49. package/modules/std/data/csv.js +1331 -0
  50. package/modules/std/data/json.js +531 -0
  51. package/modules/std/data/xml.js +441 -0
  52. package/modules/std/data/yaml.js +256 -0
  53. package/modules/std/db-worker.js +250 -0
  54. package/modules/std/db.js +664 -0
  55. package/modules/std/digest/_hash.js +443 -0
  56. package/modules/std/digest/md5.js +26 -0
  57. package/modules/std/digest/sha.js +72 -0
  58. package/modules/std/eval.js +10 -0
  59. package/modules/std/gui/objects.js +1519 -0
  60. package/modules/std/internals.js +571 -0
  61. package/modules/std/io/socks-worker.js +318 -0
  62. package/modules/std/io/socks.js +186 -0
  63. package/modules/std/io.js +475 -0
  64. package/modules/std/marshal/cbor.js +463 -0
  65. package/modules/std/marshal/graph.js +1624 -0
  66. package/modules/std/marshal.js +87 -0
  67. package/modules/std/math/bignum.js +91 -0
  68. package/modules/std/math.js +79 -0
  69. package/modules/std/net/dns.js +306 -0
  70. package/modules/std/net/http.js +820 -0
  71. package/modules/std/net/smtp.js +943 -0
  72. package/modules/std/net/url.js +109 -0
  73. package/modules/std/proc.js +602 -0
  74. package/modules/std/secure.js +3724 -0
  75. package/modules/std/string/base64.js +138 -0
  76. package/modules/std/string.js +299 -0
  77. package/modules/std/task.js +914 -0
  78. package/modules/std/time.js +579 -0
  79. package/modules/std/tui.js +188 -0
  80. package/modules/std/worker-thread.js +246 -0
  81. package/modules/std/worker.js +790 -0
  82. package/package.json +67 -0
  83. package/stdlib/modules/javascript.zzm +99 -0
  84. package/stdlib/modules/perl.zzm +105 -0
  85. package/stdlib/modules/std/archive.zzm +132 -0
  86. package/stdlib/modules/std/cache/lru.zzm +174 -0
  87. package/stdlib/modules/std/clib.zzm +112 -0
  88. package/stdlib/modules/std/colour.zzm +220 -0
  89. package/stdlib/modules/std/config.zzm +818 -0
  90. package/stdlib/modules/std/data/cbor.zzm +497 -0
  91. package/stdlib/modules/std/data/csv.zzm +285 -0
  92. package/stdlib/modules/std/data/ini.zzm +472 -0
  93. package/stdlib/modules/std/data/json/schema/core.zzm +573 -0
  94. package/stdlib/modules/std/data/json/schema/format.zzm +581 -0
  95. package/stdlib/modules/std/data/json/schema/model.zzm +255 -0
  96. package/stdlib/modules/std/data/json/schema/output.zzm +272 -0
  97. package/stdlib/modules/std/data/json/schema/relative_pointer.zzm +299 -0
  98. package/stdlib/modules/std/data/json/schema/validation.zzm +1503 -0
  99. package/stdlib/modules/std/data/json/schema.zzm +306 -0
  100. package/stdlib/modules/std/data/json.zzm +102 -0
  101. package/stdlib/modules/std/data/kdl/json.zzm +460 -0
  102. package/stdlib/modules/std/data/kdl/xml.zzm +387 -0
  103. package/stdlib/modules/std/data/kdl.zzm +1631 -0
  104. package/stdlib/modules/std/data/toml.zzm +756 -0
  105. package/stdlib/modules/std/data/toon.zzm +1017 -0
  106. package/stdlib/modules/std/data/xml/escape.zzm +156 -0
  107. package/stdlib/modules/std/data/xml.zzm +276 -0
  108. package/stdlib/modules/std/data/yaml.zzm +94 -0
  109. package/stdlib/modules/std/db.zzm +173 -0
  110. package/stdlib/modules/std/defer.zzm +75 -0
  111. package/stdlib/modules/std/digest/crc32.zzm +196 -0
  112. package/stdlib/modules/std/digest/md5.zzm +54 -0
  113. package/stdlib/modules/std/digest/sha.zzm +83 -0
  114. package/stdlib/modules/std/dump.zzm +317 -0
  115. package/stdlib/modules/std/eval.zzm +63 -0
  116. package/stdlib/modules/std/getopt.zzm +432 -0
  117. package/stdlib/modules/std/gui/dialogue.zzm +592 -0
  118. package/stdlib/modules/std/gui/objects.zzm +123 -0
  119. package/stdlib/modules/std/gui.zzm +1914 -0
  120. package/stdlib/modules/std/internals.zzm +139 -0
  121. package/stdlib/modules/std/io/socks.zzm +139 -0
  122. package/stdlib/modules/std/io.zzm +157 -0
  123. package/stdlib/modules/std/lingua/en.zzm +347 -0
  124. package/stdlib/modules/std/log.zzm +169 -0
  125. package/stdlib/modules/std/mail.zzm +2726 -0
  126. package/stdlib/modules/std/marshal.zzm +138 -0
  127. package/stdlib/modules/std/math/bignum.zzm +98 -0
  128. package/stdlib/modules/std/math/range.zzm +116 -0
  129. package/stdlib/modules/std/math/roman.zzm +156 -0
  130. package/stdlib/modules/std/math.zzm +141 -0
  131. package/stdlib/modules/std/net/dns.zzm +93 -0
  132. package/stdlib/modules/std/net/http.zzm +278 -0
  133. package/stdlib/modules/std/net/smtp.zzm +257 -0
  134. package/stdlib/modules/std/net/url.zzm +69 -0
  135. package/stdlib/modules/std/path/jsonpointer.zzm +526 -0
  136. package/stdlib/modules/std/path/kdl.zzm +1003 -0
  137. package/stdlib/modules/std/path/simple.zzm +520 -0
  138. package/stdlib/modules/std/path/z/context.zzm +147 -0
  139. package/stdlib/modules/std/path/z/evaluate.zzm +549 -0
  140. package/stdlib/modules/std/path/z/functions.zzm +874 -0
  141. package/stdlib/modules/std/path/z/lexer.zzm +490 -0
  142. package/stdlib/modules/std/path/z/node.zzm +1455 -0
  143. package/stdlib/modules/std/path/z/operators.zzm +445 -0
  144. package/stdlib/modules/std/path/z/parser.zzm +359 -0
  145. package/stdlib/modules/std/path/z.zzm +403 -0
  146. package/stdlib/modules/std/path/zz/functions.zzm +828 -0
  147. package/stdlib/modules/std/path/zz/operators.zzm +1036 -0
  148. package/stdlib/modules/std/path/zz.zzm +100 -0
  149. package/stdlib/modules/std/proc.zzm +155 -0
  150. package/stdlib/modules/std/result.zzm +149 -0
  151. package/stdlib/modules/std/secure.zzm +606 -0
  152. package/stdlib/modules/std/string/base64.zzm +66 -0
  153. package/stdlib/modules/std/string/quoted_printable.zzm +485 -0
  154. package/stdlib/modules/std/string.zzm +179 -0
  155. package/stdlib/modules/std/task.zzm +221 -0
  156. package/stdlib/modules/std/template/z.zzm +531 -0
  157. package/stdlib/modules/std/template/zz.zzm +62 -0
  158. package/stdlib/modules/std/time.zzm +188 -0
  159. package/stdlib/modules/std/tui.zzm +89 -0
  160. package/stdlib/modules/std/uuid.zzm +223 -0
  161. package/stdlib/modules/std/web/session.zzm +388 -0
  162. package/stdlib/modules/std/web/static.zzm +329 -0
  163. package/stdlib/modules/std/web.zzm +1942 -0
  164. package/stdlib/modules/std/worker.zzm +202 -0
  165. package/stdlib/modules/std/zuzuzoo.zzm +3960 -0
  166. package/stdlib/modules/test/more.zzm +528 -0
  167. package/stdlib/modules/test/parser.zzm +209 -0
@@ -0,0 +1,818 @@
1
+ =encoding utf8
2
+
3
+ =head1 NAME
4
+
5
+ std/config - High-level configuration loading, merging, and querying.
6
+
7
+ =head1 SYNOPSIS
8
+
9
+ from std/config import Config;
10
+ from std/io import Path;
11
+
12
+ let cfg := Config.load( [
13
+ Path.join( [ "config", "base.toml" ] ),
14
+ Path.join( [ "config", "local.json" ] ),
15
+ ] );
16
+
17
+ cfg.merge_flat(
18
+ {
19
+ "APP__port": "8080",
20
+ "APP__debug": "true",
21
+ },
22
+ {
23
+ prefix: "APP__",
24
+ separator: "__",
25
+ coerce: true,
26
+ },
27
+ );
28
+
29
+ let host := cfg @ "/database/host";
30
+ let port := cfg.get( "/port", 3000 );
31
+
32
+ =head1 IMPLEMENTATION SUPPORT
33
+
34
+ This module is supported by zuzu.pl, zuzu-rust, and zuzu-js on Node and
35
+ Electron. It is partially supported by zuzu-js in the browser: environment
36
+ override and multi-format branch selection coverage passes, but the main
37
+ filesystem-backed configuration coverage is unsupported.
38
+
39
+ =head1 DESCRIPTION
40
+
41
+ This module provides a C<Config> object for application-style
42
+ configuration loading and overlaying.
43
+
44
+ Use C<Config.from_data(...)> when you want to wrap an already-built
45
+ Zuzu value instead of loading from files.
46
+
47
+ It is intentionally geared toward patterns common in higher-level config
48
+ frameworks:
49
+
50
+ =over
51
+
52
+ =item *
53
+
54
+ load one or more files with format auto-detection,
55
+
56
+ =item *
57
+
58
+ deep-merge config layers,
59
+
60
+ =item *
61
+
62
+ apply flat overlays such as env-style C<FOO__BAR__BAZ> keys,
63
+
64
+ =item *
65
+
66
+ query config using ZPath and the C<@>, C<@@>, and C<@?> operators.
67
+
68
+ =back
69
+
70
+ The object itself is path-aware, so this works directly:
71
+
72
+ let city := cfg @ "/service/address/city";
73
+
74
+ =head1 EXPORTS
75
+
76
+ =head2 Classes
77
+
78
+ =over
79
+
80
+ =item C<Config>
81
+
82
+ Static methods:
83
+
84
+ =over
85
+
86
+ =item * C<from_data(data, options?)>
87
+
88
+ Parameters: C<data> is configuration data and C<options> controls
89
+ metadata. Returns: C<Config>. Wraps data in a config object.
90
+
91
+ =item * C<parse(text, format, options?)>
92
+
93
+ Parameters: C<text> is config text, C<format> is a format name, and
94
+ C<options> configures parsing. Returns: C<Config>. Parses text into a
95
+ config object.
96
+
97
+ =item * C<load(path_or_paths, options?)>
98
+
99
+ Parameters: C<path_or_paths> is one source or an array of sources.
100
+ Returns: C<Config>. Loads and layers config files.
101
+
102
+ =item * C<detect_format(path_or_name, fallback?)>
103
+
104
+ Parameters: C<path_or_name> is a path-like value and C<fallback> is
105
+ optional. Returns: C<String> or C<null>. Detects the config format.
106
+
107
+ =back
108
+
109
+ Instance methods:
110
+
111
+ =over
112
+
113
+ =item * C<type()>, C<can_have_named_children()>, C<can_have_indexed_children()>, C<can_have_named_indexed_children()>, C<children()>, C<attributes()>
114
+
115
+ Parameters: none. Returns: ZPath node metadata. Provides the inherited
116
+ node API used when a C<Config> is queried as a path root.
117
+
118
+ =item * C<do_action_on_child(child, action)>, C<ref_on_child(child)>
119
+
120
+ Parameters: C<child> is a selected node and C<action> is a path action.
121
+ Returns: value or C<Function>. Provides inherited mutation and reference
122
+ support for path assignments.
123
+
124
+ =item * C<to_data()>, C<clone()>
125
+
126
+ Parameters: none. Returns: value or C<Config>. Returns raw data or a
127
+ copy of the config.
128
+
129
+ =item * C<source()>, C<format()>, C<layers()>
130
+
131
+ Parameters: none. Returns: value. Returns config source metadata.
132
+
133
+ =item * C<get(path, fallback?)>, C<get_all(path)>, C<select(path)>
134
+
135
+ Parameters: C<path> is a path expression and C<fallback> is optional.
136
+ Returns: value or C<Array>. Reads config values.
137
+
138
+ =item * C<exists(path)>, C<require(path, message?)>
139
+
140
+ Parameters: C<path> is a path expression and C<message> is optional.
141
+ Returns: C<Boolean> or value. Tests for or requires a config value.
142
+
143
+ =item * C<query(path)>, C<first(path, fallback?)>
144
+
145
+ Parameters: C<path> is a path expression and C<fallback> is optional.
146
+ Returns: C<Array> or value. Queries config values.
147
+
148
+ =item * C<assign_first(path, value, op?)>
149
+
150
+ Parameters: C<path> selects values, C<value> is assigned, and C<op> is
151
+ optional. Returns: value. Updates the first match.
152
+
153
+ =item * C<assign_all(path, value, op?)>
154
+
155
+ Parameters: C<path> selects values, C<value> is assigned, and C<op> is
156
+ optional. Returns: value. Updates every match.
157
+
158
+ =item * C<assign_maybe(path, value, op?)>
159
+
160
+ Parameters: C<path> selects values, C<value> is assigned, and C<op> is
161
+ optional. Returns: C<Boolean>. Updates the first match when present.
162
+
163
+ =item * C<ref_first(path)>, C<ref_all(path)>, C<ref_maybe(path)>
164
+
165
+ Parameters: C<path> is a path expression. Returns: C<Function>,
166
+ C<Array>, or C<null>. Returns reference-like accessors.
167
+
168
+ =item * C<merge(data_or_config, options?)>, C<overlay(data_or_config, options?)>
169
+
170
+ Parameters: C<data_or_config> is incoming data and C<options> controls
171
+ merge behaviour. Returns: C<Config>. Merges configuration data.
172
+
173
+ =item * C<merge_flat(values, options?)>, C<merge_env(values, options?)>
174
+
175
+ Parameters: C<values> is flat config data and C<options> controls key
176
+ mapping. Returns: C<Config>. Merges flat or environment-style values.
177
+
178
+ =item * C<set(path, value)>, C<set_default(path, value)>
179
+
180
+ Parameters: C<path> is a simple path and C<value> is any value. Returns:
181
+ C<Config>. Sets or defaults a config value.
182
+
183
+ =item * C<encode(format?, options?)>, C<save(path, options?)>
184
+
185
+ Parameters: C<format>, C<path>, and C<options> control output. Returns:
186
+ C<String> or C<Config>. Encodes or saves configuration data.
187
+
188
+ =item * C<load_file(path, options?)>
189
+
190
+ Parameters: C<path> is a config source and C<options> controls parsing.
191
+ Returns: C<Config>. Loads one additional config file into the object.
192
+
193
+ =back
194
+
195
+ =back
196
+
197
+ =head1 NOTES
198
+
199
+ =over
200
+
201
+ =item *
202
+
203
+ C<merge> performs a deep merge for dictionaries.
204
+
205
+ =item *
206
+
207
+ Arrays are replaced by default; pass C<< { array_merge: "append" } >>
208
+ to append instead.
209
+
210
+ =item *
211
+
212
+ C<set> and C<set_default> create missing parent dictionaries, but only
213
+ for simple absolute paths such as C</server/port>. They intentionally do
214
+ not try to create missing nodes for complex selectors or filters.
215
+
216
+ =back
217
+
218
+ =head1 COPYRIGHT AND LICENCE
219
+
220
+ B<< std/config >> is copyright Toby Inkster.
221
+
222
+ It is free software; you may redistribute it and/or modify it under
223
+ the terms of either the Artistic License 1.0 or the GNU General Public
224
+ License version 2.
225
+
226
+ =cut
227
+
228
+ from std/cache/lru import Cache;
229
+ from std/path/z import ZPath;
230
+ from std/path/z/node import Node;
231
+ from std/string import join, split, substr, trim;
232
+
233
+
234
+ const _PATH_CACHE := new Cache( capacity: 50 );
235
+
236
+ function _opt ( options, key, fallback := null ) {
237
+ if ( options instanceof Dict and options.exists(key) ) {
238
+ return options.get(key);
239
+ }
240
+ return fallback;
241
+ }
242
+
243
+ function _unwrap_config_value ( value ) {
244
+ if (
245
+ value ≢ null and
246
+ value can raw and
247
+ value can query and
248
+ value can merge
249
+ ) {
250
+ return value.raw();
251
+ }
252
+ return value;
253
+ }
254
+
255
+ function _stringify_pathish ( pathish ) {
256
+ from std/io import Path;
257
+ if ( pathish instanceof Path ) {
258
+ return pathish.to_String();
259
+ }
260
+ return "" _ pathish;
261
+ }
262
+
263
+ function _normalize_format ( raw_format ) {
264
+ if ( raw_format ≡ null ) {
265
+ return null;
266
+ }
267
+
268
+ let fmt := lc( "" _ raw_format );
269
+ if ( fmt ≡ "yml" ) {
270
+ return "yaml";
271
+ }
272
+ return fmt;
273
+ }
274
+
275
+ function _detect_format_from_name ( source, fallback := null ) {
276
+ let text := lc( _stringify_pathish(source) );
277
+ let dot := null;
278
+ let i := length text - 1;
279
+ while ( i >= 0 ) {
280
+ let ch := substr( text, i, 1 );
281
+ if ( ch ≡ "." ) {
282
+ dot := i;
283
+ last;
284
+ }
285
+ if ( ch ≡ "/" or ch ≡ "\\" ) {
286
+ last;
287
+ }
288
+ i--;
289
+ }
290
+
291
+ if ( dot ≡ null or dot = length text - 1 ) {
292
+ return fallback;
293
+ }
294
+
295
+ return _normalize_format( substr( text, dot + 1 ) ) ?: fallback;
296
+ }
297
+
298
+ function _codec_for_format ( raw_format, options? ) {
299
+ let format := _normalize_format(raw_format);
300
+ if ( _opt( options, "codec", null ) ≢ null ) {
301
+ return _opt( options, "codec", null );
302
+ }
303
+
304
+ if ( format ≡ "json" ) {
305
+ from std/data/json import JSON;
306
+ return new JSON();
307
+ }
308
+ if ( format ≡ "yaml" ) {
309
+ from std/data/yaml import YAML;
310
+ return new YAML();
311
+ }
312
+ if ( format ≡ "toml" ) {
313
+ from std/data/toml import TOML;
314
+ return new TOML();
315
+ }
316
+ if ( format ≡ "ini" ) {
317
+ from std/data/ini import INI;
318
+ return new INI();
319
+ }
320
+ if ( format ≡ "toon" ) {
321
+ from std/data/toon import TOON;
322
+ return new TOON();
323
+ }
324
+
325
+ die `std/config does not know how to handle format '${format}'`;
326
+ }
327
+
328
+ function _ensure_path_object ( source ) {
329
+ from std/io import Path;
330
+ return source instanceof Path ? source : new Path( "" _ source );
331
+ }
332
+
333
+ function _compile_path ( pathish ) {
334
+ if ( pathish instanceof ZPath ) {
335
+ return pathish;
336
+ }
337
+ if (
338
+ pathish can query and
339
+ pathish can first and
340
+ pathish can exists and
341
+ pathish can assign_first and
342
+ pathish can ref_first
343
+ ) {
344
+ return pathish;
345
+ }
346
+
347
+ let expression := "" _ pathish;
348
+ return _PATH_CACHE.get(
349
+ expression,
350
+ fn path_text -> new ZPath( path: path_text ),
351
+ );
352
+ }
353
+
354
+ function _coerce_scalar_text ( raw_value, options? ) {
355
+ if ( not _opt( options, "coerce", false ) ) {
356
+ return raw_value;
357
+ }
358
+
359
+ if ( not( raw_value instanceof String ) ) {
360
+ return raw_value;
361
+ }
362
+
363
+ let text := trim(raw_value);
364
+ let lowered := lc(text);
365
+
366
+ if ( lowered ≡ "true" ) {
367
+ return true;
368
+ }
369
+ if ( lowered ≡ "false" ) {
370
+ return false;
371
+ }
372
+ if ( lowered ≡ "null" or lowered ≡ "~" ) {
373
+ return null;
374
+ }
375
+ if ( text ~ /^-?[0-9]+$/ ) {
376
+ return int(text);
377
+ }
378
+ if ( text ~ /^-?(?:[0-9]+\.[0-9]+|\.[0-9]+)$/ ) {
379
+ return 0 + text;
380
+ }
381
+ if (
382
+ length text >= 2 and
383
+ (
384
+ (
385
+ substr( text, 0, 1 ) ≡ "{" and
386
+ substr( text, length text - 1, 1 ) ≡ "}"
387
+ ) or
388
+ (
389
+ substr( text, 0, 1 ) ≡ "[" and
390
+ substr( text, length text - 1, 1 ) ≡ "]"
391
+ )
392
+ )
393
+ ) {
394
+ try {
395
+ from std/data/json import JSON;
396
+ return new JSON().decode(text);
397
+ }
398
+ catch {
399
+ }
400
+ }
401
+
402
+ return raw_value;
403
+ }
404
+
405
+ function _merge_values ( left, right, options? ) {
406
+ let left_value := _unwrap_config_value(left);
407
+ let right_value := _unwrap_config_value(right);
408
+
409
+ if ( left_value instanceof Dict and right_value instanceof Dict ) {
410
+ for ( let key in right_value.keys() ) {
411
+ if ( left_value.exists(key) ) {
412
+ left_value{(key)} := _merge_values(
413
+ left_value.get(key),
414
+ right_value.get(key),
415
+ options,
416
+ );
417
+ }
418
+ else {
419
+ left_value{(key)} := right_value.get(key);
420
+ }
421
+ }
422
+ return left_value;
423
+ }
424
+
425
+ if (
426
+ left_value instanceof Array and
427
+ right_value instanceof Array and
428
+ _opt( options, "array_merge", "replace" ) ≡ "append"
429
+ ) {
430
+ for ( let item in right_value ) {
431
+ left_value.push(item);
432
+ }
433
+ return left_value;
434
+ }
435
+
436
+ return right_value;
437
+ }
438
+
439
+ function _simple_path_parts ( raw_path ) {
440
+ let text := "" _ raw_path;
441
+ if ( text ≡ "" ) {
442
+ return [];
443
+ }
444
+ if ( substr( text, 0, 1 ) ≡ "/" ) {
445
+ text := substr( text, 1 );
446
+ }
447
+ if ( text ≡ "" ) {
448
+ return [];
449
+ }
450
+
451
+ let parts := [];
452
+ for ( let part in split( text, "/" ) ) {
453
+ next if part ≡ "";
454
+ die `std/config simple path cannot contain complex selector '${part}'`
455
+ if part ~ /[\*\[\]#@\(\)]/;
456
+ parts.push(part);
457
+ }
458
+ return parts;
459
+ }
460
+
461
+ function _ensure_root_dict ( current ) {
462
+ if ( current ≡ null ) {
463
+ return {};
464
+ }
465
+ if ( current instanceof Dict ) {
466
+ return current;
467
+ }
468
+ die "std/config expected Dict root for simple path mutation";
469
+ }
470
+
471
+ class Config extends Node {
472
+ let _source := null;
473
+ let _format := null;
474
+ let _layers := [];
475
+
476
+ method __build__ () {
477
+ self.set_raw( self.raw ≡ null ? {}: self.raw );
478
+ _layers := _layers ≡ null ? []: _layers;
479
+ self._build_id();
480
+ }
481
+
482
+ static method detect_format ( source, fallback := null ) {
483
+ return _detect_format_from_name( source, fallback );
484
+ }
485
+
486
+ static method from_data ( data, options? ) {
487
+ return new Config(
488
+ raw: _unwrap_config_value(data),
489
+ _source: _opt( options, "source", null ),
490
+ _format: _normalize_format( _opt( options, "format", null ) ),
491
+ _layers: _opt( options, "layers", [] ),
492
+ );
493
+ }
494
+
495
+ static method parse ( text, format, options? ) {
496
+ let codec := _codec_for_format( format, options );
497
+ let layer_source := _opt( options, "source", null );
498
+ let decoded := codec.decode(text);
499
+ return new Config(
500
+ raw: decoded,
501
+ _source: layer_source,
502
+ _format: _normalize_format(format),
503
+ _layers: [
504
+ {
505
+ source: layer_source,
506
+ format: _normalize_format(format),
507
+ },
508
+ ],
509
+ );
510
+ }
511
+
512
+ static method load ( sources, options? ) {
513
+ let cfg := new Config( raw: {}, _layers: [] );
514
+ for ( let source in ( sources instanceof Array ? sources : [ sources ] ) ) {
515
+ cfg.load_file( source, options );
516
+ }
517
+ return cfg;
518
+ }
519
+
520
+ method source () {
521
+ return _source;
522
+ }
523
+
524
+ method format () {
525
+ return _format;
526
+ }
527
+
528
+ method layers () {
529
+ return _layers;
530
+ }
531
+
532
+ method to_data () {
533
+ return self.raw;
534
+ }
535
+
536
+ method clone () {
537
+ let cloned := new Config( raw: _merge_values( {}, self.raw, {} ) );
538
+ cloned._layers := _layers.to_Array();
539
+ cloned._source := _source;
540
+ cloned._format := _format;
541
+ return cloned;
542
+ }
543
+
544
+ method _delegate_node () {
545
+ return Node.from_root( self.raw );
546
+ }
547
+
548
+ method type () {
549
+ return self._delegate_node().type();
550
+ }
551
+
552
+ method can_have_named_children () {
553
+ return self._delegate_node().can_have_named_children();
554
+ }
555
+
556
+ method can_have_indexed_children () {
557
+ return self._delegate_node().can_have_indexed_children();
558
+ }
559
+
560
+ method can_have_named_indexed_children () {
561
+ return self._delegate_node().can_have_named_indexed_children();
562
+ }
563
+
564
+ method children () {
565
+ let out := [];
566
+ for ( let child in self._delegate_node().children() ) {
567
+ out.push(
568
+ Node.wrap(
569
+ child.raw(),
570
+ self,
571
+ child.key(),
572
+ child.ix(),
573
+ ),
574
+ );
575
+ }
576
+ return out;
577
+ }
578
+
579
+ method attributes () {
580
+ let out := [];
581
+ for ( let child in self._delegate_node().attributes() ) {
582
+ out.push(
583
+ Node.wrap(
584
+ child.raw(),
585
+ self,
586
+ child.key(),
587
+ child.ix(),
588
+ ),
589
+ );
590
+ }
591
+ return out;
592
+ }
593
+
594
+ method do_action_on_child ( child, action ) {
595
+ return super( child, action ) if action{op} ne ":=";
596
+
597
+ let container := self.raw;
598
+ if ( container instanceof Dict ) {
599
+ let key := child.key();
600
+ die "Path assignment expects string dict key" if key ≡ null;
601
+ container{(key)} := action{value};
602
+ return action{value};
603
+ }
604
+ if ( container instanceof Array ) {
605
+ let ix := child.ix();
606
+ die "Path assignment expects numeric array index" if ix ≡ null;
607
+ container[ix] := action{value};
608
+ return action{value};
609
+ }
610
+ if ( container instanceof PairList ) {
611
+ let key := child.key();
612
+ die "Path assignment expects string pairlist key" if key ≡ null;
613
+ container.set( key, action{value} );
614
+ return action{value};
615
+ }
616
+ return super( child, action );
617
+ }
618
+
619
+ method ref_on_child ( child ) {
620
+ let container := self.raw;
621
+ if ( container instanceof Dict ) {
622
+ let key := child.key();
623
+ die "Path assignment expects string dict key" if key ≡ null;
624
+ return \ container{(key)};
625
+ }
626
+ if ( container instanceof Array ) {
627
+ let ix := child.ix();
628
+ die "Path assignment expects numeric array index" if ix ≡ null;
629
+ return \ container[ix];
630
+ }
631
+ if ( container instanceof PairList ) {
632
+ let key := child.key();
633
+ die "Path assignment expects string pairlist key" if key ≡ null;
634
+ return \ container{(key)};
635
+ }
636
+ return super(child);
637
+ }
638
+
639
+ method query ( pathish ) {
640
+ return _compile_path(pathish).query(self);
641
+ }
642
+
643
+ method select ( pathish ) {
644
+ return self.query(pathish);
645
+ }
646
+
647
+ method get_all ( pathish ) {
648
+ return self.query(pathish);
649
+ }
650
+
651
+ method first ( pathish, fallback? ) {
652
+ return _compile_path(pathish).first( self, fallback );
653
+ }
654
+
655
+ method get ( pathish, fallback? ) {
656
+ return self.first( pathish, fallback );
657
+ }
658
+
659
+ method exists ( pathish ) {
660
+ return _compile_path(pathish).exists(self);
661
+ }
662
+
663
+ method require ( pathish, message? ) {
664
+ if ( self.exists(pathish) ) {
665
+ return self.get(pathish);
666
+ }
667
+ die(
668
+ message ?:
669
+ `std/config required setting not found at '${pathish}'`
670
+ );
671
+ }
672
+
673
+ method assign_first ( pathish, value, op := ":=", weak := false ) {
674
+ return _compile_path(pathish).assign_first( self, value, op, weak );
675
+ }
676
+
677
+ method assign_all ( pathish, value, op := ":=", weak := false ) {
678
+ return _compile_path(pathish).assign_all( self, value, op, weak );
679
+ }
680
+
681
+ method assign_maybe ( pathish, value, op := ":=", weak := false ) {
682
+ return _compile_path(pathish).assign_maybe( self, value, op, weak );
683
+ }
684
+
685
+ method ref_first ( pathish ) {
686
+ return _compile_path(pathish).ref_first(self);
687
+ }
688
+
689
+ method ref_all ( pathish ) {
690
+ return _compile_path(pathish).ref_all(self);
691
+ }
692
+
693
+ method ref_maybe ( pathish ) {
694
+ return _compile_path(pathish).ref_maybe(self);
695
+ }
696
+
697
+ method merge ( other, options? ) {
698
+ let incoming := _unwrap_config_value(other);
699
+ self.set_raw( _merge_values( self.raw, incoming, options ) );
700
+ return self;
701
+ }
702
+
703
+ method overlay ( other, options? ) {
704
+ return self.merge( other, options );
705
+ }
706
+
707
+ method merge_flat ( values, options? ) {
708
+ return self if not( values instanceof Dict );
709
+
710
+ let prefix := "" _ _opt( options, "prefix", "" );
711
+ let separator := "" _ _opt( options, "separator", "__" );
712
+ let downcase := _opt( options, "lowercase", false );
713
+
714
+ for ( let key in values.keys() ) {
715
+ let name := "" _ key;
716
+ if ( prefix ≢ "" ) {
717
+ next if substr( name, 0, length prefix ) ne prefix;
718
+ name := substr( name, length prefix );
719
+ }
720
+ next if name ≡ "";
721
+
722
+ let parts := [];
723
+ for ( let part in split( name, separator ) ) {
724
+ next if part ≡ "";
725
+ parts.push( downcase ? lc(part) : part );
726
+ }
727
+ next if parts.length() = 0;
728
+
729
+ self.set(
730
+ "/" _ join( "/", parts ),
731
+ _coerce_scalar_text( values.get(key), options ),
732
+ );
733
+ }
734
+
735
+ return self;
736
+ }
737
+
738
+ method merge_env ( values, options? ) {
739
+ return self.merge_flat( values, options );
740
+ }
741
+
742
+ method set ( pathish, value ) {
743
+ let parts := _simple_path_parts(pathish);
744
+ if ( parts.length() = 0 ) {
745
+ self.set_raw(value);
746
+ return value;
747
+ }
748
+
749
+ self.set_raw( _ensure_root_dict( self.raw ) );
750
+ let cursor := self.raw;
751
+ let i := 0;
752
+ while ( i < parts.length() - 1 ) {
753
+ let key := parts[i];
754
+ if ( not cursor.exists(key) or cursor.get(key) ≡ null ) {
755
+ cursor{(key)} := {};
756
+ }
757
+ else if ( not( cursor.get(key) instanceof Dict ) ) {
758
+ die `std/config cannot create child path below non-Dict setting '${key}'`;
759
+ }
760
+ cursor := cursor.get(key);
761
+ i++;
762
+ }
763
+
764
+ cursor{(parts[parts.length() - 1])} := value;
765
+ return value;
766
+ }
767
+
768
+ method set_default ( pathish, value ) {
769
+ if ( not self.exists(pathish) or self.get(pathish) ≡ null ) {
770
+ return self.set( pathish, value );
771
+ }
772
+ return self.get(pathish);
773
+ }
774
+
775
+ method encode ( format?, options? ) {
776
+ let chosen := _normalize_format(
777
+ format ?: _format ?: "json"
778
+ );
779
+ return _codec_for_format( chosen, options ).encode( self.raw );
780
+ }
781
+
782
+ method save ( destination, options? ) {
783
+ let path := _ensure_path_object(destination);
784
+ let chosen := _normalize_format(
785
+ _opt( options, "format", null ) ?:
786
+ Config.detect_format( path, _format ?: "json" )
787
+ );
788
+ _codec_for_format( chosen, options ).dump( path, self.raw );
789
+ return path;
790
+ }
791
+
792
+ method load_file ( source, options? ) {
793
+ let path := _ensure_path_object(source);
794
+ if ( _opt( options, "optional", false ) and not path.exists() ) {
795
+ return self;
796
+ }
797
+
798
+ let chosen := _normalize_format(
799
+ _opt( options, "format", null ) ?:
800
+ Config.detect_format(path)
801
+ );
802
+ die `std/config could not determine format for '${path}'`
803
+ if chosen ≡ null;
804
+
805
+ let decoded := _codec_for_format( chosen, options ).load(path);
806
+ self.merge( decoded, options );
807
+
808
+ _source := path.to_String();
809
+ _format := chosen;
810
+ _layers.push(
811
+ {
812
+ source: _source,
813
+ format: chosen,
814
+ },
815
+ );
816
+ return self;
817
+ }
818
+ }