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);
        }
Example #2
0
        /// <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());
            }
        }