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'

saves the global values as JSON in system/config.json, by mode and other layers

config.define_layer 'mode:test'
with_tmpdir (dir) ->
  config.set 'name1', 'value1'
  config.set 'name1', 'value2', '', 'mode:test'
  config.set 'name2', 'value3', '', 'layer1'
  config.save_config dir
  assert.same {
    config: {name1: 'value1'}
    modes: {test: {name1: 'value2'}}
    layers: {layer1: {name2: 'value3'}}
  }, json.decode((dir / 'system/config.json').contents)

  config.reset!
  config.load_config true, dir
  assert.same 'value1', config.get 'name1'
  assert.same 'value2', config.get 'name1', '', 'mode:test'
  assert.same 'value3', config.get 'name2', '', 'layer1'

loads the system/config.lua of older versions, removing it when saving

with_tmpdir (dir) ->
  (dir / 'system')\mkdir!
  legacy = dir / 'system/config.lua'
  legacy.contents = 'return { [""] = { name1 = { default = "old", layer1 = "old-layer" } } }'
  config.load_config true, dir
  assert.same 'old', config.get 'name1'
  assert.same 'old-layer', config.get 'name1', '', 'layer1'

  config.save_config dir
  assert.is_false legacy.exists
  config.load_config true, dir
  assert.same 'old', config.get 'name1'

logs an error for a malformed system/config.json, and refuses to save over it

with_tmpdir (dir) ->
  (dir / 'system')\mkdir!
  file = dir / 'system/config.json'
  file.contents = '{"config": '
  config.load_config true, dir
  assert.includes log.last_error.message, 'Failed to load the configuration'
  assert.includes log.last_error.message, 'parse error'

  config.set 'name1', 'value1'
  assert.raises 'failed to load', -> config.save_config dir
  assert.equal '{"config": ', file.contents

(scopes saved to their own files)

local dir, file
scope = 'file/project'

write = (data) ->
  file.parent\mkdir_p! unless file.parent.exists
  file.contents = type(data) == 'string' and data or json.encode(data)

read = -> json.decode file.contents
save = -> with_tmpdir (sys_dir) -> config.save_config sys_dir

before_each ->
  config.define name: 'name1', description: 'description'
  config.define name: 'name2', description: 'description'
  config.define name: 'num', description: 'description', type_of: 'number'
  config.define name: 'choice', description: 'description', options: {'a', 'b'}
  config.define name: 'global_only', description: 'description', scope: 'global'
  config.define_layer 'mode:test'
  dir = File.tmpdir!
  file = dir / '.howl/config.json'

after_each -> dir\delete_all!

load_scope(scope, file)

sets the values in file for the scope, and its mode layers

write config: {name1: 'project'}, modes: {test: {name1: 'project-mode'}}
config.load_scope scope, file
assert.equal 'project', config.get 'name1', scope
assert.equal 'project-mode', config.get 'name1', scope, 'mode:test'
assert.is_nil config.get 'name1'

does nothing when file does not exist

config.load_scope scope, file
assert.is_nil config.get 'name1', scope

keeps values already set for the scope

config.set 'name1', 'set', scope
write config: {name1: 'file', name2: 'file'}
config.load_scope scope, file
assert.equal 'set', config.get 'name1', scope
assert.equal 'file', config.get 'name2', scope

converts the values of defined variables, skipping invalid ones and logging an error

write config: {num: '3', choice: 'c', name1: 'ok'}
config.load_scope scope, file
assert.equal 3, config.get 'num', scope
assert.is_nil config.get 'choice', scope
assert.equal 'ok', config.get 'name1', scope
assert.equal "#{file}: Illegal option 'c' for 'choice'", log.last_error.message

skips variables that can only be set globally, logging an error

write config: {global_only: 'x'}
config.load_scope scope, file
assert.is_nil config.get 'global_only', scope
assert.includes log.last_error.message, "'global_only' can only be set globally"

keeps values for variables and modes not defined yet

write config: {load_scope_later_var: 'x'}, modes: {load_scope_later: {name1: 'y'}}
config.load_scope scope, file
config.define name: 'load_scope_later_var', description: 'description'
config.define_layer 'mode:load_scope_later'
assert.equal 'x', config.get 'load_scope_later_var', scope
assert.equal 'y', config.get 'name1', scope, 'mode:load_scope_later'

does nothing for a scope already loaded, so its unsaved changes are still saved

write config: {name1: 'file'}
config.load_scope scope, file
config.set 'name1', 'changed', scope
config.load_scope scope, file
save!
assert.same {config: {name1: 'changed'}}, read!

notifies watchers of the loaded values, as local ones

handler = spy.new ->
config.watch 'name2', handler
write config: {name2: 'file'}
config.load_scope scope, file
assert.spy(handler).was_called_with 'name2', 'file', true

logs an error for a malformed file, setting nothing

write '{"config": {"name1": "x"}'
config.load_scope scope, file
assert.includes log.last_error.message, "Failed to load #{file}: parse error"
assert.is_nil config.get 'name1', scope

logs an error for a file that does not hold configuration

write modes: {test: 'x'}
config.load_scope scope, file
assert.equal "Failed to load #{file}: `modes.test` is not an object", log.last_error.message

write {1, 2}
config.load_scope scope, file
assert.equal "Failed to load #{file}: The configuration is not an object", log.last_error.message

reload_scope(scope, file)

replaces the values for the scope with those in file, notifying watchers of changes

write config: {name1: 'a', name2: 'b'}
config.load_scope scope, file
config.set 'name2', 'set', scope, 'mode:test'
handler = spy.new ->
config.watch 'name1', handler
config.watch 'name2', handler

write config: {name2: 'c'}
config.reload_scope scope, file
assert.is_nil config.get 'name1', scope
assert.equal 'c', config.get 'name2', scope
assert.equal 'c', config.get 'name2', scope, 'mode:test'
assert.spy(handler).was_called_with 'name1', nil, true
assert.spy(handler).was_called_with 'name2', 'c', true
assert.spy(handler).was_called_with 'name2', nil, true

keeps the values when file is malformed, but no longer saves them there

write config: {name1: 'a'}
config.load_scope scope, file
write '{'
config.reload_scope scope, file
assert.includes log.last_error.message, "Failed to load #{file}"
assert.equal 'a', config.get 'name1', scope

config.set 'name1', 'b', scope
save!
assert.equal '{', file.contents

save_config()

saves the values for the scope to its file, creating the directory

config.load_scope scope, file
config.set 'name1', 'project', scope
config.set 'name1', 'project-mode', scope, 'mode:test'
save!
assert.same {config: {name1: 'project'}, modes: {test: {name1: 'project-mode'}}}, read!
assert.equal '\n', file.contents\sub(-1)

does not create the file when the scope has no values

config.load_scope scope, file
save!
assert.is_false file.exists

leaves the file alone while its values are unchanged

write '{"config": {"name1": "x"}}'
config.load_scope scope, file
save!
assert.equal '{"config": {"name1": "x"}}', file.contents

config.set 'name2', 'y', scope
save!
assert.same {config: {name1: 'x', name2: 'y'}}, read!

writes the file when its values are all removed

write config: {name1: 'x'}
config.load_scope scope, file
config.set 'name1', nil, scope
save!
assert.same {}, read!

saves only persist_config when that is false for the scope

config.load_scope scope, file
config.set 'name1', 'x', scope
config.set 'persist_config', false, scope
save!
assert.same {config: {persist_config: false}}, read!

does not save the scope after a malformed file failed to load

write '{'
config.load_scope scope, file
config.set 'name1', 'x', scope
save!
assert.equal '{', file.contents

does not save scopes that were not loaded

config.set 'name1', 'x', scope
save!
assert.is_false file.exists

saves the other files and the global configuration when one fails, raising an error

other = dir / 'other/.howl/config.json'
config.load_scope 'file/other', other
config.load_scope scope, file
config.set 'name1', (-> 'not JSON'), 'file/other'
config.set 'name1', 'y', scope
config.set 'name1', 'global'
with_tmpdir (sys_dir) ->
  status, err = pcall config.save_config, sys_dir
  assert.is_false status
  assert.includes err, "Failed to save #{other}: Cannot encode a function"
  assert.same {config: {name1: 'global'}}, json.decode((sys_dir / 'system/config.json').contents)

assert.same {config: {name1: 'y'}}, read!
assert.is_false other.exists

(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