Skip to content

Commit

Permalink
API spec
Browse files Browse the repository at this point in the history
  • Loading branch information
kohler committed Sep 16, 2024
1 parent ac6cc6c commit c1feff3
Show file tree
Hide file tree
Showing 6 changed files with 439 additions and 226 deletions.
162 changes: 104 additions & 58 deletions batch/apispec.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class APISpec_Batch {
/** @var bool */
private $override_ref;
/** @var bool */
private $override_param;
/** @var bool */
private $override_tags;
/** @var bool */
private $override_schema;
Expand Down Expand Up @@ -101,6 +103,7 @@ function __construct(Conf $conf, $arg) {
}

$this->override_ref = isset($arg["override-ref"]);
$this->override_param = isset($arg["override-param"]);
$this->override_tags = isset($arg["override-tags"]);
$this->override_schema = isset($arg["override-schema"]);
$this->override_description = !isset($arg["no-override-description"]);
Expand Down Expand Up @@ -189,8 +192,21 @@ function run() {

// erase unwanted keys
foreach ($this->paths as $pj) {
foreach ($pj as $xj) {
if (!is_object($xj)) {
continue;
}
if ($xj->summary === $pj->__path
&& !isset($xj->description)
&& !isset($xj->operationId)) {
unset($xj->summary);
}
}
unset($pj->__path);
}
foreach ($this->j->tags as $tj) {
unset($tj->summary);
}

// print
if (($this->output_file ?? "-") === "-") {
Expand Down Expand Up @@ -291,7 +307,12 @@ private function expand_path_method($path, $method, $known, $uf) {
}
$lmethod = strtolower($method);
$xj = $pathj->$lmethod = $pathj->$lmethod ?? (object) [];
$this->setj->paths->$path->$lmethod = true;
if (!isset($this->setj->paths->$path->$lmethod)) {
if ($this->override_description || ($xj->summary ?? "") === "") {
$xj->summary = $path;
}
$this->setj->paths->$path->$lmethod = true;
}
$this->expand_metadata($xj, $uf, "{$lmethod} {$path}");
$this->expand_request($xj, $known, $uf, "{$path}.{$lmethod}");
$this->expand_response($xj, $uf);
Expand Down Expand Up @@ -332,13 +353,14 @@ private function merge_description($name, $xj) {
}
$xtp = new XtParams($this->conf, null);
$dj = $xtp->search_name($this->description_map, $name);
if ($dj
&& isset($dj->summary)
if (!$dj) {
return;
}
if (isset($dj->summary)
&& ($this->override_description || ($xj->summary ?? "") === "")) {
$xj->summary = $dj->summary;
}
if ($dj
&& isset($dj->description)
if (isset($dj->description)
&& ($this->override_description || ($xj->description ?? "") === "")) {
$xj->description = $dj->description;
}
Expand All @@ -361,12 +383,18 @@ private function resolve_common_schema($name) {
];
} else if ($name === "rid") {
$nj = (object) [
"type" => "integer",
"oneOf" => [
(object) ["type" => "integer", "minimum" => 1],
(object) ["type" => "string"]
],
"description" => "Review ID"
];
} else if ($name === "cid") {
$nj = (object) [
"type" => "integer",
"oneOf" => [
(object) ["type" => "integer", "minimum" => 1],
(object) ["type" => "string", "examples" => ["new", "response", "R2response"]]
],
"description" => "Comment ID"
];
} else if ($name === "ok") {
Expand Down Expand Up @@ -507,16 +535,16 @@ private function expand_request($x, $known, $uf, $path) {
} else if ($name === "redirect" && $f === 0) {
$params["redirect"] = $this->resolve_common_param("redirect");
} else if (($f & (self::F_BODY | self::F_FILE)) === 0) {
$ps = $uf->parameter_info->$name ?? null;
$params[$name] = (object) [
"name" => $name,
"in" => "query",
"required" => ($f & self::F_REQUIRED) !== 0,
"schema" => (object) []
"schema" => $ps ?? (object) []
];
} else {
$bprop[$name] = (object) [
"schema" => (object) []
];
$ps = $uf->parameter_info->$name ?? null;
$bprop[$name] = $ps ?? (object) [];
if (($f & self::F_REQUIRED) !== 0) {
$breq[] = $name;
}
Expand All @@ -526,59 +554,75 @@ private function expand_request($x, $known, $uf, $path) {
}
}
if (!empty($params)) {
$x->parameters = $x->parameters ?? [];
$xparams = [];
foreach ($x->parameters as $i => $pj) {
if (isset($pj->name) && is_string($pj->name)) {
$xparams[$pj->name] = $i;
} else if (isset($pj->{"\$ref"}) && is_string($pj->{"\$ref"})
&& preg_match('/\A\#\/components\/parameters\/([^.]*)/', $pj->{"\$ref"}, $m)) {
$xparams[$m[1]] = $i;
}
$this->apply_parameters($x, $params, $path);
}
if (!empty($bprop)) {
$rbj = $x->requestBody = $x->requestBody ?? (object) ["description" => ""];
$cj = $rbj->content = $rbj->content ?? (object) [];
$bodyj = $cj->{"multipart/form-data"}
?? $cj->{"application/x-www-form-urlencoded"}
?? (object) [];
unset($cj->{"multipart/form-data"}, $cj->{"application/x-www-form-urlencoded"}, $cj->schema);
$formtype = $has_file ? "multipart/form-data" : "application/x-www-form-urlencoded";
$cj->{$formtype} = $bodyj;
$xbschema = $bodyj->schema = $bodyj->schema ?? (object) [];
$xbschema->type = "object";
$xbschema->properties = $xbschema->properties ?? (object) [];
$this->apply_body_parameters($xbschema->properties, $bprop, $path);
if (!empty($breq)) {
$xbschema->required = $breq;
} else {
unset($xbschema->required);
}
foreach ($params as $n => $npj) {
$i = $xparams[$n] ?? null;
if ($i === null) {
$x->parameters[] = $npj;
continue;
}
}

private function apply_parameters($x, $params, $path) {
$x->parameters = $x->parameters ?? [];
$xparams = [];
foreach ($x->parameters as $i => $pj) {
if (isset($pj->name) && is_string($pj->name)) {
$xparams[$pj->name] = $i;
} else if (isset($pj->{"\$ref"}) && is_string($pj->{"\$ref"})
&& preg_match('/\A\#\/components\/parameters\/([^.]*)/', $pj->{"\$ref"}, $m)) {
$xparams[$m[1]] = $i;
}
}
foreach ($params as $n => $npj) {
$i = $xparams[$n] ?? null;
if ($i === null) {
$x->parameters[] = $npj;
continue;
}
$xpj = $x->parameters[$i];
if ($this->override_param || ($this->override_ref && isset($npj->{"\$ref"}))) {
$x->parameters[$i] = $npj;
} else if (isset($xpj->{"\$ref"}) !== isset($npj->{"\$ref"})) {
fwrite(STDERR, "{$path}.param[{$n}]: \$ref status differs\n");
} else if (isset($xpj->{"\$ref"})) {
if ($xpj->{"\$ref"} !== $npj->{"\$ref"}) {
fwrite(STDERR, "{$path}.param[{$n}]: \$ref destination differs\n");
}
$xpj = $x->parameters[$i];
if ($this->override_ref && isset($npj->{"\$ref"})) {
$x->parameters[$i] = $npj;
} else if (isset($xpj->{"\$ref"}) !== isset($npj->{"\$ref"})) {
fwrite(STDERR, "{$path}.param[{$n}]: \$ref status differs\n");
} else if (isset($xpj->{"\$ref"})) {
if ($xpj->{"\$ref"} !== $npj->{"\$ref"}) {
fwrite(STDERR, "{$path}.param[{$n}]: \$ref destination differs\n");
}
} else {
foreach ((array) $npj as $k => $v) {
if (!isset($xpj->$k)) {
$xpj->$k = $v;
} else if (is_scalar($v) && $xpj->$k !== $v) {
fwrite(STDERR, "{$path}.param[{$n}]: {$k} differs\n");
}
} else {
foreach ((array) $npj as $k => $v) {
if (!isset($xpj->$k)) {
$xpj->$k = $v;
} else if (is_scalar($v) && $xpj->$k !== $v) {
fwrite(STDERR, "{$path}.param[{$n}]: {$k} differs\n");
}
}
}
}
if (!empty($bprop)) {
$schema = (object) [
"type" => "object",
"properties" => $bprop
];
if (!empty($breq)) {
$schema->required = $breq;
}

private function apply_body_parameters($x, $bprop, $path) {
foreach ($bprop as $n => $npj) {
$xpj = $x->{$n} ?? null;
if ($xpj === null
|| $this->override_param
|| ($this->override_ref && isset($npj->{"\$ref"}))) {
$x->{$n} = $npj;
}
$formtype = $has_file ? "multipart/form-data" : "application/x-www-form-urlencoded";
$x->requestBody = (object) [
"description" => "",
"content" => (object) [
$formtype => (object) [
"schema" => (object) $schema
]
]
];
}
}

Expand All @@ -603,7 +647,8 @@ private function expand_response($x, $uf) {
if ($name === "*") {
// skip
} else {
$bprop[$name] = (object) [];
$ps = $uf->response_info->$name ?? null;
$bprop[$name] = $ps ?? (object) [];
if ($required) {
$breq[] = $name;
}
Expand Down Expand Up @@ -702,6 +747,7 @@ static function make_args($argv) {
"x,no-extensions Ignore extensions",
"i:,input: =FILE Modify existing specification in FILE",
"override-ref Overwrite conflicting \$refs in input",
"override-param",
"override-tags",
"override-schema",
"no-override-description",
Expand Down
57 changes: 57 additions & 0 deletions devel/apidoc/comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Comments

These endpoints query and modify submission comments.

Each comment has a *visibility* and a *topic* (which in the UI is called a
*thread*). These values control who can see the comment.

The default comment visibility is `"rev"`, which makes the comment visible to
PC and external reviewers. Other values are `"admin"` (visible only to
submission administrators and the comment author), `"pc"` (visible to PC
reviewers, but not external reviewers), and `"au"` (visible to authors and
reviewers).

The default comment topic is `"rev"`, the review thread. Comments on the
review thread are visible to users who can see reviews; if you can’t see
reviews, you can’t see the review thread. Other comment topics are `"paper"`,
the submission thread (visible to anyone who can see the submission), and
`"dec"`, the decision thread (visible to users who can see the submission’s
decision).


# get /{p}/comment

Return the JSON representation of a comment.

The `c` parameter specifies the comment to return. If the comment exists and
the user can view it, it will be returned in the `comment` component of the
response. Otherwise, an error response is returned.

If `c` is omitted, all viewable comments are returned in a `comments` list.


# post /{p}/comment

Create, modify, or delete a comment.

The `c` parameter specifies the comment to modify. It can be a numeric comment
ID; `new`, to create a new comment; or `response` (or a compound like
`R2response`), to create or modify a named response.

Setting `delete=1` deletes the specified comment, and the response does not
contain a `comment` component. Otherwise the comment is created or modified,
and the response `comment` component contains the new comment.

Comment attachments may be uploaded as files (requiring a request body in
`multipart/form-data` encoding), or using the [upload API](#operation/upload).
To upload a single new attachment:

* Set the `attachment:1` body parameter to `new`
* Either:
* Set `attachment:1:file` as a uploaded file containing the relevant data
* Or use the [upload API](#operation/upload) to upload the file,
and supply the upload token in the `attachment:1:upload` body parameter

To upload multiple attachments, number them sequentially (`attachment:2`,
`attachment:3`, and so forth). To delete an existing attachment, supply its
`docid` in `attachment:N`, and set `attachment:N:delete` to 1.
14 changes: 14 additions & 0 deletions devel/apidoc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,17 @@ is open source; a supported version runs on [hotcrp.com](https://hotcrp.com/).
We welcome [pull requests](https://github.com/kohler/hotcrp/pulls) that fill
out this documentation. To request documentation for an API method, please
open a [GitHub issue](https://github.com/kohler/hotcrp/issues).

## Basics

HotCRP reads parameters using [form
encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST),
either in query strings or in the request body. Complex requests either use
structured keys, such as `named_search/1/q`, or, occasionally, JSON encoding.
`multipart/form-data` is used for requests that include file data.

The `p` parameter, which defines a submission ID, can appear either in the
query string or immediately following `api/` in the query path.
`api/comment?p=1` and `api/1/comment` are the same API call.

Responses are formatted as JSON.
20 changes: 18 additions & 2 deletions etc/apiexpansions.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,27 @@
},
{
"name": "comment", "get": true, "merge": true,
"tags": ["Comments"], "order": 0
"tags": ["Comments"], "order": 0,
"parameter_info": {"content": {"type": "boolean", "description": "False omits comment content from response"}},
"response_info": {"comment": {"$ref": "#/components/schemas/comment"},
"comments": {"type": "list", "items": {"$ref": "#/components/schemas/comment"}}}
},
{
"name": "comment", "post": true, "merge": true,
"tags": ["Comments"], "order": 0
"tags": ["Comments"], "order": 0,
"parameter_info": {"override": {"type": "boolean"},
"delete": {"type": "boolean"},
"text": {"type": "string"},
"tags": {"type": "string"},
"topic": {"$ref": "#/components/schemas/comment_topic"},
"visibility": {"$ref": "#/components/schemas/comment_visibility"},
"response": {"type": "string"},
"ready": {"type": "boolean"},
"draft": {"type": "boolean"},
"blind": {"type": "boolean"},
"by_author": {"type": "boolean"},
"review_token": {"type": "string"}},
"response_info": {"comment": {"$ref": "#/components/schemas/comment"}}
},
{
"name": "mentioncompletion", "get": true, "merge": true,
Expand Down
4 changes: 2 additions & 2 deletions etc/apifunctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@
"name": "comment", "get": true, "paper": true,
"function": "Comment_API::run",
"parameters": "?c ?content",
"response": "?comment",
"response": "?comment ?comments",
"response_deprecated": "cmt"
},
{
"name": "comment", "post": true, "paper": true,
"function": "Comment_API::run",
"parameters": "c ?override ?=text ?=response ?=ready ?=topic ?=draft ?=blind ?=tags ?=visibility ?=:attachment ?=by_author ?=review_token ?delete",
"parameters": "c ?override ?=text ?=:attachment ?=tags ?=topic ?=visibility ?=ready ?=draft ?=blind ?=response ?=by_author ?=review_token ?delete",
"response": "?comment ?conflict",
"response_deprecated": "cmt"
},
Expand Down
Loading

0 comments on commit c1feff3

Please sign in to comment.