Skip to content

Commit

Permalink
Add option to customize code generation with custom liquid template
Browse files Browse the repository at this point in the history
  • Loading branch information
cezarypiatek committed Jan 21, 2024
1 parent f2d8535 commit 51b249d
Show file tree
Hide file tree
Showing 10 changed files with 657 additions and 490 deletions.
203 changes: 203 additions & 0 deletions src/WireMockInspector/CodeGenerators/CSharpFormatter.cs
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json.Linq;

namespace WireMockInspector.ViewModels;

internal static class CSharpFormatter
{
#region Reserved Keywords

private static readonly HashSet<string> CSharpReservedKeywords = new(new[]
{
"abstract",
"as",
"base",
"bool",
"break",
"byte",
"case",
"catch",
"char",
"checked",
"class",
"const",
"continue",
"decimal",
"default",
"delegate",
"do",
"double",
"else",
"enum",
"event",
"explicit",
"extern",
"false",
"finally",
"fixed",
"float",
"for",
"foreach",
"goto",
"if",
"implicit",
"in",
"int",
"interface",
"internal",
"is",
"lock",
"long",
"namespace",
"new",
"null",
"object",
"operator",
"out",
"override",
"params",
"private",
"protected",
"public",
"readonly",
"ref",
"return",
"sbyte",
"sealed",
"short",
"sizeof",
"stackalloc",
"static",
"string",
"struct",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"uint",
"ulong",
"unchecked",
"unsafe",
"ushort",
"using",
"virtual",
"void",
"volatile",
"while"
});

#endregion

private const string Null = "null";


public static string? TryToConvertJsonToAnonymousObject(object input, int ind = 0)
{
try
{
return input switch
{
JToken token => ConvertJsonToAnonymousObjectDefinition(token, ind),
string text => ConvertJsonToAnonymousObjectDefinition(JToken.Parse(text), ind),
_ => null
};
}
catch (Exception e)
{
return null;
}
}

public static string ConvertJsonToAnonymousObjectDefinition(JToken token, int ind = 0)
{
return token switch
{
JArray jArray => FormatArray(jArray, ind),
JObject jObject => FormatObject(jObject, ind),
JValue jValue => jValue.Type switch
{
JTokenType.None => Null,
JTokenType.Integer => jValue.Value != null
? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value)
: Null,
JTokenType.Float => jValue.Value != null
? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value)
: Null,
JTokenType.String => ToCSharpStringLiteral(jValue.Value?.ToString()),
JTokenType.Boolean => jValue.Value != null
? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value).ToLower()
: Null,
JTokenType.Null => Null,
JTokenType.Undefined => Null,
JTokenType.Date when jValue.Value is DateTime dateValue =>
$"DateTime.Parse({ToCSharpStringLiteral(dateValue.ToString("s"))})",
_ => $"UNHANDLED_CASE: {jValue.Type}"
},
_ => $"UNHANDLED_CASE: {token}"
};
}

public static string ToCSharpStringLiteral(string? value)
{
if (string.IsNullOrEmpty(value))
{
return "\"\"";
}

if (value.Contains('\n'))
{
var escapedValue = value?.Replace("\"", "\"\"") ?? string.Empty;
return $"@\"{escapedValue}\"";
}
else
{
var escapedValue = value?.Replace("\"", "\\\"") ?? string.Empty;
return $"\"{escapedValue}\"";
}
}

public static string FormatPropertyName(string propertyName)
{
return CSharpReservedKeywords.Contains(propertyName) ? "@" + propertyName : propertyName;
}

private static string FormatObject(JObject jObject, int ind)
{

var indStr = new string(' ', 4 * ind);
var indStrSub = new string(' ', 4 * (ind + 1));
var shouldBeDictionary = jObject.Properties().Any(x => Char.IsDigit(x.Name[0]));

if (shouldBeDictionary)
{
var items = jObject.Properties().Select(x => $"[\"{x.Name}\"] = {ConvertJsonToAnonymousObjectDefinition(x.Value, ind + 1)}");
return $"new Dictionary<string, object>\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}";
}
else
{
var items = jObject.Properties().Select(x => $"{FormatPropertyName(x.Name)} = {ConvertJsonToAnonymousObjectDefinition(x.Value, ind + 1)}");
return $"new\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}";
}


}

private static string FormatArray(JArray jArray, int ind)
{
var hasComplexItems = jArray.FirstOrDefault() is JObject or JArray;
var items = jArray.Select(x => ConvertJsonToAnonymousObjectDefinition(x, hasComplexItems ? ind + 1 : ind));
if (hasComplexItems)
{
var indStr = new string(' ', 4 * ind);
var indStrSub = new string(' ', 4 * (ind + 1));
return $"new []\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}";
}

return $"new [] {{ {string.Join(", ", items)} }}";
}
}
149 changes: 149 additions & 0 deletions src/WireMockInspector/CodeGenerators/MappingCodeGenerator.cs
@@ -0,0 +1,149 @@
using System.IO;
using System.Linq;
using System.Reflection;
using Fluid;
using Fluid.Values;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WireMock.Admin.Requests;
using WireMockInspector.ViewModels;

namespace WireMockInspector.CodeGenerators;

public static class MappingCodeGenerator
{

class JsonDataSourceReader
{
object? ConvertJsonToObject(JToken xDocument)
{
return xDocument switch
{
JArray jArray => jArray.Select(ConvertJsonToObject).ToArray(),
JObject jObject => jObject.Properties().ToDictionary(x => x.Name, x => ConvertJsonToObject(x.Value)),
JValue jValue => jValue.Value,
_ => null
};
}

public object? Read(string content)
{
var json = JToken.Parse(content);

if (json is JObject jo && jo.ContainsKey("$schema"))
{
jo.Remove("$schema");
}

return ConvertJsonToObject(json);
}
}

public static string EscapeStringForCSharp(string value) => CSharpFormatter.ToCSharpStringLiteral(value);

private static string ReadEmbeddedResource(string resourceName)
{
// Get the current assembly
Assembly assembly = Assembly.GetExecutingAssembly();

// Using stream to read the embedded file.
using var stream = assembly.GetManifestResourceStream(resourceName);
// Make sure the resource is available
if (stream == null) throw new FileNotFoundException("The specified embedded resource cannot be found.", resourceName);
using StreamReader reader = new StreamReader(stream);
return reader.ReadToEnd();
}
public const string DefaultTemplateName = "(default)";
public static string GenerateCSharpCode(LogRequestModel logRequest, LogResponseModel logResponse, MappingCodeGeneratorConfigViewModel config)
{
var options = new TemplateOptions();
options.ValueConverters.Add(o => o is JToken t? t.ToString(): null );
options.Filters.AddFilter("escape_string_for_csharp", (input, arguments, templateContext) => new StringValue(EscapeStringForCSharp(input.ToStringValue()) ));
options.Filters.AddFilter("format_as_anonymous_object", (input, arguments, templateContext) =>
{
var ind = arguments.Values.FirstOrDefault() is NumberValue nv ? (int)nv.ToNumberValue() : 0;
return input switch
{
StringValue dv => CSharpFormatter.TryToConvertJsonToAnonymousObject(dv.ToStringValue(), ind) switch
{
{ } s => new StringValue(s),
_ => NilValue.Instance,
},
_ => input
};
});
var parser = new FluidParser();

var templateCode ="";
if (config.SelectedTemplate == DefaultTemplateName)
{
templateCode = ReadEmbeddedResource("WireMockInspector.CodeGenerators.default_template.liquid");
}
else if(string.IsNullOrWhiteSpace(config.SelectedTemplate) == false)
{
var templatePath = Path.Combine(PathHelper.GetTemplateDir(), config.SelectedTemplate);
if (File.Exists(templatePath))
{
templateCode = File.ReadAllText(templatePath);
}
}


if (parser.TryParse(templateCode, out var ftemplate, out var error))
{
var reader = new JsonDataSourceReader();

var data = reader.Read(JsonConvert.SerializeObject(
new
{
request = new
{
ClientIP = logRequest.ClientIP,
DateTime = logRequest.DateTime,
Path = logRequest.Path,
AbsolutePath = logRequest.AbsolutePath,
Url = logRequest.Url,
AbsoluteUrl = logRequest.AbsoluteUrl,
ProxyUrl = logRequest.ProxyUrl,
Query = logRequest.Query,
Method = logRequest.Method,
Headers = logRequest.Headers,
Cookies = logRequest.Cookies,
Body = logRequest.Body,
BodyAsJson = logRequest.BodyAsJson?.ToString(),
BodyAsBytes = logRequest.BodyAsBytes,
BodyEncoding = logRequest.BodyEncoding,
DetectedBodyType = logRequest.DetectedBodyType,
DetectedBodyTypeFromContentType = logRequest.DetectedBodyTypeFromContentType
},
response = new
{
StatusCode = logResponse.StatusCode,
Headers = logResponse.Headers,
BodyDestination = logResponse.BodyDestination,
Body = logResponse.Body,
BodyAsJson = logResponse.BodyAsJson?.ToString(),
BodyAsBytes = logResponse.BodyAsBytes,
BodyAsFile = logResponse.BodyAsFile,
BodyAsFileIsCached = logResponse.BodyAsFileIsCached,
BodyOriginal = logResponse.BodyOriginal,
BodyEncoding = logResponse.BodyEncoding,
DetectedBodyType = logResponse.DetectedBodyType,
DetectedBodyTypeFromContentType = logResponse.DetectedBodyTypeFromContentType,
FaultType = logResponse.FaultType,
FaultPercentage = logResponse.FaultPercentage
},
config
}));
var result = ftemplate.Render(new TemplateContext(new
{
data = data
}, options));
return result;
}

return error;

}
}

0 comments on commit 51b249d

Please sign in to comment.