Skip to content

Commit

Permalink
[yaml] Add json output
Browse files Browse the repository at this point in the history
  • Loading branch information
jwnimmer-tri committed Mar 5, 2023
1 parent 74146be commit 7b7ace4
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 27 deletions.
9 changes: 9 additions & 0 deletions common/yaml/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ drake_cc_library(
],
)

drake_cc_googletest(
name = "json_test",
deps = [
":example_structs",
":yaml_io",
"//common/test_utilities:expect_throws_message",
],
)

drake_cc_googletest(
name = "yaml_doxygen_test",
deps = [
Expand Down
106 changes: 106 additions & 0 deletions common/yaml/test/json_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include <limits>

#include <gtest/gtest.h>

#include "drake/common/test_utilities/expect_throws_message.h"
#include "drake/common/yaml/test/example_structs.h"
#include "drake/common/yaml/yaml_io.h"

namespace drake {
namespace yaml {
namespace test {
namespace {

GTEST_TEST(YamlJsonTest, WriteScalars) {
AllScalarsStruct data;
constexpr char expected[] =
R"""({"some_bool":false,"some_double":1.2345,"some_float":1.2345,"some_int32":12,"some_int64":14,"some_string":"kNominalString","some_uint32":12,"some_uint64":15})""";
EXPECT_EQ(SaveJsonString(data), expected);
}

GTEST_TEST(YamlJsonTest, StringEscaping) {
StringStruct data;
data.value = "foo\n\"bar\"\t";
EXPECT_EQ(SaveJsonString(data),
R"""({"value":"foo\u000a\u0022bar\u0022\u0009"})""");
}

GTEST_TEST(YamlJsonTest, NonFinite) {
DoubleStruct data;

data.value = std::numeric_limits<double>::infinity();
EXPECT_EQ(SaveJsonString(data), R"""({"value":Infinity})""");

data.value = -data.value;
EXPECT_EQ(SaveJsonString(data), R"""({"value":-Infinity})""");

data.value = std::numeric_limits<double>::quiet_NaN();
EXPECT_EQ(SaveJsonString(data), R"""({"value":NaN})""");
}

GTEST_TEST(YamlJsonTest, WriteSequence) {
VectorStruct data;
data.value = {1.0, 2.0};
EXPECT_EQ(SaveJsonString(data), R"""({"value":[1.0,2.0]})""");
}

GTEST_TEST(YamlJsonTest, WriteMapping) {
MapStruct data;
data.value.clear();
data.value["a"] = 1.0;
data.value["b"] = 2.0;
EXPECT_EQ(SaveJsonString(data), R"""({"value":{"a":1.0,"b":2.0}})""");
}

GTEST_TEST(YamlJsonTest, WriteNested) {
OuterStruct data;
EXPECT_EQ(
SaveJsonString(data),
R"""({"inner_struct":{"inner_value":1.2345},"outer_value":1.2345})""");
}

GTEST_TEST(YamlJsonTest, WriteOptional) {
OptionalStruct data;
EXPECT_EQ(SaveJsonString(data), R"""({"value":1.2345})""");

data.value.reset();
EXPECT_EQ(SaveJsonString(data), "{}");
}

GTEST_TEST(YamlJsonTest, WriteVariant) {
VariantStruct data;

data.value = std::string{"foo"};
EXPECT_EQ(SaveJsonString(data), R"""({"value":"foo"})""");

// It would be plausible here to use the `_tag` convention to annotate
// variant tags, matching what we do in our yaml.py conventions.
data.value = DoubleStruct{};
DRAKE_EXPECT_THROWS_MESSAGE(SaveJsonString(data),
".*SaveJsonString.*mapping.*tag.*");
}

struct IntVariant {
template <typename Archive>
void Serialize(Archive* a) {
a->Visit(DRAKE_NVP(value));
}

std::variant<int, uint64_t> value{0};
};

GTEST_TEST(YamlJsonTest, WriteVariantScalar) {
IntVariant data;

data.value.emplace<int>(1);
EXPECT_EQ(SaveJsonString(data), R"""({"value":1})""");

data.value.emplace<uint64_t>(22);
DRAKE_EXPECT_THROWS_MESSAGE(SaveJsonString(data),
".*SaveJsonString.*scalar.*22.*tag.*");
}

} // namespace
} // namespace test
} // namespace yaml
} // namespace drake
27 changes: 24 additions & 3 deletions common/yaml/test/yaml_node_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ namespace {

// Check that the tag constants exist.
GTEST_TEST(YamlNodeTest, TagConstants) {
EXPECT_GT(Node::kTagFloat.size(), 0);
EXPECT_GT(Node::kTagInt.size(), 0);
EXPECT_GT(Node::kTagNull.size(), 0);
EXPECT_GT(Node::kTagBool.size(), 0);
EXPECT_GT(Node::kTagInt.size(), 0);
EXPECT_GT(Node::kTagFloat.size(), 0);
EXPECT_GT(Node::kTagStr.size(), 0);
}

Expand Down Expand Up @@ -103,14 +104,27 @@ TEST_P(YamlNodeParamaterizedTest, StaticTypeString) {
EXPECT_EQ(Node::GetTypeString(GetExpectedType()), GetExpectedTypeString());
}

// Check tag getting and setting.
// Check generic tag getting and setting.
TEST_P(YamlNodeParamaterizedTest, GetSetTag) {
Node dut = MakeEmptyDut();
EXPECT_EQ(dut.GetTag(), "");
dut.SetTag("tag");
EXPECT_EQ(dut.GetTag(), "tag");
}

// Check JSON Schema tag getting and setting.
TEST_P(YamlNodeParamaterizedTest, JsonSchemaTag) {
Node dut = MakeEmptyDut();
dut.SetTag(JsonSchemaTag::kNull);
EXPECT_EQ(dut.GetTag(), Node::kTagNull);
dut.SetTag(JsonSchemaTag::kBool);
EXPECT_EQ(dut.GetTag(), Node::kTagBool);
dut.SetTag(JsonSchemaTag::kInt);
EXPECT_EQ(dut.GetTag(), Node::kTagInt);
dut.SetTag(JsonSchemaTag::kFloat);
EXPECT_EQ(dut.GetTag(), Node::kTagFloat);
}

// It is important for our YAML subsystem performance that the Node's move
// operations actually move the stored data, instead of copying it.
TEST_P(YamlNodeParamaterizedTest, EfficientMoveConstructor) {
Expand Down Expand Up @@ -156,8 +170,15 @@ TEST_P(YamlNodeParamaterizedTest, EqualityPerTag) {
Node dut = MakeEmptyDut();
Node dut2 = MakeEmptyDut();
EXPECT_TRUE(dut == dut2);

// Different tag; not equal.
dut2.SetTag("tag");
EXPECT_FALSE(dut == dut2);

// Same tag, set via two different overloads; still equal.
dut.SetTag(JsonSchemaTag::kInt);
dut2.SetTag(std::string{Node::kTagInt});
EXPECT_TRUE(dut == dut2);
}

// Check Scalar-specific operations.
Expand Down
24 changes: 24 additions & 0 deletions common/yaml/yaml_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ std::string SaveYamlString(
const std::optional<std::string>& child_name = std::nullopt,
const std::optional<Serializable>& defaults = std::nullopt);

/** Saves data as a JSON-formatted string.
Refer to @ref yaml_serialization "YAML Serialization" for background.
Note that there is no LoadJsonString() function, because LoadYamlString() can
already load JSON data.
@param data User data to be serialized.
@returns the JSON data as a string.
@tparam Serializable must implement a @ref implementing_serialize "Serialize"
function. */
template <typename Serializable>
std::string SaveJsonString(const Serializable& data);

namespace internal {

void WriteFile(const std::string& filename, const std::string& data);
Expand Down Expand Up @@ -187,5 +202,14 @@ std::string SaveYamlString(const Serializable& data,
return archive.EmitString(child_name.value_or(std::string()));
}

// (Implementation of a function declared above. This could be defined
// inline, but we keep it with the others for consistency.)
template <typename Serializable>
std::string SaveJsonString(const Serializable& data) {
internal::YamlWriteArchive archive;
archive.Accept(data);
return archive.ToJson();
}

} // namespace yaml
} // namespace drake
43 changes: 38 additions & 5 deletions common/yaml/yaml_node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Node Node::MakeMapping() {
Node Node::MakeNull() {
Node result;
result.data_ = ScalarData{"null"};
result.tag_ = kTagNull;
result.tag_ = JsonSchemaTag::kNull;
return result;
}

Expand Down Expand Up @@ -113,7 +113,10 @@ bool Node::IsMapping() const {
}

bool operator==(const Node& a, const Node& b) {
return std::tie(a.tag_, a.data_) == std::tie(b.tag_, b.data_);
// We need to compare the canonical form of a tag (i.e., its string).
auto a_tag = a.GetTag();
auto b_tag = b.GetTag();
return std::tie(a_tag, a.data_) == std::tie(b_tag, b.data_);
}

bool operator==(const Node::ScalarData& a, const Node::ScalarData& b) {
Expand All @@ -128,12 +131,42 @@ bool operator==(const Node::MappingData& a, const Node::MappingData& b) {
return a.mapping == b.mapping;
}

const std::string& Node::GetTag() const {
return tag_;
std::string_view Node::GetTag() const {
return std::visit( // BR
overloaded{
[](std::monostate) -> std::string_view {
return {};
},
[](JsonSchemaTag tag) -> std::string_view {
switch (tag) {
case JsonSchemaTag::kNull:
return kTagNull;
case JsonSchemaTag::kBool:
return kTagBool;
case JsonSchemaTag::kInt:
return kTagInt;
case JsonSchemaTag::kFloat:
return kTagFloat;
}
DRAKE_UNREACHABLE();
},
[](const std::string& tag) -> std::string_view {
return tag;
},
},
tag_);
}

void Node::SetTag(JsonSchemaTag tag) {
tag_ = tag;
}

void Node::SetTag(std::string tag) {
tag_ = std::move(tag);
if (tag.empty()) {
tag_ = {};
} else {
tag_ = std::move(tag);
}
}

const std::string& Node::GetScalar() const {
Expand Down
37 changes: 30 additions & 7 deletions common/yaml/yaml_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ enum class NodeType {
kMapping,
};

/* Denotes one of the "JSON Schema" tags.
See https://yaml.org/spec/1.2.2/#json-schema. */
enum class JsonSchemaTag {
// https://yaml.org/spec/1.2.2/#null
kNull,
// https://yaml.org/spec/1.2.2/#boolean
kBool,
// https://yaml.org/spec/1.2.2/#integer
kInt,
// https://yaml.org/spec/1.2.2/#floating-point
kFloat,
};

/* Data type that represents a YAML node. A Node can hold one of three
possible kinds of value at runtime:
- Scalar
Expand Down Expand Up @@ -120,22 +133,29 @@ class Node final {
/* Gets this node's YAML tag.
See https://yaml.org/spec/1.2.2/#tags.
By default (i.e., at construction time), the tag will be empty. */
const std::string& GetTag() const;
std::string_view GetTag() const;

/* Sets this node's YAML tag to one of the "JSON Schema" tags.
See https://yaml.org/spec/1.2.2/#json-schema. */
void SetTag(JsonSchemaTag);

/* Sets this node's YAML tag.
See https://yaml.org/spec/1.2.2/#tags.
The tag is not checked for well-formedness nor consistency with the node's
type nor value. The caller is responsible for providing a valid tag. */
void SetTag(std::string);

// https://yaml.org/spec/1.2.2/#floating-point
static constexpr std::string_view kTagFloat{"tag:yaml.org,2002:float"};
// https://yaml.org/spec/1.2.2/#null
static constexpr std::string_view kTagNull{"tag:yaml.org,2002:null"};

// https://yaml.org/spec/1.2.2/#boolean
static constexpr std::string_view kTagBool{"tag:yaml.org,2002:bool"};

// https://yaml.org/spec/1.2.2/#integer
static constexpr std::string_view kTagInt{"tag:yaml.org,2002:int"};

// https://yaml.org/spec/1.2.2/#null
static constexpr std::string_view kTagNull{"tag:yaml.org,2002:null"};
// https://yaml.org/spec/1.2.2/#floating-point
static constexpr std::string_view kTagFloat{"tag:yaml.org,2002:float"};

// https://yaml.org/spec/1.2.2/#generic-string
static constexpr std::string_view kTagStr{"tag:yaml.org,2002:str"};
Expand Down Expand Up @@ -237,9 +257,12 @@ class Node final {
Node();

using Variant = std::variant<ScalarData, SequenceData, MappingData>;

std::string tag_;
Variant data_;

// The YAML tag is not required, but can be set to either a well-known enum or
// a bespoke string. The representation here is not canonical -- it's possible
// to set a string value that is equivalent to an enum's implied string.
std::variant<std::monostate, JsonSchemaTag, std::string> tag_;
};

} // namespace internal
Expand Down
10 changes: 5 additions & 5 deletions common/yaml/yaml_read_archive.h
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,13 @@ class YamlReadArchive final {
return;
}
// Figure out which variant<...> type we have based on the node's tag.
const std::string& tag = sub_node->GetTag();
const std::string_view tag = sub_node->GetTag();
VariantHelper(tag, nvp.name(), nvp.value());
}

// Steps through Types to extract 'size_t I' and 'typename T' for the Impl.
template <template <typename...> class Variant, typename... Types>
void VariantHelper(const std::string& tag, const char* name,
void VariantHelper(std::string_view tag, const char* name,
Variant<Types...>* storage) {
if (tag == internal::Node::kTagNull) {
// Our variant parsing does not yet support nulls. When the tag indicates
Expand All @@ -307,7 +307,7 @@ class YamlReadArchive final {
// Recursive case -- checks if `tag` matches `T` (which was the I'th type in
// the template parameter pack), or else keeps looking.
template <size_t I, typename Variant, typename T, typename... Remaining>
void VariantHelperImpl(const std::string& tag, const char* name,
void VariantHelperImpl(std::string_view tag, const char* name,
Variant* storage) {
// For the first type declared in the variant<> (I == 0), the tag can be
// absent; otherwise, the tag must match one of the variant's types.
Expand All @@ -322,13 +322,13 @@ class YamlReadArchive final {

// Base case -- no match.
template <size_t, typename Variant>
void VariantHelperImpl(const std::string& tag, const char*, Variant*) {
void VariantHelperImpl(std::string_view tag, const char*, Variant*) {
ReportError(fmt::format(
"has unsupported type tag {} while selecting a variant<>", tag));
}

// Checks if a NiceTypeName matches the yaml type tag.
bool IsTagMatch(const std::string& name, const std::string& tag) const {
bool IsTagMatch(std::string_view name, std::string_view tag) const {
// Check for the "fail safe schema" YAML types and similar.
if (name == "std::string") {
return tag == internal::Node::kTagStr;
Expand Down
Loading

0 comments on commit 7b7ace4

Please sign in to comment.