Skip to content

Commit

Permalink
GraphQL over HTTP Spec draft compliance. (#5367)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Sep 9, 2022
1 parent 5f0e65f commit 44a7ced
Show file tree
Hide file tree
Showing 147 changed files with 2,970 additions and 1,345 deletions.
@@ -0,0 +1,38 @@
using System.Buffers;

namespace CookieCrumble.Formatters;

public class HttpResponseSnapshotValueFormatter : SnapshotValueFormatter<HttpResponseMessage>
{
protected override void Format(IBufferWriter<byte> snapshot, HttpResponseMessage value)
{
var first = true;

foreach (var header in value.Headers.Concat(value.Content.Headers))
{
if (first)
{
snapshot.Append("Headers:");
snapshot.AppendLine();
first = false;
}

snapshot.Append($"{header.Key}: {string.Join(" ", header.Value)}");
snapshot.AppendLine();
}

if (!first)
{
snapshot.Append("-------------------------->");
snapshot.AppendLine();
}

snapshot.Append($"Status Code: {value.StatusCode}");

snapshot.AppendLine();
snapshot.Append("-------------------------->");
snapshot.AppendLine();

snapshot.Append(value.Content.ReadAsStringAsync().Result);
}
}
1 change: 1 addition & 0 deletions src/CookieCrumble/src/CookieCrumble/Snapshot.cs
Expand Up @@ -27,6 +27,7 @@ public sealed class Snapshot
new SchemaSnapshotValueFormatter(),
new ExceptionSnapshotValueFormatter(),
new SchemaErrorSnapshotValueFormatter(),
new HttpResponseSnapshotValueFormatter(),
#if NET6_0_OR_GREATER
new QueryPlanSnapshotValueFormatter(),
#endif
Expand Down
138 changes: 138 additions & 0 deletions src/HotChocolate/AspNetCore/src/AspNetCore/AcceptMediaType.cs
@@ -0,0 +1,138 @@
using HotChocolate.Utilities;
using Microsoft.Extensions.Primitives;

namespace HotChocolate.AspNetCore;

/// <summary>
/// Representation of a single media type entry from the accept header.
/// </summary>
public readonly struct AcceptMediaType
{
/// <summary>
/// Initializes a new instance of <see cref="AcceptMediaType"/>.
/// </summary>
/// <param name="type">
/// The type of the media type header entry.
/// </param>
/// <param name="subType">
/// The subtype of the media type header entry.
/// </param>
/// <param name="quality">
/// The value of the quality parameter `q`.
/// </param>
/// <param name="charset">
/// The charset.
/// </param>
/// <exception cref="ArgumentNullException">
/// Type or subtype are empty.
/// </exception>
internal AcceptMediaType(
StringSegment type,
StringSegment subType,
double? quality,
StringSegment charset)
{
if (!type.HasValue)
{
throw new ArgumentNullException(nameof(type));
}

if (!subType.HasValue)
{
throw new ArgumentNullException(nameof(subType));
}

Type = type.Value;
SubType = subType.Value;
Quality = quality;
Charset = charset.HasValue ? charset.Value : null;
IsUtf8 = Charset?.Equals("utf-8", StringComparison.OrdinalIgnoreCase) ?? true;

if (Type.EqualsOrdinal(ContentType.Types.All) && SubType.EqualsOrdinal(ContentType.Types.All))
{
Kind = AcceptMediaTypeKind.All;
}
else if (Type.EqualsOrdinal(ContentType.Types.Application))
{
if (SubType.EqualsOrdinal(ContentType.Types.All))
{
Kind = AcceptMediaTypeKind.AllApplication;
}
else if (SubType.EqualsOrdinal(ContentType.SubTypes.GraphQLResponse))
{
Kind = AcceptMediaTypeKind.ApplicationGraphQL;
}
else if (SubType.EqualsOrdinal(ContentType.SubTypes.Json))
{
Kind = AcceptMediaTypeKind.ApplicationJson;
}
}
else if (Type.EqualsOrdinal(ContentType.Types.MultiPart))
{
if (SubType.EqualsOrdinal(ContentType.Types.All))
{
Kind = AcceptMediaTypeKind.AllMultiPart;
}
else if (SubType.EqualsOrdinal(ContentType.SubTypes.Mixed))
{
Kind = AcceptMediaTypeKind.MultiPartMixed;
}
}
else if (Type.EqualsOrdinal(ContentType.Types.Text) && SubType.EqualsOrdinal(ContentType.SubTypes.EventStream))
{
Kind = AcceptMediaTypeKind.EventStream;
}
else
{
Kind = AcceptMediaTypeKind.Unknown;
}
}

/// <summary>
/// Gets the media type kind which is an enum representing well-known media type.
/// </summary>
public AcceptMediaTypeKind Kind { get; }

/// <summary>
/// Gets the type of the <see cref="AcceptMediaType"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/json"</c>,
/// the property gives the value <c>"application"</c>.
/// </example>
/// <remarks>
/// See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/>
/// for more details on the type.
/// </remarks>
public string Type { get; }

/// <summary>
/// Gets the subtype of the <see cref="AcceptMediaType"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"vnd.example+json"</c>.
/// </example>
/// <remarks>
/// See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/>
/// for more details on the subtype.
/// </remarks>
public string SubType { get; }

/// <summary>
/// Gets or sets the value of the quality parameter. Returns null
/// if there is no quality.
/// </summary>
public double? Quality { get; }

/// <summary>
/// Gets or sets the value of the charset parameter.
/// Returns <c>null</c> if there is no charset.
/// </summary>
public string? Charset { get; }

/// <summary>
/// Defines if the charset is UTF-8.
/// </summary>
public bool IsUtf8 { get; }
}
47 changes: 47 additions & 0 deletions src/HotChocolate/AspNetCore/src/AspNetCore/AcceptMediaTypeKind.cs
@@ -0,0 +1,47 @@
namespace HotChocolate.AspNetCore;

/// <summary>
/// Representation of well-known media kinds. We use this to avoid constant string comparison.
/// </summary>
public enum AcceptMediaTypeKind
{
/// <summary>
/// Not a well-known meda type.
/// </summary>
Unknown = 0,

/// <summary>
/// *.*
/// </summary>
All,

/// <summary>
/// application/*
/// </summary>
AllApplication,

/// <summary>
/// multipart/*
/// </summary>
AllMultiPart,

/// <summary>
/// application/graphql-response+json
/// </summary>
ApplicationGraphQL,

/// <summary>
/// application/json
/// </summary>
ApplicationJson,

/// <summary>
/// multipart/mixed
/// </summary>
MultiPartMixed,

/// <summary>
/// text/event-stream
/// </summary>
EventStream
}

This file was deleted.

77 changes: 37 additions & 40 deletions src/HotChocolate/AspNetCore/src/AspNetCore/ContentType.cs
@@ -1,51 +1,48 @@
using System.Runtime.CompilerServices;

namespace HotChocolate.AspNetCore;

internal static class ContentType
{
public const string GraphQL = "application/graphql; charset=utf-8";
public const string Json = "application/json; charset=utf-8";
public const string MultiPart = "multipart/mixed; boundary=\"-\"";
private const string _utf8 = "charset=utf-8";
private const string _boundary = "boundary=\"-\"";
public const string GraphQL = $"{Types.Application}/{SubTypes.GraphQL};{_utf8}";
public const string Json = $"{Types.Application}/{SubTypes.Json};{_utf8}";
public const string MultiPartMixed = $"{Types.MultiPart}/{SubTypes.Mixed};{_boundary};{_utf8}";
public const string GraphQLResponse = $"{Types.Application}/{SubTypes.GraphQLResponse};{_utf8}";
public const string EventStream = $"{Types.Text}/{SubTypes.EventStream};{_utf8}";

public static ReadOnlySpan<char> JsonSpan() => new char[]
private static readonly char[] _jsonArray =
{
'a',
'p',
'p',
'l',
'i',
'c',
'a',
't',
'i',
'o',
'n',
'/',
'j',
's',
'o',
'n'
'a', 'p', 'p', 'l', 'i', 'c', 'a', 't', 'i', 'o', 'n', '/', 'j', 's', 'o', 'n'
};

public static ReadOnlySpan<char> MultiPartSpan() => new char[]
private static readonly char[] _multiPartFormArray =
{
'm',
'u',
'l',
't',
'i',
'p',
'a',
'r',
't',
'/',
'f',
'o',
'r',
'm',
'-',
'd',
'a',
't',
'a'
'm', 'u', 'l', 't', 'i', 'p', 'a', 'r', 't', '/', 'f', 'o', 'r', 'm', '-', 'd', 'a',
't', 'a'
};

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<char> JsonSpan() => _jsonArray;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<char> MultiPartFormSpan() => _multiPartFormArray;

public static class Types
{
public const string All = "*";
public const string Application = "application";
public const string MultiPart = "multipart";
public const string Text = "text";
}

public static class SubTypes
{
public const string GraphQL = "graphql";
public const string GraphQLResponse = "graphql-response+json";
public const string Json = "json";
public const string Mixed = "mixed";
public const string EventStream = "event-stream";
}
}
Expand Up @@ -3,8 +3,13 @@

namespace HotChocolate.AspNetCore;

/// <summary>
/// The HTTP request interceptor allows to manipulate the GraphQL
/// request creation and the GraphQL request response creation.
/// </summary>
public class DefaultHttpRequestInterceptor : IHttpRequestInterceptor
{
/// <inheritdoc cref="IHttpRequestInterceptor.OnCreateAsync"/>
public virtual ValueTask OnCreateAsync(
HttpContext context,
IRequestExecutor requestExecutor,
Expand Down
19 changes: 5 additions & 14 deletions src/HotChocolate/AspNetCore/src/AspNetCore/ErrorHelper.cs
Expand Up @@ -19,20 +19,11 @@ public static IError RequestHasNoElements()
.SetCode(ErrorCodes.Server.RequestInvalid)
.Build();

public static IQueryResult ResponseTypeNotSupported()
=> QueryResultBuilder.CreateError(
ErrorBuilder.New()
.SetMessage(AspNetCoreResources.ErrorHelper_ResponseTypeNotSupported)
.Build());

public static IQueryResult UnknownSubscriptionError(Exception ex)
=> QueryResultBuilder.CreateError(
ErrorBuilder
.New()
.SetException(ex)
.SetCode(ErrorCodes.Execution.TaskProcessingError)
.SetMessage(AspNetCoreResources.Subscription_SendResultsAsync)
.Build());
public static IError NoSupportedAcceptMediaType()
=> ErrorBuilder.New()
.SetMessage(AspNetCoreResources.ErrorHelper_NoSupportedAcceptMediaType)
.SetCode(ErrorCodes.Server.NoSupportedAcceptMediaType)
.Build();

public static IQueryResult TypeNameIsEmpty()
=> QueryResultBuilder.CreateError(
Expand Down

0 comments on commit 44a7ced

Please sign in to comment.