Created
August 5, 2012 06:47
-
-
Save mattmccray/3262451 to your computer and use it in GitHub Desktop.
DI Experiment
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| body { | |
| font-family: Helvetica; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment