Skip to content

Commit 8d551ba

Browse files
committed
docs: generate API reference samples that run, in current idioms
1 parent 03a4ae8 commit 8d551ba

12 files changed

Lines changed: 431 additions & 34 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,39 @@ jobs:
5353
pnpm build
5454
diff -r "$RUNNER_TEMP/schemas-before-docs" dist
5555
56+
# Every code sample is run, so CI needs each sample language's runtime.
57+
- uses: actions/setup-python@v5
58+
with:
59+
python-version: '3.12'
60+
61+
- uses: shivammathur/setup-php@v2
62+
with:
63+
php-version: '8.3'
64+
tools: composer
65+
66+
- uses: ruby/setup-ruby@v1
67+
with:
68+
ruby-version: '3.3'
69+
70+
- uses: actions/setup-java@v4
71+
with:
72+
distribution: temurin
73+
java-version: '17'
74+
75+
- uses: actions/setup-go@v5
76+
with:
77+
go-version: '1.22'
78+
cache: false
79+
80+
- name: Install sample libraries
81+
run: |
82+
pip install requests
83+
mkdir -p "$RUNNER_TEMP/php"
84+
composer require guzzlehttp/guzzle:^7 --working-dir "$RUNNER_TEMP/php" --no-interaction
85+
5686
- name: Check reference code samples
87+
env:
88+
CODE_SAMPLES_PHP_DIR: ${{ runner.temp }}/php
5789
run: node tests/reference-code-samples.cjs
5890

5991
- name: Dry run release

‎build-docs.sh‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ node scripts/promote-request-examples.cjs "$OAS3_JSON" "$SAMPLES_JSON"
2525
# Convert OpenAPI to doc to Shins Markdown
2626
./node_modules/.bin/widdershins \
2727
--theme vs2015 \
28+
--user_templates templates/code-samples \
2829
--language_tabs shell:Curl http:HTTP javascript--nodejs:NodeJS php:PHP ruby:Ruby python:Python java:Java go:Go \
2930
--summary "$SAMPLES_JSON" \
3031
--outfile "$DOCS_DIR/index.html.md"
3132
rm -f "$SAMPLES_JSON"
3233

3334
cp "$DOCS_DIR/index.html.md" .shins/source/index.html.md
3435

35-
# Replace Serve, Ingest API URL's as overrides do not work
36-
sed -i -e 's/https:\/\/api.shotstack.io\/edit\/{version}\/assets/https:\/\/api.shotstack.io\/serve\/{version}\/assets/g' .shins/source/index.html.md
37-
sed -i -e 's/https:\/\/api.shotstack.io\/edit\/{version}\/sources/https:\/\/api.shotstack.io\/ingest\/{version}\/sources/g' .shins/source/index.html.md
36+
# Replace Serve, Ingest API URL's as overrides do not work. Matching the path, not the full URL, also
37+
# rewrites the HTTP samples' request lines.
38+
sed -i -e 's/\/edit\/{version}\/assets/\/serve\/{version}\/assets/g' .shins/source/index.html.md
39+
sed -i -e 's/\/edit\/{version}\/sources/\/ingest\/{version}\/sources/g' .shins/source/index.html.md
40+
sed -i -e 's/\/edit\/{version}\/upload/\/ingest\/{version}\/upload/g' .shins/source/index.html.md
3841

3942
# Build the Shins docs HTML
4043
cd .shins

‎templates/code-samples/code_go.dot‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
var body = data.bodyParameter.present;
6+
var reads = data.produces.length > 0;
7+
var verb = data.methodUpper.charAt(0) + data.methodUpper.slice(1).toLowerCase();
8+
var json = body ? JSON.stringify(data.bodyParameter.exampleValues.object, null, 2) : '';
9+
/* A raw string can't contain a backtick, so such a body falls back to an interpreted string. */
10+
var literal = json.indexOf('`') === -1 ? '`' + json + '`' : JSON.stringify(json);
11+
var imports = ['fmt'].concat(reads ? ['io'] : [], ['net/http', 'os'], body ? ['strings'] : []);
12+
var headers = sent.map(function (p) {
13+
var value = p.name.toLowerCase() === 'x-api-key' ? 'os.Getenv("SHOTSTACK_API_KEY")' : JSON.stringify(String(p.exampleValues.object));
14+
return '\treq.Header.Set("' + p.name + '", ' + value + ')';
15+
});
16+
var lines = ['package main', '', 'import ('].concat(imports.map(function (i) { return '\t"' + i + '"'; }), [')', '', 'func main() {']);
17+
if (body) lines.push('\tbody := strings.NewReader(' + literal + ')', '');
18+
lines.push('\treq, err := http.NewRequest(http.Method' + verb + ', "' + data.url + data.requiredQueryString + '", ' + (body ? 'body' : 'nil') + ')',
19+
'\tif err != nil {', '\t\tpanic(err)', '\t}', headers.join('\n'), '',
20+
'\tresp, err := http.DefaultClient.Do(req)', '\tif err != nil {', '\t\tpanic(err)', '\t}', '\tdefer resp.Body.Close()', '');
21+
if (reads) {
22+
lines.push('\tout, err := io.ReadAll(resp.Body)', '\tif err != nil {', '\t\tpanic(err)', '\t}', '\tfmt.Println(resp.Status, string(out))');
23+
} else {
24+
lines.push('\tfmt.Println(resp.Status)');
25+
}
26+
lines.push('}');
27+
var code = lines.join('\n');
28+
}}{{=code}}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
var path = data.url.replace(/^https?:\/\/[^\/]+/, '') + data.requiredQueryString;
6+
var lines = [data.methodUpper + ' ' + path + ' HTTP/1.1', 'Host: ' + data.host];
7+
sent.forEach(function (p) {
8+
lines.push(p.name + ': ' + (p.name.toLowerCase() === 'x-api-key' ? 'YOUR_API_KEY' : p.exampleValues.object));
9+
});
10+
var json = data.bodyParameter.present ? JSON.stringify(data.bodyParameter.exampleValues.object, null, 2) : '';
11+
if (json) lines.push('Content-Length: ' + Buffer.byteLength(json));
12+
var code = lines.join('\n') + (json ? '\n\n' + json : '');
13+
}}{{=code}}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
var body = data.bodyParameter.present;
6+
var verb = data.methodUpper;
7+
var publisher = body ? 'HttpRequest.BodyPublishers.ofString(body)' : 'HttpRequest.BodyPublishers.noBody()';
8+
var call = verb === 'GET' ? '.GET()' : verb === 'DELETE' && !body ? '.DELETE()'
9+
: verb === 'POST' || verb === 'PUT' ? '.' + verb + '(' + publisher + ')' : '.method("' + verb + '", ' + publisher + ')';
10+
var headers = sent.map(function (p) {
11+
var value = p.name.toLowerCase() === 'x-api-key' ? 'System.getenv("SHOTSTACK_API_KEY")' : JSON.stringify(String(p.exampleValues.object));
12+
return ' .header("' + p.name + '", ' + value + ')';
13+
});
14+
var lines = ['import java.net.URI;', 'import java.net.http.HttpClient;', 'import java.net.http.HttpRequest;',
15+
'import java.net.http.HttpResponse;', '', 'public class Main {', ' public static void main(String[] args) throws Exception {'];
16+
if (body) {
17+
/* Text blocks interpret backslashes, so JSON escapes are doubled to survive. */
18+
var json = JSON.stringify(data.bodyParameter.exampleValues.object, null, 2).replace(/\\/g, '\\\\');
19+
lines.push(' var body = """', json.split('\n').map(function (line) { return ' ' + line; }).join('\n'),
20+
' """;', '');
21+
}
22+
lines.push(' var request = HttpRequest.newBuilder(URI.create("' + data.url + data.requiredQueryString + '"))',
23+
headers.join('\n'), ' ' + call, ' .build();', '',
24+
' var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());',
25+
data.produces.length ? ' System.out.println(response.body());' : ' System.out.println(response.statusCode());',
26+
' }', '}');
27+
var code = lines.join('\n');
28+
}}{{=code}}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
var headers = sent.map(function (p) {
6+
var value = p.name.toLowerCase() === 'x-api-key' ? 'process.env.SHOTSTACK_API_KEY' : JSON.stringify(p.exampleValues.object);
7+
return ' "' + p.name + '": ' + value + ',';
8+
});
9+
var options = [];
10+
if (data.methodUpper !== 'GET') options.push(' method: "' + data.methodUpper + '",');
11+
options.push(' headers: {', headers.join('\n'), ' },');
12+
if (data.bodyParameter.present) options.push(' body: JSON.stringify(body),');
13+
var lines = [];
14+
if (data.bodyParameter.present) {
15+
lines.push('const body = ' + JSON.stringify(data.bodyParameter.exampleValues.object, null, 2) + ';', '');
16+
}
17+
lines.push('const response = await fetch("' + data.url + data.requiredQueryString + '", {', options.join('\n'), '});', '');
18+
lines.push(data.produces.length ? 'console.log(await response.json());' : 'console.log(response.status);');
19+
var code = lines.join('\n');
20+
}}{{=code}}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
/* An empty object becomes (object) [], because Guzzle would encode an empty PHP array as a JSON list. */
6+
var literal = function (value, indent) {
7+
var inner = indent + ' ';
8+
if (value === null) return 'null';
9+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
10+
if (typeof value === 'string') return "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
11+
var items = Array.isArray(value)
12+
? value.map(function (item) { return inner + literal(item, inner) + ','; })
13+
: Object.keys(value).map(function (key) { return inner + literal(key, '') + ' => ' + literal(value[key], inner) + ','; });
14+
if (!items.length) return Array.isArray(value) ? '[]' : '(object) []';
15+
return '[\n' + items.join('\n') + '\n' + indent + ']';
16+
};
17+
var body = data.bodyParameter.present;
18+
var headers = sent.filter(function (p) { return !(body && p.name === 'Content-Type'); }).map(function (p) {
19+
var value = p.name.toLowerCase() === 'x-api-key' ? "getenv('SHOTSTACK_API_KEY')" : literal(String(p.exampleValues.object), '');
20+
return ' ' + literal(p.name, '') + ' => ' + value + ',';
21+
});
22+
var lines = ['<?php', '', "require 'vendor/autoload.php';", '', 'use GuzzleHttp\\Client;', '', '$client = new Client();', '',
23+
"$response = $client->request('" + data.methodUpper + "', " + literal(data.url + data.requiredQueryString, '') + ', [',
24+
" 'headers' => [", headers.join('\n'), ' ],'];
25+
if (body) lines.push(" 'json' => " + literal(data.bodyParameter.exampleValues.object, ' ') + ',');
26+
lines.push(']);', '', data.produces.length ? 'echo $response->getBody(), PHP_EOL;' : 'echo $response->getStatusCode(), PHP_EOL;');
27+
var code = lines.join('\n');
28+
}}{{=code}}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
var literal = function (value, indent) {
6+
var inner = indent + ' ';
7+
if (value === null) return 'None';
8+
if (value === true) return 'True';
9+
if (value === false) return 'False';
10+
if (typeof value !== 'object') return JSON.stringify(value);
11+
var items = Array.isArray(value)
12+
? value.map(function (item) { return inner + literal(item, inner) + ','; })
13+
: Object.keys(value).map(function (key) { return inner + JSON.stringify(key) + ': ' + literal(value[key], inner) + ','; });
14+
var open = Array.isArray(value) ? '[' : '{';
15+
var close = Array.isArray(value) ? ']' : '}';
16+
return items.length ? open + '\n' + items.join('\n') + '\n' + indent + close : open + close;
17+
};
18+
var body = data.bodyParameter.present;
19+
var headers = sent.filter(function (p) { return !(body && p.name === 'Content-Type'); }).map(function (p) {
20+
var value = p.name.toLowerCase() === 'x-api-key' ? 'os.environ["SHOTSTACK_API_KEY"]' : JSON.stringify(p.exampleValues.object);
21+
return ' "' + p.name + '": ' + value + ',';
22+
});
23+
var lines = ['import os', '', 'import requests', ''];
24+
if (body) lines.push('body = ' + literal(data.bodyParameter.exampleValues.object, ''), '');
25+
lines.push('response = requests.' + data.method.verb + '(', ' "' + data.url + data.requiredQueryString + '",',
26+
' headers={', headers.join('\n'), ' },');
27+
if (body) lines.push(' json=body,');
28+
lines.push(' timeout=30,', ')', 'response.raise_for_status()',
29+
data.produces.length ? 'print(response.json())' : 'print(response.status_code)');
30+
var code = lines.join('\n');
31+
}}{{=code}}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
var literal = function (value, indent) {
6+
var inner = indent + ' ';
7+
if (value === null) return 'nil';
8+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
9+
if (typeof value === 'string') return "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
10+
var items = Array.isArray(value)
11+
? value.map(function (item) { return inner + literal(item, inner) + ','; })
12+
: Object.keys(value).map(function (key) {
13+
var label = /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? key + ':' : literal(key, '') + ' =>';
14+
return inner + label + ' ' + literal(value[key], inner) + ',';
15+
});
16+
var open = Array.isArray(value) ? '[' : '{';
17+
var close = Array.isArray(value) ? ']' : '}';
18+
return items.length ? open + '\n' + items.join('\n') + '\n' + indent + close : open + close;
19+
};
20+
var body = data.bodyParameter.present;
21+
var verb = data.methodUpper.charAt(0) + data.methodUpper.slice(1).toLowerCase();
22+
var headers = sent.map(function (p) {
23+
var value = p.name.toLowerCase() === 'x-api-key' ? "ENV.fetch('SHOTSTACK_API_KEY')" : literal(String(p.exampleValues.object), '');
24+
return ' ' + literal(p.name, '') + ' => ' + value + ',';
25+
});
26+
var lines = body ? ["require 'json'"] : [];
27+
lines.push("require 'net/http'", '', 'uri = URI(' + literal(data.url + data.requiredQueryString, '') + ')');
28+
if (body) lines.push('body = ' + literal(data.bodyParameter.exampleValues.object, ''));
29+
lines.push('', 'request = Net::HTTP::' + verb + '.new(uri, {', headers.join('\n'), '})');
30+
if (body) lines.push('request.body = body.to_json');
31+
lines.push('', "response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|",
32+
' http.request(request)', 'end', '', data.produces.length ? 'puts response.body' : 'puts response.code');
33+
var code = lines.join('\n');
34+
}}{{=code}}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{{
2+
/* Optional header parameters are left out: a sample would send their placeholder values. */
3+
var optional = (data.method.operation.parameters || []).filter(function (x) { return x.in === 'header' && !x.required; }).map(function (x) { return x.name; });
4+
var sent = data.allHeaders.filter(function (p) { return optional.indexOf(p.name) === -1; });
5+
var ref = data.bodyParameter.refName;
6+
var file = (ref ? ref.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() : 'body') + '.json';
7+
var args = sent.map(function (p) {
8+
var value = p.name.toLowerCase() === 'x-api-key' ? '$SHOTSTACK_API_KEY' : p.exampleValues.object;
9+
return '-H "' + p.name + ': ' + value + '"';
10+
});
11+
if (data.bodyParameter.present) args.push('-d @' + file);
12+
var method = data.methodUpper === 'GET' ? '' : '-X ' + data.methodUpper + ' ';
13+
var lines = ['curl ' + method + '"' + data.url + data.requiredQueryString + '"'].concat(args.map(function (a) { return ' ' + a; }));
14+
var code = (data.bodyParameter.present ? '# Save the body parameter below as ' + file + '\n' : '') + lines.join(' \\\n');
15+
}}{{=code}}

0 commit comments

Comments
 (0)