Skip to content

Instantly share code, notes, and snippets.

@mattmccray
Created August 5, 2012 06:47
Show Gist options
  • Select an option

  • Save mattmccray/3262451 to your computer and use it in GitHub Desktop.

Select an option

Save mattmccray/3262451 to your computer and use it in GitHub Desktop.
DI Experiment
# Chainable
dit.scope('core')
.service('View', -> Backbone.View)
.service('Model', -> Backbone.Model)
.service('List', -> Backbone.Collection)
dit.scope('core_tests', 'core')
.define 'classes existance test', (assert, View, Model, List)->
assert "Model isnt null", Model?
assert "View isnt null", View?
assert "List isnt null", List?
# Returns Scope
app= dit.scope('app', ['core'])
app.define 'MainView', (log, View)->
# log "MainView start", View
class MainView extends View
log.for @
constructor: ->
super()
@log "constructor!"
render: ->
@log "render"
@$el.html "Hello from <code>MainView</code><br/><button onclick=dit.get('testRunner')>Run Tests</button> <small>(see console)</small>"
@
app.factory 'mainView', (log, MainView)->
log "Returning factory class for mainView"
MainView
app.service 'app', (log, mainView)->
log "app start"
class Application
log.for @
run: (id)->
@log "run!", mainView
$(id).html mainView.render().el
new Application
app.service 'main', (app, log)->
log "main start"
app.run('#main')
dit.scope('app_mocks', 'app')
app_test= dit.scope('app_tests', 'app_mocks')
app_test.define 'Injectionable availability test',
(assert, MainView)->
assert "MainView isnt null", MainView?
"use strict"
logType= (type)-> -> console[type].apply console, arguments
log= logType 'log'
log.debug= logType 'debug'
log.info= logType 'info'
log.warn= logType 'warn'
log.error= logType 'error'
log.start= (name="...")-> console.group?(name)
log.end= -> console.groupEnd?()
log.for= (obj, name)->
name = "[#{ function_name obj }]" unless name?
obj_log= (args...)->
args.unshift name
log args...
if is_function(obj)
obj.prototype.log= obj_log
else
obj.log= obj_log
is_string= (obj)-> typeof(obj) is 'string'
is_object= (obj)-> Object::toString.call( obj ) is '[object Object]'
is_function= (obj)-> typeof(obj) is 'function'
is_array= (obj)-> Object::toString.call( obj ) is '[object Array]'
function_name= (fn)->
if is_function(fn)
fn.name or fn.displayName
else if is_object(fn)
fn.name or fn.constructor.name or fn.displayName or fn.constructor.displayName
else
"Unknown"
flatten= (arr, flat=[])->
for item in arr
if is_array(item)
flatten(item, flat)
else
flat.push item
flat
unique= (arr)->
uniq=[]
for item in arr
uniq.push item unless uniq.indexOf(item) >= 0
uniq
new_error= (msg, type="DitError")->
err= new Error(msg)
err.name= type
err
_scopes={}
create_scope= (name, parent_scopes...)->
modules= flatten(parent_scopes)
if _scopes[name]?
if modules.length
if name is 'root'
log.warn "You cannot add dependencies to root. Use #include() instead."
_scopes[name].include(module) for module in modules
else
_scopes[name]._modules= modules.concat(_scopes[name]._modules)
return _scopes[name]
modules.push 'root' unless modules.length or name is 'root'
_scopes[name]=
_name: name
_modules: modules
_cache: {}
_registry:
scope: -> _scopes[name]
define: (name, fn, type="provider")->
throw new_error("requires a name", "InjectorDefinitionError") unless is_string(name)
throw new_error("requires a builder function! (#{name})", "InjectorDefinitionError") unless is_function(fn)
fn._type= type
@_registry[name]= fn
delete @_cache[name] if @_cache[name]
@
# Evaluates fn only once, ever
service: (name, fn)-> @define name, fn, 'service'
# Evaluates fn for every scope
provide: (name, fn)-> @define name, fn
# Evaluates fn only once, but instantiates the return of fn for every injection
factory: (name, fn)-> @define name, fn, 'factory'
# Returns the names of all defined items for this scope
_registeredNames: ->
names= []
for key,fn of @_registry
names.push key
names
# Create a new scope
scope: (name, include...)->
# include.push @_name # Not so sure about this....
create_scope(name, include...)
_detachScope: ->
delete _scopes[@_name]
_definedScopes: ->
names= []
for key,fn of _scopes
names.push key
names
include: (other_scope)->
other_scope= @scope(other_scope) if is_string(other_scope)
if other_scope? and is_object(other_scope._registry) and other_scope._name isnt @_name
for own name, fn of other_scope._registry
@define name, fn, fn._type
else
log.warn "You can only include other scopes!"
@
get: (name, whiny=true) ->
if name.indexOf('.') > 0
[scope_name, item_name]= name.split('.')
return _scopes[scope_name].get(item_name, whiny)
returning= @_cache[name]
unless returning
[fn, cached]= @_getDefinitionFor(name)
unless fn?
returning= undefined
else if fn._type is 'service'
returning= @_cache[name]= cached
else if fn._type is 'factory'
returning= new cached()
else
returning= @_cache[name]= @inject(fn, name)
if returning is undefined and whiny
throw new_error "`#{name}` not found in scope `#{@_name}` or dependencies [#{@_modules.join(', ')}]", "InjectionError"
else
returning
_getDefinitionFor: (name)->
return [undefined, undefined] unless name? or name isnt ""
if fn= @_registry[name]
cached= @_cache[name]
if !cached? and (fn._type is 'service' or fn._type is 'factory')
cached= @inject(fn, name)
@_cache[name]= cached unless fn._type is 'factory'
else
for module in @_modules
[fn, cached]= _scopes[module]._getDefinitionFor(name)
break if fn?
[fn, cached]
inject: (fn, name="anonymous")->
return fn unless is_function(fn)
injected_params= []
injected_params.push @get(param) for param in @_extractParams(fn.toString())
fn.apply(this, injected_params) || null
_extractParams: (s)->
[full, params]= s.match @_paramsRE
deps= params.split(' ').join('').split ','
if deps.length is 1 and deps[0] is "" then [] else deps
_paramsRE: /^function[\s]*\(([\sa-zA-Z0-9_$,]*)\)/
root= create_scope('root')
.service('is_array', -> is_array)
.service('is_function', -> is_function)
.service('is_string', -> is_string)
.service('is_object', -> is_object)
.service('function_name', -> function_name)
.service('flatten', -> flatten)
.service('unique', -> unique)
.service('new_error', -> new_error)
.service('log', -> log)
(module?.exports || window).dit= root
"use strict"
testing= dit.scope('testing')
log= dit.get('log')
_assertion_count= 0
class ConsoleTestRunner
log.for @
runForScope:(@scope)->
@pass=[]
@fail=[]
@errs= []
_assertion_count= 0
@test_count= 0
for test_name in @test_names()
@test_count++
try
@scope.get test_name
@pass.push test_name
catch ex
log.start("#{@scope._name} > #{test_name}")
if ex.name is 'AssertionError'
log.warn "Expectation failed:", ex.message
log.error ex.stack
@fail.push test_name
else
log.error ex.message, ex.stack, ex
@errs.push test_name
log.end()
@assertion_count= _assertion_count
log.start("#{ @scope._name } #{_assertion_count} assertions in #{@test_count} test(s):")
@summary = "#{@pass.length} passed, #{@fail.length} failed, #{@errs.length} errors."
if @fail.length or @errs.length
log.warn @summary
else
log @summary
# log @pass.length, "passed", @pass
log.warn @fail.length, "failed", @fail if @fail.length
log.warn @errs.length, "errors", @errs if @errs.length
log.end()
@test_count
test_names: ->
names=[]
items= @scope._registeredNames()
for name in items
names.push name if name.slice(-4) is 'test'
names
testing.service 'ConsoleTestRunner', -> ConsoleTestRunner
testing.factory 'consoleTestRunner', (ConsoleTestRunner)-> ConsoleTestRunner
testing.factory 'testRunner', (scope, ConsoleTestRunner, log)->
test_runner= ->
test_count= 0
assert_count= 0
log.start("test suite")
for name in scope._definedScopes()
if name.slice(-5) is 'tests' #name.slice(4) is 'test' or
ctr= new ConsoleTestRunner
ctr.runForScope scope.scope(name)
test_count += ctr.test_count
assert_count += ctr.assertion_count
log "Done."
log.end()
[test_count, assert_count]
->
[@test_count, @assert_count]= test_runner()
this
# Assertions
testing.service 'assert', (new_error)->
(args...) ->
switch args.length
when 1
expression=args[0]
msg="expression evaluated to falsy when truthy was expected."
when 2
if typeof(args[0]) is 'string'
[msg, expression]= args
else
[expression, msg]= args
_assertion_count++
unless expression
throw new_error msg, "AssertionError"
testing.service 'assert_equal', (assert)->
(msg, actual, expected) ->
msg= "#{msg} Expected #{String(actual)} to equal #{String(expected)}"
assert (expected == actual), msg
testing.service 'deny', (assert)->
(expression, msg="expression evaluated to truthy when falsy was expected.") ->
assert !expression, msg
testing.service 'deny_equal', (deny)->
(expr_a, expr_b, msg=false) ->
msg= "Expected #{String(expr_a)} to equal #{String(expr_b)}"
deny (expr_a == expr_b), msg
testing.service 'did_throw', (assert, is_function)->
(fn, type)->
thrown_ex= false
try
fn()
catch ex
thrown_ex= ex
if type?
if is_function(type)
thrown_ex instanceof type
else
thrown_ex?.name == type
else
thrown_ex
dit.scope('root').include('testing')
# Some self tests for dit
dit_tests= dit.scope('dit_tests')
dit_tests.define 'is_array test', (is_array, assert)->
assert "arrays return true", is_array([])
assert "objects return false", !is_array({})
assert "nulls return false", !is_array(null)
assert "numerics return false", !is_array(1)
assert "string return false", !is_array("string")
assert "regexp return false", !is_array(/test/)
assert "functions return false", !is_array(/test/)
dit_tests.define 'is_function test', (is_function, assert)->
assert "functions return true", is_function(->)
assert "arrays return false", !is_function([])
assert "objects return false", !is_function({})
assert "nulls return false", !is_function(null)
assert "numerics return false", !is_function(1)
assert "string return false", !is_function("string")
assert "regexp return false", !is_function(/test/)
dit_tests.define 'is_string test', (is_string, assert)->
assert "functions return false", !is_string(->)
assert "arrays return false", !is_string([])
assert "objects return false", !is_string({})
assert "nulls return false", !is_string(null)
assert "numerics return false", !is_string(1)
assert "string return true", is_string("string")
assert "regexp return false", !is_string(/test/)
dit_tests.define 'is_object test', (is_object, assert)->
assert "functions return false", !is_object(->)
assert "arrays return false", !is_object([])
assert "objects return true", is_object({})
assert "nulls return false", !is_object(null)
assert "numerics return false", !is_object(1)
assert "string return true", !is_object("string")
assert "regexp return false", !is_object(/test/)
dit_tests.define 'flatten test', (flatten, assert)->
assert "length of [1,2,3] is 3", flatten([1,2,3]).length is 3
assert "length of [1,[2,3]] is 3", flatten([1,[2,3]]).length is 3
assert "length of [1,[2,[3]] is 3", flatten([1,[2,[3]]]).length is 3
dit_tests.define 'scope test', (scope, assert, assert_equal, is_object)->
tmp= scope.scope('temp')
define_count=0
# Define injected once per scope
tmp.define('name', ->
define_count += 1
"Matt"
)
# Instantiated once for every injection
factory_count=0
class MyClass
constructor: ->
factory_count += 1
tmp.factory('myClass', -> MyClass)
# Only inject once, for lifetime of page
service_count=0
tmp.service('url', ->
service_count +=1
"http://test.com"
)
assert_equal "#scope.get('name') returns 'Matt'", tmp.get('name'), 'Matt'
assert_equal "#scope.get('name') returns 'Matt' a second time", tmp.get('name'), "Matt"
assert_equal "definition of name was only called once", define_count, 1
assert_equal "#scope.get('url') returns 'http://test.com'", tmp.get('url'), "http://test.com"
assert_equal "#scope.get('url') returns 'http://test.com' a second time", tmp.get('url'), "http://test.com"
assert_equal "definition of name was only called once", service_count, 1
assert "#scope.get('myClass') is an object", is_object(tmp.get('myClass'))
assert_equal "definition of name was only called once", factory_count, 1
assert "#scope.get('myClass') is an object a second time", is_object(tmp.get('myClass'))
assert_equal "definition of name was called twice", factory_count, 2
sub= scope.scope('temp_sub', ['temp'])
assert_equal "#sub_scope.get('name') returns 'Matt'", sub.get('name'), "Matt"
assert_equal "#sub_scope.get('name') returns 'Matt' a second time", sub.get('name'), "Matt"
assert_equal "definition of name was only called twice", define_count, 2
sub.define('name', -> "Dan")
assert_equal "#sub_scope.get('name') returns 'Matt'", sub.get('name'), "Dan"
assert_equal "#sub_scope.get('url') returns 'http://test.com'", sub.get('url'), "http://test.com"
assert_equal "#sub_scope.get('url') returns 'http://test.com' a second time", sub.get('url'), "http://test.com"
assert_equal "definition of url service was only called once", service_count, 1
scope.scope('temp_sub')._detachScope()
scope.scope('temp')._detachScope()
assert "temp scope removed from scope list", scope._definedScopes().indexOf('temp') == -1
dit_tests.define 'injection test', (scope, assert, assert_equal, did_throw, is_object, is_string, is_function, log)->
tmp= scope.scope('temp')
# should inject a string
tmp.service 'name', -> "Matt"
# should inject a function
tmp.service 'url', ->
-> "http://test.com"
# should inject an object
tmp.service 'user', ->
username:"inkwellian"
# should inject an instance
class Connection
@count=0
constructor: ->
@count= Connection.count = Connection.count + 1
@type="connection"
tmp.factory 'conn', ->
Connection
tmp.inject( (name)->
assert "result of #scope.injection is string", is_string(name)
assert_equal "result of #scope.injection is 'Matt'", name, "Matt"
)
tmp.inject( (url)->
assert "result of #scope.injection is function", is_function(url)
assert_equal "result of #scope.injection is 'http://test.com'", url(), "http://test.com"
)
tmp.inject( (user)->
assert "result of #scope.injection is object", is_object(user)
assert_equal "result.username of #scope.injection is 'inkwellian'", user.username, "inkwellian"
)
tmp.inject( (conn)->
assert "result of #scope.injection is object", is_object(conn)
assert "result of #scope.injection is instance of Connection", (conn instanceof Connection)
assert_equal "result.type of #scope.injection is 'connection'", conn.type, "connection"
assert_equal "result.count of #scope.injection is 1", conn.count, 1
)
tmp.inject( (conn)->
assert "result of #scope.injection is object", is_object(conn)
assert "result of #scope.injection is instance of Connection", (conn instanceof Connection)
assert_equal "result.type of #scope.injection is 'connection'", conn.type, "connection"
assert_equal "result.count of #scope.injection is 2", conn.count, 2
)
inline_injection= -> tmp.inject( (missing)-> )
assert "missing injectionables should throw an instance of Error", did_throw(inline_injection, Error)
assert "missing injectionables should throw an InjectionError", did_throw(inline_injection, 'InjectionError')
tmp._detachScope()
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>DI Testbed</title>
<link rel="stylesheet" href="style.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="http://documentcloud.github.com/underscore/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone-min.js"></script>
<script src="http://coffeescript.org/extras/coffee-script.js"></script>
<script type="text/coffeescript" src="dit.coffee"></script>
<script type="text/coffeescript" src="dit.testing.coffee"></script>
<script type="text/coffeescript" src="app.coffee"></script>
<script type="text/coffeescript" src="app_tests.coffee"></script>
</head>
<body>
<h1>dit()</h1>
<div id="main"></div>
</body>
<script type="text/coffeescript">
dit.get('app.main')
</script>
</html>
body {
font-family: Helvetica;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment