From fd8c665866ed2910dc3b222dec103c256049e8f8 Mon Sep 17 00:00:00 2001 From: Cezary Piatek Date: Sat, 20 Jan 2024 13:12:31 +0100 Subject: [PATCH 1/2] Prevent app from crashing when server is not available during auto-load --- src/WireMockInspector/ReactiveEx.cs | 10 ++++++++++ src/WireMockInspector/Views/MainWindow.axaml.cs | 11 +++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 src/WireMockInspector/ReactiveEx.cs diff --git a/src/WireMockInspector/ReactiveEx.cs b/src/WireMockInspector/ReactiveEx.cs new file mode 100644 index 0000000..8830780 --- /dev/null +++ b/src/WireMockInspector/ReactiveEx.cs @@ -0,0 +1,10 @@ +using System; +using System.Reactive.Linq; + +namespace WireMockInspector; + +public static class ReactiveEx +{ + public static IObservable DiscardExceptions(this IObservable observable) + => observable.Catch(Observable.Empty()); +} \ No newline at end of file diff --git a/src/WireMockInspector/Views/MainWindow.axaml.cs b/src/WireMockInspector/Views/MainWindow.axaml.cs index 9e3b369..7fc1516 100644 --- a/src/WireMockInspector/Views/MainWindow.axaml.cs +++ b/src/WireMockInspector/Views/MainWindow.axaml.cs @@ -52,10 +52,13 @@ public MainWindow() if (Settings.AutoLoad) { - ViewModel.LoadRequestsCommand.Execute().ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => - { - ViewModel.RequestSearchTerm = Settings.RequestFilters; - }).DisposeWith(disposables); + ViewModel.LoadRequestsCommand.Execute() + .ObserveOn(RxApp.MainThreadScheduler) + .DiscardExceptions() + .Subscribe(_ => + { + ViewModel.RequestSearchTerm = Settings.RequestFilters; + }).DisposeWith(disposables); } if (string.IsNullOrWhiteSpace(Settings.InstanceName) == false) From fbadfd0a10fff2aba9d7ac1440b6452ae5f52732 Mon Sep 17 00:00:00 2001 From: Cezary Piatek Date: Sun, 21 Jan 2024 14:51:29 +0100 Subject: [PATCH 2/2] Add option to customize code generation with custom liquid template --- .../CodeGenerators/CSharpFormatter.cs | 203 ++++++++ .../CodeGenerators/MappingCodeGenerator.cs | 149 ++++++ .../CodeGenerators/default_template.liquid | 76 +++ .../ViewModels/CodeGenerator.cs | 468 ------------------ .../ViewModels/MainWindowViewModel.cs | 27 +- .../MappingCodeGeneratorConfigViewModel.cs | 121 +++++ .../MappingCodeGeneratorViewModel.cs | 48 ++ .../ViewModels/PathHelper.cs | 31 ++ src/WireMockInspector/Views/RequestPage.axaml | 18 +- .../WireMockInspector.csproj | 6 + 10 files changed, 657 insertions(+), 490 deletions(-) create mode 100644 src/WireMockInspector/CodeGenerators/CSharpFormatter.cs create mode 100644 src/WireMockInspector/CodeGenerators/MappingCodeGenerator.cs create mode 100644 src/WireMockInspector/CodeGenerators/default_template.liquid delete mode 100644 src/WireMockInspector/ViewModels/CodeGenerator.cs create mode 100644 src/WireMockInspector/ViewModels/MappingCodeGeneratorConfigViewModel.cs create mode 100644 src/WireMockInspector/ViewModels/MappingCodeGeneratorViewModel.cs create mode 100644 src/WireMockInspector/ViewModels/PathHelper.cs diff --git a/src/WireMockInspector/CodeGenerators/CSharpFormatter.cs b/src/WireMockInspector/CodeGenerators/CSharpFormatter.cs new file mode 100644 index 0000000..4a1e6bc --- /dev/null +++ b/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 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\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)} }}"; + } +} \ No newline at end of file diff --git a/src/WireMockInspector/CodeGenerators/MappingCodeGenerator.cs b/src/WireMockInspector/CodeGenerators/MappingCodeGenerator.cs new file mode 100644 index 0000000..5bb12dc --- /dev/null +++ b/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; + + } +} \ No newline at end of file diff --git a/src/WireMockInspector/CodeGenerators/default_template.liquid b/src/WireMockInspector/CodeGenerators/default_template.liquid new file mode 100644 index 0000000..4973e3b --- /dev/null +++ b/src/WireMockInspector/CodeGenerators/default_template.liquid @@ -0,0 +1,76 @@ +{%- assign request = data.request -%} +{%- assign response = data.response -%} +{%- assign config = data.config -%} + +var mappingBuilder = new MappingBuilder(); +mappingBuilder + .Given(Request.Create() +{%- if config.IncludeMethod %} + .UsingMethod({{ request.Method | escape_string_for_csharp }}) +{%- endif -%} +{%- if config.IncludePath %} + .WithPath({{ request.Path | escape_string_for_csharp }}) +{%- endif -%} +{%- if config.IncludeClientIP and request.ClientIP %} + .WithClientIP({{ request.ClientIP | escape_string_for_csharp }}) +{%- endif -%} +{%- if config.IncludeAbsolutePath and request.AbsolutePath %} + .WithAbsolutePath({{ request.AbsolutePath | escape_string_for_csharp }}) +{%- endif -%} +{%- if config.IncludeUrl and request.Url %} + .WithUrl({{ request.Url | escape_string_for_csharp }}) +{%- endif -%} +{%- if config.IncludeAbsoluteUrl and request.AbsoluteUrl %} + .WithAbsoluteUrl({{ request.AbsoluteUrl | escape_string_for_csharp }}) +{%- endif -%} +{%- if config.IncludeProxyUrl and request.ProxyUrl %} + .WithProxyUrl({{ request.ProxyUrl | escape_string_for_csharp }}) +{%- endif -%} +{%- if config.IncludeQuery and request.Query %} + {%- for item in request.Query %} + {%- assign query_key = item[0] %} + {%- assign query_values = item[1] | join: ", " | escape_string_for_csharp %} + .WithParam({{ query_key | escape_string_for_csharp }}, {{ query_values }}) +{%- endfor -%} +{%- endif -%} +{%- if config.IncludeHeaders and request.Headers %} + {%- for item in request.Headers %} + {%- assign header_key = item[0] %} + {%- assign header_values = item[1] | join: ", " | escape_string_for_csharp %} + .WithHeader({{ header_key | escape_string_for_csharp }}, {{ header_values }}) +{%- endfor -%} +{%- endif -%} +{%- if config.IncludeCookies and request.Cookies %} + {%- for item in request.Cookies %} + {%- assign cookie_key = item[0] %} + {%- assign cookie_value = item[1] | escape_string_for_csharp %} + .WithCookie({{ cookie_key | escape_string_for_csharp }}, {{ cookie_value }}) +{%- endfor -%} +{%- endif -%} +{%- if config.IncludeBody %} + {%- if request.Body %} + {%- assign body = request.Body | escape_string_for_csharp %} + .WithBody({{ body }}) + {%- elsif request.BodyAsJson %} + .WithBodyAsJson({{ request.BodyAsJson | format_as_anonymous_object: 2 }}) + {%- endif -%} +{%- endif -%} + ) + .RespondWith(Response.Create(){%- if config.IncludeStatusCode and response.StatusCode %} + .WithStatusCode({{ response.StatusCode }}){%- endif -%} +{%- if config.IncludeHeadersResponse and response.Headers %} + {%- for item in response.Headers %} + {%- assign header_key = item[0] %} + {%- assign header_values = item[1] | join: ", " | escape_string_for_csharp %} + .WithHeader({{ header_key | escape_string_for_csharp }}, {{ header_values }}) +{%- endfor -%} +{%- endif -%} +{%- if config.IncludeBodyResponse %} + {%- if response.Body %} + {%- assign body = response.Body | escape_string_for_csharp %} + .WithBody({{ body }}) + {%- elsif response.BodyAsJson %} + .WithBodyAsJson({{ response.BodyAsJson | format_as_anonymous_object: 2 }}) + {%- endif -%} +{%- endif %} + ); \ No newline at end of file diff --git a/src/WireMockInspector/ViewModels/CodeGenerator.cs b/src/WireMockInspector/ViewModels/CodeGenerator.cs deleted file mode 100644 index 243c3cb..0000000 --- a/src/WireMockInspector/ViewModels/CodeGenerator.cs +++ /dev/null @@ -1,468 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reactive.Linq; -using System.Text; -using DynamicData.Binding; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using ReactiveUI; -using WireMock.Admin.Requests; - -namespace WireMockInspector.ViewModels; - -public class MappingCodeGeneratorViewModel : ViewModelBase -{ - private LogRequestModel _request; - public LogRequestModel Request - { - get => _request; - set => this.RaiseAndSetIfChanged(ref _request, value); - } - - private LogResponseModel _response; - public LogResponseModel Response - { - get => _response; - set => this.RaiseAndSetIfChanged(ref _response, value); - } - - private MappingCodeGeneratorConfigViewModel _config; - - public MappingCodeGeneratorConfigViewModel Config - { - get; - private set; - } = new MappingCodeGeneratorConfigViewModel(); - - private readonly ObservableAsPropertyHelper _outputCode; - public MarkdownCode OutputCode => _outputCode.Value; - - - public MappingCodeGeneratorViewModel() - { - - Config.WhenAnyPropertyChanged() - .Where(x=> x is not null) - .Select(x => - { - var code = CodeGenerator.GenerateCSharpCode(Request, Response, x); - return new MarkdownCode("cs", code); - }).ToProperty(this, x => x.OutputCode, out _outputCode); - } -} -public class MappingCodeGeneratorConfigViewModel : ViewModelBase -{ - // Request attributes - private bool _includeClientIP = true; - public bool IncludeClientIP - { - get => _includeClientIP; - set => this.RaiseAndSetIfChanged(ref _includeClientIP, value); - } - - private bool _includeDateTime = true; - public bool IncludeDateTime - { - get => _includeDateTime; - set => this.RaiseAndSetIfChanged(ref _includeDateTime, value); - } - - private bool _includePath = true; - public bool IncludePath - { - get => _includePath; - set => this.RaiseAndSetIfChanged(ref _includePath, value); - } - - private bool _includeAbsolutePath = true; - public bool IncludeAbsolutePath - { - get => _includeAbsolutePath; - set => this.RaiseAndSetIfChanged(ref _includeAbsolutePath, value); - } - - private bool _includeUrl = true; - public bool IncludeUrl - { - get => _includeUrl; - set => this.RaiseAndSetIfChanged(ref _includeUrl, value); - } - - private bool _includeAbsoluteUrl = true; - public bool IncludeAbsoluteUrl - { - get => _includeAbsoluteUrl; - set => this.RaiseAndSetIfChanged(ref _includeAbsoluteUrl, value); - } - - private bool _includeProxyUrl = true; - public bool IncludeProxyUrl - { - get => _includeProxyUrl; - set => this.RaiseAndSetIfChanged(ref _includeProxyUrl, value); - } - - private bool _includeQuery = true; - public bool IncludeQuery - { - get => _includeQuery; - set => this.RaiseAndSetIfChanged(ref _includeQuery, value); - } - - private bool _includeMethod = true; - public bool IncludeMethod - { - get => _includeMethod; - set => this.RaiseAndSetIfChanged(ref _includeMethod, value); - } - - private bool _includeHeaders = true; - public bool IncludeHeaders - { - get => _includeHeaders; - set => this.RaiseAndSetIfChanged(ref _includeHeaders, value); - } - - private bool _includeCookies = true; - public bool IncludeCookies - { - get => _includeCookies; - set => this.RaiseAndSetIfChanged(ref _includeCookies, value); - } - - private bool _includeBody = true; - public bool IncludeBody - { - get => _includeBody; - set => this.RaiseAndSetIfChanged(ref _includeBody, value); - } - - // Response attributes - private bool _includeStatusCode = true; - public bool IncludeStatusCode - { - get => _includeStatusCode; - set => this.RaiseAndSetIfChanged(ref _includeStatusCode, value); - } - - private bool _includeHeadersResponse = true; - public bool IncludeHeadersResponse - { - get => _includeHeadersResponse; - set => this.RaiseAndSetIfChanged(ref _includeHeadersResponse, value); - } - - private bool _includeBodyResponse = true; - public bool IncludeBodyResponse - { - get => _includeBodyResponse; - set => this.RaiseAndSetIfChanged(ref _includeBodyResponse, value); - } -} -public class CodeGenerator -{ - - - public static string EscapeStringForCSharp(string value) => CSharpFormatter.ToCSharpStringLiteral(value); - - public static string GenerateCSharpCode(LogRequestModel request, LogResponseModel response, MappingCodeGeneratorConfigViewModel config) - { - StringBuilder codeBuilder = new StringBuilder(); - - codeBuilder.AppendLine("var mappingBuilder = new MappingBuilder();"); - codeBuilder.AppendLine("mappingBuilder"); - codeBuilder.AppendLine(" .Given(Request.Create()"); - if (config.IncludeMethod) - codeBuilder.AppendLine($" .UsingMethod({EscapeStringForCSharp(request.Method)})"); - if (config.IncludePath) - codeBuilder.AppendLine($" .WithPath({EscapeStringForCSharp(request.Path)})"); - - if (config.IncludeClientIP && request.ClientIP != null) - codeBuilder.AppendLine($" .WithClientIP({EscapeStringForCSharp(request.ClientIP)})"); - - if (config.IncludeDateTime && request.DateTime != default) - codeBuilder.AppendLine($" .WithDateTime({EscapeStringForCSharp(request.DateTime.ToString())})"); - - if (config.IncludeAbsolutePath && request.AbsolutePath != null) - codeBuilder.AppendLine($" .WithAbsolutePath({EscapeStringForCSharp(request.AbsolutePath)})"); - - if (config.IncludeUrl && request.Url != null) - codeBuilder.AppendLine($" .WithUrl({EscapeStringForCSharp(request.Url)})"); - - if (config.IncludeAbsoluteUrl && request.AbsoluteUrl != null) - codeBuilder.AppendLine($" .WithAbsoluteUrl({EscapeStringForCSharp(request.AbsoluteUrl)})"); - - if (config.IncludeProxyUrl && request.ProxyUrl != null) - codeBuilder.AppendLine($" .WithProxyUrl({EscapeStringForCSharp(request.ProxyUrl)})"); - - if (config.IncludeQuery && request.Query != null) - { - foreach (var query in request.Query) - { - string values = string.Join(", ", query.Value.Select(x=> EscapeStringForCSharp(x))); - codeBuilder.AppendLine($" .WithParam({EscapeStringForCSharp(query.Key)}, {values})"); - } - } - - if (config.IncludeHeaders && request.Headers != null) - { - foreach (var header in request.Headers) - { - string values = string.Join(", ", header.Value.Select(x=> EscapeStringForCSharp(x))); - codeBuilder.AppendLine($" .WithHeader({EscapeStringForCSharp(header.Key)}, {values})"); - } - } - - if (config.IncludeCookies && request.Cookies != null) - { - foreach (var cookie in request.Cookies) - codeBuilder.AppendLine($" .WithCookie({EscapeStringForCSharp(cookie.Key)}, {EscapeStringForCSharp(cookie.Value)})"); - } - - if (config.IncludeBody) - { - if ((request.Body) is {} body) - { - try - { - var parsedJson = JToken.Parse(body); - codeBuilder.AppendLine($" .WithBodyAsJson({CSharpFormatter.ConvertJsonToAnonymousObjectDefinition(parsedJson,2)})"); - } - catch (Exception e) - { - string escapedBody = EscapeStringForCSharp(body.ToString()); - codeBuilder.AppendLine($" .WithBody({escapedBody})"); - } - - - }else if (request.BodyAsJson is JToken bodyAsJson) - { - codeBuilder.AppendLine($" .WithBodyAsJson({CSharpFormatter.ConvertJsonToAnonymousObjectDefinition(bodyAsJson, 2)})"); - } - } - - codeBuilder.AppendLine($" )"); - codeBuilder.AppendLine(" .RespondWith(Response.Create()"); - if (config.IncludeStatusCode && response.StatusCode != null) - codeBuilder.AppendLine($" .WithStatusCode({response.StatusCode})"); - - if (config.IncludeHeadersResponse && response.Headers != null) - { - foreach (var header in response.Headers) - { - string values = string.Join(", ", header.Value.Select(x=> EscapeStringForCSharp(x))); - codeBuilder.AppendLine($" .WithHeader({EscapeStringForCSharp(header.Key)}, {values})"); - } - } - - if (config.IncludeBodyResponse) - { - if ((response.Body) is {} body) - { - try - { - var parsedJson = JToken.Parse(body); - codeBuilder.AppendLine($" .WithBodyAsJson({CSharpFormatter.ConvertJsonToAnonymousObjectDefinition(parsedJson,2)})"); - } - catch (Exception e) - { - string escapedBody = EscapeStringForCSharp(body.ToString()); - codeBuilder.AppendLine($" .WithBody({escapedBody})"); - } - - - }else if (response.BodyAsJson is JToken bodyAsJson) - { - codeBuilder.AppendLine($" .WithBodyAsJson({CSharpFormatter.ConvertJsonToAnonymousObjectDefinition(bodyAsJson, 2)})"); - } - - } - - codeBuilder.AppendLine($" );"); - - return codeBuilder.ToString(); - } - -} - - -internal static class CSharpFormatter -{ - #region Reserved Keywords - - private static readonly HashSet 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 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\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)} }}"; - } -} \ No newline at end of file diff --git a/src/WireMockInspector/ViewModels/MainWindowViewModel.cs b/src/WireMockInspector/ViewModels/MainWindowViewModel.cs index cfe4776..60dc086 100644 --- a/src/WireMockInspector/ViewModels/MainWindowViewModel.cs +++ b/src/WireMockInspector/ViewModels/MainWindowViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Reactive; using System.Reactive.Linq; @@ -23,6 +24,7 @@ using WireMock.Admin.Settings; using WireMock.Client; using WireMock.Types; +using WireMockInspector.CodeGenerators; using ChangeType = DiffPlex.DiffBuilder.Model.ChangeType; using Formatting = Newtonsoft.Json.Formatting; @@ -456,13 +458,12 @@ await _settingsManager.UpdateSettings(settings => Response = model.Raw.Response, Config = { + + SelectedTemplate = MappingCodeGenerator.DefaultTemplateName, + Templates = GetAvailableTemplates().ToList(), IncludeClientIP = false, - IncludeDateTime = false, IncludePath = true, - IncludeAbsolutePath = false, IncludeUrl = false, - IncludeAbsoluteUrl = false, - IncludeProxyUrl = false, IncludeQuery = true, IncludeMethod = true, IncludeHeaders = true, @@ -475,7 +476,22 @@ await _settingsManager.UpdateSettings(settings => }; }).ToProperty(this, x=>x.CodeGenerator, out _codeGenerator); } + + + + private IEnumerable GetAvailableTemplates() + { + var templateDir = PathHelper.GetTemplateDir(); + + yield return MappingCodeGenerator.DefaultTemplateName; + + foreach (var file in Directory.GetFiles(templateDir, "*.liquid")) + { + yield return Path.GetFileName(file); + } + } + private readonly ObservableAsPropertyHelper _codeGenerator; public MappingCodeGeneratorViewModel CodeGenerator => _codeGenerator.Value; @@ -1325,4 +1341,5 @@ public enum MappingHitType OnlyPartialMatch, PerfectMatch } -} \ No newline at end of file +} + diff --git a/src/WireMockInspector/ViewModels/MappingCodeGeneratorConfigViewModel.cs b/src/WireMockInspector/ViewModels/MappingCodeGeneratorConfigViewModel.cs new file mode 100644 index 0000000..a924cd2 --- /dev/null +++ b/src/WireMockInspector/ViewModels/MappingCodeGeneratorConfigViewModel.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using ReactiveUI; + +namespace WireMockInspector.ViewModels; + +[DataContract] +public class MappingCodeGeneratorConfigViewModel : ViewModelBase +{ + // Request attributes + private bool _includeClientIP = true; + + [DataMember] + public bool IncludeClientIP + { + get => _includeClientIP; + set => this.RaiseAndSetIfChanged(ref _includeClientIP, value); + } + + private bool _includePath = true; + + [DataMember] + public bool IncludePath + { + get => _includePath; + set => this.RaiseAndSetIfChanged(ref _includePath, value); + } + + private bool _includeUrl = true; + + [DataMember] + public bool IncludeUrl + { + get => _includeUrl; + set => this.RaiseAndSetIfChanged(ref _includeUrl, value); + } + + private bool _includeQuery = true; + + [DataMember] + public bool IncludeQuery + { + get => _includeQuery; + set => this.RaiseAndSetIfChanged(ref _includeQuery, value); + } + + private bool _includeMethod = true; + + [DataMember] + public bool IncludeMethod + { + get => _includeMethod; + set => this.RaiseAndSetIfChanged(ref _includeMethod, value); + } + + private bool _includeHeaders = true; + + [DataMember] + public bool IncludeHeaders + { + get => _includeHeaders; + set => this.RaiseAndSetIfChanged(ref _includeHeaders, value); + } + + private bool _includeCookies = true; + + [DataMember] + public bool IncludeCookies + { + get => _includeCookies; + set => this.RaiseAndSetIfChanged(ref _includeCookies, value); + } + + private bool _includeBody = true; + + [DataMember] + public bool IncludeBody + { + get => _includeBody; + set => this.RaiseAndSetIfChanged(ref _includeBody, value); + } + + // Response attributes + private bool _includeStatusCode = true; + + [DataMember] + public bool IncludeStatusCode + { + get => _includeStatusCode; + set => this.RaiseAndSetIfChanged(ref _includeStatusCode, value); + } + + private bool _includeHeadersResponse = true; + + [DataMember] + public bool IncludeHeadersResponse + { + get => _includeHeadersResponse; + set => this.RaiseAndSetIfChanged(ref _includeHeadersResponse, value); + } + + private bool _includeBodyResponse = true; + + [DataMember] + public bool IncludeBodyResponse + { + get => _includeBodyResponse; + set => this.RaiseAndSetIfChanged(ref _includeBodyResponse, value); + } + + + public List Templates { get; set; } + + private string _selectedTemplate; + + public string SelectedTemplate + { + get => _selectedTemplate; + set => this.RaiseAndSetIfChanged(ref _selectedTemplate, value); + } +} \ No newline at end of file diff --git a/src/WireMockInspector/ViewModels/MappingCodeGeneratorViewModel.cs b/src/WireMockInspector/ViewModels/MappingCodeGeneratorViewModel.cs new file mode 100644 index 0000000..4756289 --- /dev/null +++ b/src/WireMockInspector/ViewModels/MappingCodeGeneratorViewModel.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Reactive.Linq; +using DynamicData.Binding; +using ReactiveUI; +using WireMock.Admin.Requests; +using WireMockInspector.CodeGenerators; + +namespace WireMockInspector.ViewModels; + +public class MappingCodeGeneratorViewModel : ViewModelBase +{ + private LogRequestModel _request; + public LogRequestModel Request + { + get => _request; + set => this.RaiseAndSetIfChanged(ref _request, value); + } + + private LogResponseModel _response; + public LogResponseModel Response + { + get => _response; + set => this.RaiseAndSetIfChanged(ref _response, value); + } + + private MappingCodeGeneratorConfigViewModel _config; + + public MappingCodeGeneratorConfigViewModel Config + { + get; + private set; + } = new MappingCodeGeneratorConfigViewModel(); + + private readonly ObservableAsPropertyHelper _outputCode; + public MarkdownCode OutputCode => _outputCode.Value; + + public MappingCodeGeneratorViewModel() + { + + Config.WhenAnyPropertyChanged() + .Where(x=> x is not null) + .Select(x => + { + var code = MappingCodeGenerator.GenerateCSharpCode(Request, Response, x); + return new MarkdownCode("cs", code); + }).ToProperty(this, x => x.OutputCode, out _outputCode); + } +} \ No newline at end of file diff --git a/src/WireMockInspector/ViewModels/PathHelper.cs b/src/WireMockInspector/ViewModels/PathHelper.cs new file mode 100644 index 0000000..6bd1072 --- /dev/null +++ b/src/WireMockInspector/ViewModels/PathHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; + +namespace WireMockInspector.ViewModels; + +public static class PathHelper +{ + public static string GetTemplateDir() + { + var settingsPath = GetSettingsDir(); + var templateDir = Path.Combine(settingsPath, "templates"); + if (Directory.Exists(templateDir) == false) + { + Directory.CreateDirectory(templateDir); + } + + return templateDir; + } + + + public static string GetSettingsDir() + { + var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WireMockInspector"); + if (Directory.Exists(path) == false) + { + Directory.CreateDirectory(path); + } + + return path; + } +} \ No newline at end of file diff --git a/src/WireMockInspector/Views/RequestPage.axaml b/src/WireMockInspector/Views/RequestPage.axaml index daea34c..bf9b8df 100644 --- a/src/WireMockInspector/Views/RequestPage.axaml +++ b/src/WireMockInspector/Views/RequestPage.axaml @@ -52,34 +52,18 @@ - - + - - - - - - - - - - - - - - - diff --git a/src/WireMockInspector/WireMockInspector.csproj b/src/WireMockInspector/WireMockInspector.csproj index ad4994b..8a06080 100644 --- a/src/WireMockInspector/WireMockInspector.csproj +++ b/src/WireMockInspector/WireMockInspector.csproj @@ -51,6 +51,7 @@ + @@ -71,4 +72,9 @@ \ + + + + +