-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add option to customize code generation with custom liquid template
- Loading branch information
1 parent
fd8c665
commit fbadfd0
Showing
10 changed files
with
657 additions
and
490 deletions.
There are no files selected for viewing
203 changes: 203 additions & 0 deletions
203
src/WireMockInspector/CodeGenerators/CSharpFormatter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
149
src/WireMockInspector/CodeGenerators/MappingCodeGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
} | ||
} |
Oops, something went wrong.