using System; using System.Linq; using System.Text; namespace ServiceClientGenerator.Endpoints { using Json.LitJson; using ServiceClientGenerator; using ServiceClientGenerator.Endpoints.Partitions; using System.Collections.Generic; using System.Data; /// /// Translates Endpoints RuleSet to valid set of C# IF statements. /// Every rule translates to IF(condition1 && condition2 && ...). /// Tree rule translates to more inner IFs. /// Every condition is a call to standard function, that can have multiple parameters including other function calls. /// Condition can return a result that can be stored as internal reference which can be used in other rules/conditions. /// Every final IF statement either returns an Endpoint or throws meaningful AmazonClientException. /// /// /// This shows example of generated code. /// /// if (StringEquals(refs["Region"], "us-east-1") && BooleanEquals(refs["UseFIPS"], false) && BooleanEquals(refs["UseDualStack"], false)) /// { /// return new Endpoint(Interpolate(@"{url#scheme}://{url#authority}{url#path}", refs)); /// } /// /// public static class CodeGen { const int INITIAL_INDENT = 3; const int INDENT_SIZE = 4; /// /// Translates Endpoints RuleSet to valid set of C# IF statements /// public static string GenerateRules(RuleSet rules) { var code = new StringBuilder(); foreach (Rule rule in rules.rules) { GenerateRule(rule, code, INITIAL_INDENT); } return code.ToString(); } private static void GenerateRule(Rule rule, StringBuilder code, int indent) { const string AND = @" && "; var codeIndent = new string(' ', indent * INDENT_SIZE); var singleIndent = new string(' ', INDENT_SIZE); var innerIndent = codeIndent; var nonEmptyConditions = rule.conditions.Length > 0; if (nonEmptyConditions) { code.Append($@"{codeIndent}if ("); foreach (var condition in rule.conditions) { GenerateStandardFunction(condition.Function, code, true); code.Append(AND); } code.Remove(code.Length - AND.Length, AND.Length); code.Append($@")"); code.AppendLine(); code.AppendLine($@"{codeIndent}{{"); innerIndent = codeIndent + singleIndent; } switch (rule.Type) { case RuleType.Error: { var value = MaybeInterpolate(rule.error); code.AppendLine($@"{innerIndent}throw new AmazonClientException({value});"); break; } case RuleType.Endpoint: { string url = GenerateUrl(rule.endpoint.url); var properties = rule.endpoint.properties != null ? rule.endpoint.properties.ToJson().SanitizeQuotes() : String.Empty; var headers = rule.endpoint.headers != null ? rule.endpoint.headers.ToJson().SanitizeQuotes() : String.Empty; code.AppendLine($@"{innerIndent}return new Endpoint({url}, InterpolateJson(@""{properties}"", refs), InterpolateJson(@""{headers}"", refs));"); break; } case RuleType.Tree: { foreach(var subRule in rule.rules) { GenerateRule(subRule, code, indent + (nonEmptyConditions ? 1 : 0)); } break; } default: throw new Exception("Unknown rule type."); } if (nonEmptyConditions) { code.AppendLine($@"{codeIndent}}}"); } } /// /// Url can be defined as template string | reference | function /// private static string GenerateUrl(JsonData url) { if (url.IsString) { return MaybeInterpolate((string)url); } if (url.IsObject) { if (url.PropertyNames.Contains("ref")) { return $"(string)refs[\"{url["ref"]}\"]"; } if (url.PropertyNames.Contains("fn")) { var function = new StringBuilder(); GenerateStandardFunction(new Function((string)url["fn"], url["argv"]), function); return $"(string){function}"; } } throw new Exception("Unknown Endpoint Url definition."); } private static string MaybeInterpolate(string s) { s = s.SanitizeQuotes(); return s.Contains('{') ? $@"Interpolate(@""{s}"", refs)" : $@"""{s}"""; } private static void GenerateStandardFunction(Function function, StringBuilder code, bool isCondition = false) { const string COMMA_AND_SPACE = @", "; if (!string.IsNullOrEmpty(function.Assign)) { code.Append($@"(refs[""{function.Assign}""] = "); } switch (function.Name) { case "not": { code.Append(@"!"); GenerateFunctionArgument(function.Name, 0, function.Arguments[0], code); break; } default: { code.Append($@"{GetFunctionName(function.Name)}("); for (int i = 0; i < function.Arguments.Count; i++) { var arg = function.Arguments[i]; GenerateFunctionArgument(function.Name, i, arg, code); code.Append(COMMA_AND_SPACE); } code.Remove(code.Length - COMMA_AND_SPACE.Length, COMMA_AND_SPACE.Length); code.Append(@")"); break; } } if (!string.IsNullOrEmpty(function.Assign)) { code.Append(@") != null"); } else if (isCondition && !IsBooleanFunction(function.Name)) { code.Append(@" != null"); } } private static string GetFunctionName(string name) { if (name == "stringEquals" || name == "booleanEquals") return "Equals"; return name.Replace("aws.", "").ToUpperFirstCharacter(); } private static bool IsBooleanFunction(string name) { switch (name) { case "not": case "isSet": case "stringEquals": case "booleanEquals": case "isValidHostLabel": case "aws.isVirtualHostableS3Bucket": return true; default: return false; } } // functions parameter types private static Dictionary> functionParamTypes = new Dictionary> { ["aws.partition"] = new List { "string" }, ["aws.parseArn"] = new List { "string" }, ["aws.isVirtualHostableS3Bucket"] = new List { "string", "bool" }, ["isValidHostLabel"] = new List { "string", "bool" }, ["parseURL"] = new List { "string" }, ["uriEncode"] = new List { "string" }, ["substring"] = new List { "string", "int", "int", "bool" } }; private static void GenerateFunctionArgument(string functionName, int argumentIndex, Argument argument, StringBuilder code) { switch (argument) { case StringArgument arg: { var value = MaybeInterpolate(arg.Value); code.Append($"{value}"); break; } case IntegerArgument arg: { code.Append($"{arg.Value}"); break; } case BoolArgument arg: { code.Append($"{arg.Value.ToString().ToLower()}"); // true | false break; } case ReferenceArgument arg: { AddTypeCast(functionName, argumentIndex, code); code.Append($@"refs[""{arg.ReferenceName}""]"); break; } case FunctionArgument arg: { AddTypeCast(functionName, argumentIndex, code); GenerateStandardFunction(arg.Value, code); break; } default: throw new Exception("Unknown argument type."); } } private static void AddTypeCast(string functionName, int argumentIndex, StringBuilder code) { if (functionParamTypes.ContainsKey(functionName)) { var argumentType = functionParamTypes[functionName][argumentIndex]; code.Append($@"({argumentType})"); } } } }