Skip to content

Commit

Permalink
Merge pull request #29 from CodebreakerApp/9-create-client-library
Browse files Browse the repository at this point in the history
9 create client library
  • Loading branch information
christiannagel authored Jul 26, 2023
2 parents a847f7b + 31dab2f commit 212635b
Show file tree
Hide file tree
Showing 17 changed files with 637 additions and 16 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/codebreaker-lib-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Client lib

on:

# Automatically trigger it when detected changes in repo
push:
branches:
[ main ]
paths:
- 'src/clients/common/Codebreaker.GameAPIs.Client/**'
- 'src/services/gameapi/Codebreaker.GameAPIs.Analyzers.Tests/**'
- 'src/Codebreaker.GameA.sln'
- '.github/workflows/codebreaker-lib-client.yml'
- '.github/workflows/createnuget-withbuildnumber.yml'
- '.github/workflows/publishnuget-azuredevops.yml'
- '.github/workflows/publishnuget-nugetserver.yml'

# Allow manually trigger
workflow_dispatch:

jobs:
build:
uses: CodebreakerApp/Codebreaker.Backend/.github/workflows/createnuget-withbuildnumber.yml@main
with:
version-suffix: beta.
version-number: ${{ github.run_number }}
version-offset: 10
solutionfile-path: src/Codebreaker.GameAPIs.Client.sln
projectfile-path: src/clients/common/Codebreaker.GameAPIs.Client/Codebreaker.GameAPIs.Client.csproj
dotnet-version: '8.0.x'
artifact-name: codebreaker-clientlib
branch-name: main

publishdevops:
uses: CodebreakerApp/Codebreaker.Backend/.github/workflows/publishnuget-azuredevops.yml@main
needs: build
with:
artifact-name: codebreaker-clientlib
secrets: inherit

publishnuget:
uses: CodebreakerApp/Codebreaker.Backend/.github/workflows/publishnuget-nugetserver.yml@main
needs: publishdevops
with:
artifact-name: codebreaker-clientlib
secrets: inherit
48 changes: 32 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
# Codebreaker
# Codebreaker Backends

This is the GitHub repo for the backend services of the **Codebreaker** solution.

See these repositories for clients:

* [WinUI, .NET MAUI, WPF](https://github.com/codebreakerapp/Codebreaker.Xaml)
* [Blazor](https://github.com/codebreakerapp/Codebreaker.Blazor)

See this repository for the book about the backend:

[Pragmatic Microservices with C# and Azure](https://github.com/CodebreakerApp/Pragmatic-Microservices-With-C-Sharp-and-Azure)

## Version 3

This repository is just in progress to update all the backend services to version 3 (along with the book repo). What can be used yet?

### Games API

The new Games API service with access to SQL Server and Azure Cosmos DB. Currently, you need to run this locally. A hosted version will be available with a later itation (see the progress in the book repo).

### Analyzers Library

A NuGet package for the library **CNinnovation.Codebreaker.Analyzers** (preview version) is published to the NuGet server. This library is used by the games API, and you need it creating your own custom games. The source code is available here.

### Client Library

A NugGet package for the library **CNinnovation.Codbreaker.Client** (preview version) is published to the NuGet server. This library is used by clients to call the games service.

## Builds

### Libraries

|Branch|Shared|Client Services|MVVM|Data|
|:--:|:--:|:--:|:--:|:--:|
**main**|[![Shared](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-shared.yml/badge.svg)](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-shared.yml)|[![Client Services](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-services.yml/badge.svg)](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-services.yml)|[![MVVM NuGet](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-viewmodels.yml/badge.svg)](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-viewmodels.yml)|[![Data](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-data.yml/badge.svg)](https://github.com/CNinnovation/codebreaker/actions/workflows/codebreaker-lib-data.yml)
|Branch|Analyzers|Client Library|
|:--:|:--:|:--:|
**main**|[![Analyzers](https://github.com/CodebreakerApp/Codebreaker.Backend/actions/workflows/codebreaker-lib-analyzers.yml/badge.svg)](https://github.com/CodebreakerApp/Codebreaker.Backend/actions/workflows/codebreaker-lib-analyzers.yml)|[![Client Library](https://github.com/CodebreakerApp/Codebreaker.Backend/actions/workflows/codebreaker-lib-client.yml/badge.svg)](https://github.com/CodebreakerApp/Codebreaker.Backend/actions/workflows/codebreaker-lib-client.yml)

### APIs

|Banch|Game API|Bot|Live|User|
|Banch|Games API|Bot|Live|User|
|:--:|:--:|:--:|:--:|:--:|
**main**|[![API](https://github.com/CNILearn/codebreaker/actions/workflows/codebreakerapi-AutoDeployTrigger-ee54dca3-868c-4c78-9b6c-72e2c6719e10.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreakerapi-AutoDeployTrigger-ee54dca3-868c-4c78-9b6c-72e2c6719e10.yml)|[![Bot](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-bot.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-bot.yml)|[![Live](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-live.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-live.yml)|[![User](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-user.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-user.yml)

### Blazor Clients

|Banch|Pure|Mud|Fast|
|:--:|:--:|:--:|:--:|
**main**|[![Pure Blazor App](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-blazor-pure.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-blazor-pure.yml)|[![Mud Blazor App](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-blazor-mud.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-blazor-mud.yml)|[![Fast Blazor App](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-blazor-fastui.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-blazor-fastui.yml)

### More Clients

|Branch|Android|Win UI|
|:--:|:--:|:--:
**main**|[![MAUI Android](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-maui-android.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-maui-android.yml)|[![WinUI](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-winui.yml/badge.svg)](https://github.com/CNILearn/codebreaker/actions/workflows/codebreaker-winui.yml)

### Integration Tests

Expand Down
31 changes: 31 additions & 0 deletions src/Codebreaker.GameAPIs.Client.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.33913.275
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codebreaker.GameAPIs.Client", "clients\common\Codebreaker.GameAPIs.Client\Codebreaker.GameAPIs.Client.csproj", "{87B3D527-0BB4-4B7E-9A29-525F86836FBD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codebreaker.GameAPIs.Client.Tests", "clients\common\Codebreaker.GameAPIs.Client.Tests\Codebreaker.GameAPIs.Client.Tests.csproj", "{D6A22A41-9853-4EDE-8B25-033308390292}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{87B3D527-0BB4-4B7E-9A29-525F86836FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87B3D527-0BB4-4B7E-9A29-525F86836FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87B3D527-0BB4-4B7E-9A29-525F86836FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87B3D527-0BB4-4B7E-9A29-525F86836FBD}.Release|Any CPU.Build.0 = Release|Any CPU
{D6A22A41-9853-4EDE-8B25-033308390292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6A22A41-9853-4EDE-8B25-033308390292}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6A22A41-9853-4EDE-8B25-033308390292}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6A22A41-9853-4EDE-8B25-033308390292}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {81F354FE-BC60-4FE4-93D7-7F93B1982EA3}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0-preview.6.23329.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Codebreaker.GameAPIs.Client\Codebreaker.GameAPIs.Client.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Net;

using Microsoft.Extensions.Configuration;

using Moq;
using Moq.Protected;

using Xunit.Abstractions;

namespace Codebreaker.GameAPIs.Client.Tests;

public class TestGamesClient
{
private readonly ITestOutputHelper _outputHelper;

public TestGamesClient(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
}

[Fact]
public async Task TestStartGame6x4Async()
{
// Arrange
var configMock = new Mock<IConfiguration>();
configMock.Setup(x => x[It.IsAny<string>()]).Returns("http://localhost:5000");

Mock<HttpMessageHandler> handlerMock = new (MockBehavior.Strict);
string returnMessage = """
{
"gameId": "af8dd39f-6e16-41ef-9155-dcd3cf081e87",
"gameType": "Game6x4",
"playerName": "test",
"numberCodes": 4,
"maxMoves": 12,
"fieldValues": {
"colors": [
"Red",
"Green",
"Blue",
"Yellow",
"Purple",
"Orange"
]
}
}
""";
handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(returnMessage)
}).Verifiable();

HttpClient httpClient = new(handlerMock.Object)
{
BaseAddress = new System.Uri(configMock.Object["GameAPIs"] ?? throw new InvalidOperationException())
};

var gamesClient = new GamesClient(httpClient);

// Act
var response = await gamesClient.StartGameAsync(Models.GameType.Game6x4, "test");

// Assert
Assert.Equal(4, response.NumberCodes);
Assert.Equal(12, response.MaxMoves);
Assert.Single(response.FieldValues.Keys);
Assert.Equal("colors", response.FieldValues.Keys.First());
Assert.Equal(6, response.FieldValues["colors"].Length);

handlerMock.Protected().Verify(
"SendAsync",
Times.Once(),
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>CNinnovation.Codebreaker.GamesClient</PackageId>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<PackageTags>
Codebreaker;CNinnovation;GamesClient;
</PackageTags>
<Description>
This library contains client code to access the Codebreaker Games APIp. Reference this library to create a client application to access the Codebreaker Games API.
See https://github.com/codebreakerapp for more information on the complete solution.
</Description>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<PackageIcon>codebreaker.jpeg</PackageIcon>
</PropertyGroup>

<ItemGroup>
<None Include="docs/readme.md" Pack="true" PackagePath="\" />
<None Include="Images/codebreaker.jpeg" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
103 changes: 103 additions & 0 deletions src/clients/common/Codebreaker.GameAPIs.Client/GamesClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;

using Codebreaker.GameAPIs.Client.Models;

namespace Codebreaker.GameAPIs.Client;

/// <summary>
/// Client to interact with the Codebreaker Game API.
/// </summary>
public class GamesClient
{
private readonly HttpClient _httpClient;
private readonly static JsonSerializerOptions s_jsonOptions = new()
{
PropertyNameCaseInsensitive = true
};

public GamesClient(HttpClient httpClient)
{
_httpClient = httpClient;
}

/// <summary>
/// Starts a new game
/// </summary>
/// <param name="gameType">The game type with one of the <see cref="GameType"/>enum values</param>
/// <param name="playerName">The name of the player</param>
/// <param name="cancellationToken">Optional cancellation token to cancel the request early</param>
/// <returns>A tuple with the unique game id, the number of codes that need to be filled, the maximum available moves, and possible field values for guesses</returns>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="HttpRequestException"></exception>"
public async Task<(Guid GameId, int NumberCodes, int MaxMoves, IDictionary<string, string[]> FieldValues)>
StartGameAsync(GameType gameType, string playerName, CancellationToken cancellationToken = default)
{
CreateGameRequest createGameRequest = new(gameType, playerName);
var response = await _httpClient.PostAsJsonAsync("/games", createGameRequest, s_jsonOptions, cancellationToken);
response.EnsureSuccessStatusCode();
var gameResponse = await response.Content.ReadFromJsonAsync<CreateGameResponse>(s_jsonOptions, cancellationToken) ?? throw new InvalidOperationException();
return (gameResponse.GameId, gameResponse.NumberCodes, gameResponse.MaxMoves, gameResponse.FieldValues);
}

/// <summary>
/// Set a game move by supplying guess pegs. This method returns the results of the move (the key pegs), and whether the game ended, and whether the game was won.
/// </summary>
/// <param name="gameId">The game id received from StartGameAsync</param>
/// <param name="playerName">The player name (needs to be the same as received). This must match with the game started.</param>
/// <param name="gameType">The game type with one of the <see cref="GameType"/>enum values. This must match with the game started.</param>
/// <param name="moveNumber">The incremented move number. The game analyzer returns an error if this does not match the state of the game.</param>
/// <param name="guessPegs">The guess pegs for this move. The number of guess pegs must conform to the number codes returned when creating the game.</param>
/// <param name="cancellationToken">Optional cancellation token to cancel the request early.</param>
/// <returns></returns>
/// <exception cref="HttpRequestException"></exception>"
/// <exception cref="InvalidOperationException"></exception>
public async Task<(string[] Results, bool Ended, bool IsVictory)> SetMoveAsync(Guid gameId, string playerName, GameType gameType, int moveNumber, string[] guessPegs, CancellationToken cancellationToken = default)
{
UpdateGameRequest updateGameRequest = new(gameId, gameType, playerName, moveNumber)
{
GuessPegs = guessPegs
};
var response = await _httpClient.PatchAsJsonAsync($"/games/{gameId}", updateGameRequest, s_jsonOptions, cancellationToken);
response.EnsureSuccessStatusCode();
var moveResponse = await response.Content.ReadFromJsonAsync<UpdateGameResponse>(s_jsonOptions, cancellationToken)
?? throw new InvalidOperationException();
(_, _, _, bool ended, bool isVictory, string[] results) = moveResponse;
return (results, ended, isVictory);
}

/// <summary>
/// Retrieves a game by ID.
/// </summary>
/// <param name="gameId">The unique identifier of a game.</param>
/// <param name="cancellationToken">Optional cancellation token to cancel the request early.</param>
/// <returns>The <see cref="Game"/> if it exists, otherwise null.</returns>
/// <exception cref="HttpRequestException"></exception>
public async Task<Game?> GetGameAsync(Guid gameId, CancellationToken cancellationToken = default)
{
Game? game = default;
try
{
game = await _httpClient.GetFromJsonAsync<Game>($"/games/{gameId}", s_jsonOptions, cancellationToken);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return default;
}
return game;
}

/// <summary>
/// Retrieves a list of games matching a specified query.
/// </summary>
/// <param name="query">The games query object containing parameters to filter games.</param>
/// <param name="cancellationToken">Cancellation token to cancel the request early.</param>
/// <returns>An IEnumerable collection of Game objects that match the specified query.</returns>
/// <exception cref="HttpRequestException"></exception>
public async Task<IEnumerable<Game>> GetGamesAsync(GamesQuery query, CancellationToken cancellationToken = default)
{
IEnumerable<Game> games = (await _httpClient.GetFromJsonAsync<IEnumerable<Game>>($"/games/{query.AsUrlQuery()}", s_jsonOptions, cancellationToken)) ?? Enumerable.Empty<Game>();
return games;
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 212635b

Please sign in to comment.