Контракт http

This commit is contained in:
2026-05-31 23:09:30 +03:00
parent e691f14e5d
commit 01b935e112
18 changed files with 204 additions and 10 deletions

View File

@@ -0,0 +1,20 @@
using System.Text.Json;
using Contracts.Json;
namespace Contracts;
public static class ApiJsonOptions
{
public static JsonSerializerOptions Instance { get; } = Create();
private static JsonSerializerOptions Create()
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = null,
};
options.Converters.Add(new DecimalFromStringJsonConverter());
return options;
}
}

View File

@@ -1,6 +0,0 @@
namespace Contracts;
public class Class1
{
}

View File

@@ -0,0 +1,7 @@
namespace Contracts;
public static class Commands
{
public const string GetMenu = "GetMenu";
public const string SendOrder = "SendOrder";
}

View File

@@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Contracts.Json;
public sealed class DecimalFromStringJsonConverter : JsonConverter<decimal>
{
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String => decimal.Parse(
reader.GetString()!,
NumberStyles.Number,
CultureInfo.InvariantCulture),
JsonTokenType.Number => reader.GetDecimal(),
_ => throw new JsonException($"Unexpected token {reader.TokenType} when parsing decimal."),
};
}
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) =>
writer.WriteNumberValue(value);
}

View File

@@ -0,0 +1,8 @@
using Domain.Entities;
namespace Contracts.Menu;
public sealed class GetMenuData
{
public IReadOnlyList<Dish> MenuItems { get; init; } = [];
}

View File

@@ -0,0 +1,6 @@
namespace Contracts.Menu;
public sealed class GetMenuParameters
{
public bool WithPrice { get; set; } = true;
}

View File

@@ -0,0 +1,10 @@
using Domain.Entities;
namespace Contracts.Orders;
public sealed class SendOrderParameters
{
public required string OrderId { get; init; }
public IReadOnlyList<OrderItem> MenuItems { get; init; } = [];
}

View File

@@ -0,0 +1,11 @@
namespace Contracts.Requests;
public class ApiRequest
{
public string Command { get; set; } = "";
}
public class ApiRequest<TParameters> : ApiRequest
{
public TParameters? CommandParameters { get; set; }
}

View File

@@ -0,0 +1,33 @@
using System.Text.Json;
namespace Contracts.Requests;
public static class ApiRequestDeserializer
{
public static ApiRequest Deserialize(string json) =>
Deserialize(JsonDocument.Parse(json).RootElement);
public static ApiRequest Deserialize(ReadOnlySpan<byte> utf8Json) =>
Deserialize(JsonSerializer.Deserialize<JsonElement>(utf8Json, ApiJsonOptions.Instance)!);
public static async Task<ApiRequest> DeserializeAsync(Stream stream, CancellationToken cancellationToken = default)
{
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
return Deserialize(document.RootElement);
}
public static ApiRequest Deserialize(JsonElement json)
{
if (!json.TryGetProperty("Command", out var commandElement))
{
return json.Deserialize<ApiRequest>(ApiJsonOptions.Instance)!;
}
return commandElement.GetString() switch
{
Commands.GetMenu => json.Deserialize<GetMenuApiRequest>(ApiJsonOptions.Instance)!,
Commands.SendOrder => json.Deserialize<SendOrderApiRequest>(ApiJsonOptions.Instance)!,
_ => json.Deserialize<ApiRequest>(ApiJsonOptions.Instance)!,
};
}
}

View File

@@ -0,0 +1,11 @@
using Contracts.Menu;
namespace Contracts.Requests;
public sealed class GetMenuApiRequest : ApiRequest<GetMenuParameters>
{
public GetMenuApiRequest()
{
Command = Commands.GetMenu;
}
}

View File

@@ -0,0 +1,11 @@
using Contracts.Orders;
namespace Contracts.Requests;
public sealed class SendOrderApiRequest : ApiRequest<SendOrderParameters>
{
public SendOrderApiRequest()
{
Command = Commands.SendOrder;
}
}

View File

@@ -0,0 +1,10 @@
namespace Contracts.Responses;
public class ApiResponse
{
public required string Command { get; init; }
public bool Success { get; init; }
public string ErrorMessage { get; init; } = "";
}

View File

@@ -0,0 +1,33 @@
using System.Text.Json;
namespace Contracts.Responses;
public static class ApiResponseDeserializer
{
public static ApiResponse Deserialize(string json) =>
Deserialize(JsonDocument.Parse(json).RootElement);
public static ApiResponse Deserialize(ReadOnlySpan<byte> utf8Json) =>
Deserialize(JsonSerializer.Deserialize<JsonElement>(utf8Json, ApiJsonOptions.Instance)!);
public static async Task<ApiResponse> DeserializeAsync(Stream stream, CancellationToken cancellationToken = default)
{
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
return Deserialize(document.RootElement);
}
public static ApiResponse Deserialize(JsonElement json)
{
if (!json.TryGetProperty("Command", out var commandElement))
{
return json.Deserialize<ApiResponse>(ApiJsonOptions.Instance)!;
}
return commandElement.GetString() switch
{
Commands.GetMenu => json.Deserialize<GetMenuApiResponse>(ApiJsonOptions.Instance)!,
Commands.SendOrder => json.Deserialize<SendOrderApiResponse>(ApiJsonOptions.Instance)!,
_ => json.Deserialize<ApiResponse>(ApiJsonOptions.Instance)!,
};
}
}

View File

@@ -0,0 +1,8 @@
using Contracts.Menu;
namespace Contracts.Responses;
public sealed class GetMenuApiResponse : ApiResponse
{
public GetMenuData? Data { get; init; }
}

View File

@@ -0,0 +1,4 @@
namespace Contracts.Responses;
public sealed class SendOrderApiResponse : ApiResponse;

View File

@@ -16,9 +16,9 @@ public sealed class Order
Id = id ?? Guid.NewGuid();
}
public void AddItem(string menuItemId, decimal quantity)
public void AddItem(string id, decimal quantity)
{
ArgumentException.ThrowIfNullOrWhiteSpace(menuItemId);
ArgumentException.ThrowIfNullOrWhiteSpace(id);
if (quantity <= 0)
{
@@ -30,7 +30,7 @@ public sealed class Order
_items.Add(new OrderItem
{
MenuItemId = menuItemId,
Id = id,
Quantity = quantity,
});
}

View File

@@ -8,7 +8,7 @@ public sealed class OrderItem
/// <summary>
/// Идентификатор блюда на сервере (<see cref="Dish.Id"/>).
/// </summary>
public required string MenuItemId { get; init; }
public required string Id { get; init; }
/// <summary>
/// Количество. Для весовых блюд допускаются дробные значения.