mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-12 04:25:31 +08:00
488dff0f90
Converted all references of global atom to NylasEnv Temporary rename atom.io find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.io/temporaryAtomIoReplacement/g' atom.config to NylasEnv.config find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.config/NylasEnv.config/g' atom.packages -> NylasEnv.packages atom.commands -> NylasEnv.commands atom.getLoadSettings find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.commands/NylasEnv.commands/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.getLoadSettings/NylasEnv.getLoadSettings/g' More common atom methods find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.styles/NylasEnv.styles/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.emitError/NylasEnv.emitError/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.inSpecMode/NylasEnv.inSpecMode/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.inDevMode/NylasEnv.inDevMode/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.getWindowType/NylasEnv.getWindowType/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.displayWindow/NylasEnv.displayWindow/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.quit/NylasEnv.quit/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.close/NylasEnv.close/g' More atom method changes find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.keymaps/NylasEnv.keymaps/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.hide/NylasEnv.hide/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.getCurrentWindow/NylasEnv.getCurrentWindow/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.menu/NylasEnv.menu/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.getConfigDirPath/NylasEnv.getConfigDirPath/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.isMainWindow/NylasEnv.isMainWindow/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.finishUnload/NylasEnv.finishUnload/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.isWorkWindow/NylasEnv.isWorkWindow/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.showSaveDialog/NylasEnv.showSaveDialog/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.append/NylasEnv.append/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.confirm/NylasEnv.confirm/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.clipboard/NylasEnv.clipboard/g' find -E . -regex ".*\.(coffee|cjsx|js|md|cmd|es6)" -print0 | xargs -0 sed -i "" 's/atom.getVersion/NylasEnv.getVersion/g' More atom renaming Rename atom methods More atom methods Fix grunt config variable Change atom.cmd to N1.cmd Rename atom.coffee and atom.js to nylas-env.coffee nylas-env.js Fix atom global reference in specs manually Fix atom requires Change engine from atom to nylas got rid of global/nylas-env rename to nylas-win-bootup Fix onWindowPropsChanged to onWindowPropsReceived fix nylas-workspace atom-text-editor to nylas-theme-wrap atom-text-editor -> nylas-theme-wrap Replacing atom keyword AtomWindow -> NylasWindow Replace Atom -> N1 Rename atom items nylas.asar -> atom.asar Remove more atom references Remove 6to5 references Remove license exception for atom
1203 lines
39 KiB
CoffeeScript
1203 lines
39 KiB
CoffeeScript
_ = require 'underscore'
|
|
_ = _.extend(_, require('./config-utils'))
|
|
fs = require 'fs-plus'
|
|
EmitterMixin = require('emissary').Emitter
|
|
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
|
|
CSON = require 'season'
|
|
path = require 'path'
|
|
async = require 'async'
|
|
pathWatcher = require 'pathwatcher'
|
|
Grim = require 'grim'
|
|
|
|
Color = require './color'
|
|
ScopedPropertyStore = require 'scoped-property-store'
|
|
ScopeDescriptor = require './scope-descriptor'
|
|
|
|
if global.application
|
|
app = global.application
|
|
else
|
|
app = require('remote').getGlobal('application')
|
|
|
|
# Essential: Used to access all of N1's configuration details.
|
|
#
|
|
# An instance of this class is always available as the `NylasEnv.config` global.
|
|
#
|
|
# ## Getting and setting config settings.
|
|
#
|
|
# ```coffee
|
|
# # Note that with no value set, ::get returns the setting's default value.
|
|
# NylasEnv.config.get('my-package.myKey') # -> 'defaultValue'
|
|
#
|
|
# NylasEnv.config.set('my-package.myKey', 'value')
|
|
# NylasEnv.config.get('my-package.myKey') # -> 'value'
|
|
# ```
|
|
#
|
|
# You may want to watch for changes. Use {::observe} to catch changes to the setting.
|
|
#
|
|
# ```coffee
|
|
# NylasEnv.config.set('my-package.myKey', 'value')
|
|
# NylasEnv.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
|
|
# NylasEnv.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 an integer setting that only
|
|
# allows integers greater than `0`:
|
|
#
|
|
# ```coffee
|
|
# # When no value has been set, `::get` returns the setting's default value
|
|
# NylasEnv.config.get('my-package.anInt') # -> 12
|
|
#
|
|
# # The string will be coerced to the integer 123
|
|
# NylasEnv.config.set('my-package.anInt', '123')
|
|
# NylasEnv.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
|
|
# NylasEnv.config.set('my-package.anInt', '-20')
|
|
# NylasEnv.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) -> # ...
|
|
# # ...
|
|
# ```
|
|
#
|
|
# ## 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
|
|
# NylasEnv.config.set('my-package.thingVolume', '10')
|
|
# NylasEnv.config.get('my-package.thingVolume') # -> 10
|
|
#
|
|
# # It respects the min / max
|
|
# NylasEnv.config.set('my-package.thingVolume', '400')
|
|
# NylasEnv.config.get('my-package.thingVolume') # -> 11
|
|
#
|
|
# # If it cannot be coerced, the value will not be set
|
|
# NylasEnv.config.set('my-package.thingVolume', 'cats')
|
|
# NylasEnv.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
|
|
# NylasEnv.config.set('my-package.someSetting', 'true')
|
|
# NylasEnv.config.get('my-package.someSetting') # -> true
|
|
#
|
|
# NylasEnv.config.set('my-package.someSetting', '12')
|
|
# NylasEnv.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
|
|
# ```
|
|
#
|
|
# #### object
|
|
#
|
|
# Value must be an object. This allows you to nest config options. Sub options
|
|
# must be under a `properties key`
|
|
#
|
|
# ```coffee
|
|
# config:
|
|
# someSetting:
|
|
# type: 'object'
|
|
# properties:
|
|
# myChildIntOption:
|
|
# 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'
|
|
# ```
|
|
#
|
|
# ### Other Supported Keys
|
|
#
|
|
# #### enum
|
|
#
|
|
# All types support an `enum` key. The enum key lets you specify all values
|
|
# that the config setting can possibly be. `enum` _must_ be an array of values
|
|
# of your specified type. Schema:
|
|
#
|
|
# ```coffee
|
|
# config:
|
|
# someSetting:
|
|
# type: 'integer'
|
|
# default: 4
|
|
# enum: [2, 4, 6, 8]
|
|
# ```
|
|
#
|
|
# Usage:
|
|
#
|
|
# ```coffee
|
|
# NylasEnv.config.set('my-package.someSetting', '2')
|
|
# NylasEnv.config.get('my-package.someSetting') # -> 2
|
|
#
|
|
# # will not set values outside of the enum values
|
|
# NylasEnv.config.set('my-package.someSetting', '3')
|
|
# NylasEnv.config.get('my-package.someSetting') # -> 2
|
|
#
|
|
# # If it cannot be coerced, the value will not be set
|
|
# NylasEnv.config.set('my-package.someSetting', '4')
|
|
# NylasEnv.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.
|
|
#
|
|
# ```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!
|
|
#
|
|
# ## Best practices
|
|
#
|
|
# * Don't depend on (or write to) configuration keys outside of your keypath.
|
|
#
|
|
module.exports =
|
|
class Config
|
|
EmitterMixin.includeInto(this)
|
|
@schemaEnforcers = {}
|
|
|
|
@addSchemaEnforcer: (typeName, enforcerFunction) ->
|
|
@schemaEnforcers[typeName] ?= []
|
|
@schemaEnforcers[typeName].push(enforcerFunction)
|
|
|
|
@addSchemaEnforcers: (filters) ->
|
|
for typeName, functions of filters
|
|
for name, enforcerFunction of functions
|
|
@addSchemaEnforcer(typeName, enforcerFunction)
|
|
|
|
@executeSchemaEnforcers: (keyPath, value, schema) ->
|
|
error = null
|
|
types = schema.type
|
|
types = [types] unless Array.isArray(types)
|
|
for type in types
|
|
try
|
|
enforcerFunctions = @schemaEnforcers[type].concat(@schemaEnforcers['*'])
|
|
for enforcer in 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
|
|
|
|
throw error if error?
|
|
value
|
|
|
|
# Created during initialization, available as `NylasEnv.config`
|
|
constructor: ({@configDirPath, @resourcePath}={}) ->
|
|
@emitter = new Emitter
|
|
@schema =
|
|
type: 'object'
|
|
properties: {}
|
|
@defaultSettings = {}
|
|
@settings = {}
|
|
@scopedSettingsStore = new ScopedPropertyStore
|
|
@configFileHasErrors = false
|
|
@configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
|
|
@configFilePath ?= path.join(@configDirPath, 'config.cson')
|
|
@transactDepth = 0
|
|
@savePending = false
|
|
|
|
@requestLoad = _.debounce(@loadUserConfig, 100)
|
|
@debouncedLoad = _.debounce(@loadUserConfig, 100)
|
|
@requestSave = =>
|
|
@savePending = true
|
|
debouncedSave.call(this)
|
|
save = =>
|
|
@savePending = false
|
|
@save()
|
|
debouncedSave = _.debounce(save, 100)
|
|
|
|
###
|
|
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
|
|
# NylasEnv.config.observe 'core.themes', (value) ->
|
|
# # do stuff with value
|
|
# ```
|
|
#
|
|
# * `keyPath` {String} name of the key to observe
|
|
# * `options` {Object}
|
|
# * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from
|
|
# the root of the syntax tree to a token. Get one by calling
|
|
# {editor.getLastCursor().getScopeDescriptor()}.
|
|
# * `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: ->
|
|
if arguments.length is 2
|
|
[keyPath, callback] = arguments
|
|
else if arguments.length is 3 and (_.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor)
|
|
Grim.deprecate """
|
|
Passing a scope descriptor as the first argument to Config::observe is deprecated.
|
|
Pass a `scope` in an options hash as the third argument instead.
|
|
"""
|
|
[scopeDescriptor, keyPath, callback] = arguments
|
|
else if arguments.length is 3 and (_.isString(arguments[0]) and _.isObject(arguments[1]))
|
|
[keyPath, options, callback] = arguments
|
|
scopeDescriptor = options.scope
|
|
if options.callNow?
|
|
Grim.deprecate """
|
|
Config::observe no longer takes a `callNow` option. Use ::onDidChange instead.
|
|
Note that ::onDidChange passes its callback different arguments.
|
|
"""
|
|
else
|
|
console.error 'An unsupported form of Config::observe is being used.'
|
|
return
|
|
|
|
if scopeDescriptor?
|
|
@observeScopedKeyPath(scopeDescriptor, keyPath, callback)
|
|
else
|
|
@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.
|
|
# * `optional` (optional) {Object}
|
|
# * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from
|
|
# the root of the syntax tree to a token. Get one by calling
|
|
# {editor.getLastCursor().getScopeDescriptor()}.
|
|
# 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.
|
|
# * `keyPath` the keyPath of the changed key
|
|
#
|
|
# Returns a {Disposable} with the following keys on which you can call
|
|
# `.dispose()` to unsubscribe.
|
|
onDidChange: ->
|
|
if arguments.length is 1
|
|
[callback] = arguments
|
|
else if arguments.length is 2
|
|
[keyPath, callback] = arguments
|
|
else if _.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor
|
|
Grim.deprecate """
|
|
Passing a scope descriptor as the first argument to Config::onDidChange is deprecated.
|
|
Pass a `scope` in an options hash as the third argument instead.
|
|
"""
|
|
[scopeDescriptor, keyPath, callback] = arguments
|
|
else
|
|
[keyPath, options, callback] = arguments
|
|
scopeDescriptor = options.scope
|
|
|
|
if scopeDescriptor?
|
|
@onDidChangeScopedKeyPath(scopeDescriptor, keyPath, callback)
|
|
else
|
|
@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
|
|
# NylasEnv.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
|
|
# NylasEnv.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
|
|
# ```
|
|
#
|
|
# This setting in ruby files might be different than the global tabLength setting
|
|
#
|
|
# ```coffee
|
|
# NylasEnv.config.get('editor.tabLength') # => 4
|
|
# NylasEnv.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
|
|
# NylasEnv.config.get('editor.tabLength', scope: @editor.getRootScopeDescriptor()) # => 2
|
|
# ```
|
|
#
|
|
# Additionally, you can get the setting at the specific cursor position.
|
|
#
|
|
# ```coffee
|
|
# scopeDescriptor = @editor.getLastCursor().getScopeDescriptor()
|
|
# NylasEnv.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()}
|
|
#
|
|
# Returns the value from N1's default settings, the user's configuration
|
|
# file in the type specified by the configuration schema.
|
|
get: ->
|
|
if arguments.length > 1
|
|
if typeof arguments[0] is 'string' or not arguments[0]?
|
|
[keyPath, options] = arguments
|
|
{scope} = options
|
|
else
|
|
Grim.deprecate """
|
|
Passing a scope descriptor as the first argument to Config::get is deprecated.
|
|
Pass a `scope` in an options hash as the final argument instead.
|
|
"""
|
|
[scope, keyPath] = arguments
|
|
else
|
|
[keyPath] = arguments
|
|
|
|
if scope?
|
|
value = @getRawScopedValue(scope, keyPath, options)
|
|
value ? @getRawValue(keyPath, options)
|
|
else
|
|
@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) ->
|
|
{scope, sources} = options if options?
|
|
result = []
|
|
|
|
if scope?
|
|
scopeDescriptor = ScopeDescriptor.fromObject(scope)
|
|
result = result.concat @scopedSettingsStore.getAll(scopeDescriptor.getScopeChain(), keyPath, options)
|
|
|
|
if globalValue = @getRawValue(keyPath, options)
|
|
result.push(scopeSelector: '*', value: globalValue)
|
|
|
|
result
|
|
|
|
# Essential: Sets the value for a configuration setting.
|
|
#
|
|
# This value is stored in N1's internal configuration file.
|
|
#
|
|
# ### Examples
|
|
#
|
|
# You might want to change the themes programmatically:
|
|
#
|
|
# ```coffee
|
|
# NylasEnv.config.set('core.themes', ['ui-light', 'my-custom-theme'])
|
|
# ```
|
|
#
|
|
# You can also set scoped settings. For example, you might want change the
|
|
# `editor.tabLength` only for ruby files.
|
|
#
|
|
# ```coffee
|
|
# NylasEnv.config.get('editor.tabLength') # => 4
|
|
# NylasEnv.config.get('editor.tabLength', scope: ['source.ruby']) # => 4
|
|
# NylasEnv.config.get('editor.tabLength', scope: ['source.js']) # => 4
|
|
#
|
|
# # Set ruby to 2
|
|
# NylasEnv.config.set('editor.tabLength', 2, scopeSelector: 'source.ruby') # => true
|
|
#
|
|
# # Notice it's only set to 2 in the case of ruby
|
|
# NylasEnv.config.get('editor.tabLength') # => 4
|
|
# NylasEnv.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
|
|
# NylasEnv.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'
|
|
# * `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: ->
|
|
if arguments[0]?[0] is '.'
|
|
Grim.deprecate """
|
|
Passing a scope selector as the first argument to Config::set is deprecated.
|
|
Pass a `scopeSelector` in an options hash as the final argument instead.
|
|
"""
|
|
[scopeSelector, keyPath, value] = arguments
|
|
shouldSave = true
|
|
else
|
|
[keyPath, value, options] = arguments
|
|
scopeSelector = options?.scopeSelector
|
|
source = options?.source
|
|
shouldSave = options?.save ? true
|
|
|
|
if source and not scopeSelector
|
|
throw new Error("::set with a 'source' and no 'sourceSelector' is not yet implemented!")
|
|
|
|
source ?= @getUserConfigPath()
|
|
|
|
unless value is undefined
|
|
try
|
|
value = @makeValueConformToSchema(keyPath, value)
|
|
catch e
|
|
return false
|
|
|
|
if scopeSelector?
|
|
@setRawScopedValue(keyPath, value, source, scopeSelector)
|
|
else
|
|
@setRawValue(keyPath, value)
|
|
|
|
@requestSave() if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors
|
|
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 typeof options is 'string'
|
|
Grim.deprecate """
|
|
Passing a scope selector as the first argument to Config::unset is deprecated.
|
|
Pass a `scopeSelector` in an options hash as the second argument instead.
|
|
"""
|
|
scopeSelector = keyPath
|
|
keyPath = options
|
|
else
|
|
{scopeSelector, source} = options ? {}
|
|
|
|
source ?= @getUserConfigPath()
|
|
|
|
if scopeSelector?
|
|
if keyPath?
|
|
settings = @scopedSettingsStore.propertiesForSourceAndSelector(source, scopeSelector)
|
|
if _.valueForKeyPath(settings, keyPath)?
|
|
@scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector)
|
|
_.setValueForKeyPath(settings, keyPath, undefined)
|
|
settings = withoutEmptyObjects(settings)
|
|
@set(null, settings, {scopeSelector, source, priority: @priorityForSource(source)}) if settings?
|
|
@requestSave()
|
|
else
|
|
@scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector)
|
|
@emitChangeEvent()
|
|
else
|
|
for scopeSelector of @scopedSettingsStore.propertiesForSource(source)
|
|
@unset(keyPath, {scopeSelector, source})
|
|
if keyPath? and source is @getUserConfigPath()
|
|
@set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))
|
|
|
|
# Extended: Get an {Array} of all of the `source` {String}s with which
|
|
# settings have been added via {::set}.
|
|
getSources: ->
|
|
_.uniq(_.pluck(@scopedSettingsStore.propertySets, 'source')).sort()
|
|
|
|
# Deprecated: Restore the global setting at `keyPath` to its default value.
|
|
#
|
|
# Returns the new value.
|
|
restoreDefault: (scopeSelector, keyPath) ->
|
|
Grim.deprecate("Use ::unset instead.")
|
|
@unset(scopeSelector, keyPath)
|
|
@get(keyPath)
|
|
|
|
# Deprecated: Get the global default value of the key path. _Please note_ that in most
|
|
# cases calling this is not necessary! {::get} returns the default value when
|
|
# a custom value is not specified.
|
|
#
|
|
# * `scopeSelector` (optional) {String}. eg. '.source.ruby'
|
|
# * `keyPath` The {String} name of the key.
|
|
#
|
|
# Returns the default value.
|
|
getDefault: ->
|
|
Grim.deprecate("Use `::get(keyPath, {scope, excludeSources: [NylasEnv.config.getUserConfigPath()]})` instead")
|
|
if arguments.length is 1
|
|
[keyPath] = arguments
|
|
else
|
|
[scopeSelector, keyPath] = arguments
|
|
scope = [scopeSelector]
|
|
@get(keyPath, {scope, excludeSources: [@getUserConfigPath()]})
|
|
|
|
# Deprecated: Is the value at `keyPath` its default value?
|
|
#
|
|
# * `scopeSelector` (optional) {String}. eg. '.source.ruby'
|
|
# * `keyPath` The {String} name of the key.
|
|
#
|
|
# Returns a {Boolean}, `true` if the current value is the default, `false`
|
|
# otherwise.
|
|
isDefault: ->
|
|
Grim.deprecate("Use `not ::get(keyPath, {scope, sources: [NylasEnv.config.getUserConfigPath()]})?` instead")
|
|
if arguments.length is 1
|
|
[keyPath] = arguments
|
|
else
|
|
[scopeSelector, keyPath] = arguments
|
|
scope = [scopeSelector]
|
|
not @get(keyPath, {scope, sources: [@getUserConfigPath()]})?
|
|
|
|
# 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.
|
|
getSchema: (keyPath) ->
|
|
keys = splitKeyPath(keyPath)
|
|
schema = @schema
|
|
for key in keys
|
|
break unless schema?
|
|
schema = schema.properties?[key]
|
|
schema
|
|
|
|
# Deprecated: Returns a new {Object} containing all of the global settings and
|
|
# defaults. Returns the scoped settings when a `scopeSelector` is specified.
|
|
getSettings: ->
|
|
Grim.deprecate "Use ::get(keyPath) instead"
|
|
_.deepExtend({}, @settings, @defaultSettings)
|
|
|
|
# Extended: Get the {String} path to the config file being used.
|
|
getUserConfigPath: ->
|
|
@configFilePath
|
|
|
|
# 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) ->
|
|
@transactDepth++
|
|
try
|
|
callback()
|
|
finally
|
|
@transactDepth--
|
|
@emitChangeEvent()
|
|
|
|
###
|
|
Section: Internal methods used by core
|
|
###
|
|
|
|
pushAtKeyPath: (keyPath, value) ->
|
|
arrayValue = @get(keyPath) ? []
|
|
result = arrayValue.push(value)
|
|
@set(keyPath, arrayValue)
|
|
result
|
|
|
|
unshiftAtKeyPath: (keyPath, value) ->
|
|
arrayValue = @get(keyPath) ? []
|
|
result = arrayValue.unshift(value)
|
|
@set(keyPath, arrayValue)
|
|
result
|
|
|
|
removeAtKeyPath: (keyPath, value) ->
|
|
arrayValue = @get(keyPath) ? []
|
|
result = _.remove(arrayValue, value)
|
|
@set(keyPath, arrayValue)
|
|
result
|
|
|
|
setSchema: (keyPath, schema) ->
|
|
unless isPlainObject(schema)
|
|
throw new Error("Error loading schema for #{keyPath}: schemas can only be objects!")
|
|
|
|
unless typeof schema.type?
|
|
throw new Error("Error loading schema for #{keyPath}: schema objects must have a type attribute")
|
|
|
|
rootSchema = @schema
|
|
if keyPath
|
|
for key in splitKeyPath(keyPath)
|
|
rootSchema.type = 'object'
|
|
rootSchema.properties ?= {}
|
|
properties = rootSchema.properties
|
|
properties[key] ?= {}
|
|
rootSchema = properties[key]
|
|
|
|
_.extend rootSchema, schema
|
|
@setDefaults(keyPath, @extractDefaultsFromSchema(schema))
|
|
@setScopedDefaultsFromSchema(keyPath, schema)
|
|
@resetSettingsForSchemaChange()
|
|
|
|
load: ->
|
|
@initializeConfigDirectory()
|
|
@loadUserConfig()
|
|
@observeUserConfig()
|
|
|
|
###
|
|
Section: Private methods managing the user's config file
|
|
###
|
|
|
|
initializeConfigDirectory: (done) ->
|
|
return if fs.existsSync(@configDirPath)
|
|
fs.makeTreeSync(@configDirPath)
|
|
templateConfigDirPath = fs.resolve(@resourcePath, 'dot-nylas')
|
|
fs.copySync(templateConfigDirPath, @configDirPath)
|
|
|
|
loadUserConfig: ->
|
|
manager = app.sharedFileManager
|
|
if not manager.processCanReadFile(@configFilePath)
|
|
@requestLoad()
|
|
return
|
|
|
|
unless fs.existsSync(@configFilePath)
|
|
fs.makeTreeSync(path.dirname(@configFilePath))
|
|
CSON.writeFileSync(@configFilePath, {})
|
|
|
|
try
|
|
unless @savePending
|
|
userConfig = CSON.readFileSync(@configFilePath)
|
|
@resetUserSettings(userConfig)
|
|
@configFileHasErrors = false
|
|
catch error
|
|
@configFileHasErrors = true
|
|
message = "Failed to load `#{path.basename(@configFilePath)}`"
|
|
|
|
detail = if error.location?
|
|
# stack is the output from CSON in this case
|
|
error.stack
|
|
else
|
|
# message will be EACCES permission denied, et al
|
|
error.message
|
|
|
|
@notifyFailure(message, detail)
|
|
|
|
observeUserConfig: ->
|
|
try
|
|
@watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) =>
|
|
@requestLoad() if eventType is 'change' and @watchSubscription?
|
|
catch error
|
|
@notifyFailure """
|
|
Unable to watch path: `#{path.basename(@configFilePath)}`. Make sure you have permissions to
|
|
`#{@configFilePath}`. On linux there are currently problems with watch
|
|
sizes.
|
|
"""
|
|
|
|
unobserveUserConfig: ->
|
|
@watchSubscription?.close()
|
|
@watchSubscription = null
|
|
|
|
notifyFailure: (errorMessage, detail) ->
|
|
console.log(errorMessage, detail)
|
|
|
|
save: ->
|
|
manager = app.sharedFileManager
|
|
manager.processWillWriteFile(@configFilePath)
|
|
allSettings = {'*': @settings}
|
|
allSettings = _.extend allSettings, @scopedSettingsStore.propertiesForSource(@getUserConfigPath())
|
|
CSON.writeFileSync(@configFilePath, allSettings)
|
|
manager.processDidWriteFile(@configFilePath)
|
|
|
|
###
|
|
Section: Private methods managing global settings
|
|
###
|
|
|
|
resetUserSettings: (newSettings) ->
|
|
|
|
unless isPlainObject(newSettings)
|
|
@settings = {}
|
|
@emitChangeEvent()
|
|
return
|
|
|
|
if newSettings.global?
|
|
newSettings['*'] = newSettings.global
|
|
delete newSettings.global
|
|
|
|
if newSettings['*']?
|
|
scopedSettings = newSettings
|
|
newSettings = newSettings['*']
|
|
delete scopedSettings['*']
|
|
@resetUserScopedSettings(scopedSettings)
|
|
|
|
@transact =>
|
|
@settings = {}
|
|
@set(key, value, save: false) for key, value of newSettings
|
|
|
|
getRawValue: (keyPath, options) ->
|
|
unless options?.excludeSources?.indexOf(@getUserConfigPath()) >= 0
|
|
value = _.valueForKeyPath(@settings, keyPath)
|
|
unless options?.sources?.length > 0
|
|
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
|
|
|
|
if value?
|
|
value = @deepClone(value)
|
|
_.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)
|
|
else
|
|
value = @deepClone(defaultValue)
|
|
|
|
value
|
|
|
|
setRawValue: (keyPath, value) ->
|
|
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
|
|
value = undefined if _.isEqual(defaultValue, value)
|
|
|
|
if keyPath?
|
|
_.setValueForKeyPath(@settings, keyPath, value)
|
|
else
|
|
@settings = value
|
|
@emitChangeEvent()
|
|
|
|
observeKeyPath: (keyPath, options, callback) ->
|
|
callback(@get(keyPath))
|
|
@onDidChangeKeyPath keyPath, (event) -> callback(event.newValue)
|
|
|
|
onDidChangeKeyPath: (keyPath, callback) ->
|
|
oldValue = @get(keyPath)
|
|
@emitter.on 'did-change', =>
|
|
newValue = @get(keyPath)
|
|
unless _.isEqual(oldValue, newValue)
|
|
event = {oldValue, newValue}
|
|
oldValue = newValue
|
|
callback(event)
|
|
|
|
isSubKeyPath: (keyPath, subKeyPath) ->
|
|
return false unless keyPath? and subKeyPath?
|
|
pathSubTokens = splitKeyPath(subKeyPath)
|
|
pathTokens = splitKeyPath(keyPath).slice(0, pathSubTokens.length)
|
|
_.isEqual(pathTokens, pathSubTokens)
|
|
|
|
setRawDefault: (keyPath, value) ->
|
|
_.setValueForKeyPath(@defaultSettings, keyPath, value)
|
|
@emitChangeEvent()
|
|
|
|
setDefaults: (keyPath, defaults) ->
|
|
if defaults? and isPlainObject(defaults)
|
|
keys = splitKeyPath(keyPath)
|
|
for key, childValue of defaults
|
|
continue unless defaults.hasOwnProperty(key)
|
|
@setDefaults(keys.concat([key]).join('.'), childValue)
|
|
else
|
|
try
|
|
defaults = @makeValueConformToSchema(keyPath, defaults)
|
|
@setRawDefault(keyPath, defaults)
|
|
catch e
|
|
console.warn("'#{keyPath}' could not set the default. Attempted default: #{JSON.stringify(defaults)}; Schema: #{JSON.stringify(@getSchema(keyPath))}")
|
|
|
|
deepClone: (object) ->
|
|
if object instanceof Color
|
|
object.clone()
|
|
else if _.isArray(object)
|
|
object.map (value) => @deepClone(value)
|
|
else if isPlainObject(object)
|
|
_.mapObject object, (key, value) => [key, @deepClone(value)]
|
|
else
|
|
object
|
|
|
|
# `schema` will look something like this
|
|
#
|
|
# ```coffee
|
|
# type: 'string'
|
|
# default: 'ok'
|
|
# scopes:
|
|
# '.source.js':
|
|
# default: 'omg'
|
|
# ```
|
|
setScopedDefaultsFromSchema: (keyPath, schema) ->
|
|
if schema.scopes? and isPlainObject(schema.scopes)
|
|
scopedDefaults = {}
|
|
for scope, scopeSchema of schema.scopes
|
|
continue unless scopeSchema.hasOwnProperty('default')
|
|
scopedDefaults[scope] = {}
|
|
_.setValueForKeyPath(scopedDefaults[scope], keyPath, scopeSchema.default)
|
|
@scopedSettingsStore.addProperties('schema-default', scopedDefaults)
|
|
|
|
if schema.type is 'object' and schema.properties? and isPlainObject(schema.properties)
|
|
keys = splitKeyPath(keyPath)
|
|
for key, childValue of schema.properties
|
|
continue unless schema.properties.hasOwnProperty(key)
|
|
@setScopedDefaultsFromSchema(keys.concat([key]).join('.'), childValue)
|
|
|
|
return
|
|
|
|
extractDefaultsFromSchema: (schema) ->
|
|
if schema.default?
|
|
schema.default
|
|
else if schema.type is 'object' and schema.properties? and isPlainObject(schema.properties)
|
|
defaults = {}
|
|
properties = schema.properties or {}
|
|
defaults[key] = @extractDefaultsFromSchema(value) for key, value of properties
|
|
defaults
|
|
|
|
makeValueConformToSchema: (keyPath, value, options) ->
|
|
if options?.suppressException
|
|
try
|
|
@makeValueConformToSchema(keyPath, value)
|
|
catch e
|
|
undefined
|
|
else
|
|
value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath)
|
|
value
|
|
|
|
# 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=@getUserConfigPath()) ->
|
|
@transact =>
|
|
@settings = @makeValueConformToSchema(null, @settings, suppressException: true)
|
|
priority = @priorityForSource(source)
|
|
selectorsAndSettings = @scopedSettingsStore.propertiesForSource(source)
|
|
@scopedSettingsStore.removePropertiesForSource(source)
|
|
for scopeSelector, settings of selectorsAndSettings
|
|
settings = @makeValueConformToSchema(null, settings, suppressException: true)
|
|
@setRawScopedValue(null, settings, source, scopeSelector)
|
|
return
|
|
|
|
###
|
|
Section: Private Scoped Settings
|
|
###
|
|
|
|
priorityForSource: (source) ->
|
|
if source is @getUserConfigPath()
|
|
1000
|
|
else
|
|
0
|
|
|
|
emitChangeEvent: ->
|
|
@emitter.emit 'did-change' unless @transactDepth > 0
|
|
|
|
resetUserScopedSettings: (newScopedSettings) ->
|
|
source = @getUserConfigPath()
|
|
priority = @priorityForSource(source)
|
|
@scopedSettingsStore.removePropertiesForSource(source)
|
|
|
|
for scopeSelector, settings of newScopedSettings
|
|
settings = @makeValueConformToSchema(null, settings, suppressException: true)
|
|
validatedSettings = {}
|
|
validatedSettings[scopeSelector] = withoutEmptyObjects(settings)
|
|
@scopedSettingsStore.addProperties(source, validatedSettings, {priority}) if validatedSettings[scopeSelector]?
|
|
|
|
@emitChangeEvent()
|
|
|
|
addScopedSettings: (source, selector, value, options) ->
|
|
Grim.deprecate("Use ::set instead")
|
|
settingsBySelector = {}
|
|
settingsBySelector[selector] = value
|
|
disposable = @scopedSettingsStore.addProperties(source, settingsBySelector, options)
|
|
@emitChangeEvent()
|
|
new Disposable =>
|
|
disposable.dispose()
|
|
@emitChangeEvent()
|
|
|
|
setRawScopedValue: (keyPath, value, source, selector, options) ->
|
|
if keyPath?
|
|
newValue = {}
|
|
_.setValueForKeyPath(newValue, keyPath, value)
|
|
value = newValue
|
|
|
|
settingsBySelector = {}
|
|
settingsBySelector[selector] = value
|
|
@scopedSettingsStore.addProperties(source, settingsBySelector, priority: @priorityForSource(source))
|
|
@emitChangeEvent()
|
|
|
|
getRawScopedValue: (scopeDescriptor, keyPath, options) ->
|
|
scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
|
|
@scopedSettingsStore.getPropertyValue(scopeDescriptor.getScopeChain(), keyPath, options)
|
|
|
|
observeScopedKeyPath: (scope, keyPath, callback) ->
|
|
callback(@get(keyPath, {scope}))
|
|
@onDidChangeScopedKeyPath scope, keyPath, (event) -> callback(event.newValue)
|
|
|
|
onDidChangeScopedKeyPath: (scope, keyPath, callback) ->
|
|
oldValue = @get(keyPath, {scope})
|
|
@emitter.on 'did-change', =>
|
|
newValue = @get(keyPath, {scope})
|
|
unless _.isEqual(oldValue, newValue)
|
|
event = {oldValue, newValue}
|
|
oldValue = newValue
|
|
callback(event)
|
|
|
|
settingsForScopeDescriptor: (scopeDescriptor, keyPath) ->
|
|
Grim.deprecate("Use Config::getAll instead")
|
|
entries = @getAll(null, scope: scopeDescriptor)
|
|
value for {value} in entries when _.valueForKeyPath(value, keyPath)?
|
|
|
|
# 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
|
|
'integer':
|
|
coerce: (keyPath, value, schema) ->
|
|
value = parseInt(value)
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) or not isFinite(value)
|
|
value
|
|
|
|
'number':
|
|
coerce: (keyPath, value, schema) ->
|
|
value = parseFloat(value)
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) or not isFinite(value)
|
|
value
|
|
|
|
'boolean':
|
|
coerce: (keyPath, value, schema) ->
|
|
switch typeof value
|
|
when 'string'
|
|
if value.toLowerCase() is 'true'
|
|
true
|
|
else if value.toLowerCase() is 'false'
|
|
false
|
|
else
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'")
|
|
when 'boolean'
|
|
value
|
|
else
|
|
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) ->
|
|
unless typeof value is 'string'
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string")
|
|
value
|
|
|
|
'null':
|
|
# null sort of isnt supported. It will just unset in this case
|
|
coerce: (keyPath, value, schema) ->
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be null") unless value in [undefined, null]
|
|
value
|
|
|
|
'object':
|
|
coerce: (keyPath, value, schema) ->
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value)
|
|
return value unless schema.properties?
|
|
|
|
newValue = {}
|
|
for prop, propValue of value
|
|
childSchema = schema.properties[prop]
|
|
if childSchema?
|
|
try
|
|
newValue[prop] = @executeSchemaEnforcers("#{keyPath}.#{prop}", propValue, childSchema)
|
|
catch error
|
|
console.warn "Error setting item in object: #{error.message}"
|
|
else
|
|
# Just pass through un-schema'd values
|
|
newValue[prop] = propValue
|
|
|
|
newValue
|
|
|
|
'array':
|
|
coerce: (keyPath, value, schema) ->
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an array") unless Array.isArray(value)
|
|
itemSchema = schema.items
|
|
if itemSchema?
|
|
newValue = []
|
|
for item in value
|
|
try
|
|
newValue.push @executeSchemaEnforcers(keyPath, item, itemSchema)
|
|
catch error
|
|
console.warn "Error setting item in array: #{error.message}"
|
|
newValue
|
|
else
|
|
value
|
|
|
|
'color':
|
|
coerce: (keyPath, value, schema) ->
|
|
color = Color.parse(value)
|
|
unless color?
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a color")
|
|
color
|
|
|
|
'*':
|
|
coerceMinimumAndMaximum: (keyPath, value, schema) ->
|
|
return value unless typeof value is 'number'
|
|
if schema.minimum? and typeof schema.minimum is 'number'
|
|
value = Math.max(value, schema.minimum)
|
|
if schema.maximum? and typeof schema.maximum is 'number'
|
|
value = Math.min(value, schema.maximum)
|
|
value
|
|
|
|
validateEnum: (keyPath, value, schema) ->
|
|
possibleValues = schema.enum
|
|
return value unless possibleValues? and Array.isArray(possibleValues) and possibleValues.length
|
|
|
|
for possibleValue in possibleValues
|
|
# Using `isEqual` for possibility of placing enums on array and object schemas
|
|
return value if _.isEqual(possibleValue, value)
|
|
|
|
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} is not one of #{JSON.stringify(possibleValues)}")
|
|
|
|
isPlainObject = (value) ->
|
|
_.isObject(value) and not _.isArray(value) and not _.isFunction(value) and not _.isString(value) and not (value instanceof Color)
|
|
|
|
splitKeyPath = (keyPath) ->
|
|
return [] unless keyPath?
|
|
startIndex = 0
|
|
keyPathArray = []
|
|
for char, i in keyPath
|
|
if char is '.' and (i is 0 or keyPath[i-1] != '\\')
|
|
keyPathArray.push keyPath.substring(startIndex, i)
|
|
startIndex = i + 1
|
|
keyPathArray.push keyPath.substr(startIndex, keyPath.length)
|
|
keyPathArray
|
|
|
|
withoutEmptyObjects = (object) ->
|
|
resultObject = undefined
|
|
if isPlainObject(object)
|
|
for key, value of object
|
|
newValue = withoutEmptyObjects(value)
|
|
if newValue?
|
|
resultObject ?= {}
|
|
resultObject[key] = newValue
|
|
else
|
|
resultObject = object
|
|
resultObject
|