diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..252e2ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +bin +obj +build +out +libs +.gradle +*.iml +*.ipr +*.iws +*~ +*.user +gradle.properties diff --git a/build.gradle b/build.gradle new file mode 100755 index 0000000..8b129ab --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'idea' + +buildscript { + repositories { + ivy { url '../repository' } + ivy { url 'http://unity-technologies.github.com/kaizen/repositories/integration' } + } + dependencies { + classpath group: 'kaizen', name: 'kaizen', version: 'latest.integration' + } +} + +apply plugin: 'kaizen-bundle' + +version = '1.0' + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000..7f1e239 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000..c14d177 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 27 09:13:56 BRT 2012 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.2-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..e61422d --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/bin/bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100755 index 0000000..4b8726d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,15 @@ +include 'CodeEditor.Composition' + +//include 'CodeEditor.Text' +//include 'CodeEditor.Collections' +//include 'CodeEditor.UI.UnityEditor' + +rootProject.children.each { project -> + def projectDirName = "src/$project.name" + + project.projectDir = new File(settingsDir, projectDirName) + assert project.projectDir.isDirectory() + + project.buildFileName = "${project.name}.gradle" + assert project.buildFile.isFile() +} diff --git a/src/CodeEditor.Composition.Tests/CodeEditor.Composition.Tests.csproj b/src/CodeEditor.Composition.Tests/CodeEditor.Composition.Tests.csproj new file mode 100755 index 0000000..fa18cc6 --- /dev/null +++ b/src/CodeEditor.Composition.Tests/CodeEditor.Composition.Tests.csproj @@ -0,0 +1,71 @@ + + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {2ED73522-E34D-4CAB-8A42-6421FB9B1E77} + Library + Properties + CodeEditor.Composition.Tests + CodeEditor.Composition.Tests + v3.5 + 512 + + + 3.5 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\..\NUnit\bin\nunit.framework.dll + + + + 3.5 + + + + + + + + + + + + + + + {9DB8BFD3-06C8-4A8C-8842-5931B924B56C} + CodeEditor.Composition + + + + + \ No newline at end of file diff --git a/src/CodeEditor.Composition.Tests/CodeEditor.Composition.Tests.gradle b/src/CodeEditor.Composition.Tests/CodeEditor.Composition.Tests.gradle new file mode 100644 index 0000000..602d055 --- /dev/null +++ b/src/CodeEditor.Composition.Tests/CodeEditor.Composition.Tests.gradle @@ -0,0 +1,4 @@ +dependencies { + editor project(path: ':CodeEditor.Composition', configuration: 'editor') + editor group: 'nunit', name: 'nunit.framework', version: '2.6.0.12051' +} diff --git a/src/CodeEditor.Composition.Tests/CompositionContainerTest.cs b/src/CodeEditor.Composition.Tests/CompositionContainerTest.cs new file mode 100755 index 0000000..040397b --- /dev/null +++ b/src/CodeEditor.Composition.Tests/CompositionContainerTest.cs @@ -0,0 +1,142 @@ +using CodeEditor.Composition.Hosting; +using CodeEditor.Composition.Primitives; +using NUnit.Framework; + +namespace CodeEditor.Composition.Tests +{ + [TestFixture] + public class CompositionContainerTest + { + private CompositionContainer _container; + + [SetUp] + public void SetUp() + { + _container = new CompositionContainer(GetType().Assembly); + } + + [Test] + public void ContainerIsExportedAsExportProvider() + { + Assert.AreSame(_container, GetExportedValue()); + } + + [Test] + public void GetExportedValueAlwaysReturnSameValue() + { + var service = GetExportedValue(); + Assert.IsNotNull(service); + Assert.AreSame(service, GetExportedValue()); + } + + [Test] + public void GetExportedValueSatisfiesPropertyImports() + { + Assert.AreSame(GetExportedValue(), GetExportedValue().AService); + } + + [Test] + public void GetExportedValueSatisfiesFieldImports() + { + Assert.AreSame(GetExportedValue(), GetExportedValue().AService); + } + + [Test] + public void GetExportedValueSatisfiesInterfaceImportImplementedByInternalType() + { + Assert.AreSame(GetExportedValue(), GetExportedValue().AService); + } + + [Test] + public void GetExportedValueSatisfiesImportingConstructor() + { + Assert.AreSame(GetExportedValue(), GetExportedValue().AService); + } + + [Test] + public void PartCanExportMultipleContracts() + { + Assert.AreSame(GetExportedValue(), GetExportedValue()); + Assert.IsTrue(GetExportedValue() is PartWithMultipleContracts); + } + + private T GetExportedValue() + { + return _container.GetExportedValue(); + } + + // ReSharper disable ClassNeverInstantiated.Global + [Export] + public class AService + { + } + + [Export] + public class AServiceWithDependencies + { + [Import] + // ReSharper disable UnusedAutoPropertyAccessor.Global + public AService AService { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Global + } + + [Export] + public class AServiceWithFieldDependencies + { + [Import] + public AService AService; + } + + public interface IServiceWithDependencies + { + IService AService { get; } + } + + public interface IService + { + } + + // ReSharper disable UnusedMember.Global + [Export(typeof(IService))] + internal class ServiceImpl : IService + { + } + + [Export(typeof(IServiceWithDependencies))] + internal class ServiceWithDependenciesImpl : IServiceWithDependencies + { + [Import] + // ReSharper disable UnusedAutoPropertyAccessor.Global + public IService AService { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Global + } + // ReSharper restore UnusedMember.Global + + [Export] + public class ServiceWithImportingConstructor + { + [ImportingConstructor] + ServiceWithImportingConstructor(IService service) + { + AService = service; + } + + public IService AService { get; private set; } + } + // ReSharper restore ClassNeverInstantiated.Global + + public interface IContract1 + { + } + + public interface IContract2 + { + } + + [Export(typeof(IContract1))] + [Export(typeof(IContract2))] + public class PartWithMultipleContracts : IContract1, IContract2 + { + } + } +} diff --git a/src/CodeEditor.Composition.Tests/ImportDefinitionProviderTest.cs b/src/CodeEditor.Composition.Tests/ImportDefinitionProviderTest.cs new file mode 100755 index 0000000..4a7fb60 --- /dev/null +++ b/src/CodeEditor.Composition.Tests/ImportDefinitionProviderTest.cs @@ -0,0 +1,48 @@ +using System.Linq; +using CodeEditor.Composition.Primitives; +using NUnit.Framework; + +namespace CodeEditor.Composition.Tests +{ + [TestFixture] + public class ImportDefinitionProviderTest + { + [Test] + public void LazyImport() + { + var provider = new ImportDefinitionProvider(); + var import = provider.ImportsFor(typeof(ClassWithLazyImport)).Single(); + Assert.AreEqual(typeof(IService), import.ContractType); + Assert.AreEqual(ImportCardinality.One, import.Cardinality); + } + + [Test] + public void LazyImportMany() + { + var provider = new ImportDefinitionProvider(); + var import = provider.ImportsFor(typeof(ClassWithLazyImportMany)).Single(); + Assert.AreEqual(typeof(IService), import.ContractType); + Assert.AreEqual(ImportCardinality.Many, import.Cardinality); + } + + public class ClassWithLazyImport + { + [Import] + public Lazy Service; + } + + public class ClassWithLazyImportMany + { + [ImportMany] + public Lazy[] Services; + } + + public interface IService + { + } + + public interface IServiceMetadata + { + } + } +} diff --git a/src/CodeEditor.Composition.Tests/LazyImportManyTest.cs b/src/CodeEditor.Composition.Tests/LazyImportManyTest.cs new file mode 100755 index 0000000..e531164 --- /dev/null +++ b/src/CodeEditor.Composition.Tests/LazyImportManyTest.cs @@ -0,0 +1,76 @@ +using System.Linq; +using CodeEditor.Composition.Hosting; +using NUnit.Framework; + +namespace CodeEditor.Composition.Tests +{ + [TestFixture] + public class LazyImportManyTest + { + [Test] + public void OnlyServicesWithMatchingMetadataAreProvided() + { + var container = new CompositionContainer(GetType().Assembly); + + var service = container.GetExportedValue(); + Assert.IsNotNull(service.Imports); + + var expected = new[] + { + new {Type = typeof(Service1), Name = "Foo"}, + new {Type = typeof(Service2), Name = "Bar"} + }; + var actual = service.Imports + .Select(import => new {Type = import.Value.GetType(), import.Metadata.Name}); + + CollectionAssert.AreEquivalent(expected, actual.ToArray()); + } + + [Export] +// ReSharper disable ClassNeverInstantiated.Local + class ServiceWithImports + { + [ImportMany] +#pragma warning disable 649 + public Lazy[] Imports; +#pragma warning restore 649 + } + + interface IService + { + } + + [ExportServiceWithMetadata("Foo")] + class Service1 : IService + { + } + + [ExportServiceWithMetadata("Bar")] + class Service2 : IService + { + } + + [Export(typeof(IService))] +// ReSharper disable UnusedMember.Local + class ServiceWithoutMetadata : IService +// ReSharper restore UnusedMember.Local + { + } + // ReSharper restore ClassNeverInstantiated.Local + + interface IServiceMetadata + { + string Name { get; } + } + + class ExportServiceWithMetadata : ExportAttribute, IServiceMetadata + { + public string Name { get; private set; } + + public ExportServiceWithMetadata(string name) : base(typeof(IService)) + { + Name = name; + } + } + } +} diff --git a/src/CodeEditor.Composition.Tests/LazyImportTest.cs b/src/CodeEditor.Composition.Tests/LazyImportTest.cs new file mode 100755 index 0000000..6bf33d1 --- /dev/null +++ b/src/CodeEditor.Composition.Tests/LazyImportTest.cs @@ -0,0 +1,50 @@ +using CodeEditor.Composition.Hosting; +using NUnit.Framework; + +namespace CodeEditor.Composition.Tests +{ + [TestFixture] + public class LazyImportTest + { + [Test] + public void MetadataIsProvided() + { + var container = new CompositionContainer(GetType().Assembly); + + var service = container.GetExportedValue(); + + Assert.IsNotNull(service.Import); + Assert.AreEqual(42, service.Import.Metadata.Value); + + Assert.IsNotNull(service.Import.Value); + Assert.AreSame(service.Import.Value, service.Import.Value); + } + + [Export] + public class ServiceWithLazyImport + { + [Import] + public Lazy Import; + } + + [ExportWithMetadata(42)] + public class ServiceWithMetadata + { + } + + public interface IServiceMetadata + { + int Value { get; } + } + + class ExportWithMetadataAttribute : ExportAttribute, IServiceMetadata + { + public int Value { get; private set; } + + public ExportWithMetadataAttribute(int value) + { + Value = value; + } + } + } +} diff --git a/src/CodeEditor.Composition.Tests/LazyTest.cs b/src/CodeEditor.Composition.Tests/LazyTest.cs new file mode 100755 index 0000000..781a99d --- /dev/null +++ b/src/CodeEditor.Composition.Tests/LazyTest.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; + +namespace CodeEditor.Composition.Tests +{ + [TestFixture] + public class LazyTest + { + [Test] + public void FactoryIsInvokedOnlyOnce() + { + var count = 0; + var lazy = new Lazy(() => (object) ++count); + Assert.AreEqual(0, count); + + Assert.AreEqual(1, lazy.Value); + Assert.AreEqual(1, count); + + Assert.AreEqual(1, lazy.Value); + Assert.AreEqual(1, count); + } + } +} diff --git a/src/CodeEditor.Composition.Tests/Properties/AssemblyInfo.cs b/src/CodeEditor.Composition.Tests/Properties/AssemblyInfo.cs new file mode 100755 index 0000000..af35c95 --- /dev/null +++ b/src/CodeEditor.Composition.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("CodeEditor.Composition.Tests")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/CodeEditor.Composition/CodeEditor.Composition.csproj b/src/CodeEditor.Composition/CodeEditor.Composition.csproj new file mode 100755 index 0000000..f0e24f3 --- /dev/null +++ b/src/CodeEditor.Composition/CodeEditor.Composition.csproj @@ -0,0 +1,77 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {9DB8BFD3-06C8-4A8C-8842-5931B924B56C} + Library + Properties + CodeEditor.Composition + CodeEditor.Composition + v3.5 + 512 + 3.5 + ..\..\..\ + + + true + full + false + bin\$(Configuration)\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\$(Configuration)\ + TRACE + prompt + 4 + + + + + 3.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CodeEditor.Composition/CodeEditor.Composition.gradle b/src/CodeEditor.Composition/CodeEditor.Composition.gradle new file mode 100644 index 0000000..020b3af --- /dev/null +++ b/src/CodeEditor.Composition/CodeEditor.Composition.gradle @@ -0,0 +1,2 @@ +version = '1.0' + diff --git a/src/CodeEditor.Composition/CompositionError.cs b/src/CodeEditor.Composition/CompositionError.cs new file mode 100755 index 0000000..34bf008 --- /dev/null +++ b/src/CodeEditor.Composition/CompositionError.cs @@ -0,0 +1,17 @@ +using System; + +namespace CodeEditor.Composition +{ + public class CompositionError + { + public CompositionError(Type contractType, string message) + { + ContractType = contractType; + Message = message; + } + + public Type ContractType { get; private set; } + + public string Message { get; private set; } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/CompositionException.cs b/src/CodeEditor.Composition/CompositionException.cs new file mode 100755 index 0000000..b8270bc --- /dev/null +++ b/src/CodeEditor.Composition/CompositionException.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace CodeEditor.Composition +{ + public class CompositionException : Exception + { + private readonly IList _errors = new List(); + + public CompositionException(CompositionError error) : base(error.Message) + { + Add(error); + } + + public CompositionException(Exception cause, CompositionError error) : base(error.Message, cause) + { + Add(error); + } + + public IList Errors + { + get { return _errors; } + } + + private void Add(CompositionError error) + { + _errors.Add(error); + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/ContractAttribute.cs b/src/CodeEditor.Composition/ContractAttribute.cs new file mode 100755 index 0000000..0019418 --- /dev/null +++ b/src/CodeEditor.Composition/ContractAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace CodeEditor.Composition +{ + public abstract class ContractAttribute : Attribute + { + private readonly Type _contractType; + + protected ContractAttribute(Type contractType) + { + _contractType = contractType; + } + + public Type ContractType + { + get { return _contractType; } + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/ExportAttribute.cs b/src/CodeEditor.Composition/ExportAttribute.cs new file mode 100755 index 0000000..75e127a --- /dev/null +++ b/src/CodeEditor.Composition/ExportAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace CodeEditor.Composition +{ + /// + /// Marks a class as an export. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class ExportAttribute : ContractAttribute + { + public ExportAttribute(Type contractType) : base(contractType) {} + + public ExportAttribute() : base(null) {} + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Hosting/AggregateCatalog.cs b/src/CodeEditor.Composition/Hosting/AggregateCatalog.cs new file mode 100755 index 0000000..6d843ae --- /dev/null +++ b/src/CodeEditor.Composition/Hosting/AggregateCatalog.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CodeEditor.Composition.Primitives; + +namespace CodeEditor.Composition.Hosting +{ + public class AggregateCatalog : IExportDefinitionProvider + { + private readonly IExportDefinitionProvider[] _exportDefinitionProviders; + + public AggregateCatalog(IEnumerable exportProviders) : this(exportProviders.ToArray()) + { + } + + public AggregateCatalog(params IExportDefinitionProvider[] exportDefinitionProviders) + { + _exportDefinitionProviders = exportDefinitionProviders; + } + + public IEnumerable GetExports(Type contractType) + { + return _exportDefinitionProviders.SelectMany(provider => provider.GetExports(contractType)); + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Hosting/AssemblyCatalog.cs b/src/CodeEditor.Composition/Hosting/AssemblyCatalog.cs new file mode 100755 index 0000000..bffba78 --- /dev/null +++ b/src/CodeEditor.Composition/Hosting/AssemblyCatalog.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using CodeEditor.Composition.Primitives; + +namespace CodeEditor.Composition.Hosting +{ + public class AssemblyCatalog : IExportDefinitionProvider + { + public static IExportDefinitionProvider For(IEnumerable assemblies) + { + return new AggregateCatalog(AssemblyCatalogsFor(assemblies)); + } + + private static IEnumerable AssemblyCatalogsFor(IEnumerable assemblies) + { + return assemblies + .Where(assembly => assembly.IsSafeForComposition()) + .Select(assembly => (IExportDefinitionProvider) new AssemblyCatalog(assembly)); + } + + private Dictionary _exports; + private readonly Assembly _assembly; + + public AssemblyCatalog(Assembly assembly) + { + _assembly = assembly; + } + + public IEnumerable GetExports(Type contractType) + { + ExportDefinition[] exportsDefinition; + return Exports().TryGetValue(contractType, out exportsDefinition) + ? exportsDefinition + : NoExportsDefinition; + } + + private Dictionary Exports() + { + lock (this) + return _exports ?? (_exports = ComputeExports()); + } + + private Dictionary ComputeExports() + { + return _assembly + .GetTypes() + .SelectMany(ExportsFromType) + .Where(e => e != null) + .GroupBy(e => e.ContractType) + .ToDictionary(e => e.Key, e => e.ToArray()); + } + + private IEnumerable ExportsFromType(Type implementation) + { + return CustomAttribute + .AllFrom(implementation) + .Select(attribute => new ExportDefinition(attribute.ContractType ?? implementation, implementation)); + } + + private static readonly ExportDefinition[] NoExportsDefinition = new ExportDefinition[0]; + } +} diff --git a/src/CodeEditor.Composition/Hosting/AssemblyExtensions.cs b/src/CodeEditor.Composition/Hosting/AssemblyExtensions.cs new file mode 100755 index 0000000..2575ec8 --- /dev/null +++ b/src/CodeEditor.Composition/Hosting/AssemblyExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace CodeEditor.Composition.Hosting +{ + static class AssemblyExtensions + { + public static bool IsSafeForComposition(this Assembly assembly) + { + return CaresForComposition(assembly) && TypesAreAccessible(assembly); + } + + private static bool CaresForComposition(Assembly assembly) + { + return assembly + .GetReferencedAssemblies() + .Any(name => name.FullName.Equals(typeof(ExportAttribute).Assembly.FullName)); + } + + private static bool TypesAreAccessible(Assembly assembly) + { + try + { + assembly.GetTypes(); + return true; + } + catch (Exception e) + { + Trace.Write(string.Format("Error loading types from assembly `{0}': {1}", assembly, e)); + return false; + } + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Hosting/CompositionContainer.cs b/src/CodeEditor.Composition/Hosting/CompositionContainer.cs new file mode 100755 index 0000000..4f9c0eb --- /dev/null +++ b/src/CodeEditor.Composition/Hosting/CompositionContainer.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using CodeEditor.Composition.Primitives; + +namespace CodeEditor.Composition.Hosting +{ + public class CompositionContainer : ICompositionContainer + { + private readonly Dictionary _exports = new Dictionary(); + private readonly Dictionary> _parts = new Dictionary>(); + private readonly IExportDefinitionProvider _exportDefinitionProvider; + private readonly ImportDefinitionProvider _importDefinitionProvider = new ImportDefinitionProvider(); + + public CompositionContainer(Assembly assembly) + : this(new AssemblyCatalog(assembly)) + { + } + + public CompositionContainer(params Assembly[] assemblies) + : this(AssemblyCatalog.For(assemblies)) + { + } + + public CompositionContainer(IExportDefinitionProvider definitionProvider) + { + _exportDefinitionProvider = definitionProvider; + AddExportedValue(this); + } + + public void AddExportedValue(T value) + { + AddExport(new Export(new ExportDefinition(typeof(T), value.GetType()), () => value)); + } + + private void AddExport(Export export) + { + _exports.Add(export.Definition.ContractType, new[] {export}); + } + + public T GetExportedValue() + { + return (T) GetExportedValue(typeof(T)); + } + + public object GetExportedValue(Type contractType) + { + var export = GetExport(contractType); + if (export == null) + throw NoExportFoundError(contractType); + return export.Value; + } + + private Export GetExport(Type contractType) + { + return GetExports(contractType).SingleOrDefault(); + } + + public IEnumerable GetExports(Type contractType) + { + lock (_exports) + return DoGetExports(contractType); + } + + private IEnumerable DoGetExports(Type contractType) + { + Export[] existing; + if (_exports.TryGetValue(contractType, out existing)) + return existing; + var exports = CreateExportsFor(contractType); + _exports.Add(contractType, exports); + return exports; + } + + private Export[] CreateExportsFor(Type contractType) + { + return _exportDefinitionProvider + .GetExports(contractType) + .Select(e => new Export(e, FactoryFor(e))) + .ToArray(); + } + + private Func FactoryFor(ExportDefinition exportDefinition) + { + return AccessorFor(exportDefinition, GetPartFor(exportDefinition.Implementation)); + } + + private Lazy GetPartFor(Type implementation) + { + lock (_parts) + return DoGetPartFor(implementation); + } + + private Lazy DoGetPartFor(Type implementation) + { + Lazy existing; + if (_parts.TryGetValue(implementation, out existing)) + return existing; + var part = NewPartFor(implementation); + _parts.Add(implementation, part); + return part; + } + + private Lazy NewPartFor(Type implementation) + { + return new Lazy(() => InstantiatePart(implementation)); + } + + private object InstantiatePart(Type implementation) + { + var importingConstructor = ImportingConstructorOf(implementation); + var part = importingConstructor != null + ? CreateInstanceThrough(importingConstructor) + : Activator.CreateInstance(implementation); + ComposeParts(part); + return part; + } + + private Func AccessorFor(ExportDefinition exportDefinition, Lazy part) + { + return () => + { + try + { + return part.Value; + } + catch (Exception e) + { + throw new CompositionException(e, + new CompositionError(exportDefinition.ContractType, string.Format("Failed to create `{0}' to satisfy `{1}'!", exportDefinition.Implementation, exportDefinition.ContractType))); + } + }; + } + + private object CreateInstanceThrough(ConstructorInfo importingConstructor) + { + return importingConstructor.Invoke(ExportedValuesFor(importingConstructor).ToArray()); + } + + private IEnumerable ExportedValuesFor(ConstructorInfo importingConstructor) + { + return importingConstructor.GetParameters().Select(p => GetExportedValue(p.ParameterType)); + } + + private static ConstructorInfo ImportingConstructorOf(Type implementation) + { + return implementation.InstanceConstructors().SingleOrDefault(IsImportingConstructor); + } + + private static bool IsImportingConstructor(ConstructorInfo c) + { + return Attribute.IsDefined(c, typeof(ImportingConstructor)); + } + + private void ComposeParts(object part) + { + foreach (var import in ImportsOf(part)) + Satisfy(import, part); + } + + private void Satisfy(ImportDefinition importDefinition, object part) + { + var exports = GetExportsSatisfying(importDefinition); + Validate(importDefinition, exports); + importDefinition.SatisfyWith(exports, part); + } + + private static void Validate(ImportDefinition importDefinition, Export[] exports) + { + switch (importDefinition.Cardinality) + { + case ImportCardinality.One: + if (exports.Length == 0) + throw NoExportFoundError(importDefinition.ContractType); + if (exports.Length != 1) + throw TooManyExportsError(importDefinition, exports); + break; + } + } + + private static CompositionException TooManyExportsError(ImportDefinition importDefinition, Export[] exports) + { + return new CompositionException( + new CompositionError(importDefinition.ContractType, string.Format("Too many exports for `{0}': `{1}'.", importDefinition.ContractType, exports.Select(e => e.Definition.Implementation.FullName).ToList()))); + } + + private Export[] GetExportsSatisfying(ImportDefinition importDefinition) + { + return GetExports(importDefinition.ContractType) + .Where(importDefinition.IsSatisfiableBy) + .ToArray(); + } + + private IEnumerable ImportsOf(object value) + { + return _importDefinitionProvider.ImportsFor(value.GetType()); + } + + private static CompositionException NoExportFoundError(Type contractType) + { + return new CompositionException( + new CompositionError(contractType, string.Format("Export `{0}' not found!", contractType))); + } + } +} diff --git a/src/CodeEditor.Composition/Hosting/DirectoryCatalog.cs b/src/CodeEditor.Composition/Hosting/DirectoryCatalog.cs new file mode 100755 index 0000000..b57b349 --- /dev/null +++ b/src/CodeEditor.Composition/Hosting/DirectoryCatalog.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using CodeEditor.Composition.Primitives; + +namespace CodeEditor.Composition.Hosting +{ + public class DirectoryCatalog : IExportDefinitionProvider + { + private readonly IExportDefinitionProvider _catalog; + + public DirectoryCatalog(string directory) + { + _catalog = AssemblyCatalog.For(AllAssembliesIn(directory)); + } + + public IEnumerable GetExports(Type contractType) + { + return _catalog.GetExports(contractType); + } + + private static Assembly[] AllAssembliesIn(string directory) + { + return Directory.GetFiles(directory, "*.dll").Select(s => Assembly.LoadFrom(s)).ToArray(); + } + } +} diff --git a/src/CodeEditor.Composition/ICompositionContainer.cs b/src/CodeEditor.Composition/ICompositionContainer.cs new file mode 100755 index 0000000..66bcd52 --- /dev/null +++ b/src/CodeEditor.Composition/ICompositionContainer.cs @@ -0,0 +1,8 @@ +using CodeEditor.Composition.Primitives; + +namespace CodeEditor.Composition +{ + public interface ICompositionContainer : IExportProvider + { + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/ImportAttribute.cs b/src/CodeEditor.Composition/ImportAttribute.cs new file mode 100755 index 0000000..260da89 --- /dev/null +++ b/src/CodeEditor.Composition/ImportAttribute.cs @@ -0,0 +1,20 @@ +using System; +using CodeEditor.Composition.Primitives; + +namespace CodeEditor.Composition +{ + /// + /// Imports a service. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class ImportAttribute : ContractAttribute + { + public ImportAttribute(Type contractType) : base(contractType) {} + public ImportAttribute() : base(null) {} + + public virtual ImportCardinality Cardinality + { + get { return ImportCardinality.One; } + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/ImportManyAttribute.cs b/src/CodeEditor.Composition/ImportManyAttribute.cs new file mode 100755 index 0000000..33fc7b1 --- /dev/null +++ b/src/CodeEditor.Composition/ImportManyAttribute.cs @@ -0,0 +1,20 @@ +using System; +using CodeEditor.Composition.Primitives; + +namespace CodeEditor.Composition +{ + /// + /// Imports all providers of a service. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class ImportManyAttribute : ImportAttribute + { + public ImportManyAttribute(Type contractType) : base(contractType) { } + public ImportManyAttribute() : base(null) { } + + public override ImportCardinality Cardinality + { + get { return ImportCardinality.Many; } + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/ImportingConstructor.cs b/src/CodeEditor.Composition/ImportingConstructor.cs new file mode 100755 index 0000000..e66c488 --- /dev/null +++ b/src/CodeEditor.Composition/ImportingConstructor.cs @@ -0,0 +1,9 @@ +using System; + +namespace CodeEditor.Composition +{ + [AttributeUsage(AttributeTargets.Constructor)] + public class ImportingConstructor : Attribute + { + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Lazy.cs b/src/CodeEditor.Composition/Lazy.cs new file mode 100755 index 0000000..a1e55ad --- /dev/null +++ b/src/CodeEditor.Composition/Lazy.cs @@ -0,0 +1,62 @@ +using System; + +namespace CodeEditor.Composition +{ + /// + /// A lazy import to allow selecting the right service based on + /// available metadata. + /// + /// The service is only requested from the container when + /// is called. + /// + /// the contract type + /// the metadata type (must be compatible with a metadata attribute) + public class Lazy : Lazy where T: class + { + private readonly TMetadata _metadata; + + public Lazy(Func valueFactory, TMetadata metadata) : base(valueFactory) + { + _metadata = metadata; + } + + public Lazy(Func valueFactory, TMetadata metadata) : base(() => (T)valueFactory()) + { + _metadata = metadata; + } + + public TMetadata Metadata + { + get { return _metadata; } + } + } + + public class Lazy where T : class + { + private Func _valueFactory; + private bool _hasValue; + private T _value; + + public Lazy(Func valueFactory) + { + _valueFactory = valueFactory; + } + + public T Value + { + get + { + lock (this) + { + if (!_hasValue) + { + _value = _valueFactory(); + _valueFactory = null; + _hasValue = true; + } + return _value; + } + } + } + } +} diff --git a/src/CodeEditor.Composition/Primitives/CustomAttribute.cs b/src/CodeEditor.Composition/Primitives/CustomAttribute.cs new file mode 100755 index 0000000..e2d04ab --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/CustomAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace CodeEditor.Composition.Primitives +{ + static class CustomAttribute where T : Attribute + { + public static T From(MemberInfo member) + { + return (T)Attribute.GetCustomAttribute(member, typeof(T)); + } + + public static IEnumerable AllFrom(MemberInfo member) + { + return Attribute.GetCustomAttributes(member, typeof(T)).Cast(); + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Primitives/Export.cs b/src/CodeEditor.Composition/Primitives/Export.cs new file mode 100755 index 0000000..9dc77f8 --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/Export.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CodeEditor.Composition.Primitives +{ + public interface IExportProvider + { + IEnumerable GetExports(Type contractType); + } + + public static class ExportProviderExtensions + { + public static IEnumerable GetExportsWhereMetadata(this IExportProvider provider, Func predicate, Type contractType) + { + return provider.GetExports(contractType).Where(e => e.Metadata.OfType().Any(predicate)); + } + } + + public class Export : Lazy, IMetadataProvider + { + public Export(ExportDefinition definition, Func valueFactory) : base(valueFactory) + { + Definition = definition; + } + + public ExportDefinition Definition + { + get; private set; + } + + public IEnumerable Metadata + { + get { return Definition.Metadata; } + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Primitives/ExportDefinition.cs b/src/CodeEditor.Composition/Primitives/ExportDefinition.cs new file mode 100755 index 0000000..a59037f --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/ExportDefinition.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace CodeEditor.Composition.Primitives +{ + public class ExportDefinition : IMetadataProvider + { + public ExportDefinition(Type contractType, Type definition) + { + ContractType = contractType; + Implementation = definition; + } + + public Type ContractType { get; private set; } + + public Type Implementation { get; private set; } + + public IEnumerable Metadata + { + get { return Attribute.GetCustomAttributes(Implementation); } + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Primitives/IExportDefinitionProvider.cs b/src/CodeEditor.Composition/Primitives/IExportDefinitionProvider.cs new file mode 100755 index 0000000..8842b49 --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/IExportDefinitionProvider.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace CodeEditor.Composition.Primitives +{ + public interface IExportDefinitionProvider + { + IEnumerable GetExports(Type contractType); + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Primitives/IMetadataProvider.cs b/src/CodeEditor.Composition/Primitives/IMetadataProvider.cs new file mode 100755 index 0000000..1046369 --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/IMetadataProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace CodeEditor.Composition.Primitives +{ + public interface IMetadataProvider + { + IEnumerable Metadata { get; } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Primitives/ImportCardinality.cs b/src/CodeEditor.Composition/Primitives/ImportCardinality.cs new file mode 100755 index 0000000..e59fc0a --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/ImportCardinality.cs @@ -0,0 +1,8 @@ +namespace CodeEditor.Composition.Primitives +{ + public enum ImportCardinality + { + One, + Many + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Primitives/ImportDefinition.cs b/src/CodeEditor.Composition/Primitives/ImportDefinition.cs new file mode 100755 index 0000000..0b0c427 --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/ImportDefinition.cs @@ -0,0 +1,32 @@ +using System; + +namespace CodeEditor.Composition.Primitives +{ + public class ImportDefinition + { + private readonly Action _action; + private readonly Func _constraint; + + public ImportDefinition(Type contractType, ImportCardinality cardinality, Action action, Func constraint) + { + ContractType = contractType; + Cardinality = cardinality; + _action = action; + _constraint = constraint; + } + + public Type ContractType { get; private set; } + + public ImportCardinality Cardinality { get; private set; } + + public void SatisfyWith(Export[] export, object target) + { + _action(export, target); + } + + public bool IsSatisfiableBy(Export export) + { + return _constraint(export); + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Primitives/ImportDefinitionProvider.cs b/src/CodeEditor.Composition/Primitives/ImportDefinitionProvider.cs new file mode 100755 index 0000000..0a98e1a --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/ImportDefinitionProvider.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace CodeEditor.Composition.Primitives +{ + public class ImportDefinitionProvider + { + public IEnumerable ImportsFor(Type type) + { + return type + .InstanceMembers() + .Select(m => new {Member = m, Attribute = ImportAttributeFrom(m)}) + .Where(m => m.Attribute != null) + .Select(m => FromMember(m.Member, m.Attribute)); + } + + private static ImportDefinition FromMember(MemberInfo member, ImportAttribute import) + { + var property = member as PropertyInfo; + if (property != null) + return FromProperty(property, import); + + var field = member as FieldInfo; + if (field != null) + return FromField(field, import); + + throw new CompositionException(new CompositionError(import.ContractType, string.Format("Unsupported import `{0}'.", member))); + } + + private static ImportDefinition FromField(FieldInfo fieldInfo, ImportAttribute import) + { + return ImportDefinitionFrom(import, fieldInfo.FieldType, fieldInfo.SetValue); + } + + private static ImportDefinition FromProperty(PropertyInfo p, ImportAttribute import) + { + return ImportDefinitionFrom(import, p.PropertyType, (part, value) => p.SetValue(part, value, null)); + } + + private static ImportDefinition ImportDefinitionFrom(ImportAttribute import, Type actualType, Action setter) + { + return new ImportDefinitionBuilder(import, actualType, setter).Build(); + } + + private static ImportAttribute ImportAttributeFrom(MemberInfo member) + { + return CustomAttribute.From(member); + } + } + + internal class ImportDefinitionBuilder + { + private readonly ImportAttribute _import; + private readonly Type _actualType; + private readonly Action _setter; + private readonly bool _isLazyType; + private readonly Type _contractType; + private readonly Type _elementType; + + public ImportDefinitionBuilder(ImportAttribute import, Type actualType, Action setter) + { + _import = import; + _actualType = actualType; + _setter = setter; + _elementType = ElementType(); + _isLazyType = IsLazyType(_elementType); + _contractType = ContractType(); + } + + public ImportDefinition Build() + { + return new ImportDefinition(_contractType, Cardinality, BuildSetter(), Constraint()); + } + + private Func Constraint() + { + if (_isLazyType) + return MetadataConstraintFor(MetadataType); + return _ => true; + } + + private static Func MetadataConstraintFor(Type metadataType) + { + return export => export.Metadata.Any(metadataType.IsInstanceOfType); + } + + private ImportCardinality Cardinality + { + get { return _import.Cardinality; } + } + + private Action BuildSetter() + { + return _isLazyType + ? LazySetterFor(_elementType, _setter, Cardinality, MetadataType) + : EagerSetterFor(_elementType, _setter, Cardinality); + } + + private Type MetadataType + { + get { return _elementType.GetGenericArguments()[1]; } + } + + private static Action LazySetterFor(Type elementType, Action setter, ImportCardinality cardinality, Type metadataType) + { + if (cardinality == ImportCardinality.Many) + return (exports, part) => setter(part, ArrayOf(elementType, exports.Select(e => LazyInstanceFor(elementType, metadataType, e)))); + return (exports, part) => setter(part, LazyInstanceFor(elementType, metadataType, exports.Single())); + } + + private static Action EagerSetterFor(Type elementType, Action setter, ImportCardinality cardinality) + { + if (cardinality == ImportCardinality.Many) + return (exports, part) => setter(part, ArrayOf(elementType, exports.Select(e => e.Value))); + return (exports, part) => setter(part, exports.Single().Value); + } + + private static Array ArrayOf(Type elementType, IEnumerable elements) + { + var source = elements.ToArray(); + var result = Array.CreateInstance(elementType, source.Length); + Array.Copy(source, result, source.Length); + return result; + } + + private static object LazyInstanceFor(Type lazyType, Type metadataType, Export export) + { + Func factory = () => export.Value; + var metadata = export.Metadata.Single(metadataType.IsInstanceOfType); + return Activator.CreateInstance(lazyType, new[] { factory, metadata }); + } + + private Type ContractType() + { + return _import.ContractType ?? InferContractTypeFromElementType(); + } + + private Type InferContractTypeFromElementType() + { + return _isLazyType + ? _elementType.GetGenericArguments()[0] + : _elementType; + } + + private Type ElementType() + { + return _import.Cardinality == ImportCardinality.Many + ? _actualType.GetElementType() + : _actualType; + } + + private static bool IsLazyType(Type t) + { + return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Lazy<,>); + } + } +} diff --git a/src/CodeEditor.Composition/Primitives/TypeExtensions.cs b/src/CodeEditor.Composition/Primitives/TypeExtensions.cs new file mode 100755 index 0000000..8f11e90 --- /dev/null +++ b/src/CodeEditor.Composition/Primitives/TypeExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Reflection; + +namespace CodeEditor.Composition.Primitives +{ + static class TypeExtensions + { + private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + public static MemberInfo[] InstanceMembers(this Type type) + { + return type.GetMembers(InstanceMemberFlags); + } + + public static ConstructorInfo[] InstanceConstructors(this Type type) + { + return type.GetConstructors(InstanceMemberFlags); + } + } +} \ No newline at end of file diff --git a/src/CodeEditor.Composition/Properties/AssemblyInfo.cs b/src/CodeEditor.Composition/Properties/AssemblyInfo.cs new file mode 100755 index 0000000..50bfc28 --- /dev/null +++ b/src/CodeEditor.Composition/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("CodeEditor.Composition")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("CodeEditor")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")]