interest: update toUri() for packet format v0.3

Refs: #4709
Change-Id: Id8c6fee0e8d2ad4e9d267854cdf0f41bb46f453a
diff --git a/ndn-cxx/interest.cpp b/ndn-cxx/interest.cpp
index e5d8467..42917e2 100644
--- a/ndn-cxx/interest.cpp
+++ b/ndn-cxx/interest.cpp
@@ -615,17 +615,27 @@
   os << interest.getName();
 
   char delim = '?';
-  if (interest.getMustBeFresh()) {
-    os << delim << "ndn.MustBeFresh=" << interest.getMustBeFresh();
+  auto printOne = [&] (const auto&... args) {
+    os << delim;
     delim = '&';
+    using expand = int[];
+    (void)expand{(os << args, 0)...}; // use a fold expression when we switch to C++17
+  };
+
+  if (interest.getCanBePrefix()) {
+    printOne("CanBePrefix");
   }
-  if (interest.getInterestLifetime() != DEFAULT_INTEREST_LIFETIME) {
-    os << delim << "ndn.InterestLifetime=" << interest.getInterestLifetime().count();
-    delim = '&';
+  if (interest.getMustBeFresh()) {
+    printOne("MustBeFresh");
   }
   if (interest.hasNonce()) {
-    os << delim << "ndn.Nonce=" << interest.getNonce();
-    delim = '&';
+    printOne("Nonce=", interest.getNonce());
+  }
+  if (interest.getInterestLifetime() != DEFAULT_INTEREST_LIFETIME) {
+    printOne("Lifetime=", interest.getInterestLifetime().count());
+  }
+  if (interest.getHopLimit()) {
+    printOne("HopLimit=", static_cast<unsigned>(*interest.getHopLimit()));
   }
 
   return os;
diff --git a/ndn-cxx/interest.hpp b/ndn-cxx/interest.hpp
index 39cfb53..9a612b3 100644
--- a/ndn-cxx/interest.hpp
+++ b/ndn-cxx/interest.hpp
@@ -92,9 +92,10 @@
 
   /** @brief Return a URI-like string that represents the Interest.
    *
-   *  The string starts with `getName().toUri()`.
-   *  If the Interest contains selectors, they are included as a query string.
-   *  Example: "/test/name?ndn.MustBeFresh=1"
+   *  The string always starts with `getName().toUri()`. After the name, if any of the
+   *  Interest's CanBePrefix, MustBeFresh, Nonce, InterestLifetime, or HopLimit fields
+   *  are present, their textual representation is appended as a query string.
+   *  Example: "/test/name?MustBeFresh&Nonce=123456"
    */
   std::string
   toUri() const;
diff --git a/tests/unit/interest.t.cpp b/tests/unit/interest.t.cpp
index ebbff4a..e253ff5 100644
--- a/tests/unit/interest.t.cpp
+++ b/tests/unit/interest.t.cpp
@@ -748,6 +748,39 @@
   BOOST_CHECK_EQUAL(i.isParametersDigestValid(), true);
 }
 
+BOOST_AUTO_TEST_CASE(ToUri)
+{
+  Interest i;
+  i.setCanBePrefix(false);
+  BOOST_CHECK_EQUAL(i.toUri(), "/");
+
+  i.setName("/foo");
+  BOOST_CHECK_EQUAL(i.toUri(), "/foo");
+
+  i.setCanBePrefix(true);
+  BOOST_CHECK_EQUAL(i.toUri(), "/foo?CanBePrefix");
+
+  i.setMustBeFresh(true);
+  BOOST_CHECK_EQUAL(i.toUri(), "/foo?CanBePrefix&MustBeFresh");
+
+  i.setNonce(1234);
+  BOOST_CHECK_EQUAL(i.toUri(), "/foo?CanBePrefix&MustBeFresh&Nonce=1234");
+
+  i.setInterestLifetime(2_s);
+  BOOST_CHECK_EQUAL(i.toUri(), "/foo?CanBePrefix&MustBeFresh&Nonce=1234&Lifetime=2000");
+
+  i.setHopLimit(18);
+  BOOST_CHECK_EQUAL(i.toUri(), "/foo?CanBePrefix&MustBeFresh&Nonce=1234&Lifetime=2000&HopLimit=18");
+
+  i.setCanBePrefix(false);
+  i.setMustBeFresh(false);
+  i.setHopLimit(nullopt);
+  i.setApplicationParameters("2402CAFE"_block);
+  BOOST_CHECK_EQUAL(i.toUri(),
+                    "/foo/params-sha256=8621f5e8321f04104640c8d02877d7c5142cad6e203c5effda1783b1a0e476d6"
+                    "?Nonce=1234&Lifetime=2000");
+}
+
 BOOST_AUTO_TEST_SUITE_END() // TestInterest
 
 } // namespace tests