From c682603417e1ef8290aacf274ff49821bd204c0b Mon Sep 17 00:00:00 2001
From: Nat Goodspeed <nat@lindenlab.com>
Date: Mon, 19 Dec 2022 16:29:06 -0500
Subject: DRTVWR-558: Extend LL::apply() to LLSD array arguments.

Make apply(function, std::array) and apply(function, std::vector) available
even when we borrow the C++17 implementation of apply(function, std::tuple).

Add apply(function, LLSD) with interpretations:

* isUndefined() is treated as an empty array, for calling a nullary function
* scalar LLSD is treated as a single-entry array, for calling a unary function
* isArray() converts function parameters using LLSDParam
* isMap() is an error.

Add unit tests for all flavors of LL::apply().

(cherry picked from commit 3006c24251c6259d00df9e0f4f66b8a617e6026d)
---
 indra/llcommon/CMakeLists.txt       |   2 +
 indra/llcommon/apply.h              |  23 +++--
 indra/llcommon/llsdutil.h           |  81 ++++++++++++++++-
 indra/llcommon/tests/apply_test.cpp | 171 ++++++++++++++++++++++++++++++++++++
 4 files changed, 269 insertions(+), 8 deletions(-)
 create mode 100644 indra/llcommon/tests/apply_test.cpp

(limited to 'indra/llcommon')

diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index 91ae2440fa..941d2d7baf 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -293,6 +293,8 @@ if (LL_TESTS)
 
   #set(TEST_DEBUG on)
   set(test_libs llcommon)
+  LL_ADD_INTEGRATION_TEST(apply "" "${test_libs}")
+  LL_ADD_INTEGRATION_TEST(bitpack "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(classic_callback "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(commonmisc "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(lazyeventapi "" "${test_libs}")
diff --git a/indra/llcommon/apply.h b/indra/llcommon/apply.h
index 7c58d63bc0..26753f5017 100644
--- a/indra/llcommon/apply.h
+++ b/indra/llcommon/apply.h
@@ -13,6 +13,7 @@
 #define LL_APPLY_H
 
 #include <boost/type_traits/function_traits.hpp>
+#include <cassert>
 #include <tuple>
 
 namespace LL
@@ -54,6 +55,9 @@ namespace LL
         },                                                          \
         (ARGS))
 
+/*****************************************************************************
+*   apply(function, tuple)
+*****************************************************************************/
 #if __cplusplus >= 201703L
 
 // C++17 implementation
@@ -63,8 +67,8 @@ using std::apply;
 
 // Derived from https://stackoverflow.com/a/20441189
 // and https://en.cppreference.com/w/cpp/utility/apply
-template <typename CALLABLE, typename TUPLE, std::size_t... I>
-auto apply_impl(CALLABLE&& func, TUPLE&& args, std::index_sequence<I...>)
+template <typename CALLABLE, typename... ARGS, std::size_t... I>
+auto apply_impl(CALLABLE&& func, const std::tuple<ARGS...>& args, std::index_sequence<I...>)
 {
     // call func(unpacked args)
     return std::forward<CALLABLE>(func)(std::move(std::get<I>(args))...);
@@ -81,6 +85,11 @@ auto apply(CALLABLE&& func, const std::tuple<ARGS...>& args)
                       std::index_sequence_for<ARGS...>{});
 }
 
+#endif // C++14
+
+/*****************************************************************************
+*   apply(function, std::array)
+*****************************************************************************/
 // per https://stackoverflow.com/a/57510428/5533635
 template <typename CALLABLE, typename T, size_t SIZE>
 auto apply(CALLABLE&& func, const std::array<T, SIZE>& args)
@@ -88,13 +97,15 @@ auto apply(CALLABLE&& func, const std::array<T, SIZE>& args)
     return apply(std::forward<CALLABLE>(func), std::tuple_cat(args));
 }
 
+/*****************************************************************************
+*   apply(function, std::vector)
+*****************************************************************************/
 // per https://stackoverflow.com/a/28411055/5533635
 template <typename CALLABLE, typename T, std::size_t... I>
 auto apply_impl(CALLABLE&& func, const std::vector<T>& args, std::index_sequence<I...>)
 {
-    return apply_impl(std::forward<CALLABLE>(func),
-                      std::make_tuple(std::forward<T>(args[I])...),
-                      I...);
+    return apply(std::forward<CALLABLE>(func),
+                 std::make_tuple(args[I]...));
 }
 
 // this goes beyond C++17 std::apply()
@@ -108,8 +119,6 @@ auto apply(CALLABLE&& func, const std::vector<T>& args)
                       std::make_index_sequence<arity>());
 }
 
-#endif // C++14
-
 } // namespace LL
 
 #endif /* ! defined(LL_APPLY_H) */
diff --git a/indra/llcommon/llsdutil.h b/indra/llcommon/llsdutil.h
index 372278c51a..eaf8825791 100644
--- a/indra/llcommon/llsdutil.h
+++ b/indra/llcommon/llsdutil.h
@@ -31,6 +31,7 @@
 
 #include "llsd.h"
 #include <boost/functional/hash.hpp>
+#include <cassert>
 
 // U32
 LL_COMMON_API LLSD ll_sd_from_U32(const U32);
@@ -332,6 +333,31 @@ private:
     T _value;
 };
 
+/**
+ * LLSDParam<LLSD> is for when you don't already have the target parameter
+ * type in hand. Instantiate LLSDParam<LLSD>(your LLSD object), and the
+ * templated conversion operator will try to select a more specific LLSDParam
+ * specialization.
+ */
+template <>
+class LLSDParam<LLSD>
+{
+private:
+    LLSD value_;
+
+public:
+    LLSDParam(const LLSD& value): value_(value) {}
+
+    /// if we're literally being asked for an LLSD parameter, avoid infinite
+    /// recursion
+    operator LLSD() const { return value_; }
+
+    /// otherwise, instantiate a more specific LLSDParam<T> to convert; that
+    /// preserves the existing customization mechanism
+    template <typename T>
+    operator T() const { return LLSDParam<T>(value_); }
+};
+
 /**
  * Turns out that several target types could accept an LLSD param using any of
  * a few different conversions, e.g. LLUUID's constructor can accept LLUUID or
@@ -350,7 +376,7 @@ class LLSDParam<T>                              \
 {                                               \
 public:                                         \
     LLSDParam(const LLSD& value):               \
-        _value((T)value.AS())                      \
+        _value((T)value.AS())                   \
     {}                                          \
                                                 \
     operator T() const { return _value; }       \
@@ -555,4 +581,57 @@ struct hash<LLSD>
     }
 };
 }
+
+namespace LL
+{
+
+/*****************************************************************************
+*   apply(function, LLSD array)
+*****************************************************************************/
+// Derived from https://stackoverflow.com/a/20441189
+// and https://en.cppreference.com/w/cpp/utility/apply .
+// We can't simply make a tuple from the LLSD array and then apply() that
+// tuple to the function -- how would make_tuple() deduce the correct
+// parameter type for each entry? We must go directly to the target function.
+template <typename CALLABLE, std::size_t... I>
+auto apply_impl(CALLABLE&& func, const LLSD& array, std::index_sequence<I...>)
+{
+    // call func(unpacked args), using generic LLSDParam<LLSD> to convert each
+    // entry in 'array' to the target parameter type
+    return std::forward<CALLABLE>(func)(LLSDParam<LLSD>(array[I])...);
+}
+
+template <typename CALLABLE>
+auto apply(CALLABLE&& func, const LLSD& args)
+{
+    LLSD array;
+    constexpr auto arity = boost::function_traits<CALLABLE>::arity;
+    // LLSD supports a number of types, two of which are aggregates: Map and
+    // Array. We don't try to support Map: supporting Map would seem to
+    // promise that we could somehow match the string key to 'func's parameter
+    // names. Uh sorry, maybe in some future version of C++ with reflection.
+    assert(! args.isMap());
+    // We expect an LLSD array, but what the heck, treat isUndefined() as a
+    // zero-length array for calling a nullary 'func'.
+    if (args.isUndefined() || args.isArray())
+    {
+        // this works because LLSD().size() == 0
+        assert(args.size() == arity);
+        array = args;
+    }
+    else                            // args is one of the scalar types
+    {
+        // scalar_LLSD.size() == 0, so don't test that here.
+        // You can pass a scalar LLSD only to a unary 'func'.
+        assert(arity == 1);
+        // make an array of it
+        array = llsd::array(args);
+    }
+    return apply_impl(std::forward<CALLABLE>(func),
+                      array,
+                      std::make_index_sequence<arity>());
+}
+
+} // namespace LL
+
 #endif // LL_LLSDUTIL_H
diff --git a/indra/llcommon/tests/apply_test.cpp b/indra/llcommon/tests/apply_test.cpp
new file mode 100644
index 0000000000..1f1085a702
--- /dev/null
+++ b/indra/llcommon/tests/apply_test.cpp
@@ -0,0 +1,171 @@
+/**
+ * @file   apply_test.cpp
+ * @author Nat Goodspeed
+ * @date   2022-12-19
+ * @brief  Test for apply.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "apply.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "../test/lltut.h"
+#include "llsd.h"
+#include "llsdutil.h"
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    namespace statics
+    {
+        /*------------------------------ data ------------------------------*/
+        // Although we're using types from the LLSD namespace, we're not
+        // constructing LLSD values, but rather instances of the C++ types
+        // supported by LLSD.
+        static LLSD::Boolean b{true};
+        static LLSD::Integer i{17};
+        static LLSD::Real    f{3.14};
+        static LLSD::String  s{ "hello" };
+        static LLSD::UUID    uu{ "baadf00d-dead-beef-baad-feedb0ef" };
+        static LLSD::Date    dt{ "2022-12-19" };
+        static LLSD::URI     uri{ "http://secondlife.com" };
+        static LLSD::Binary  bin{ 0x01, 0x02, 0x03, 0x04, 0x05 };
+
+        static std::vector<LLSD::String> quick
+        {
+            "The", "quick", "brown", "fox", "etc."
+        };
+
+        static std::array<int, 5> fibs
+        {
+            0, 1, 1, 2, 3
+        };
+
+        // ensure that apply() actually reaches the target method --
+        // lack of ensure_equals() failure could be due to no-op apply()
+        bool called{ false };
+
+        /*------------------------- test functions -------------------------*/
+        void various(LLSD::Boolean b, LLSD::Integer i, LLSD::Real f, const LLSD::String& s,
+                     const LLSD::UUID& uu, const LLSD::Date& dt,
+                     const LLSD::URI& uri, const LLSD::Binary& bin)
+        {
+            called = true;
+            ensure_equals(  "b mismatch", b,   statics::b);
+            ensure_equals(  "i mismatch", i,   statics::i);
+            ensure_equals(  "f mismatch", f,   statics::f);
+            ensure_equals(  "s mismatch", s,   statics::s);
+            ensure_equals( "uu mismatch", uu,  statics::uu);
+            ensure_equals( "dt mismatch", dt,  statics::dt);
+            ensure_equals("uri mismatch", uri, statics::uri);
+            ensure_equals("bin mismatch", bin, statics::bin);
+        }
+
+        void strings(std::string s0, std::string s1, std::string s2, std::string s3, std::string s4)
+        {
+            called = true;
+            ensure_equals("s0 mismatch", s0, statics::quick[0]);
+            ensure_equals("s1 mismatch", s1, statics::quick[1]);
+            ensure_equals("s2 mismatch", s2, statics::quick[2]);
+            ensure_equals("s3 mismatch", s3, statics::quick[3]);
+            ensure_equals("s4 mismatch", s4, statics::quick[4]);
+        }
+
+        void ints(int i0, int i1, int i2, int i3, int i4)
+        {
+            called = true;
+            ensure_equals("i0 mismatch", i0, statics::fibs[0]);
+            ensure_equals("i1 mismatch", i1, statics::fibs[1]);
+            ensure_equals("i2 mismatch", i2, statics::fibs[2]);
+            ensure_equals("i3 mismatch", i3, statics::fibs[3]);
+            ensure_equals("i4 mismatch", i4, statics::fibs[4]);
+        }
+
+        void intfunc(int i)
+        {
+            called = true;
+            ensure_equals("i mismatch", i, statics::i);
+        }
+
+        void voidfunc()
+        {
+            called = true;
+        }
+    } // namespace statics
+
+    struct apply_data
+    {
+        apply_data()
+        {
+            // reset called before each test
+            statics::called = false;
+        }
+    };
+    typedef test_group<apply_data> apply_group;
+    typedef apply_group::object object;
+    apply_group applygrp("apply");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("apply(tuple)");
+        LL::apply(statics::various,
+                  std::make_tuple(statics::b, statics::i, statics::f, statics::s,
+                                  statics::uu, statics::dt, statics::uri, statics::bin));
+        ensure("apply(tuple) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("apply(array)");
+        LL::apply(statics::ints, statics::fibs);
+        ensure("apply(array) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("apply(vector)");
+        LL::apply(statics::strings, statics::quick);
+        ensure("apply(vector) failed", statics::called);
+    }
+
+    // The various apply(LLSD) tests exercise only the success cases because
+    // the failure cases trigger assert() fail, which is hard to catch.
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("apply(LLSD())");
+        LL::apply(statics::voidfunc, LLSD());
+        ensure("apply(LLSD()) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("apply(LLSD scalar)");
+        LL::apply(statics::intfunc, LLSD(statics::i));
+        ensure("apply(LLSD scalar) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<6>()
+    {
+        set_test_name("apply(LLSD array)");
+        LL::apply(statics::various,
+                  llsd::array(statics::b, statics::i, statics::f, statics::s,
+                              statics::uu, statics::dt, statics::uri, statics::bin));
+        ensure("apply(LLSD array) failed", statics::called);
+    }
+} // namespace tut
-- 
cgit v1.2.3