Implement scoped config settings

You can pass a scope stack when calling `config.get`, which will prefer
settings under the most specific matching scope selector for the given
scope stack.
This commit is contained in:
Nathan Sobo
2012-12-21 18:20:01 -07:00
parent 25aadda742
commit 9b6c310239
2 changed files with 92 additions and 24 deletions

View File

@@ -1,19 +1,32 @@
describe "Config", ->
describe ".get(keyPath) and .set(keyPath, value)", ->
it "allows a key path's value to be read and written", ->
expect(config.set("foo.bar.baz", 42)).toBe 42
expect(config.get("foo.bar.baz")).toBe 42
expect(config.get("bogus.key.path")).toBeUndefined()
describe ".get([scope], keyPath) and .set([scope], keyPath, value)", ->
describe "when called without a scope argument", ->
it "allows a key path's value to be read and written", ->
expect(config.set("foo.bar.baz", 42)).toBe 42
expect(config.get("foo.bar.baz")).toBe 42
expect(config.get("bogus.key.path")).toBeUndefined()
it "updates observers and saves when a key path is set", ->
observeHandler = jasmine.createSpy "observeHandler"
config.observe "foo.bar.baz", observeHandler
observeHandler.reset()
it "updates observers and saves when a key path is set", ->
observeHandler = jasmine.createSpy "observeHandler"
config.observe "foo.bar.baz", observeHandler
observeHandler.reset()
config.set("foo.bar.baz", 42)
config.set("foo.bar.baz", 42)
expect(config.save).toHaveBeenCalled()
expect(observeHandler).toHaveBeenCalledWith 42
expect(config.save).toHaveBeenCalled()
expect(observeHandler).toHaveBeenCalledWith 42
describe "when called with a scope argument", ->
it "returns the config value for the most specific selector", ->
expect(config.set(".source.coffee .string.quoted.double.coffee", "foo.bar.baz", 42)).toBe 42
config.set(".source .string.quoted.double", "foo.bar.baz", 22)
config.set(".source", "foo.bar.baz", 11)
config.set("foo.bar.baz", 1)
expect(config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 42
expect(config.get([".source.js", ".string.quoted.double.js"], "foo.bar.baz")).toBe 22
expect(config.get([".source.js", ".variable.assignment.js"], "foo.bar.baz")).toBe 11
expect(config.get([".text"], "foo.bar.baz")).toBe 1
describe ".setDefaults(keyPath, defaults)", ->
it "assigns any previously-unassigned keys to the object at the key path", ->

View File

@@ -1,6 +1,9 @@
fs = require 'fs'
_ = require 'underscore'
EventEmitter = require 'event-emitter'
{$$} = require 'space-pen'
jQuery = require 'jquery'
Specificity = require 'specificity'
configDirPath = fs.absolute("~/.atom")
configJsonPath = fs.join(configDirPath, "config.json")
@@ -31,15 +34,33 @@ class Config
userConfig = JSON.parse(fs.read(configJsonPath))
_.extend(@settings, userConfig)
get: (keyPath) ->
get: (args...) ->
scopeStack = args.shift() if args.length > 1
keyPath = args.shift()
keys = @keysForKeyPath(keyPath)
value = @settings
for key in keys
break unless value = value[key]
value
set: (keyPath, value) ->
settingsToSearch = []
settingsToSearch.push(@settingsForScopeChain(scopeStack)...) if scopeStack
settingsToSearch.push(@settings)
for settings in settingsToSearch
value = settings
for key in keys
value = value[key]
break unless value?
return value if value?
undefined
set: (args...) ->
scope = args.shift() if args.length > 2
keyPath = args.shift()
value = args.shift()
keys = @keysForKeyPath(keyPath)
if scope
keys.unshift(scope)
keys.unshift('scopes')
hash = @settings
while keys.length > 1
key = keys.shift()
@@ -60,12 +81,6 @@ class Config
_.defaults hash, defaults
@update()
keysForKeyPath: (keyPath) ->
if typeof keyPath is 'string'
keyPath.split(".")
else
new Array(keyPath...)
observe: (keyPath, callback) ->
value = @get(keyPath)
previousValue = _.clone(value)
@@ -87,6 +102,46 @@ class Config
save: ->
fs.write(configJsonPath, JSON.stringify(@settings, undefined, 2) + "\n")
keysForKeyPath: (keyPath) ->
if typeof keyPath is 'string'
keyPath.split(".")
else
new Array(keyPath...)
settingsForScopeChain: (scopeStack) ->
return [] unless @settings.scopes?
matchingScopeSelectors = []
node = @buildDomNodeFromScopeChain(scopeStack)
while node
scopeSelectorsForNode = []
for scopeSelector of @settings.scopes
if jQuery.find.matchesSelector(node, scopeSelector)
scopeSelectorsForNode.push(scopeSelector)
scopeSelectorsForNode.sort (a, b) -> Specificity(b) - Specificity(a)
matchingScopeSelectors.push(scopeSelectorsForNode...)
node = node.parentNode
matchingScopeSelectors.map (scopeSelector) => @settings.scopes[scopeSelector]
buildDomNodeFromScopeChain: (scopeStack) ->
scopeStack = new Array(scopeStack...)
element = $$ ->
elementsForRemainingScopes = =>
classString = scopeStack.shift()
classes = classString.replace(/^\./, '').replace(/\./g, ' ')
if scopeStack.length
@div class: classes, elementsForRemainingScopes
else
@div class: classes
elementsForRemainingScopes()
deepestChild = element.find(":not(:has(*))")
if deepestChild.length
deepestChild[0]
else
element[0]
requireUserInitScript: ->
try
require userInitScriptPath if fs.exists(userInitScriptPath)