1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741 |
- const _ = require('underscore-plus');
- const { Emitter } = require('event-kit');
- const {
- getValueAtKeyPath,
- setValueAtKeyPath,
- deleteValueAtKeyPath,
- pushKeyPath,
- splitKeyPath
- } = require('key-path-helpers');
- const Color = require('./color');
- const ScopedPropertyStore = require('scoped-property-store');
- const ScopeDescriptor = require('./scope-descriptor');
- const schemaEnforcers = {};
- // Essential: Used to access all of Pulsar's configuration details.
- //
- // An instance of this class is always available as the `atom.config` global.
- //
- // ## Getting and setting config settings.
- //
- // ```coffee
- // # Note that with no value set, ::get returns the setting's default value.
- // atom.config.get('my-package.myKey') # -> 'defaultValue'
- //
- // atom.config.set('my-package.myKey', 'value')
- // atom.config.get('my-package.myKey') # -> 'value'
- // ```
- //
- // You may want to watch for changes. Use {::observe} to catch changes to the setting.
- //
- // ```coffee
- // atom.config.set('my-package.myKey', 'value')
- // atom.config.observe 'my-package.myKey', (newValue) ->
- // # `observe` calls immediately and every time the value is changed
- // console.log 'My configuration changed:', newValue
- // ```
- //
- // If you want a notification only when the value changes, use {::onDidChange}.
- //
- // ```coffee
- // atom.config.onDidChange 'my-package.myKey', ({newValue, oldValue}) ->
- // console.log 'My configuration changed:', newValue, oldValue
- // ```
- //
- // ### Value Coercion
- //
- // Config settings each have a type specified by way of a
- // [schema](json-schema.org). For example we might want an integer setting that only
- // allows integers greater than `0`:
- //
- // ```coffee
- // # When no value has been set, `::get` returns the setting's default value
- // atom.config.get('my-package.anInt') # -> 12
- //
- // # The string will be coerced to the integer 123
- // atom.config.set('my-package.anInt', '123')
- // atom.config.get('my-package.anInt') # -> 123
- //
- // # The string will be coerced to an integer, but it must be greater than 0, so is set to 1
- // atom.config.set('my-package.anInt', '-20')
- // atom.config.get('my-package.anInt') # -> 1
- // ```
- //
- // ## Defining settings for your package
- //
- // Define a schema under a `config` key in your package main.
- //
- // ```coffee
- // module.exports =
- // # Your config schema
- // config:
- // someInt:
- // type: 'integer'
- // default: 23
- // minimum: 1
- //
- // activate: (state) -> # ...
- // # ...
- // ```
- //
- // See [package docs](https://pulsar-edit.dev/docs/launch-manual/sections/core-hacking/#package-word-count) for
- // more info.
- //
- // ## Config Schemas
- //
- // We use [json schema](http://json-schema.org) which allows you to define your value's
- // default, the type it should be, etc. A simple example:
- //
- // ```coffee
- // # We want to provide an `enableThing`, and a `thingVolume`
- // config:
- // enableThing:
- // type: 'boolean'
- // default: false
- // thingVolume:
- // type: 'integer'
- // default: 5
- // minimum: 1
- // maximum: 11
- // ```
- //
- // The type keyword allows for type coercion and validation. If a `thingVolume` is
- // set to a string `'10'`, it will be coerced into an integer.
- //
- // ```coffee
- // atom.config.set('my-package.thingVolume', '10')
- // atom.config.get('my-package.thingVolume') # -> 10
- //
- // # It respects the min / max
- // atom.config.set('my-package.thingVolume', '400')
- // atom.config.get('my-package.thingVolume') # -> 11
- //
- // # If it cannot be coerced, the value will not be set
- // atom.config.set('my-package.thingVolume', 'cats')
- // atom.config.get('my-package.thingVolume') # -> 11
- // ```
- //
- // ### Supported Types
- //
- // The `type` keyword can be a string with any one of the following. You can also
- // chain them by specifying multiple in an an array. For example
- //
- // ```coffee
- // config:
- // someSetting:
- // type: ['boolean', 'integer']
- // default: 5
- //
- // # Then
- // atom.config.set('my-package.someSetting', 'true')
- // atom.config.get('my-package.someSetting') # -> true
- //
- // atom.config.set('my-package.someSetting', '12')
- // atom.config.get('my-package.someSetting') # -> 12
- // ```
- //
- // #### string
- //
- // Values must be a string.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'string'
- // default: 'hello'
- // ```
- //
- // #### integer
- //
- // Values will be coerced into integer. Supports the (optional) `minimum` and
- // `maximum` keys.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'integer'
- // default: 5
- // minimum: 1
- // maximum: 11
- // ```
- //
- // #### number
- //
- // Values will be coerced into a number, including real numbers. Supports the
- // (optional) `minimum` and `maximum` keys.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'number'
- // default: 5.3
- // minimum: 1.5
- // maximum: 11.5
- // ```
- //
- // #### boolean
- //
- // Values will be coerced into a Boolean. `'true'` and `'false'` will be coerced into
- // a boolean. Numbers, arrays, objects, and anything else will not be coerced.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'boolean'
- // default: false
- // ```
- //
- // #### array
- //
- // Value must be an Array. The types of the values can be specified by a
- // subschema in the `items` key.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'array'
- // default: [1, 2, 3]
- // items:
- // type: 'integer'
- // minimum: 1.5
- // maximum: 11.5
- // ```
- //
- // #### color
- //
- // Values will be coerced into a {Color} with `red`, `green`, `blue`, and `alpha`
- // properties that all have numeric values. `red`, `green`, `blue` will be in
- // the range 0 to 255 and `value` will be in the range 0 to 1. Values can be any
- // valid CSS color format such as `#abc`, `#abcdef`, `white`,
- // `rgb(50, 100, 150)`, and `rgba(25, 75, 125, .75)`.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'color'
- // default: 'white'
- // ```
- //
- // #### object / Grouping other types
- //
- // A config setting with the type `object` allows grouping a set of config
- // settings. The group will be visually separated and has its own group headline.
- // The sub options must be listed under a `properties` key.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'object'
- // properties:
- // myChildIntOption:
- // type: 'integer'
- // minimum: 1.5
- // maximum: 11.5
- // ```
- //
- // ### Other Supported Keys
- //
- // #### enum
- //
- // All types support an `enum` key, which lets you specify all the values the
- // setting can take. `enum` may be an array of allowed values (of the specified
- // type), or an array of objects with `value` and `description` properties, where
- // the `value` is an allowed value, and the `description` is a descriptive string
- // used in the settings view.
- //
- // In this example, the setting must be one of the 4 integers:
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'integer'
- // default: 4
- // enum: [2, 4, 6, 8]
- // ```
- //
- // In this example, the setting must be either 'foo' or 'bar', which are
- // presented using the provided descriptions in the settings pane:
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'string'
- // default: 'foo'
- // enum: [
- // {value: 'foo', description: 'Foo mode. You want this.'}
- // {value: 'bar', description: 'Bar mode. Nobody wants that!'}
- // ]
- // ```
- //
- // If you only have a few elements, you can display your enum as a list of
- // radio buttons in the settings view rather than a select list. To do so,
- // specify `radio: true` as a sibling property to the `enum` array.
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'string'
- // default: 'foo'
- // enum: [
- // {value: 'foo', description: 'Foo mode. You want this.'}
- // {value: 'bar', description: 'Bar mode. Nobody wants that!'}
- // ]
- // radio: true
- // ```
- //
- // Usage:
- //
- // ```coffee
- // atom.config.set('my-package.someSetting', '2')
- // atom.config.get('my-package.someSetting') # -> 2
- //
- // # will not set values outside of the enum values
- // atom.config.set('my-package.someSetting', '3')
- // atom.config.get('my-package.someSetting') # -> 2
- //
- // # If it cannot be coerced, the value will not be set
- // atom.config.set('my-package.someSetting', '4')
- // atom.config.get('my-package.someSetting') # -> 4
- // ```
- //
- // #### title and description
- //
- // The settings view will use the `title` and `description` keys to display your
- // config setting in a readable way. By default the settings view humanizes your
- // config key, so `someSetting` becomes `Some Setting`. In some cases, this is
- // confusing for users, and a more descriptive title is useful.
- //
- // Descriptions will be displayed below the title in the settings view.
- //
- // For a group of config settings the humanized key or the title and the
- // description are used for the group headline.
- //
- // ```coffee
- // config:
- // someSetting:
- // title: 'Setting Magnitude'
- // description: 'This will affect the blah and the other blah'
- // type: 'integer'
- // default: 4
- // ```
- //
- // __Note__: You should strive to be so clear in your naming of the setting that
- // you do not need to specify a title or description!
- //
- // Descriptions allow a subset of
- // [Markdown formatting](https://help.github.com/articles/github-flavored-markdown/).
- // Specifically, you may use the following in configuration setting descriptions:
- //
- // * **bold** - `**bold**`
- // * *italics* - `*italics*`
- // * [links](https://pulsar-edit.dev) - `[links](https://pulsar-edit.dev)`
- // * `code spans` - `` `code spans` ``
- // * line breaks - `line breaks<br/>`
- // * ~~strikethrough~~ - `~~strikethrough~~`
- //
- // #### order
- //
- // The settings view orders your settings alphabetically. You can override this
- // ordering with the order key.
- //
- // ```coffee
- // config:
- // zSetting:
- // type: 'integer'
- // default: 4
- // order: 1
- // aSetting:
- // type: 'integer'
- // default: 4
- // order: 2
- // ```
- //
- // ## Manipulating values outside your configuration schema
- //
- // It is possible to manipulate(`get`, `set`, `observe` etc) values that do not
- // appear in your configuration schema. For example, if the config schema of the
- // package 'some-package' is
- //
- // ```coffee
- // config:
- // someSetting:
- // type: 'boolean'
- // default: false
- // ```
- //
- // You can still do the following
- //
- // ```coffee
- // let otherSetting = atom.config.get('some-package.otherSetting')
- // atom.config.set('some-package.stillAnotherSetting', otherSetting * 5)
- // ```
- //
- // In other words, if a function asks for a `key-path`, that path doesn't have to
- // be described in the config schema for the package or any package. However, as
- // highlighted in the best practices section, you are advised against doing the
- // above.
- //
- // ## Best practices
- //
- // * Don't depend on (or write to) configuration keys outside of your keypath.
- //
- class Config {
- static addSchemaEnforcer(typeName, enforcerFunction) {
- if (schemaEnforcers[typeName] == null) {
- schemaEnforcers[typeName] = [];
- }
- return schemaEnforcers[typeName].push(enforcerFunction);
- }
- static addSchemaEnforcers(filters) {
- for (let typeName in filters) {
- const functions = filters[typeName];
- for (let name in functions) {
- const enforcerFunction = functions[name];
- this.addSchemaEnforcer(typeName, enforcerFunction);
- }
- }
- }
- static executeSchemaEnforcers(keyPath, value, schema) {
- let error = null;
- let types = schema.type;
- if (!Array.isArray(types)) {
- types = [types];
- }
- for (let type of types) {
- try {
- const enforcerFunctions = schemaEnforcers[type].concat(
- schemaEnforcers['*']
- );
- for (let enforcer of enforcerFunctions) {
- // At some point in one's life, one must call upon an enforcer.
- value = enforcer.call(this, keyPath, value, schema);
- }
- error = null;
- break;
- } catch (e) {
- error = e;
- }
- }
- if (error != null) {
- throw error;
- }
- return value;
- }
- // Created during initialization, available as `atom.config`
- constructor(params = {}) {
- this.clear();
- this.initialize(params);
- }
- initialize({ saveCallback, mainSource, projectHomeSchema }) {
- if (saveCallback) {
- this.saveCallback = saveCallback;
- }
- if (mainSource) this.mainSource = mainSource;
- if (projectHomeSchema) {
- this.schema.properties.core.properties.projectHome = projectHomeSchema;
- this.defaultSettings.core.projectHome = projectHomeSchema.default;
- }
- }
- clear() {
- this.emitter = new Emitter();
- this.schema = {
- type: 'object',
- properties: {}
- };
- this.defaultSettings = {};
- this.settings = {};
- this.projectSettings = {};
- this.projectFile = null;
- this.scopedSettingsStore = new ScopedPropertyStore();
- this.settingsLoaded = false;
- this.transactDepth = 0;
- this.pendingOperations = [];
- this.legacyScopeAliases = new Map();
- this.requestSave = _.debounce(() => this.save(), 1);
- }
- /*
- Section: Config Subscription
- */
- // Essential: Add a listener for changes to a given key path. This is different
- // than {::onDidChange} in that it will immediately call your callback with the
- // current value of the config entry.
- //
- // ### Examples
- //
- // You might want to be notified when the themes change. We'll watch
- // `core.themes` for changes
- //
- // ```coffee
- // atom.config.observe 'core.themes', (value) ->
- // # do stuff with value
- // ```
- //
- // * `keyPath` {String} name of the key to observe
- // * `options` (optional) {Object}
- // * `scope` (optional) {ScopeDescriptor} describing a path from
- // the root of the syntax tree to a token. Get one by calling
- // {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
- // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
- // for more information.
- // * `callback` {Function} to call when the value of the key changes.
- // * `value` the new value of the key
- //
- // Returns a {Disposable} with the following keys on which you can call
- // `.dispose()` to unsubscribe.
- observe(...args) {
- let callback, keyPath, options, scopeDescriptor;
- if (args.length === 2) {
- [keyPath, callback] = args;
- } else if (
- args.length === 3 &&
- (_.isString(args[0]) && _.isObject(args[1]))
- ) {
- [keyPath, options, callback] = args;
- scopeDescriptor = options.scope;
- } else {
- console.error(
- 'An unsupported form of Config::observe is being used. See https://atom.io/docs/api/latest/Config for details'
- );
- return;
- }
- if (scopeDescriptor != null) {
- return this.observeScopedKeyPath(scopeDescriptor, keyPath, callback);
- } else {
- return this.observeKeyPath(
- keyPath,
- options ?? {},
- callback
- );
- }
- }
- // Essential: Add a listener for changes to a given key path. If `keyPath` is
- // not specified, your callback will be called on changes to any key.
- //
- // * `keyPath` (optional) {String} name of the key to observe. Must be
- // specified if `scopeDescriptor` is specified.
- // * `options` (optional) {Object}
- // * `scope` (optional) {ScopeDescriptor} describing a path from
- // the root of the syntax tree to a token. Get one by calling
- // {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
- // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
- // for more information.
- // * `callback` {Function} to call when the value of the key changes.
- // * `event` {Object}
- // * `newValue` the new value of the key
- // * `oldValue` the prior value of the key.
- //
- // Returns a {Disposable} with the following keys on which you can call
- // `.dispose()` to unsubscribe.
- onDidChange(...args) {
- let callback, keyPath, scopeDescriptor;
- if (args.length === 1) {
- [callback] = args;
- } else if (args.length === 2) {
- [keyPath, callback] = args;
- } else {
- let options;
- [keyPath, options, callback] = args;
- scopeDescriptor = options.scope;
- }
- if (scopeDescriptor != null) {
- return this.onDidChangeScopedKeyPath(scopeDescriptor, keyPath, callback);
- } else {
- return this.onDidChangeKeyPath(keyPath, callback);
- }
- }
- /*
- Section: Managing Settings
- */
- // Essential: Retrieves the setting for the given key.
- //
- // ### Examples
- //
- // You might want to know what themes are enabled, so check `core.themes`
- //
- // ```coffee
- // atom.config.get('core.themes')
- // ```
- //
- // With scope descriptors you can get settings within a specific editor
- // scope. For example, you might want to know `editor.tabLength` for ruby
- // files.
- //
- // ```coffee
- // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
- // ```
- //
- // This setting in ruby files might be different than the global tabLength setting
- //
- // ```coffee
- // atom.config.get('editor.tabLength') # => 4
- // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
- // ```
- //
- // You can get the language scope descriptor via
- // {TextEditor::getRootScopeDescriptor}. This will get the setting specifically
- // for the editor's language.
- //
- // ```coffee
- // atom.config.get('editor.tabLength', scope: @editor.getRootScopeDescriptor()) # => 2
- // ```
- //
- // Additionally, you can get the setting at the specific cursor position.
- //
- // ```coffee
- // scopeDescriptor = @editor.getLastCursor().getScopeDescriptor()
- // atom.config.get('editor.tabLength', scope: scopeDescriptor) # => 2
- // ```
- //
- // * `keyPath` The {String} name of the key to retrieve.
- // * `options` (optional) {Object}
- // * `sources` (optional) {Array} of {String} source names. If provided, only
- // values that were associated with these sources during {::set} will be used.
- // * `excludeSources` (optional) {Array} of {String} source names. If provided,
- // values that were associated with these sources during {::set} will not
- // be used.
- // * `scope` (optional) {ScopeDescriptor} describing a path from
- // the root of the syntax tree to a token. Get one by calling
- // {editor.getLastCursor().getScopeDescriptor()}
- // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
- // for more information.
- //
- // Returns the value from Pulsar's default settings, the user's configuration
- // file in the type specified by the configuration schema.
- get(...args) {
- let keyPath, options, scope;
- if (args.length > 1) {
- if (typeof args[0] === 'string' || args[0] == null) {
- [keyPath, options] = args;
- ({ scope } = options);
- }
- } else {
- [keyPath] = args;
- }
- if (scope != null) {
- const value = this.getRawScopedValue(scope, keyPath, options);
- return value != null ? value : this.getRawValue(keyPath, options);
- } else {
- return this.getRawValue(keyPath, options);
- }
- }
- // Extended: Get all of the values for the given key-path, along with their
- // associated scope selector.
- //
- // * `keyPath` The {String} name of the key to retrieve
- // * `options` (optional) {Object} see the `options` argument to {::get}
- //
- // Returns an {Array} of {Object}s with the following keys:
- // * `scopeDescriptor` The {ScopeDescriptor} with which the value is associated
- // * `value` The value for the key-path
- getAll(keyPath, options) {
- let globalValue, result, scope;
- if (options != null) {
- ({ scope } = options);
- }
- if (scope != null) {
- let legacyScopeDescriptor;
- const scopeDescriptor = ScopeDescriptor.fromObject(scope);
- result = this.scopedSettingsStore.getAll(
- scopeDescriptor.getScopeChain(),
- keyPath,
- options
- );
- legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor(
- scopeDescriptor
- );
- if (legacyScopeDescriptor) {
- result.push(
- ...Array.from(
- this.scopedSettingsStore.getAll(
- legacyScopeDescriptor.getScopeChain(),
- keyPath,
- options
- ) || []
- )
- );
- }
- } else {
- result = [];
- }
- globalValue = this.getRawValue(keyPath, options);
- if (globalValue) {
- result.push({ scopeSelector: '*', value: globalValue });
- }
- return result;
- }
- // Essential: Sets the value for a configuration setting.
- //
- // This value is stored in Pulsar's internal configuration file.
- //
- // ### Examples
- //
- // You might want to change the themes programmatically:
- //
- // ```coffee
- // atom.config.set('core.themes', ['atom-light-ui', 'atom-light-syntax'])
- // ```
- //
- // You can also set scoped settings. For example, you might want change the
- // `editor.tabLength` only for ruby files.
- //
- // ```coffee
- // atom.config.get('editor.tabLength') # => 4
- // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 4
- // atom.config.get('editor.tabLength', scope: ['source.js']) # => 4
- //
- // # Set ruby to 2
- // atom.config.set('editor.tabLength', 2, scopeSelector: '.source.ruby') # => true
- //
- // # Notice it's only set to 2 in the case of ruby
- // atom.config.get('editor.tabLength') # => 4
- // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
- // atom.config.get('editor.tabLength', scope: ['source.js']) # => 4
- // ```
- //
- // * `keyPath` The {String} name of the key.
- // * `value` The value of the setting. Passing `undefined` will revert the
- // setting to the default value.
- // * `options` (optional) {Object}
- // * `scopeSelector` (optional) {String}. eg. '.source.ruby'
- // See [the scopes docs](https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#scoped-settings-scopes-and-scope-descriptors)
- // for more information.
- // * `source` (optional) {String} The name of a file with which the setting
- // is associated. Defaults to the user's config file.
- //
- // Returns a {Boolean}
- // * `true` if the value was set.
- // * `false` if the value was not able to be coerced to the type specified in the setting's schema.
- set(...args) {
- let [keyPath, value, options = {}] = args;
- if (!this.settingsLoaded) {
- this.pendingOperations.push(() => this.set(keyPath, value, options));
- }
- // We should never use the scoped store to set global settings, since they are kept directly
- // in the config object.
- const scopeSelector =
- options.scopeSelector !== '*' ? options.scopeSelector : undefined;
- let source = options.source;
- const shouldSave = options.save != null ? options.save : true;
- if (source && !scopeSelector && source !== this.projectFile) {
- throw new Error(
- "::set with a 'source' and no 'scopeSelector' is not yet implemented!"
- );
- }
- if (!source) source = this.mainSource;
- if (value !== undefined) {
- try {
- value = this.makeValueConformToSchema(keyPath, value);
- } catch (e) {
- return false;
- }
- }
- if (scopeSelector != null) {
- this.setRawScopedValue(keyPath, value, source, scopeSelector);
- } else {
- this.setRawValue(keyPath, value, { source });
- }
- if (source === this.mainSource && shouldSave && this.settingsLoaded) {
- this.requestSave();
- }
- return true;
- }
- // Essential: Restore the setting at `keyPath` to its default value.
- //
- // * `keyPath` The {String} name of the key.
- // * `options` (optional) {Object}
- // * `scopeSelector` (optional) {String}. See {::set}
- // * `source` (optional) {String}. See {::set}
- unset(keyPath, options) {
- if (!this.settingsLoaded) {
- this.pendingOperations.push(() => this.unset(keyPath, options));
- }
- let { scopeSelector, source } = options != null ? options : {};
- if (source == null) {
- source = this.mainSource;
- }
- if (scopeSelector != null) {
- if (keyPath != null) {
- let settings = this.scopedSettingsStore.propertiesForSourceAndSelector(
- source,
- scopeSelector
- );
- if (getValueAtKeyPath(settings, keyPath) != null) {
- this.scopedSettingsStore.removePropertiesForSourceAndSelector(
- source,
- scopeSelector
- );
- setValueAtKeyPath(settings, keyPath, undefined);
- settings = withoutEmptyObjects(settings);
- if (settings != null) {
- this.set(null, settings, {
- scopeSelector,
- source,
- priority: this.priorityForSource(source)
- });
- }
- const configIsReady =
- source === this.mainSource && this.settingsLoaded;
- if (configIsReady) {
- return this.requestSave();
- }
- }
- } else {
- this.scopedSettingsStore.removePropertiesForSourceAndSelector(
- source,
- scopeSelector
- );
- return this.emitChangeEvent();
- }
- } else {
- for (scopeSelector in this.scopedSettingsStore.propertiesForSource(
- source
- )) {
- this.unset(keyPath, { scopeSelector, source });
- }
- if (keyPath != null && source === this.mainSource) {
- return this.set(
- keyPath,
- getValueAtKeyPath(this.defaultSettings, keyPath)
- );
- }
- }
- }
- // Extended: Get an {Array} of all of the `source` {String}s with which
- // settings have been added via {::set}.
- getSources() {
- return _.uniq(
- _.pluck(this.scopedSettingsStore.propertySets, 'source')
- ).sort();
- }
- // Extended: Retrieve the schema for a specific key path. The schema will tell
- // you what type the keyPath expects, and other metadata about the config
- // option.
- //
- // * `keyPath` The {String} name of the key.
- //
- // Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`.
- // Returns `null` when the keyPath has no schema specified, but is accessible
- // from the root schema.
- getSchema(keyPath) {
- const keys = splitKeyPath(keyPath);
- let { schema } = this;
- for (let key of keys) {
- let childSchema;
- if (schema.type === 'object') {
- childSchema =
- schema.properties != null ? schema.properties[key] : undefined;
- if (childSchema == null) {
- if (isPlainObject(schema.additionalProperties)) {
- childSchema = schema.additionalProperties;
- } else if (schema.additionalProperties === false) {
- return null;
- } else {
- return { type: 'any' };
- }
- }
- } else {
- return null;
- }
- schema = childSchema;
- }
- return schema;
- }
- getUserConfigPath() {
- return this.mainSource;
- }
- // Extended: Suppress calls to handler functions registered with {::onDidChange}
- // and {::observe} for the duration of `callback`. After `callback` executes,
- // handlers will be called once if the value for their key-path has changed.
- //
- // * `callback` {Function} to execute while suppressing calls to handlers.
- transact(callback) {
- this.beginTransaction();
- try {
- return callback();
- } finally {
- this.endTransaction();
- }
- }
- getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor) {
- return null;
- }
- /*
- Section: Internal methods used by core
- */
- // Private: Suppress calls to handler functions registered with {::onDidChange}
- // and {::observe} for the duration of the {Promise} returned by `callback`.
- // After the {Promise} is either resolved or rejected, handlers will be called
- // once if the value for their key-path has changed.
- //
- // * `callback` {Function} that returns a {Promise}, which will be executed
- // while suppressing calls to handlers.
- //
- // Returns a {Promise} that is either resolved or rejected according to the
- // `{Promise}` returned by `callback`. If `callback` throws an error, a
- // rejected {Promise} will be returned instead.
- transactAsync(callback) {
- let endTransaction;
- this.beginTransaction();
- try {
- endTransaction = fn => (...args) => {
- this.endTransaction();
- return fn(...args);
- };
- const result = callback();
- return new Promise((resolve, reject) => {
- return result
- .then(endTransaction(resolve))
- .catch(endTransaction(reject));
- });
- } catch (error) {
- this.endTransaction();
- return Promise.reject(error);
- }
- }
- beginTransaction() {
- this.transactDepth++;
- }
- endTransaction() {
- this.transactDepth--;
- this.emitChangeEvent();
- }
- pushAtKeyPath(keyPath, value) {
- const left = this.get(keyPath);
- const arrayValue = left == null ? [] : left;
- const result = arrayValue.push(value);
- this.set(keyPath, arrayValue);
- return result;
- }
- unshiftAtKeyPath(keyPath, value) {
- const left = this.get(keyPath);
- const arrayValue = left == null ? [] : left;
- const result = arrayValue.unshift(value);
- this.set(keyPath, arrayValue);
- return result;
- }
- removeAtKeyPath(keyPath, value) {
- const left = this.get(keyPath);
- const arrayValue = left == null ? [] : left;
- const result = _.remove(arrayValue, value);
- this.set(keyPath, arrayValue);
- return result;
- }
- setSchema(keyPath, schema) {
- if (!isPlainObject(schema)) {
- throw new Error(
- `Error loading schema for ${keyPath}: schemas can only be objects!`
- );
- }
- if (schema.type == null) {
- throw new Error(
- `Error loading schema for ${keyPath}: schema objects must have a type attribute`
- );
- }
- let rootSchema = this.schema;
- if (keyPath) {
- for (let key of splitKeyPath(keyPath)) {
- rootSchema.type = 'object';
- if (rootSchema.properties == null) {
- rootSchema.properties = {};
- }
- const { properties } = rootSchema;
- if (properties[key] == null) {
- properties[key] = {};
- }
- rootSchema = properties[key];
- }
- }
- Object.assign(rootSchema, schema);
- this.transact(() => {
- this.setDefaults(keyPath, this.extractDefaultsFromSchema(schema));
- this.setScopedDefaultsFromSchema(keyPath, schema);
- this.resetSettingsForSchemaChange();
- });
- }
- save() {
- if (this.saveCallback) {
- let allSettings = { '*': this.settings };
- allSettings = Object.assign(
- allSettings,
- this.scopedSettingsStore.propertiesForSource(this.mainSource)
- );
- allSettings = sortObject(allSettings);
- this.saveCallback(allSettings);
- }
- }
- /*
- Section: Private methods managing global settings
- */
- resetUserSettings(newSettings, options = {}) {
- this._resetSettings(newSettings, options);
- }
- _resetSettings(newSettings, options = {}) {
- const source = options.source;
- newSettings = Object.assign({}, newSettings);
- if (newSettings.global != null) {
- newSettings['*'] = newSettings.global;
- delete newSettings.global;
- }
- if (newSettings['*'] != null) {
- const scopedSettings = newSettings;
- newSettings = newSettings['*'];
- delete scopedSettings['*'];
- this.resetScopedSettings(scopedSettings, { source });
- }
- return this.transact(() => {
- this._clearUnscopedSettingsForSource(source);
- this.settingsLoaded = true;
- for (let key in newSettings) {
- const value = newSettings[key];
- this.set(key, value, { save: false, source });
- }
- if (this.pendingOperations.length) {
- for (let op of this.pendingOperations) {
- op();
- }
- this.pendingOperations = [];
- }
- });
- }
- _clearUnscopedSettingsForSource(source) {
- if (source === this.projectFile) {
- this.projectSettings = {};
- } else {
- this.settings = {};
- }
- }
- resetProjectSettings(newSettings, projectFile) {
- // Sets the scope and source of all project settings to `path`.
- newSettings = Object.assign({}, newSettings);
- const oldProjectFile = this.projectFile;
- this.projectFile = projectFile;
- if (this.projectFile != null) {
- this._resetSettings(newSettings, { source: this.projectFile });
- } else {
- this.scopedSettingsStore.removePropertiesForSource(oldProjectFile);
- this.projectSettings = {};
- }
- }
- clearProjectSettings() {
- this.resetProjectSettings({}, null);
- }
- getRawValue(keyPath, options = {}) {
- let { excludeSources, sources } = options;
- let value;
- // If `excludeSources` is missing or does not exclude the main source…
- if (!excludeSources || !excludeSources.includes(this.mainSource)) {
- value = getValueAtKeyPath(this.settings, keyPath);
- // we should prefer the project specific setting as long as…
- if (
- this.projectFile != null &&
- // `excludeSources` is missing or does not include the project-specific
- // source, and…
- (
- !excludeSources || !excludeSources.includes(this.projectFile)
- ) &&
- // `sources` is missing or includes the project-specific source.
- (
- !sources || sources.includes(this.projectFile)
- )
- ) {
- let projectValue = getValueAtKeyPath(this.projectSettings, keyPath);
- if (projectValue === undefined) {
- // There is no project-specific override for this key path. `value`
- // stays as `value` and we pretend this never happened.
- } else if (isPlainObject(value) && isPlainObject(projectValue)) {
- // This key path returned an object, so we need to merge the contents
- // of the two objects into a third composite object. First we clone
- // the project object so as not to modify it…
- projectValue = this.deepClone(projectValue);
- // …then we copy over the regular value's properties, preferring the
- // project-specific value wherever there is overlap.
- this.deepDefaults(projectValue, value);
- value = projectValue;
- } else {
- // This is a single value, so we prefer the project version.
- value = projectValue;
- }
- }
- }
- let defaultValue;
- if (!options.sources || options.sources.length === 0) {
- defaultValue = getValueAtKeyPath(this.defaultSettings, keyPath);
- }
- if (value != null) {
- value = this.deepClone(value);
- if (isPlainObject(value) && isPlainObject(defaultValue)) {
- this.deepDefaults(value, defaultValue);
- }
- return value;
- } else {
- return this.deepClone(defaultValue);
- }
- }
- setRawValue(keyPath, value, options = {}) {
- const source = options.source ? options.source : undefined;
- const settingsToChange =
- source === this.projectFile ? 'projectSettings' : 'settings';
- const defaultValue = getValueAtKeyPath(this.defaultSettings, keyPath);
- if (_.isEqual(defaultValue, value)) {
- if (keyPath != null) {
- deleteValueAtKeyPath(this[settingsToChange], keyPath);
- } else {
- this[settingsToChange] = null;
- }
- } else {
- if (keyPath != null) {
- setValueAtKeyPath(this[settingsToChange], keyPath, value);
- } else {
- this[settingsToChange] = value;
- }
- }
- return this.emitChangeEvent();
- }
- observeKeyPath(keyPath, options, callback) {
- callback(this.get(keyPath));
- return this.onDidChangeKeyPath(keyPath, event => callback(event.newValue));
- }
- onDidChangeKeyPath(keyPath, callback) {
- let oldValue = this.get(keyPath);
- return this.emitter.on('did-change', () => {
- const newValue = this.get(keyPath);
- if (!_.isEqual(oldValue, newValue)) {
- const event = { oldValue, newValue };
- oldValue = newValue;
- return callback(event);
- }
- });
- }
- isSubKeyPath(keyPath, subKeyPath) {
- if (keyPath == null || subKeyPath == null) {
- return false;
- }
- const pathSubTokens = splitKeyPath(subKeyPath);
- const pathTokens = splitKeyPath(keyPath).slice(0, pathSubTokens.length);
- return _.isEqual(pathTokens, pathSubTokens);
- }
- setRawDefault(keyPath, value) {
- setValueAtKeyPath(this.defaultSettings, keyPath, value);
- return this.emitChangeEvent();
- }
- setDefaults(keyPath, defaults) {
- if (defaults != null && isPlainObject(defaults)) {
- const keys = splitKeyPath(keyPath);
- this.transact(() => {
- const result = [];
- for (let key in defaults) {
- const childValue = defaults[key];
- if (!defaults.hasOwnProperty(key)) {
- continue;
- }
- result.push(
- this.setDefaults(keys.concat([key]).join('.'), childValue)
- );
- }
- return result;
- });
- } else {
- try {
- defaults = this.makeValueConformToSchema(keyPath, defaults);
- this.setRawDefault(keyPath, defaults);
- } catch (e) {
- console.warn(
- `'${keyPath}' could not set the default. Attempted default: ${JSON.stringify(
- defaults
- )}; Schema: ${JSON.stringify(this.getSchema(keyPath))}`
- );
- }
- }
- }
- deepClone(object) {
- if (object instanceof Color) {
- return object.clone();
- } else if (Array.isArray(object)) {
- return object.map(value => this.deepClone(value));
- } else if (isPlainObject(object)) {
- return _.mapObject(object, (key, value) => [key, this.deepClone(value)]);
- } else {
- return object;
- }
- }
- deepDefaults(target) {
- let result = target;
- let i = 0;
- while (++i < arguments.length) {
- const object = arguments[i];
- if (isPlainObject(result) && isPlainObject(object)) {
- for (let key of Object.keys(object)) {
- result[key] = this.deepDefaults(result[key], object[key]);
- }
- } else {
- if (result == null) {
- result = this.deepClone(object);
- }
- }
- }
- return result;
- }
- // `schema` will look something like this
- //
- // ```coffee
- // type: 'string'
- // default: 'ok'
- // scopes:
- // '.source.js':
- // default: 'omg'
- // ```
- setScopedDefaultsFromSchema(keyPath, schema) {
- if (schema.scopes != null && isPlainObject(schema.scopes)) {
- const scopedDefaults = {};
- for (let scope in schema.scopes) {
- const scopeSchema = schema.scopes[scope];
- if (!scopeSchema.hasOwnProperty('default')) {
- continue;
- }
- scopedDefaults[scope] = {};
- setValueAtKeyPath(scopedDefaults[scope], keyPath, scopeSchema.default);
- }
- this.scopedSettingsStore.addProperties('schema-default', scopedDefaults);
- }
- if (
- schema.type === 'object' &&
- schema.properties != null &&
- isPlainObject(schema.properties)
- ) {
- const keys = splitKeyPath(keyPath);
- for (let key in schema.properties) {
- const childValue = schema.properties[key];
- if (!schema.properties.hasOwnProperty(key)) {
- continue;
- }
- this.setScopedDefaultsFromSchema(
- keys.concat([key]).join('.'),
- childValue
- );
- }
- }
- }
- extractDefaultsFromSchema(schema) {
- if (schema.default != null) {
- return schema.default;
- } else if (
- schema.type === 'object' &&
- schema.properties != null &&
- isPlainObject(schema.properties)
- ) {
- const defaults = {};
- const properties = schema.properties || {};
- for (let key in properties) {
- const value = properties[key];
- defaults[key] = this.extractDefaultsFromSchema(value);
- }
- return defaults;
- }
- }
- makeValueConformToSchema(keyPath, value, options) {
- if (options != null ? options.suppressException : undefined) {
- try {
- return this.makeValueConformToSchema(keyPath, value);
- } catch (e) {
- return undefined;
- }
- } else {
- let schema;
- if ((schema = this.getSchema(keyPath)) == null) {
- if (schema === false) {
- throw new Error(`Illegal key path ${keyPath}`);
- }
- }
- return this.constructor.executeSchemaEnforcers(keyPath, value, schema);
- }
- }
- // When the schema is changed / added, there may be values set in the config
- // that do not conform to the schema. This will reset make them conform.
- resetSettingsForSchemaChange(source) {
- if (source == null) {
- source = this.mainSource;
- }
- return this.transact(() => {
- this.settings = this.makeValueConformToSchema(null, this.settings, {
- suppressException: true
- });
- const selectorsAndSettings = this.scopedSettingsStore.propertiesForSource(
- source
- );
- this.scopedSettingsStore.removePropertiesForSource(source);
- for (let scopeSelector in selectorsAndSettings) {
- let settings = selectorsAndSettings[scopeSelector];
- settings = this.makeValueConformToSchema(null, settings, {
- suppressException: true
- });
- this.setRawScopedValue(null, settings, source, scopeSelector);
- }
- });
- }
- /*
- Section: Private Scoped Settings
- */
- priorityForSource(source) {
- switch (source) {
- case this.mainSource:
- return 1000;
- case this.projectFile:
- return 2000;
- default:
- return 0;
- }
- }
- emitChangeEvent() {
- if (this.transactDepth <= 0) {
- return this.emitter.emit('did-change');
- }
- }
- resetScopedSettings(newScopedSettings, options = {}) {
- const source = options.source == null ? this.mainSource : options.source;
- const priority = this.priorityForSource(source);
- this.scopedSettingsStore.removePropertiesForSource(source);
- for (let scopeSelector in newScopedSettings) {
- let settings = newScopedSettings[scopeSelector];
- settings = this.makeValueConformToSchema(null, settings, {
- suppressException: true
- });
- const validatedSettings = {};
- validatedSettings[scopeSelector] = withoutEmptyObjects(settings);
- if (validatedSettings[scopeSelector] != null) {
- this.scopedSettingsStore.addProperties(source, validatedSettings, {
- priority
- });
- }
- }
- return this.emitChangeEvent();
- }
- setRawScopedValue(keyPath, value, source, selector, options) {
- if (keyPath != null) {
- const newValue = {};
- setValueAtKeyPath(newValue, keyPath, value);
- value = newValue;
- }
- const settingsBySelector = {};
- settingsBySelector[selector] = value;
- this.scopedSettingsStore.addProperties(source, settingsBySelector, {
- priority: this.priorityForSource(source)
- });
- return this.emitChangeEvent();
- }
- getRawScopedValue(scopeDescriptor, keyPath, options) {
- scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor);
- const result = this.scopedSettingsStore.getPropertyValue(
- scopeDescriptor.getScopeChain(),
- keyPath,
- options
- );
- const legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor(
- scopeDescriptor
- );
- if (result != null) {
- return result;
- } else if (legacyScopeDescriptor) {
- return this.scopedSettingsStore.getPropertyValue(
- legacyScopeDescriptor.getScopeChain(),
- keyPath,
- options
- );
- }
- }
- observeScopedKeyPath(scope, keyPath, callback) {
- callback(this.get(keyPath, { scope }));
- return this.onDidChangeScopedKeyPath(scope, keyPath, event =>
- callback(event.newValue)
- );
- }
- onDidChangeScopedKeyPath(scope, keyPath, callback) {
- let oldValue = this.get(keyPath, { scope });
- return this.emitter.on('did-change', () => {
- const newValue = this.get(keyPath, { scope });
- if (!_.isEqual(oldValue, newValue)) {
- const event = { oldValue, newValue };
- oldValue = newValue;
- callback(event);
- }
- });
- }
- }
- // Base schema enforcers. These will coerce raw input into the specified type,
- // and will throw an error when the value cannot be coerced. Throwing the error
- // will indicate that the value should not be set.
- //
- // Enforcers are run from most specific to least. For a schema with type
- // `integer`, all the enforcers for the `integer` type will be run first, in
- // order of specification. Then the `*` enforcers will be run, in order of
- // specification.
- Config.addSchemaEnforcers({
- any: {
- coerce(keyPath, value, schema) {
- return value;
- }
- },
- integer: {
- coerce(keyPath, value, schema) {
- value = parseInt(value);
- if (isNaN(value) || !isFinite(value)) {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} cannot be coerced into an int`
- );
- }
- return value;
- }
- },
- number: {
- coerce(keyPath, value, schema) {
- value = parseFloat(value);
- if (isNaN(value) || !isFinite(value)) {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} cannot be coerced into a number`
- );
- }
- return value;
- }
- },
- boolean: {
- coerce(keyPath, value, schema) {
- switch (typeof value) {
- case 'string':
- if (value.toLowerCase() === 'true') {
- return true;
- } else if (value.toLowerCase() === 'false') {
- return false;
- } else {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} must be a boolean or the string 'true' or 'false'`
- );
- }
- case 'boolean':
- return value;
- default:
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} must be a boolean or the string 'true' or 'false'`
- );
- }
- }
- },
- string: {
- validate(keyPath, value, schema) {
- if (typeof value !== 'string') {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} must be a string`
- );
- }
- return value;
- },
- validateMaximumLength(keyPath, value, schema) {
- if (
- typeof schema.maximumLength === 'number' &&
- value.length > schema.maximumLength
- ) {
- return value.slice(0, schema.maximumLength);
- } else {
- return value;
- }
- }
- },
- null: {
- // null sort of isnt supported. It will just unset in this case
- coerce(keyPath, value, schema) {
- if (![undefined, null].includes(value)) {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} must be null`
- );
- }
- return value;
- }
- },
- object: {
- coerce(keyPath, value, schema) {
- if (!isPlainObject(value)) {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} must be an object`
- );
- }
- if (schema.properties == null) {
- return value;
- }
- let defaultChildSchema = null;
- let allowsAdditionalProperties = true;
- if (isPlainObject(schema.additionalProperties)) {
- defaultChildSchema = schema.additionalProperties;
- }
- if (schema.additionalProperties === false) {
- allowsAdditionalProperties = false;
- }
- const newValue = {};
- for (let prop in value) {
- const propValue = value[prop];
- const childSchema =
- schema.properties[prop] != null
- ? schema.properties[prop]
- : defaultChildSchema;
- if (childSchema != null) {
- try {
- newValue[prop] = this.executeSchemaEnforcers(
- pushKeyPath(keyPath, prop),
- propValue,
- childSchema
- );
- } catch (error) {
- console.warn(`Error setting item in object: ${error.message}`);
- }
- } else if (allowsAdditionalProperties) {
- // Just pass through un-schema'd values
- newValue[prop] = propValue;
- } else {
- console.warn(`Illegal object key: ${keyPath}.${prop}`);
- }
- }
- return newValue;
- }
- },
- array: {
- coerce(keyPath, value, schema) {
- if (!Array.isArray(value)) {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} must be an array`
- );
- }
- const itemSchema = schema.items;
- if (itemSchema != null) {
- const newValue = [];
- for (let item of value) {
- try {
- newValue.push(
- this.executeSchemaEnforcers(keyPath, item, itemSchema)
- );
- } catch (error) {
- console.warn(`Error setting item in array: ${error.message}`);
- }
- }
- return newValue;
- } else {
- return value;
- }
- }
- },
- color: {
- coerce(keyPath, value, schema) {
- const color = Color.parse(value);
- if (color == null) {
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} cannot be coerced into a color`
- );
- }
- return color;
- }
- },
- '*': {
- coerceMinimumAndMaximum(keyPath, value, schema) {
- if (typeof value !== 'number') {
- return value;
- }
- if (schema.minimum != null && typeof schema.minimum === 'number') {
- value = Math.max(value, schema.minimum);
- }
- if (schema.maximum != null && typeof schema.maximum === 'number') {
- value = Math.min(value, schema.maximum);
- }
- return value;
- },
- validateEnum(keyPath, value, schema) {
- let possibleValues = schema.enum;
- if (Array.isArray(possibleValues)) {
- possibleValues = possibleValues.map(value => {
- if (value.hasOwnProperty('value')) {
- return value.value;
- } else {
- return value;
- }
- });
- }
- if (
- possibleValues == null ||
- !Array.isArray(possibleValues) ||
- !possibleValues.length
- ) {
- return value;
- }
- for (let possibleValue of possibleValues) {
- // Using `isEqual` for possibility of placing enums on array and object schemas
- if (_.isEqual(possibleValue, value)) {
- return value;
- }
- }
- throw new Error(
- `Validation failed at ${keyPath}, ${JSON.stringify(
- value
- )} is not one of ${JSON.stringify(possibleValues)}`
- );
- }
- }
- });
- let isPlainObject = value =>
- _.isObject(value) &&
- !Array.isArray(value) &&
- !_.isFunction(value) &&
- !_.isString(value) &&
- !(value instanceof Color);
- let sortObject = value => {
- if (!isPlainObject(value)) {
- return value;
- }
- const result = {};
- for (let key of Object.keys(value).sort()) {
- result[key] = sortObject(value[key]);
- }
- return result;
- };
- const withoutEmptyObjects = object => {
- let resultObject;
- if (isPlainObject(object)) {
- for (let key in object) {
- const value = object[key];
- const newValue = withoutEmptyObjects(value);
- if (newValue != null) {
- if (resultObject == null) {
- resultObject = {};
- }
- resultObject[key] = newValue;
- }
- }
- } else {
- resultObject = object;
- }
- return resultObject;
- };
- module.exports = Config;
|