From 8fcd57da6164764d2fd05c51a628aee890f5689d Mon Sep 17 00:00:00 2001 From: Eoin O'Connor Date: Mon, 19 Aug 2019 20:52:41 +1000 Subject: [PATCH 1/7] [https://github.com/eoin55/HoneyBear.HalClient/issues/24] - Fixed a typo in a class name. Also, fixed a deprecated NuGet parameter (PackageLicenseUrl). --- Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj | 2 +- ...ceConverterExtenstions.cs => ResourceConverterExtensions.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Src/HoneyBear.HalClient/Models/{ResourceConverterExtenstions.cs => ResourceConverterExtensions.cs} (97%) diff --git a/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj b/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj index 3d41813..101c275 100644 --- a/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj +++ b/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj @@ -15,7 +15,7 @@ A lightweight fluent .NET client for navigating and consuming HAL APIs. Includes support for .NET Standard. bin\$(Configuration)\$(TargetFramework)\HoneyBear.HalClient.xml true - https://github.com/eoin55/HoneyBear.HalClient/blob/master/LICENSE + https://github.com/eoin55/HoneyBear.HalClient/blob/master/LICENSE https://github.com/eoin55/HoneyBear.HalClient HAL JSON Hypermedia HATEOAS REST DotNetCore NetStandard https://github.com/eoin55/HoneyBear.HalClient diff --git a/Src/HoneyBear.HalClient/Models/ResourceConverterExtenstions.cs b/Src/HoneyBear.HalClient/Models/ResourceConverterExtensions.cs similarity index 97% rename from Src/HoneyBear.HalClient/Models/ResourceConverterExtenstions.cs rename to Src/HoneyBear.HalClient/Models/ResourceConverterExtensions.cs index ac1b30a..e547bf4 100644 --- a/Src/HoneyBear.HalClient/Models/ResourceConverterExtenstions.cs +++ b/Src/HoneyBear.HalClient/Models/ResourceConverterExtensions.cs @@ -11,7 +11,7 @@ namespace HoneyBear.HalClient.Models /// /// Contains a set of IResource extensions. /// - public static class ResourceConverterExtenstions + public static class ResourceConverterExtensions { internal static T Data(this IResource source) where T : class, new() From 033db48f766a588dc397e6d95ba539b10e99c60f Mon Sep 17 00:00:00 2001 From: Eoin O'Connor Date: Mon, 19 Aug 2019 20:59:37 +1000 Subject: [PATCH 2/7] [https://github.com/eoin55/HoneyBear.HalClient/issues/24] - Fixed an issue where the HalClient wasn't retrieving embedded resources. Now it looks for embedded resources by matching on the self link. If there are no matches, it follows the link (via a HTTP request). --- .../HalClientAsyncUnitTests.cs | 86 +++++++-- .../HalClientTestContext.cs | 164 +++++++++++++++--- .../HalClientUnitTests.cs | 86 +++++++-- Src/HoneyBear.HalClient/HalClient.cs | 112 +++++++++--- 4 files changed, 374 insertions(+), 74 deletions(-) diff --git a/Src/HoneyBear.HalClient.Unit.Tests/HalClientAsyncUnitTests.cs b/Src/HoneyBear.HalClient.Unit.Tests/HalClientAsyncUnitTests.cs index 634dc0d..6a607f9 100644 --- a/Src/HoneyBear.HalClient.Unit.Tests/HalClientAsyncUnitTests.cs +++ b/Src/HoneyBear.HalClient.Unit.Tests/HalClientAsyncUnitTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading.Tasks; using HoneyBear.HalClient.Unit.Tests.ProxyResources; using NUnit.Framework; @@ -52,7 +51,7 @@ public void Navigate_to_single_embedded_resource() Task Act(IHalClient sut) => sut .RootAsync(RootUri) .GetAsync("order", new {orderRef = _context.OrderRef}, Curie) - .GetAsync("orderitem", Curie); + .GetAsync("orderitem-query", Curie); _context.ActAsync(Act); @@ -85,13 +84,60 @@ public void Navigate_to_paged_embedded_resource() Task Act(IHalClient sut) => sut .RootAsync(RootUri) .GetAsync("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) - .GetAsync("order", Curie); + .GetAsync("order", new {orderRef = _context.OrderRef}, Curie); _context.ActAsync(Act); _context.AssertThatEmbeddedPagedResourceIsPresent(); } + [Test] + public void Navigate_to_paged_embedded_resource_and_navigate_to_embedded_resource_via_parent() + { + _context + .ArrangeHomeResource() + .ArrangePagedResource(); + + Task Act(IHalClient sut) + { + var resource = + sut + .RootAsync(RootUri) + .GetAsync("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) + .Item(); + + return sut.GetAsync(resource, "order", new {orderRef = _context.OrderRef}, Curie); + } + + _context.ActAsync(Act); + + _context.AssertThatSingleResourceIsPresent(); + } + + [Test] + public void Navigate_to_paged_linked_resource_and_navigate_to_linked_resource_via_parent() + { + _context + .ArrangeHomeResource() + .ArrangePagedResourceWithLinkedResources() + .ArrangeSingleResource(); + + Task Act(IHalClient sut) + { + var resource = + sut + .RootAsync(RootUri) + .GetAsync("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) + .Item(); + + return sut.GetAsync(resource, "order", new {orderRef = _context.OrderRef}, Curie); + } + + _context.ActAsync(Act); + + _context.AssertThatSingleResourceIsPresent(); + } + [Test] public void Navigate_to_paged_embedded_resource_and_navigate_to_embedded_resource_array() { @@ -107,15 +153,12 @@ Task Act(IHalClient sut) .GetAsync("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) .GetAsync("order", Curie); - var order = - nav - .Items() - .First(); + var order = nav.Item(); return nav .GetAsync(order, "orderitem-query", Curie) - .GetAsync("orderitem", Curie); + .GetAsync("orderitem", new {orderItemRef = _context.OrderItemRef}, Curie); } _context.ActAsync(Act); @@ -136,17 +179,14 @@ Task Act(IHalClient sut) sut .RootAsync(RootUri) .GetAsync("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) - .GetAsync("order", Curie); + .GetAsync("order", new {orderRef = _context.OrderRef}, Curie); - var order = - nav - .Items() - .First(); + var order = nav.Item(); return nav .GetAsync(order, "orderitem-query", Curie) - .GetAsync("orderitem", Curie); + .GetAsync("orderitem", new {orderItemRef = _context.OrderItemRef}, Curie); } _context.ActAsync(Act); @@ -560,7 +600,7 @@ Task Act(IHalClient sut) => sut } [Test] - public void HalClientAsync_can_be_created_with_specifed_HttpClient() => + public void HalClientAsync_can_be_created_with_specified_HttpClient() => _context.AssertThatHttpClientCanBeProvided(); [Test] @@ -624,5 +664,21 @@ public void Throws_exception_when_HTTP_request_is_unsuccessful() Assert.Throws(() => _context.ActAsync(Act)); } + + [Test] + public void Throws_exception_when_resource_does_not_exist() + { + _context + .ArrangeHomeResource() + .ArrangePagedResource() + .ArrangeFailedOrderRequest(); + + Task Act(IHalClient sut) => sut + .RootAsync(RootUri) + .GetAsync("order-queryby-user", new { userRef = HalClientTestContext.UserRef }, Curie) + .GetAsync("order", new { orderRef = HalClientTestContext.NonExistentOrderRef }, Curie); + + Assert.Throws(() => _context.ActAsync(Act)); + } } } \ No newline at end of file diff --git a/Src/HoneyBear.HalClient.Unit.Tests/HalClientTestContext.cs b/Src/HoneyBear.HalClient.Unit.Tests/HalClientTestContext.cs index daa0001..594c55d 100644 --- a/Src/HoneyBear.HalClient.Unit.Tests/HalClientTestContext.cs +++ b/Src/HoneyBear.HalClient.Unit.Tests/HalClientTestContext.cs @@ -25,9 +25,11 @@ internal sealed class HalClientTestContext public const string RootUri = "/v1/version/1"; public const string Curie = "retail"; public Guid OrderRef => _order.OrderRef; + public Guid OrderItemRef => _orderItem.OrderItemRef; public OrderAdd OrderAdd { get; } public OrderEdit OrderEdit { get; } public static readonly Guid UserRef = Guid.NewGuid(); + public static readonly Guid NonExistentOrderRef = Guid.NewGuid(); private readonly IHalClient _sut; private readonly IJsonHttpClient _http; @@ -35,6 +37,7 @@ internal sealed class HalClientTestContext private readonly dynamic _curies; private readonly Version _version; private readonly Order _order; + private readonly Order _otherOrder; private readonly OrderItem _orderItem; private readonly PagedList _paged; private bool _hasCurie; @@ -48,7 +51,8 @@ public HalClientTestContext() _http = _fixture.Freeze(); _version = _fixture.Create(); - _order = _fixture.Build().With(x => x.DeliveryDate, null).Create(); + _order = _fixture.Build().With(x => x.OrderNumber, "2").With(x => x.DeliveryDate, null).Create(); + _otherOrder = _fixture.Build().With(x => x.OrderNumber, "1").Create(); _orderItem = _fixture.Create(); _paged = _fixture.Create(); OrderAdd = _fixture.Create(); @@ -101,16 +105,27 @@ public HalClientTestContext ArrangeSingleResource() return this; } - public void ArrangePagedResource() => + public HalClientTestContext ArrangePagedResource() + { ArrangeGet($"/v1/order?userRef={UserRef}", CreatePagedResourceJson()); + return this; + } + + public HalClientTestContext ArrangePagedResourceWithLinkedResources() + { + ArrangeGet($"/v1/order?userRef={UserRef}", CreatePagedResourceWithLinkedResourcesJson()); + + return this; + } + public void ArrangePagedResourceWithEmbeddedArrayOfResources() => ArrangeGet($"/v1/order?userRef={UserRef}", CreatePagedResourceWithEmbeddedArrayOfResourcesJson()); public void ArrangePagedResourceWithLinkedArrayOfResources() { ArrangeGet($"/v1/order?userRef={UserRef}", CreatePagedResourceWithLinkedArrayOfResourcesJson()); - ArrangeGet($"/v1/orderitem?orderRef={OrderRef}", CreatePagedResourceWithArrayOfResourcesJson()); + ArrangeGet($"/v1/orderitem?orderRef={OrderRef}", CreatePagedResourceWithArrayOfResourcesJson(OrderRef)); } public void ArrangeDefaultPagedResource() => @@ -171,6 +186,11 @@ public void ArrangeFailedHomeRequest() => .Expect(h => h.GetAsync(RootUri)) .Return(NotFound()); + public void ArrangeFailedOrderRequest() => + _http + .Expect(h => h.GetAsync($"/v1/order/{NonExistentOrderRef}")) + .Return(NotFound()); + public void Act(Func act) => _result = act(_sut); @@ -363,12 +383,13 @@ private object CreateSingleResourceJson() => curies = _curies, self = new {href = $"/v1/order/{OrderRef}"}, retail_order_edit = new {href = $"/v1/order/{OrderRef}"}, - retail_order_delete = new {href = $"/v1/order/{OrderRef}"} + retail_order_delete = new {href = $"/v1/order/{OrderRef}"}, + retail_orderitem_query = new {href = $"/v1/orderitem?orderRef={OrderRef}"} }, _embedded = new { - retail_orderitem = + retail_orderitem_query = new[] { new @@ -402,12 +423,13 @@ private object CreateSingleResourceJsonWithoutCurie() => { self = new {href = $"/v1/order/{OrderRef}"}, order_edit = new {href = $"/v1/order/{OrderRef}"}, - order_delete = new {href = $"/v1/order/{OrderRef}"} + order_delete = new {href = $"/v1/order/{OrderRef}"}, + orderitem_query = new {href = $"/v1/orderitem?orderRef={OrderRef}"} }, _embedded = new { - orderitem = + orderitem_query = new[] { new @@ -439,7 +461,8 @@ private object CreatePagedResourceJson() => new { curies = _curies, - self = new {href = $"/v1/order?userRef={UserRef}"} + self = new {href = $"/v1/order?userRef={UserRef}"}, + retail_order = new {href = "/v1/order/{orderRef}", templated = true} }, _embedded = new @@ -447,6 +470,19 @@ private object CreatePagedResourceJson() => retail_order = new[] { + new + { + _otherOrder.OrderRef, + _otherOrder.OrderNumber, + _otherOrder.Status, + _otherOrder.Total, + _links = + new + { + curies = _curies, + self = new {href = $"/v1/order/{_otherOrder.OrderRef}"} + } + }, new { _order.OrderRef, @@ -457,13 +493,29 @@ private object CreatePagedResourceJson() => new { curies = _curies, - self = new {href = $"/v1/order/{OrderRef}"} + self = new {href = $"/v1/order/{_order.OrderRef}"} } } } } }; + private object CreatePagedResourceWithLinkedResourcesJson() => + new + { + _paged.PageNumber, + _paged.PageSize, + _paged.KnownPagesAvailable, + _paged.TotalItemsCount, + _links = + new + { + curies = _curies, + self = new {href = $"/v1/order?userRef={UserRef}"}, + retail_order = new {href = "/v1/order/{orderRef}", templated = true} + } + }; + private object CreatePagedResourceWithEmbeddedArrayOfResourcesJson() => new { @@ -475,7 +527,8 @@ private object CreatePagedResourceWithEmbeddedArrayOfResourcesJson() => new { curies = _curies, - self = new {href = $"/v1/order?userRef={UserRef}"} + self = new {href = $"/v1/order?userRef={UserRef}"}, + retail_order = new {href = "/v1/order/{orderRef}", templated = true} }, _embedded = new @@ -483,6 +536,26 @@ private object CreatePagedResourceWithEmbeddedArrayOfResourcesJson() => retail_order = new[] { + new + { + _otherOrder.OrderRef, + _otherOrder.OrderNumber, + _otherOrder.Status, + _otherOrder.DeliveryDate, + _otherOrder.Total, + _links = + new + { + curies = _curies, + self = new {href = $"/v1/order/{_otherOrder.OrderRef}"}, + retail_orderitem_query = new {href = $"/v1/orderitem?orderRef={_otherOrder.OrderRef}"} + }, + _embedded = + new + { + retail_orderitem_query = CreatePagedResourceWithArrayOfResourcesJson(_otherOrder.OrderRef) + } + }, new { _order.OrderRef, @@ -494,12 +567,13 @@ private object CreatePagedResourceWithEmbeddedArrayOfResourcesJson() => new { curies = _curies, - self = new {href = $"/v1/order/{OrderRef}"} + self = new {href = $"/v1/order/{_order.OrderRef}"}, + retail_orderitem_query = new {href = $"/v1/orderitem?orderRef={_order.OrderRef}"} }, _embedded = new { - retail_orderitem_query = CreatePagedResourceWithArrayOfResourcesJson() + retail_orderitem_query = CreatePagedResourceWithArrayOfResourcesJson(_order.OrderRef) } } } @@ -517,7 +591,8 @@ private object CreatePagedResourceWithLinkedArrayOfResourcesJson() => new { curies = _curies, - self = new {href = $"/v1/order?userRef={UserRef}"} + self = new {href = $"/v1/order?userRef={UserRef}"}, + retail_order = new {href = "/v1/order/{orderRef}", templated = true} }, _embedded = new @@ -525,6 +600,21 @@ private object CreatePagedResourceWithLinkedArrayOfResourcesJson() => retail_order = new[] { + new + { + _otherOrder.OrderRef, + _otherOrder.OrderNumber, + _otherOrder.Status, + _otherOrder.DeliveryDate, + _otherOrder.Total, + _links = + new + { + curies = _curies, + self = new {href = $"/v1/order/{_otherOrder.OrderRef}"}, + retail_orderitem_query = new {href = $"/v1/orderitem?orderRef={_otherOrder.OrderRef}"} + } + }, new { _order.OrderRef, @@ -536,16 +626,18 @@ private object CreatePagedResourceWithLinkedArrayOfResourcesJson() => new { curies = _curies, - self = new {href = $"/v1/order/{OrderRef}"}, - retail_orderitem_query = new {href = $"/v1/orderitem?orderRef={OrderRef}"} + self = new {href = $"/v1/order/{_order.OrderRef}"}, + retail_orderitem_query = new {href = $"/v1/orderitem?orderRef={_order.OrderRef}"} } } } } }; - private object CreatePagedResourceWithArrayOfResourcesJson() => - new + private object CreatePagedResourceWithArrayOfResourcesJson(Guid orderRef) + { + var other = _fixture.Create(); + return new { _paged.PageNumber, _paged.PageSize, @@ -555,7 +647,8 @@ private object CreatePagedResourceWithArrayOfResourcesJson() => new { curies = _curies, - self = new {href = $"/v1/orderitem?orderRef={OrderRef}"} + self = new {href = $"/v1/orderitem?orderRef={orderRef}"}, + retail_orderitem = new {href = "/v1/orderitem/{orderItemRef}", templated = true} }, _embedded = new @@ -580,6 +673,7 @@ private object CreatePagedResourceWithArrayOfResourcesJson() => } } }; + } private object CreateDefaultPagedResourceJson() => new @@ -592,7 +686,8 @@ private object CreateDefaultPagedResourceJson() => new { curies = _curies, - self = new {href = "/v1/order"} + self = new {href = "/v1/order"}, + retail_order = new {href = "/v1/order/{orderRef}", templated = true} }, _embedded = new @@ -600,6 +695,35 @@ private object CreateDefaultPagedResourceJson() => retail_order = new[] { + new + { + _otherOrder.OrderRef, + _otherOrder.OrderNumber, + _otherOrder.Status, + _otherOrder.DeliveryDate, + _otherOrder.Total, + _links = + new + { + curies = _curies, + self = new {href = $"/v1/order/{_otherOrder.OrderRef}"} + }, + _embedded = + new + { + retail_user = + new + { + UserRef, + _links = + new + { + curies = _curies, + self = new {href = $"/v1/user/{UserRef}"} + } + } + } + }, new { _order.OrderRef, @@ -611,7 +735,7 @@ private object CreateDefaultPagedResourceJson() => new { curies = _curies, - self = new {href = $"/v1/order/{OrderRef}"} + self = new {href = $"/v1/order/{_order.OrderRef}"} }, _embedded = new diff --git a/Src/HoneyBear.HalClient.Unit.Tests/HalClientUnitTests.cs b/Src/HoneyBear.HalClient.Unit.Tests/HalClientUnitTests.cs index 9637698..2afdb2f 100644 --- a/Src/HoneyBear.HalClient.Unit.Tests/HalClientUnitTests.cs +++ b/Src/HoneyBear.HalClient.Unit.Tests/HalClientUnitTests.cs @@ -1,5 +1,4 @@ -using System.Linq; -using HoneyBear.HalClient.Models; +using HoneyBear.HalClient.Models; using HoneyBear.HalClient.Unit.Tests.ProxyResources; using NUnit.Framework; @@ -51,7 +50,7 @@ public void Navigate_to_single_embedded_resource() IHalClient Act(IHalClient sut) => sut .Root(RootUri) .Get("order", new {orderRef = _context.OrderRef}, Curie) - .Get("orderitem", Curie); + .Get("orderitem-query", Curie); _context.Act(Act); @@ -84,13 +83,60 @@ public void Navigate_to_paged_embedded_resource() IHalClient Act(IHalClient sut) => sut .Root(RootUri) .Get("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) - .Get("order", Curie); + .Get("order", new {orderRef = _context.OrderRef}, Curie); _context.Act(Act); _context.AssertThatEmbeddedPagedResourceIsPresent(); } + [Test] + public void Navigate_to_paged_embedded_resource_and_navigate_to_embedded_resource_via_parent() + { + _context + .ArrangeHomeResource() + .ArrangePagedResource(); + + IHalClient Act(IHalClient sut) + { + var resource = + sut + .Root(RootUri) + .Get("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) + .Item(); + + return sut.Get(resource, "order", new {orderRef = _context.OrderRef}, Curie); + } + + _context.Act(Act); + + _context.AssertThatSingleResourceIsPresent(); + } + + [Test] + public void Navigate_to_paged_linked_resource_and_navigate_to_linked_resource_via_parent() + { + _context + .ArrangeHomeResource() + .ArrangePagedResourceWithLinkedResources() + .ArrangeSingleResource(); + + IHalClient Act(IHalClient sut) + { + var resource = + sut + .Root(RootUri) + .Get("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) + .Item(); + + return sut.Get(resource, "order", new {orderRef = _context.OrderRef}, Curie); + } + + _context.Act(Act); + + _context.AssertThatSingleResourceIsPresent(); + } + [Test] public void Navigate_to_paged_embedded_resource_and_navigate_to_embedded_resource_array() { @@ -104,14 +150,13 @@ IHalClient Act(IHalClient sut) sut .Root(RootUri) .Get("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) - .Get("order", Curie) - .Items() - .First(); + .Get("order", new {orderRef = _context.OrderRef}, Curie) + .Item(); return sut .Get(order, "orderitem-query", Curie) - .Get("orderitem", Curie); + .Get("orderitem", new {orderItemRef = _context.OrderItemRef}, Curie); } _context.Act(Act); @@ -132,14 +177,13 @@ IHalClient Act(IHalClient sut) sut .Root(RootUri) .Get("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) - .Get("order", Curie) - .Items() - .First(); + .Get("order", new {orderRef = _context.OrderRef}, Curie) + .Item(); return sut .Get(order, "orderitem-query", Curie) - .Get("orderitem", Curie); + .Get("orderitem", new {orderItemRef = _context.OrderItemRef}, Curie); } _context.Act(Act); @@ -553,7 +597,7 @@ IHalClient Act(IHalClient sut) => sut } [Test] - public void HalClient_can_be_created_with_specifed_HttpClient() => + public void HalClient_can_be_created_with_specified_HttpClient() => _context.AssertThatHttpClientCanBeProvided(); [Test] @@ -617,5 +661,21 @@ public void Throws_exception_when_HTTP_request_is_unsuccessful() Assert.Throws(() => _context.Act(Act)); } + + [Test] + public void Throws_exception_when_resource_does_not_exist() + { + _context + .ArrangeHomeResource() + .ArrangePagedResource() + .ArrangeFailedOrderRequest(); + + IHalClient Act(IHalClient sut) => sut + .Root(RootUri) + .Get("order-queryby-user", new {userRef = HalClientTestContext.UserRef}, Curie) + .Get("order", new {orderRef = HalClientTestContext.NonExistentOrderRef}, Curie); + + Assert.Throws(() => _context.Act(Act)); + } } } \ No newline at end of file diff --git a/Src/HoneyBear.HalClient/HalClient.cs b/Src/HoneyBear.HalClient/HalClient.cs index c0c4fa2..86844b5 100644 --- a/Src/HoneyBear.HalClient/HalClient.cs +++ b/Src/HoneyBear.HalClient/HalClient.cs @@ -177,12 +177,8 @@ public IHalClient Get(string rel, object parameters, string curie) { var relationship = Relationship(rel, curie); - var embedded = _current.FirstOrDefault(r => r.Embedded.Any(e => e.Rel == relationship)); - if (embedded != null) - { - var current = embedded.Embedded.Where(e => e.Rel == relationship); - return new HalClient(this, current); - } + if (TryGetEmbedded(relationship, parameters, out var embedded)) + return new HalClient(this, embedded); return BuildAndExecute(relationship, parameters, uri => _client.GetAsync(uri)); } @@ -199,12 +195,8 @@ public async Task GetAsync(string rel, object parameters, string cur { var relationship = Relationship(rel, curie); - var embedded = _current.FirstOrDefault(r => r.Embedded.Any(e => e.Rel == relationship)); - if (embedded != null) - { - var current = embedded.Embedded.Where(e => e.Rel == relationship); - return new HalClient(this, current); - } + if (TryGetEmbedded(relationship, parameters, out var embedded)) + return new HalClient(this, embedded); return await BuildAndExecuteAsync(relationship, parameters, uri => _client.GetAsync(uri)); } @@ -253,16 +245,18 @@ public IHalClient Get(IResource resource, string rel, object parameters, string { var relationship = Relationship(rel, curie); - if (resource.Embedded.Any(e => e.Rel == relationship)) - { - var current = resource.Embedded.Where(e => e.Rel == relationship); - return new HalClient(this, current); - } + // If there are no parameters, check if there are any embedded resources with the given link relation + if (parameters == null && TryGetEmbedded(resource, relationship, out var embedded)) + return new HalClient(this, embedded); var link = resource.Links.FirstOrDefault(l => l.Rel == relationship); if (link == null) throw new FailedToResolveRelationship(relationship); + // If there are parameters, check if any of the embedded resources match on the self link URI + if (parameters != null && TryGetEmbedded(resource, relationship, parameters, link, out embedded)) + return new HalClient(this, embedded); + return Execute(Construct(link, parameters), uri => _client.GetAsync(uri)); } @@ -279,16 +273,18 @@ public async Task GetAsync(IResource resource, string rel, object pa { var relationship = Relationship(rel, curie); - if (resource.Embedded.Any(e => e.Rel == relationship)) - { - var current = resource.Embedded.Where(e => e.Rel == relationship); - return new HalClient(this, current); - } + // If there are no parameters, check if there are any embedded resources with the given link relation + if (parameters == null && TryGetEmbedded(resource, relationship, out var embedded)) + return new HalClient(this, embedded); var link = resource.Links.FirstOrDefault(l => l.Rel == relationship); if (link == null) throw new FailedToResolveRelationship(relationship); + // If there are parameters, check if any of the embedded resources match on the self link URI + if (parameters != null && TryGetEmbedded(resource, relationship, parameters, link, out embedded)) + return new HalClient(this, embedded); + return await ExecuteAsync(Construct(link, parameters), uri => _client.GetAsync(uri)); } @@ -561,23 +557,87 @@ public bool Has(string rel, string curie) || _current.Any(r => r.Links.Any(l => l.Rel == relationship)); } + private bool TryGetEmbedded(string relationship, object parameters, out IEnumerable embedded) + { + embedded = null; + + // If there are no parameters, check if there are any embedded resources with the given link relation + if (parameters == null) + return TryGetEmbedded(relationship, out embedded); + + // Check if any of the current resource's links match the given link relation + var current = _current.FirstOrDefault(r => r.Links.Any(l => l.Rel == relationship)); + if (current == null) + return false; + + // Check if any of the embedded resources match on the self link URI + var allEmbedded = _current.FirstOrDefault(r => r.Embedded.Any(e => e.Rel == relationship && e.Links.Any(s => s.Rel == "self"))); + if (allEmbedded == null) + return false; + + var linkUri = Construct(current.Links.First(l => l.Rel == relationship), parameters); + + embedded = + allEmbedded.Embedded.Where(e => + e.Rel == relationship + && e.Links.Any(s => s.Rel == "self") + && Construct(e.Links.First(s => s.Rel == "self"), parameters) == linkUri); + return embedded.Any(); + } + + private bool TryGetEmbedded(string relationship, out IEnumerable embedded) + { + var resource = _current.FirstOrDefault(r => r.Embedded.Any(e => e.Rel == relationship)); + if (resource == null) + { + embedded = null; + return false; + } + + embedded = resource.Embedded.Where(e => e.Rel == relationship); + return true; + } + + private static bool TryGetEmbedded(IResource resource, string relationship, out IEnumerable embedded) + { + if (resource.Embedded.Any(e => e.Rel == relationship)) + { + embedded = resource.Embedded.Where(e => e.Rel == relationship); + return true; + } + + embedded = null; + return false; + } + + private static bool TryGetEmbedded(IResource resource, string relationship, object parameters, ILink link, out IEnumerable embedded) + { + var linkUri = Construct(link, parameters); + embedded = + resource.Embedded.Where(e => + e.Rel == relationship + && e.Links.Any(s => s.Rel == "self") + && Construct(e.Links.First(s => s.Rel == "self"), parameters) == linkUri); + return embedded.Any(); + } + private IHalClient BuildAndExecute(string relationship, object parameters, Func> command) { var resource = _current.FirstOrDefault(r => r.Links.Any(l => l.Rel == relationship)); if (resource == null) throw new FailedToResolveRelationship(relationship); - var link = resource.Links.FirstOrDefault(l => l.Rel == relationship); + var link = resource.Links.First(l => l.Rel == relationship); return Execute(Construct(link, parameters), command); } - internal Task BuildAndExecuteAsync(string relationship, object parameters, Func> command) + private Task BuildAndExecuteAsync(string relationship, object parameters, Func> command) { var resource = _current.FirstOrDefault(r => r.Links.Any(l => l.Rel == relationship)); if (resource == null) throw new FailedToResolveRelationship(relationship); - var link = resource.Links.FirstOrDefault(l => l.Rel == relationship); + var link = resource.Links.First(l => l.Rel == relationship); return ExecuteAsync(Construct(link, parameters), command); } @@ -588,7 +648,7 @@ private IHalClient Execute(string uri, Func> c return Process(result); } - internal async Task ExecuteAsync(string uri, Func> command) + private async Task ExecuteAsync(string uri, Func> command) { var result = await command(uri); From 3061ca85ce349f28323105442a445986e25798f4 Mon Sep 17 00:00:00 2001 From: Eoin O'Connor Date: Mon, 19 Aug 2019 21:06:10 +1000 Subject: [PATCH 3/7] [https://github.com/eoin55/HoneyBear.HalClient/issues/24] - Bumped the minor version number, as this fix could change the behaviour for clients. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 6315e5e..cd311a9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.2.{build} +version: 2.3.{build} image: Visual Studio 2017 skip_tags: true configuration: Release From 8bb9aeecd55b0053c606c1e86956a5724b0a49e4 Mon Sep 17 00:00:00 2001 From: Eoin O'Connor Date: Wed, 4 Sep 2019 20:45:00 +1000 Subject: [PATCH 4/7] [https://github.com/eoin55/HoneyBear.HalClient/issues/24] - Attempting to fix an issue with the NuGet PackageLicense property. --- Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj b/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj index 101c275..1a5db93 100644 --- a/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj +++ b/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj @@ -15,7 +15,7 @@ A lightweight fluent .NET client for navigating and consuming HAL APIs. Includes support for .NET Standard. bin\$(Configuration)\$(TargetFramework)\HoneyBear.HalClient.xml true - https://github.com/eoin55/HoneyBear.HalClient/blob/master/LICENSE + MIT https://github.com/eoin55/HoneyBear.HalClient HAL JSON Hypermedia HATEOAS REST DotNetCore NetStandard https://github.com/eoin55/HoneyBear.HalClient From 7cf3e670008eacff4bfd3d6917a91f45df8efbea Mon Sep 17 00:00:00 2001 From: Eoin O'Connor Date: Wed, 4 Sep 2019 21:28:41 +1000 Subject: [PATCH 5/7] [https://github.com/eoin55/HoneyBear.HalClient/issues/24] - Reverting last commit: Attempting to fix an issue with the NuGet PackageLicense property. --- Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj b/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj index 1a5db93..101c275 100644 --- a/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj +++ b/Src/HoneyBear.HalClient/HoneyBear.HalClient.csproj @@ -15,7 +15,7 @@ A lightweight fluent .NET client for navigating and consuming HAL APIs. Includes support for .NET Standard. bin\$(Configuration)\$(TargetFramework)\HoneyBear.HalClient.xml true - MIT + https://github.com/eoin55/HoneyBear.HalClient/blob/master/LICENSE https://github.com/eoin55/HoneyBear.HalClient HAL JSON Hypermedia HATEOAS REST DotNetCore NetStandard https://github.com/eoin55/HoneyBear.HalClient From e396ff1e367aaaf2172cf4d42fe1a820ea2242f2 Mon Sep 17 00:00:00 2001 From: Eoin O'Connor Date: Wed, 4 Sep 2019 21:30:16 +1000 Subject: [PATCH 6/7] [https://github.com/eoin55/HoneyBear.HalClient/issues/24] - Attempting to fix an issue with OpenCover, which appears to have stopped producing coverage reports. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index cd311a9..83da79a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,7 +24,7 @@ build_script: after_test: - ps: >- . $env:USERPROFILE\.nuget\packages\OpenCover\4.6.519\tools\OpenCover.Console.exe ` - -register:user ` + -register:Path32 ` -filter:"+[HoneyBear.HalClient]*" ` -target:"$env:USERPROFILE\.nuget\packages\NUnit.ConsoleRunner\3.8.0\tools\nunit3-console.exe" ` -targetargs:".\Src\HoneyBear.HalClient.Unit.Tests\bin\$env:CONFIGURATION\net471\HoneyBear.HalClient.Unit.Tests.dll" ` From 2f68661fc18e2fc6b8262960604c75c5d3a9fb6f Mon Sep 17 00:00:00 2001 From: Eoin O'Connor Date: Wed, 4 Sep 2019 21:37:13 +1000 Subject: [PATCH 7/7] * [https://github.com/eoin55/HoneyBear.HalClient/issues/24] - Attempting (again) to fix an issue with OpenCover, which appears to have stopped producing coverage reports. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 83da79a..6c57fae 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,7 +24,7 @@ build_script: after_test: - ps: >- . $env:USERPROFILE\.nuget\packages\OpenCover\4.6.519\tools\OpenCover.Console.exe ` - -register:Path32 ` + -register:Path64 ` -filter:"+[HoneyBear.HalClient]*" ` -target:"$env:USERPROFILE\.nuget\packages\NUnit.ConsoleRunner\3.8.0\tools\nunit3-console.exe" ` -targetargs:".\Src\HoneyBear.HalClient.Unit.Tests\bin\$env:CONFIGURATION\net471\HoneyBear.HalClient.Unit.Tests.dll" `