subsystems
Version:
simple dependency injection
277 lines (220 loc) • 9.32 kB
text/coffeescript
# Author: maartyl
# I will need:
# @inject : info about what it requires (nothing more)
# @system : provides :start which toposorts and starts all subsystems
# @start function :: System -> It'sAPI (generally map of started subsystems)
#
#
# solve problem of defining deps on this system
# HAS TO BE EXPLICIT ^
#
# and cont gets map of all the started subsystems
#
# systems goes through all objects and asks:
# Hey, mate, are you an inject? and if so, it's a dependency.
# : how do I put such injects on this system itself? ...
# - maybe the most simple way: just provide inject, instead of subsystem, yeah...
# - I like this and I'm pretty sure it will work ^^
# //since all must be resolved, before start is called.
#
# each system has to provide everything for his subs, even if all he can do is explicitly state he needs it.
#
# if a dependency CANNOT BE FOUND: must produce nice error
# if a CYCLIC dependency is found: must produce nice error
#
# how will I perform the INJECTION ITSELF?
# : all 'done' store in a map
# : keep info who needs what: take from the map, before calling start
# -- (Will be there by the time it's needed; if not: missing dependency (would probably notice in toposort anyway))
# : keep 'meta' who needs what, though. (should be a function! (for simplicity, composability...))
# -- and THAT FUNCTION CAN JUST END WITH CALLING START, having everything ordered implicitly
# -- takes rest of initialization as argument... - all async and good ^^
#
# let's call that: next(started, cont) (== inject + call start)
# : this function itself probably needs current context (loaded deps map; what to call at end; sorted deps...)
#
# NO DEPENDENCY CAN BE CALLED START
#
#
# CONT used by start:: (err, api)->()
topo = require 'toposort'
util = require 'util'
jstr = (o) -> util.inspect o, depth:5
isFn = (o) -> typeof o is 'function'
OK = null # passed in error field
class SubsystemInjectorStub
constructor: (@dep)->@
isInjector = (obj)-> obj instanceof SubsystemInjectorStub
inject = (dependency_name) ->
if dependency_name is 'start' then throw new Error "No dependency may be called 'start'."
new SubsystemInjectorStub dependency_name
@inject = inject
# retuns list of dependencies
# [[to from]] (to: which fild to inject) (from: dependency name)
scan = (obj) -> [k, v.dep] for k, v of obj when isInjector v
# get dependencies of a subsystem
# @dependencies = scan #possibly in the future, if needed
# builds edges(dependencies)
# FOR SINGLE NODE name
make_edges = (name, scanned) -> [dep, name] for [field, dep] in scanned
# returns list that can be passed topo (to get order of starting)
# map_subs :: (dependency_name -> subsystem)
# cont :: (err, all_dep_edges, nexts(functions to start component + inject)) -> ()
make_deps = (map_subs, cont) ->
deps_edges = []
nexts = {}
for dep, sub of map_subs
scanned = scan sub #get all fields with InjectorStub and what they depend on
deps_edges = deps_edges.concat make_edges dep, scanned #append all dependencies to edges
unless isFn sub.start
return cont new Error "Passed subsystem doesn't have start method - #{jstr dep}:#{jstr sub}."
nexts[dep] = do(dep, scanned, sub, starter=sub.start)-> (started, cont) ->
sub.start = (cont) -> cont OK, started[dep] #replace start method with just map lookup to cashed result
if dep of started then return cont OK, started[dep] #probably always checked outside, but better safe...
for [field, sub_dep] in scanned #start_map check doesn't catch some edge cases (unreal dep...)
unless sub_dep of started then return cont new Error "Unmet dependency: #{jstr sub_dep}."
sub[field] = started[sub_dep] #inject: set field to required from map of started deps
starter.call sub, cont
cont OK, deps_edges, nexts
build_context = (nodes, nexts, started, final_cont) ->
index: 0 #where am I on nodes (which to start next)
nodes: nodes # toposorted dependency names
nexts: nexts # functions to start subsystems
started: started #already started subsystems
cont: final_cont
execute_context = (cxt) ->
if cxt.index >= cxt.nodes.length # already started all subsystems
return cxt.cont OK, cxt.started
dep = cxt.nodes[cxt.index++]
if dep of cxt.started then return execute_context cxt # already started (needs to be here, not in nexts[])
#injects + provides api: stored in started and used by following nexts
cxt.nexts[dep] cxt.started, (err, sub_api) ->
if err then return cxt.cont err
cxt.started[dep] = sub_api
execute_context cxt
# map :: (dependency_name -> subsystem)
# started: (dep_name -> api) #for when this system already depends on another
start_map = (map, started, final_cont) -> make_deps map, (err, deps, nexts) ->
if err then return final_cont err
unsorted_nodes = do->(k for k, v of map) # include all nodes (even if unmentioned in depepndencies)
try nodes = topo.array unsorted_nodes, deps #topologically sort dependency graph
catch err then return final_cont err #cyclic dependency
unmets = (jstr dep for dep in nodes when not (nexts[dep]? or dep of started))
if unmets.length isnt 0
return final_cont new Error "Unmet dependencies: #{unmets}."
execute_context build_context nodes, nexts, started, final_cont
# @system: something that 'just' composes it, START is what matters
# + keeps track of which deps are met externally (injects) and which need to be started
map2system = (map) ->
unmet_keys = [] # whether system itself has some :inject values
met_keys =[]
for k, v of map
if isInjector v
unmet_keys.push k
else
met_keys.push k
map.start = (cont) ->
started = {}
unstarted = {}
for k in unmet_keys
if isInjector map[k] then cont new Error "System with unmet dependency: #{jstr k}."
started[k] = map[k]
for k in met_keys
unstarted[k] = map[k]
start_map unstarted, started, cont
map
# creates subsystem which starts all subsystems in map and passes to
# cont(of it's start) all APIs produced (with same keys as original subsystems)
# map :: (dependency_name -> subsystem)
@system = (map) ->
if map.start? then throw new Error "No dependency may be called 'start'."
map2system map
# this system is assumed to have no unmet dependencies
@start = (system, cont) ->
s = scan system
if s.length isnt 0
then cont new Error "Cannot start system with unmet dependencies: #{jstr dep for [k, dep] in s}."
else
unless isFn system.start
then cont new Error 'No start method on system.'
else system.start cont
## UTILS
fmap = (dep, fn) -> new class
value: inject dep
start: (cont) => # cont OK, fn @value
try v = fn @value #if fn throws: fail continuation
catch err then return cont err
cont OK, v
# subsystem which exports some function of it's single dependency
@fmap = fmap
# field of a dependency (following arguments are used as gield of that and of that...)
@field = (dep, fields...) -> fmap dep, (v)->
ret = v
for field in fields
unless field of Object ret
throw new Error "Field #{jstr field} not present."
ret = ret[field]
ret
rename = (system, map) ->
s = scan system
for [key, old] in s when dep = map[old]
system[key] = inject dep
system
# rename dependencies
# MUTATES system ('s injects)
# map (old(inside(require))name -> new(outside)name)
@rename = (system, map) ->
if map.start? then throw new Error "No dependency may be called 'start'."
rename system, map
wrap = (api) -> start: (cont) -> cont OK, api
#just wrap value into a system structure
@wrap = wrap
api_diff = (ss) ->
reference = {} #store what is curretnlty in ss, so one can do diff at end and only export changes
for k, v of ss
reference[k] = v
() -> #returns function that creats api from diff compared to first call
api = {}
for k, v of ss
unless reference[k] is v #if already present in reference: not new: don't export
api[k] = v #put all diffs on ss to api
api
subsystem = (core) -> (deps, ctor) ->
if deps.start? then throw new Error "No dependency field may be called 'start'."
-> #return creator function
ss = {} #subsystem
for k, v of deps
ss[k] = inject v
ss.start = core ctor.bind(ss), ss
ss
#start versions
#ctor:: ()->()
subsystem.sync = (ctor, ss) -> (cont) ->
mk_api = api_diff ss
try do ctor #update (initialize) ss, after all deps met
catch err then return cont err
cont OK, do mk_api
#ctor:: (cont::(err)->()) -> ()
subsystem.async = (ctor, ss) -> (cont) ->
mk_api = api_diff ss
try ctor (err) ->
if err then return cont err
cont OK, do mk_api
#ctor:: () -> api
subsystem.ret = (ctor, ss) -> (cont) ->
cont.apply undefined,
try [OK, do ctor]
catch err then [err]
#ctor:: (cont) -> ()
#cont:: (err, api) -> ()
# subsystem.ret.async = (ctor, ss) -> (cont) -> ctor cont
subsystem.ret.async = (ctor, ss) -> ctor
#allows very simple creation of (lowest level) subsystems
#exports diff of `this` created by ctor
# :: (deps, ctor) -> (args...) -> system
# deps :: {field -> dep_name}
# ctor :: ()->() - updates `this`
@subsystem = subsystem subsystem.sync
@subsystem.async = subsystem subsystem.async
@subsystem.ret = subsystem subsystem.ret
@subsystem.ret.async = subsystem subsystem.ret.async