Skip to content

Commit

Permalink
v7.4.1
Browse files Browse the repository at this point in the history
  • Loading branch information
Samuel Abraham committed Nov 12, 2023
1 parent 47e29e0 commit 9154eb1
Show file tree
Hide file tree
Showing 41 changed files with 714 additions and 462 deletions.
2 changes: 1 addition & 1 deletion src/TypeCache.GraphQL/Extensions/GraphQLExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static Type ToGraphQLType(this Type @this, bool isInputType)
? @this.GenericTypeArguments.First()!.ToGraphQLType(false)
: throw new ArgumentOutOfRangeException(nameof(@this), Invariant($"{nameof(Task)} and {nameof(ValueTask)} are not allowed as GraphQL types."));

var scalarGraphType = @this.GetDataType().ToGraphType();
var scalarGraphType = @this.GetScalarType().ToGraphType();
if (scalarGraphType is not null)
return scalarGraphType;

Expand Down
24 changes: 13 additions & 11 deletions src/TypeCache.GraphQL/Resolvers/PropertyFieldResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

using System;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using GraphQL;
using TypeCache.Extensions;
using TypeCache.Utilities;
using static System.FormattableString;
using static System.Globalization.CultureInfo;
using static System.Text.RegularExpressions.RegexOptions;

namespace TypeCache.GraphQL.Resolvers;

Expand Down Expand Up @@ -57,17 +58,19 @@ public PropertyFieldResolver(PropertyInfo propertyInfo)
dateTime = dateTime.ChangeTimeZone(currentTimeZone, timeZone!);
}

value = format.IsNotBlank() ? dateTime.ToString(format, InvariantCulture) : dateTime;
return format.IsNotBlank() ? dateTime.ToString(format, InvariantCulture) : dateTime;
}
else if (value is DateTimeOffset dateTimeOffset)

if (value is DateTimeOffset dateTimeOffset)
{
var timeZone = context.GetArgument<string>("timeZone");
if (timeZone.IsNotBlank())
dateTimeOffset = dateTimeOffset.ToTimeZone(timeZone);

value = format.IsNotBlank() ? dateTimeOffset.ToString(format, InvariantCulture) : dateTimeOffset;
return format.IsNotBlank() ? dateTimeOffset.ToString(format, InvariantCulture) : dateTimeOffset;
}
else if (value is string text)

if (value is string text)
{
var trim = context.GetArgument<string>("trim");
if (trim is not null)
Expand All @@ -84,7 +87,7 @@ public PropertyFieldResolver(PropertyInfo propertyInfo)
var pattern = context.GetArgument<string>("match");
if (pattern.IsNotBlank())
{
var match = RegexCache.SinglelinePattern(pattern).Match(text);
var match = Regex.Match(text, pattern, Compiled | Singleline);
if (match.Success)
text = match.Value;
else
Expand All @@ -95,19 +98,18 @@ public PropertyFieldResolver(PropertyInfo propertyInfo)
if (text.Length > length)
text = text.Left(length.Value);

text = context.GetArgument<StringCase?>("case") switch
return context.GetArgument<StringCase?>("case") switch
{
StringCase.Lower => text.ToLower(),
StringCase.LowerInvariant => text.ToLowerInvariant(),
StringCase.Upper => text.ToUpper(),
StringCase.UpperInvariant => text.ToUpperInvariant(),
_ => text
};

value = text;
}
else if (format.IsNotBlank())
value = string.Format(InvariantCulture, Invariant($"{{0:{format}}}"), value);

if (format.IsNotBlank())
return string.Format(InvariantCulture, Invariant($"{{0:{format}}}"), value);

return value;
}
Expand Down
2 changes: 1 addition & 1 deletion src/TypeCache.GraphQL/TypeCache.GraphQL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<RootNamespace>TypeCache.GraphQL</RootNamespace>
<PackageId>TypeCache.GraphQL</PackageId>
<Version>7.4.0</Version>
<Version>7.4.1</Version>
<Authors>Samuel Abraham &lt;[email protected]&gt;</Authors>
<Company>Samuel Abraham &lt;[email protected]&gt;</Company>
<Title>TypeCache GraphQL</Title>
Expand Down
18 changes: 9 additions & 9 deletions src/TypeCache.GraphQL/Types/GraphQLEnumType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ public GraphQLEnumType()
this.Description = typeof(T).GraphQLDescription();
this.DeprecationReason = typeof(T).GraphQLDeprecationReason();

var changeEnumCase = EnumOf<T>.Attributes switch
var changeEnumCase = Enum<T>.Tokens switch
{
_ when EnumOf<T>.Attributes.TryFirst<ConstantCaseAttribute>(out var attribute) => attribute.ChangeEnumCase,
_ when EnumOf<T>.Attributes.TryFirst<CamelCaseAttribute>(out var attribute) => attribute.ChangeEnumCase,
_ when EnumOf<T>.Attributes.TryFirst<PascalCaseAttribute>(out var attribute) => attribute.ChangeEnumCase,
_ when Enum<T>.Attributes.TryFirst<ConstantCaseAttribute>(out var attribute) => attribute.ChangeEnumCase,
_ when Enum<T>.Attributes.TryFirst<CamelCaseAttribute>(out var attribute) => attribute.ChangeEnumCase,
_ when Enum<T>.Attributes.TryFirst<PascalCaseAttribute>(out var attribute) => attribute.ChangeEnumCase,
_ => new Func<string, string>(_ => _)
};

EnumOf<T>.Tokens.Values
.Where(token => !token.Attributes.Any<GraphQLIgnoreAttribute>())
.Select(token => new EnumValueDefinition(token.Attributes.FirstOrDefault<GraphQLNameAttribute>()?.Name ?? changeEnumCase(token.Name), token.Value)
Enum<T>.Tokens
.Where(_ => !_.Attributes.Any<GraphQLIgnoreAttribute>())
.Select(_ => new EnumValueDefinition(_.Attributes.FirstOrDefault<GraphQLNameAttribute>()?.Name ?? changeEnumCase(_.Name), _.Value)
{
Description = token.Attributes.FirstOrDefault<GraphQLDescriptionAttribute>()?.Description,
DeprecationReason = token.Attributes.FirstOrDefault<GraphQLDeprecationReasonAttribute>()?.DeprecationReason
Description = _.Attributes.FirstOrDefault<GraphQLDescriptionAttribute>()?.Description,
DeprecationReason = _.Attributes.FirstOrDefault<GraphQLDeprecationReasonAttribute>()?.DeprecationReason
})
.ToArray()
.ForEach(this.Add);
Expand Down
2 changes: 1 addition & 1 deletion src/TypeCache.GraphQL/Types/GraphQLNumberType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public GraphQLNumberType() : base(
typeof(T).Name,
value => T.TryParse(value.Value.Span, CultureInfo.InvariantCulture, out _),
value => T.Parse(value.Value.Span, CultureInfo.InvariantCulture),
value => typeof(T).GetDataType() switch
value => typeof(T).GetScalarType() switch
{
ScalarType.SByte => ValueConverter.ConvertToSByte(value),
ScalarType.Int16 => ValueConverter.ConvertToInt16(value),
Expand Down
2 changes: 1 addition & 1 deletion src/TypeCache.GraphQL/Types/GraphQLStringType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public GraphQLStringType() : base(
typeof(T).Name,
value => T.TryParse(value.Value.Span, CultureInfo.InvariantCulture, out _),
value => T.Parse(value.Value.Span, CultureInfo.InvariantCulture),
value => typeof(T).GetDataType() switch
value => typeof(T).GetScalarType() switch
{
ScalarType.Char => ValueConverter.ConvertToChar(value),
ScalarType.DateOnly => ValueConverter.ConvertToDateOnly(value),
Expand Down
4 changes: 1 addition & 3 deletions src/TypeCache.GraphQL/Web/GraphQLMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.DI;
Expand All @@ -12,7 +11,6 @@
using GraphQL.Validation;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using TypeCache.Extensions;
using static System.FormattableString;
using static System.Net.Mime.MediaTypeNames;
using static System.StringSplitOptions;
Expand Down Expand Up @@ -93,7 +91,7 @@ public async Task Invoke(HttpContext httpContext
}

httpContext.Response.ContentType = Application.Json;
httpContext.Response.StatusCode = (int)HttpStatusCode.OK;
httpContext.Response.StatusCode = StatusCodes.Status200OK;
await graphQLSerializer.WriteAsync(httpContext.Response.Body, result, httpContext.RequestAborted);
}
}
162 changes: 162 additions & 0 deletions src/TypeCache.Web/Extensions/EndpointRouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,167 @@
// Copyright (c) 2021 Samuel Abraham

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using TypeCache.Converters;
using TypeCache.Extensions;
using TypeCache.Web.Filters;
using TypeCache.Web.Handlers;
using static System.FormattableString;
using static System.Net.Mime.MediaTypeNames;

namespace TypeCache.Web.Extensions;

public static class EndpointRouteBuilderExtensions
{
/// <summary>
/// <c>/request/headers</c><br/><br/>
/// Maps a GET endpoint that returns all request header values.<br/>
/// Supported formats (<b>Accept</b> header):
/// <list type="bullet">
/// <item><c>application/json</c></item>
/// <item><c>application/xml</c></item>
/// <item><c>text/plain</c></item>
/// <item><c>text/html</c></item>
/// <item><c>text/xml</c></item>
/// </list>
/// </summary>
public static IEndpointConventionBuilder MapGetRequestHeaders(this IEndpointRouteBuilder @this)
=> @this.MapGet("/request/headers", async context =>
{
var acceptRequestHeader = context.Request.Headers.Accept.ToString();
context.Response.StatusCode = context.Request.Headers.Any() ? StatusCodes.Status200OK : StatusCodes.Status204NoContent;
context.Response.Headers.ContentType = acceptRequestHeader switch
{
Text.Plain or Text.Html or Application.Xml or Text.Xml or Application.Json => acceptRequestHeader,
_ => Application.Json
};
var response = string.Empty;
if (acceptRequestHeader.Equals(Text.Plain, StringComparison.OrdinalIgnoreCase))
{
var responseBuilder = new StringBuilder();
foreach (var pair in context.Request.Headers)
{
responseBuilder.Append(Invariant($"{pair.Key}: {pair.Value.ToString()}<br>"));
}
response = responseBuilder.ToString();
}
else if (acceptRequestHeader.Equals(Text.Html, StringComparison.OrdinalIgnoreCase))
{
var table = new XElement("table",
new XElement("tr",
new XElement("th", "Header"),
new XElement("th", "Value")));
foreach (var pair in context.Request.Headers)
{
table.Add(new XElement("tr",
new XElement("td", pair.Key),
new XElement("td", pair.Value.ToString())));
}
response = table.ToString();
}
else if (acceptRequestHeader.Equals(Application.Xml, StringComparison.OrdinalIgnoreCase)
|| acceptRequestHeader.Equals(Text.Xml, StringComparison.OrdinalIgnoreCase))
{
var headers = new XElement("headers");
foreach (var pair in context.Request.Headers)
{
headers.Add(new XElement(pair.Key.Replace(' ', '_'), pair.Value.ToString()));
}
response = new XDocument(headers).ToString();
}
else
{
response = JsonSerializer.Serialize(context.Request.Headers, CreateJsonSerializerOptions());
}
await context.Response.WriteAsync(response, context.RequestAborted);
});

/// <summary>
/// <c>/request/headers/{key}</c><br/><br/>
/// Maps a GET endpoint that returns the request header value for the specified parameter <c><paramref name="key"/></c>.<br/>
/// If no parameter is specified, then the route will handle any key.<br/>
/// Supported formats (<b>Accept</b> header):
/// <list type="bullet">
/// <item><c>application/json</c></item>
/// <item><c>application/xml</c></item>
/// <item><c>text/plain</c></item>
/// <item><c>text/html</c></item>
/// <item><c>text/xml</c></item>
/// </list>
/// </summary>
public static IEndpointConventionBuilder MapGetRequestHeaderValue(this IEndpointRouteBuilder @this, string? key = null)
=> @this.MapGet(key.IsNotBlank() ? Invariant($"/request/headers/{key}") : "/request/headers/{key}", async context =>
{
key ??= context.GetRouteValue(nameof(key))!.ToString()!;
var response = string.Empty;
var acceptRequestHeader = context.Request.Headers.Accept.ToString().ToLowerInvariant();
context.Response.StatusCode = context.Request.Headers.TryGetValue(key, out var value) ? StatusCodes.Status200OK : StatusCodes.Status204NoContent;
context.Response.Headers.ContentType = acceptRequestHeader switch
{
Text.Plain or Text.Html or Application.Xml or Text.Xml or Application.Json => acceptRequestHeader,
_ => Application.Json
};
response = (context.Response.StatusCode, acceptRequestHeader) switch
{
(StatusCodes.Status200OK, Text.Plain) => Invariant($"{key}: {value.ToString()}"),
(StatusCodes.Status204NoContent, Text.Plain) => Invariant($"{key}: "),
(StatusCodes.Status200OK, Text.Html) => Invariant($"<h1>{key}</h1><br><b>{value.ToString()}<b/>"),
(StatusCodes.Status204NoContent, Text.Html) => Invariant($"<h1>{key}</h1><br>"),
(StatusCodes.Status200OK, Application.Xml or Text.Xml) => new XDocument(new XElement(key.Replace(' ', '_'), value.ToString())).ToString(),
(StatusCodes.Status204NoContent, Application.Xml or Text.Xml) => new XDocument(new XElement(key.Replace(' ', '_'))).ToString(),
(StatusCodes.Status200OK, _) => JsonSerializer.Serialize(new Dictionary<string, string?>(1) { { key, value.ToString() } }, CreateJsonSerializerOptions()),
_ => JsonSerializer.Serialize(new Dictionary<string, string?>(1) { { key, null } }, CreateJsonSerializerOptions())
};
await context.Response.WriteAsync(response, context.RequestAborted);
});

/// <summary>
/// <c>/ping/{<paramref name="name"/>}</c><br/><br/>
/// Maps a GET endpoint that calls a simple GET endpoint registered with <c><see cref="IHttpClientFactory"/></c> to test connectivity.<br/>
/// Headers are optionally propagated using the <c>Microsoft.AspNetCore.HeaderPropagation</c> package.<br/>
/// Uses <c><paramref name="requestUri"/></c> as a relative <c><see cref="Uri"/></c> and propagates all query parameters.<br/>
/// An example use of this would be to ping another system's endpoint to ensure that the firewall allows the call to be made,
/// eliminating the need to install curl in the container or pod.
/// </summary>
public static IEndpointConventionBuilder MapGetPing(this IEndpointRouteBuilder @this, string name, Uri requestUri)
=> @this.MapGet(Invariant($"/ping/{name}"), async (HttpContext context, IHttpClientFactory factory) =>
{
var httpClient = factory.CreateClient(name);
using var responseMessage = await httpClient.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, context.RequestAborted);
context.Response.StatusCode = (int)responseMessage.StatusCode;
foreach (var pair in responseMessage.Headers)
{
context.Response.Headers[pair.Key] = new StringValues(pair.Value?.ToArray());
}
if (context.Response.SupportsTrailers())
{
foreach (var pair in responseMessage.TrailingHeaders)
{
context.Response.AppendTrailer(pair.Key, new StringValues(pair.Value?.ToArray()));
}
}
await responseMessage.Content.CopyToAsync(context.Response.Body, context.RequestAborted);
});

/// <summary>
/// Endpoints that return composed SQL.<br/>
/// <code>
Expand Down Expand Up @@ -328,4 +478,16 @@ public static RouteHandlerBuilder MapSqlApiUpdateBatch(this IEndpointRouteBuilde
=> @this.MapPut(Invariant($"/{{dataSource:string}}/table/{{database:string}}/{{schema:string}}/{{table:string}}/batch"), SqlApiHandler.UpdateTableBatch)
.AddEndpointFilter<SqlApiEndpointFilter>()
.WithName("Batch Update");

private static JsonSerializerOptions CreateJsonSerializerOptions()
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
options.Converters.Add(new StringValuesJsonConverter());
return options;
}
}
11 changes: 5 additions & 6 deletions src/TypeCache.Web/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using static System.Net.Mime.MediaTypeNames;
Expand All @@ -11,20 +10,20 @@ namespace TypeCache.Web.Extensions;

public static class HttpContextExtensions
{
public static async ValueTask<T?> GetJsonRequestAsync<T>(this HttpContext @this, JsonSerializerOptions? options = null, CancellationToken token = default)
=> await JsonSerializer.DeserializeAsync<T>(@this.Request.Body, options, token);
public static async ValueTask<T?> GetJsonRequestAsync<T>(this HttpContext @this, JsonSerializerOptions? options = null)
=> await JsonSerializer.DeserializeAsync<T>(@this.Request.Body, options, @this.RequestAborted);

public static async ValueTask<string> GetRequestAsync(this HttpContext @this)
{
using var reader = new StreamReader(@this.Request.Body);
return await reader.ReadToEndAsync();
return await reader.ReadToEndAsync(@this.RequestAborted);
}

public static async ValueTask WriteJsonResponseAsync<T>(this HttpContext @this, T response, JsonSerializerOptions? options = null, CancellationToken token = default)
public static async ValueTask WriteJsonResponseAsync<T>(this HttpContext @this, T response, JsonSerializerOptions? options = null)
{
@this.Response.ContentType = Application.Json;
@this.Response.StatusCode = StatusCodes.Status200OK;
await JsonSerializer.SerializeAsync(@this.Response.Body, response, options, token);
await JsonSerializer.SerializeAsync(@this.Response.Body, response, options, @this.RequestAborted);
}

public static async ValueTask WriteResponseAsync(this HttpContext @this, string response)
Expand Down
2 changes: 1 addition & 1 deletion src/TypeCache.Web/TypeCache.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<RootNamespace>TypeCache.Web</RootNamespace>
<PackageId>TypeCache.Web</PackageId>
<Version>7.3.13</Version>
<Version>7.4.1</Version>
<Authors>Samuel Abraham &lt;[email protected]&gt;</Authors>
<Company>Samuel Abraham &lt;[email protected]&gt;</Company>
<Title>TypeCache Web Library</Title>
Expand Down
Loading

0 comments on commit 9154eb1

Please sign in to comment.