HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component composition.OptionBuilder by barry
expand copy to clipboardexpand
//NOTE: this implementation doesn't currently deal fully with the case of a component implementing two different interfaces; in this case the same component should always be selected for each of its interfaces that appear in a composition

//the below five types are our cache, used when deriving compositions
data CompOption {
	char path[]
	char proxyTag[] //set if this is a proxy (added manually via addProxy()); it won't have a corresponding ComponentInfo
	char proxyParams[] //set if this is a proxy (added manually via addProxy()); it won't have a corresponding ComponentInfo
}

data InterfaceOptions {
	//the interface name
	char intf[]
	//the implementation options (components)
	CompOption options[]
}

data Intf {
	char id[]
	bool isNative
}

data ComponentInfo {
	char path[]
	Intf providedIntfs[]
	Intf requiredIntfs[]
}

data InfoCache {
	String searchPaths[]
	String excludeListIntf[]
	String excludeListComp[]
	InterfaceOptions interfaces[]
	ComponentInfo components[]
}

component provides OptionBuilder requires io.Output out, data.StringUtil stringUtil, data.IntUtil iu, util.ObjectFile, System system, os.SystemInfo systemInfo, data.query.Search search, composition.Search csearch, data.json.JSONParser parser, data.Permute permute, Loader loader {

	InfoCache icache

	OptionBuilder:OptionBuilder(char path[], opt String searchPaths[], String interfaceExcludeList[], String componentExcludeList[])
		{
		icache = new InfoCache()
		icache.searchPaths = searchPaths
		icache.excludeListIntf = interfaceExcludeList
		icache.excludeListComp = componentExcludeList

		scanComponentsR(path, icache, searchPaths, interfaceExcludeList, componentExcludeList)

		/*
		for (int i = 0; i < icache.interfaces.arrayLength; i++)
			{
			out.println("interface: $(icache.interfaces[i].intf) -- $(icache.interfaces[i].options.arrayLength) options")
			}
		*/
		}

	ComponentInfo readInterfaces(char path[])
		{
		ComponentInfo nci = new ComponentInfo(path)

		IDC com = loader.load(path)

		InterfaceSpec interfaces[] = com.getRequires()

		for (int i = 0; i < interfaces.arrayLength; i++)
			{
			char id[] = interfaces[i].alias
			bool isNative = interfaces[i].flags == InterfaceSpec.F_NATIVE
			nci.requiredIntfs = new Intf[](nci.requiredIntfs, new Intf(id, isNative))
			}
		
		interfaces = com.getProvides()

		for (int i = 0; i < interfaces.arrayLength; i++)
			{
			char id[] = interfaces[i].alias
			nci.providedIntfs = new Intf[](nci.providedIntfs, new Intf(id))
			}
		
		return nci
		}
	
	void scanComponentsR(char start[], InfoCache ic, String searchPaths[], opt String excludeList[], String excludeListComp[])
		{
		//out.println("scanning '$start'")

		//prep the new component (but don't add it until we've checked the exclude list re: provided interfaces)
		ComponentInfo nci = readInterfaces(start)

		for (int i = 0; i < nci.providedIntfs.arrayLength; i++)
			{
			if (excludeList.findFirst(String.[string], new String(nci.providedIntfs[i].id)) != null) return
			}
		
		//add as a cached component
		ic.components = new ComponentInfo[](ic.components, nci)

		//add as an implementation option of each of its provided interfaces
		for (int i = 0; i < nci.providedIntfs.arrayLength; i++)
			{
			InterfaceOptions iopt = ic.interfaces.findFirst(InterfaceOptions.[intf], new InterfaceOptions(nci.providedIntfs[i].id))

			if (iopt == null)
				{
				iopt = new InterfaceOptions(nci.providedIntfs[i].id)
				ic.interfaces = new InterfaceOptions[](ic.interfaces, iopt)
				}
			
			iopt.options = new CompOption[](iopt.options, new CompOption(start))
			}

		//scan for implementation options of each required interface (IF that interface is not already in the cache), and recurse on those
		for (int i = 0; i < nci.requiredIntfs.arrayLength; i++)
			{
			if (!nci.requiredIntfs[i].isNative && ic.interfaces.findFirst(InterfaceOptions.[intf], new InterfaceOptions(nci.requiredIntfs[i].id)) == null)
				{
				String options[] = csearch.getComponentsIn(nci.requiredIntfs[i].id, searchPaths)

				for (int j = 0; j < options.arrayLength; j++)
					{
					if (excludeListComp.findFirst(String.[string], options[j]) == null)
						scanComponentsR(options[j].string, ic, searchPaths, excludeList, excludeListComp)
					}
				}
			}
		}
	
	bool compositionEqual(Composition a, Composition b)
		{
		if (a.options.arrayLength != b.options.arrayLength) return false

		for (int i = 0; i < a.options.arrayLength; i++)
			{
			if (a.options[i].intf != b.options[i].intf || a.options[i].comp != b.options[i].comp) return false
			}
		
		return true
		}

	bool isCompositionNew(Composition n, Composition set[])
		{
		//check if n is the same as anything in set
		// - we should be able to only check against the last value in set

		for (int i = set.arrayLength - 1; i != INT_MAX; i--)
			{
			if (compositionEqual(n, set[i])) return false
			}
		
		return true
		}
	
	Composition makeComposition(InfoCache ic, int options[])
		{
		//here we make a composition from "options", BUT we need to ignore any components that aren't actually needed (by using the cache of componentinfo required interface records)

		//we start by assuming we need nothing, then tick each interface when we discover that we need it
		int needCount = 0
		bool needed[] = new bool[ic.interfaces.arrayLength]
		bool needChange = true

		while (needChange)
			{
			needChange = false

			for (int i = 0; i < ic.interfaces.arrayLength; i++)
				{
				//check if any selected options use this interface

				if (!needed[i])
					{
					if (ic.interfaces[i].intf == "App")
						{
						needed[i] = true
						needCount ++
						needChange = true
						}
						else
						{
						for (int j = 0; j < ic.interfaces.arrayLength; j++)
							{
							//is this interface actually needed itself? (if not, it may later become needed, in which case we re-scan again)
							if (j != i && needed[j])
								{
								//look up this component
								ComponentInfo ci = ic.components.findFirst(ComponentInfo.[path], new ComponentInfo(ic.interfaces[j].options[options[j]].path))

								//check if it has this interface (note that ci can be null here if the component is a proxy)
								if (ci != null && ci.requiredIntfs.findFirst(Intf.[id], new Intf(ic.interfaces[i].intf)) != null)
									{
									//out.println("comp $(ci.path) requires $(ic.interfaces[i].intf)")
									needed[i] = true
									needCount ++
									needChange = true
									break
									}
								}
							}
						}
					}
				}
			}
		
		//now create the composition out of the actual interfaces that we needed, and their selected options
		Composition result = new Composition()
		result.options = new InterfaceActual[needCount]

		int indexA
		for (int i = 0; i < ic.interfaces.arrayLength; i++)
			{
			if (needed[i])
				{
				InterfaceActual iactual = new InterfaceActual(ic.interfaces[i].intf, ic.interfaces[i].options[options[i]].path, ic.interfaces[i].options[options[i]].proxyTag, ic.interfaces[i].options[options[i]].proxyParams)
				result.options[indexA] = iactual

				//add required interfaces (if it's not a proxy)
				if (iactual.proxyTag == null)
					{
					ComponentInfo ci = ic.components.findFirst(ComponentInfo.[path], new ComponentInfo(iactual.comp))
					iactual.req = new ReqIntf[ci.requiredIntfs.arrayLength]
					for (int j = 0; j < ci.requiredIntfs.arrayLength; j++)
						{
						iactual.req[j] = new ReqIntf(ci.requiredIntfs[j].id, ci.requiredIntfs[j].isNative)
						}
					}

				indexA ++
				}
			}

		return result
		}
	
	char[] intArrayToString(int a[])
		{
		char result[]

		for (int i = 0; i < a.arrayLength; i++)
			{
			if (result == null)
				result = "$(a[i])"
				else
				result = "$result, $(a[i])"
			}

		return result
		}
	
	char[] getFlatComposition(Composition c)
		{
		char result[]

		for (int i = 0; i < c.options.arrayLength; i++)
			{
			char comp[] = c.options[i].comp

			if (c.options[i].proxyTag != null) comp = "$comp{$(c.options[i].proxyTag)/$(c.options[i].proxyParams)}"

			if (result == null)
				result = "$(c.options[i].intf):$comp"
				else
				result = "$result, $(c.options[i].intf):$comp"
			}

		return result
		}
	
	//this function generates all compositions from the given option set, allowing some options to be non-moving, and allowing filter of the result by only those compositions which include a specific component (the latter two options are useful in adding new components)
	// - the filter is needed because this object itself does not store the full list of all compositions ever found; when adding a new component it could therefore return duplicates of compositions that do NOT involve that component, since they would pass the isCompositionNew() test which only considers compositions built in the local scope of this function
	Composition[] generateCompositions(InfoCache ic, int options[], int optionsMax[], opt bool optionsLock[], opt char mustIncludeComp[])
		{
		Composition result[]

		bool continue = true
		while (continue)
			{
			//derive the current permutation
			Composition newc = makeComposition(ic, options)

			if (mustIncludeComp == null || newc.options.findFirst(InterfaceActual.[comp], new InterfaceActual(comp = mustIncludeComp)) != null)
				{
				if (isCompositionNew(newc, result))
					{
					//out.println("adding composition option $(intArrayToString(options))")
					//out.println(" - $(getFlatComposition(newc))")
					result = new Composition[](result, newc)
					}
					else
					{
					//out.println("skipping repeated option $(intArrayToString(options))")
					}
				}

			//get the next one (if any)
			options = permute.getNext(options, optionsMax, optionsLock)

			continue = (options != null)
			}

		return result
		}
	
	Composition[] OptionBuilder:getCompositions()
		{
		//record of which permutation we're currently trying
		int options[] = new int[icache.interfaces.arrayLength]
		int optionsMax[] = new int[icache.interfaces.arrayLength]

		for (int i = 0; i < optionsMax.arrayLength; i++)
			{
			optionsMax[i] = icache.interfaces[i].options.arrayLength - 1
			}
		
		return generateCompositions(icache, options, optionsMax)
		}

	Composition[] OptionBuilder:addComponent(char path[])
		{
		// - here we add the single component to the list of options for the given interface, and we scan that component's required interfaces to check if there are any different required interfaces (but NOT new implementations of known required interfaces) to add them to the list
		// - we then generate new permutations by forcing the choice at that required interface to always be the new component's index, but allowing every other interface's choices to operate like clock hands as usual, starting from all 0's

		InfoCache ic = icache

		scanComponentsR(path, ic, ic.searchPaths, ic.excludeListIntf, ic.excludeListComp)

		int options[] = new int[ic.interfaces.arrayLength]
		int optionsMax[] = new int[ic.interfaces.arrayLength]
		bool optionsLock[] = new bool[ic.interfaces.arrayLength]

		for (int i = 0; i < optionsMax.arrayLength; i++)
			{
			optionsMax[i] = ic.interfaces[i].options.arrayLength - 1

			if (ic.interfaces[i].options.findFirst(CompOption.[path], new CompOption(path)) != null)
				{
				optionsLock[i] = true
				options[i] = optionsMax[i] //NOTE: potentially a bad assumption that this is the index of our newly-added thing?
				}
			}
		
		return generateCompositions(ic, options, optionsMax, optionsLock, path)
		}
	
	Data[] removeArrayCell(Data array[], Data cell)
		{
		Data result[] = new Data[array.arrayLength-1] from typeof(array)

		int j = 0
		for (int i = 0; i < array.arrayLength; i++)
			{
			if (array[i] !== cell)
				{
				result[j] = array[i]
				j ++
				}
			}

		return result
		}
	
	//this will return two lists: one of compositions that have now disappeared, and one of new compositions
	// - there may also be nothing returned at all, if the compositions haven't changed as a result of the update...
	CompositionUpdate OptionBuilder:updComponent(Composition compositions[], char path[])
		{
		// - we can start by re-scanning that component's required interfaces to check if there are any different ones (but NOT new implementations of known required interfaces) to add them to the list
		// - then re-generate new permutations by forcing the choice at that required interface to always be the updated component's index, but allowing every other interface's choices to operate like clock hands as usual, starting from all 0's (but note we only need to do this re-generation if we actually added any new required interfaces in step 1)
		// - then we need to replace existing composition options with new ones, ideally in-place, but also be able to expand with any new ones, and also be able to contract our list if there are now fewer; we need to potentially adapt from and to the old/new version of this component if we're currently in a composition that's using it

		//more simply:
		// - we start by re-scanning the component's required interfaces; if it has the same ones as its prior version, we just report there are no changes
		// - if it has different ones, we first remove the existing component and report which compositions are gone
		// - we then re-add the component and get the set of new compositions, returning those as well

		ComponentInfo nci = readInterfaces(path)

		ComponentInfo oci = icache.components.findFirst(ComponentInfo.[path], new ComponentInfo(path = path))

		if (oci == null)
			{
			throw new Exception("component not found in component cache")
			}
		
		bool structureChange = false

		if (oci.requiredIntfs.arrayLength != nci.requiredIntfs.arrayLength)
			{
			structureChange = true
			}
			else
			{
			for (int i = 0; i < oci.requiredIntfs.arrayLength; i++)
				{
				if (oci.requiredIntfs[i].id != nci.requiredIntfs[i].id)
					{
					structureChange = true
					break
					}
				}
			}
		
		if (!structureChange)
			{
			return new CompositionUpdate(true)
			}
			else
			{
			CompositionUpdate ncu = new CompositionUpdate(true)
			
			//remove the component, but in this case we're allowed to remove it if it's the last-option on the list
			InfoCache ic = icache

			for (int i = 0; i < oci.providedIntfs.arrayLength; i++)
				{
				InterfaceOptions iopt = ic.interfaces.findFirst(InterfaceOptions.[intf], new InterfaceOptions(oci.providedIntfs[i].id))

				CompOption copt = null
				for (int j = 0; j < iopt.options.arrayLength; j++)
					{
					if (iopt.options[j].path == path)
						{
						copt = iopt.options[j]
						break
						}
					}
				
				iopt.options = removeArrayCell(iopt.options, copt)
				}
			
			//now remove the component info
			ic.components = removeArrayCell(ic.components, oci)

			//and get the list of compositions we need to remove
			Composition result[] = null

			for (int i = 0; i < compositions.arrayLength; i++)
				{
				for (int j = 0; j < compositions[i].options.arrayLength; j++)
					{
					InterfaceActual iact = compositions[i].options[j]

					if (iact.comp == path)
						{
						result = new Composition[](result, compositions[i])
						break
						}
					}
				}
			
			ncu.remOptions = result

			//add the "new" component, with the usual procedure
			ncu.addOptions = addComponent(path)

			//...and do cleanup of our cache, in case some interfaces are no longer needed
			cacheCleanup(icache)

			return ncu
			}
		}
	
	void cacheCleanup(InfoCache ic)
		{
		bool changes = true
		while (changes)
			{
			changes = false

			for (int i = 0; i < ic.interfaces.arrayLength; i++)
				{
				if (ic.interfaces[i].intf != "App")
					{
					bool referenced = false

					for (int j = 0; j < ic.components.arrayLength; j++)
						{
						if (ic.components[j].requiredIntfs.findFirst(Intf.[id], new Intf(ic.interfaces[i].intf)) != null)
							{
							referenced = true
							break
							}
						}
					
					if (!referenced)
						{
						//out.println("cache-clean: intf $(ic.interfaces[i].intf) is no longer referenced")

						//remove it, and all of its components (its components that aren't ref'd by any other provided interface options)
						for (int j = 0; j < ic.interfaces[i].options.arrayLength; j++)
							{
							bool comRefEx = false

							//check if any other interfaces consider this component to be an option (we won't remove it from the global components list if so)
							for (int k = 0; k < ic.interfaces.arrayLength; k++)
								{
								if (k != j)
									{
									if (ic.interfaces[k].options.findFirst(CompOption.[path], new CompOption(ic.interfaces[i].options[j].path)) != null)
										{
										comRefEx = true
										break
										}
									}
								}

							if (!comRefEx)
								{
								ComponentInfo cinfo = ic.components.findFirst(ComponentInfo.[path], new ComponentInfo(ic.interfaces[i].options[j].path))
								ic.components = removeArrayCell(ic.components, cinfo)
								}
							}
						
						ic.interfaces = removeArrayCell(ic.interfaces, ic.interfaces[i])

						changes = true
						break
						}
					}
				}
			}
		}
	
	Composition[] OptionBuilder:remComponent(Composition compositions[], char path[])
		{
		// - this is just remove all compositions featuring the given component, and potentially adapt away from the current choice
		//  - it also removes the given component from the componentinfo cache
		// - we'd also need to check the InfoCache to remove any required interfaces that are no longer needed
		//  - so basically we scan through what's left in the componentinfo cache, and make sure every interface is referred to by something; if not remove that interface and all associated componentinfos, then if we did such a removal we repeat again to re-check

		InfoCache ic = icache

		ComponentInfo cinfo = ic.components.findFirst(ComponentInfo.[path], new ComponentInfo(path))

		if (cinfo == null) throw new Exception("component $path is not known")

		for (int i = 0; i < cinfo.providedIntfs.arrayLength; i++)
			{
			InterfaceOptions iopt = ic.interfaces.findFirst(InterfaceOptions.[intf], new InterfaceOptions(cinfo.providedIntfs[i].id))
			if (iopt.options.arrayLength == 1) throw new Exception("cannot remove only remaining option for an interface")
			}

		for (int i = 0; i < cinfo.providedIntfs.arrayLength; i++)
			{
			InterfaceOptions iopt = ic.interfaces.findFirst(InterfaceOptions.[intf], new InterfaceOptions(cinfo.providedIntfs[i].id))

			CompOption copt = null
			for (int j = 0; j < iopt.options.arrayLength; j++)
				{
				if (iopt.options[j].path == path)
					{
					copt = iopt.options[j]
					break
					}
				}
			
			iopt.options = removeArrayCell(iopt.options, copt)
			}
		
		//now remove the component info
		ic.components = removeArrayCell(ic.components, cinfo)

		//now we need to clean up side effects: check the infocache for any interfaces that are no longer referenced by anything's required interface list
		cacheCleanup(ic)
		
		//and return the list of compositions we need to remove
		Composition result[] = null

		for (int i = 0; i < compositions.arrayLength; i++)
			{
			for (int j = 0; j < compositions[i].options.arrayLength; j++)
				{
				InterfaceActual iact = compositions[i].options[j]

				if (iact.comp == path)
					{
					result = new Composition[](result, compositions[i])
					break
					}
				}
			}

		return result
		}

	//function to inject a custom proxy; this means that we'll generate a set of additional permutations on the assumption that proxyComponent is a valid implementation of forInterface
	// - and we will NOT perform wiring of the proxy or any of its sub-components
	// - the "tag" and "parameters" fields here are entirely for the API user, and will be passed back to their custom load function
	// - when you call setConfig(), you can provide a set of custom loaders associated with tags and we'll call your loader to load and unload this proxy
	Composition[] OptionBuilder:addProxy(char forInterface[], char proxyComponent[], char tag[], char parameters[])
		{
		//this works broadly the same as addComponent(), but we don't add the proxy as an actual component to our cache, and don't follow its interfaces
		//...so instead we're going to add the component as an option to forInterface, but note that the component is a proxy and has delegated loading, storing the "tag" and "parameters" things with it to pass to the delegated loader
		// - if someone calls setConfig() and there isn't a given delegated loader for this tag, then setConfig() will refuse to adapt to that config
		InfoCache ic = icache

		InterfaceOptions iopt = ic.interfaces.findFirst(InterfaceOptions.[intf], new InterfaceOptions(forInterface))

		if (iopt == null) throw new Exception("interface $forInterface is not known")

		iopt.options = new CompOption[](iopt.options, new CompOption(proxyComponent, tag, parameters))

		int options[] = new int[ic.interfaces.arrayLength]
		int optionsMax[] = new int[ic.interfaces.arrayLength]
		bool optionsLock[] = new bool[ic.interfaces.arrayLength]

		for (int i = 0; i < optionsMax.arrayLength; i++)
			{
			optionsMax[i] = ic.interfaces[i].options.arrayLength - 1

			if (ic.interfaces[i].intf == forInterface && ic.interfaces[i].options.findFirst(CompOption.[path], new CompOption(proxyComponent)) != null)
				{
				optionsLock[i] = true
				options[i] = optionsMax[i] //NOTE: potentially a bad assumption that this is the index of our newly-added thing?
				}
			}
		
		return generateCompositions(ic, options, optionsMax, optionsLock)
		}
	
	Composition[] OptionBuilder:remProxy(Composition compositions[], char forInterface[], char proxyComponent[], char tag[], char parameters[])
		{
		InfoCache ic = icache

		//update our infocache to remove this option
		InterfaceOptions iopt = ic.interfaces.findFirst(InterfaceOptions.[intf], new InterfaceOptions(forInterface))

		if (iopt == null) throw new Exception("interface $forInterface is not known")

		CompOption copt = null
		for (int i = 0; i < iopt.options.arrayLength; i++)
			{
			if (iopt.options[i].path == proxyComponent && iopt.options[i].proxyTag == tag && iopt.options[i].proxyParams == parameters)
				{
				copt = iopt.options[i]
				break
				}
			}
		
		if (copt == null) throw new Exception("proxy $forInterface:$proxyComponent is not known")

		if (iopt.options.arrayLength == 1) throw new Exception("cannot remove only remaining option for an interface")

		iopt.options = removeArrayCell(iopt.options, copt)

		//return the list of compositions we need to remove
		Composition result[] = null

		for (int i = 0; i < compositions.arrayLength; i++)
			{
			for (int j = 0; j < compositions[i].options.arrayLength; j++)
				{
				InterfaceActual iact = compositions[i].options[j]

				if (iact.intf == forInterface && iact.comp == proxyComponent && iact.proxyTag == tag && iact.proxyParams == parameters)
					{
					result = new Composition[](result, compositions[i])
					break
					}
				}
			}

		return result
		}
	}
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n composition.OptionBuilder -m "reason for update" -u yourUsername
Version 2 by barry
Version 1 (this version) by barry
Notes for this version: Standard Library Initialisation