Skip to content

Commit

Permalink
Introduce object model for http requests and responses.
Browse files Browse the repository at this point in the history
  • Loading branch information
shyamnamboodiripad committed Apr 28, 2023
1 parent 0db28cf commit 53432dc
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 46 deletions.
130 changes: 87 additions & 43 deletions src/Microsoft.DotNet.Interactive.HttpRequest/HttpRequestKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.CommandLine;
using System.Diagnostics;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.Interactive.Commands;
Expand All @@ -30,7 +32,7 @@ public class HttpRequestKernel :
private readonly Argument<Uri> _hostArgument = new();

private readonly Dictionary<string, string> _variables = new(StringComparer.InvariantCultureIgnoreCase);
private Uri _baseAddress;
private Uri? _baseAddress;
private static readonly Regex IsRequest;
private static readonly Regex IsHeader;

Expand All @@ -40,16 +42,14 @@ public class HttpRequestKernel :
static HttpRequestKernel()
{
var verbs = string.Join("|",
typeof(HttpMethod).GetProperties(BindingFlags.Static | BindingFlags.Public).Select(p => p.GetValue(null).ToString()));
typeof(HttpMethod).GetProperties(BindingFlags.Static | BindingFlags.Public).Select(p => p.GetValue(null)!.ToString()));

IsRequest = new Regex(@"^\s*(" + verbs + ")", RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.IgnoreCase);

IsHeader = new Regex(@"^\s*(?<key>[\w-]+):\s*(?<value>.*)", RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.IgnoreCase);
}



public HttpRequestKernel(string name = null, HttpClient client = null)
public HttpRequestKernel(string? name = null, HttpClient? client = null)
: base(name ?? "http")
{
KernelInfo.LanguageName = "HTTP";
Expand All @@ -68,13 +68,16 @@ public HttpRequestKernel(string name = null, HttpClient client = null)
RegisterForDisposal(_client);
}

public Uri BaseAddress
public Uri? BaseAddress
{
get => _baseAddress;
set
{
_baseAddress = value;
SetValue("host", value?.Host);
if (value is not null)
{
SetValue("host", value.Host);
}
}
}

Expand Down Expand Up @@ -106,8 +109,8 @@ public void SetValue(string valueName, string value)

public async Task HandleAsync(SubmitCode command, KernelInvocationContext context)
{
var requests = ParseRequests(command.Code).ToArray();
var diagnostics = requests.SelectMany(r => r.Diagnostics).ToArray();
var parsedRequests = ParseRequests(command.Code).ToArray();
var diagnostics = parsedRequests.SelectMany(r => r.Diagnostics).ToArray();

PublishDiagnostics(context, command, diagnostics);

Expand All @@ -117,40 +120,63 @@ public async Task HandleAsync(SubmitCode command, KernelInvocationContext contex
return;
}

foreach (var httpRequest in requests)
foreach (var parsedRequest in parsedRequests)
{
var message = new HttpRequestMessage(new HttpMethod(httpRequest.Verb), httpRequest.Address);
if (!string.IsNullOrWhiteSpace(httpRequest.Body))
var requestMessage = new HttpRequestMessage(new HttpMethod(parsedRequest.Verb), parsedRequest.Address);
if (!string.IsNullOrWhiteSpace(parsedRequest.Body))
{
message.Content = new StringContent(httpRequest.Body);
requestMessage.Content = new StringContent(parsedRequest.Body);
}

foreach (var kvp in httpRequest.Headers)
foreach (var kvp in parsedRequest.Headers)
{
switch (kvp.Key.ToLowerInvariant())
{
case "content-type":
if (message.Content is null)
if (requestMessage.Content is null)
{
message.Content = new StringContent(httpRequest.Body);
requestMessage.Content = new StringContent(parsedRequest.Body);
}
message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(kvp.Value);
requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(kvp.Value);
break;
case "accept":
message.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(kvp.Value));
requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(kvp.Value));
break;
case "user-agent":
message.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(kvp.Value));
requestMessage.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(kvp.Value));
break;
default:
message.Headers.Add(kvp.Key, kvp.Value);
requestMessage.Headers.Add(kvp.Key, kvp.Value);
break;
}
}

var response = await _client.SendAsync(message);
context.Display(response, HtmlFormatter.MimeType, PlainTextFormatter.MimeType);
var response = await GetResponseWithTimingAsync(requestMessage, context.CancellationToken);
// TODO: Store response in a dictionary if it happens to be a named request.

context.Display(response);
}
}

private async Task<HttpResponse> GetResponseWithTimingAsync(
HttpRequestMessage requestMessage,
CancellationToken cancellationToken)
{
HttpResponse response;
var stopWatch = Stopwatch.StartNew();

try
{
var responseMessage = await _client.SendAsync(requestMessage, cancellationToken);
response = await responseMessage.ToHttpResponseAsync(cancellationToken);
}
finally
{
stopWatch.Stop();
}

response.ElapsedMilliseconds = stopWatch.Elapsed.TotalMilliseconds;
return response;
}

private void PublishDiagnostics(KernelInvocationContext context, KernelCommand command, IEnumerable<Diagnostic> diagnostics)
Expand Down Expand Up @@ -247,10 +273,10 @@ private static bool MightContainRequest(IEnumerable<string> lines)
return lines.Any(line => IsRequest.IsMatch(line));
//return lines.Any() && lines.Any(line => !string.IsNullOrWhiteSpace(line));
}
private IEnumerable<HttpRequest> ParseRequests(string requests)

private IEnumerable<ParsedHttpRequest> ParseRequests(string requests)
{
var parsedRequests = new List<HttpRequest>();
var parsedRequests = new List<ParsedHttpRequest>();

/*
* A request as first verb and endpoint (optional version), this command could be multiline
Expand All @@ -261,10 +287,10 @@ private IEnumerable<HttpRequest> ParseRequests(string requests)
foreach (var (request, diagnostics) in InterpolateAndGetDiagnostics(requests))
{
var body = new StringBuilder();
string verb = null;
string address = null;
string? verb = null;
string? address = null;
var headerValues = new Dictionary<string, string>();
var lines = request.Split(new[] {'\n'});
var lines = request.Split(new[] { '\n' });
for (var index = 0; index < lines.Length; index++)
{
var line = lines[index];
Expand All @@ -275,7 +301,7 @@ private IEnumerable<HttpRequest> ParseRequests(string requests)
continue;
}

var parts = line.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
verb = parts[0].Trim();
address = parts[1].Trim();
}
Expand All @@ -297,29 +323,47 @@ private IEnumerable<HttpRequest> ParseRequests(string requests)
}
}

var bodyText = body.ToString().Trim();

if (string.IsNullOrWhiteSpace(address) && BaseAddress is null)
if (string.IsNullOrWhiteSpace(verb))
{
throw new InvalidOperationException("Cannot perform HttpRequest without a valid uri.");
throw new InvalidOperationException("Cannot perform HttpRequest without a valid verb.");
}

var uri = new Uri(address, UriKind.RelativeOrAbsolute);
if (!uri.IsAbsoluteUri && BaseAddress is null)
var uri = GetAbsoluteUriString(address);
var bodyText = body.ToString().Trim();
parsedRequests.Add(new ParsedHttpRequest(verb, uri, bodyText, headerValues, diagnostics));
}

return parsedRequests;
}

private string GetAbsoluteUriString(string? address)
{
Uri uri;

if (string.IsNullOrWhiteSpace(address))
{
uri = BaseAddress is null
? throw new InvalidOperationException("Cannot perform HttpRequest without a valid uri.")
: BaseAddress;
}
else
{
uri = new Uri(address, UriKind.RelativeOrAbsolute);

if (!uri.IsAbsoluteUri)
{
throw new InvalidOperationException($"Cannot use relative path {uri} without a base address.");
uri = BaseAddress is null
? throw new InvalidOperationException($"Cannot use relative path {uri} without a base address.")
: new Uri(BaseAddress, uri);
}

uri = uri.IsAbsoluteUri ? uri : new Uri(BaseAddress, uri);
parsedRequests.Add(new HttpRequest(verb, uri.AbsoluteUri, bodyText, headerValues, diagnostics));
}

return parsedRequests;
return uri.AbsoluteUri;
}

private class HttpRequest
private class ParsedHttpRequest
{
public HttpRequest(string verb, string address, string body, IEnumerable<KeyValuePair<string, string>> headers, IEnumerable<Diagnostic> diagnostics)
public ParsedHttpRequest(string verb, string address, string body, IEnumerable<KeyValuePair<string, string>> headers, IEnumerable<Diagnostic> diagnostics)
{
Verb = verb;
Address = address;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.DotNet.Interactive.Formatting;

namespace Microsoft.DotNet.Interactive.HttpRequest;

public class HttpRequestKernelExtension
public class HttpRequestKernelExtension
{
public static void Load(Kernel kernel)
public static void Load(Kernel kernel)
{
if (kernel.RootKernel is CompositeKernel compositeKernel)
{
var httpRequestKernel = new HttpRequestKernel();
compositeKernel.Add(httpRequestKernel);
httpRequestKernel.UseValueSharing();

RegisterFormatters();

KernelInvocationContext.Current?.DisplayAs($"""
Added kernel `{httpRequestKernel.Name}`. Send HTTP requests using the following syntax:

Expand All @@ -24,4 +27,25 @@ public static void Load(Kernel kernel)
}
}

private static void RegisterFormatters()
{
// TODO: Add Json.
Formatter.SetPreferredMimeTypesFor(typeof(HttpResponse), HtmlFormatter.MimeType, PlainTextFormatter.MimeType);

Formatter.Register<HttpResponse>(
(value, context) =>
{
value.FormatAsHtml(context);
return true;
},
HtmlFormatter.MimeType);

Formatter.Register<HttpResponse>(
(value, context) =>
{
value.FormatAsPlainText(context);
return true;
},
PlainTextFormatter.MimeType);
}
}
Loading

0 comments on commit 53432dc

Please sign in to comment.