Skip to content

Commit

Permalink
fix #145 by handling trees from server
Browse files Browse the repository at this point in the history
Prior to version 4 of the API we had to recursively fetch objects
to simulate a depth parameter. But the library wasn't handling
trees correctly when it used depths > 1. This resolves the issue
by writing a new method that handles GetObjects when version > 3.
  • Loading branch information
michaelgwelch committed Aug 3, 2024
1 parent 3d2c305 commit 2555d90
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 37 deletions.
123 changes: 122 additions & 1 deletion MetasysServices.Tests/MetasysClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1914,6 +1914,127 @@ public void TestGetObjectsUnauthorizedThrowsException()
PrintMessage($"TestGetObjectsUnauthorizedThrowsException: {e.Message}");
}


#region Large JSON Example
const string GetObjectsResponseThreeLevelsV4 = @"
{
""self"": ""https://r12adsdaily.cg.na.jci.com/api/v4/objects/9b9b3a80-bc5b-582a-8879-b1f1d1a0c5e4/objects?flatten=false&includeExtensions=true&includeInternal=false&depth=2"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2"",
""hasChildrenMatchingQuery"": true,
""name"": ""R12AdsDaily-e2"",
""id"": ""9b9b3a80-bc5b-582a-8879-b1f1d1a0c5e4"",
""objectType"": ""objectTypeEnumSet.n50Class"",
""classification"": ""device"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP1"",
""hasChildrenMatchingQuery"": true,
""name"": ""Field Bus MSTP1"",
""id"": ""aaeb5c40-9f36-5066-904d-fa81bab16fa6"",
""objectType"": ""objectTypeEnumSet.fieldBusClass"",
""classification"": ""integration"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP1.FAC2612-5"",
""hasChildrenMatchingQuery"": true,
""name"": ""AHU-1"",
""id"": ""773e4df4-af2d-5c3c-8011-2795e7c6fcbc"",
""objectType"": ""objectTypeEnumSet.fieldDeviceClass"",
""classification"": ""controller"",
""items"": []
},
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP1.AHU-1B"",
""hasChildrenMatchingQuery"": true,
""name"": ""SDA-F1-AHU-1"",
""id"": ""3644d0b6-130f-5131-b573-71298f414b41"",
""objectType"": ""objectTypeEnumSet.fieldDeviceClass"",
""classification"": ""controller"",
""items"": []
}
]
},
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP2"",
""hasChildrenMatchingQuery"": true,
""name"": ""Field Bus MSTP2"",
""id"": ""f2c7cae9-5942-5769-8afc-58060ae67727"",
""objectType"": ""objectTypeEnumSet.fieldBusClass"",
""classification"": ""integration"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP2.AHU-2"",
""hasChildrenMatchingQuery"": true,
""name"": ""SD-F2-AHU-2"",
""id"": ""ff796531-9b5f-572d-b21a-a44681acbc36"",
""objectType"": ""objectTypeEnumSet.fieldDeviceClass"",
""classification"": ""controller"",
""items"": []
}
]
},
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/VRF"",
""hasChildrenMatchingQuery"": true,
""name"": ""VRF"",
""id"": ""524e084d-159e-53cd-bae9-c91ab3f21d03"",
""objectType"": ""objectTypeEnumSet.containerClass"",
""classification"": ""folder"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/VRF.VRF-1"",
""hasChildrenMatchingQuery"": true,
""name"": ""IDU-1"",
""id"": ""06de2cd1-2c08-536f-bd50-bf883d55dedb"",
""objectType"": ""objectTypeEnumSet.containerClass"",
""classification"": ""folder"",
""items"": []
}
]
}
]
}
]
}
";

#endregion

[Test]
public void TestObjectsLevel3WithV4()
{
// This should not recurse like earlier versions. It should just fetch with a depth of 3 (one call)
// and assemble the correct response, which should leave off the root object per library design
// Arrange
httpTest.RespondWith(GetObjectsResponseThreeLevelsV4);
var originalApiVersion = client.Version;
try
{

client.Version = ApiVersion.v4;

// Act
var objects = client.GetObjects(mockid, 3);

// Assert
httpTest.ShouldHaveCalled($"https://hostname/api/v4/objects/{mockid}/objects")
.WithQueryParamValue("flatten", "false")
.WithQueryParamValue("includeInternal", "false")
.WithQueryParamValue("includeExtensions", "false")
.Times(1);

Assert.That(httpTest.CallLog.Count, Is.EqualTo(1));

Assert.That(objects.Count(), Is.EqualTo(3));
}
finally
{
client.Version = originalApiVersion;
}
}

#endregion

#region GetSpaces Tests
Expand Down Expand Up @@ -2559,4 +2680,4 @@ private class TestData
}
#endregion
}
}
}
97 changes: 97 additions & 0 deletions MetasysServices.Tests/MetasysObjectTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

namespace JohnsonControls.Metasys.BasicServices;

public class MetasysObjectTests
{


[Test]
public void Test()
{
// Arrange
var objectTree = JsonConvert.DeserializeObject<JToken>(ObjectTreeThreeLevels);

// Act
var metasysObject = new MetasysObject(objectTree, ApiVersion.v4);

// Assertions
Assert.That(metasysObject.ChildrenCount == 2);
Assert.That(metasysObject.Children.First().ChildrenCount == 3);
}



private const string ObjectTreeThreeLevels = @"
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics"",
""name"": ""Graphics"",
""id"": ""76609398-fa4b-51e0-bfa3-eab94d289241"",
""objectType"": ""objectTypeEnumSet.containerClass"",
""classification"": ""folder"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common"",
""name"": ""Common"",
""id"": ""55bd3987-6e6a-567d-8894-59684d2358d9"",
""objectType"": ""objectTypeEnumSet.containerClass"",
""classification"": ""folder"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common.Lighting"",
""name"": ""Lighting"",
""id"": ""ccaf1ca6-4ad5-5d07-a177-c38f68ae8235"",
""objectType"": ""objectTypeEnumSet.xamlGraphicClass"",
""classification"": """",
""items"": []
},
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common.Lighting_Alt"",
""name"": ""Lighting_Alt"",
""id"": ""f80ba7bb-6f9e-571f-a8c1-e1162ff20870"",
""objectType"": ""objectTypeEnumSet.xamlGraphicClass"",
""classification"": """",
""items"": []
},
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common.Weather Data"",
""name"": ""Weather Data"",
""id"": ""a7a4bd74-a4c4-53cb-b316-2c14f627e8bc"",
""objectType"": ""objectTypeEnumSet.xamlGraphicClass"",
""classification"": """",
""items"": []
}
]
},
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Hospital"",
""name"": ""Hospital"",
""id"": ""dca754ae-811e-5e7a-896c-f9fee7446b82"",
""objectType"": ""objectTypeEnumSet.containerClass"",
""classification"": ""folder"",
""items"": [
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Hospital.AHU-2"",
""name"": ""AHU-2"",
""id"": ""22a56b58-f5b6-55fb-9828-c7f0ce96ae2e"",
""objectType"": ""objectTypeEnumSet.xamlGraphicClass"",
""classification"": """",
""items"": []
},
{
""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Hospital.2ND_FLR"",
""name"": ""2ND_FLR"",
""id"": ""61839195-b443-56d4-a768-5bc7c0e61416"",
""objectType"": ""objectTypeEnumSet.xamlGraphicClass"",
""classification"": """",
""items"": []
}
]
}
]
}";

}
53 changes: 45 additions & 8 deletions MetasysServices/BasicServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,53 @@ protected List<NetworkDevice> ToNetworkDevice(List<JToken> items, ApiVersion ver
return objects;
}


/// <summary>
/// Gets objects from the server
/// </summary>
/// <remarks>
/// This method requires that Version 4 or greater of the API is being used. It overrides
/// the value of any <c>flatten</c> parameter to be `false` so that we get back a tree of
/// objects.
/// </remarks>
/// <param name="id"></param>
/// <param name="parameters"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">If <see cref="Vers"/> is less than <see cref="ApiVersion.v4"/></exception>
/// <returns>
/// If <paramref name="id"/> is specified then this method returns the children of the specified object (and any of their children if level > 1).
/// If <c>id</d> is <c>null</c> then this method returns the root object (and it's children; unless level is 0).

Check warning on line 180 in MetasysServices/BasicServiceProvider.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has badly formed XML -- 'End tag 'd' does not match the start tag 'c'.'

Check warning on line 180 in MetasysServices/BasicServiceProvider.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has badly formed XML -- 'End tag 'd' does not match the start tag 'c'.'
/// </returns>
protected async Task<List<MetasysObject>> GetObjectsAsync(Guid? id, Dictionary<string, string> parameters = null)
{
if (Version <= ApiVersion.v3)
{
throw new InvalidOperationException("This method requires v4 or later of the REST API");
}

object[] pathSegments = id == null ? ((List<object>)[]).ToArray() : [id.ToString(), "objects"];

parameters["flatten"] = "false";
var result = await GetRequestAsync("objects", parameters, pathSegments);
var firstNode = result["items"][0];
var rootObject = new MetasysObject(firstNode, Version);

if (id == null)
{
return [rootObject];
}
return rootObject.Children.ToList();
}


/// <summary>
/// Gets all child objects given a parent Guid asynchronously by requesting each available page.
/// Level indicates how deep to retrieve objects.
/// </summary>
/// <remarks>
/// A level of 1 only retrieves immediate children of the parent object.
/// <para> This should not be called on a site that supports REST API 4 or later as it makes a series of recursive calls that can
/// be handled in one call to the api</para>
/// </remarks>
/// <param name="id">The id of the object.</param>
/// <param name="parameters">Query string parameters in Key/Value format.</param>
Expand All @@ -176,6 +217,10 @@ protected List<NetworkDevice> ToNetworkDevice(List<JToken> items, ApiVersion ver
/// <exception cref="MetasysHttpParsingException"></exception>
protected async Task<List<TreeObject>> GetObjectChildrenAsync(Guid id, Dictionary<string, string> parameters = null, int levels = 1)
{
if (Version > ApiVersion.v3)
{
throw new InvalidOperationException("This operation doesn't exist for API > 3");
}
if (levels < 1)
{
return null;
Expand Down Expand Up @@ -293,7 +338,6 @@ public async Task<IEnumerable<MetasysObjectType>> GetResourceTypesAsync(string r
}

/// <summary>
/// Gets the type from a token retrieved from a typeUrl
/// </summary>
/// <param name="typeToken"></param>
/// <exception cref="MetasysHttpException"></exception>
Expand Down Expand Up @@ -501,7 +545,6 @@ protected async Task<JToken> GetRequestAsync(string resource, Dictionary<string,
JToken response = null;
// Create URL with base resource
Url url = new Url(resource);
// Concatenate segments with base resource url
url.AppendPathSegments(pathSegments);
// Set query parameters according to the input dictionary
if (parameters != null)
Expand All @@ -527,7 +570,6 @@ protected async Task<JToken> GetRequestAsync(string resource, Dictionary<string,
}

/// <summary>
/// Get typed items for the given resource asynchronously.
/// </summary>
/// <remarks>Optionally accepts query string parameters and additional path segments.</remarks>
/// <typeparam name="T"></typeparam>
Expand All @@ -543,7 +585,6 @@ protected async Task<PagedResult<T>> GetPagedResultsAsync<T>(string resource, Di


/// <summary>
/// Gets all items for the given resource asynchronously by requesting each available page.
/// </summary>
/// <param name="resource">The main resource to read.</param>
/// <param name="parameters">Query string parameters in Key Value format.</param>
Expand Down Expand Up @@ -674,7 +715,6 @@ protected async Task<JToken> GetBatchRequestAsync(string endpoint, IEnumerable<G
// Concatenate batch segment to use batch request and prepare the list of requests
url.AppendPathSegments("batch");
var objectsRequests = new List<ObjectRequest>();
// Concatenate batch segment to use batch request and prepare the list of requests
foreach (var id in ids)
{
foreach (var r in resources)
Expand Down Expand Up @@ -716,7 +756,6 @@ protected async Task<JToken> PostBatchRequestAsync(string endpoint, IEnumerable<
// Concatenate batch segment to use batch request and prepare the list of requests
url.AppendPathSegments("batch");
var objectsRequests = new List<ObjectRequest>();
// Concatenate batch segment to use batch request and prepare the list of requests
foreach (var r in requests)
{
Url relativeUrl = new Url(r.ObjectId.ToString());
Expand Down Expand Up @@ -778,7 +817,6 @@ protected async Task<JToken> PutBatchRequestAsync(string endpoint, IEnumerable<B
// Concatenate batch segment to use batch request and prepare the list of requests
url.AppendPathSegments("batch");
var objectsRequests = new List<ObjectRequest>();
// Concatenate batch segment to use batch request and prepare the list of requests
foreach (var r in requests)
{
Url relativeUrl = new Url(r.ObjectId.ToString());
Expand Down Expand Up @@ -851,7 +889,6 @@ protected async Task<JToken> PatchBatchRequestAsync(string endpoint, IEnumerable
// Concatenate batch segment to use batch request and prepare the list of requests
url.AppendPathSegments("batch");
var objectsRequests = new List<ObjectRequest>();
// Concatenate batch segment to use batch request and prepare the list of requests
foreach (var r in requests)
{
string activityManagementStatus = "";
Expand Down
Loading

0 comments on commit 2555d90

Please sign in to comment.