Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index/index.cc
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ static auto index_main(const std::string_view &program,
entry.parent_path() / "schema-html.metapack",
{entry, schema_path / "dependencies.metapack",
schema_path / "health.metapack",
schema_path / "dependents.metapack",
// We rely on the configuration for site metadata
mark_configuration_path, mark_version_path},
configuration, mutex, "Rendering", relative_path.string(),
Expand Down
97 changes: 97 additions & 0 deletions src/web/pages/schema.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <chrono> // std::chrono
#include <filesystem> // std::filesystem
#include <functional> // std::reference_wrapper
#include <set> // std::set
#include <sstream> // std::ostringstream
#include <vector> // std::vector

Expand Down Expand Up @@ -122,6 +123,27 @@ auto GENERATE_WEB_SCHEMA::handler(
const auto health{read_json(dependencies.at(2))};
assert(health.is_object());
assert(health.defines("errors"));
const auto dependents_json{read_json(dependencies.at(3))};
assert(dependents_json.is_array());

// Collect unique dependent schemas, preferring direct over indirect.
// The dependency tree already takes care of self-references, so
// a schema should never appear as its own dependent
std::set<sourcemeta::core::JSON::String> direct_dependent_schemas;
std::set<sourcemeta::core::JSON::String> indirect_dependent_schemas;
for (const auto &dependent : dependents_json.as_array()) {
assert(dependent.at("from") != dependent.at("to"));
if (dependent.at("to") == meta.at("identifier")) {
direct_dependent_schemas.emplace(dependent.at("from").to_string());
} else {
indirect_dependent_schemas.emplace(dependent.at("from").to_string());
}
}

// If a schema is a direct dependent, don't also show it as indirect
for (const auto &schema : direct_dependent_schemas) {
indirect_dependent_schemas.erase(schema);
}

// Tab navigation
std::vector<sourcemeta::core::HTMLNode> nav_items;
Expand All @@ -147,6 +169,18 @@ auto GENERATE_WEB_SCHEMA::handler(
span({{"class",
"ms-2 badge rounded-pill text-bg-secondary align-text-top"}},
std::to_string(dependencies_json.size())))));
nav_items.emplace_back(li(
{{"class", "nav-item"}},
button(
{{"class", "nav-link"},
{"type", "button"},
{"role", "tab"},
{"data-sourcemeta-ui-tab-target", "dependents"}},
span("Dependents"),
span({{"class",
"ms-2 badge rounded-pill text-bg-secondary align-text-top"}},
std::to_string(direct_dependent_schemas.size() +
indirect_dependent_schemas.size())))));
nav_items.emplace_back(li(
{{"class", "nav-item"}},
button(
Expand Down Expand Up @@ -248,6 +282,69 @@ auto GENERATE_WEB_SCHEMA::handler(
div({{"data-sourcemeta-ui-tab-id", "dependencies"}, {"class", "d-none"}},
dependencies_content));

// Dependents tab
std::ostringstream dependent_summary;
dependent_summary
<< "This schema has " << direct_dependent_schemas.size() << " direct "
<< (direct_dependent_schemas.size() == 1 ? "dependent" : "dependents")
<< " and " << indirect_dependent_schemas.size() << " indirect "
<< (indirect_dependent_schemas.size() == 1 ? "dependent" : "dependents")
<< ".";

std::vector<sourcemeta::core::HTMLNode> dependents_content;
dependents_content.emplace_back(p(dependent_summary.str()));

if (!direct_dependent_schemas.empty() ||
!indirect_dependent_schemas.empty()) {
std::vector<sourcemeta::core::HTMLNode> dep_tab_rows;

const auto render_dependent_row{
[&dep_tab_rows,
&configuration](const sourcemeta::core::JSON::String &schema,
const bool is_direct) -> void {
std::vector<sourcemeta::core::HTMLNode> row_cells;
if (is_direct) {
row_cells.emplace_back(
td(span({{"class", "badge text-bg-primary"}}, "Direct")));
} else {
row_cells.emplace_back(
td(span({{"class", "badge text-bg-dark"}}, "Indirect")));
}

if (schema.starts_with(configuration.url)) {
std::filesystem::path dependent_schema_url{
schema.substr(configuration.url.size())};
if (dependent_schema_url.extension() == ".json") {
dependent_schema_url.replace_extension("");
}
row_cells.emplace_back(
td(code(a({{"href", dependent_schema_url.string()}},
dependent_schema_url.string()))));
} else {
row_cells.emplace_back(td(code(schema)));
}

dep_tab_rows.emplace_back(tr(row_cells));
}};

for (const auto &schema : direct_dependent_schemas) {
render_dependent_row(schema, true);
}

for (const auto &schema : indirect_dependent_schemas) {
render_dependent_row(schema, false);
}

dependents_content.emplace_back(
table({{"class", "table table-bordered"}},
thead(tr(th({{"scope", "col"}}, "Type"),
th({{"scope", "col"}}, "Dependent"))),
tbody(dep_tab_rows)));
}
container_children.emplace_back(
div({{"data-sourcemeta-ui-tab-id", "dependents"}, {"class", "d-none"}},
dependents_content));

// Health tab
const auto errors_count{health.at("errors").size()};
std::vector<sourcemeta::core::HTMLNode> health_content;
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/populated/api/list.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jsonpath "$.entries[0].health" == 0
jsonpath "$.entries[0].path" == "/test/v2.0/"
jsonpath "$.entries[1].name" == "schemas"
jsonpath "$.entries[1].type" == "directory"
jsonpath "$.entries[1].health" == 3
jsonpath "$.entries[1].health" == 7
jsonpath "$.entries[1].path" == "/test/schemas/"
jsonpath "$.entries[2].name" == "same"
jsonpath "$.entries[2].type" == "directory"
Expand Down
53 changes: 53 additions & 0 deletions test/e2e/populated/api/schemas-dependencies.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,59 @@ Link: </self/v1/schemas/api/schemas/evaluate/response>; rel="describedby"
[Asserts]
jsonpath "$.valid" == true

GET {{base}}/self/v1/api/schemas/dependencies/test/schemas/recursive
HTTP 200
Content-Type: application/json
Access-Control-Allow-Origin: *
Link: </self/v1/schemas/api/schemas/dependencies/response>; rel="describedby"
[Captures]
last_response: body
schema_path: header "Link" regex "<([^>]+)>"
[Asserts]
header "ETag" exists
header "Last-Modified" exists
jsonpath "$" count == 0

POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
{{last_response}}
```
HTTP 200
Link: </self/v1/schemas/api/schemas/evaluate/response>; rel="describedby"
[Asserts]
jsonpath "$.valid" == true

GET {{base}}/self/v1/api/schemas/dependencies/test/schemas/cycle/foo
HTTP 200
Content-Type: application/json
Access-Control-Allow-Origin: *
Link: </self/v1/schemas/api/schemas/dependencies/response>; rel="describedby"
[Captures]
last_response: body
schema_path: header "Link" regex "<([^>]+)>"
[Asserts]
header "ETag" exists
header "Last-Modified" exists
jsonpath "$" count == 3
jsonpath "$[0].from" == "{{base}}/test/schemas/cycle/foo"
jsonpath "$[0].to" == "{{base}}/test/schemas/cycle/bar"
jsonpath "$[0].at" == "/additionalProperties/$ref"
jsonpath "$[1].from" == "{{base}}/test/schemas/cycle/bar"
jsonpath "$[1].to" == "{{base}}/test/schemas/cycle/baz"
jsonpath "$[1].at" == "/additionalProperties/$ref"
jsonpath "$[2].from" == "{{base}}/test/schemas/cycle/baz"
jsonpath "$[2].to" == "{{base}}/test/schemas/cycle/foo"
jsonpath "$[2].at" == "/additionalProperties/$ref"

POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
{{last_response}}
```
HTTP 200
Link: </self/v1/schemas/api/schemas/evaluate/response>; rel="describedby"
[Asserts]
jsonpath "$.valid" == true

HEAD {{base}}/self/v1/api/schemas/dependencies/test/v2.0/schema
HTTP 200
Content-Type: application/json
Expand Down
50 changes: 50 additions & 0 deletions test/e2e/populated/api/schemas-dependents.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,56 @@ Link: </self/v1/schemas/api/schemas/evaluate/response>; rel="describedby"
[Asserts]
jsonpath "$.valid" == true

GET {{base}}/self/v1/api/schemas/dependents/test/schemas/recursive
HTTP 200
Content-Type: application/json
Access-Control-Allow-Origin: *
Link: </self/v1/schemas/api/schemas/dependents/response>; rel="describedby"
[Captures]
last_response: body
schema_path: header "Link" regex "<([^>]+)>"
[Asserts]
header "ETag" exists
header "Last-Modified" exists
jsonpath "$" count == 0

POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
{{last_response}}
```
HTTP 200
Link: </self/v1/schemas/api/schemas/evaluate/response>; rel="describedby"
[Asserts]
jsonpath "$.valid" == true

GET {{base}}/self/v1/api/schemas/dependents/test/schemas/cycle/foo
HTTP 200
Content-Type: application/json
Access-Control-Allow-Origin: *
Link: </self/v1/schemas/api/schemas/dependents/response>; rel="describedby"
[Captures]
last_response: body
schema_path: header "Link" regex "<([^>]+)>"
[Asserts]
header "ETag" exists
header "Last-Modified" exists
jsonpath "$" count == 3
jsonpath "$[0].from" == "{{base}}/test/schemas/cycle/bar"
jsonpath "$[0].to" == "{{base}}/test/schemas/cycle/baz"
jsonpath "$[1].from" == "{{base}}/test/schemas/cycle/baz"
jsonpath "$[1].to" == "{{base}}/test/schemas/cycle/foo"
jsonpath "$[2].from" == "{{base}}/test/schemas/cycle/foo"
jsonpath "$[2].to" == "{{base}}/test/schemas/cycle/bar"

POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
{{last_response}}
```
HTTP 200
Link: </self/v1/schemas/api/schemas/evaluate/response>; rel="describedby"
[Asserts]
jsonpath "$.valid" == true

HEAD {{base}}/self/v1/api/schemas/dependents/test/bundling/double
HTTP 200
Content-Type: application/json
Expand Down
Loading