private static IComponentFactory GetComponentJson(IExceptionContext ectx, Type signatureType, string name, JObject settings, ModuleCatalog catalog) { Contracts.AssertValue(ectx); ectx.AssertValue(signatureType); ectx.AssertNonEmpty(name); ectx.AssertValueOrNull(settings); ectx.AssertValue(catalog); if (!catalog.TryGetComponentKind(signatureType, out string kind)) { throw ectx.Except($"Component type '{signatureType}' is not a valid signature type."); } if (!catalog.TryFindComponent(kind, name, out ModuleCatalog.ComponentInfo component)) { var available = catalog.GetAllComponents(kind).Select(x => $"'{x.Name}'"); throw ectx.Except($"Component '{name}' of kind '{kind}' is not found. Available components are: {string.Join(", ", available)}"); } var inputBuilder = new InputBuilder(ectx, component.ArgumentType, catalog); if (settings != null) { foreach (var pair in settings) { if (!inputBuilder.TrySetValueJson(pair.Key, pair.Value)) { throw ectx.Except($"Unexpected value for component '{name}', field '{pair.Key}': '{pair.Value}'"); } } } var missing = inputBuilder.GetMissingValues().ToArray(); if (missing.Length > 0) { throw ectx.Except($"The following required inputs were not provided for component '{name}': {string.Join(", ", missing)}"); } return(inputBuilder.GetInstance() as IComponentFactory); }
/// <summary> /// Build a token for component default value. This will look up the component in the catalog, and if it finds an entry, it will /// build a JSON structure that would be parsed into the default value. /// /// This is an inherently fragile setup in case when the factory is not trivial, but it will work well for 'property bag' factories /// that we are currently using. /// </summary> private static JToken BuildComponentToken(IExceptionContext ectx, IComponentFactory value, ModuleCatalog catalog) { Contracts.AssertValueOrNull(ectx); ectx.AssertValue(value); ectx.AssertValue(catalog); var type = value.GetType(); ModuleCatalog.ComponentInfo componentInfo; if (!catalog.TryFindComponent(type, out componentInfo)) { // The default component is not in the catalog. This is, technically, allowed, but it means that there's no JSON representation // for the default value. We will emit the one the won't parse back. return(new JValue("(custom component)")); } ectx.Assert(componentInfo.ArgumentType == type); // Try to invoke default ctor for the factory to obtain defaults. object defaults; try { defaults = Activator.CreateInstance(type); } catch (MissingMemberException ex) { // There was no default constructor found. // This should never happen, since ModuleCatalog would error out if there is no default ctor. ectx.Assert(false); throw ectx.Except(ex, "Couldn't find default constructor"); } var jResult = new JObject(); var jSettings = new JObject(); jResult[FieldNames.Name] = componentInfo.Name; // Iterate over all fields of the factory object, and compare the values with the defaults. // If the value differs, insert it into the settings object. bool anyValue = false; foreach (var fieldInfo in type.GetFields()) { var attr = fieldInfo.GetCustomAttributes(typeof(ArgumentAttribute), false).FirstOrDefault() as ArgumentAttribute; if (attr == null || attr.Visibility == ArgumentAttribute.VisibilityType.CmdLineOnly) { continue; } ectx.Assert(!fieldInfo.IsStatic && !fieldInfo.IsInitOnly && !fieldInfo.IsLiteral); bool needValue = false; object actualValue = fieldInfo.GetValue(value); if (attr.IsRequired) { needValue = true; } else { object defaultValue = fieldInfo.GetValue(defaults); needValue = !Equals(actualValue, defaultValue); } if (!needValue) { continue; } jSettings[attr.Name ?? fieldInfo.Name] = BuildValueToken(ectx, actualValue, fieldInfo.FieldType, catalog); anyValue = true; } if (anyValue) { jResult[FieldNames.Settings] = jSettings; } return(jResult); }
public JObject GetJsonObject(object instance, Dictionary <string, List <ParameterBinding> > inputBindingMap, Dictionary <ParameterBinding, VariableBinding> inputMap) { Contracts.CheckValue(instance, nameof(instance)); Contracts.Check(instance.GetType() == _type); var result = new JObject(); var defaults = Activator.CreateInstance(_type); for (int i = 0; i < _fields.Length; i++) { var field = _fields[i]; var attr = _attrs[i]; var instanceVal = field.GetValue(instance); var defaultsVal = field.GetValue(defaults); if (inputBindingMap.TryGetValue(field.Name, out List <ParameterBinding> bindings)) { // Handle variables. Contracts.Assert(bindings.Count > 0); VariableBinding varBinding; var paramBinding = bindings[0]; if (paramBinding is SimpleParameterBinding) { Contracts.Assert(bindings.Count == 1); bool success = inputMap.TryGetValue(paramBinding, out varBinding); Contracts.Assert(success); Contracts.AssertValue(varBinding); result.Add(field.Name, new JValue(varBinding.ToJson())); } else if (paramBinding is ArrayIndexParameterBinding) { // Array parameter bindings. var array = new JArray(); foreach (var parameterBinding in bindings) { Contracts.Assert(parameterBinding is ArrayIndexParameterBinding); bool success = inputMap.TryGetValue(parameterBinding, out varBinding); Contracts.Assert(success); Contracts.AssertValue(varBinding); array.Add(new JValue(varBinding.ToJson())); } result.Add(field.Name, array); } else { // Dictionary parameter bindings. Not supported yet. Contracts.Assert(paramBinding is DictionaryKeyParameterBinding); throw Contracts.ExceptNotImpl("Dictionary of variables not yet implemented."); } } else if (instanceVal == null && defaultsVal != null) { // Handle null values. result.Add(field.Name, new JValue(instanceVal)); } else if (instanceVal != null && (attr.Input.IsRequired || !instanceVal.Equals(defaultsVal))) { // A required field will be serialized regardless of whether or not its value is identical to the default. var type = instanceVal.GetType(); if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional <>)) { var isExplicit = ExtractOptional(ref instanceVal, ref type); if (!isExplicit) { continue; } } if (type == typeof(JArray)) { result.Add(field.Name, (JArray)instanceVal); } else if (type.IsGenericType && ((type.GetGenericTypeDefinition() == typeof(Var <>)) || type.GetGenericTypeDefinition() == typeof(ArrayVar <>) || type.GetGenericTypeDefinition() == typeof(DictionaryVar <>))) { result.Add(field.Name, new JValue($"${((IVarSerializationHelper)instanceVal).VarName}")); } else if (type == typeof(bool) || type == typeof(string) || type == typeof(char) || type == typeof(double) || type == typeof(float) || type == typeof(int) || type == typeof(long) || type == typeof(uint) || type == typeof(ulong)) { // Handle simple types. result.Add(field.Name, new JValue(instanceVal)); } else if (type.IsEnum) { // Handle enums. result.Add(field.Name, new JValue(instanceVal.ToString())); } else if (type.IsArray) { // Handle arrays. var array = (Array)instanceVal; var jarray = new JArray(); var elementType = type.GetElementType(); if (elementType == typeof(bool) || elementType == typeof(string) || elementType == typeof(char) || elementType == typeof(double) || elementType == typeof(float) || elementType == typeof(int) || elementType == typeof(long) || elementType == typeof(uint) || elementType == typeof(ulong)) { foreach (object item in array) { jarray.Add(new JValue(item)); } } else { var builder = new InputBuilder(_ectx, elementType, _catalog); foreach (object item in array) { jarray.Add(builder.GetJsonObject(item, inputBindingMap, inputMap)); } } result.Add(field.Name, jarray); } else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary <,>) && type.GetGenericArguments()[0] == typeof(string)) { // Handle dictionaries. // REVIEW: Needs to be implemented when we will have entry point arguments that contain dictionaries. } else if (typeof(IComponentFactory).IsAssignableFrom(type)) { // Handle component factories. bool success = _catalog.TryFindComponent(type, out ModuleCatalog.ComponentInfo instanceInfo); Contracts.Assert(success); var builder = new InputBuilder(_ectx, type, _catalog); var instSettings = builder.GetJsonObject(instanceVal, inputBindingMap, inputMap); ModuleCatalog.ComponentInfo defaultInfo = null; JObject defSettings = new JObject(); if (defaultsVal != null) { var deftype = defaultsVal.GetType(); if (deftype.IsGenericType && deftype.GetGenericTypeDefinition() == typeof(Optional <>)) { ExtractOptional(ref defaultsVal, ref deftype); } success = _catalog.TryFindComponent(deftype, out defaultInfo); Contracts.Assert(success); builder = new InputBuilder(_ectx, deftype, _catalog); defSettings = builder.GetJsonObject(defaultsVal, inputBindingMap, inputMap); } if (instanceInfo.Name != defaultInfo?.Name || instSettings.ToString() != defSettings.ToString()) { var jcomponent = new JObject { { FieldNames.Name, new JValue(instanceInfo.Name) } }; if (instSettings.ToString() != defSettings.ToString()) { jcomponent.Add(FieldNames.Settings, instSettings); } result.Add(field.Name, jcomponent); } } else { // REVIEW: pass in the bindings once we support variables in inner fields. // Handle structs. var builder = new InputBuilder(_ectx, type, _catalog); result.Add(field.Name, builder.GetJsonObject(instanceVal, new Dictionary <string, List <ParameterBinding> >(), new Dictionary <ParameterBinding, VariableBinding>())); } } } return(result); }
public static string GetValue(ModuleCatalog catalog, Type fieldType, object fieldValue, GeneratedClasses generatedClasses, string rootNameSpace) { if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Var <>)) { return($"new Var<{GetCSharpTypeName(fieldType.GetGenericTypeArgumentsEx()[0])}>()"); } if (fieldType.IsArray && Var <int> .CheckType(fieldType.GetElementType())) { return($"new ArrayVar<{GetCSharpTypeName(fieldType.GetElementType())}>()"); } if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Dictionary <,>) && fieldType.GetGenericTypeArgumentsEx()[0] == typeof(string)) { return($"new DictionaryVar<{GetCSharpTypeName(fieldType.GetGenericTypeArgumentsEx()[1])}>()"); } if (Var <int> .CheckType(fieldType)) { return($"new Var<{GetCSharpTypeName(fieldType)}>()"); } if (fieldValue == null) { return(null); } if (!fieldType.IsInterface) { try { var defaultFieldValue = Activator.CreateInstance(fieldType); if (defaultFieldValue == fieldValue) { return(null); } } catch (MissingMethodException) { // No parameterless constructor, ignore. } } var typeEnum = TlcModule.GetDataType(fieldType); fieldType = ExtractOptionalOrNullableType(fieldType, out bool isNullable, out bool isOptional); switch (typeEnum) { case TlcModule.DataKind.Array: var arr = fieldValue as Array; if (arr != null && arr.GetLength(0) > 0) { return($"{{ {string.Join(", ", arr.Cast<object>().Select(item => GetValue(catalog, fieldType.GetElementType(), item, generatedClasses, rootNameSpace)))} }}"); } return(null); case TlcModule.DataKind.String: var strval = fieldValue as string; if (strval != null) { return(Quote(strval)); } return(null); case TlcModule.DataKind.Float: if (fieldValue is double d) { if (double.IsPositiveInfinity(d)) { return("double.PositiveInfinity"); } if (double.IsNegativeInfinity(d)) { return("double.NegativeInfinity"); } if (d != 0) { return(d.ToString("R") + "d"); } } else if (fieldValue is float f) { if (float.IsPositiveInfinity(f)) { return("float.PositiveInfinity"); } if (float.IsNegativeInfinity(f)) { return("float.NegativeInfinity"); } if (f != 0) { return(f.ToString("R") + "f"); } } return(null); case TlcModule.DataKind.Int: if (fieldValue is int i) { if (i != 0) { return(i.ToString()); } } else if (fieldValue is long l) { if (l != 0) { return(l.ToString()); } } return(null); case TlcModule.DataKind.Bool: return((bool)fieldValue ? "true" : "false"); case TlcModule.DataKind.Enum: string enumAsString = fieldValue.ToString(); if (fieldType.GetField(enumAsString).GetCustomAttribute <HideEnumValueAttribute>() != null) { // The default value for the enum has the hiding attribute on it. We will search for // alternate names. Regrettably I see no way beyond a manual scan. string unhiddenName = Enum.GetNames(fieldType).Zip(Enum.GetValues(fieldType).Cast <object>(), (name, val) => (name, val)) .Where(pair => pair.val.Equals(fieldValue)) .Where(pair => fieldType.GetField(pair.name).GetCustomAttribute <HideEnumValueAttribute>() == null) .Select(pair => pair.name).FirstOrDefault(); enumAsString = unhiddenName ?? throw Contracts.Except($"Could not find unhidden alternative for '{fieldValue}' in type '{fieldType}'"); } if (generatedClasses.IsGenerated(fieldType.FullName)) { return(generatedClasses.GetApiName(fieldType, rootNameSpace) + "." + enumAsString); } else { return(generatedClasses.GetApiName(fieldType, "Runtime") + "." + enumAsString); } case TlcModule.DataKind.Char: return($"'{GetCharAsString((char)fieldValue)}'"); case TlcModule.DataKind.Component: var type = fieldValue.GetType(); ModuleCatalog.ComponentInfo componentInfo; if (!catalog.TryFindComponent(fieldType, type, out componentInfo)) { return(null); } object defaultComponent = null; try { defaultComponent = Activator.CreateInstance(componentInfo.ArgumentType); } catch (MissingMethodException) { // No parameterless constructor, ignore. } var propertyBag = new List <string>(); if (defaultComponent != null) { foreach (var fieldInfo in componentInfo.ArgumentType.GetFields()) { var inputAttr = fieldInfo.GetCustomAttributes(typeof(ArgumentAttribute), false).FirstOrDefault() as ArgumentAttribute; if (inputAttr == null || inputAttr.Visibility == ArgumentAttribute.VisibilityType.CmdLineOnly) { continue; } if (fieldInfo.FieldType == typeof(JArray) || fieldInfo.FieldType == typeof(JObject)) { continue; } var propertyValue = GetValue(catalog, fieldInfo.FieldType, fieldInfo.GetValue(fieldValue), generatedClasses, rootNameSpace); var defaultPropertyValue = GetValue(catalog, fieldInfo.FieldType, fieldInfo.GetValue(defaultComponent), generatedClasses, rootNameSpace); if (propertyValue != defaultPropertyValue) { propertyBag.Add($"{Capitalize(inputAttr.Name ?? fieldInfo.Name)} = {propertyValue}"); } } } var properties = propertyBag.Count > 0 ? $" {{ {string.Join(", ", propertyBag)} }}" : ""; return($"new {GetComponentName(componentInfo)}(){properties}"); case TlcModule.DataKind.Unknown: return($"new {generatedClasses.GetApiName(fieldType, rootNameSpace)}()"); default: return(fieldValue.ToString()); } }