Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions GraphQL.Client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{98D4
.github\dependabot.yml = .github\dependabot.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.ApiTests", "tests\GraphQL.Client.ApiTests\GraphQL.Client.ApiTests.csproj", "{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -129,6 +131,10 @@ Global
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.Build.0 = Release|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -150,6 +156,7 @@ Global
{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD} = {89AD33AB-64F6-4F82-822F-21DF7A10CEC0}
{98D4DDDD-DE15-4997-B888-9BC806C7416C} = {63F75859-4698-4EDE-8B70-4ACBB8BC425A}
{DB0C542C-D0CC-4AD5-95AF-50A208C6A885} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4}
Expand Down
89 changes: 89 additions & 0 deletions tests/GraphQL.Client.ApiTests/ApiApprovalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Diagnostics;
using System.Reflection;
using System.Xml.Linq;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Http;
using GraphQL.Client.LocalExecution;
using GraphQL.Client.Serializer.Newtonsoft;
using GraphQL.Client.Serializer.SystemTextJson;
using PublicApiGenerator;
using Shouldly;
using Xunit;

namespace GraphQL.ApiTests;

public class ApiApprovalTests
{
[Theory]
[InlineData(typeof(NewtonsoftJsonSerializer))]
[InlineData(typeof(SystemTextJsonSerializer))]
[InlineData(typeof(GraphQLRequest))]
[InlineData(typeof(GraphQLLocalExecutionClient))]
[InlineData(typeof(IGraphQLWebSocketClient))]
[InlineData(typeof(IGraphQLClient))]
[InlineData(typeof(GraphQLHttpRequest))]
public void PublicApi(Type type)
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string projectName = type.Assembly.GetName().Name!;
string testDir = Path.Combine(baseDir, $"..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..");
string projectDir = Path.Combine(testDir, "..");
string srcDir = Path.Combine(projectDir, "..", "src");
string buildDir = Path.Combine(srcDir, projectName, "bin", Environment.GetEnvironmentVariable("CI") == null ? "Debug" : "Release");
Debug.Assert(Directory.Exists(buildDir), $"Directory '{buildDir}' doesn't exist");
string csProject = Path.Combine(srcDir, projectName, projectName + ".csproj");
var project = XDocument.Load(csProject);
string[] tfms = project.Descendants("TargetFrameworks").Union(project.Descendants("TargetFramework")).First().Value.Split(";", StringSplitOptions.RemoveEmptyEntries);

// There may be old stuff from earlier builds like net45, netcoreapp3.0, etc. so filter it out
string[] actualTfmDirs = Directory.GetDirectories(buildDir).Where(dir => tfms.Any(tfm => dir.EndsWith(tfm))).ToArray();
Debug.Assert(actualTfmDirs.Length > 0, $"Directory '{buildDir}' doesn't contain subdirectories matching {string.Join(";", tfms)}");

(string tfm, string content)[] publicApi = actualTfmDirs.Select(tfmDir => (new DirectoryInfo(tfmDir).Name.Replace(".", ""), Assembly.LoadFile(Path.Combine(tfmDir, projectName + ".dll")).GeneratePublicApi(new ApiGeneratorOptions
{
IncludeAssemblyAttributes = false,
//AllowNamespacePrefixes = new[] { "Microsoft.Extensions.DependencyInjection" },
ExcludeAttributes = new[] { "System.Diagnostics.DebuggerDisplayAttribute", "System.Diagnostics.CodeAnalysis.AllowNullAttribute" }
}) + Environment.NewLine)).ToArray();

if (publicApi.DistinctBy(item => item.content).Count() == 1)
{
AutoApproveOrFail(publicApi[0].content, "");
}
else
{
foreach (var item in publicApi.ToLookup(item => item.content))
{
AutoApproveOrFail(item.Key, string.Join("+", item.Select(x => x.tfm).OrderBy(x => x)));
}
}

// Approval test should (re)generate approved.txt files locally if needed.
// Approval test should fail on CI.
// https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
void AutoApproveOrFail(string publicApi, string folder)
{
string file = null!;

try
{
publicApi.ShouldMatchApproved(options => options.SubFolder(folder).NoDiff().WithFilenameGenerator((testMethodInfo, discriminator, fileType, fileExtension) => file = $"{type.Assembly.GetName().Name}.{fileType}.{fileExtension}"));
}
catch (ShouldMatchApprovedException) when (Environment.GetEnvironmentVariable("CI") == null)
{
string? received = Path.Combine(testDir, folder, file);
string? approved = received.Replace(".received.txt", ".approved.txt");
if (File.Exists(received) && File.Exists(approved))
{
File.Copy(received, approved, overwrite: true);
File.Delete(received);
}
else
{
throw;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace GraphQL.Client.Abstractions.Websocket
{
public static class GraphQLWebSocketMessageType
{
public const string GQL_COMPLETE = "complete";
public const string GQL_CONNECTION_ACK = "connection_ack";
public const string GQL_CONNECTION_ERROR = "connection_error";
public const string GQL_CONNECTION_INIT = "connection_init";
public const string GQL_CONNECTION_KEEP_ALIVE = "ka";
public const string GQL_CONNECTION_TERMINATE = "connection_terminate";
public const string GQL_DATA = "data";
public const string GQL_ERROR = "error";
public const string GQL_NEXT = "next";
public const string GQL_PING = "ping";
public const string GQL_PONG = "pong";
public const string GQL_START = "start";
public const string GQL_STOP = "stop";
public const string GQL_SUBSCRIBE = "subscribe";
}
public class GraphQLWebSocketRequest : System.Collections.Generic.Dictionary<string, object>, System.IEquatable<GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest>
{
public const string ID_KEY = "id";
public const string PAYLOAD_KEY = "payload";
public const string TYPE_KEY = "type";
public GraphQLWebSocketRequest() { }
public string Id { get; set; }
public object? Payload { get; set; }
public string Type { get; set; }
public bool Equals(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest other) { }
public override bool Equals(object obj) { }
public override int GetHashCode() { }
public void SendCanceled() { }
public void SendCompleted() { }
public void SendFailed(System.Exception e) { }
public System.Threading.Tasks.Task SendTask() { }
public static bool operator !=(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request2) { }
public static bool operator ==(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request2) { }
}
public class GraphQLWebSocketResponse : System.IEquatable<GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse>
{
public GraphQLWebSocketResponse() { }
public string Id { get; set; }
public string Type { get; set; }
public bool Equals(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse other) { }
public override bool Equals(object obj) { }
public override int GetHashCode() { }
public static bool operator !=(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response2) { }
public static bool operator ==(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response1, GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse response2) { }
}
public class GraphQLWebSocketResponse<TPayload> : GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse, System.IEquatable<GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse<TPayload>>
{
public GraphQLWebSocketResponse() { }
public TPayload Payload { get; set; }
public bool Equals(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse<TPayload>? other) { }
public override bool Equals(object? obj) { }
public override int GetHashCode() { }
}
public enum GraphQLWebsocketConnectionState
{
Disconnected = 0,
Connecting = 1,
Connected = 2,
}
public interface IGraphQLWebSocketClient : GraphQL.Client.Abstractions.IGraphQLClient
{
System.IObservable<object?> PongStream { get; }
System.IObservable<System.Exception> WebSocketReceiveErrors { get; }
string? WebSocketSubProtocol { get; }
System.IObservable<GraphQL.Client.Abstractions.Websocket.GraphQLWebsocketConnectionState> WebsocketConnectionState { get; }
System.Threading.Tasks.Task InitializeWebsocketConnection();
System.Threading.Tasks.Task SendPingAsync(object? payload);
System.Threading.Tasks.Task SendPongAsync(object? payload);
}
public interface IGraphQLWebsocketJsonSerializer : GraphQL.Client.Abstractions.IGraphQLJsonSerializer
{
GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse<TResponse> DeserializeToWebsocketResponse<TResponse>(byte[] bytes);
System.Threading.Tasks.Task<GraphQL.Client.Abstractions.Websocket.WebsocketMessageWrapper> DeserializeToWebsocketResponseWrapperAsync(System.IO.Stream stream);
byte[] SerializeToBytes(GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketRequest request);
}
public class WebsocketMessageWrapper : GraphQL.Client.Abstractions.Websocket.GraphQLWebSocketResponse
{
public WebsocketMessageWrapper() { }
[System.Runtime.Serialization.IgnoreDataMember]
public byte[] MessageBytes { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace GraphQL.Client.Abstractions
{
public static class GraphQLClientExtensions
{
public static System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType) { }
public static System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType, System.Action<System.Exception> exceptionHandler) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, string query, object? variables = null, string? operationName = null, System.Func<TResponse> defineResponseType = null, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, GraphQL.GraphQLRequest request, System.Func<TResponse> defineResponseType, System.Threading.CancellationToken cancellationToken = default) { }
public static System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this GraphQL.Client.Abstractions.IGraphQLClient client, string query, object? variables = null, string? operationName = null, System.Func<TResponse> defineResponseType = null, System.Threading.CancellationToken cancellationToken = default) { }
}
public static class GraphQLJsonSerializerExtensions
{
public static TOptions AndReturn<TOptions>(this System.Action<TOptions> configure, TOptions options) { }
public static TOptions New<TOptions>(this System.Action<TOptions> configure) { }
}
public interface IGraphQLClient
{
System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(GraphQL.GraphQLRequest request);
System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(GraphQL.GraphQLRequest request, System.Action<System.Exception> exceptionHandler);
System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(GraphQL.GraphQLRequest request, System.Threading.CancellationToken cancellationToken = default);
System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(GraphQL.GraphQLRequest request, System.Threading.CancellationToken cancellationToken = default);
}
public interface IGraphQLJsonSerializer
{
System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> DeserializeFromUtf8StreamAsync<TResponse>(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken);
string SerializeToString(GraphQL.GraphQLRequest request);
}
}
namespace GraphQL.Client.Abstractions.Utilities
{
public static class StringExtensions
{
public static string Capitalize(this string str) { }
public static string StripIndent(this string str) { }
public static string ToCamelCase(this string str) { }
public static string ToConstantCase(this string str) { }
public static string ToKebabCase(this string str) { }
public static string ToLowerCase(this string str) { }
public static string ToLowerFirst(this string str) { }
public static string ToPascalCase(this string str) { }
public static string ToSnakeCase(this string str) { }
public static string ToUpperCase(this string str) { }
public static string ToUpperFirst(this string str) { }
public static System.Collections.Generic.IEnumerable<string> ToWords(this string str) { }
}
public static class StringUtils
{
public static string Capitalize(string str) { }
public static string ChangeCase(string str, System.Func<string, string> composer) { }
public static string ChangeCase(string str, System.Func<string, int, string> composer) { }
public static string ChangeCase(string str, string sep, System.Func<string, string> composer) { }
public static string ChangeCase(string str, string sep, System.Func<string, int, string> composer) { }
public static string StripIndent(string str) { }
public static string ToCamelCase(string str) { }
public static string ToConstantCase(string str) { }
public static string ToKebabCase(string str) { }
public static string ToLowerCase(string str) { }
public static string ToLowerFirst(string str) { }
public static string ToPascalCase(string str) { }
public static string ToSnakeCase(string str) { }
public static string ToUpperCase(string str) { }
public static string ToUpperFirst(string str) { }
public static System.Collections.Generic.IEnumerable<string> ToWords(string str) { }
}
}
21 changes: 21 additions & 0 deletions tests/GraphQL.Client.ApiTests/GraphQL.Client.ApiTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../tests.props" />

<PropertyGroup>
<TargetFramework>net7</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="PublicApiGenerator" Version="11.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\GraphQL.Client.LocalExecution\GraphQL.Client.LocalExecution.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.Newtonsoft\GraphQL.Client.Serializer.Newtonsoft.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.SystemTextJson\GraphQL.Client.Serializer.SystemTextJson.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client\GraphQL.Client.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace GraphQL.Client.LocalExecution
{
public static class GraphQLLocalExecutionClient
{
public static GraphQL.Client.LocalExecution.GraphQLLocalExecutionClient<TSchema> New<TSchema>(TSchema schema, GraphQL.Client.Abstractions.IGraphQLJsonSerializer clientSerializer, GraphQL.IGraphQLTextSerializer serverSerializer)
where TSchema : GraphQL.Types.ISchema { }
}
public class GraphQLLocalExecutionClient<TSchema> : GraphQL.Client.Abstractions.IGraphQLClient
where TSchema : GraphQL.Types.ISchema
{
public GraphQLLocalExecutionClient(TSchema schema, GraphQL.IDocumentExecuter documentExecuter, GraphQL.Client.Abstractions.IGraphQLJsonSerializer serializer, GraphQL.IGraphQLTextSerializer documentSerializer) { }
public TSchema Schema { get; }
public GraphQL.Client.Abstractions.IGraphQLJsonSerializer Serializer { get; }
public System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(GraphQL.GraphQLRequest request) { }
public System.IObservable<GraphQL.GraphQLResponse<TResponse>> CreateSubscriptionStream<TResponse>(GraphQL.GraphQLRequest request, System.Action<System.Exception> exceptionHandler) { }
public void Dispose() { }
public System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(GraphQL.GraphQLRequest request, System.Threading.CancellationToken cancellationToken = default) { }
public System.Threading.Tasks.Task<GraphQL.GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(GraphQL.GraphQLRequest request, System.Threading.CancellationToken cancellationToken = default) { }
}
public static class ServiceCollectionExtensions
{
public static GraphQL.DI.IGraphQLBuilder AddGraphQLLocalExecutionClient<TSchema>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
where TSchema : GraphQL.Types.ISchema { }
}
}
Loading