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 29, 2024
1 parent dc0b44b commit e74f3a8
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 6 deletions.
121 changes: 121 additions & 0 deletions MetasysServices.Tests/MetasysClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1901,6 +1901,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
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"": []
}
]
}
]
}";

}
45 changes: 45 additions & 0 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>

Check warning on line 177 in MetasysServices/BasicServiceProvider.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has cref attribute 'Vers' that could not be resolved

Check warning on line 177 in MetasysServices/BasicServiceProvider.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has cref attribute 'Vers' that could not be resolved
/// <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</c> is <c>null</c> then this method returns the root object (and it's children; unless level is 0).
/// </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 id 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(Object 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
9 changes: 6 additions & 3 deletions MetasysServices/MetasysClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public int Timeout
if (Spaces != null) { Spaces.Version = version.Value; }
if (Streams != null) { Streams.Version = version.Value; }
if (Trends != null) { Trends.Version = version.Value; }
base.Version = value;
}
}

Expand Down Expand Up @@ -513,10 +514,12 @@ public async Task<IEnumerable<MetasysObject>> GetObjectsAsync(ObjectId id, int l
parameters = new Dictionary<string, string>
{
{ "depth", levels.ToString() },
{ "flatten", "true".ToString() }, //This parameter is needed to get the data in a 'flat' way and keep consistency in the logic to retrieve the objects
{ "includeExtensions", includeExtensions.ToString() },
{ "includeInternal", includeInternalObjects.ToString() } //This param has different name when version > v3
{ "flatten", "false" },
{ "includeExtensions", includeExtensions.ToString().ToLower() },
{ "includeInternal", includeInternalObjects.ToString().ToLower() } //This param has different name when version > v3
};

return await GetObjectsAsync(id, parameters);

Check warning on line 522 in MetasysServices/MetasysClient.cs

View workflow job for this annotation

GitHub Actions / build

'ObjectId.implicit operator Guid(ObjectId)' is obsolete: 'This is not guaranteed to always succeed. Please use methods that use strings or ObjectIds for object identifiers instead.'

Check warning on line 522 in MetasysServices/MetasysClient.cs

View workflow job for this annotation

GitHub Actions / build

'ObjectId.implicit operator Guid(ObjectId)' is obsolete: 'This is not guaranteed to always succeed. Please use methods that use strings or ObjectIds for object identifiers instead.'
}

var objects = await GetObjectChildrenAsync(id, parameters, levels).ConfigureAwait(false);
Expand Down
23 changes: 20 additions & 3 deletions MetasysServices/Models/MetasysObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ public class MetasysObject : Utils.ObjectUtil
/// <summary>
/// The number of direct children objects.
/// </summary>
/// <value>The number of children or -1 if there is no children data.</value>
public int ChildrenCount { set; get; }
/// <value>The number of children.</value>
public int ChildrenCount => Children.Count();

/// <summary>
/// Name of the Equipment Definition mapped with the Equipment Instance
Expand All @@ -84,12 +84,29 @@ public MetasysObject() { }
public string Classification { get; set; }
private const string DefaultClassification = "object";

/// <summary>
/// Create a tree of objects out of a root node
/// </summary>
/// <param name="token"></param>
/// <param name="version"></param>
internal MetasysObject(JToken token, ApiVersion version)
{
var root = token;
var children = root["items"] as JArray;
Initialize(root, version);

Children = children?.Select(child => new MetasysObject(child, version)).ToList() ?? [];
}

internal MetasysObject(JToken token, ApiVersion version, IEnumerable<MetasysObject> children = null, MetasysObjectTypeEnum? type = null)
{
Children = children ?? new List<MetasysObject>(); // Return empty list by convention for null
ChildrenCount = Children?.Count() ?? 0; // Children count is 0 when children is null
Type = type;
Initialize(token, version);
}

private void Initialize(JToken token, ApiVersion version)
{
JObject jobj = token.ToObject<JObject>();
try
{
Expand Down

0 comments on commit e74f3a8

Please sign in to comment.