Skip to content

Commit 4f2498f

Browse files
authored
Allow converting Configuration back to JSON (#626)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent cb4ceae commit 4f2498f

6 files changed

Lines changed: 256 additions & 76 deletions

File tree

src/configuration/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME configuration
2-
PRIVATE_HEADERS error.h SOURCES parse.cc configuration.cc lock.cc fetch.cc)
2+
PRIVATE_HEADERS error.h SOURCES parse.cc json.cc configuration.cc lock.cc fetch.cc)
33

44
if(BLAZE_INSTALL)
55
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME configuration)

src/configuration/include/sourcemeta/blaze/configuration.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ struct SOURCEMETA_BLAZE_CONFIGURATION_EXPORT Configuration {
188188
const std::filesystem::path &base_path)
189189
-> Configuration;
190190

191+
/// Serialize a configuration to JSON
192+
[[nodiscard]]
193+
auto to_json() const -> sourcemeta::core::JSON;
194+
191195
/// Read and parse a configuration file
192196
[[nodiscard]]
193197
static auto read_json(const std::filesystem::path &path,

src/configuration/json.cc

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#include <sourcemeta/blaze/configuration.h>
2+
3+
#include <sourcemeta/core/json.h>
4+
5+
#include <algorithm> // std::ranges::sort
6+
#include <string> // std::string
7+
#include <vector> // std::vector
8+
9+
namespace sourcemeta::blaze {
10+
11+
auto Configuration::to_json() const -> sourcemeta::core::JSON {
12+
auto result{sourcemeta::core::JSON::make_object()};
13+
14+
if (this->title.has_value()) {
15+
result.assign("title", sourcemeta::core::JSON{this->title.value()});
16+
}
17+
18+
if (this->description.has_value()) {
19+
result.assign("description",
20+
sourcemeta::core::JSON{this->description.value()});
21+
}
22+
23+
if (this->email.has_value()) {
24+
result.assign("email", sourcemeta::core::JSON{this->email.value()});
25+
}
26+
27+
if (this->github.has_value()) {
28+
result.assign("github", sourcemeta::core::JSON{this->github.value()});
29+
}
30+
31+
if (this->website.has_value()) {
32+
result.assign("website", sourcemeta::core::JSON{this->website.value()});
33+
}
34+
35+
result.assign("path",
36+
sourcemeta::core::JSON{this->absolute_path.generic_string()});
37+
result.assign("baseUri", sourcemeta::core::JSON{this->base});
38+
39+
if (this->default_dialect.has_value()) {
40+
result.assign("defaultDialect",
41+
sourcemeta::core::JSON{this->default_dialect.value()});
42+
}
43+
44+
if (!this->extension.empty()) {
45+
auto extension_array{sourcemeta::core::JSON::make_array()};
46+
// Sort for deterministic output
47+
std::vector<std::string> sorted_extensions{this->extension.cbegin(),
48+
this->extension.cend()};
49+
std::ranges::sort(sorted_extensions);
50+
for (const auto &entry : sorted_extensions) {
51+
extension_array.push_back(sourcemeta::core::JSON{entry});
52+
}
53+
54+
result.assign("extension", std::move(extension_array));
55+
}
56+
57+
if (!this->resolve.empty()) {
58+
auto resolve_object{sourcemeta::core::JSON::make_object()};
59+
for (const auto &pair : this->resolve) {
60+
resolve_object.assign(pair.first, sourcemeta::core::JSON{pair.second});
61+
}
62+
63+
result.assign("resolve", std::move(resolve_object));
64+
}
65+
66+
if (!this->dependencies.empty()) {
67+
auto dependencies_object{sourcemeta::core::JSON::make_object()};
68+
for (const auto &pair : this->dependencies) {
69+
dependencies_object.assign(
70+
pair.first, sourcemeta::core::JSON{pair.second.generic_string()});
71+
}
72+
73+
result.assign("dependencies", std::move(dependencies_object));
74+
}
75+
76+
for (const auto &pair : this->extra.as_object()) {
77+
result.assign(pair.first, pair.second);
78+
}
79+
80+
return result;
81+
}
82+
83+
} // namespace sourcemeta::blaze

test/configuration/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ sourcemeta_googletest(NAMESPACE sourcemeta PROJECT blaze NAME configuration
44
configuration_applies_to_test.cc
55
configuration_find_test.cc
66
configuration_from_json_test.cc
7-
configuration_read_json_test.cc
7+
configuration_json_test.cc
88
configuration_lock_test.cc
99
configuration_lock_parse_v1_test.cc
1010
configuration_fetch_test.cc
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <sourcemeta/blaze/configuration.h>
4+
#include <sourcemeta/core/json.h>
5+
6+
#include "configuration_test_utils.h"
7+
8+
#include <unordered_map> // std::unordered_map
9+
10+
TEST(Configuration_json, read_json_valid_1) {
11+
std::unordered_map<std::string, std::string> files;
12+
files["/test/blaze.json"] = R"JSON({
13+
"title": "Sourcemeta",
14+
"description": "The JSON Schema company",
15+
"email": "hello@sourcemeta.com",
16+
"github": "sourcemeta",
17+
"website": "https://www.sourcemeta.com",
18+
"path": "./schemas",
19+
"baseUri": "https://schemas.sourcemeta.com",
20+
"defaultDialect": "http://json-schema.org/draft-07/schema#",
21+
"resolve": {
22+
"https://other.com/single.json": "../single.json"
23+
}
24+
})JSON";
25+
26+
const auto manifest{sourcemeta::blaze::Configuration::read_json(
27+
"/test/blaze.json", MAKE_READER(files))};
28+
29+
EXPECT_TRUE(manifest.title.has_value());
30+
EXPECT_EQ(manifest.title.value(), "Sourcemeta");
31+
EXPECT_TRUE(manifest.description.has_value());
32+
EXPECT_EQ(manifest.description.value(), "The JSON Schema company");
33+
EXPECT_TRUE(manifest.email.has_value());
34+
EXPECT_EQ(manifest.email.value(), "hello@sourcemeta.com");
35+
EXPECT_TRUE(manifest.github.has_value());
36+
EXPECT_EQ(manifest.github.value(), "sourcemeta");
37+
EXPECT_TRUE(manifest.website.has_value());
38+
EXPECT_EQ(manifest.website.value(), "https://www.sourcemeta.com");
39+
EXPECT_EQ(manifest.absolute_path, std::filesystem::path{"/test"} / "schemas");
40+
EXPECT_EQ(manifest.base, "https://schemas.sourcemeta.com");
41+
EXPECT_TRUE(manifest.default_dialect.has_value());
42+
EXPECT_EQ(manifest.default_dialect.value(),
43+
"http://json-schema.org/draft-07/schema#");
44+
EXPECT_EQ(manifest.resolve.size(), 1);
45+
EXPECT_TRUE(manifest.resolve.contains("https://other.com/single.json"));
46+
EXPECT_EQ(manifest.resolve.at("https://other.com/single.json"),
47+
"../single.json");
48+
EXPECT_EQ(manifest.extra.size(), 0);
49+
}
50+
51+
TEST(Configuration_json, read_json_valid_without_path) {
52+
std::unordered_map<std::string, std::string> files;
53+
files["/test/blaze.json"] = R"JSON({
54+
"title": "Test Config Without Path",
55+
"description": "A test configuration file without a path property",
56+
"baseUri": "https://example.com"
57+
})JSON";
58+
59+
const auto manifest{sourcemeta::blaze::Configuration::read_json(
60+
"/test/blaze.json", MAKE_READER(files))};
61+
62+
EXPECT_TRUE(manifest.title.has_value());
63+
EXPECT_EQ(manifest.title.value(), "Test Config Without Path");
64+
EXPECT_TRUE(manifest.description.has_value());
65+
EXPECT_EQ(manifest.description.value(),
66+
"A test configuration file without a path property");
67+
EXPECT_FALSE(manifest.email.has_value());
68+
EXPECT_FALSE(manifest.github.has_value());
69+
EXPECT_FALSE(manifest.website.has_value());
70+
EXPECT_EQ(manifest.absolute_path, std::filesystem::path{"/test"});
71+
EXPECT_EQ(manifest.base, "https://example.com");
72+
EXPECT_FALSE(manifest.default_dialect.has_value());
73+
EXPECT_EQ(manifest.resolve.size(), 0);
74+
EXPECT_EQ(manifest.extra.size(), 0);
75+
}
76+
77+
TEST(Configuration_json, to_json_all_fields) {
78+
sourcemeta::blaze::Configuration config;
79+
config.title = "Sourcemeta";
80+
config.description = "The JSON Schema company";
81+
config.email = "hello@sourcemeta.com";
82+
config.github = "sourcemeta";
83+
config.website = "https://www.sourcemeta.com";
84+
config.absolute_path = "/test/schemas";
85+
config.base = "https://schemas.sourcemeta.com";
86+
config.default_dialect = "http://json-schema.org/draft-07/schema#";
87+
config.extension = {".json", ".yaml"};
88+
config.resolve.emplace("https://other.com/single.json", "../single.json");
89+
config.dependencies.emplace(
90+
"https://json-schema.org/draft/2020-12/schema",
91+
std::filesystem::path{"/test/vendor/2020-12.json"});
92+
93+
const auto result{config.to_json()};
94+
95+
const auto expected{sourcemeta::core::parse_json(R"JSON({
96+
"title": "Sourcemeta",
97+
"description": "The JSON Schema company",
98+
"email": "hello@sourcemeta.com",
99+
"github": "sourcemeta",
100+
"website": "https://www.sourcemeta.com",
101+
"path": "/test/schemas",
102+
"baseUri": "https://schemas.sourcemeta.com",
103+
"defaultDialect": "http://json-schema.org/draft-07/schema#",
104+
"extension": [ ".json", ".yaml" ],
105+
"resolve": {
106+
"https://other.com/single.json": "../single.json"
107+
},
108+
"dependencies": {
109+
"https://json-schema.org/draft/2020-12/schema": "/test/vendor/2020-12.json"
110+
}
111+
})JSON")};
112+
113+
EXPECT_EQ(result, expected);
114+
}
115+
116+
TEST(Configuration_json, to_json_minimal) {
117+
sourcemeta::blaze::Configuration config;
118+
config.absolute_path = "/test";
119+
config.base = "https://example.com";
120+
121+
const auto result{config.to_json()};
122+
123+
const auto expected{sourcemeta::core::parse_json(R"JSON({
124+
"path": "/test",
125+
"baseUri": "https://example.com"
126+
})JSON")};
127+
128+
EXPECT_EQ(result, expected);
129+
}
130+
131+
TEST(Configuration_json, to_json_with_extra) {
132+
sourcemeta::blaze::Configuration config;
133+
config.absolute_path = "/test";
134+
config.base = "https://example.com";
135+
config.extra.assign("x-foo", sourcemeta::core::JSON{"bar"});
136+
137+
const auto result{config.to_json()};
138+
139+
const auto expected{sourcemeta::core::parse_json(R"JSON({
140+
"path": "/test",
141+
"baseUri": "https://example.com",
142+
"x-foo": "bar"
143+
})JSON")};
144+
145+
EXPECT_EQ(result, expected);
146+
}
147+
148+
TEST(Configuration_json, to_json_roundtrip) {
149+
const auto input{sourcemeta::core::parse_json(R"JSON({
150+
"title": "Sourcemeta",
151+
"description": "The JSON Schema company",
152+
"email": "hello@sourcemeta.com",
153+
"github": "sourcemeta",
154+
"website": "https://www.sourcemeta.com",
155+
"baseUri": "https://schemas.sourcemeta.com",
156+
"defaultDialect": "http://json-schema.org/draft-07/schema#",
157+
"path": "/test/schemas",
158+
"extension": [ ".json", ".yaml", ".yml" ],
159+
"x-foo": "bar"
160+
})JSON")};
161+
162+
const auto config{
163+
sourcemeta::blaze::Configuration::from_json(input, "/test")};
164+
const auto output{config.to_json()};
165+
166+
EXPECT_EQ(output, input);
167+
}

test/configuration/configuration_read_json_test.cc

Lines changed: 0 additions & 74 deletions
This file was deleted.

0 commit comments

Comments
 (0)