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 6, 2023
1 parent 6d3c199 commit 64d8551
Show file tree
Hide file tree
Showing 10 changed files with 436 additions and 33 deletions.
10 changes: 10 additions & 0 deletions common/yaml/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ drake_cc_library(
],
)

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

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

#include <gtest/gtest.h>

#include "drake/common/temp_directory.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;
EXPECT_EQ(SaveJsonString(data), R"""({"some_bool":false,)"""
R"""("some_double":1.2345,)"""
R"""("some_float":1.2345,)"""
R"""("some_int32":12,)"""
R"""("some_int64":14,)"""
R"""("some_string":"kNominalString",)"""
R"""("some_uint32":12,)"""
R"""("some_uint64":15})""");
}

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

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})""");

// There is no syntax that could express this in JSON.
data.value.emplace<uint64_t>(22);
DRAKE_EXPECT_THROWS_MESSAGE(SaveJsonString(data),
".*SaveJsonString.*scalar.*22.*tag.*");
}

GTEST_TEST(YamlJsonTest, FileRoundTrip) {
DoubleStruct data;
data.value = 22.25;
const std::string filename = temp_directory() + "/file_round_trip.json";
SaveJsonFile(filename, data);
const auto readback = LoadYamlFile<DoubleStruct>(filename);
EXPECT_EQ(readback.value, 22.25);
}

} // 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
9 changes: 5 additions & 4 deletions common/yaml/yaml_io.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ namespace drake {
namespace yaml {
namespace internal {

void WriteFile(const std::string& filename, const std::string& data) {
void WriteFile(std::string_view function_name, const std::string& filename,
const std::string& data) {
std::ofstream out(filename, std::ios::binary);
if (out.fail()) {
throw std::runtime_error(fmt::format(
"SaveYamlFile() could not open '{}' for writing", filename));
throw std::runtime_error(fmt::format("{}() could not open '{}' for writing",
function_name, filename));
}
out << data;
if (out.fail()) {
throw std::runtime_error(
fmt::format("SaveYamlFile() could not write to '{}'", filename));
fmt::format("{}() could not write to '{}'", function_name, filename));
}
}

Expand Down
53 changes: 51 additions & 2 deletions common/yaml/yaml_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "drake/common/yaml/yaml_io_options.h"
Expand Down Expand Up @@ -115,9 +116,40 @@ 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 file.
Refer to @ref yaml_serialization "YAML Serialization" for background.
Note that there is no LoadJsonFile() 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>
void SaveJsonFile(const std::string& filename, const Serializable& data);

/** 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);
void WriteFile(std::string_view function_name, const std::string& filename,
const std::string& data);

template <typename Serializable>
static Serializable LoadNode(internal::Node node,
Expand Down Expand Up @@ -168,7 +200,8 @@ template <typename Serializable>
void SaveYamlFile(const std::string& filename, const Serializable& data,
const std::optional<std::string>& child_name,
const std::optional<Serializable>& defaults) {
internal::WriteFile(filename, SaveYamlString(data, child_name, defaults));
internal::WriteFile("SaveYamlFile", filename,
SaveYamlString(data, child_name, defaults));
}

// (Implementation of a function declared above. This could be defined
Expand All @@ -187,5 +220,21 @@ 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>
void SaveJsonFile(const std::string& filename, const Serializable& data) {
internal::WriteFile("SaveJsonFile", filename, SaveJsonString(data));
}

// (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
40 changes: 35 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,39 @@ 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{
[](const std::string& tag) -> std::string_view {
return tag;
},
[](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();
},
},
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
Loading

0 comments on commit 64d8551

Please sign in to comment.