// Copyright (c) Microsoft. All rights reserved. using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using Microsoft.Agents.ObjectModel; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen; internal abstract class CodeTemplate { private bool _endsWithNewline; private string CurrentIndentField { get; set; } = string.Empty; /// /// Create the template output /// public abstract string TransformText(); #region Object Model helpers public static string VariableName(PropertyPath path) => Throw.IfNull(path.VariableName); public static string VariableScope(PropertyPath path) => Throw.IfNull(path.NamespaceAlias); public static string FormatBoolValue(bool? value, bool defaultValue = false) => value ?? defaultValue ? "true" : "false"; public static string FormatStringValue(string? value) { if (value is null) { return "null"; } if (value.Contains('\n') || value.Contains('\r')) { return @$"""""""{Environment.NewLine}{value}{Environment.NewLine}"""""""; } if (value.Contains('"') || value.Contains('\\')) { return @$"""""""{value}"""""""; } return @$"""{value}"""; } public static string FormatValue(string? value) { if (typeof(TValue) == typeof(string)) { return FormatStringValue(value); } if (value is null) { return "null"; } if (typeof(TValue).IsEnum) { return $"{typeof(TValue).Name}.{value}"; } return $"{value}"; } public static string FormatDataValue(DataValue value) => value switch { BlankDataValue => "null", BooleanDataValue booleanValue => FormatBoolValue(booleanValue.Value), FloatDataValue decimalValue => $"{decimalValue.Value}", NumberDataValue numberValue => $"{numberValue.Value}", DateDataValue dateValue => $"new DateTime({dateValue.Value.Ticks}, DateTimeKind.{dateValue.Value.Kind})", DateTimeDataValue datetimeValue => $"new DateTimeOffset({datetimeValue.Value.Ticks}, TimeSpan.FromTicks({datetimeValue.Value.Offset}))", TimeDataValue timeValue => $"TimeSpan.FromTicks({timeValue.Value.Ticks})", StringDataValue stringValue => FormatStringValue(stringValue.Value), OptionDataValue optionValue => @$"""{optionValue.Value}""", // Indenting is important here to make the generated code readable. Don't change it without testing the output. RecordDataValue recordValue => $""" [ {string.Join(",\n ", recordValue.Properties.Select(p => $"[\"{p.Key}\"] = {FormatDataValue(p.Value)}"))} ] """, _ => throw new DeclarativeModelException($"Unable to format '{value.GetType().Name}'"), }; public static TTarget FormatEnum(TSource value, IDictionary map, TTarget? defaultValue = default) { if (map.TryGetValue(value, out TTarget? target)) { return target; } if (defaultValue is null) { throw new DeclarativeModelException($"No default value suppied for '{typeof(TTarget).Name}'"); } return defaultValue; } public static string GetTypeAlias() => GetTypeAlias(typeof(TValue)); public static string GetTypeAlias(Type type) { return type switch { Type t when t == typeof(bool) => "bool", Type t when t == typeof(byte) => "byte", Type t when t == typeof(sbyte) => "sbyte", Type t when t == typeof(char) => "char", Type t when t == typeof(decimal) => "decimal", Type t when t == typeof(double) => "double", Type t when t == typeof(float) => "float", Type t when t == typeof(int) => "int", Type t when t == typeof(uint) => "uint", Type t when t == typeof(long) => "long", Type t when t == typeof(ulong) => "ulong", Type t when t == typeof(nint) => "nint", Type t when t == typeof(nuint) => "nuint", Type t when t == typeof(short) => "short", Type t when t == typeof(ushort) => "ushort", Type t when t == typeof(string) => "string", Type t when t == typeof(object) => "object", _ => type.Name }; } #endregion #region Properties /// /// The string builder that generation-time code is using to assemble generated output /// public StringBuilder GenerationEnvironment { get { return field ??= new StringBuilder(); } set; } /// /// The error collection for the generation process /// public CompilerErrorCollection Errors => field ??= []; /// /// A list of the lengths of each indent that was added with PushIndent /// private List IndentLengths { get => field ??= []; } /// /// Gets the current indent we use when adding lines to the output /// public string CurrentIndent { get { return this.CurrentIndentField; } } /// /// Current transformation session /// public virtual IDictionary? Session { get; set; } #endregion #region Transform-time helpers /// /// Write text directly into the generated output /// public void Write(string textToAppend) { if (string.IsNullOrEmpty(textToAppend)) { return; } // If we're starting off, or if the previous text ended with a newline, // we have to append the current indent first. if ((this.GenerationEnvironment.Length == 0) || this._endsWithNewline) { this.GenerationEnvironment.Append(this.CurrentIndentField); this._endsWithNewline = false; } // Check if the current text ends with a newline if (textToAppend.EndsWith(Environment.NewLine, StringComparison.CurrentCulture)) { this._endsWithNewline = true; } // This is an optimization. If the current indent is "", then we don't have to do any // of the more complex stuff further down. if (this.CurrentIndentField.Length == 0) { this.GenerationEnvironment.Append(textToAppend); return; } // Everywhere there is a newline in the text, add an indent after it textToAppend = textToAppend.Replace(Environment.NewLine, Environment.NewLine + this.CurrentIndentField); // If the text ends with a newline, then we should strip off the indent added at the very end // because the appropriate indent will be added when the next time Write() is called if (this._endsWithNewline) { this.GenerationEnvironment.Append(textToAppend, 0, textToAppend.Length - this.CurrentIndentField.Length); } else { this.GenerationEnvironment.Append(textToAppend); } } /// /// Write text directly into the generated output /// public void WriteLine(string textToAppend) { this.Write(textToAppend); this.GenerationEnvironment.AppendLine(); this._endsWithNewline = true; } /// /// Write formatted text directly into the generated output /// public void Write(string format, params object[] args) { this.Write(string.Format(CultureInfo.CurrentCulture, format, args)); } /// /// Write formatted text directly into the generated output /// public void WriteLine(string format, params object[] args) { this.WriteLine(string.Format(CultureInfo.CurrentCulture, format, args)); } /// /// Raise an error /// public void Error(string message) { CompilerError error = new() { ErrorText = message }; this.Errors.Add(error); } /// /// Raise a warning /// public void Warning(string message) { CompilerError error = new() { ErrorText = message, IsWarning = true }; error.ErrorText = message; error.IsWarning = true; this.Errors.Add(error); } /// /// Increase the indent /// public void PushIndent(string indent) { if (indent is null) { throw new ArgumentNullException(nameof(indent)); } this.CurrentIndentField += indent; this.IndentLengths.Add(indent.Length); } /// /// Remove the last indent that was added with PushIndent /// public string PopIndent() { string returnValue = string.Empty; if (this.IndentLengths.Count > 0) { int indentLength = this.IndentLengths[this.IndentLengths.Count - 1]; this.IndentLengths.RemoveAt(this.IndentLengths.Count - 1); if (indentLength > 0) { returnValue = this.CurrentIndentField.Substring(this.CurrentIndentField.Length - indentLength); this.CurrentIndentField = this.CurrentIndentField.Remove(this.CurrentIndentField.Length - indentLength); } } return returnValue; } /// /// Remove any indentation /// public void ClearIndent() { this.IndentLengths.Clear(); this.CurrentIndentField = string.Empty; } #endregion #region ToString Helpers /// /// Utility class to produce culture-oriented representation of an object as a string. /// public sealed class ToStringInstanceHelper { /// /// This is called from the compile/run appdomain to convert objects within an expression block to a string /// #pragma warning disable CA1822 // Required to be non-static for use in generated code public string ToStringWithCulture(object objectToConvert) => $"{objectToConvert}"; #pragma warning restore CA1822 } /// /// Helper to produce culture-oriented representation of an object as a string /// public ToStringInstanceHelper ToStringHelper { get; } = new(); #endregion }