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 e313df4..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; @@ -19,6 +33,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 +52,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 +78,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 +190,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 +226,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 +283,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 +335,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,31 +370,13 @@ 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(); - var template = new UriTemplate(expression); - if (parameters != null) - { - var missing = parameters.Select(p => 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); } + /// > public void Dispose() { HttpClient.Dispose(); 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) 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"})); + } + } +}