Skip to content
This repository was archived by the owner on Sep 3, 2024. It is now read-only.

Commit eabb74c

Browse files
Merge pull request #10 from thefringeninja/json-benefits-schema
Json Schema
2 parents 2c634ea + 3482c1d commit eabb74c

20 files changed

Lines changed: 328 additions & 63 deletions

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project>
33
<PropertyGroup>
4-
<VersionPrefix>1.0.0-rc1</VersionPrefix>
4+
<VersionPrefix>1.0.0-rc2</VersionPrefix>
55
<Authors>João P. Bragança</Authors>
66
<PackageProjectUrl>https://github.com/damianh/SqlStreamStore.HAL</PackageProjectUrl>
77
<PackageLicenseUrl>https://github.com/damianh/SqlStreamStore.HAL/blob/master/LICENSE</PackageLicenseUrl>
Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
namespace SqlStreamStore.HAL.DevServer
22
{
33
using System;
4+
using System.Linq;
5+
using System.Net.Http;
6+
using System.Net.Http.Headers;
47
using System.Threading.Tasks;
58
using Microsoft.AspNetCore.Builder;
69
using Microsoft.AspNetCore.Hosting;
710
using Microsoft.AspNetCore.Http;
811
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Primitives;
913
using MidFunc = System.Func<
1014
Microsoft.AspNetCore.Http.HttpContext,
1115
System.Func<System.Threading.Tasks.Task>,
@@ -15,10 +19,12 @@
1519
internal class DevServerStartup : IStartup
1620
{
1721
private readonly IStreamStore _streamStore;
22+
private readonly HttpClient _httpClient;
1823

1924
public DevServerStartup(IStreamStore streamStore)
2025
{
2126
_streamStore = streamStore;
27+
_httpClient = new HttpClient();
2228
}
2329

2430
public IServiceProvider ConfigureServices(IServiceCollection services) => services
@@ -27,8 +33,10 @@ public IServiceProvider ConfigureServices(IServiceCollection services) => servic
2733

2834
public void Configure(IApplicationBuilder app) => app
2935
.UseResponseCompression()
36+
.Use(VaryAccept)
3037
.Use(CatchAndDisplayErrors)
31-
.Use(AllowAllOrigins)
38+
.Use(SqlStreamStreamBrowserJavascript)
39+
.Use(SqlStreamStreamBrowserHtml)
3240
.UseSqlStreamStoreHal(_streamStore);
3341

3442
private static MidFunc CatchAndDisplayErrors => async (context, next) =>
@@ -43,19 +51,81 @@ public void Configure(IApplicationBuilder app) => app
4351
}
4452
};
4553

46-
// don't actually do this in production
47-
private static MidFunc AllowAllOrigins => (context, next) =>
54+
private static MidFunc VaryAccept => (context, next) =>
4855
{
49-
context.Response.OnStarting(_ =>
50-
{
51-
var response = (HttpResponse) _;
52-
response.Headers["Access-Control-Allow-Origin"] = "*";
56+
Task Vary(object state)
57+
{
58+
var response = (HttpResponse)state;
59+
60+
response.Headers.AppendCommaSeparatedValues("Vary", "Accept");
61+
62+
return Task.CompletedTask;
63+
}
64+
65+
context.Response.OnStarting(Vary, context.Response);
66+
67+
return next();
68+
};
5369

54-
return Task.CompletedTask;
55-
},
56-
context.Response);
70+
private MidFunc SqlStreamStreamBrowserJavascript => (context, next) =>
71+
{
72+
if(context.Request.Path.Value?.EndsWith(".js") ?? false)
73+
{
74+
var segments = context.Request.Path.ToUriComponent().Split('/');
75+
if(segments.Length > 2)
76+
{
77+
return RedirectToPathBase(context, $"/{segments.Last()}");
78+
}
5779

80+
return ForwardToClientDevServer(
81+
context,
82+
context.Request.PathBase + context.Request.Path);
83+
}
5884
return next();
5985
};
86+
87+
private MidFunc SqlStreamStreamBrowserHtml => (context, next)
88+
=> GetAcceptHeaders(context.Request)
89+
.Any(header => header == "text/html")
90+
? ForwardToClientDevServer(context, context.Request.PathBase.ToUriComponent())
91+
: next();
92+
93+
private static string[] GetAcceptHeaders(HttpRequest contextRequest)
94+
=> Array.ConvertAll(
95+
contextRequest.Headers.GetCommaSeparatedValues("Accept"),
96+
value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header)
97+
? header.MediaType
98+
: null);
99+
100+
private Task RedirectToPathBase(HttpContext context, PathString path)
101+
{
102+
context.Response.Redirect(context.Request.PathBase + path);
103+
104+
return Task.CompletedTask;
105+
}
106+
107+
private async Task ForwardToClientDevServer(HttpContext context, PathString path)
108+
{
109+
using(var request = new HttpRequestMessage(
110+
new HttpMethod(context.Request.Method),
111+
new UriBuilder
112+
{
113+
Port = 3000,
114+
Host = "localhost",
115+
Path = path.ToUriComponent(),
116+
Query = context.Request.QueryString.ToUriComponent()
117+
}.Uri))
118+
using(var response = await _httpClient.SendAsync(request))
119+
using(var stream = await response.Content.ReadAsStreamAsync())
120+
{
121+
context.Response.StatusCode = (int) response.StatusCode;
122+
foreach(var header in response.Headers.Concat(response.Content.Headers))
123+
{
124+
context.Response.Headers.Add(header.Key, new StringValues(header.Value.ToArray()));
125+
}
126+
127+
await stream.CopyToAsync(context.Response.Body, 8192, context.RequestAborted);
128+
}
129+
}
60130
}
61131
}

src/SqlStreamStore.HAL.Tests/StreamMetadataTests.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ await _fixture.HttpClient.SendAsync(
4040
((string) resource.State.metadataJson).ShouldBeNull();
4141

4242
resource.ShouldLink(Constants.Relations.Self, "metadata");
43-
resource.ShouldLink(Constants.Relations.Feed, "../");
43+
resource.ShouldLink(Constants.Relations.Metadata, "metadata");
44+
resource.ShouldLink(Constants.Relations.Feed, $"../{StreamId}");
4445
}
4546
}
4647

@@ -85,7 +86,8 @@ await _fixture.HttpClient.SendAsync(
8586
})).ShouldBeTrue();
8687

8788
resource.ShouldLink(Constants.Relations.Self, "metadata");
88-
resource.ShouldLink(Constants.Relations.Feed, "../");
89+
resource.ShouldLink(Constants.Relations.Metadata, "metadata");
90+
resource.ShouldLink(Constants.Relations.Feed, $"../{StreamId}");
8991
}
9092
}
9193

@@ -126,7 +128,8 @@ public async Task set_metadata()
126128
})).ShouldBeTrue();
127129

128130
resource.ShouldLink(Constants.Relations.Self, "metadata");
129-
resource.ShouldLink(Constants.Relations.Feed, "../");
131+
resource.ShouldLink(Constants.Relations.Metadata, "metadata");
132+
resource.ShouldLink(Constants.Relations.Feed, $"../{StreamId}");
130133
}
131134
}
132135

src/SqlStreamStore.HAL/Constants.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,15 @@ public static class Relations
3131
public const string Feed = "streamStore:feed";
3232
public const string Message = "streamStore:message";
3333
public const string Metadata = "streamStore:metadata";
34+
public const string AppendToStream = "streamStore:append";
35+
public const string Delete = "streamStore:delete";
3436
}
3537

3638
public static class Streams
3739
{
3840
public const string All = "stream";
3941
public const string Metadata = "metadata";
4042
}
41-
42-
public static IReadOnlyDictionary<int, string> ReasonPhrases { get; }
43-
= new ReadOnlyDictionary<int, string>(new Dictionary<int, string>
44-
{
45-
[200] = "OK",
46-
[201] = "Created",
47-
[307] = "Moved Temporarily",
48-
[400] = "Bad Request",
49-
[404] = "Not Found",
50-
[405] = "Method Not Allowed",
51-
[409] = "Conflict"
52-
});
5343

5444
public static class ReadDirection
5545
{

src/SqlStreamStore.HAL/ExceptionHandlingMiddleware.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ private static readonly IDictionary<Type, Func<Exception, Response>> s_exception
3939
[typeof(InvalidAppendRequestException)] = ex => new Response(new HALResponse(new
4040
{
4141
type = ex.GetType().Name,
42-
title = "Bad format."
42+
title = "Bad format.",
43+
detail = ex.Message
4344
}), 400),
4445
[typeof(Exception)] = s_defaultExceptionHandler
4546
};

src/SqlStreamStore.HAL/HttpContextExtensions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
namespace SqlStreamStore.HAL
22
{
3+
using System;
34
using System.IO;
45
using System.Linq;
56
using System.Net.Http;
7+
using System.Net.Http.Headers;
68
using System.Threading.Tasks;
79
using Microsoft.AspNetCore.Http;
810
using Newtonsoft.Json;
911
using Newtonsoft.Json.Serialization;
12+
using SqlStreamStore.Streams;
1013

1114
internal static class HttpContextExtensions
1215
{
@@ -76,6 +79,14 @@ public static int GetExpectedVersion(this HttpRequest request)
7679
request.Headers[Constants.Headers.ExpectedVersion],
7780
out var expectedVersion)
7881
? expectedVersion
79-
: Streams.ExpectedVersion.Any;
82+
: ExpectedVersion.Any;
83+
84+
public static string[] GetAcceptHeaders(this HttpRequest contextRequest)
85+
=> Array.ConvertAll(
86+
contextRequest.Headers
87+
.GetCommaSeparatedValues("Accept"),
88+
value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header)
89+
? header.MediaType
90+
: null);
8091
}
8192
}

src/SqlStreamStore.HAL/Resources/AllStreamResource.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ public async Task<Response> GetPage(
6666
payload,
6767
metadata = message.JsonMetadata
6868
})
69-
.AddLinks(Links.Message.Self(message)))));
69+
.AddLinks(
70+
Links.Message.Self(message),
71+
Links.Message.Feed(message)))));
7072

7173
if(operation.FromPositionInclusive == Position.End)
7274
{
@@ -157,6 +159,10 @@ public static class Message
157159
public static Link Self(StreamMessage message) => new Link(
158160
Constants.Relations.Self,
159161
$"streams/{message.StreamId}/{message.StreamVersion}");
162+
163+
public static Link Feed(StreamMessage message) => new Link(
164+
Constants.Relations.Feed,
165+
$"streams/{message.StreamId}");
160166
}
161167
}
162168
}

src/SqlStreamStore.HAL/Resources/AppendStreamOperation.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,34 +51,44 @@ private AppendStreamOperation(HttpRequest request, JObject body)
5151
: this(request, new JArray { body })
5252
{ }
5353

54-
private static NewStreamMessage ParseNewStreamMessage(JToken newStreamMessage, int index)
54+
private static NewStreamMessageDto ParseNewStreamMessage(JToken newStreamMessage, int index)
5555
{
5656
if(!Guid.TryParse(newStreamMessage.Value<string>("messageId"), out var messageId))
5757
{
58-
throw new InvalidAppendRequestException($"'{nameof(messageId)}' at index {index} was improperly formatted.");
58+
throw new InvalidAppendRequestException(
59+
$"'{nameof(messageId)}' at index {index} was improperly formatted.");
5960
}
61+
6062
if(messageId == Guid.Empty)
6163
{
6264
throw new InvalidAppendRequestException($"'{nameof(messageId)}' at index {index} was empty.");
6365
}
66+
6467
var type = newStreamMessage.Value<string>("type");
6568

6669
if(type == null)
6770
{
6871
throw new InvalidAppendRequestException($"'{nameof(type)}' at index {index} was not set.");
6972
}
70-
71-
return new NewStreamMessage(
72-
messageId,
73-
type,
74-
newStreamMessage.Value<JToken>("jsonData").ToString(),
75-
newStreamMessage.Value<JToken>("jsonMetadata")?.ToString());
73+
74+
return new NewStreamMessageDto
75+
{
76+
MessageId = messageId,
77+
Type = type,
78+
JsonData = newStreamMessage.Value<JToken>("jsonData"),
79+
JsonMetadata = newStreamMessage.Value<JToken>("jsonMetadata")
80+
};
7681
}
82+
7783
public string StreamId { get; }
7884
public int ExpectedVersion { get; }
79-
public NewStreamMessage[] NewStreamMessages { get; }
85+
public NewStreamMessageDto[] NewStreamMessages { get; }
8086

81-
public Task<AppendResult> Invoke(IStreamStore streamStore, CancellationToken ct)
82-
=> streamStore.AppendToStream(StreamId, ExpectedVersion, NewStreamMessages, ct);
87+
public Task<AppendResult> Invoke(IStreamStore streamStore, CancellationToken ct)
88+
=> streamStore.AppendToStream(
89+
StreamId,
90+
ExpectedVersion,
91+
Array.ConvertAll(NewStreamMessages, dto => dto.ToNewStreamMessage()),
92+
ct);
8393
}
8494
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace SqlStreamStore.HAL.Resources
2+
{
3+
using System;
4+
using Newtonsoft.Json.Linq;
5+
using SqlStreamStore.Streams;
6+
7+
internal class NewStreamMessageDto
8+
{
9+
public Guid MessageId { get; set; }
10+
public string Type { get; set; }
11+
public JToken JsonData { get; set; }
12+
public JToken JsonMetadata { get; set; }
13+
14+
public NewStreamMessage ToNewStreamMessage()
15+
=> new NewStreamMessage(MessageId, Type, JsonData.ToString(), JsonMetadata?.ToString());
16+
}
17+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/hyper-schema#",
3+
"title": "Append to Stream",
4+
"type": "object",
5+
"required": [
6+
"messageId",
7+
"type"
8+
],
9+
"properties": {
10+
"messageId": {
11+
"type": "string",
12+
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$",
13+
"x-schema-form": {
14+
"key": "messageId",
15+
"type": "uuid"
16+
}
17+
},
18+
"type": {
19+
"type": "string"
20+
},
21+
"jsonData": {
22+
"type": "object",
23+
"x-schema-form": {
24+
"key": "jsonData",
25+
"type": "textarea",
26+
"rows": 30
27+
}
28+
},
29+
"jsonMetadata": {
30+
"type": "string",
31+
"x-schema-form": {
32+
"key": "jsonMetadata",
33+
"type": "textarea",
34+
"rows": 30
35+
}
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)