From 67576a8962c2506b8ee10da48dffba311acb0042 Mon Sep 17 00:00:00 2001
From: bbiff <bbiff@yandex-team.com>
Date: Sun, 11 Sep 2022 00:15:54 +0300
Subject: support duration and timestamp as string in json2proto

---
 library/cpp/protobuf/json/config.h               |  8 +++
 library/cpp/protobuf/json/json2proto.cpp         | 71 ++++++++++++++++++++++++
 library/cpp/protobuf/json/json2proto.h           |  8 +++
 library/cpp/protobuf/json/proto2json_printer.cpp | 23 ++++++++
 library/cpp/protobuf/json/ut/json2proto_ut.cpp   | 26 +++++++++
 library/cpp/protobuf/json/ut/proto2json_ut.cpp   | 18 ++++++
 library/cpp/protobuf/json/ut/test.proto          | 11 ++++
 7 files changed, 165 insertions(+)

(limited to 'library/cpp')

diff --git a/library/cpp/protobuf/json/config.h b/library/cpp/protobuf/json/config.h
index dc84fb4d5d..8b9d60ab4f 100644
--- a/library/cpp/protobuf/json/config.h
+++ b/library/cpp/protobuf/json/config.h
@@ -65,6 +65,9 @@ namespace NProtobufJson {
         /// with FieldNameMode.
         bool UseJsonName = false;
 
+        // Allow nonstandard conversions, e.g. from google.protobuf.Duration to string
+        bool ConvertTimeAsString = false;
+
         /// Transforms will be applied only to string values (== protobuf fields of string / bytes type).
         /// yajl_encode_string will be used if no transforms are specified.
         TVector<TStringTransformPtr> StringTransforms;
@@ -125,6 +128,11 @@ namespace NProtobufJson {
             return *this;
         }
 
+        TSelf& SetConvertTimeAsString(bool value) {
+            ConvertTimeAsString = value;
+            return *this;
+        }
+
         TSelf& SetExtensionFieldNameMode(ExtFldNameMode mode) {
             ExtensionFieldNameMode = mode;
             return *this;
diff --git a/library/cpp/protobuf/json/json2proto.cpp b/library/cpp/protobuf/json/json2proto.cpp
index 92bfe9755b..903ffa1a85 100644
--- a/library/cpp/protobuf/json/json2proto.cpp
+++ b/library/cpp/protobuf/json/json2proto.cpp
@@ -3,6 +3,7 @@
 
 #include <library/cpp/json/json_value.h>
 
+#include <google/protobuf/util/time_util.h>
 #include <google/protobuf/message.h>
 #include <google/protobuf/descriptor.h>
 
@@ -77,6 +78,52 @@ static TString GetFieldName(const google::protobuf::FieldDescriptor& field,
     return name;
 }
 
+
+static void JsonString2Duration(const NJson::TJsonValue& json,
+                 google::protobuf::Message& proto,
+                 const google::protobuf::FieldDescriptor& field,
+                 const NProtobufJson::TJson2ProtoConfig& config) {
+    using namespace google::protobuf;
+    if (!json.GetString() && !config.CastRobust) {
+        ythrow yexception() << "Invalid type of JSON field '" << field.name() << "': "
+                            << "IsString() failed while "
+                            << "CPPTYPE_STRING is expected.";
+    }
+    TString jsonString = json.GetStringRobust();
+
+    Duration durationFromString;
+
+    if (!util::TimeUtil::FromString(jsonString, &durationFromString)) {
+        ythrow yexception() << "error while parsing google.protobuf.Duration from string on field '" <<
+                                                                                    field.name() << "'";
+    }
+
+    proto.CopyFrom(durationFromString);
+
+}
+
+static void JsonString2Timestamp(const NJson::TJsonValue& json,
+                 google::protobuf::Message& proto,
+                 const google::protobuf::FieldDescriptor& field,
+                 const NProtobufJson::TJson2ProtoConfig& config) {
+    using namespace google::protobuf;
+    if (!json.GetString() && !config.CastRobust) {
+        ythrow yexception() << "Invalid type of JSON field '" << field.name() << "': "
+                            << "IsString() failed while "
+                            << "CPPTYPE_STRING is expected.";
+    }
+    TString jsonString = json.GetStringRobust();
+
+    Timestamp timestampFromString;
+
+    if (!util::TimeUtil::FromString(jsonString, &timestampFromString)) {
+        ythrow yexception() << "error while parsing google.protobuf.Timestamp from string on field '" <<
+                                                                                    field.name() << "'";
+    }
+
+    proto.CopyFrom(timestampFromString);
+}
+
 static void
 JsonString2Field(const NJson::TJsonValue& json,
                  google::protobuf::Message& proto,
@@ -110,6 +157,24 @@ JsonString2Field(const NJson::TJsonValue& json,
         reflection->SetString(&proto, &field, value);
 }
 
+static bool
+HandleString2TimeConversion(const NJson::TJsonValue& json,
+                 google::protobuf::Message& proto,
+                 const google::protobuf::FieldDescriptor& field,
+                 const NProtobufJson::TJson2ProtoConfig& config) {
+    using namespace google::protobuf;
+    auto type = proto.GetDescriptor()->well_known_type();
+
+    if (type == Descriptor::WellKnownType::WELLKNOWNTYPE_DURATION) {
+        JsonString2Duration(json, proto, field, config);
+        return true;
+    } else if (type == Descriptor::WellKnownType::WELLKNOWNTYPE_TIMESTAMP) {
+        JsonString2Timestamp(json, proto, field, config);
+        return true;
+    }
+    return false;
+}
+
 static const NProtoBuf::EnumValueDescriptor*
 FindEnumValue(const NProtoBuf::EnumDescriptor* enumField,
               TStringBuf target, bool (*equals)(TStringBuf, TStringBuf)) {
@@ -215,6 +280,12 @@ Json2SingleField(const NJson::TJsonValue& json,
         case FieldDescriptor::CPPTYPE_MESSAGE: {
             Message* innerProto = reflection->MutableMessage(&proto, &field);
             Y_ASSERT(!!innerProto);
+
+            if (config.AllowString2TimeConversion &&
+                                        HandleString2TimeConversion(fieldJson, *innerProto, field, config)) {
+                break;
+            }
+
             NProtobufJson::MergeJson2Proto(fieldJson, *innerProto, config);
 
             break;
diff --git a/library/cpp/protobuf/json/json2proto.h b/library/cpp/protobuf/json/json2proto.h
index 4c33498dfa..ed89c6cf63 100644
--- a/library/cpp/protobuf/json/json2proto.h
+++ b/library/cpp/protobuf/json/json2proto.h
@@ -103,6 +103,11 @@ namespace NProtobufJson {
             return *this;
         }
 
+        TSelf& SetAllowString2TimeConversion(bool value) {
+            AllowString2TimeConversion = value;
+            return *this;
+        }
+
         FldNameMode FieldNameMode = FieldNameOriginalCase;
         bool AllowUnknownFields = true;
 
@@ -144,6 +149,9 @@ namespace NProtobufJson {
 
         /// Allow js-style comments (both // and /**/)
         bool AllowComments = false;
+
+        /// Allow nonstandard conversions, e.g. google.protobuf.Duration from String
+        bool AllowString2TimeConversion = false;
     };
 
     /// @throw yexception
diff --git a/library/cpp/protobuf/json/proto2json_printer.cpp b/library/cpp/protobuf/json/proto2json_printer.cpp
index 6123eab0f2..409147dc06 100644
--- a/library/cpp/protobuf/json/proto2json_printer.cpp
+++ b/library/cpp/protobuf/json/proto2json_printer.cpp
@@ -2,9 +2,13 @@
 #include "config.h"
 #include "util.h"
 
+#include <google/protobuf/util/time_util.h>
+
 #include <util/generic/yexception.h>
 #include <util/string/ascii.h>
 #include <util/string/cast.h>
+#include <util/system/win_undef.h>
+
 
 namespace NProtobufJson {
     using namespace NProtoBuf;
@@ -176,6 +180,22 @@ namespace NProtobufJson {
         }
     }
 
+    bool HandleTimeConversion(const Message& proto, IJsonOutput& json) {
+        using namespace google::protobuf;
+        auto type = proto.GetDescriptor()->well_known_type();
+
+        if (type == Descriptor::WellKnownType::WELLKNOWNTYPE_DURATION) {
+            const auto& duration = static_cast<const Duration&>(proto);
+            json.Write(util::TimeUtil::ToString(duration));
+            return true;
+        } else if (type == Descriptor::WellKnownType::WELLKNOWNTYPE_TIMESTAMP) {
+            const auto& timestamp = static_cast<const Timestamp&>(proto);
+            json.Write(util::TimeUtil::ToString(timestamp));
+            return true;
+        }
+        return false;
+    }
+
     void TProto2JsonPrinter::PrintSingleField(const Message& proto,
                                               const FieldDescriptor& field,
                                               IJsonOutput& json,
@@ -228,6 +248,9 @@ namespace NProtobufJson {
 
                 case FieldDescriptor::CPPTYPE_MESSAGE: {
                     json.WriteKey(key);
+                    if (Config.ConvertTimeAsString && HandleTimeConversion(reflection->GetMessage(proto, &field), json)) {
+                        break;
+                    }
                     Print(reflection->GetMessage(proto, &field), json);
                     break;
                 }
diff --git a/library/cpp/protobuf/json/ut/json2proto_ut.cpp b/library/cpp/protobuf/json/ut/json2proto_ut.cpp
index 0dfe57bc7a..d213c9e913 100644
--- a/library/cpp/protobuf/json/ut/json2proto_ut.cpp
+++ b/library/cpp/protobuf/json/ut/json2proto_ut.cpp
@@ -8,6 +8,7 @@
 #include <library/cpp/json/json_reader.h>
 #include <library/cpp/json/json_writer.h>
 
+#include <library/cpp/protobuf/interop/cast.h>
 #include <library/cpp/protobuf/json/json2proto.h>
 
 #include <library/cpp/testing/unittest/registar.h>
@@ -1144,4 +1145,29 @@ Y_UNIT_TEST(TestAllowComments) {
     UNIT_ASSERT_VALUES_EQUAL(proto.GetI64(), 3423);
 } // TestAllowComments
 
+Y_UNIT_TEST(TestSimplifiedDuration) {
+    NJson::TJsonValue json;
+    TSingleDuration simpleDuration;
+    json["Duration"] = "10.1s";
+    NProtobufJson::Json2Proto(json, simpleDuration, NProtobufJson::TJson2ProtoConfig().SetAllowString2TimeConversion(true));
+    UNIT_ASSERT_EQUAL(NProtoInterop::CastFromProto(simpleDuration.GetDuration()), TDuration::MilliSeconds(10100));
+} // TestSimplifiedDuration
+
+Y_UNIT_TEST(TestUnwrappedDuration) {
+    NJson::TJsonValue json;
+    TSingleDuration duration;
+    json["Duration"]["seconds"] = 2;
+    NProtobufJson::Json2Proto(json, duration, NProtobufJson::TJson2ProtoConfig());
+    UNIT_ASSERT_EQUAL(NProtoInterop::CastFromProto(duration.GetDuration()), TDuration::MilliSeconds(2000));
+} // TestUnwrappedDuration
+
+Y_UNIT_TEST(TestSimplifiedTimestamp) {
+    NJson::TJsonValue json;
+    TSingleTimestamp simpleTimestamp;
+    json["Timestamp"] = "2014-08-26T15:52:15Z";
+    NProtobufJson::Json2Proto(json, simpleTimestamp, NProtobufJson::TJson2ProtoConfig().SetAllowString2TimeConversion(true));
+    UNIT_ASSERT_EQUAL(NProtoInterop::CastFromProto(simpleTimestamp.GetTimestamp()), TInstant::ParseIso8601("2014-08-26T15:52:15Z"));
+
+} // TestSimplifiedTimestamp
+
 } // TJson2ProtoTest
diff --git a/library/cpp/protobuf/json/ut/proto2json_ut.cpp b/library/cpp/protobuf/json/ut/proto2json_ut.cpp
index 07e52d7f2f..d4d6d374e9 100644
--- a/library/cpp/protobuf/json/ut/proto2json_ut.cpp
+++ b/library/cpp/protobuf/json/ut/proto2json_ut.cpp
@@ -1019,4 +1019,22 @@ Y_UNIT_TEST(TestExtension) {
     UNIT_ASSERT_EQUAL(Proto2Json(proto, cfg), "{\"bar\":1}");
 } // TestExtension
 
+Y_UNIT_TEST(TestSimplifiedDuration) {
+    TString json;
+    TSingleDuration simpleDuration;
+    simpleDuration.mutable_duration()->set_seconds(10);
+    simpleDuration.mutable_duration()->set_nanos(101);
+    Proto2Json(simpleDuration, json, TProto2JsonConfig().SetConvertTimeAsString(true));
+    UNIT_ASSERT_EQUAL_C(json, "{\"Duration\":\"10.000000101s\"}", "real value is " << json);
+} // TestSimplifiedDuration
+
+Y_UNIT_TEST(TestSimplifiedTimestamp) {
+    TString json;
+    TSingleTimestamp simpleTimestamp;
+    simpleTimestamp.mutable_timestamp()->set_seconds(10000000);
+    simpleTimestamp.mutable_timestamp()->set_nanos(504);
+    Proto2Json(simpleTimestamp, json, TProto2JsonConfig().SetConvertTimeAsString(true));
+    UNIT_ASSERT_EQUAL_C(json, "{\"Timestamp\":\"1970-04-26T17:46:40.000000504Z\"}", "real value is " << json);
+} // TestSimplifiedTimestamp
+
 } // TProto2JsonTest
diff --git a/library/cpp/protobuf/json/ut/test.proto b/library/cpp/protobuf/json/ut/test.proto
index 0fa996fd41..6bc1984373 100644
--- a/library/cpp/protobuf/json/ut/test.proto
+++ b/library/cpp/protobuf/json/ut/test.proto
@@ -1,5 +1,8 @@
 package NProtobufJsonTest;
 
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+
 enum EEnum {
     E_0 = 0;
     E_1 = 1;
@@ -194,6 +197,14 @@ message TSingleRepeatedInt {
     repeated int32 RepeatedInt = 1;
 }
 
+message TSingleDuration {
+    required google.protobuf.Duration Duration = 1;
+}
+
+message TSingleTimestamp {
+    required google.protobuf.Timestamp Timestamp = 1;
+}
+
 message TExtensionField {
 	extensions 100 to 199;
 }
-- 
cgit v1.2.3