checkpoint

This commit is contained in:
2021-11-10 18:46:29 -06:00
parent 2a3b7b32b4
commit 20f44c8b90
26 changed files with 519 additions and 407 deletions

View File

@@ -1,8 +1,12 @@
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Text;
@@ -16,10 +20,8 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
Task<bool> CreateBoardState(Models.Session session);
Task<bool> CreateSession(Models.SessionMetadata session);
Task<bool> CreateUser(Models.User user);
Task<IList<Models.SessionMetadata>> ReadSessionMetadatas();
Task<Models.User?> ReadGuestUser(Guid webSessionId);
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
Task<Models.Session?> ReadSession(string name);
Task<Models.Shogi?> ReadShogi(string name);
Task<bool> UpdateSession(Models.SessionMetadata session);
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
Task<Models.User?> ReadUser(string userName);
@@ -27,6 +29,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
public class GameboardRepository : IGameboardRepository
{
/// <summary>
/// Returns session, board state, and user documents, grouped by session.
/// </summary>
private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate";
/// <summary>
/// Returns session and user documents, grouped by session.
/// </summary>
private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata";
private static readonly string View_User = "_design/user/_view/user";
private const string ApplicationJson = "application/json";
private readonly HttpClient client;
private readonly ILogger<GameboardRepository> logger;
@@ -37,86 +48,123 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
this.logger = logger;
}
public async Task<IList<Models.SessionMetadata>> ReadSessionMetadatas()
public async Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas()
{
var selector = new Dictionary<string, object>(2)
{
[nameof(SessionDocument.DocumentType)] = WhichDocumentType.Session
};
var q = new { Selector = selector };
var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content);
var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var results = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent);
if (results != null)
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null)
{
return results
.docs
.Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
.ToList();
var groupedBySession = result.rows.GroupBy(row => row.id);
var sessions = new List<Models.SessionMetadata>(result.total_rows / 3);
foreach (var group in groupedBySession)
{
/**
* A group contains 3 elements.
* 1) The session metadata.
* 2) User document of Player1.
* 3) User document of Player2.
*/
var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>();
var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>();
var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject<UserDocument>();
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2));
}
}
return new Collection<Models.SessionMetadata>(sessions);
}
return new List<Models.SessionMetadata>(0);
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
}
public async Task<Models.Session?> ReadSession(string name)
{
var readShogiTask = ReadShogi(name);
var response = await client.GetAsync(name);
var responseContent = await response.Content.ReadAsStringAsync();
var couchModel = JsonConvert.DeserializeObject<SessionDocument>(responseContent);
var shogi = await readShogiTask;
if (shogi == null)
var queryParams = new QueryBuilder
{
return null;
{ "include_docs", "true" },
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
}.ToQueryString();
var query = $"{View_SessionWithBoardState}{queryParams}";
logger.LogInformation("ReadSession() query: {query}", query);
var response = await client.GetAsync(query);
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null && result.rows.Length > 2)
{
var group = result.rows;
/**
* A group contains 3 type of elements.
* 1) The session metadata.
* 2) User documents of Player1 and Player2.
* 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :(
* 3) BoardState
*/
var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
? group[2].doc.ToObject<UserDocument>()
: null;
var moves = group
.Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session.
// TODO: Deserialize just the Move property.
.Select(row => row.doc.ToObject<BoardStateDocument>())
.Select(boardState =>
{
var move = boardState!.Move!;
return move.PieceFromHand.HasValue
? new Models.Move(move.PieceFromHand.Value, move.To)
: new Models.Move(move.From!, move.To, move.IsPromotion);
})
.ToList();
var shogi = new Models.Shogi(moves);
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
return new Models.Session(session.Name, session.IsPrivate, shogi, new(player1Doc), player2);
}
}
return couchModel.ToDomainModel(shogi);
return null;
}
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name)
{
var response = await client.GetAsync(name);
var queryParams = new QueryBuilder
{
{ "include_docs", "true" },
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
}.ToQueryString();
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var couchModel = JsonConvert.DeserializeObject<SessionDocument>(responseContent);
return couchModel.ToDomainModel();
}
public async Task<Models.Shogi?> ReadShogi(string name)
{
var selector = new Dictionary<string, object>(2)
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null && result.rows.Length > 2)
{
[nameof(BoardStateDocument.DocumentType)] = WhichDocumentType.BoardState,
[nameof(BoardStateDocument.Name)] = name
};
var sort = new Dictionary<string, object>(1)
{
[nameof(BoardStateDocument.CreatedDate)] = "asc"
};
var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } });
var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadShogi), responseContent, query);
return null;
var group = result.rows;
/**
* A group contains 3 elements.
* 1) The session metadata.
* 2) User document of Player1.
* 3) User document of Player2.
*/
var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
? group[2].doc.ToObject<UserDocument>()
: null;
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2);
}
}
var boardStates = JsonConvert
.DeserializeObject<CouchFindResult<BoardStateDocument>>(responseContent)
.docs;
if (boardStates.Length == 0) return null;
// Skip(1) because the first BoardState has no move; it represents the initial board state of a new Session.
var moves = boardStates.Skip(1).Select(couchModel =>
{
var move = couchModel.Move;
Models.Move model = move!.PieceFromHand.HasValue
? new Models.Move(move.PieceFromHand.Value, move.To)
: new Models.Move(move.From!, move.To, move.IsPromotion);
return model;
}).ToList();
return new Models.Shogi(moves);
return null;
}
/// <summary>
@@ -149,7 +197,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
public async Task<bool> UpdateSession(Models.SessionMetadata session)
{
var couchModel = new SessionDocument(session);
// GET existing session to get revisionId.
var readResponse = await client.GetAsync(session.Name);
if (!readResponse.IsSuccessStatusCode) return false;
var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync());
// PUT the document with the revisionId.
var couchModel = new SessionDocument(session)
{
RevisionId = sessionDocument?.RevisionId
};
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PutAsync(couchModel.Id, content);
return response.IsSuccessStatusCode;
@@ -205,66 +262,31 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return string.Empty;
}
public async Task<Models.User?> ReadUser(string userName)
public async Task<Models.User?> ReadUser(string id)
{
try
var queryParams = new QueryBuilder
{
var document = new UserDocument(userName);
var uri = new Uri(client.BaseAddress!, HttpUtility.UrlEncode(document.Id));
var response = await client.GetAsync(HttpUtility.UrlEncode(document.Id));
var response2 = await client.GetAsync(uri);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var user = JsonConvert.DeserializeObject<UserDocument>(responseContent);
{ "include_docs", "true" },
{ "key", JsonConvert.SerializeObject(id) },
}.ToQueryString();
var response = await client.GetAsync($"{View_User}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<UserDocument>>(responseContent);
if (result != null && result.rows.Length > 0)
{
return new Models.User(result.rows[0].doc);
}
return new Models.User(user.Name);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
return null;
}
public async Task<bool> CreateUser(Models.User user)
{
var couchModel = new UserDocument(user.Name, user.WebSessionId);
var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
public async Task<Models.User?> ReadGuestUser(Guid webSessionId)
{
var selector = new Dictionary<string, object>(2)
{
[nameof(UserDocument.DocumentType)] = WhichDocumentType.User,
[nameof(UserDocument.WebSessionId)] = webSessionId.ToString()
};
var query = JsonConvert.SerializeObject(new { selector, limit = 1 });
var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadGuestUser), responseContent, query);
return null;
}
var result = JsonConvert.DeserializeObject<CouchFindResult<UserDocument>>(responseContent);
if (!string.IsNullOrWhiteSpace(result.warning))
{
logger.LogError(new InvalidOperationException(result.warning), result.warning);
return null;
}
var userDocument = result.docs.SingleOrDefault();
if (userDocument != null)
{
return new Models.User(userDocument.Name);
}
return null;
}
}
}