API tooling (#3033)
* Updates to API generation and tooling - Added build script and build instructions for API files - Updated `.gitignore` to keep node_module folders out, wherever they occur - Rewrite existing YAML files to use new component system - Compile rewritten YAML files - Add more validation checks to the route builders and dispatchers - Add a user-facing documentation page with interactive view using RapiDoc - Add a generic 404 handler for missing routes under `/api`, which returns JSON instead of HTML - Clean up spec output route slightly to make it valid OpenAPI 3.0.0 * Auto-fill API Key in docs Co-authored-by: Cocoa <momijizukamori@gmail.com>
This commit is contained in:
parent
b9ddeb84fe
commit
a0ca65ee1b
|
@ -15,6 +15,9 @@ src/proxy/proxy
|
|||
.vstags
|
||||
.perl-cpm
|
||||
|
||||
# Ignore node_modules, wherever they occur
|
||||
/**/node_modules/
|
||||
|
||||
# Ignore SCSS cache
|
||||
.sass-cache
|
||||
# Ignore compiled CSS
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
This folder contains the YAML files used to generate and validate [OpenAPI](https://openapis.org) routes for Dreamwidth, and to build the spec file supplied to end users. `src` contains the files you should edit - reusable components should go into `src\components\` and can then be referenced using JS Schema reference notation (eg, `$ref: components/schemas/username.yaml`). This cuts down on items that need to be retyped, and keeps descriptions of items consistent across different endpoints. `dist` contains the compiled YAML files that are used by the Perl endpoint controllers. Because YAML has no mechanism for file includes, there is unfortunately still a manual step required to rebuild the `dist` files when the `src` files are changed. First install node and then the necessary packages (`npm install` from inside this folder), and then run the `build.js` file (`node build.js`). This will compile the YAML files, and print any errors encountered along the way to the terminal.
|
|
@ -0,0 +1,36 @@
|
|||
const $RefParser = require("@apidevtools/json-schema-ref-parser");
|
||||
const YAML = require('yaml');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
async function* walk(dir) {
|
||||
for await (const d of await fs.promises.opendir(dir)) {
|
||||
const entry = path.join(dir, d.name);
|
||||
if (d.isDirectory()) yield* walk(entry);
|
||||
else if (d.isFile()) yield entry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function main() {
|
||||
for await (const p of walk('src/')) {
|
||||
let out_path = p.replace('src/', 'dist/')
|
||||
$RefParser.dereference(p, (err, schema) => {
|
||||
if (err) {
|
||||
console.log(p)
|
||||
console.error(err);
|
||||
}
|
||||
else {
|
||||
// console.log(YAML.stringify(schema));
|
||||
fs.writeFile(out_path, YAML.stringify(schema), err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
// file written successfully
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,7 @@
|
|||
paths:
|
||||
/comments/screening:
|
||||
get:
|
||||
description: Returns descriptions of all possible comment screening options.
|
||||
responses:
|
||||
"200":
|
||||
description: A list of comment screening options and their descriptions.
|
|
@ -0,0 +1,8 @@
|
|||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: A description of the error encountered.
|
||||
example: "Bad format for username. Errors: String is too long: 77/25."
|
||||
success:
|
||||
type: number
|
|
@ -0,0 +1,12 @@
|
|||
description: Bad or missing request parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: A description of the error encountered.
|
||||
example: "Bad format for username. Errors: String is too long: 77/25."
|
||||
success:
|
||||
type: number
|
|
@ -0,0 +1,12 @@
|
|||
description: Username specified does not exist.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: A description of the error encountered.
|
||||
example: "Bad format for username. Errors: String is too long: 77/25."
|
||||
success:
|
||||
type: number
|
|
@ -0,0 +1,21 @@
|
|||
type: object
|
||||
required:
|
||||
- comment
|
||||
- picid
|
||||
- username
|
||||
- url
|
||||
- keywords
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
picid:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
description: The name of the journal this icon belongs to.
|
||||
url:
|
||||
type: string
|
||||
keywords:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
|
@ -0,0 +1,5 @@
|
|||
type: string
|
||||
minLength: 3
|
||||
maxLength: 25
|
||||
pattern: ^[0-9A-Za-z_]+$
|
||||
example: example
|
|
@ -0,0 +1,65 @@
|
|||
paths:
|
||||
"/users/{username}/icons/{picid}":
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: The username you want icon information for
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 25
|
||||
pattern: ^[0-9A-Za-z_]+$
|
||||
example: example
|
||||
- name: picid
|
||||
in: path
|
||||
description: The picid you want information for.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
get:
|
||||
description: Returns a single icon for a specified picid and username
|
||||
responses:
|
||||
"200":
|
||||
description: An icon with it's information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- comment
|
||||
- picid
|
||||
- username
|
||||
- url
|
||||
- keywords
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
picid:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
description: The name of the journal this icon belongs to.
|
||||
url:
|
||||
type: string
|
||||
keywords:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
"400":
|
||||
description: Bad or missing request parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
&a1
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: A description of the error encountered.
|
||||
example: "Bad format for username. Errors: String is too long: 77/25."
|
||||
success:
|
||||
type: number
|
||||
"404":
|
||||
description: No such username or icon.
|
||||
schema: *a1
|
|
@ -0,0 +1,57 @@
|
|||
paths:
|
||||
"/users/{username}/icons":
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: The username you want icon information for
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 25
|
||||
pattern: ^[0-9A-Za-z_]+$
|
||||
example: example
|
||||
get:
|
||||
description: Returns all icons for a specified username.
|
||||
responses:
|
||||
"200":
|
||||
description: a list of icons
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- comment
|
||||
- picid
|
||||
- username
|
||||
- url
|
||||
- keywords
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
picid:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
description: The name of the journal this icon belongs to.
|
||||
url:
|
||||
type: string
|
||||
keywords:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
"404":
|
||||
description: Username specified does not exist.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: A description of the error encountered.
|
||||
example: "Bad format for username. Errors: String is too long: 77/25."
|
||||
success:
|
||||
type: number
|
|
@ -0,0 +1,7 @@
|
|||
paths:
|
||||
/spec:
|
||||
get:
|
||||
description: Returns the API specification
|
||||
responses:
|
||||
"200":
|
||||
description: This API specification!
|
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
paths:
|
||||
/users/{username}/icons:
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: The username you want icon information for
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
description: Returns all icons for a specified username.
|
||||
responses:
|
||||
200:
|
||||
description: a list of icons
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- comment
|
||||
- picid
|
||||
- username
|
||||
- url
|
||||
- keywords
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
picid:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
keywords:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"name": "dreamwidth",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dreamwidth",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^9.0.9",
|
||||
"yaml": "^2.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz",
|
||||
"integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==",
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/call-me-maybe": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
|
||||
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz",
|
||||
"integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==",
|
||||
"requires": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
|
||||
},
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"call-me-maybe": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"yaml": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
|
||||
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg=="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^9.0.9",
|
||||
"yaml": "^2.1.3"
|
||||
},
|
||||
"name": "dreamwidth",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build:json": "boats -i ./src/index.yml.njk -o ./build/${npm_package_name}.json",
|
||||
"build:yaml": "boats -i ./src/index.yml.njk -o ./build/${npm_package_name}.yml",
|
||||
"build": "npm run build:json && npm run build:yaml"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"description": "",
|
||||
"private": true
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: A description of the error encountered.
|
||||
example: "Bad format for username. Errors: String is too long: 77/25."
|
||||
success:
|
||||
type: number
|
|
@ -0,0 +1,5 @@
|
|||
description: Bad or missing request parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../error.yaml
|
|
@ -0,0 +1,5 @@
|
|||
description: Username specified does not exist.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../error.yaml
|
|
@ -0,0 +1,21 @@
|
|||
type: object
|
||||
required:
|
||||
- comment
|
||||
- picid
|
||||
- username
|
||||
- url
|
||||
- keywords
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
picid:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
description: The name of the journal this icon belongs to.
|
||||
url:
|
||||
type: string
|
||||
keywords:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
|
@ -0,0 +1,5 @@
|
|||
type: string
|
||||
minLength: 3
|
||||
maxLength: 25
|
||||
pattern: "^[0-9A-Za-z_]+$"
|
||||
example: example
|
|
@ -6,8 +6,8 @@ paths:
|
|||
in: path
|
||||
description: The username you want icon information for
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
schema:
|
||||
$ref: components/schemas/username.yaml
|
||||
- name: picid
|
||||
in: path
|
||||
description: The picid you want information for.
|
||||
|
@ -21,20 +21,11 @@ paths:
|
|||
description: An icon with it's information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
picid:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
keywords:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
schema:
|
||||
$ref: components/schemas/icon.yaml
|
||||
404:
|
||||
description: No such icon.
|
||||
description: No such username or icon.
|
||||
schema:
|
||||
$ref: components/error.yaml
|
||||
400:
|
||||
$ref: components/errors/400.yaml
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
paths:
|
||||
/users/{username}/icons:
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: The username you want icon information for
|
||||
required: true
|
||||
schema:
|
||||
$ref: components/schemas/username.yaml
|
||||
get:
|
||||
description: Returns all icons for a specified username.
|
||||
responses:
|
||||
200:
|
||||
description: a list of icons
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: components/schemas/icon.yaml
|
||||
404:
|
||||
$ref: components/errors/404-user.yaml
|
|
@ -969,6 +969,7 @@ sub trans {
|
|||
uri => "/v$ver$2",
|
||||
role => 'api'
|
||||
);
|
||||
$ret //= DW::Routing->call( uri => "/internal/api/404" );
|
||||
return $ret if defined $ret;
|
||||
}
|
||||
|
||||
|
|
|
@ -174,4 +174,22 @@ sub hash {
|
|||
return $_[0]->{keyhash};
|
||||
}
|
||||
|
||||
# Usage: get_one (user)
|
||||
# Given a user, either return the first found key for them, or
|
||||
# if they have no keys yet, generate one. Intended for use in
|
||||
# situations where we have a logged in user and want to get a working API
|
||||
# key for them, without forcing them to jump through the menu hoops themselves.
|
||||
sub get_one {
|
||||
my ( $self, $u ) = @_;
|
||||
my $apikeys = $self->get_keys_for_user($u);
|
||||
my $key;
|
||||
|
||||
if (defined($apikeys->[0])) {
|
||||
$key = $apikeys ->[0];
|
||||
} else {
|
||||
$key = $self->new_for_user($u);
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -66,9 +66,12 @@ sub param {
|
|||
# Creates a special instance of DW::API::Parameter object and
|
||||
# adds it as the requestBody definition for the calling method
|
||||
sub body {
|
||||
my ( $self, @args ) = @_;
|
||||
my $param = DW::API::Parameter->define_parameter(@args);
|
||||
$self->{requestBody} = $param;
|
||||
my ( $self, $config ) = @_;
|
||||
$self->{requestBody}->{required} = $config->{required};
|
||||
for my $ct ( keys( $config->{content} ) ) {
|
||||
my $param = DW::API::Parameter->define_body( $config->{content}->{$ct}, $ct );
|
||||
$self->{requestBody}{content}{$ct} = $param;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -197,12 +200,24 @@ sub TO_JSON {
|
|||
$json->{parameters} = [ values %{ $self->{params} } ];
|
||||
}
|
||||
|
||||
if ( defined $self->{requestBody} ) {
|
||||
$json->{requestBody} = $self->{requestBody};
|
||||
if ( defined $self->{requestBody}{required} && $self->{requestBody}{required} ) {
|
||||
$json->{requestBody}{required} = $JSON::true;
|
||||
}
|
||||
else {
|
||||
delete $json->{requestBody}{required};
|
||||
}
|
||||
}
|
||||
|
||||
my $responses = $self->{responses};
|
||||
|
||||
for my $key ( keys %{ $self->{responses} } ) {
|
||||
$json->{responses}{$key} = { description => $responses->{$key}{desc} };
|
||||
$json->{responses}{$key}{schema} = $responses->{$key}{schema}
|
||||
if defined $responses->{$key}{schema};
|
||||
for my $return_type ( keys %{ $self->{responses}{$key}{content} } ) {
|
||||
$json->{responses}{$key}{content}{$return_type}{schema} = $responses->{$key}{content}{$return_type}{schema}
|
||||
if defined $responses->{$key}{content}{$return_type}{schema};
|
||||
}
|
||||
}
|
||||
|
||||
return $json;
|
||||
|
|
|
@ -46,24 +46,37 @@ sub define_parameter {
|
|||
}
|
||||
elsif ( defined $args->{content} ) {
|
||||
$parameter->{content} = $args->{content};
|
||||
$parameter->{in} = 'requestBody';
|
||||
}
|
||||
|
||||
bless $parameter, $class;
|
||||
$parameter->_validate;
|
||||
$parameter->_validate_json;
|
||||
return $parameter;
|
||||
|
||||
}
|
||||
|
||||
sub define_body {
|
||||
my ( $class, $args, $content ) = @_;
|
||||
my $parameter = { in => 'requestBody', };
|
||||
|
||||
if ( defined $args->{schema} ) {
|
||||
$parameter->{schema} = $args->{schema};
|
||||
}
|
||||
bless $parameter, $class;
|
||||
if ( $content eq 'application/json' ) {
|
||||
$parameter->_validate_json;
|
||||
return $parameter;
|
||||
}
|
||||
}
|
||||
|
||||
# Usage: validate ( Parameter object )
|
||||
# Does some simple validation checks for parameter objects
|
||||
# Makes sure required fields are present, and that the
|
||||
# location given is a valid one.
|
||||
|
||||
sub _validate {
|
||||
sub _validate_json {
|
||||
my $self = $_[0];
|
||||
for my $field (@REQ_ATTRIBUTES) {
|
||||
croak "$self is missing required field $field" unless defined $self->{$field};
|
||||
}
|
||||
|
||||
my $location = $self->{in};
|
||||
croak "$location isn't a valid parameter location" unless grep( $location, @LOCATIONS );
|
||||
|
||||
|
@ -73,13 +86,6 @@ sub _validate {
|
|||
croak "Can only define one of content or schema!" if $has_schema && $has_content;
|
||||
croak "Must define at least one of content or schema!" unless $has_content || $has_schema;
|
||||
|
||||
# requestBody is a special instance of Parameter and has stricter rules
|
||||
if ( $location eq "requestBody" ) {
|
||||
if ( not defined( keys %{ $self->{content} } ) ) {
|
||||
croak "requestBody must have at least one content-type!";
|
||||
}
|
||||
}
|
||||
|
||||
# Run schema validators
|
||||
DW::Controller::API::REST::schema($self) if ( defined $self->{schema} );
|
||||
|
||||
|
@ -102,8 +108,11 @@ sub TO_JSON {
|
|||
in => $self->{in},
|
||||
};
|
||||
|
||||
# Schema fields we need to force to be numeric
|
||||
|
||||
if ( defined $self->{schema} ) {
|
||||
$json->{schema} = $self->{schema};
|
||||
force_numeric( $json->{schema} );
|
||||
}
|
||||
elsif ( defined $self->{content} ) {
|
||||
$json->{content} = $self->{content};
|
||||
|
@ -111,13 +120,41 @@ sub TO_JSON {
|
|||
# content type is just a hash, but we don't want to print the validator too
|
||||
for my $content_type ( keys %{ $json->{content} } ) {
|
||||
delete $json->{content}->{$content_type}{validator};
|
||||
force_numeric( $json->{content}->{$content_type}{schema} );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $self->{in} eq "requestBody" ) {
|
||||
|
||||
#remove some fields that requestBody doesn't need
|
||||
delete $json->{in};
|
||||
delete $json->{name};
|
||||
delete $json->{description};
|
||||
}
|
||||
|
||||
$json->{required} = $JSON::true if defined $self->{required} && $self->{required};
|
||||
return $json;
|
||||
|
||||
}
|
||||
|
||||
sub force_numeric {
|
||||
my $schema = $_[0];
|
||||
my @numerics = ( 'minLength', 'maxLength', 'minimum', 'maximum', 'minItems', 'maxItems' );
|
||||
|
||||
if ( $schema->{type} eq 'object' ) {
|
||||
for my $prop ( keys %{ $schema->{properties} } ) {
|
||||
force_numeric( $schema->{properties}{$prop} );
|
||||
}
|
||||
}
|
||||
elsif ( $schema->{type} eq 'array' ) {
|
||||
force_numeric( $schema->{items} );
|
||||
}
|
||||
else {
|
||||
foreach my $item (@numerics) {
|
||||
$schema->{$item} += 0 if defined( $schema->{$item} );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ our %TYPE_REGEX = (
|
|||
boolean => '(true|false)',
|
||||
);
|
||||
our %METHODS = ( get => 1, post => 1, delete => 1 );
|
||||
our $API_PATH = "$ENV{LJHOME}/api/";
|
||||
our $API_PATH = "$ENV{LJHOME}/api/dist/";
|
||||
|
||||
# Usage: path ( yaml_source_path, ver, hash_of_HTTP_handlers )
|
||||
# Creates a new path object for use in DW::Controller::API::REST
|
||||
|
@ -146,11 +146,14 @@ sub _dispatcher {
|
|||
|
||||
my $r = $rv->{r};
|
||||
my $keystr = $r->header_in('Authorization');
|
||||
$keystr =~ s/Bearer (\w+)/$1/;
|
||||
my $apikey = DW::API::Key->get_key($keystr);
|
||||
my $apikey;
|
||||
if ( defined($keystr) ) {
|
||||
$keystr =~ s/Bearer (\w+)/$1/;
|
||||
$apikey = DW::API::Key->get_key($keystr);
|
||||
}
|
||||
|
||||
# all paths require an API key except the spec (which informs users that they need a key and where to put it)
|
||||
unless ( $apikey || $self->{path}{name} eq "/spec" ) {
|
||||
unless ( defined($apikey) || $self->{path}{name} eq "/spec" ) {
|
||||
$r->print( to_json( { success => 0, error => "Missing or invalid API key" } ) );
|
||||
$r->status('401');
|
||||
return;
|
||||
|
@ -169,21 +172,24 @@ sub _dispatcher {
|
|||
|
||||
# check path-level parameters.
|
||||
for my $param ( keys %{ $self->{path}{params} } ) {
|
||||
_validate_param( $param, $self->{path}{params}{$param}, $r, $path_params, $args );
|
||||
my $valid =
|
||||
_validate_param( $param, $self->{path}{params}{$param}, $r, $path_params, $args );
|
||||
return unless $valid;
|
||||
}
|
||||
|
||||
my $method = lc $r->method;
|
||||
my $handler = $self->{path}{methods}->{$method}->{handler};
|
||||
my $method_self = $self->{path}{methods}->{$method};
|
||||
|
||||
# check method-level parameters
|
||||
for my $param ( keys %{ $method_self->{params} } ) {
|
||||
_validate_param( $param, $self->{params}{$param}, $r, $args );
|
||||
my $valid = _validate_param( $param, $method_self->{params}{$param}, $r, undef, $args );
|
||||
return unless $valid;
|
||||
}
|
||||
|
||||
# if we accept a request body, validate that too.
|
||||
if ( defined $method_self->{requestBody} ) {
|
||||
_validate_body( $method_self->{requestBody}, $r, $args );
|
||||
my $valid = _validate_body( $method_self->{requestBody}, $r, $args );
|
||||
return unless $valid;
|
||||
}
|
||||
|
||||
# some handlers need to know what version they are
|
||||
|
@ -196,7 +202,7 @@ sub _dispatcher {
|
|||
# Generic response for unimplemented API methods.
|
||||
$r->print( to_json( { success => 0, error => "Not Implemented" } ) );
|
||||
$r->status('501');
|
||||
return;
|
||||
return $r->OK;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,10 +241,14 @@ sub _validate_param {
|
|||
unless ( defined $p ) {
|
||||
$r->print( to_json( { success => 0, error => "Missing required parameter $param" } ) );
|
||||
$r->status('400');
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
# non-required parameters may be undef without it being an error
|
||||
# but we shouldn't try to validate them if they're undef.
|
||||
return 1 unless ( defined $p );
|
||||
|
||||
# run the schema validator
|
||||
my @errors = $pval->validate($p);
|
||||
if (@errors) {
|
||||
|
@ -246,10 +256,11 @@ sub _validate_param {
|
|||
$r->print(
|
||||
to_json( { success => 0, error => "Bad format for $param. Errors: $err_str" } ) );
|
||||
$r->status('400');
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$arg_obj->{$ploc}{$param} = $p;
|
||||
return 1;
|
||||
}
|
||||
|
||||
# Usage: _validate_body (requestBody config, request, arg object)
|
||||
|
@ -263,9 +274,9 @@ sub _validate_param {
|
|||
|
||||
sub _validate_body {
|
||||
my ( $config, $r, $arg_obj ) = @_;
|
||||
|
||||
my $preq = $config->{required};
|
||||
my $content_type = lc $r->header_in('Content-Type');
|
||||
$content_type =~ s/;.*//; # drop data that isn't the MIMEtype
|
||||
my $p;
|
||||
|
||||
if ( $content_type eq 'application/json' ) {
|
||||
|
@ -284,6 +295,9 @@ sub _validate_body {
|
|||
}
|
||||
$p = $upload_hash;
|
||||
}
|
||||
else {
|
||||
warn "Unexpected content-type $content_type";
|
||||
}
|
||||
|
||||
# make sure that required parameters are supplied
|
||||
if ($preq) {
|
||||
|
@ -291,21 +305,26 @@ sub _validate_body {
|
|||
$r->print(
|
||||
to_json( { success => 0, error => "Missing or badly formatted request!" } ) );
|
||||
$r->status('400');
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
# non-required parameters may be undef without it being an error
|
||||
# but we shouldn't try to validate them if they're undef.
|
||||
#return 1 unless ( defined $p && defined($config->{content}->{$content_type}{validator}));
|
||||
|
||||
# run the schema validator
|
||||
my @errors = $config->{content}->{$content_type}{validator}->validate($p);
|
||||
my @errors = $config->{content}{$content_type}{validator}->validate($p);
|
||||
if (@errors) {
|
||||
my $err_str = join( ', ', map { $_->{message} } @errors );
|
||||
$r->print(
|
||||
to_json( { success => 0, error => "Bad format for request body. Errors: $err_str" } ) );
|
||||
$r->status('400');
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$arg_obj->{body} = $p;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
# Usage: schema ($object_ref)
|
||||
|
@ -354,4 +373,77 @@ sub TO_JSON {
|
|||
return $json;
|
||||
}
|
||||
|
||||
sub params {
|
||||
my $self = $_[0];
|
||||
my $parameters = [ values %{ $self->{path}{params} } ];
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
sub methods {
|
||||
my $self = $_[0];
|
||||
my $methods = $self->{path}{methods};
|
||||
return $methods;
|
||||
}
|
||||
|
||||
sub to_template {
|
||||
my $self = $_[0];
|
||||
my $parameters = [ values %{ $self->{path}{params} } ];
|
||||
my $methods = $self->{path}{methods};
|
||||
my $vars = {
|
||||
params => $parameters,
|
||||
methods => $methods
|
||||
};
|
||||
return DW::Template->render_template( 'api/path.tt', $vars, { no_sitescheme => 1 } );
|
||||
|
||||
}
|
||||
|
||||
DW::Routing->register_string( '/api', \&api_handler, app => 1 );
|
||||
DW::Routing->register_string( '/api/', \&api_handler, app => 1 );
|
||||
|
||||
sub api_handler {
|
||||
my ( $ok, $rv ) = controller();
|
||||
return $rv unless $ok;
|
||||
my $r = $rv->{r};
|
||||
my $u = $rv->{u};
|
||||
my $remote = $rv->{remote};
|
||||
|
||||
my %api = %API_DOCS;
|
||||
|
||||
my $paths = $api{1};
|
||||
my $vars;
|
||||
$vars->{paths} = $paths;
|
||||
$vars->{key} = DW::API::Key->get_one($remote);
|
||||
|
||||
return DW::Template->render_template( 'api.tt', $vars );
|
||||
}
|
||||
|
||||
DW::Routing->register_string( '/api/getkey', \&key_handler, app => 1 );
|
||||
|
||||
sub key_handler {
|
||||
my ( $ok, $rv ) = controller();
|
||||
return $rv unless $ok;
|
||||
my $r = $rv->{r};
|
||||
my $remote = $rv->{remote};
|
||||
|
||||
my $key = DW::API::Key->get_one($remote);
|
||||
|
||||
$r->status(200);
|
||||
$r->content_type('text/plain; charset=utf-8');
|
||||
$r->print( $key->{keyhash} );
|
||||
return $r->OK;
|
||||
}
|
||||
|
||||
DW::Routing->register_string( '/internal/api/404', \&api_404_handler, app => 1 );
|
||||
|
||||
sub api_404_handler {
|
||||
my ( $ok, $rv ) = controller( anonymous => 1 );
|
||||
return $rv unless $ok;
|
||||
my $r = $rv->{r};
|
||||
|
||||
$r->status(404);
|
||||
$r->content_type('application/json; charset=utf-8');
|
||||
$r->print( to_json( { success => 0, error => "Not found." } ) );
|
||||
return $r->OK;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -27,6 +27,7 @@ sub rest_get {
|
|||
my ( $self, $args ) = @_;
|
||||
|
||||
my $u = LJ::load_user( $args->{path}{username} );
|
||||
return $self->rest_error("404") unless defined $u;
|
||||
|
||||
# if we're given a picid, try to load that userpic
|
||||
if ( defined( $args->{path}{picid} ) && $args->{path}{picid} ne "" ) {
|
||||
|
|
|
@ -42,22 +42,25 @@ sub _spec_20 {
|
|||
|
||||
my $security_defs =
|
||||
{ "api_key" =>
|
||||
{ "type" => "http", "scheme" => "Bearer", "bearerFormat" => "Bearer <api_key>" } };
|
||||
{ "type" => "http", "scheme" => "bearer", "bearerFormat" => "Bearer <api_key>" } };
|
||||
|
||||
my @security = map {
|
||||
{ $_ => [] }
|
||||
} keys(%$security_defs);
|
||||
my %spec = (
|
||||
openapi => '3.0.0',
|
||||
servers => (
|
||||
servers => [
|
||||
{
|
||||
url => "$LJ::WEB_DOMAIN/api/v$ver"
|
||||
},
|
||||
),
|
||||
],
|
||||
info => {
|
||||
title => "$LJ::SITENAME API",
|
||||
description => "An OpenAPI-compatible API for $LJ::SITENAME",
|
||||
version => $ver,
|
||||
|
||||
},
|
||||
security => keys(%$security_defs),
|
||||
security => \@security,
|
||||
components => {
|
||||
securitySchemes => $security_defs,
|
||||
}
|
||||
|
|
|
@ -136,6 +136,8 @@ sub get_call_opts {
|
|||
# APIs are versioned, so we only want to check for endpoints that match
|
||||
# the version the user is requesting.
|
||||
if ( $call_opts->role eq 'api' ) {
|
||||
# return early if we weren't given an API version
|
||||
return unless defined( $call_opts->apiver );
|
||||
|
||||
# check the static endpoints for this api version first
|
||||
if ( exists $api_endpoints{ $call_opts->apiver } ) {
|
||||
|
|
|
@ -204,8 +204,8 @@ sub decode {
|
|||
sub decode_unknown_type {
|
||||
my ( $class, $what ) = @_;
|
||||
|
||||
# booleans get converted to undef for false and 1 for true
|
||||
return $what ? 1 : undef if JSON::is_bool($what);
|
||||
# booleans get converted to 0 for false and 1 for true
|
||||
return $what ? 1 : 0 if JSON::is_bool($what);
|
||||
|
||||
# otherwise, stringify
|
||||
return "$what";
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,39 @@
|
|||
[%- sections.title = "API" -%]
|
||||
[%- CALL dw.active_resource_group( "foundation" ) -%]
|
||||
[%- dw.need_res( { group => "foundation" },
|
||||
"js/vendor/rapidoc-min.js"
|
||||
"js/components/jquery.collapse.js"
|
||||
"stc/css/components/collapse.css"
|
||||
"stc/api.css"
|
||||
"stc/css/components/foundation-icons.css"
|
||||
) -%]
|
||||
|
||||
<div class="alert-box secondary">This API, and it's documentation, are not yet fully finalized. We will do our best not to rename/remove routes listed here, but more options may be added, and there may be errors with existing routes. If you find an error or missing functionality, please report it at <a href="https://dw-beta.dreamwidth.org/15368.html">this entry</a>!</div>
|
||||
|
||||
<p>This is documentation for the Dreamwidth REST API. An API is a way of providing information that is easy for programs to access and use, but isn't always particularly friendly for humans. This document attempts to show what information can be requested, and how, in a slightly more human-readable format, and provide an interface for users to test various requests without having to deal with external programs or a commandline. A machine-readable version of this spec is available at <a href="[% site.root %]/api/v1/spec">/api/v1/spec</a>.</p>
|
||||
<p>
|
||||
For this API, users are identified with an API key rather than a username and password. The API key doesn't have access to all the same functions a logged in user to the site has, but it does have access to all the private information you have access to, and the ability to make posts as you, so protect it as you would your password. If you've accidentally shared it, you can delete a key from the management page. The API key that will be used for calls on this page is <b><span id="api_key">[% key.keyhash %]</span></b>. If you'd like to see all your API keys and manage them, go to <a href="[% site.root %]/manage/emailpost">the mobile post settings page</a>.</p>
|
||||
|
||||
|
||||
<rapi-doc
|
||||
spec-url="[% site.root %]/api/v1/spec"
|
||||
layout="column"
|
||||
render-style="view"
|
||||
show-header='false'
|
||||
show-info='false'
|
||||
allow-server-selection = 'false'
|
||||
allow-api-list-style-selection ='false'
|
||||
allow-authentication="true"
|
||||
id="apiDoc"
|
||||
> </rapi-doc>
|
||||
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
const rapidocEl = document.getElementById('apiDoc');
|
||||
rapidocEl.addEventListener('spec-loaded', (e) => {
|
||||
const keyInputEl = document.getElementById('api_key');
|
||||
rapidocEl.setApiKey('api_key',keyInputEl.innerText);
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
Loading…
Reference in New Issue