settingregistry.ts 25 KB

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