sourceafIoc::RegistryBuilder.fan


** Builds a `Registry` from Modules. Modules may be added manually, defined by 
** [meta-data]`sys::Pod.meta` in dependent pods or defined by [index properties]`docLang::Env#index`
class RegistryBuilder {
    private const static Log    logger      := Utils.getLog(RegistryBuilder#)
    
    private BuildCtx    ctx         := BuildCtx("Building IoC Registry")
    private OneShotLock lock        := OneShotLock(IocMessages.registryBuilt)
    private ModuleDef[] moduleDefs  := [,]
    private Str:Obj     options
    
    ** Create a 'RegistryBuilder'. 
    ** 
    ** Builder 'Options' are reserved for future use. 
    new make([Str:Obj]? options := null) {
        this.options = (options != null) ? options : Utils.makeMap(Str#, Obj#).add("suppressLogging", false)
        addModule(IocModule#)
    }

    ** Adds a module to the registry
    This addModule(Type moduleType) {
        (RegistryBuilder) Utils.stackTraceFilter |->RegistryBuilder| {      
            ctx.track("Adding module definition for '$moduleType.qname'") |->| {
                lock.check
                if (moduleType != IocModule# && !options["suppressLogging"] && !moduleTypes.contains(moduleType))
                    logger.info("Adding module definition for $moduleType.qname")

                ctx.withModule(moduleType) |->| {           
                    if (moduleDefs.find { it.moduleType == moduleType } != null) {
                        // Debug because sometimes you can't help adding the same module twice (via dependencies)
                        // afBedSheet is a prime example
                        logger.debug(IocMessages.moduleAlreadyAdded(moduleType))
                        return
                    }
                    
                    moduleDef := ModuleDefImpl(ctx.tracker, moduleType)
                    addModuleDef(moduleDef)
                    
                    if (moduleType.hasFacet(SubModule#)) {
                        subModule := Utils.getFacetOnType(moduleType, SubModule#) as SubModule
                        ctx.track("Found SubModule facet on $moduleType.qname : $subModule.modules") |->| {
                            subModule.modules.each { 
                                addModule(it)
                            }
                        }
                    } else
                        ctx.log("No SubModules found")
                }
            }
            return this
        }
    }

    ** Adds many modules to the registry
    This addModules(Type[] moduleTypes) {
        (RegistryBuilder) Utils.stackTraceFilter |->Obj| {      
            lock.check
            moduleTypes.each |moduleType| {
                addModule(moduleType)
            }
            return this
        }
    }

    ** Checks all dependencies of the given [pod]`sys::Pod` for the meta-data key 'afIoc.module' 
    ** which defines the qualified name of a module to load.
    This addModulesFromDependencies(Pod pod, Bool addTransitiveDependencies := true) {
        (RegistryBuilder) Utils.stackTraceFilter |->Obj| {
            if (!options["suppressLogging"])
                logger.info("Adding modules from dependencies of '$pod.name'")
            addModulesFromDependenciesRecursive(pod, addTransitiveDependencies)
            return this
        }
    }

    ** Looks for all index properties of the key 'afIoc.module' which defines a qualified name of 
    ** a module to load.
    This addModulesFromIndexProperties() {
        (RegistryBuilder) Utils.stackTraceFilter |->Obj| {      
            if (!options["suppressLogging"])
                logger.info("Adding modules from index properties")
            ctx.track("Adding modules from index properties") |->| {
                lock.check
                moduleNames := Env.cur.index("afIoc.module")
                moduleNames.each {
                    addModuleFromTypeName(it)
                }
                if (moduleNames.isEmpty)
                    ctx.log("No modules found")
            }
            return this
        }
    }

    ** Returns a list of modules types currently held by this builder.
    Type[] moduleTypes() {
        moduleDefs.map { it.moduleType }
    }
    
    ** Constructs and returns the registry; this may only be done once. The caller is responsible 
    ** for invoking `Registry.startup`
    ** 
    ** Options are passed to the registry to specify extra behaviour:
    **  -  'logServiceCreation': Bool specifies if each service creation should be logged to INFO. 
    **      Default is 'false'. For extensive debug info, use 
    **      [IocHelper.debugOperation()]`IocHelper.debugOperation`.
    **  -  'disableProxies': Bool specifies if all proxy generation for mixin fronted services  
    **      should be disabled. Default is 'false'.
    **  -  'suppressStartupMsg': Bool specifies if the default (verbose) startup log message should 
    **      be suppressed. Default is 'false'.
    Registry build([Str:Obj?]? options := null) {
        Utils.stackTraceFilter |->Obj| {
            lock.lock

            options = options?.rw ?: Utils.makeMap(Str#, Obj?#)
            
            defaults := Utils.makeMap(Str#, Obj#).addAll([
                "logServiceCreation"        : false,
                "disableProxies"            : false,
                "suppressStartupMsg"        : false,
                "bannerText"                : "Alien-Factory IoC v$typeof.pod.version"
            ])

            defaults.each |val, key| {
                optType := options[key]?.typeof
                if (optType != null && optType != val.typeof)
                    throw IocErr(IocMessages.invalidRegistryValue(key, optType, val.typeof))
            }

            registry := RegistryImpl(ctx.tracker, moduleDefs, defaults.setAll(options))
            ctx.tracker.end
            return registry
        }
    }
    
    // ---- Private Methods -----------------------------------------------------------------------

    private Type[] addModulesFromDependenciesRecursive(Pod pod, Bool addTransitiveDependencies) {
        ctx.track("Adding modules from dependencies of '$pod.name'") |->Type[]| {
            lock.check

            Type?[] modTypes := [,]
        
            // don't forget me!
            ctx.withPod(pod) |->| {
                modType := addModuleFromPod(pod)
                if (modType != null)
                    modTypes.add(modType)

                pod.depends.each {
                    dependency := Pod.find(it.name)
                    ctx.withPod(dependency) |->| {
                        modType = addModuleFromPod(dependency)
                        if (modType != null)
                            modTypes.add(modType)

                        if (addTransitiveDependencies) {
                            mods := ctx.track("Adding transitive dependencies for '$dependency.name'") |->Obj| {
                                deps := addModulesFromDependenciesRecursive(dependency, addTransitiveDependencies)
                                if (deps.isEmpty)
                                    ctx.log("No transitive dependencies found")
                                return deps
                            } 
                            modTypes.addAll(mods)
                        } else
                            ctx.log("Not looking for transitive dependencies")
                    }
                }

                if (modTypes.isEmpty)
                    ctx.log("No modules found")             
            }

            return modTypes
        }
    }   
    
    private Type? addModuleFromPod(Pod pod) {
        qname := pod.meta[IocConstants.podMetaModuleName]
        if (qname != null) {
            return ctx.track("Pod '$pod.name' defines module $qname") |->Obj| {
                return addModuleFromTypeName(qname)
            }
        }
        return null
    }

    private Type addModuleFromTypeName(Str moduleTypeName) {
        moduleType := Type.find(moduleTypeName)
        addModule(moduleType)
        return moduleType
    }

    private This addModuleDef(ModuleDef moduleDef) {
        this.moduleDefs.add(moduleDef)
        return this
    }
}

internal class BuildCtx {
    private Pod[]       podStack        := [,]
    private Type[]      moduleStack     := [,]
            OpTracker   tracker

    new make(Str desc) {
        tracker = OpTracker(desc)
    }
    
    Obj? track(Str description, |->Obj?| operation) {
        tracker.track(description, operation)
    }

    Void log(Str description) {
        tracker.log(description)
    }

    Obj? withModule(Type module, |->Obj?| operation) {
        moduleStack.push(module)
        try {
            // check for recursion
            moduleStack[0..<-1].each { 
                if (it == module)
                    throw IocErr(IocMessages.moduleRecursion(moduleStack.map { it.qname }))
            }           
            return operation.call()
        } finally {         
            moduleStack.pop
        }
    }   

    Void withPod(Pod pod, |->Obj?| operation) {
        podStack.push(pod)
        try {
            ignore := false
            
            // check for recursion
            podStack[0..<-1].each { 
                if (it == pod) {
                    this.log("Pod '$pod.name' already inspected...ignoring")
                    ignore = true
                }
            }
            
            if (!ignore)
                operation.call()
            
        } finally {         
            moduleStack.pop
        }
    }   
}