howl.config

before_each ->
  config.reset!
  app.editor = nil

reset clears all set values, but keeps the definitions

config.define name: 'var', description: 'test'
config.define name: 'var2', description: 'test'
config.set 'var', 'set'
config.set_default 'var2', 'set-default'

config.reset!

assert.is_not_nil config.definitions['var']
assert.is_nil config.get 'var'
assert.is_nil config.get 'var2'

global variables can be set and get directly on config

config.define name: 'direct', description: 'test', default: 123
assert.equal config.direct, 123
config.direct = 'bar'
assert.equal config.direct, 'bar'
assert.equal config.get('direct'), 'bar'

define(options)

raises an error if name is missing

assert.raises 'name', -> config.define {}

raises an error if the description option is missing

assert.raises 'description', -> config.define name: 'foo'

.definitions

is a table of the current definitions, keyed by name

var = name: 'foo', description: 'foo variable'
config.define var
assert.equal type(config.definitions), 'table'
assert.same var, config.definitions.foo

writing directly to it raises an error

assert.has_error -> config.definitions.frob = 'crazy'

set(name, value)

sets <name> globally to <value>

var = name: 'foo', description: 'foo variable'
config.define var
config.set 'foo', 2
assert.equal config.get('foo'), 2

an error is raised if <name> is not defined

assert.raises 'Undefined', -> config.set 'que', 'si'

setting a value of nil clears the value

var = name: 'foo', description: 'foo variable'
config.define var
config.set 'foo', 'bar'
config.set 'foo', nil
assert.is_nil config.foo

get(name, scope, layer)

before_each -> config.define name: 'var', description: 'test variable'

returns the global value of <name>

config.set 'var', 'hello'
assert.equal config.get('var'), 'hello'

(scopes)

get() returns value set for specific scope

config.set 'var', 'global-value'
config.set 'var', 'scope-value', 'scope1'
assert.equal 'scope-value', config.get 'var', 'scope1'

get() looks up the value set() in parent scopes if necessary

config.set 'var', 'global-value'
config.set 'var', 'top-value', 'root'
config.set 'var', 'scope-value', 'root/scope1'
assert.equal 'scope-value', config.get 'var', 'root/scope1'
assert.equal 'top-value', config.get 'var', 'root/scope2'
assert.equal 'global-value', config.get 'var', 'scope3'

(layers)

before_each ->
  config.define name: 'var', description: 'test variable'
  config.define_layer 'layer1'
  config.define_layer 'layer2'

set() errors for undefined layer

assert.raises 'Unknown', -> config.set 'var', 'value', '', 'no-such-layer'

get() looks up specified layer before default

config.set 'var', 'layer1-value', '', 'layer1'
config.set 'var', 'default-value', ''
assert.equal 'layer1-value', config.get 'var', '', 'layer1'
assert.equal 'default-value', config.get 'var', '', 'layer2'

layers are looked up at each scope

config.set 'var', 'layer1-top', '', 'layer1'
config.set 'var', 'layer2-top', '', 'layer2'
config.set 'var', 'default-top', ''
config.set 'var', 'layer1-scope', 'scope1', 'layer1'

assert.equal 'layer1-scope', config.get 'var', 'scope1', 'layer1'
assert.equal 'layer2-top', config.get 'var', 'scope1', 'layer2'

(when a default is provided in the definition)

before_each -> config.define name: 'with_default', description: 'test', default: 123

the default value is returned if no value has been set

assert.equal config.get('with_default'), 123

if a value has been set it takes precedence

config.set 'with_default', 'foo'
assert.equal config.get('with_default'), 'foo'

(when a default has been set by set_default)

before_each ->
  config.define name: 'var', description: 'test variable', default: 'def-default'
  config.define_layer 'layer1'
  config.define_layer 'layer2'

set_default value takes precedence over definition default

config.set_default 'var', 'set-default', 'layer1'
assert.equal 'set-default', config.get 'var', 'scope1', 'layer1'
assert.equal 'def-default', config.get 'var', 'scope1'

set_default for a layer takes precedence over global config value

config.set_default 'var', 'set-default', 'layer1'
config.var = 'global-value'
assert.equal 'set-default', config.get 'var', 'scope1', 'layer1'
assert.equal 'global-value', config.get 'var', 'scope1'

set_default value is not persisted

with_tmpdir (dir) ->
  config.set_default 'var', 'set-default', 'layer1'
  config.save_config dir
  config.load_config true, dir
  assert.equal 'set-default', config.get 'var', 'scope1', 'layer1'
  assert.equal 'def-default', config.get 'var', 'scope1'

(when a validate function is provided)

is called with the value to be set whenever the variable is set

validate = spy.new -> true
config.define name: 'validated', description: 'test', :validate
config.set 'validated', 'my_value'
assert.spy(validate).was_called_with 'my_value'

an error is raised if the function returns false for to-be set value

config.define name: 'validated', description: 'test', validate: -> false
assert.error -> config.set 'validated', 'foo'
config.define name: 'validated', description: 'test', validate: -> nil
assert.no_error -> config.set 'validated', 'foo'
config.define name: 'validated', description: 'test', validate: -> true
assert.no_error -> config.set 'validated', 'foo'

an error is not raised if the function returns truish for to-be set value

config.define name: 'validated', description: 'test', validate: -> true
config.set 'validated', 'foo'
assert.equal config.get('validated'), 'foo'
config.define name: 'validated', description: 'test', validate: -> 2
config.set 'validated', 'foo2'
assert.equal config.get('validated'), 'foo2'

the function is not called when clearing a value by setting it to nil

validate = Spy!
config.define name: 'validated', description: 'test', :validate
config.set 'validated', nil
assert.is_false validate.called

(when a convert function is provided)

is called with the value to be set and the return value is used instead

config.define name: 'converted', description: 'test', convert: -> 'wanted'
config.set 'converted', 'requested'
assert.equal config.converted, 'wanted'

(when options is provided)

before_each ->
  config.define
    name: 'with_options'
    description: 'test'
    options: { 'one', 'two' }

an error is raised if the to-be set value is not a valid option

assert.raises 'option', -> config.set 'with_options', 'three'

an error is not raised if the to-be set value is a valid option

config.set 'with_options', 'one'

options can be a function returning a table

config.define name: 'with_options_func', description: 'test', options: ->
  { 'one', 'two' }

assert.raises 'option', -> config.set 'with_options_func', 'three'
config.set 'with_options_func', 'one'

options can be a table of tables containg values and descriptions

options = {
  { 'one', 'description for one' }
  { 'two', 'description for two' }
}

config.define name: 'with_options_desc', description: 'test', :options

assert.raises 'option', -> config.set 'with_options_desc', 'three'
config.set 'with_options_desc', 'one'

(when scope is provided)

raises an error if it is not "local" or "global"

assert.raises 'scope', -> config.define name: 'bla', description: 'foo', scope: 'blarg'

(.. with local scope for a variable)

an error is raised when trying to set the global value of the variable

config.define name: 'local', description: 'test', scope: 'local'
assert.error -> config.set 'local', 'foo'

(when type_of is provided)

raises an error if the type is not recognized

assert.raises 'type', -> config.define name: 'bla', description: 'foo', type_of: 'blarg'

(.. and is "boolean")

def = nil
before_each ->
  config.define name: 'bool', description: 'foo', type_of: 'boolean'
  def = config.definitions.bool

options are {true, false}

assert.same def.options, { true, false }

convert handles boolean types and "true" and "false"

assert.equal def.convert(true), true
assert.equal def.convert(false), false
assert.equal def.convert('true'), true
assert.equal def.convert('false'), false
assert.equal def.convert('blargh'), 'blargh'

converts to boolean upon assignment

config.bool = 'false'
assert.equal config.bool, false

(.. and is "number")

def = nil
before_each ->
  config.define name: 'number', description: 'foo', type_of: 'number'
  def = config.definitions.number

convert handles numbers and string numbers

assert.equal def.convert(1), 1
assert.equal def.convert('1'), 1
assert.equal def.convert(0.5), 0.5
assert.equal def.convert('0.5'), 0.5
assert.equal def.convert('blargh'), 'blargh'

validate returns true for numbers only

assert.is_true def.validate 1
assert.is_true def.validate 1.2
assert.is_false def.validate '1'
assert.is_false def.validate 'blargh'

converts to number upon assignment

config.number = '1'
assert.equal config.number, 1

(.. and is "positive_number")

def = nil
before_each ->
  config.define name: 'positive_number', description: 'foo', type_of: 'positive_number'
  def = config.definitions.positive_number

convert handles numbers and string numbers

assert.equal def.convert(1), 1
assert.equal def.convert('1'), 1
assert.equal def.convert(0.5), 0.5
assert.equal def.convert('0.5'), 0.5
assert.equal def.convert('blargh'), 'blargh'

validate returns true for positive numbers only

assert.is_true def.validate 1
assert.is_true def.validate 1.2
assert.is_false def.validate -1
assert.is_false def.validate -1.2
assert.is_false def.validate '1'
assert.is_false def.validate 'blargh'

converts to number upon assignment

config.number = '1'
assert.equal config.number, 1

(.. and is "string_list")

def = nil
before_each ->
  config.define name: 'string_list', description: 'foo', type_of: 'string_list'
  def = config.definitions.string_list

validate returns true for table values

assert.is_true def.validate {}
assert.is_false def.validate '1'
assert.is_false def.validate 23

convert

leaves string tables alone

orig = { 'hi', 'there' }
assert.same orig, def.convert orig

converts values in other tables as necessary

orig = { 1, 2 }
assert.same { '1', '2', }, def.convert orig

converts simple values into a table

assert.same { '1' }, def.convert '1'
assert.same { '1' }, def.convert 1

converts a blank string into an empty table

assert.same {}, def.convert ''
assert.same {}, def.convert '  '

converts a comma separated string into a list of values

assert.same { '1', '2' }, def.convert '1,2'
assert.same { '1', '2' }, def.convert ' 1 ,   2 '

(watching)

before_each -> config.define name: 'trigger', description: 'watchable'

watch(name, function) register a watcher for <name>

assert.not_error -> config.watch 'foo', -> true

set invokes watchers with <name>, <value> and false

callback = Spy!
config.watch 'trigger', callback
config.set 'trigger', 'value'
assert.same callback.called_with, { 'trigger', 'value', false }

define(..) invokes watchers with <name>, <default-value> and false

callback = spy.new ->
config.watch 'undefined', callback
config.define name: 'undefined', description: 'springs into life', default: 123
assert.spy(callback).was_called_with 'undefined', 123, false

(.. when a callback raises an error)

before_each -> config.watch 'trigger', -> error 'oh noes'

other callbacks are still invoked

callback = Spy!
config.watch 'trigger', callback
config.set 'trigger', 'value'
assert.is_true callback.called

an error is logged

config.set 'trigger', 'value'
assert.match log.last_error.message, 'watcher'

proxy

config.define
  name: 'my_var'
  description: 'base'
  type_of: 'number'

local proxy_inner, proxy_outer

before_each ->
  config.my_var = 123
  proxy_inner = config.proxy '/outer/inner'
  proxy_outer = config.proxy '/outer'

returns a table with access to all previously defined variables

assert.equal 123, proxy_outer.my_var
assert.equal 123, proxy_inner.my_var

changing a variable changes it locally only

proxy_inner.my_var = 321
assert.equal 321, proxy_inner.my_var
assert.equal 123, proxy_outer.my_var
assert.equal 123, config.my_var

assignments are still validated and converted as usual

assert.has_error -> proxy_inner.my_var = 'not a number'
proxy_inner.my_var = '111'
assert.equal 111, proxy_inner.my_var

an error is raised if trying to set a variable with global scope

config.define name: 'global', description: 'global', scope: 'global'
assert.has_error -> proxy_inner.global = 'illegal'

an error is raised if the variable is not defined

assert.raises 'Undefined', -> proxy_inner.que = 'si'

setting a value to nil clears the value

proxy_inner.my_var = 666
proxy_inner.my_var = nil
assert.equal 123, proxy_inner.my_var

setting a variable via a proxy invokes watchers with <name>, <value> and true

callback = spy.new ->
config.watch 'my_var', callback
proxy_inner.my_var = 333
assert.spy(callback).was_called_with 'my_var', 333, true

(chaining)

resolution automatically walks up scope hierarchy

proxy_inner.my_var = 1
proxy_outer.my_var = 2

assert.same 1, proxy_inner.my_var
assert.same 2, proxy_outer.my_var

proxy_inner.my_var = nil
assert.same 2, proxy_inner.my_var

proxy_outer.my_var = nil
assert.same 123, proxy_inner.my_var

(.. when layers are specified)

local proxy_inner_layered, proxy_outer_layered, proxy_inner_mixed, proxy_inner_sub

before_each ->
  config.define_layer 'layer:one'
  config.define_layer 'layer:sub', parent: 'layer:one'
  proxy_inner_layered = config.proxy '/outer/inner', 'layer:one'
  proxy_outer_layered = config.proxy '/outer', 'layer:one'
  proxy_inner_mixed = config.proxy '/outer/inner', 'default', 'layer:one'
  proxy_inner_sub = config.proxy '/outer/inner', 'layer:sub'

layer is checked before default values at each scope

proxy_inner_layered.my_var = 4
proxy_inner.my_var = 3
proxy_outer_layered.my_var = 2
proxy_outer.my_var = 1

assert.same 4, proxy_inner_layered.my_var

proxy_inner_layered.my_var = nil
assert.same 3, proxy_inner_layered.my_var

proxy_inner.my_var = nil
assert.same 2, proxy_inner_layered.my_var

read for a layer auto delegates up to parent layer

proxy_inner_layered.my_var = 5
assert.same 5, proxy_inner_sub.my_var

read layer can be different from write layer

proxy_inner_layered.my_var = 4
proxy_inner_mixed.my_var = 3

assert.same 4, proxy_inner_mixed.my_var

(replace())

before_each ->
  config.define_layer 'layer1'
  config.define_layer 'layer2'
  config.define name: 'name1', description: 'description'
  config.define name: 'name2', description: 'description'

clobbers new scope and deep copies all values from scope to new scope

config.set 'name1', 'value1', 'here', 'layer1'
config.set 'name2', 'value2', 'here', 'layer2'

assert.is_nil config.get 'name1', 'there', 'layer1'
assert.is_nil config.get 'name2', 'there', 'layer2'

config.replace 'here', 'there'

assert.same 'value1', config.get 'name1', 'there', 'layer1'
assert.same 'value2', config.get 'name2', 'there', 'layer2'


config.set 'name1', 'value1-new', 'here', 'layer1'
assert.same 'value1', config.get 'name1', 'there', 'layer1'

(merge())

before_each ->
  config.define_layer 'layer1'
  config.define_layer 'layer2'
  config.define name: 'name1', description: 'description'
  config.define name: 'name2', description: 'description'
  config.define name: 'name3', description: 'description'

deep copies values from scope to new scope, preserves other values in old scope

config.set 'name1', 'value1', 'here', 'layer1'
config.set 'name2', 'value2', 'here', 'layer2'
config.set 'name1', 'there-value1', 'there', 'layer1'
config.set 'name3', 'there-value3', 'there', 'layer2'

assert.same 'there-value1', config.get 'name1', 'there', 'layer1'
assert.same nil, config.get 'name2', 'there', 'layer2'

config.merge 'here', 'there'

assert.same 'value1', config.get 'name1', 'there', 'layer1'
assert.same 'value2', config.get 'name2', 'there', 'layer2'
assert.same 'there-value3', config.get 'name3', 'there', 'layer2'

(delete())

before_each ->
  config.define name: 'name1', description: 'description'

deletes all values at specified scope

config.set 'name1', 'val1', 'scope1'
config.set 'name1', 'top-val'
config.delete 'scope1'
assert.equal 'top-val', config.get 'name1', 'scope1'

errors when trying to delete global scope

assert.raises 'global', -> config.delete ''

(persistence)

before_each ->
  config.define name: 'name1', description: 'description'
  config.define name: 'name2', description: 'description'
  config.define_layer 'layer1'

save_config() saves and load_config() loads the saved config

with_tmpdir (dir) ->
  config.set 'name1', 'value1'
  config.set 'name2', 'value2'
  config.save_config dir

  config.set 'name1', nil
  assert.same nil, config.get 'name1'

  config.load_config true, dir
  assert.same 'value1', config.get 'name1'
  assert.same 'value2', config.get 'name2'

non global scopes are not persisted

with_tmpdir (dir) ->
  config.set 'name1', 'value1', 'scope1'
  config.save_config dir

  config.set 'name1', 'value2', 'scope1'

  config.load_config true, dir
  assert.same nil, config.get 'name1', 'scope1'

does not save values if persist_config is false

with_tmpdir (dir) ->
  config.set 'name1', 'value1'
  config.set 'name2', 'value2'
  config.set 'persist_config', false
  config.save_config dir

  config.set 'name1', nil
  config.set 'name2', nil
  assert.same nil, config.get 'name1'

  config.load_config true, dir
  assert.same nil, config.get 'name1'
  assert.same nil, config.get 'name1', 'scope1'

saves persist_config value

with_tmpdir (dir) ->
  config.set 'name1', 'value1'
  config.set 'persist_config', false
  config.save_config dir

  config.set 'name1', nil
  assert.same nil, config.get 'name1'

  config.load_config true, dir
  assert.same false, config.get 'persist_config'

does not save buffer scopes

with_tmpdir (dir) ->
  config.set 'name1', 'value1-global'
  config.set 'name1', 'value2-buffer', 'buffer/123'
  config.save_config dir
  config.load_config true, dir
  assert.same 'value1-global', config.get 'name1', 'buffer/123'

(for_file)

before_each ->
  config.define name: 'name1', description: ''
  config.define name: 'name2', description: ''
  config.define name: 'name3', description: ''
  config.define_layer 'layer1'
  config.define_layer 'layer2'

returns a proxy that inherits from global

with_tmpdir (dir) ->
  config.set 'name1', 'value1'
  assert.same 'value1', config.for_file(dir .. 'abc').name1

returns the same config for same file path

with_tmpdir (dir) ->
  fileconfig = config.for_file(dir .. 'abc')
  fileconfig.name1 = 'file-value1'
  fileconfig2 =  config.for_file(dir.path .. '/abc')
  assert.same 'file-value1', fileconfig2.name1

constructs scopes that inherit from ancestor file scopes

with_tmpdir (dir) ->
  with config.for_file(dir)
    .name1 = 'dir-value1'
    .name2 = 'dir-value2'
  with config.for_file(dir .. 'abc')
    .name1 = 'file-value1'
    .name3= 'file-value3'
  assert.same 'file-value1', config.for_file(dir .. 'abc').name1
  assert.same 'dir-value2', config.for_file(dir .. 'abc').name2
  assert.same nil, config.for_file(dir).name33
  assert.same nil, config.name3

(for_layer)

creates another proxy for specified layer at same scope

with_tmpdir (dir) ->
  with config.for_file(dir).for_layer('layer1')
    .name1 = 'dir-layer1-value1'
  with config.for_file(dir)
    .name1 = 'dir-value1'

  with config.for_file(dir..'abc').for_layer('layer1')
    .name1 =  'file-layer1-value1'

  assert.same 'file-layer1-value1', config.for_file(dir..'abc').for_layer('layer1').name1
  assert.same 'dir-value1', config.for_file(dir..'abc').name1