settingregistry.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import Ajv from 'ajv';
  4. import * as json from 'comment-json';
  5. import {
  6. find
  7. } from '@phosphor/algorithm';
  8. import {
  9. JSONExt, JSONObject, JSONValue, ReadonlyJSONObject, Token
  10. } from '@phosphor/coreutils';
  11. import {
  12. IDisposable
  13. } from '@phosphor/disposable';
  14. import {
  15. ISignal, Signal
  16. } from '@phosphor/signaling';
  17. import {
  18. IDataConnector
  19. } from './interfaces';
  20. /**
  21. * The key in the schema for setting editor icon class hints.
  22. */
  23. export
  24. const ICON_CLASS_KEY ='jupyter.lab.setting-icon-class';
  25. /**
  26. * The key in the schema for setting editor icon label hints.
  27. */
  28. export
  29. const ICON_LABEL_KEY = 'jupyter.lab.setting-icon-label';
  30. /**
  31. * An alias for the JSON deep copy function.
  32. */
  33. const copy = JSONExt.deepCopy;
  34. /**
  35. * An implementation of a schema validator.
  36. */
  37. export
  38. interface ISchemaValidator {
  39. /**
  40. * Validate a plugin's schema and user data; populate the `composite` data.
  41. *
  42. * @param plugin - The plugin being validated. Its `composite` data will be
  43. * populated by reference.
  44. *
  45. * @param populate - Whether plugin data should be populated, defaults to
  46. * `true`.
  47. *
  48. * @return A list of errors if either the schema or data fail to validate or
  49. * `null` if there are no errors.
  50. */
  51. validateData(plugin: ISettingRegistry.IPlugin, populate?: boolean): ISchemaValidator.IError[] | null;
  52. }
  53. /**
  54. * A namespace for schema validator interfaces.
  55. */
  56. export
  57. namespace ISchemaValidator {
  58. /**
  59. * A schema validation error definition.
  60. */
  61. export
  62. interface IError {
  63. /**
  64. * The path in the data where the error occurred.
  65. */
  66. dataPath: string;
  67. /**
  68. * The keyword whose validation failed.
  69. */
  70. keyword: string;
  71. /**
  72. * The error message.
  73. */
  74. message: string;
  75. /**
  76. * Optional parameter metadata that might be included in an error.
  77. */
  78. params?: ReadonlyJSONObject;
  79. /**
  80. * The path in the schema where the error occurred.
  81. */
  82. schemaPath: string;
  83. }
  84. }
  85. /* tslint:disable */
  86. /**
  87. * The setting registry token.
  88. */
  89. export
  90. const ISettingRegistry = new Token<ISettingRegistry>('@jupyterlab/coreutils:ISettingRegistry');
  91. /* tslint:enable */
  92. /**
  93. * A namespace for setting registry interfaces.
  94. */
  95. export
  96. namespace ISettingRegistry {
  97. /**
  98. * The settings for a specific plugin.
  99. */
  100. export
  101. interface IPlugin extends JSONObject {
  102. /**
  103. * The name of the plugin.
  104. */
  105. id: string;
  106. /**
  107. * The collection of values for a specified setting.
  108. */
  109. data: ISettingBundle;
  110. /**
  111. * The raw user settings data as a string containing JSON with comments.
  112. */
  113. raw: string;
  114. /**
  115. * The JSON schema for the plugin.
  116. */
  117. schema: ISchema;
  118. }
  119. /**
  120. * A schema type that is a minimal subset of the formal JSON Schema along with
  121. * optional JupyterLab rendering hints.
  122. */
  123. export
  124. interface ISchema extends JSONObject {
  125. /**
  126. * The JupyterLab icon class hint for a plugin can be overridden by user
  127. * settings. It can also be root level and therefore "private".
  128. */
  129. 'jupyter.lab.setting-icon-class'?: string;
  130. /**
  131. * The JupyterLab icon label hint for a plugin can be overridden by user
  132. * settings. It can also be root level and therefore "private".
  133. */
  134. 'jupyter.lab.setting-icon-label'?: string;
  135. /**
  136. * The default value, if any.
  137. */
  138. default?: any;
  139. /**
  140. * The schema description.
  141. */
  142. description?: string;
  143. /**
  144. * The schema's child properties.
  145. */
  146. properties?: {
  147. /**
  148. * The JupyterLab icon class hint for a plugin can be overridden by user
  149. * settings. It can also be root level and therefore "private".
  150. */
  151. 'jupyter.lab.setting-icon-class'?: ISchema;
  152. /**
  153. * The JupyterLab icon label hint for a plugin can be overridden by user
  154. * settings. It can also be root level and therefore "private".
  155. */
  156. 'jupyter.lab.setting-icon-label'?: ISchema;
  157. /**
  158. * Arbitrary setting keys can be added.
  159. */
  160. [key: string]: ISchema;
  161. };
  162. /**
  163. * The title of the schema.
  164. */
  165. title?: string;
  166. /**
  167. * The type or types of the data.
  168. */
  169. type?: string | string[];
  170. }
  171. /**
  172. * The setting values for a plugin.
  173. */
  174. export
  175. interface ISettingBundle extends JSONObject {
  176. /**
  177. * A composite of the user setting values and the plugin schema defaults.
  178. *
  179. * #### Notes
  180. * The `composite` values will always be a superset of the `user` values.
  181. */
  182. composite: JSONObject;
  183. /**
  184. * The user setting values.
  185. */
  186. user: JSONObject;
  187. }
  188. /**
  189. * An interface for manipulating the settings of a specific plugin.
  190. */
  191. export
  192. interface ISettings extends IDisposable {
  193. /**
  194. * A signal that emits when the plugin's settings have changed.
  195. */
  196. readonly changed: ISignal<this, void>;
  197. /**
  198. * Get the composite of user settings and extension defaults.
  199. */
  200. readonly composite: JSONObject;
  201. /*
  202. * The plugin name.
  203. */
  204. readonly plugin: string;
  205. /**
  206. * The plugin settings raw text value.
  207. */
  208. readonly raw: string;
  209. /**
  210. * Get the plugin settings schema.
  211. */
  212. readonly schema: ISettingRegistry.ISchema;
  213. /**
  214. * Get the user settings.
  215. */
  216. readonly user: JSONObject;
  217. /**
  218. * Return the defaults in a commented JSON format.
  219. */
  220. annotatedDefaults(): string;
  221. /**
  222. * Calculate the default value of a setting by iterating through the schema.
  223. *
  224. * @param key - The name of the setting whose default value is calculated.
  225. *
  226. * @returns A calculated default JSON value for a specific setting.
  227. */
  228. default(key: string): JSONValue | undefined;
  229. /**
  230. * Get an individual setting.
  231. *
  232. * @param key - The name of the setting being retrieved.
  233. *
  234. * @returns The setting value.
  235. */
  236. get(key: string): { composite: JSONValue, user: JSONValue };
  237. /**
  238. * Remove a single setting.
  239. *
  240. * @param key - The name of the setting being removed.
  241. *
  242. * @returns A promise that resolves when the setting is removed.
  243. *
  244. * #### Notes
  245. * This function is asynchronous because it writes to the setting registry.
  246. */
  247. remove(key: string): Promise<void>;
  248. /**
  249. * Save all of the plugin's user settings at once.
  250. */
  251. save(raw: string): Promise<void>;
  252. /**
  253. * Set a single setting.
  254. *
  255. * @param key - The name of the setting being set.
  256. *
  257. * @param value - The value of the setting.
  258. *
  259. * @returns A promise that resolves when the setting has been saved.
  260. *
  261. * #### Notes
  262. * This function is asynchronous because it writes to the setting registry.
  263. */
  264. set(key: string, value: JSONValue): Promise<void>;
  265. /**
  266. * Validates raw settings with comments.
  267. *
  268. * @param raw - The JSON with comments string being validated.
  269. *
  270. * @returns A list of errors or `null` if valid.
  271. */
  272. validate(raw: string): ISchemaValidator.IError[] | null;
  273. }
  274. }
  275. /**
  276. * An implementation of a setting registry.
  277. */
  278. export
  279. interface ISettingRegistry extends SettingRegistry {}
  280. /**
  281. * The default implementation of a schema validator.
  282. */
  283. export
  284. class DefaultSchemaValidator implements ISchemaValidator {
  285. /**
  286. * Instantiate a schema validator.
  287. */
  288. constructor() {
  289. this._composer.addSchema(Private.SCHEMA, 'main');
  290. this._validator.addSchema(Private.SCHEMA, 'main');
  291. }
  292. /**
  293. * Validate a plugin's schema and user data; populate the `composite` data.
  294. *
  295. * @param plugin - The plugin being validated. Its `composite` data will be
  296. * populated by reference.
  297. *
  298. * @param populate - Whether plugin data should be populated, defaults to
  299. * `true`.
  300. *
  301. * @return A list of errors if either the schema or data fail to validate or
  302. * `null` if there are no errors.
  303. */
  304. validateData(plugin: ISettingRegistry.IPlugin, populate = true): ISchemaValidator.IError[] | null {
  305. const validate = this._validator.getSchema(plugin.id);
  306. const compose = this._composer.getSchema(plugin.id);
  307. // If the schemas do not exist, add them to the validator and continue.
  308. if (!validate || !compose) {
  309. const errors = this._addSchema(plugin.id, plugin.schema);
  310. if (errors) {
  311. return errors;
  312. }
  313. return this.validateData(plugin);
  314. }
  315. // Parse the raw commented JSON into a user map.
  316. let user: JSONObject;
  317. try {
  318. const strip = true;
  319. user = json.parse(plugin.raw, null, strip) as JSONObject;
  320. } catch (error) {
  321. if (error instanceof SyntaxError) {
  322. return [{
  323. dataPath: '', keyword: 'syntax', schemaPath: '',
  324. message: error.message
  325. }];
  326. }
  327. const { column, description } = error;
  328. const line = error.lineNumber;
  329. return [{
  330. dataPath: '', keyword: 'parse', schemaPath: '',
  331. message: `${description} (line ${line} column ${column})`
  332. }];
  333. }
  334. if (!validate(user)) {
  335. return validate.errors as ISchemaValidator.IError[];
  336. }
  337. // Copy the user data before merging defaults into composite map.
  338. const composite = copy(user);
  339. if (!compose(composite)) {
  340. return compose.errors as ISchemaValidator.IError[];
  341. }
  342. if (populate) {
  343. plugin.data = { composite, user };
  344. }
  345. return null;
  346. }
  347. /**
  348. * Add a schema to the validator.
  349. *
  350. * @param plugin - The plugin ID.
  351. *
  352. * @param schema - The schema being added.
  353. *
  354. * @return A list of errors if the schema fails to validate or `null` if there
  355. * are no errors.
  356. *
  357. * #### Notes
  358. * It is safe to call this function multiple times with the same plugin name.
  359. */
  360. private _addSchema(plugin: string, schema: ISettingRegistry.ISchema): ISchemaValidator.IError[] | null {
  361. const composer = this._composer;
  362. const validator = this._validator;
  363. const validate = validator.getSchema('main');
  364. // Validate against the main schema.
  365. if (!(validate(schema) as boolean)) {
  366. return validate.errors as ISchemaValidator.IError[];
  367. }
  368. // Validate against the JSON schema meta-schema.
  369. if (!(validator.validateSchema(schema) as boolean)) {
  370. return validator.errors as ISchemaValidator.IError[];
  371. }
  372. // Remove if schema already exists.
  373. composer.removeSchema(plugin);
  374. validator.removeSchema(plugin);
  375. // Add schema to the validator and composer.
  376. composer.addSchema(schema, plugin);
  377. validator.addSchema(schema, plugin);
  378. return null;
  379. }
  380. private _composer = new Ajv({ useDefaults: true });
  381. private _validator = new Ajv();
  382. }
  383. /**
  384. * The default concrete implementation of a setting registry.
  385. */
  386. export
  387. class SettingRegistry {
  388. /**
  389. * Create a new setting registry.
  390. */
  391. constructor(options: SettingRegistry.IOptions) {
  392. this._connector = options.connector;
  393. this.validator = options.validator || new DefaultSchemaValidator();
  394. }
  395. /**
  396. * The schema of the setting registry.
  397. */
  398. readonly schema = Private.SCHEMA;
  399. /**
  400. * The schema validator used by the setting registry.
  401. */
  402. readonly validator: ISchemaValidator;
  403. /**
  404. * A signal that emits the name of a plugin when its settings change.
  405. */
  406. get pluginChanged(): ISignal<this, string> {
  407. return this._pluginChanged;
  408. }
  409. /**
  410. * Returns a list of plugin settings held in the registry.
  411. */
  412. get plugins(): ISettingRegistry.IPlugin[] {
  413. const plugins = this._plugins;
  414. return Object.keys(plugins)
  415. .map(p => copy(plugins[p]) as ISettingRegistry.IPlugin);
  416. }
  417. /**
  418. * Get an individual setting.
  419. *
  420. * @param plugin - The name of the plugin whose settings are being retrieved.
  421. *
  422. * @param key - The name of the setting being retrieved.
  423. *
  424. * @returns A promise that resolves when the setting is retrieved.
  425. */
  426. get(plugin: string, key: string): Promise<{ composite: JSONValue, user: JSONValue }> {
  427. const plugins = this._plugins;
  428. if (plugin in plugins) {
  429. const { composite, user } = plugins[plugin].data;
  430. const result = {
  431. composite: key in composite ? copy(composite[key]) : undefined,
  432. user: key in user ? copy(user[key]) : undefined
  433. };
  434. return Promise.resolve(result);
  435. }
  436. return this.load(plugin).then(() => this.get(plugin, key));
  437. }
  438. /**
  439. * Load a plugin's settings into the setting registry.
  440. *
  441. * @param plugin - The name of the plugin whose settings are being loaded.
  442. *
  443. * @returns A promise that resolves with a plugin settings object or rejects
  444. * if the plugin is not found.
  445. */
  446. load(plugin: string): Promise<ISettingRegistry.ISettings> {
  447. const plugins = this._plugins;
  448. const registry = this;
  449. // If the plugin exists, resolve.
  450. if (plugin in plugins) {
  451. const settings = new Settings({ plugin: plugins[plugin], registry });
  452. return Promise.resolve(settings);
  453. }
  454. // If the plugin needs to be loaded from the data connector, fetch.
  455. return this.reload(plugin);
  456. }
  457. /**
  458. * Reload a plugin's settings into the registry even if they already exist.
  459. *
  460. * @param plugin - The name of the plugin whose settings are being reloaded.
  461. *
  462. * @returns A promise that resolves with a plugin settings object or rejects
  463. * with a list of `ISchemaValidator.IError` objects if it fails.
  464. */
  465. reload(plugin: string): Promise<ISettingRegistry.ISettings> {
  466. const connector = this._connector;
  467. const plugins = this._plugins;
  468. const registry = this;
  469. // If the plugin needs to be loaded from the connector, fetch.
  470. return connector.fetch(plugin).then(data => {
  471. // Validate the response from the connector; populate `composite` field.
  472. try {
  473. this._validate(data);
  474. } catch (errors) {
  475. const output = [`Validating ${plugin} failed:`];
  476. (errors as ISchemaValidator.IError[]).forEach((error, index) => {
  477. const { dataPath, schemaPath, keyword, message } = error;
  478. output.push(`${index} - schema @ ${schemaPath}, data @ ${dataPath}`);
  479. output.push(`\t${keyword} ${message}`);
  480. });
  481. console.error(output.join('\n'));
  482. throw new Error(`Failed validating ${plugin}`);
  483. }
  484. // Emit that a plugin has changed.
  485. this._pluginChanged.emit(plugin);
  486. return new Settings({ plugin: plugins[plugin], registry });
  487. });
  488. }
  489. /**
  490. * Remove a single setting in the registry.
  491. *
  492. * @param plugin - The name of the plugin whose setting is being removed.
  493. *
  494. * @param key - The name of the setting being removed.
  495. *
  496. * @returns A promise that resolves when the setting is removed.
  497. */
  498. remove(plugin: string, key: string): Promise<void> {
  499. const plugins = this._plugins;
  500. if (!(plugin in plugins)) {
  501. return Promise.resolve(undefined);
  502. }
  503. const raw = json.parse(plugins[plugin].raw);
  504. // Delete both the value and any associated comment.
  505. delete raw[key];
  506. delete raw[`// ${key}`];
  507. plugins[plugin].raw = json.stringify(raw);
  508. return this._save(plugin);
  509. }
  510. /**
  511. * Set a single setting in the registry.
  512. *
  513. * @param plugin - The name of the plugin whose setting is being set.
  514. *
  515. * @param key - The name of the setting being set.
  516. *
  517. * @param value - The value of the setting being set.
  518. *
  519. * @returns A promise that resolves when the setting has been saved.
  520. *
  521. */
  522. set(plugin: string, key: string, value: JSONValue): Promise<void> {
  523. const plugins = this._plugins;
  524. if (!(plugin in plugins)) {
  525. return this.load(plugin).then(() => this.set(plugin, key, value));
  526. }
  527. const raw = json.parse(plugins[plugin].raw);
  528. plugins[plugin].raw = json.stringify({ ...raw, [key]: value });
  529. return this._save(plugin);
  530. }
  531. /**
  532. * Upload a plugin's settings.
  533. *
  534. * @param plugin - The name of the plugin whose settings are being set.
  535. *
  536. * @param raw - The raw plugin settings being uploaded.
  537. *
  538. * @returns A promise that resolves when the settings have been saved.
  539. */
  540. upload(plugin: string, raw: string): Promise<void> {
  541. const plugins = this._plugins;
  542. if (!(plugin in plugins)) {
  543. return this.load(plugin).then(() => this.upload(plugin, raw));
  544. }
  545. // Set the local copy.
  546. plugins[plugin].raw = raw;
  547. return this._save(plugin);
  548. }
  549. /**
  550. * Save a plugin in the registry.
  551. */
  552. private _save(plugin: string): Promise<void> {
  553. const plugins = this._plugins;
  554. if (!(plugin in plugins)) {
  555. const message = `${plugin} does not exist in setting registry.`;
  556. return Promise.reject(new Error(message));
  557. }
  558. try {
  559. this._validate(plugins[plugin]);
  560. } catch (errors) {
  561. const message = `${plugin} failed to validate; check console for errors.`;
  562. console.warn(`${plugin} validation errors:`, errors);
  563. return Promise.reject(new Error(message));
  564. }
  565. return this._connector.save(plugin, plugins[plugin].raw)
  566. .then(() => { this._pluginChanged.emit(plugin); });
  567. }
  568. /**
  569. * Validate a plugin's data and schema, compose the `composite` data.
  570. */
  571. private _validate(plugin: ISettingRegistry.IPlugin): void {
  572. // Validate the user data and create the composite data.
  573. const errors = this.validator.validateData(plugin);
  574. if (errors) {
  575. throw errors;
  576. }
  577. // Set the local copy.
  578. this._plugins[plugin.id] = plugin;
  579. }
  580. private _connector: IDataConnector<ISettingRegistry.IPlugin, string>;
  581. private _pluginChanged = new Signal<this, string>(this);
  582. private _plugins: { [name: string]: ISettingRegistry.IPlugin } = Object.create(null);
  583. }
  584. /**
  585. * A manager for a specific plugin's settings.
  586. */
  587. export
  588. class Settings implements ISettingRegistry.ISettings {
  589. /**
  590. * Instantiate a new plugin settings manager.
  591. */
  592. constructor(options: Settings.IOptions) {
  593. const { plugin } = options;
  594. this.plugin = plugin.id;
  595. this.registry = options.registry;
  596. this._composite = plugin.data.composite || { };
  597. this._raw = plugin.raw || '{ }';
  598. this._schema = plugin.schema || { type: 'object' };
  599. this._user = plugin.data.user || { };
  600. this.registry.pluginChanged.connect(this._onPluginChanged, this);
  601. }
  602. /**
  603. * A signal that emits when the plugin's settings have changed.
  604. */
  605. get changed(): ISignal<this, void> {
  606. return this._changed;
  607. }
  608. /**
  609. * Get the composite of user settings and extension defaults.
  610. */
  611. get composite(): JSONObject {
  612. return this._composite;
  613. }
  614. /**
  615. * Test whether the plugin settings manager disposed.
  616. */
  617. get isDisposed(): boolean {
  618. return this._isDisposed;
  619. }
  620. /**
  621. * Get the plugin settings schema.
  622. */
  623. get schema(): ISettingRegistry.ISchema {
  624. return this._schema;
  625. }
  626. /**
  627. * Get the plugin settings raw text value.
  628. */
  629. get raw(): string {
  630. return this._raw;
  631. }
  632. /**
  633. * Get the user settings.
  634. */
  635. get user(): JSONObject {
  636. return this._user;
  637. }
  638. /*
  639. * The plugin name.
  640. */
  641. readonly plugin: string;
  642. /**
  643. * The system registry instance used by the settings manager.
  644. */
  645. readonly registry: SettingRegistry;
  646. /**
  647. * Return the defaults in a commented JSON format.
  648. */
  649. annotatedDefaults(): string {
  650. return Private.annotatedDefaults(this._schema, this.plugin);
  651. }
  652. /**
  653. * Calculate the default value of a setting by iterating through the schema.
  654. *
  655. * @param key - The name of the setting whose default value is calculated.
  656. *
  657. * @returns A calculated default JSON value for a specific setting.
  658. */
  659. default(key: string): JSONValue | undefined {
  660. return Private.reifyDefault(this.schema, key);
  661. }
  662. /**
  663. * Dispose of the plugin settings resources.
  664. */
  665. dispose(): void {
  666. if (this._isDisposed) {
  667. return;
  668. }
  669. this._isDisposed = true;
  670. Signal.clearData(this);
  671. }
  672. /**
  673. * Get an individual setting.
  674. *
  675. * @param key - The name of the setting being retrieved.
  676. *
  677. * @returns The setting value.
  678. *
  679. * #### Notes
  680. * This method returns synchronously because it uses a cached copy of the
  681. * plugin settings that is synchronized with the registry.
  682. */
  683. get(key: string): { composite: JSONValue, user: JSONValue } {
  684. const { composite, user } = this;
  685. return {
  686. composite: key in composite ? copy(composite[key]) : undefined,
  687. user: key in user ? copy(user[key]) : undefined
  688. };
  689. }
  690. /**
  691. * Remove a single setting.
  692. *
  693. * @param key - The name of the setting being removed.
  694. *
  695. * @returns A promise that resolves when the setting is removed.
  696. *
  697. * #### Notes
  698. * This function is asynchronous because it writes to the setting registry.
  699. */
  700. remove(key: string): Promise<void> {
  701. return this.registry.remove(this.plugin, key);
  702. }
  703. /**
  704. * Save all of the plugin's user settings at once.
  705. */
  706. save(raw: string): Promise<void> {
  707. return this.registry.upload(this.plugin, raw);
  708. }
  709. /**
  710. * Set a single setting.
  711. *
  712. * @param key - The name of the setting being set.
  713. *
  714. * @param value - The value of the setting.
  715. *
  716. * @returns A promise that resolves when the setting has been saved.
  717. *
  718. * #### Notes
  719. * This function is asynchronous because it writes to the setting registry.
  720. */
  721. set(key: string, value: JSONValue): Promise<void> {
  722. return this.registry.set(this.plugin, key, value);
  723. }
  724. /**
  725. * Validates raw settings with comments.
  726. *
  727. * @param raw - The JSON with comments string being validated.
  728. *
  729. * @returns A list of errors or `null` if valid.
  730. */
  731. validate(raw: string): ISchemaValidator.IError[] | null {
  732. const data = { composite: { }, user: { } };
  733. const id = this.plugin;
  734. const schema = this._schema;
  735. const validator = this.registry.validator;
  736. return validator.validateData({ data, id, raw, schema }, false);
  737. }
  738. /**
  739. * Handle plugin changes in the setting registry.
  740. */
  741. private _onPluginChanged(sender: any, plugin: string): void {
  742. if (plugin === this.plugin) {
  743. const found = find(this.registry.plugins, p => p.id === plugin);
  744. if (!found) {
  745. return;
  746. }
  747. const { composite, user } = found.data;
  748. const { raw, schema } = found;
  749. this._composite = composite || { };
  750. this._raw = raw;
  751. this._schema = schema || { type: 'object' };
  752. this._user = user || { };
  753. this._changed.emit(undefined);
  754. }
  755. }
  756. private _changed = new Signal<this, void>(this);
  757. private _composite: JSONObject = Object.create(null);
  758. private _isDisposed = false;
  759. private _raw = '{ }';
  760. private _schema: ISettingRegistry.ISchema = Object.create(null);
  761. private _user: JSONObject = Object.create(null);
  762. }
  763. /**
  764. * A namespace for `SettingRegistry` statics.
  765. */
  766. export
  767. namespace SettingRegistry {
  768. /**
  769. * The instantiation options for a setting registry
  770. */
  771. export
  772. interface IOptions {
  773. /**
  774. * The data connector used by the setting registry.
  775. */
  776. connector: IDataConnector<ISettingRegistry.IPlugin, string>;
  777. /**
  778. * The validator used to enforce the settings JSON schema.
  779. */
  780. validator?: ISchemaValidator;
  781. }
  782. }
  783. /**
  784. * A namespace for `Settings` statics.
  785. */
  786. export
  787. namespace Settings {
  788. /**
  789. * The instantiation options for a `Settings` object.
  790. */
  791. export
  792. interface IOptions {
  793. /**
  794. * The setting values for a plugin.
  795. */
  796. plugin: ISettingRegistry.IPlugin;
  797. /**
  798. * The system registry instance used by the settings manager.
  799. */
  800. registry: SettingRegistry;
  801. }
  802. }
  803. /**
  804. * A namespace for private module data.
  805. */
  806. export
  807. namespace Private {
  808. /* tslint:disable */
  809. /**
  810. * The schema for settings.
  811. */
  812. export
  813. const SCHEMA: ISettingRegistry.ISchema = {
  814. "$schema": "http://json-schema.org/draft-06/schema",
  815. "title": "Jupyter Settings/Preferences Schema",
  816. "description": "Jupyter settings/preferences schema v0.1.0",
  817. "type": "object",
  818. "additionalProperties": true,
  819. "properties": {
  820. [ICON_CLASS_KEY]: { "type": "string", "default": "jp-FileIcon" },
  821. [ICON_LABEL_KEY]: { "type": "string", "default": "Plugin" }
  822. }
  823. };
  824. /* tslint:enable */
  825. /**
  826. * The default indentation level, uses spaces instead of tabs.
  827. */
  828. const indent = ' ';
  829. /**
  830. * Replacement text for schema properties missing a `description` field.
  831. */
  832. const nondescript = '[missing schema description]';
  833. /**
  834. * Replacement text for schema properties missing a `default` field.
  835. */
  836. const undefaulted = '[missing schema default]';
  837. /**
  838. * Replacement text for schema properties missing a `title` field.
  839. */
  840. const untitled = '[missing schema title]';
  841. /**
  842. * Returns an annotated (JSON with comments) version of a schema's defaults.
  843. */
  844. export
  845. function annotatedDefaults(schema: ISettingRegistry.ISchema, plugin: string): string {
  846. const { description, properties, title } = schema;
  847. const keys = Object.keys(properties).sort((a, b) => a.localeCompare(b));
  848. const length = Math.max((description || nondescript).length, plugin.length);
  849. return [
  850. '{',
  851. prefix(`${title || untitled}`),
  852. prefix(plugin),
  853. prefix(description || nondescript),
  854. prefix(line(length)),
  855. '',
  856. keys.map(key => docstring(schema, key)).join(',\n\n'),
  857. '}'
  858. ].join('\n');
  859. }
  860. /**
  861. * Returns a documentation string for a specific schema property.
  862. */
  863. function docstring(schema: ISettingRegistry.ISchema, key: string): string {
  864. const { description, title } = schema.properties[key];
  865. const reified = reifyDefault(schema, key);
  866. const defaults = reified === undefined ? prefix(`"${key}": ${undefaulted}`)
  867. : prefix(`"${key}": ${JSON.stringify(reified, null, 2)}`, indent);
  868. return [
  869. prefix(`${title || untitled}`),
  870. prefix(description || nondescript),
  871. defaults
  872. ].join('\n');
  873. }
  874. /**
  875. * Returns a line of a specified length.
  876. */
  877. function line(length: number, ch = '*'): string {
  878. return (new Array(length + 1)).join(ch);
  879. }
  880. /**
  881. * Returns a documentation string with a comment prefix added on every line.
  882. */
  883. function prefix(source: string, pre = `${indent}\/\/ `): string {
  884. return pre + source.split('\n').join(`\n${pre}`);
  885. }
  886. /**
  887. * Create a fully extrapolated default value for a root key in a schema.
  888. */
  889. export
  890. function reifyDefault(schema: ISettingRegistry.ISchema, root?: string): JSONValue | undefined {
  891. // If the property is at the root level, traverse its schema.
  892. schema = (root ? schema.properties[root] : schema) || { };
  893. // If the property has no default or is a primitive, return.
  894. if (!('default' in schema) || schema.type !== 'object') {
  895. return schema.default;
  896. }
  897. // Make a copy of the default value to populate.
  898. const result = JSONExt.deepCopy(schema.default);
  899. // Iterate through and populate each child property.
  900. for (let property in schema.properties || { }) {
  901. result[property] = reifyDefault(schema.properties[property]);
  902. }
  903. return result;
  904. }
  905. }