@flowscripter/dynamic-cli-framework 12.1.0 → 12.2.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 (49) hide show
  1. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  2. package/dist/src/cli/BaseCLI.js +1 -1
  3. package/dist/src/runtime/runner.d.ts.map +1 -1
  4. package/dist/src/runtime/runner.js +4 -1
  5. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +23 -13
  6. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
  7. package/dist/src/service/configuration/ConfigurationServiceProvider.js +69 -179
  8. package/dist/src/service/configuration/DefaultConfigurationService.d.ts +78 -0
  9. package/dist/src/service/configuration/DefaultConfigurationService.d.ts.map +1 -0
  10. package/dist/src/service/configuration/DefaultConfigurationService.js +296 -0
  11. package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
  12. package/dist/src/service/configuration/DefaultKeyValueService.js +7 -3
  13. package/dist/src/service/configuration/DefaultSecretService.d.ts +17 -2
  14. package/dist/src/service/configuration/DefaultSecretService.d.ts.map +1 -1
  15. package/dist/src/service/configuration/DefaultSecretService.js +44 -5
  16. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts +3 -1
  17. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
  18. package/dist/src/service/configuration/KeyValueServiceProvider.js +6 -3
  19. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts +3 -7
  20. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts.map +1 -1
  21. package/dist/src/service/configuration/command/ConfigDumpSubCommand.js +5 -10
  22. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts +16 -0
  23. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts.map +1 -0
  24. package/dist/src/service/configuration/command/ConfigGetSubCommand.js +137 -0
  25. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts +18 -0
  26. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts.map +1 -0
  27. package/dist/src/service/configuration/command/ConfigSetSubCommand.js +289 -0
  28. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts +15 -0
  29. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts.map +1 -0
  30. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.js +101 -0
  31. package/dist/src/service/configuration/configTargets.d.ts +91 -0
  32. package/dist/src/service/configuration/configTargets.d.ts.map +1 -0
  33. package/dist/src/service/configuration/configTargets.js +348 -0
  34. package/dist/src/service/configuration/resolveSecrets.d.ts.map +1 -1
  35. package/dist/src/service/configuration/resolveSecrets.js +7 -1
  36. package/package.json +1 -1
  37. package/src/cli/BaseCLI.ts +1 -0
  38. package/src/runtime/runner.ts +4 -1
  39. package/src/service/configuration/ConfigurationServiceProvider.ts +95 -227
  40. package/src/service/configuration/DefaultConfigurationService.ts +378 -0
  41. package/src/service/configuration/DefaultKeyValueService.ts +9 -3
  42. package/src/service/configuration/DefaultSecretService.ts +51 -8
  43. package/src/service/configuration/KeyValueServiceProvider.ts +7 -2
  44. package/src/service/configuration/command/ConfigDumpSubCommand.ts +6 -10
  45. package/src/service/configuration/command/ConfigGetSubCommand.ts +198 -0
  46. package/src/service/configuration/command/ConfigSetSubCommand.ts +401 -0
  47. package/src/service/configuration/command/ConfigUnsetSubCommand.ts +126 -0
  48. package/src/service/configuration/configTargets.ts +481 -0
  49. package/src/service/configuration/resolveSecrets.ts +7 -1
@@ -1,7 +1,3 @@
1
- import process from "node:process";
2
- import path from "node:path";
3
- import fs from "node:fs/promises";
4
- import { Stats } from "node:fs";
5
1
  import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
6
2
  import ConfigCommand from "./command/ConfigCommand.ts";
7
3
  import type {
@@ -13,19 +9,23 @@ import type {
13
9
  } from "@flowscripter/dynamic-cli-framework-api";
14
10
  import getLogger from "../../util/logger.ts";
15
11
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
16
- import type { GroupCommand } from "@flowscripter/dynamic-cli-framework-api";
12
+ import type { SubCommand, GroupCommand } from "@flowscripter/dynamic-cli-framework-api";
17
13
  import ConfigDumpSubCommand from "./command/ConfigDumpSubCommand.ts";
18
14
  import ConfigGroupCommand from "./command/ConfigGroupCommand.ts";
15
+ import ConfigGetSubCommand from "./command/ConfigGetSubCommand.ts";
16
+ import ConfigSetSubCommand from "./command/ConfigSetSubCommand.ts";
17
+ import ConfigUnsetSubCommand from "./command/ConfigUnsetSubCommand.ts";
18
+ import type { CommandRegistry } from "@flowscripter/dynamic-cli-framework-api";
19
19
  import {
20
20
  CONFIGURATION_SERVICE_ID,
21
- SECRET_SENTINEL_PREFIX,
21
+ type ConfigurationService,
22
22
  } from "@flowscripter/dynamic-cli-framework-api";
23
- import type { ConfigurationService } from "@flowscripter/dynamic-cli-framework-api";
24
- import DefaultSecretService from "./DefaultSecretService.ts";
23
+ import DefaultConfigurationService, {
24
+ type KeyValueServiceScopeType,
25
+ } from "./DefaultConfigurationService.ts";
26
+ import DefaultSecretService, { type SecretsApi } from "./DefaultSecretService.ts";
25
27
  import type ConfigFormat from "./format/ConfigFormat.ts";
26
- import type { ConfigData } from "./format/ConfigFormat.ts";
27
28
  import jsonFormat from "./format/JsonConfigFormat.ts";
28
- import resolveSecrets from "./resolveSecrets.ts";
29
29
  import type { Command } from "@flowscripter/dynamic-cli-framework-api";
30
30
  import argumentValueMerge from "../../runtime/values/argumentValueMerge.ts";
31
31
  import {
@@ -39,23 +39,29 @@ import {
39
39
  getSubCommandValuesFromEnvVars,
40
40
  } from "../../util/envVarHelper.ts";
41
41
 
42
- const logger = getLogger("ConfigurationServiceProvider");
42
+ export type { KeyValueServiceScopeType };
43
43
 
44
- export type KeyValueServiceScopeType = "command" | "service";
44
+ const logger = getLogger("ConfigurationServiceProvider");
45
45
 
46
- class ConfigurationServiceImpl implements ConfigurationService {
47
- readonly #provider: ConfigurationServiceProvider;
46
+ /**
47
+ * The only view of the configuration which is registered in the {@link Context}, and therefore reachable by
48
+ * every command and service. It deliberately exposes nothing but the config file location: the
49
+ * {@link DefaultConfigurationService} (default values, per-scope key-value data, secret resolution and
50
+ * persistence) is only handed to the `config` group commands and {@link KeyValueServiceProvider}.
51
+ */
52
+ class ConfigurationLocationService implements ConfigurationService {
53
+ readonly #service: DefaultConfigurationService;
48
54
 
49
- public constructor(provider: ConfigurationServiceProvider) {
50
- this.#provider = provider;
55
+ public constructor(service: DefaultConfigurationService) {
56
+ this.#service = service;
51
57
  }
52
58
 
53
59
  public get configLocation(): string | undefined {
54
- return this.#provider.configLocation;
60
+ return this.#service.configLocation;
55
61
  }
56
62
 
57
63
  public setConfigLocation(location: string): void {
58
- this.#provider.setConfigLocation(location);
64
+ this.#service.setConfigLocation(location);
59
65
  }
60
66
  }
61
67
 
@@ -65,11 +71,9 @@ class ConfigurationServiceImpl implements ConfigurationService {
65
71
  *
66
72
  * The same configuration file also backs the raw, per-scope key-value data consumed by
67
73
  * {@link KeyValueServiceProvider} (which holds a direct reference to this provider - see
68
- * {@link getKeyValueData} and {@link flushIfDirty} - rather than looking this provider up via
69
- * {@link Context}, since only {@link configLocation}/{@link setConfigLocation} are safe to expose
70
- * generally; {@link getConfigString} is wired directly to {@link ConfigDumpSubCommand} for the same
71
- * reason - a full config dump would leak every other command's/service's key-value data if it
72
- * were reachable through a general lookup).
74
+ * {@link getKeyValueData} and {@link flushIfDirty}). The configuration data and its persistence are
75
+ * owned by {@link DefaultConfigurationService}, which is passed directly to the `config` group commands. Only a
76
+ * view exposing the config file location is registered under {@link CONFIGURATION_SERVICE_ID}.
73
77
  *
74
78
  * **Default Command Arguments**
75
79
  *
@@ -120,10 +124,10 @@ class ConfigurationServiceImpl implements ConfigurationService {
120
124
  * default configuration will be used. The location of the configuration file can be modified via the
121
125
  * {@link ConfigCommand}.
122
126
  *
123
- * NOTE: You may store default arguments for secrets by manually configuring secrets in your OS
124
- * credential store and referencing them in the config file using the sentinel format
125
- * `__SECRET__:<bun_secret_name>`. These values will be resolved from the OS secret store
126
- * before being returned as default argument values. See {@link KeyValueServiceProvider} for the
127
+ * NOTE: Secrets in default arguments are referenced in the config file or an environment variable using
128
+ * the sentinel format `__SECRET__:<bun_secret_name>` and resolved from the OS secret store before being
129
+ * returned as default argument values. A command's defaults may only reference secrets stored for that
130
+ * command via `config set` (named `defaults_Z<command>_Z<option path>`, each part escaped). See {@link KeyValueServiceProvider} for the
127
131
  * generic key-value store which uses the same secret sentinel mechanism.
128
132
  *
129
133
  * *Environment Variables*
@@ -174,35 +178,15 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
174
178
  public readonly secretServiceEnabled: boolean;
175
179
  public readonly configFormat: ConfigFormat;
176
180
 
177
- // the location of the currently managed configuration data
178
- public configLocation: string | undefined;
179
-
180
- // the format specific handle for the text last read or written, used to preserve comments and layout on write
181
- #configHandle: unknown;
182
-
183
- // the configuration data to be used by the CLI runner implementation when setting command defaults.
184
- // SingleValueType is for GlobalCommandArgument values, Values is for
185
- // SubCommandArgument values.
186
- public defaultsData: Map<string, Values | SingleValueType> = new Map();
187
-
188
- // the underlying per-scope key-value data, read from/written to the configuration file. Exposed
189
- // (raw, not wrapped in a KeyValueService) only via getKeyValueData(), for KeyValueServiceProvider's
190
- // exclusive use.
191
- #commandKeyValueData = new Map<string, Map<string, ValueNode>>();
192
- #serviceKeyValueData = new Map<string, Map<string, ValueNode>>();
193
-
194
- // secret service used only to resolve secret sentinels embedded in default command argument
195
- // values (getDefaultArgumentValues()) - unrelated to per-command/per-service KV scoping, so it
196
- // uses its own fixed, dedicated scope prefix ("defaults"). Its scope is otherwise irrelevant,
197
- // since only getSecret() (which doesn't consult scope) is ever called on it.
198
- #defaultsSecretService: DefaultSecretService | undefined;
199
-
200
181
  /**
201
- * A {@link ConfigurationService} view of this provider, registered in the {@link Context} under
202
- * {@link CONFIGURATION_SERVICE_ID}. Its `configLocation` getter always reflects this provider's
203
- * current value.
182
+ * The {@link DefaultConfigurationService} which owns the configuration data. It is never registered
183
+ * in the {@link Context}: it is handed directly to the `config` group commands only.
204
184
  */
205
- readonly #configurationService: ConfigurationService;
185
+ readonly #configurationService: DefaultConfigurationService;
186
+
187
+ readonly #hasCommandRegistry: boolean;
188
+
189
+ readonly #secretsApi: SecretsApi | undefined;
206
190
 
207
191
  /**
208
192
  * Create an instance of the service provider with the specified details.
@@ -213,6 +197,9 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
213
197
  * @param secretServiceEnabled optionally enable OS-native secret storage via Bun.secrets for resolving
214
198
  * secrets embedded in default argument values: `configEnabled` must be true in this case
215
199
  * @param configFormat optionally set the syntax of the configuration file, defaults to JSON.
200
+ * @param commandRegistry optionally provide the registry of all commands, required for the `config:get`,
201
+ * `config:set` and `config:unset` commands to be provided.
202
+ * @param secretsApi optionally override the OS secret store used for storing SECRET default values.
216
203
  */
217
204
  public constructor(
218
205
  servicePriority: number,
@@ -220,6 +207,8 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
220
207
  configEnabled = false,
221
208
  secretServiceEnabled = false,
222
209
  configFormat: ConfigFormat = jsonFormat,
210
+ commandRegistry?: CommandRegistry,
211
+ secretsApi?: SecretsApi,
223
212
  ) {
224
213
  if (!configEnabled && secretServiceEnabled) {
225
214
  throw new Error("configEnabled must be true if secretServiceEnabled is true");
@@ -229,8 +218,14 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
229
218
  this.configEnabled = configEnabled;
230
219
  this.secretServiceEnabled = secretServiceEnabled;
231
220
  this.configFormat = configFormat;
232
-
233
- this.#configurationService = new ConfigurationServiceImpl(this);
221
+ this.#hasCommandRegistry = commandRegistry !== undefined;
222
+ this.#secretsApi = secretsApi;
223
+
224
+ this.#configurationService = new DefaultConfigurationService(
225
+ envVarsEnabled,
226
+ configFormat,
227
+ commandRegistry,
228
+ );
234
229
  }
235
230
 
236
231
  public getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo> {
@@ -238,22 +233,53 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
238
233
 
239
234
  if (this.configEnabled) {
240
235
  commands.push(new ConfigCommand(this.servicePriority));
241
- commands.push(
242
- new ConfigGroupCommand([new ConfigDumpSubCommand(() => this.getConfigString())]),
243
- );
236
+ const members: Array<SubCommand> = [new ConfigDumpSubCommand(this.#configurationService)];
237
+ if (this.#hasCommandRegistry) {
238
+ members.push(
239
+ new ConfigGetSubCommand(this.#configurationService),
240
+ new ConfigSetSubCommand(this.#configurationService),
241
+ new ConfigUnsetSubCommand(this.#configurationService),
242
+ );
243
+ }
244
+ commands.push(new ConfigGroupCommand(members));
244
245
  }
245
246
  if (this.secretServiceEnabled) {
246
- this.#defaultsSecretService = new DefaultSecretService(cliConfig.name, "defaults");
247
+ this.#configurationService.setDefaultsSecretService(
248
+ new DefaultSecretService(cliConfig.name, ["defaults"], this.#secretsApi),
249
+ );
247
250
  }
248
251
  return Promise.resolve({
249
- service: this.#configurationService,
252
+ service: new ConfigurationLocationService(this.#configurationService),
250
253
  // this may be empty if configEnabled is false
251
254
  commands,
252
255
  });
253
256
  }
254
257
 
258
+ /**
259
+ * The {@link DefaultConfigurationService} which owns the configuration data, for trusted wiring and tests.
260
+ */
261
+ public get configurationService(): DefaultConfigurationService {
262
+ return this.#configurationService;
263
+ }
264
+
265
+ public get configLocation(): string | undefined {
266
+ return this.#configurationService.configLocation;
267
+ }
268
+
269
+ public set configLocation(location: string | undefined) {
270
+ this.#configurationService.configLocation = location;
271
+ }
272
+
273
+ public get defaultsData(): Map<string, Values | SingleValueType> {
274
+ return this.#configurationService.defaultsData;
275
+ }
276
+
277
+ public set defaultsData(data: Map<string, Values | SingleValueType>) {
278
+ this.#configurationService.defaultsData = data;
279
+ }
280
+
255
281
  public setConfigLocation(configLocation: string) {
256
- this.configLocation = configLocation;
282
+ this.#configurationService.setConfigLocation(configLocation);
257
283
  }
258
284
 
259
285
  /**
@@ -308,19 +334,8 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
308
334
  }
309
335
  }
310
336
 
311
- if (result !== undefined && this.#defaultsSecretService) {
312
- result = await resolveSecrets<PopulatedValues | PopulatedValueType>(
313
- result,
314
- async (bunSecretName) => {
315
- const secretValue = await this.#defaultsSecretService!.getSecret(bunSecretName);
316
- if (secretValue === null) {
317
- throw new Error(
318
- `Secret not found in OS secret store for sentinel: '${SECRET_SENTINEL_PREFIX}${bunSecretName}'`,
319
- );
320
- }
321
- return secretValue;
322
- },
323
- );
337
+ if (result !== undefined) {
338
+ result = await this.#configurationService.resolveDefaultSecrets(result, commandKey);
324
339
  }
325
340
 
326
341
  return result;
@@ -337,12 +352,7 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
337
352
  scopeType: KeyValueServiceScopeType,
338
353
  scopeKey: string,
339
354
  ): Map<string, ValueNode> {
340
- const keyValueData =
341
- scopeType === "command" ? this.#commandKeyValueData : this.#serviceKeyValueData;
342
- if (!keyValueData.has(scopeKey)) {
343
- keyValueData.set(scopeKey, new Map());
344
- }
345
- return keyValueData.get(scopeKey)!;
355
+ return this.#configurationService.getKeyValueData(scopeType, scopeKey);
346
356
  }
347
357
 
348
358
  /**
@@ -352,160 +362,18 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
352
362
  *
353
363
  * @param isDirty whether any scoped KeyValueService has unwritten changes.
354
364
  */
355
- public async flushIfDirty(isDirty: boolean): Promise<void> {
356
- if (!isDirty) {
357
- return;
358
- }
359
- if (this.configLocation === undefined) {
360
- throw new Error("Attempt to write updated config with no configLocation set");
361
- }
362
- await this.#writeConfig(this.configLocation);
365
+ public flushIfDirty(isDirty: boolean): Promise<void> {
366
+ return this.#configurationService.flushIfDirty(isDirty);
363
367
  }
364
368
 
365
369
  public async initService(context: Context): Promise<void> {
366
370
  if (!this.configEnabled) {
367
371
  return;
368
372
  }
369
-
370
- let isDefault = false;
371
- if (this.configLocation === undefined) {
372
- isDefault = true;
373
- // default to `$HOME/.<application_name>.<format extension>`
374
- const home = process.env["HOME"];
375
- if (home) {
376
- this.configLocation = path.join(
377
- home,
378
- `.${context.cliConfig.name.replace(/\W/g, "")}.${this.configFormat.fileExtension}`,
379
- );
380
- }
381
- }
382
- if (this.configLocation === undefined) {
383
- return;
384
- }
385
- let fileInfo: Stats;
386
- try {
387
- fileInfo = await fs.lstat(this.configLocation);
388
- } catch (err) {
389
- const error = err as Error;
390
- if (isDefault) {
391
- logger.debug(
392
- "Default config file location: %s does not exist or is not visible - ignoring: %s",
393
- this.configLocation,
394
- error.message,
395
- );
396
- return;
397
- } else {
398
- throw new Error(
399
- `Config file location: '${this.configLocation}' doesn't exist or not visible: ${error.message}`,
400
- );
401
- }
402
- }
403
- if (fileInfo.isDirectory()) {
404
- throw new Error(
405
- `Config file location: '${this.configLocation}' is a directory and not a file!`,
406
- );
407
- }
408
-
409
- await this.#readConfig(this.configLocation);
410
- }
411
-
412
- async #readConfig(location: string): Promise<void> {
413
- try {
414
- logger.debug("Reading config from: %s", location);
415
-
416
- const data = await fs.readFile(location);
417
- const { data: config, handle } = this.configFormat.parse(data.toString());
418
- this.#configHandle = handle;
419
-
420
- this.defaultsData.clear();
421
- this.#commandKeyValueData.clear();
422
- this.#serviceKeyValueData.clear();
423
- if (config.defaults !== undefined) {
424
- const defaults = config.defaults;
425
- Object.keys(defaults).forEach((commandName) => {
426
- this.defaultsData.set(commandName, defaults[commandName] as Values);
427
- logger.debug("Set default argument values for command name: %s", commandName);
428
- });
429
- }
430
- if (config["key-values"] !== undefined) {
431
- const keyValues = config["key-values"];
432
- if (keyValues.commands !== undefined) {
433
- const commandKeyValues = keyValues.commands;
434
- Object.keys(commandKeyValues).forEach((commandName) => {
435
- this.#commandKeyValueData.set(
436
- commandName,
437
- new Map(Object.entries(commandKeyValues[commandName])),
438
- );
439
- logger.debug("Set default argument values for command name: %s", commandName);
440
- });
441
- }
442
- if (keyValues.services !== undefined) {
443
- const serviceKeyValues = keyValues.services;
444
- Object.keys(serviceKeyValues).forEach((serviceId) => {
445
- this.#serviceKeyValueData.set(
446
- serviceId,
447
- new Map(Object.entries(serviceKeyValues[serviceId])),
448
- );
449
- logger.debug("Set default argument values for service ID: %s", serviceId);
450
- });
451
- }
452
- }
453
- } catch (err) {
454
- throw new Error(`Failed to read config at location: '${this.configLocation}': ${err}`);
455
- }
456
- }
457
-
458
- async #writeConfig(location: string): Promise<void> {
459
- logger.debug("Writing config to: %s", location);
460
-
461
- try {
462
- const config = this.#getConfigData();
463
- const text =
464
- this.#configHandle === undefined
465
- ? this.configFormat.serialize(config)
466
- : this.configFormat.apply(this.#configHandle, config);
467
- await fs.writeFile(location, text);
468
- this.#configHandle = this.configFormat.parse(text).handle;
469
- } catch (err) {
470
- throw new Error(`Failed to write config at location: '${this.configLocation}': ${err}`);
471
- }
373
+ await this.#configurationService.init(context.cliConfig.name);
472
374
  }
473
375
 
474
376
  public getConfigString(): string {
475
- return this.configFormat.serialize(this.#getConfigData());
476
- }
477
-
478
- #getConfigData(): ConfigData {
479
- const config: {
480
- defaults?: Record<string, Values | SingleValueType>;
481
- "key-values"?: {
482
- commands?: Record<string, Record<string, ValueNode>>;
483
- services?: Record<string, Record<string, ValueNode>>;
484
- };
485
- } = {};
486
-
487
- if (this.defaultsData.size > 0) {
488
- config.defaults = Object.fromEntries(this.defaultsData);
489
- }
490
- if (this.#commandKeyValueData.size > 0 || this.#serviceKeyValueData.size > 0) {
491
- config["key-values"] = {};
492
- if (this.#commandKeyValueData.size > 0) {
493
- config["key-values"].commands = {};
494
- for (const [commandName, keyValueData] of this.#commandKeyValueData.entries()) {
495
- if (keyValueData.size > 0) {
496
- config["key-values"].commands[commandName] = Object.fromEntries(keyValueData);
497
- }
498
- }
499
- }
500
- if (this.#serviceKeyValueData.size > 0) {
501
- config["key-values"].services = {};
502
- for (const [serviceId, keyValueData] of this.#serviceKeyValueData.entries()) {
503
- if (keyValueData.size > 0) {
504
- config["key-values"].services[serviceId] = Object.fromEntries(keyValueData);
505
- }
506
- }
507
- }
508
- }
509
- return config;
377
+ return this.#configurationService.getConfigString();
510
378
  }
511
379
  }