json utility: add sort

This commit adds a utility function |sortJsonArrayByKey|. It can sort an
json array by value of a given key of each element.

Use cases includes:
1. sort the MemberCollection by @odata.id

Tested:
1. unit test passed;

Signed-off-by: Nan Zhou <nanzhoumails@gmail.com>
Signed-off-by: Ed Tanous <edtanous@google.com>
Change-Id: Idc175fab3af5c6102a5a3439b712b659ecb76468
diff --git a/redfish-core/include/utils/json_utils.hpp b/redfish-core/include/utils/json_utils.hpp
index b98cf0e..5ecce0a 100644
--- a/redfish-core/include/utils/json_utils.hpp
+++ b/redfish-core/include/utils/json_utils.hpp
@@ -18,10 +18,12 @@
 #include "error_messages.hpp"
 #include "http_request.hpp"
 #include "http_response.hpp"
+#include "human_sort.hpp"
 #include "logging.hpp"
 
 #include <nlohmann/json.hpp>
 
+#include <algorithm>
 #include <array>
 #include <cmath>
 #include <cstddef>
@@ -590,5 +592,106 @@
     }
     return readJson(jsonRequest, res, key, std::forward<UnpackTypes&&>(in)...);
 }
+
+// Determines if two json objects are less, based on the presence of the
+// @odata.id key
+inline int odataObjectCmp(const nlohmann::json& a, const nlohmann::json& b)
+{
+    using object_t = nlohmann::json::object_t;
+    const object_t* aObj = a.get_ptr<const object_t*>();
+    const object_t* bObj = b.get_ptr<const object_t*>();
+
+    if (aObj == nullptr)
+    {
+        if (bObj == nullptr)
+        {
+            return 0;
+        }
+        return -1;
+    }
+    if (bObj == nullptr)
+    {
+        return 1;
+    }
+    object_t::const_iterator aIt = aObj->find("@odata.id");
+    object_t::const_iterator bIt = bObj->find("@odata.id");
+    // If either object doesn't have the key, they get "sorted" to the end.
+    if (aIt == aObj->end())
+    {
+        if (bIt == bObj->end())
+        {
+            return 0;
+        }
+        return -1;
+    }
+    if (bIt == bObj->end())
+    {
+        return 1;
+    }
+    const nlohmann::json::string_t* nameA =
+        aIt->second.get_ptr<const std::string*>();
+    const nlohmann::json::string_t* nameB =
+        bIt->second.get_ptr<const std::string*>();
+    // If either object doesn't have a string as the key, they get "sorted" to
+    // the end.
+    if (nameA == nullptr)
+    {
+        if (nameB == nullptr)
+        {
+            return 0;
+        }
+        return -1;
+    }
+    if (nameB == nullptr)
+    {
+        return 1;
+    }
+    boost::urls::url_view aUrl(*nameA);
+    boost::urls::url_view bUrl(*nameB);
+    auto segmentsAIt = aUrl.segments().begin();
+    auto segmentsBIt = bUrl.segments().begin();
+
+    while (true)
+    {
+        if (segmentsAIt == aUrl.segments().end())
+        {
+            if (segmentsBIt == bUrl.segments().end())
+            {
+                return 0;
+            }
+            return -1;
+        }
+        if (segmentsBIt == bUrl.segments().end())
+        {
+            return 1;
+        }
+        int res = alphanumComp(*segmentsAIt, *segmentsBIt);
+        if (res != 0)
+        {
+            return res;
+        }
+
+        segmentsAIt++;
+        segmentsBIt++;
+    }
+};
+
+struct ODataObjectLess
+{
+    bool operator()(const nlohmann::json& left,
+                    const nlohmann::json& right) const
+    {
+        return odataObjectCmp(left, right) < 0;
+    }
+};
+
+// Sort the JSON array by |element[key]|.
+// Elements without |key| or type of |element[key]| is not string are smaller
+// those whose |element[key]| is string.
+inline void sortJsonArrayByOData(nlohmann::json::array_t& array)
+{
+    std::sort(array.begin(), array.end(), ODataObjectLess());
+}
+
 } // namespace json_util
 } // namespace redfish
diff --git a/test/redfish-core/include/utils/json_utils_test.cpp b/test/redfish-core/include/utils/json_utils_test.cpp
index fa5e39a..3fca4e7 100644
--- a/test/redfish-core/include/utils/json_utils_test.cpp
+++ b/test/redfish-core/include/utils/json_utils_test.cpp
@@ -354,5 +354,58 @@
     EXPECT_THAT(res.jsonValue, IsEmpty());
 }
 
+TEST(odataObjectCmp, PositiveCases)
+{
+    EXPECT_EQ(0, odataObjectCmp(R"({"@odata.id": "/redfish/v1/1"})"_json,
+                                R"({"@odata.id": "/redfish/v1/1"})"_json));
+    EXPECT_EQ(0, odataObjectCmp(R"({"@odata.id": ""})"_json,
+                                R"({"@odata.id": ""})"_json));
+    EXPECT_EQ(0, odataObjectCmp(R"({"@odata.id": 42})"_json,
+                                R"({"@odata.id": 0})"_json));
+    EXPECT_EQ(0, odataObjectCmp(R"({})"_json, R"({})"_json));
+
+    EXPECT_GT(0, odataObjectCmp(R"({"@odata.id": "/redfish/v1"})"_json,
+                                R"({"@odata.id": "/redfish/v1/1"})"_json));
+    EXPECT_LT(0, odataObjectCmp(R"({"@odata.id": "/redfish/v1/1"})"_json,
+                                R"({"@odata.id": "/redfish/v1"})"_json));
+
+    EXPECT_LT(0, odataObjectCmp(R"({"@odata.id": "/10"})"_json,
+                                R"({"@odata.id": "/1"})"_json));
+    EXPECT_GT(0, odataObjectCmp(R"({"@odata.id": "/1"})"_json,
+                                R"({"@odata.id": "/10"})"_json));
+
+    EXPECT_GT(0, odataObjectCmp(R"({})"_json, R"({"@odata.id": "/1"})"_json));
+    EXPECT_LT(0, odataObjectCmp(R"({"@odata.id": "/1"})"_json, R"({})"_json));
+
+    EXPECT_GT(0, odataObjectCmp(R"({"@odata.id": 4})"_json,
+                                R"({"@odata.id": "/1"})"_json));
+    EXPECT_LT(0, odataObjectCmp(R"({"@odata.id": "/1"})"_json,
+                                R"({"@odata.id": 4})"_json));
+}
+
+TEST(SortJsonArrayByKey, ElementMissingKeyReturnsFalseArrayIsPartlySorted)
+{
+    nlohmann::json::array_t array =
+        R"([{"@odata.id" : "/redfish/v1/100"}, {"@odata.id": "/redfish/v1/1"}, {"@odata.id" : "/redfish/v1/20"}])"_json;
+    sortJsonArrayByOData(array);
+    // Objects with other keys are always larger than those with the specified
+    // key.
+    EXPECT_THAT(array,
+                ElementsAre(R"({"@odata.id": "/redfish/v1/1"})"_json,
+                            R"({"@odata.id" : "/redfish/v1/20"})"_json,
+                            R"({"@odata.id" : "/redfish/v1/100"})"_json));
+}
+
+TEST(SortJsonArrayByKey, SortedByStringValueOnSuccessArrayIsSorted)
+{
+    nlohmann::json::array_t array =
+        R"([{"@odata.id": "/redfish/v1/20"}, {"@odata.id" : "/redfish/v1"}, {"@odata.id" : "/redfish/v1/100"}])"_json;
+    sortJsonArrayByOData(array);
+    EXPECT_THAT(array,
+                ElementsAre(R"({"@odata.id": "/redfish/v1"})"_json,
+                            R"({"@odata.id" : "/redfish/v1/20"})"_json,
+                            R"({"@odata.id" : "/redfish/v1/100"})"_json));
+}
+
 } // namespace
 } // namespace redfish::json_util