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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,39 @@ jobs:
pnpm build
diff -r "$RUNNER_TEMP/schemas-before-docs" dist

# Every code sample is run, so CI needs each sample language's runtime.
- uses: actions/setup-python@v5
with:
python-version: '3.12'

- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer

- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: false

- name: Install sample libraries
run: |
pip install requests
mkdir -p "$RUNNER_TEMP/php"
composer require guzzlehttp/guzzle:^7 --working-dir "$RUNNER_TEMP/php" --no-interaction

- name: Check reference code samples
env:
CODE_SAMPLES_PHP_DIR: ${{ runner.temp }}/php
run: node tests/reference-code-samples.cjs

- name: Dry run release
Expand Down
9 changes: 6 additions & 3 deletions build-docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ node scripts/promote-request-examples.cjs "$OAS3_JSON" "$SAMPLES_JSON"
# Convert OpenAPI to doc to Shins Markdown
./node_modules/.bin/widdershins \
--theme vs2015 \
--user_templates templates/code-samples \
--language_tabs shell:Curl http:HTTP javascript--nodejs:NodeJS php:PHP ruby:Ruby python:Python java:Java go:Go \
--summary "$SAMPLES_JSON" \
--outfile "$DOCS_DIR/index.html.md"
rm -f "$SAMPLES_JSON"

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

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

# Build the Shins docs HTML
cd .shins
Expand Down
32 changes: 32 additions & 0 deletions templates/code-samples/code_go.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{#def.sample}}package main

import (
"fmt"
{{? sample.returnsJson }} "io"
{{?}} "net/http"
"os"
{{? sample.hasBody }} "strings"
{{?}})

func main() {
{{? sample.hasBody }} body := strings.NewReader({{= sample.goBody }})

{{?}} req, err := http.NewRequest(http.Method{{= sample.verb }}, "{{= sample.url }}", {{= sample.hasBody ? 'body' : 'nil' }})
if err != nil {
panic(err)
}
{{~ sample.headers('os.Getenv("SHOTSTACK_API_KEY")', JSON.stringify) :h }} req.Header.Set("{{= h.name }}", {{= h.value }})
{{~}}
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()

{{? sample.returnsJson }} out, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(resp.Status, string(out))
{{??}} fmt.Println(resp.Status)
{{?}}}
6 changes: 6 additions & 0 deletions templates/code-samples/code_http.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{#def.sample}}{{= sample.method }} {{= sample.path }} HTTP/1.1
Host: {{= data.host }}{{~ sample.headers('YOUR_API_KEY', sample.raw) :h }}
{{= h.name }}: {{= h.value }}{{~}}{{? sample.hasBody }}
Content-Length: {{= Buffer.byteLength(sample.json) }}

{{= sample.json }}{{?}}
20 changes: 20 additions & 0 deletions templates/code-samples/code_java.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{#def.sample}}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
public static void main(String[] args) throws Exception {
{{? sample.hasBody }} var body = """
{{= sample.javaTextBlock }}
""";

{{?}} var request = HttpRequest.newBuilder(URI.create("{{= sample.url }}"))
{{~ sample.headers('System.getenv("SHOTSTACK_API_KEY")', JSON.stringify) :h }} .header("{{= h.name }}", {{= h.value }})
{{~}} .{{= sample.javaMethod }}
.build();

var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println({{= sample.returnsJson ? 'response.body()' : 'response.statusCode()' }});
}
}
11 changes: 11 additions & 0 deletions templates/code-samples/code_nodejs.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{#def.sample}}{{? sample.hasBody }}const body = {{= sample.json }};

{{?}}const response = await fetch("{{= sample.url }}", {
{{? sample.method !== 'GET' }} method: "{{= sample.method }}",
{{?}} headers: {
{{~ sample.headers('process.env.SHOTSTACK_API_KEY', JSON.stringify) :h }} "{{= h.name }}": {{= h.value }},
{{~}} },
{{? sample.hasBody }} body: JSON.stringify(body),
{{?}}});

console.log({{= sample.returnsJson ? 'await response.json()' : 'response.status' }});
16 changes: 16 additions & 0 deletions templates/code-samples/code_php.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{#def.sample}}<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client();

$response = $client->request('{{= sample.method }}', {{= sample.single(sample.url) }}, [
'headers' => [
{{~ sample.headers("getenv('SHOTSTACK_API_KEY')", sample.single, true) :h }} {{= sample.single(h.name) }} => {{= h.value }},
{{~}} ],
{{? sample.hasBody }} 'json' => {{= sample.literal(sample.body, sample.php, ' ') }},
{{?}}]);

echo {{= sample.returnsJson ? '$response->getBody()' : '$response->getStatusCode()' }}, PHP_EOL;
16 changes: 16 additions & 0 deletions templates/code-samples/code_python.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{#def.sample}}import os

import requests

{{? sample.hasBody }}body = {{= sample.literal(sample.body, sample.python) }}

{{?}}response = requests.{{= data.method.verb }}(
"{{= sample.url }}",
headers={
{{~ sample.headers('os.environ["SHOTSTACK_API_KEY"]', JSON.stringify, true) :h }} "{{= h.name }}": {{= h.value }},
{{~}} },
{{? sample.hasBody }} json=body,
{{?}} timeout=30,
)
response.raise_for_status()
print({{= sample.returnsJson ? 'response.json()' : 'response.status_code' }})
16 changes: 16 additions & 0 deletions templates/code-samples/code_ruby.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{#def.sample}}{{? sample.hasBody }}require 'json'
{{?}}require 'net/http'

uri = URI({{= sample.single(sample.url) }})
{{? sample.hasBody }}body = {{= sample.literal(sample.body, sample.ruby) }}
{{?}}
request = Net::HTTP::{{= sample.verb }}.new(uri, {
{{~ sample.headers("ENV.fetch('SHOTSTACK_API_KEY')", sample.single) :h }} {{= sample.single(h.name) }} => {{= h.value }},
{{~}}})
{{? sample.hasBody }}request.body = body.to_json
{{?}}
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end

puts {{= sample.returnsJson ? 'response.body' : 'response.code' }}
4 changes: 4 additions & 0 deletions templates/code-samples/code_shell.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{#def.sample}}{{? sample.hasBody }}# Save the body parameter below as {{= sample.bodyFile }}
{{?}}curl {{? sample.method !== 'GET' }}-X {{= sample.method }} {{?}}"{{= sample.url }}"{{~ sample.headers('$SHOTSTACK_API_KEY', sample.raw) :h }} \
-H "{{= h.name }}: {{= h.value }}"{{~}}{{? sample.hasBody }} \
-d @{{= sample.bodyFile }}{{?}}
74 changes: 74 additions & 0 deletions templates/code-samples/sample.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{{
/* Shared by the code_*.dot templates, which include it at their very start so it adds no output.
The templates read like the samples they produce; anything computed lives here. */
var sample = {};
sample.method = data.methodUpper;
sample.verb = data.methodUpper.charAt(0) + data.methodUpper.slice(1).toLowerCase();
sample.url = data.url + data.requiredQueryString;
sample.path = sample.url.replace(/^https?:\/\/[^\/]+/, '');
sample.hasBody = !!data.bodyParameter.present;
sample.body = data.bodyParameter.exampleValues.object;
sample.json = sample.hasBody ? JSON.stringify(sample.body, null, 2) : '';
sample.returnsJson = data.produces.length > 0;
var ref = data.bodyParameter.refName;
sample.bodyFile = (ref ? ref.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() : 'body') + '.json';

sample.raw = function (text) { return text; };
sample.single = function (text) { return "'" + text.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"; };

/* Optional header parameters are left out: a sample would send their placeholder values. */
var optional = (data.method.operation.parameters || [])
.filter(function (p) { return p.in === 'header' && !p.required; })
.map(function (p) { return p.name; });
sample.headers = function (apiKey, quote, dropContentType) {
return data.allHeaders
.filter(function (p) { return optional.indexOf(p.name) === -1; })
.filter(function (p) { return !(dropContentType && sample.hasBody && p.name === 'Content-Type'); })
.map(function (p) {
var value = p.name.toLowerCase() === 'x-api-key' ? apiKey : quote(String(p.exampleValues.object));
return { name: p.name, value: value };
});
};

/* Renders a JSON value in a language's literal syntax. */
sample.literal = function (value, syntax, indent) {
indent = indent || '';
var inner = indent + syntax.indent;
if (value === null) return syntax.nil;
if (typeof value === 'boolean') return syntax.bool(value);
if (typeof value === 'number') return String(value);
if (typeof value === 'string') return syntax.string(value);
var list = Array.isArray(value);
var items = list
? value.map(function (item) { return inner + sample.literal(item, syntax, inner) + ','; })
: Object.keys(value).map(function (key) { return inner + syntax.key(key) + sample.literal(value[key], syntax, inner) + ','; });
var brackets = list ? syntax.list : syntax.map;
if (!items.length) return list ? brackets.join('') : syntax.emptyMap;
return brackets[0] + '\n' + items.join('\n') + '\n' + indent + brackets[1];
};
sample.python = {
nil: 'None', bool: function (b) { return b ? 'True' : 'False'; }, string: JSON.stringify,
key: function (k) { return JSON.stringify(k) + ': '; }, list: ['[', ']'], map: ['{', '}'], emptyMap: '{}', indent: ' '
};
/* An empty object is (object) [], because Guzzle would encode an empty PHP array as a JSON list. */
sample.php = {
nil: 'null', bool: String, string: sample.single,
key: function (k) { return sample.single(k) + ' => '; }, list: ['[', ']'], map: ['[', ']'], emptyMap: '(object) []', indent: ' '
};
sample.ruby = {
nil: 'nil', bool: String, string: sample.single,
key: function (k) { return /^[A-Za-z_][A-Za-z0-9_]*$/.test(k) ? k + ': ' : sample.single(k) + ' => '; },
list: ['[', ']'], map: ['{', '}'], emptyMap: '{}', indent: ' '
};

/* Java text blocks interpret backslashes, so JSON escapes are doubled to survive. */
sample.javaTextBlock = sample.json.replace(/\\/g, '\\\\').split('\n').map(function (line) { return ' ' + line; }).join('\n');
var publisher = sample.hasBody ? 'HttpRequest.BodyPublishers.ofString(body)' : 'HttpRequest.BodyPublishers.noBody()';
sample.javaMethod = sample.method === 'GET' ? 'GET()'
: sample.method === 'DELETE' && !sample.hasBody ? 'DELETE()'
: sample.method === 'POST' || sample.method === 'PUT' ? sample.method + '(' + publisher + ')'
: 'method("' + sample.method + '", ' + publisher + ')';

/* A Go raw string can't contain a backtick, so such a body falls back to an interpreted string. */
sample.goBody = sample.json.indexOf('`') === -1 ? '`' + sample.json + '`' : JSON.stringify(sample.json);
}}
Loading
Loading