From f1aab15c18191ec0dee4a82756556824649ee4be Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 20:11:18 +1000 Subject: [PATCH 1/3] Document SeqApiClient; reorganize so that the exposed HttpClient sends authenticated requests with correct Accept headers --- src/Seq.Api/Client/SeqApiClient.cs | 167 ++++++++++++++++++++++++++--- 1 file changed, 155 insertions(+), 12 deletions(-) diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index e313df4..2187c74 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -19,6 +19,9 @@ namespace Seq.Api.Client { + /// + /// A low-level client that provides navigation over the linked resource structure of the Seq HTTP API. + /// public class SeqApiClient : IDisposable { readonly string _apiKey; @@ -35,6 +38,14 @@ public class SeqApiClient : IDisposable Converters = { new StringEnumConverter(), new LinkCollectionConverter() } }); + /// + /// Construct a . + /// + /// The base URL of the Seq server. + /// An API key to use when making requests to the server, if required. + /// Whether default credentials will be sent with HTTP requests; the default is true. + /// The time to wait before canceling long-running HTTP requests; the default (null) is to use the + /// system request timeout, normally 100 seconds. To disable timing out at the client, pass . public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true, TimeSpan? requestTimeout = null) { ServerUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl)); @@ -53,45 +64,110 @@ public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCrede baseAddress += "/"; HttpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) }; + HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType)); + + if (_apiKey != null) + HttpClient.DefaultRequestHeaders.Add("X-Seq-ApiKey", _apiKey); + if (requestTimeout != null) HttpClient.Timeout = requestTimeout.Value; } + /// + /// The base URL of the Seq server. + /// public string ServerUrl { get; } + /// + /// The HTTP client used when making requests to the Seq server. The HTTP client is configured with the server's base address, collects any cookies + /// sent with responses from the API, and will send the appropriate Accept and X-Seq-ApiKey headers by default. + /// public HttpClient HttpClient { get; } + /// + /// Get the root entity describing the server and providing links to other resources available from the API. + /// + /// A supporting cancellation. + /// The root entity. public Task GetRootAsync(CancellationToken cancellationToken = default) { return HttpGetAsync("api", cancellationToken); } + /// + /// Issue a GET request returning a by following the from . + /// + /// The entity type into which the response should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// The response entity. public Task GetAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); return HttpGetAsync(linkUri, cancellationToken); } + /// + /// Issue a GET request returning a string by following the from . + /// + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// The response as a string. public Task GetStringAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); return HttpGetStringAsync(linkUri, cancellationToken); } + /// + /// Issue a GET request returning a list of by following the from . + /// + /// The entity type into which the response items should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// The response entities. public Task> ListAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); return HttpGetAsync>(linkUri, cancellationToken); } + /// + /// Issue a POST request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// A task that signals request completion. public async Task PostAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - new StreamReader(stream).ReadToEnd(); + using (var reader = new StreamReader(stream)) + reader.ReadToEnd(); } + /// + /// Issue a POST request accepting a serialized and returning a by following from . + /// + /// The entity type that will be serialized into the request payload. + /// The entity type into which the response should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response entity. public async Task PostAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); @@ -100,14 +176,35 @@ public async Task PostAsync(ILinked entity, strin return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream))); } + /// + /// Issue a POST request accepting a serialized and returning a string by following from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response string. public async Task PostReadStringAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - return await new StreamReader(stream).ReadToEndAsync(); + using (var reader = new StreamReader(stream)) + return await reader.ReadToEndAsync(); } + /// + /// Issue a POST request accepting a serialized and returning a stream by following from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response stream. public async Task PostReadStreamAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); @@ -115,22 +212,55 @@ public async Task PostReadStreamAsync(ILinked entity, string li return await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); } + /// + /// Issue a PUT request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// A task that signals request completion. public async Task PutAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Put, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - new StreamReader(stream).ReadToEnd(); + using (var reader = new StreamReader(stream)) + reader.ReadToEnd(); } + /// + /// Issue a DELETE request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// A task that signals request completion. public async Task DeleteAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Delete, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - new StreamReader(stream).ReadToEnd(); + using (var reader = new StreamReader(stream)) + reader.ReadToEnd(); } + /// + /// Issue a DELETE request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// The entity type into which the response should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response entity. public async Task DeleteAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); @@ -139,11 +269,28 @@ public async Task DeleteAsync(ILinked entity, str return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream))); } + /// + /// Connect to a websocket at the address specified by following from . + /// + /// The type of the values received over the websocket. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// A stream of values from the websocket. public async Task> StreamAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { return await WebSocketStreamAsync(entity, link, parameters, reader => _serializer.Deserialize(new JsonTextReader(reader)), cancellationToken); } + /// + /// Connect to a websocket at the address specified by following from . + /// + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// A stream of raw messages from the websocket. public async Task> StreamTextAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { return await WebSocketStreamAsync(entity, link, parameters, reader => reader.ReadToEnd(), cancellationToken); @@ -174,16 +321,12 @@ async Task HttpGetStringAsync(string url, CancellationToken cancellation { var request = new HttpRequestMessage(HttpMethod.Get, url); var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - return await new StreamReader(stream).ReadToEndAsync(); + using (var reader = new StreamReader(stream)) + return await reader.ReadToEndAsync(); } async Task HttpSendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - if (_apiKey != null) - request.Headers.Add("X-Seq-ApiKey", _apiKey); - - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType)); - var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); @@ -213,8 +356,7 @@ HttpContent MakeJsonContent(object content) static string ResolveLink(ILinked entity, string link, IDictionary parameters = null) { - Link linkItem; - if (!entity.Links.TryGetValue(link, out linkItem)) + if (!entity.Links.TryGetValue(link, out var linkItem)) throw new NotSupportedException($"The requested link `{link}` isn't available on entity `{entity}`."); var expression = linkItem.GetUri(); @@ -238,6 +380,7 @@ static string ResolveLink(ILinked entity, string link, IDictionary> public void Dispose() { HttpClient.Dispose(); From ef9b2594da0de66f123c09466f0d6be0d74d73f1 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 21:20:56 +1000 Subject: [PATCH 2/3] Improve documentation coverage of core API client/model types. Fixes some historical issues with Link, removing the confusing isLiteral mechanism to cement the expectation that all links in the Seq API are encoded as URI templates, even if they contain zero parameters --- seq-api.sln.DotSettings | 5 + src/Seq.Api/Client/SeqApiClient.cs | 36 ++++--- src/Seq.Api/Client/SeqApiException.cs | 16 +++- src/Seq.Api/Model/Entity.cs | 30 +++++- src/Seq.Api/Model/ILinked.cs | 22 ++++- src/Seq.Api/Model/Link.cs | 93 ++++++++++++------- src/Seq.Api/Model/LinkCollection.cs | 24 +++-- src/Seq.Api/Model/ResourceGroup.cs | 24 ++++- src/Seq.Api/SeqConnection.cs | 16 +++- .../Serialization/LinkCollectionConverter.cs | 20 +++- .../Streams/ObservableStream.Unsubscriber.cs | 7 +- src/Seq.Api/Streams/ObservableStream.cs | 8 +- 12 files changed, 230 insertions(+), 71 deletions(-) diff --git a/seq-api.sln.DotSettings b/seq-api.sln.DotSettings index 969cc30..6e98e3d 100644 --- a/seq-api.sln.DotSettings +++ b/seq-api.sln.DotSettings @@ -1,11 +1,16 @@  AD True + True True + True + True True True True True + True True + True True diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index 2187c74..6de445c 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -359,25 +373,7 @@ static string ResolveLink(ILinked entity, string link, IDictionary p.Key).Except(template.GetParameterNames()).ToArray(); - if (missing.Any()) - throw new ArgumentException($"The URI template `{expression}` does not contain parameter: `{string.Join("`, `", missing)}`."); - - foreach (var parameter in parameters) - { - var value = parameter.Value is DateTime time - ? time.ToString("O") - : parameter.Value; - - template.SetParameter(parameter.Key, value); - } - } - - return template.Resolve(); + return linkItem.GetUri(parameters); } /// > diff --git a/src/Seq.Api/Client/SeqApiException.cs b/src/Seq.Api/Client/SeqApiException.cs index 71d5966..fc53da9 100644 --- a/src/Seq.Api/Client/SeqApiException.cs +++ b/src/Seq.Api/Client/SeqApiException.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Net; namespace Seq.Api.Client diff --git a/src/Seq.Api/Model/Entity.cs b/src/Seq.Api/Model/Entity.cs index b31f7c3..7d3a98f 100644 --- a/src/Seq.Api/Model/Entity.cs +++ b/src/Seq.Api/Model/Entity.cs @@ -1,14 +1,42 @@ -namespace Seq.Api.Model +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model { + /// + /// A uniquely-identifiable resource available from the Seq HTTP API. + /// public abstract class Entity : ILinked { + /// + /// Construct an . + /// protected Entity() { Links = new LinkCollection(); } + /// + /// The entity's unique identifier. This will be null if a newly-instantiated + /// has not yet been sent to the server. + /// public string Id { get; set; } + /// + /// A collection of outbound links from the entity. This will be empty if the entity + /// was instantiated locally and not received from the API. + /// public LinkCollection Links { get; set; } } } diff --git a/src/Seq.Api/Model/ILinked.cs b/src/Seq.Api/Model/ILinked.cs index df7dc62..ae30187 100644 --- a/src/Seq.Api/Model/ILinked.cs +++ b/src/Seq.Api/Model/ILinked.cs @@ -1,7 +1,27 @@ -namespace Seq.Api.Model +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model { + /// + /// A resource that carries outbound links. + /// public interface ILinked { + /// + /// Links associated with the resource. + /// LinkCollection Links { get; } } } \ No newline at end of file diff --git a/src/Seq.Api/Model/Link.cs b/src/Seq.Api/Model/Link.cs index b3a77d2..0513d68 100644 --- a/src/Seq.Api/Model/Link.cs +++ b/src/Seq.Api/Model/Link.cs @@ -1,50 +1,81 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Tavis.UriTemplates; +// ReSharper disable PossibleMultipleEnumeration + namespace Seq.Api.Model { - public class Link + /// + /// A hypermedia link. A link is an RFC 6570 URI template that can be + /// parameterized in order to produce a complete URI (if the template contains no + /// parameters then it may also be a literal URI). + /// + public struct Link { - readonly bool _isLiteral; - readonly string _href; - readonly IDictionary _parameters; - - public Link(string href, bool isLiteral = true) - : this(href, new Dictionary()) - { - _isLiteral = isLiteral; - } + /// + /// An empty link. + /// + public static Link Empty { get; } = default; - public Link(string href, IDictionary parameters) + /// + /// Construct a link. + /// + /// The URI template. + public Link(string template) { - if (href == null) throw new ArgumentNullException(nameof(href)); - if (parameters == null) throw new ArgumentNullException(nameof(parameters)); - _href = href; - _parameters = parameters; + Template = template ?? throw new ArgumentNullException(nameof(template)); } - public Link(string href, object parameters) - : this(href, parameters - .GetType() - .GetTypeInfo() - .DeclaredProperties - .ToDictionary(p => p.Name, p => p.GetValue(parameters))) - { - } + /// + /// Get the unprocessed URI template. + /// + public string Template { get; } - public string GetUri() + /// + /// Parameterize the link to construct a URI. + /// + /// Parameters to substitute into the template, if any. + /// A constructed URI. + /// This method ensures that templates containing parameters cannot be accidentally + /// used as URIs. + public string GetUri(IDictionary parameters = null) { - if (_isLiteral) return _href; + if (Template == null) throw new InvalidOperationException("Attempted to process an empty URI template."); - var template = new UriTemplate(_href); - foreach (var parameter in _parameters) + var template = new UriTemplate(Template); + if (parameters != null) { - template.SetParameter(parameter.Key, parameter.Value); + var missing = parameters.Select(p => p.Key).Except(template.GetParameterNames()); + if (missing.Any()) + throw new ArgumentException($"The URI template `{Template}` does not contain parameter: `{string.Join("`, `", missing)}`."); + + foreach (var parameter in parameters) + { + var value = parameter.Value is DateTime time + ? time.ToString("O") + : parameter.Value; + + template.SetParameter(parameter.Key, value); + } } + return template.Resolve(); } } -} \ No newline at end of file +} diff --git a/src/Seq.Api/Model/LinkCollection.cs b/src/Seq.Api/Model/LinkCollection.cs index e4afbf3..aebab05 100644 --- a/src/Seq.Api/Model/LinkCollection.cs +++ b/src/Seq.Api/Model/LinkCollection.cs @@ -1,18 +1,30 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using Newtonsoft.Json; using Seq.Api.Serialization; namespace Seq.Api.Model { + /// + /// A collection of s indexed by case-insensitive name. + /// [JsonConverter(typeof(LinkCollectionConverter))] public class LinkCollection : Dictionary { public LinkCollection() : base(StringComparer.OrdinalIgnoreCase) { } - - public string GetUri(string name) - { - return this[name].GetUri(); - } } } diff --git a/src/Seq.Api/Model/ResourceGroup.cs b/src/Seq.Api/Model/ResourceGroup.cs index f634ddd..3837ca8 100644 --- a/src/Seq.Api/Model/ResourceGroup.cs +++ b/src/Seq.Api/Model/ResourceGroup.cs @@ -1,12 +1,34 @@ -namespace Seq.Api.Model +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model { + /// + /// A resource group is a logical partitioning of the API, usually + /// associated with a particular type of entity. + /// public class ResourceGroup : ILinked { + /// + /// Construct a . + /// public ResourceGroup() { Links = new LinkCollection(); } + /// public LinkCollection Links { get; set; } } } diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index cc5aa28..3c03704 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.Net; using System.Threading; diff --git a/src/Seq.Api/Serialization/LinkCollectionConverter.cs b/src/Seq.Api/Serialization/LinkCollectionConverter.cs index 01dc797..c560bed 100644 --- a/src/Seq.Api/Serialization/LinkCollectionConverter.cs +++ b/src/Seq.Api/Serialization/LinkCollectionConverter.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; @@ -6,12 +20,12 @@ namespace Seq.Api.Serialization { - public class LinkCollectionConverter : JsonConverter + class LinkCollectionConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var lc = (LinkCollection)value; - var dictionary = lc.ToDictionary(kv => kv.Key, kv => kv.Value.GetUri()); + var dictionary = lc.ToDictionary(kv => kv.Key, kv => kv.Value.Template); serializer.Serialize(writer, dictionary); } diff --git a/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs b/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs index e4380e3..f7f510b 100644 --- a/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs +++ b/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs @@ -12,6 +12,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + using System; namespace Seq.Api.Streams @@ -25,10 +26,8 @@ sealed class Unsubscriber : IDisposable public Unsubscriber(ObservableStream sink, IObserver observer) { - if (sink == null) throw new ArgumentNullException(nameof(sink)); - if (observer == null) throw new ArgumentNullException(nameof(observer)); - _stream = sink; - _observer = observer; + _stream = sink ?? throw new ArgumentNullException(nameof(sink)); + _observer = observer ?? throw new ArgumentNullException(nameof(observer)); } public void Dispose() diff --git a/src/Seq.Api/Streams/ObservableStream.cs b/src/Seq.Api/Streams/ObservableStream.cs index 0ddc419..2af4853 100644 --- a/src/Seq.Api/Streams/ObservableStream.cs +++ b/src/Seq.Api/Streams/ObservableStream.cs @@ -24,8 +24,10 @@ namespace Seq.Api.Streams { - // Some questionable synchronization in this one; probably better to take the Rx dependency - // and use the Rx support classes here. + /// + /// A stream of values read from a . + /// + /// The type of the values. public sealed partial class ObservableStream : IObservable, IDisposable { readonly object _syncRoot = new object(); @@ -42,6 +44,7 @@ internal ObservableStream(ClientWebSocket socket, Func deserializ _socket = socket ?? throw new ArgumentNullException(nameof(socket)); } + /// public IDisposable Subscribe(IObserver observer) { if (observer == null) throw new ArgumentNullException(nameof(observer)); @@ -182,6 +185,7 @@ static void OnError(IEnumerable exceptions) throw new AggregateException("At least one observer failed to accept the event", exceptions); } + /// public void Dispose() { lock (_syncRoot) From de522651a59103133671e5f0b1fe29c33098e5e8 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 21:27:16 +1000 Subject: [PATCH 3/3] Add some very basic tests for Link --- test/Seq.Api.Tests/LinkTests.cs | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/Seq.Api.Tests/LinkTests.cs diff --git a/test/Seq.Api.Tests/LinkTests.cs b/test/Seq.Api.Tests/LinkTests.cs new file mode 100644 index 0000000..ae6a636 --- /dev/null +++ b/test/Seq.Api.Tests/LinkTests.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Seq.Api.Model; +using Xunit; + +namespace Seq.Api.Tests +{ + public class LinkTests + { + [Fact] + public void ALinkWithNoParametersIsLiteral() + { + const string uri = "https://example.com"; + var link = new Link(uri); + var constructed = link.GetUri(); + Assert.Equal(uri, constructed); + } + + [Fact] + public void AParameterizedLinkCanBeConstructed() + { + const string template = "https://example.com/{name}"; + var link = new Link(template); + var constructed = link.GetUri(new Dictionary {["name"] = "test"}); + Assert.Equal("https://example.com/test", constructed); + } + + [Fact] + public void InvalidParametersAreDetected() + { + const string template = "https://example.com"; + var link = new Link(template); + Assert.Throws(() => link.GetUri(new Dictionary {["name"] = "test"})); + } + } +}