Skip to content

Commit 6b4799b

Browse files
committed
Promote integration snippets in the schema pages
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent b11188c commit 6b4799b

7 files changed

Lines changed: 279 additions & 38 deletions

File tree

docs/integrations.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,45 @@ custom integrations, and we are always eager to hear more about use cases that
1111
demand a more direct integration. If you have any ideas, please reach out on
1212
[GitHub Discussions](https://github.com/sourcemeta/one/discussions)!
1313

14+
## Tools
15+
16+
### JSON Schema CLI
17+
18+
The [JSON Schema CLI](https://github.com/sourcemeta/jsonschema) can fetch
19+
schemas from Sourcemeta One, resolve their references, and bundle them locally
20+
using the `install` command. For example, to install a schema and its
21+
dependencies into a local `schemas` directory:
22+
23+
```sh
24+
jsonschema install https://schemas.example.com/my/schema.json schemas/schema.json
25+
```
26+
27+
The CLI will fetch the schema, resolve any `$ref` references it depends on, and
28+
write a self-contained bundled result to the given path. On subsequent runs, it
29+
only re-fetches dependencies that have changed.
30+
31+
For projects with multiple schemas, you can declare them in a
32+
`jsonschema.json` configuration file:
33+
34+
```json title="jsonschema.json"
35+
{
36+
"dependencies": {
37+
"https://schemas.example.com/schemas/user.json": "./schemas/user.json",
38+
"https://schemas.example.com/schemas/order.json": "./schemas/order.json"
39+
}
40+
}
41+
```
42+
43+
Then run `jsonschema install` without arguments to fetch them all. A
44+
`jsonschema.lock.json` lock file is created to track dependency hashes for
45+
reproducible installations. We recommend committing both files to version
46+
control.
47+
48+
!!! tip
49+
50+
Use `jsonschema install --frozen` in CI/CD to verify that installed
51+
schemas match the lock file exactly, catching any unexpected changes.
52+
1453
## Languages
1554

1655
### Deno

src/web/pages/schema.cc

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,84 @@ auto GENERATE_WEB_SCHEMA::handler(
6363

6464
content_children.emplace_back(div(header_children));
6565

66+
// Integration snippets
67+
const auto schema_name{
68+
std::filesystem::path{meta.at("path").to_string()}.filename().string()};
69+
const auto cli_snippet{"jsonschema install " + canonical + " schemas/" +
70+
schema_name + ".json"};
71+
const auto openapi_snippet{R"($ref: ")" + canonical + R"(")"};
72+
const auto deno_snippet{R"(import schema from ")" + canonical +
73+
R"(" with { type: "json" };)"};
74+
75+
std::vector<sourcemeta::core::HTMLNode> usage_buttons;
76+
usage_buttons.emplace_back(
77+
button({{"class", "btn btn-sm btn-outline-secondary"},
78+
{"type", "button"},
79+
{"data-sourcemeta-ui-tab-target", "usage-cli"}},
80+
"CLI"));
81+
usage_buttons.emplace_back(
82+
button({{"class", "btn btn-sm btn-outline-secondary"},
83+
{"type", "button"},
84+
{"data-sourcemeta-ui-tab-target", "usage-openapi"}},
85+
"OpenAPI"));
86+
usage_buttons.emplace_back(
87+
button({{"class", "btn btn-sm btn-outline-secondary"},
88+
{"type", "button"},
89+
{"data-sourcemeta-ui-tab-target", "usage-deno"}},
90+
"Deno"));
91+
92+
std::vector<sourcemeta::core::HTMLNode> usage_row;
93+
usage_row.emplace_back(
94+
span({{"class", "text-secondary fw-light text-nowrap"}}, "Use with"));
95+
usage_row.emplace_back(
96+
div({{"class", "btn-group flex-shrink-0 me-2"}, {"role", "group"}},
97+
usage_buttons));
98+
usage_row.emplace_back(
99+
div({{"data-sourcemeta-ui-tab-id", "usage-cli"},
100+
{"class", "d-none d-flex align-items-center flex-grow-1 gap-2"},
101+
{"style", "min-width: 0"}},
102+
code({{"class", "bg-white border p-2 font-monospace flex-grow-1 "
103+
"text-dark text-break"}},
104+
span("$ "),
105+
a({{"href", "/integrations/#json-schema-cli"},
106+
{"target", "_blank"},
107+
{"class", "text-dark"}},
108+
"jsonschema install"),
109+
span(" " + canonical + " schemas/" + schema_name + ".json")),
110+
button({{"class", "btn btn-sm btn-outline-secondary"},
111+
{"type", "button"},
112+
{"data-sourcemeta-ui-copy", cli_snippet}},
113+
i({{"class", "bi bi-clipboard"}}))));
114+
usage_row.emplace_back(
115+
div({{"data-sourcemeta-ui-tab-id", "usage-openapi"},
116+
{"class", "d-none d-flex align-items-center flex-grow-1 gap-2"},
117+
{"style", "min-width: 0"}},
118+
code({{"class", "bg-white border p-2 font-monospace flex-grow-1 "
119+
"text-dark text-break"}},
120+
openapi_snippet),
121+
button({{"class", "btn btn-sm btn-outline-secondary"},
122+
{"type", "button"},
123+
{"data-sourcemeta-ui-copy", openapi_snippet}},
124+
i({{"class", "bi bi-clipboard"}}))));
125+
usage_row.emplace_back(
126+
div({{"data-sourcemeta-ui-tab-id", "usage-deno"},
127+
{"class", "d-none d-flex align-items-center flex-grow-1 gap-2"},
128+
{"style", "min-width: 0"}},
129+
code({{"class", "bg-white border p-2 font-monospace flex-grow-1 "
130+
"text-dark text-break"}},
131+
deno_snippet),
132+
button({{"class", "btn btn-sm btn-outline-secondary"},
133+
{"type", "button"},
134+
{"data-sourcemeta-ui-copy", deno_snippet}},
135+
i({{"class", "bi bi-clipboard"}}))));
136+
137+
content_children.emplace_back(
138+
div({{"data-sourcemeta-ui-tab-group", "usage"},
139+
{"class", "bg-light border rounded px-3 py-2 mt-4 d-flex "
140+
"flex-wrap flex-md-nowrap align-items-center small "
141+
"gap-2"}},
142+
usage_row));
143+
66144
// Information table
67145
std::vector<sourcemeta::core::HTMLNode> table_rows;
68146

@@ -145,6 +223,8 @@ auto GENERATE_WEB_SCHEMA::handler(
145223
indirect_dependent_schemas.erase(schema);
146224
}
147225

226+
std::vector<sourcemeta::core::HTMLNode> details_children;
227+
148228
// Tab navigation
149229
std::vector<sourcemeta::core::HTMLNode> nav_items;
150230
nav_items.emplace_back(li(
@@ -193,7 +273,7 @@ auto GENERATE_WEB_SCHEMA::handler(
193273
"ms-2 badge rounded-pill text-bg-secondary align-text-top"}},
194274
std::to_string(health.at("errors").size())))));
195275

196-
container_children.emplace_back(
276+
details_children.emplace_back(
197277
ul({{"class", "nav nav-tabs mt-4 mb-3"}}, nav_items));
198278

199279
// Examples tab
@@ -212,7 +292,7 @@ auto GENERATE_WEB_SCHEMA::handler(
212292
examples_content.emplace_back(
213293
div({{"class", "list-group"}}, example_items));
214294
}
215-
container_children.emplace_back(
295+
details_children.emplace_back(
216296
div({{"data-sourcemeta-ui-tab-id", "examples"}, {"class", "d-none"}},
217297
examples_content));
218298

@@ -278,7 +358,7 @@ auto GENERATE_WEB_SCHEMA::handler(
278358
th({{"scope", "col"}}, "Dependency"))),
279359
tbody(dep_table_rows)));
280360
}
281-
container_children.emplace_back(
361+
details_children.emplace_back(
282362
div({{"data-sourcemeta-ui-tab-id", "dependencies"}, {"class", "d-none"}},
283363
dependencies_content));
284364

@@ -341,7 +421,7 @@ auto GENERATE_WEB_SCHEMA::handler(
341421
th({{"scope", "col"}}, "Dependent"))),
342422
tbody(dep_tab_rows)));
343423
}
344-
container_children.emplace_back(
424+
details_children.emplace_back(
345425
div({{"data-sourcemeta-ui-tab-id", "dependents"}, {"class", "d-none"}},
346426
dependents_content));
347427

@@ -387,10 +467,15 @@ auto GENERATE_WEB_SCHEMA::handler(
387467
}
388468
health_content.emplace_back(div({{"class", "list-group"}}, error_items));
389469
}
390-
container_children.emplace_back(
470+
details_children.emplace_back(
391471
div({{"data-sourcemeta-ui-tab-id", "health"}, {"class", "d-none"}},
392472
health_content));
393473

474+
container_children.emplace_back(
475+
div({{"data-sourcemeta-ui-tab-group", "details"},
476+
{"data-sourcemeta-ui-tab-url-param", "tab"}},
477+
details_children));
478+
394479
std::ostringstream html_content;
395480
html_content << "<!DOCTYPE html>"
396481
<< html::make_page(configuration, canonical, title, description,

src/web/scripts/copy.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
document.querySelectorAll("[data-sourcemeta-ui-copy]").forEach((button) => {
2+
button.addEventListener("click", () => {
3+
const text = button.getAttribute("data-sourcemeta-ui-copy");
4+
navigator.clipboard.writeText(text);
5+
const icon = button.querySelector("i");
6+
if (icon) {
7+
icon.classList.replace("bi-clipboard", "bi-check");
8+
setTimeout(() => icon.classList.replace("bi-check", "bi-clipboard"), 1500);
9+
}
10+
});
11+
});

src/web/scripts/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "./tabs.js";
2+
import "./copy.js";
23
import "./search.js";
34

45
import { Editor } from "./editor.js";

src/web/scripts/tabs.js

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
1-
const tabButtons = document.querySelectorAll("[data-sourcemeta-ui-tab-target]");
2-
const tabViews = document.querySelectorAll("[data-sourcemeta-ui-tab-id]");
1+
document.querySelectorAll("[data-sourcemeta-ui-tab-group]").forEach((group) => {
2+
const tabButtons = group.querySelectorAll("[data-sourcemeta-ui-tab-target]");
3+
const tabViews = group.querySelectorAll("[data-sourcemeta-ui-tab-id]");
4+
const urlParam = group.getAttribute("data-sourcemeta-ui-tab-url-param");
35

4-
function selectTab(tab, name, updateURL) {
5-
tabButtons.forEach(element => element.classList.remove("active"));
6-
tab.classList.add("active");
7-
tabViews.forEach((panel) => {
8-
if (panel.getAttribute("data-sourcemeta-ui-tab-id") === name) {
9-
panel.classList.remove("d-none");
10-
} else {
11-
panel.classList.add("d-none");
12-
}
13-
});
6+
function selectTab(tab, name, updateURL) {
7+
tabButtons.forEach(element => element.classList.remove("active"));
8+
tab.classList.add("active");
9+
tabViews.forEach((panel) => {
10+
if (panel.getAttribute("data-sourcemeta-ui-tab-id") === name) {
11+
panel.classList.remove("d-none");
12+
} else {
13+
panel.classList.add("d-none");
14+
}
15+
});
1416

15-
if (updateURL) {
16-
const url = new URL(window.location);
17-
url.searchParams.set("tab", name);
18-
window.history.replaceState({}, "", url);
17+
if (updateURL && urlParam) {
18+
const url = new URL(window.location);
19+
url.searchParams.set(urlParam, name);
20+
window.history.replaceState({}, "", url);
21+
}
1922
}
20-
}
2123

22-
const initialTab = new URL(window.location).searchParams.get("tab");
23-
if (initialTab) {
24-
tabButtons.forEach((element) => {
25-
if (element.getAttribute("data-sourcemeta-ui-tab-target") === initialTab) {
26-
selectTab(element, initialTab, true);
24+
if (urlParam) {
25+
const initialTab = new URL(window.location).searchParams.get(urlParam);
26+
let matched = false;
27+
if (initialTab) {
28+
tabButtons.forEach((element) => {
29+
if (element.getAttribute("data-sourcemeta-ui-tab-target") === initialTab) {
30+
selectTab(element, initialTab, true);
31+
matched = true;
32+
}
33+
});
2734
}
28-
});
29-
} else if (tabButtons[0]) {
30-
selectTab(tabButtons[0],
31-
tabButtons[0].getAttribute("data-sourcemeta-ui-tab-target"),
32-
// Don't update the URL for the default one to avoid some unnecessary noise
33-
false);
34-
}
3535

36-
tabButtons.forEach((tab) => {
37-
tab.addEventListener("click", () => {
38-
const targetId = tab.getAttribute("data-sourcemeta-ui-tab-target");
39-
selectTab(tab, targetId, true);
36+
if (!matched && tabButtons[0]) {
37+
selectTab(tabButtons[0],
38+
tabButtons[0].getAttribute("data-sourcemeta-ui-tab-target"),
39+
false);
40+
}
41+
} else if (tabButtons[0]) {
42+
selectTab(tabButtons[0],
43+
tabButtons[0].getAttribute("data-sourcemeta-ui-tab-target"),
44+
false);
45+
}
46+
47+
tabButtons.forEach((tab) => {
48+
tab.addEventListener("click", () => {
49+
const targetId = tab.getAttribute("data-sourcemeta-ui-tab-target");
50+
selectTab(tab, targetId, true);
51+
});
4052
});
4153
});

src/web/style.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
@import "../../vendor/bootstrap/scss/functions";
88

99
$border-radius: 0;
10+
$btn-border-radius: 0;
1011
$breadcrumb-font-size: 15px;
1112

1213
@import "../../vendor/bootstrap/scss/variables";

0 commit comments

Comments
 (0)