Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

utility: new utility method to convert proto value to string #36334

Merged
merged 8 commits into from
Oct 7, 2024

Conversation

wbpcode
Copy link
Member

@wbpcode wbpcode commented Sep 25, 2024

Commit Message: utility: new utility method to convert proto value to string
Additional Description:

New utility method to convert the proto value to json. This could work even the ENVOY_ENABLE_YAML is not set and is exception free.

Risk Level: low.
Testing: unit test.
Docs Changes: n/a.
Release Notes: n/a.
Platform Specific Features: n/a.

Signed-off-by: wangbaiping <wangbaiping@bytedance.com>
@wbpcode
Copy link
Member Author

wbpcode commented Sep 25, 2024

This method will be used in the access log formatter first.

Comment on lines 916 to 938
switch (value.kind_case()) {
case ProtobufWkt::Value::KIND_NOT_SET:
case ProtobufWkt::Value::kNullValue:
streamer.addNull();
break;
case ProtobufWkt::Value::kNumberValue:
streamer.addNumber(value.number_value());
break;
case ProtobufWkt::Value::kStringValue:
streamer.addString(value.string_value());
break;
case ProtobufWkt::Value::kBoolValue:
streamer.addBool(value.bool_value());
break;
case ProtobufWkt::Value::kStructValue: {
auto map = streamer.makeRootMap();
structValueToJson(value.struct_value(), *map);
break;
}
case ProtobufWkt::Value::kListValue:
auto arr = streamer.makeRootArray();
listValueToJson(value.list_value(), *arr);
break;
Copy link
Member Author

@wbpcode wbpcode Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are some duplicated code in the ValueUtil::toJsonString and valueToJson. If the Streamer could provides addMap() and addArray(), then we can deduplicate these code by template. But it's up to @jmarantz

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following what's duplicated yet; can you describe more If there's a small refactor to the streamer can you propose that as a separate PR just to make it go faster?

Copy link
Member Author

@wbpcode wbpcode Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void valueToJson(const ProtobufWkt::Value& value, JsonStringStreamer::Level& level) {
  switch (value.kind_case()) {
  case ProtobufWkt::Value::KIND_NOT_SET:
  case ProtobufWkt::Value::kNullValue:
    level.addNull();
    break;
  case ProtobufWkt::Value::kNumberValue:
    level.addNumber(value.number_value());
    break;
  case ProtobufWkt::Value::kStringValue:
    level.addString(value.string_value());
    break;
  case ProtobufWkt::Value::kBoolValue:
    level.addBool(value.bool_value());
    break;
  case ProtobufWkt::Value::kStructValue: {
    auto map = level.addMap();
    structValueToJson(value.struct_value(), *map);
    break;
  }
  case ProtobufWkt::Value::kListValue: {
    auto array = level.addArray();
    listValueToJson(value.list_value(), *array);
    break;
  }
  }
}

vs

void ValueUtil::appendJsonToString(const ProtobufWkt::Value& value, std::string& dest) {
  JsonStringStreamer streamer(dest);
  switch (value.kind_case()) {
  case ProtobufWkt::Value::KIND_NOT_SET:
  case ProtobufWkt::Value::kNullValue:
    streamer.addNull();
    break;
  case ProtobufWkt::Value::kNumberValue:
    streamer.addNumber(value.number_value());
    break;
  case ProtobufWkt::Value::kStringValue:
    streamer.addString(value.string_value());
    break;
  case ProtobufWkt::Value::kBoolValue:
    streamer.addBool(value.bool_value());
    break;
  case ProtobufWkt::Value::kStructValue: {
    auto map = streamer.makeRootMap();
    structValueToJson(value.struct_value(), *map);
    break;
  }
  case ProtobufWkt::Value::kListValue:
    auto arr = streamer.makeRootArray();
    listValueToJson(value.list_value(), *arr);
    break;
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmarantz will address other comments first. Also create a minor refactoring PR. But it completely up to you.

Copy link
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm at high level.

break;
case ProtobufWkt::Value::kStructValue: {
auto map = level.addMap();
structValueToJson(value.struct_value(), *map);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you could drop the temp here and just call this with *level.addMap(), so you wouldn't need the new scope. Up to you.

If you decide to keep the temp then please don't use auto for this case per style guide.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*level.addMap() looks good.

break;
}
case ProtobufWkt::Value::kListValue: {
auto array = level.addArray();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

}
// Sort the keys to make the output deterministic.
std::sort(sorted_fields.begin(), sorted_fields.end(),
[](auto a, auto b) { return a.get().first < b.get().first; });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explicit types per style guide

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it.

source/common/protobuf/utility.cc Outdated Show resolved Hide resolved
@@ -852,6 +911,34 @@ ProtobufWkt::Value ValueUtil::listValue(const std::vector<ProtobufWkt::Value>& v
return val;
}

void ValueUtil::toJsonString(const ProtobufWkt::Value& value, std::string& dest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming this toJsonString makes me think it will return a string. Should we call this appendJsonToString?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

Comment on lines 916 to 938
switch (value.kind_case()) {
case ProtobufWkt::Value::KIND_NOT_SET:
case ProtobufWkt::Value::kNullValue:
streamer.addNull();
break;
case ProtobufWkt::Value::kNumberValue:
streamer.addNumber(value.number_value());
break;
case ProtobufWkt::Value::kStringValue:
streamer.addString(value.string_value());
break;
case ProtobufWkt::Value::kBoolValue:
streamer.addBool(value.bool_value());
break;
case ProtobufWkt::Value::kStructValue: {
auto map = streamer.makeRootMap();
structValueToJson(value.struct_value(), *map);
break;
}
case ProtobufWkt::Value::kListValue:
auto arr = streamer.makeRootArray();
listValueToJson(value.list_value(), *arr);
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following what's duplicated yet; can you describe more If there's a small refactor to the streamer can you propose that as a separate PR just to make it go faster?

Signed-off-by: wangbaiping <wangbaiping@bytedance.com>
source/common/protobuf/utility.cc Outdated Show resolved Hide resolved
structValueToJson(value.struct_value(), *streamer.makeRootMap());
break;
case ProtobufWkt::Value::kListValue:
listValueToJson(value.list_value(), *streamer.makeRootArray());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing how renaming makeRootArray to addArray in the streamer here would eliminate duplication; can you explain?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we rename the makeRoot* to add*, then we can update our two function to following to eliminate the duplicated code:

template <class StreamerOrLevel>
void valueToJson(const ProtobufWkt::Value& value, StreamerOrLevel& streamer_or_level) {
  switch (value.kind_case()) {
  case ProtobufWkt::Value::KIND_NOT_SET:
  case ProtobufWkt::Value::kNullValue:
    streamer_or_level.addNull();
    break;
  case ProtobufWkt::Value::kNumberValue:
    streamer_or_level.addNumber(value.number_value());
    break;
  case ProtobufWkt::Value::kStringValue:
    streamer_or_level.addString(value.string_value());
    break;
  case ProtobufWkt::Value::kBoolValue:
    streamer_or_level.addBool(value.bool_value());
    break;
  case ProtobufWkt::Value::kStructValue:
    structValueToJson(value.struct_value(), *streamer_or_level.addMap());
    break;
  case ProtobufWkt::Value::kListValue:
    listValueToJson(value.list_value(), *streamer_or_level.addArray());
    break;
  }
}
void ValueUtil::appendJsonToString(const ProtobufWkt::Value& value, std::string& dest) {
  JsonStringStreamer streamer(dest);
  valueToJson(value, streamer);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can compare this with #36334 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, OK. I think I am OK with the way you have it in this PR with a duplicated switch. I can see the benefit of typing less code, but I feel like it creates an external dependency that Json::StreamerBase and Json::StreamerBase::Level have to have the same API, and if we are going to do that I think they should probably implement the same interface. I think it's probably not worth it to save this duplicated switch.

I am really a big fan of C++in most ways but one that would be nice is a language feature for describing an interface that a class passed into a template must implement.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept? Haha.

repeated.Add()->set_value(20);
EXPECT_THAT(RepeatedPtrUtil::debugString(repeated),
testing::ContainsRegex("\\[.*[\n]*value:\\s*10\n,.*[\n]*value:\\s*20\n\\]"));
testing::ContainsRegex("\\[value: 10\n, value: 20\n\\]"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this test work without this expectation change?

Copy link
Member Author

@wbpcode wbpcode Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at least cannot work at my local env. I can revert it to see whether will our ci say ok.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine, our CI say it ok

ValueUtil::toJsonString(v, json_string);
EXPECT_EQ(json_string, "null");
json_string.clear();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a test-method helper that takes a Value as an arg and returns the json string, with these 4 lines. Then you can structure tests like

  ProtobufWkt::Value v;
  EXPECT_EQ("null", toJson(v));
  v.set_null_value(ProtobufWkt::NULL_VALUE));
  EXPECT_EQ("null", toJson(v));

I think this will make tests more information-dense and easier to read.

@jmarantz
Copy link
Contributor

/wait

Signed-off-by: wangbaiping <wangbaiping@bytedance.com>
Copy link
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushing some more comments.

/wait

@@ -10,6 +10,7 @@
#include "source/common/common/assert.h"
#include "source/common/common/documentation_url.h"
#include "source/common/common/fmt.h"
#include "source/common/json/json_streamer.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move the new utility that depends on JsonStreamer into the json library? I think it's confusing, at the directory level, to have json depend on proto and vice versa. I don't think it's hard to break the cycle by moving the new utility.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

@@ -106,6 +107,60 @@ absl::Status validateDurationAsMillisecondsNoThrow(const ProtobufWkt::Duration&
return validateDurationNoThrow(duration, kMaxInt64Nanoseconds);
}

using JsonStringStreamer = Json::StreamerBase<Json::StringOutput>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to the end of json_streamer.h, next to the nickname for the buffered streamer. You can name it StringStreamer since it will be in the 'Json' namespace.

We should do a follow-up PR to rename Json::Streamer to Json::BufferStreamer for consistency.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

structValueToJson(value.struct_value(), *streamer.makeRootMap());
break;
case ProtobufWkt::Value::kListValue:
listValueToJson(value.list_value(), *streamer.makeRootArray());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, OK. I think I am OK with the way you have it in this PR with a duplicated switch. I can see the benefit of typing less code, but I feel like it creates an external dependency that Json::StreamerBase and Json::StreamerBase::Level have to have the same API, and if we are going to do that I think they should probably implement the same interface. I think it's probably not worth it to save this duplicated switch.

I am really a big fan of C++in most ways but one that would be nice is a language feature for describing an interface that a class passed into a template must implement.

ValueUtil::toJsonString(v, json_string);
EXPECT_EQ(json_string, "true");
json_string.clear();
EXPECT_EQ(toJson(v), "true");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a lot nicer. Do we need to declare a new ProtobufWkt::Value in each section? Or would this have the same effect:

ProtobufWkt::Value v;
EXPECT_EQ(toJson(v), "null");
v.set_bool_value(true);
EXPECT_EQ(toJson(v), "null");
v.set_number_value(1);
EXPECT_EQ(toJson(v), "1");
...

alternatively you could put ProtobufWkt::Value value_; in the test fixture and have every one of these stanzas be a separate 1 or 2-line TEST_F.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

Signed-off-by: wangbaiping <wangbaiping@bytedance.com>
@wbpcode
Copy link
Member Author

wbpcode commented Oct 1, 2024

/retest

Signed-off-by: wangbaiping <wangbaiping@bytedance.com>
Signed-off-by: wangbaiping <wangbaiping@bytedance.com>
Copy link
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, thanks!

@wbpcode
Copy link
Member Author

wbpcode commented Oct 2, 2024

Defer this to @alyssawilk for a senior maintainer review. :)

Copy link
Contributor

@alyssawilk alyssawilk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for the delay, was out sick
LGTM assuming you're going to use this in a follow-up (which from PR description looks like). Can we convert both NO_YAML and YAML builds to this and remove legacy code?

@wbpcode
Copy link
Member Author

wbpcode commented Oct 7, 2024

sorry for the delay, was out sick
LGTM assuming you're going to use this in a follow-up (which from PR description looks like). Can we convert both NO_YAML and YAML builds to this and remove legacy code?

Sadly, only part of positions could be optimized with this PR for now.

@wbpcode wbpcode merged commit 68b4559 into envoyproxy:main Oct 7, 2024
21 checks passed
@wbpcode wbpcode deleted the dev-value-to-string branch October 7, 2024 14:30
Stevenjin8 pushed a commit to Stevenjin8/envoy that referenced this pull request Oct 10, 2024
…oxy#36334)

Commit Message: utility: new utility method to convert proto value to
string
Additional Description:

New utility method to convert the proto value to json. This could work
even the `ENVOY_ENABLE_YAML` is not set and is exception free.

Risk Level: low.
Testing: unit test.
Docs Changes: n/a.
Release Notes: n/a.
Platform Specific Features: n/a.

---------

Signed-off-by: wangbaiping <wangbaiping@bytedance.com>
Signed-off-by: Steven Jin Xuan <sjinxuan@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants