net/addr/subnet: Add ToStr conversion

Change-Id: Id06045fd638fa53eb5725eadd63cace148fc53f3
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/include/stdplus/net/addr/subnet.hpp b/include/stdplus/net/addr/subnet.hpp
index e9a174c..7f898c5 100644
--- a/include/stdplus/net/addr/subnet.hpp
+++ b/include/stdplus/net/addr/subnet.hpp
@@ -1,5 +1,9 @@
+#include <fmt/core.h>
+
 #include <stdplus/net/addr/ip.hpp>
 #include <stdplus/numeric/endian.hpp>
+#include <stdplus/numeric/str.hpp>
+#include <stdplus/str/conv.hpp>
 
 #include <limits>
 #include <type_traits>
@@ -67,9 +71,13 @@
 
 void invalidSubnetPfx(std::size_t pfx);
 
-template <typename Addr, typename Pfx>
+template <typename Addr_, typename Pfx_>
 class Subnet46
 {
+  public:
+    using Addr = Addr_;
+    using Pfx = Pfx_;
+
   private:
     static constexpr inline std::size_t maxPfx = sizeof(Addr) * 8;
     static_assert(std::is_unsigned_v<Pfx> && std::is_integral_v<Pfx>);
@@ -115,24 +123,28 @@
 
 } // namespace detail
 
-using Subnet4 = detail::Subnet46<In4Addr, uint8_t>;
-using Subnet6 = detail::Subnet46<In6Addr, uint8_t>;
+using Subnet4 = detail::Subnet46<In4Addr, std::uint8_t>;
+using Subnet6 = detail::Subnet46<In6Addr, std::uint8_t>;
 
 class SubnetAny
 {
+  public:
+    using Addr = InAnyAddr;
+    using Pfx = std::uint8_t;
+
   private:
-    InAnyAddr addr;
-    uint8_t pfx;
+    Addr addr;
+    Pfx pfx;
 
   public:
-    constexpr SubnetAny(auto addr, uint8_t pfx) : addr(addr), pfx(pfx)
+    constexpr SubnetAny(auto addr, Pfx pfx) : addr(addr), pfx(pfx)
     {
         if (detail::addrBits(addr) < pfx)
         {
             detail::invalidSubnetPfx(pfx);
         }
     }
-    constexpr SubnetAny(InAnyAddr addr, uint8_t pfx) : addr(addr), pfx(pfx)
+    constexpr SubnetAny(InAnyAddr addr, Pfx pfx) : addr(addr), pfx(pfx)
     {
         if (std::visit([](auto v) { return detail::addrBits(v); }, addr) < pfx)
         {
@@ -192,4 +204,46 @@
     }
 };
 
+namespace detail
+{
+
+template <typename Subnet>
+struct SubnetToStr
+{
+    using type = Subnet;
+    using ToDec = IntToStr<10, typename Subnet::Pfx>;
+    // Addr + sep + 3 prefix chars
+    static constexpr std::size_t buf_size =
+        ToStr<typename Subnet::Addr>::buf_size + 1 + ToDec::buf_size;
+
+    template <typename CharT>
+    constexpr CharT* operator()(CharT* buf, Subnet v) const noexcept
+    {
+        buf = ToStr<typename Subnet::Addr>{}(buf, v.getAddr());
+        (buf++)[0] = '/';
+        return ToDec{}(buf, v.getPfx());
+    }
+};
+
+} // namespace detail
+
+template <typename Addr, typename Pfx>
+struct ToStr<detail::Subnet46<Addr, Pfx>> :
+    detail::SubnetToStr<detail::Subnet46<Addr, Pfx>>
+{};
+
+template <>
+struct ToStr<SubnetAny> : detail::SubnetToStr<SubnetAny>
+{};
+
 } // namespace stdplus
+
+template <typename Addr, typename Pfx, typename CharT>
+struct fmt::formatter<stdplus::detail::Subnet46<Addr, Pfx>, CharT> :
+    stdplus::Format<stdplus::ToStr<stdplus::detail::Subnet46<Addr, Pfx>>, CharT>
+{};
+
+template <typename CharT>
+struct fmt::formatter<stdplus::SubnetAny, CharT> :
+    stdplus::Format<stdplus::ToStr<stdplus::SubnetAny>, CharT>
+{};
diff --git a/src/net/addr/subnet.cpp b/src/net/addr/subnet.cpp
index 36d6049..d7cb24f 100644
--- a/src/net/addr/subnet.cpp
+++ b/src/net/addr/subnet.cpp
@@ -15,4 +15,9 @@
 template class Subnet46<In4Addr, uint8_t>;
 template class Subnet46<In6Addr, uint8_t>;
 
+template char* SubnetToStr<Subnet4>::operator()(char*, Subnet4) const noexcept;
+template char* SubnetToStr<Subnet6>::operator()(char*, Subnet6) const noexcept;
+template char* SubnetToStr<SubnetAny>::operator()(char*,
+                                                  SubnetAny) const noexcept;
+
 } // namespace stdplus::detail
diff --git a/test/net/addr/subnet.cpp b/test/net/addr/subnet.cpp
index 561507c..5024928 100644
--- a/test/net/addr/subnet.cpp
+++ b/test/net/addr/subnet.cpp
@@ -51,6 +51,15 @@
     EXPECT_TRUE(Subnet4(addr4Full, 0).contains(In4Addr{}));
 }
 
+TEST(Subnet4, ToStr)
+{
+    ToStrHandle<ToStr<Subnet4>> tsh;
+    EXPECT_EQ("0.0.0.0/16", tsh(Subnet4({}, 16)));
+    EXPECT_EQ("255.0.255.255/28", tsh(Subnet4(In4Addr{255, 0, 255, 255}, 28)));
+    EXPECT_EQ("a 1.2.3.4/32 b",
+              fmt::format("a {} b", Subnet4(In4Addr{1, 2, 3, 4}, 32)));
+}
+
 TEST(Subnet6, Basic)
 {
     EXPECT_NO_THROW(Subnet6(in6_addr{0xff}, 128));
@@ -102,6 +111,15 @@
     EXPECT_TRUE(Subnet6(addr6Full, 0).contains(In6Addr{}));
 }
 
+TEST(Subnet6, ToStr)
+{
+    ToStrHandle<ToStr<Subnet6>> tsh;
+    EXPECT_EQ("::/0", tsh(Subnet6({}, 0)));
+    EXPECT_EQ("ff00::/128", tsh(Subnet6(In6Addr{0xff}, 128)));
+    EXPECT_EQ("a 102:304::/32 b",
+              fmt::format("a {} b", Subnet6(In6Addr{1, 2, 3, 4}, 32)));
+}
+
 TEST(SubnetAny, Basic)
 {
     EXPECT_NO_THROW(SubnetAny(in_addr{0xffffffff}, 32));
@@ -157,4 +175,15 @@
     EXPECT_FALSE(SubnetAny(addr4Full, 32).contains(InAnyAddr{In4Addr{}}));
 }
 
+TEST(SubnetAny, ToStr)
+{
+    ToStrHandle<ToStr<SubnetAny>> tsh;
+    EXPECT_EQ("0.0.0.0/16", tsh(SubnetAny(In4Addr{}, 16)));
+    EXPECT_EQ("ff00::/128", tsh(SubnetAny(In6Addr{0xff}, 128)));
+    EXPECT_EQ("a 102:304::/32 b",
+              fmt::format("a {} b", SubnetAny(In6Addr{1, 2, 3, 4}, 32)));
+    EXPECT_EQ("a 1.2.3.4/32 b",
+              fmt::format("a {} b", SubnetAny(In4Addr{1, 2, 3, 4}, 32)));
+}
+
 } // namespace stdplus