Compare commits

...

1 Commits

Author SHA1 Message Date
Markus Kahl 290ef6ac08 OpenAPI 3.0 spec, swagger UI 3 years ago
  1. 36
      app/controllers/docs_controller.rb
  2. 1
      app/views/docs/index.html.erb
  3. 2
      config/routes.rb
  4. 8
      docs/api/apiv3/endpoints/actions.apib
  5. 18
      docs/api/apiv3/endpoints/attachments.apib
  6. 16
      docs/api/apiv3/endpoints/documents.apib
  7. 6
      docs/api/apiv3/endpoints/groups.apib
  8. 26
      docs/api/apiv3/endpoints/help_texts.apib
  9. 14
      docs/api/apiv3/endpoints/members.apib
  10. 7
      docs/api/apiv3/endpoints/news.apib
  11. 8
      docs/api/apiv3/endpoints/placeholder_users.apib
  12. 5
      docs/api/apiv3/endpoints/posts.apib
  13. 12
      docs/api/apiv3/endpoints/projects.apib
  14. 22
      docs/api/apiv3/endpoints/queries.apib
  15. 2
      docs/api/apiv3/endpoints/revisions.apib
  16. 6
      docs/api/apiv3/endpoints/root.apib
  17. 29
      docs/api/apiv3/endpoints/time_entries.apib
  18. 18
      docs/api/apiv3/endpoints/user-preferences.apib
  19. 14
      docs/api/apiv3/endpoints/users.apib
  20. 20
      docs/api/apiv3/endpoints/versions.apib
  21. 6
      docs/api/apiv3/endpoints/wiki_pages.apib
  22. 83
      docs/api/apiv3/endpoints/work-packages.apib
  23. 2
      frontend/package.json
  24. 3
      frontend/src/app/components/api/docs/docs.component.html
  25. 5
      frontend/src/app/components/api/docs/docs.component.sass
  26. 48
      frontend/src/app/components/api/docs/docs.component.ts
  27. 4
      frontend/src/app/global-dynamic-components.const.ts
  28. 7
      lib/api/open_api.rb
  29. 469
      lib/api/open_api/blueprint_import.rb
  30. 10
      lib/api/v3/root.rb
  31. 10
      lib/tasks/api.rake

@ -0,0 +1,36 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
class DocsController < ApplicationController
before_action :require_login
def index
end
end

@ -38,6 +38,8 @@ OpenProject::Application.routes.draw do
get '/health_checks/all' => 'ok_computer/ok_computer#show', check: 'full'
mount OkComputer::Engine, at: "/health_checks"
get "/docs" => 'docs#index'
# Redirect deprecated issue links to new work packages uris
get '/issues(/)' => redirect("#{rails_relative_url_root}/work_packages")
# The URI.escape doesn't escape / unless you ask it to.

@ -78,6 +78,7 @@ The configuration is adaptable by admins within the administration of the OpenPr
"self": {
"href": "/api/v3/actions/work_packages/create",
"title": "Add work package"
}
},
"_type": "Action",
"id": "work_packages/create",
@ -147,6 +148,7 @@ Returns an individual action.
"self": {
"href": "/api/v3/actions/work_packages/assign_versions",
"title": "Assigning version"
}
},
"_type": "Action",
"id": "work_packages/assign_versions",
@ -297,7 +299,7 @@ has sufficient permissions.
"id": "work_packages/assignee/p123-567"
},
{
"_links":
"_links": {
"self": {
"href": "/api/v3/capabilities/memberships/create/p345-821",
"title": "Create members"
@ -318,7 +320,7 @@ has sufficient permissions.
"id": "memberships/create/p345-821"
},
{
"_links":
"_links": {
"self": {
"href": "/api/v3/capabilities/users/delete/g-567",
"title": "Delete user"
@ -334,7 +336,7 @@ has sufficient permissions.
},
"_type": "Capability",
"id": "users/delete/g-567"
},
}
]
}
}

@ -131,7 +131,7 @@ Instead the `fileName` inside the JSON of the metadata part will be used.
"self": {
"href": "/api/v3/attachments/1"
},
"container" {
"container": {
"href": "/api/v3/work_packages/1"
},
"author": {
@ -139,7 +139,7 @@ Instead the `fileName` inside the JSON of the metadata part will be used.
},
"staticDownloadLocation": {
"href": "/api/v3/attachments/1/download"
}
},
"downloadLocation": {
"href": "/some/remote/aws/url/image.png"
}
@ -155,7 +155,7 @@ Instead the `fileName` inside the JSON of the metadata part will be used.
"contentType": "image/png",
"digest": {
"algorithm": "md5",
"64c26a8403cd796ea4cf913cda2ee4a9":
"hash": "64c26a8403cd796ea4cf913cda2ee4a9"
},
"createdAt": "2014-05-21T08:51:20Z"
}
@ -296,7 +296,7 @@ Permanently deletes the specified attachment.
}
}
## List attachments [GET]
## List attachments by post [GET]
+ Parameters
+ id (required, integer, `1`) ... ID of the post whose attachments will be listed
@ -323,7 +323,7 @@ Permanently deletes the specified attachment.
"message": "The requested resource could not be found."
}
## Add attachment [POST]
## Add attachment to post [POST]
Adds an attachment with the post as it's container.
@ -577,7 +577,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
}
}
## List attachments [GET]
## List attachments by wiki page [GET]
+ Parameters
+ id (required, integer, `1`) ... ID of the wiki page whose attachments will be listed
@ -604,7 +604,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
"message": "The requested resource could not be found."
}
## Add attachment [POST]
## Add attachment to wiki page [POST]
Adds an attachment with the wiki page as it's container.
@ -881,7 +881,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
}
}
## List attachments [GET]
## List attachments by work package [GET]
+ Parameters
+ id (required, integer, `1`) ... ID of the work package whose attachments will be listed
@ -908,7 +908,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
"message": "The specified work package does not exist."
}
## Add attachment [POST]
## Add attachment to work package [POST]
To add an attachment to a work package, a client needs to issue a request of type `multipart/form-data`
with exactly two parts.

@ -49,7 +49,7 @@ None yet
},
"self": {
"href": "/api/v3/documents/1",
"title": "Some document",
"title": "Some document"
},
"project": {
"href": "/api/v3/projects/19",
@ -58,23 +58,19 @@ None yet
},
"_embedded": {
"project": {
"_type": "Project",
<-- omitted for brevity -->
}
"_type": "Project..."
},
attachments": {
"attachments": {
"_type": "Collection",
"total": 2,
"count": 2,
"_embedded": {
<-- omitted for brevity -->
},
"_embedded...": { "elements": [] },
"_links": {
"self": {
"href": "/api/v3/documents/1/attachments"
}
}
},
}
}
}
@ -136,7 +132,7 @@ None yet
},
"self": {
"href": "/api/v3/documents/1",
"title": "Some document",
"title": "Some document"
},
"project": {
"href": "/api/v3/projects/19",

@ -49,7 +49,7 @@ to work with this resource.
"method": "delete"
},
"memberships": {
"href": "/api/v3/memberships?filters=[{"principal":{"operator":"=","values":["9"]}}]",
"href": "/api/v3/memberships?filters=[{\"principal\":{\"operator\":\"=\",\"values\":[\"9\"]}}]",
"title": "Memberships"
},
"updateImmediately": {
@ -338,7 +338,7 @@ Deletes the group.
"title": "The group"
},
"memberships": {
"href": "/api/v3/memberships?filters=[{"principal":{"operator":"=","values":["9"]}}]",
"href": "/api/v3/memberships?filters=[{\"principal\":{\"operator\":\"=\",\"values\":[\"9\"]}}]",
"title": "Memberships"
},
"members": [
@ -365,7 +365,7 @@ Deletes the group.
"title": "Another group"
},
"memberships": {
"href": "/api/v3/memberships?filters=[{"principal":{"operator":"=","values":["123"]}}]",
"href": "/api/v3/memberships?filters=[{\"principal\":{\"operator\":\"=\",\"values\":[\"123\"]}}]",
"title": "Memberships"
},
"members": [

@ -37,13 +37,13 @@
"_type": "HelpText",
"_links": {
"self": {
"href": "/api/v3/help_texts/1",
"href": "/api/v3/help_texts/1"
}
},
"id": 1,
"attribute": 'id',
"attributeCaption": 'ID',
"scope": 'WorkPackage',
"attribute": "id",
"attributeCaption": "ID",
"scope": "WorkPackage",
"helpText": {
"format": "markdown",
"raw": "Help text for id attribute.",
@ -54,13 +54,13 @@
"_type": "HelpText",
"_links": {
"self": {
"href": "/api/v3/help_texts/2",
"href": "/api/v3/help_texts/2"
}
},
"id": 2,
"attribute": 'status',
"attributeCaption": 'Status',
"scope": 'WorkPackage',
"attribute": "status",
"attributeCaption": "Status",
"scope": "WorkPackage",
"helpText": {
"format": "markdown",
"raw": "Help text for status attribute.",
@ -87,17 +87,17 @@
"_type": "HelpText",
"_links": {
"self": {
"href": "/api/v3/help_texts/1",
"href": "/api/v3/help_texts/1"
},
"editText": {
"type": "text/html",
"href": "/admin/attribute_help_texts/1/edit",
"href": "/admin/attribute_help_texts/1/edit"
}
},
"id": 1,
"attribute": 'id',
"attributeCaption": 'ID',
"scope": 'WorkPackage',
"attribute": "id",
"attributeCaption": "ID",
"scope": "WorkPackage",
"helpText": {
"format": "markdown",
"raw": "Help text for id attribute.",

@ -33,7 +33,7 @@ When creating and updating memberships, a custom message can be sent to users of
+ Body
{
"_links":
"_links": {
"self": {
"href": "/api/v3/memberships/11",
"title": "Some user"
@ -73,11 +73,9 @@ When creating and updating memberships, a custom message can be sent to users of
"createdAt": "2015-03-20T12:56:56Z",
"updatedAt": "2018-12-20T18:16:11Z",
"_embedded": {
"project": "<-- omitted for brevity -->",
"principal": "<-- omitted for brevity -->",
"roles": [
"<-- omitted for brevity -->"
]
"project...": {},
"principal...": {},
"roles...": []
}
}
@ -977,8 +975,8 @@ For more details and all possible responses see the general specification of [Fo
"createdAt": "2016-02-29T12:50:20+00:00",
"updatedAt": "2016-02-29T12:50:20+00:00",
"type": null
}]
}
}
]
}
}

@ -55,13 +55,10 @@ None yet
},
"_embedded": {
"project": {
"_type": "Project",
<-- omitted for brevity -->
}
"_type": "Project..."
},
"author": {
"_type": "User",
<-- omitted for brevity -->
"_type": "User..."
}
}
}

@ -35,7 +35,7 @@
},
"show": {
"href": "/placeholder_users/1",
"type": 'text/html'
"type": "text/html"
},
"delete": {
"href": "/api/v3/placeholder_users/1",
@ -215,16 +215,16 @@ and the response will only accept the deletion request. The actual deletion will
},
"showUser": {
"href": "/placeholder_users/1",
"type": 'text/html'
"type": "text/html"
},
"updateImmediately": {
"href": "/api/v3/placeholder_users/1",
"title": "Update placeholder"
"title": "Update placeholder",
"method": "PATCH"
},
"delete": {
"href": "/api/v3/placeholder_users/1",
"title": "Delete placeholder"
"title": "Delete placeholder",
"method": "DELETE"
}
},

@ -36,10 +36,7 @@ Represents a post in a board. Posts are also referred to as messages in the appl
"subject": "A post with a subject",
"_embedded": {
"project": {
"_type": "Project",
"id": 1,
<-- abbreviated -->
}
"_type": "Project..."
}
},
"_links": {

@ -80,9 +80,9 @@ Depending on custom fields defined for projects, additional properties might exi
},
"workPackages": {
"href": "/api/v3/projects/1/work_packages"
}
},
"memberships": {
"href": "/api/v3/memberships?filters=[{"project":{"operator":"=","values":["1"]}}]
"href": "/api/v3/memberships?filters=[{\"project\":{\"operator\":\"=\",\"values\":[\"1\"]}}]"
},
"customField456": {
"href": "/api/v3/users/315",
@ -496,8 +496,8 @@ the project scheduled for deletion, it is archived at once.
"createdAt": "2016-02-29T12:50:20+00:00",
"updatedAt": "2016-02-29T12:50:20+00:00",
"type": null
}]
}
}
]
}
}
@ -2056,8 +2056,8 @@ The copy endpoint has a `_meta` property that allows defining which associations
"createdAt": "2016-02-29T12:50:20+00:00",
"updatedAt": "2016-02-29T12:50:20+00:00",
"type": null
}]
}
}
]
}
}

@ -196,7 +196,7 @@ If the values are nonprimitive (e.g. User, Project), they will be listed as obje
}
}
},
highlightedAttributes: []
"highlightedAttributes": []
},
"_links": {
"self": {
@ -236,7 +236,7 @@ If the values are nonprimitive (e.g. User, Project), they will be listed as obje
"title": "Updated on"
}
],
highlightedAttributes: [],
"highlightedAttributes": [],
"groupBy": {
"href": null,
"title": null
@ -488,7 +488,7 @@ Delete the query identified by the id parameter
"method": "post"
}
},
highlightedAttributes: []
"highlightedAttributes": []
}
},
"_links": {
@ -529,7 +529,7 @@ Delete the query identified by the id parameter
"title": "Updated on"
}
],
highlightedAttributes: [],
"highlightedAttributes": [],
"groupBy": {
"href": null,
"title": null
@ -2013,6 +2013,10 @@ Retrieve the schema for global queries, those, that are not assigned to a projec
Retrieve the schema for project queries.
+ Parameters
+ id (required, integer, `1`) ... Project id
+ Response 200 (application/hal+json)
[Schema For Project Queries][]
@ -2030,7 +2034,7 @@ Retrieve the schema for project queries.
}
## Query Available Projects [/api/v3/queries/available_projects]
## Available projects for query [/api/v3/queries/available_projects]
+ Model
+ Body
@ -2105,18 +2109,18 @@ Retrieve the schema for project queries.
"createdAt": "2016-02-29T12:50:20+00:00",
"updatedAt": "2016-02-29T12:50:20+00:00",
"type": null
}]
}
}
]
}
}
## Available projects [GET]
## Available projects for query [GET]
Gets a list of projects that are available as projects a query can be assigned to.
+ Response 200 (application/hal+json)
[Query Available Projects][]
[Available projects for query][]
+ Response 403 (application/hal+json)

@ -32,7 +32,7 @@ Revisions are sets of updates to files in the context of repositories linked in
"self": {
"href": "/api/v3/revisions/1"
},
"project" {
"project": {
"href": "/api/v3/projects/1"
},
"author": {

@ -30,14 +30,14 @@ a client should be able to discover further resources in the API.
+ Body
{
"_links" : {
"configuration" : {
"_links": {
"configuration": {
"href" : "/api/v3/configuration"
},
"user": {
"href": "/api/v3/users/1",
"title": "John Sheppard"
};
},
"userPreferences" : {
"href" : "/api/v3/my_preferences"
},

@ -52,18 +52,10 @@ Depending on custom fields defined for time entries, additional properties might
"updatedAt": "2015-03-20T12:56:56Z",
"customField12": 5,
"_embedded": {
"project": {
...
},
"workPackage": {
...
},
"user": {
...
},
"activity": {
...
}
"project...": {},
"workPackage...": {},
"user...": {},
"activity...": {}
},
"_links": {
"self": {
@ -95,7 +87,8 @@ Depending on custom fields defined for time entries, additional properties might
},
"customField4": {
"href": "/api/v3/users/5",
"title" "Some other user"
"title": "Some other user"
}
}
}
@ -1218,12 +1211,10 @@ For more details and all possible responses see the general specification of [Fo
"_embedded": {
"elements": [
{
"_type": "Project",
<< omitted for brevity >>
"_type": "Project..."
},
{
"_type": "Project",
<< omitted for brevity >>
"_type": "Project..."
}
]
}
@ -1288,9 +1279,7 @@ None
"position": 10,
"default": false,
"_embedded": {
"projects": [
...
]
"projects...": []
},
"_links": {
"self": {

@ -21,21 +21,21 @@
+ Body
{
"_type" : "UserPreferences",
"_links" : {
"self" : {
"href" : "/api/v3/my_preferences",
"_type": "UserPreferences",
"_links": {
"self": {
"href": "/api/v3/my_preferences"
},
"user": {
"href": "/api/v3/users/1",
"title": "John Sheppard"
}
},
"hideMail" : false,
"timeZone" : "Europe/Berlin",
"commentSortDescending" : true,
"warnOnLeavingUnsaved" : true,
"accessibilityMode" : false
"hideMail": false,
"timeZone": "Europe/Berlin",
"commentSortDescending": true,
"warnOnLeavingUnsaved": true,
"accessibilityMode": false
}

@ -79,7 +79,7 @@ Please note that custom fields are not yet supported by the api although the bac
},
"show": {
"href": "/users/1",
"type": 'text/html'
"type": "text/html"
},
"lock": {
"href": "/api/v3/users/1/lock",
@ -92,7 +92,7 @@ Please note that custom fields are not yet supported by the api although the bac
"delete": {
"href": "/api/v3/users/1",
"method": "DELETE"
},
}
},
"id": 1,
"login": "j.sheppard",
@ -484,21 +484,21 @@ Permanently deletes the specified user account.
},
"showUser": {
"href": "/users/1",
"type": 'text/html'
"type": "text/html"
},
"lock": {
"href": "/api/v3/users/1/lock",
"title": "Set lock on j.sheppard"
"title": "Set lock on j.sheppard",
"method": "POST"
},
"update": {
"href": "/api/v3/users/1",
"title": "Update j.sheppard"
"title": "Update j.sheppard",
"method": "PATCH"
},
"delete": {
"href": "/api/v3/users/1",
"title": "Delete j.sheppard"
"title": "Delete j.sheppard",
"method": "DELETE"
}
},
@ -708,7 +708,7 @@ Lists users. Only administrators or users with manage_user global permission hav
"href": "/api/v3/users/schema"
}
}
}
}
## View user schema [GET]

@ -41,13 +41,14 @@ Depending on custom fields defined for versions, additional properties might exi
{
"_links": {
"self": { "href": "/api/v3/versions/11" },
"update": { "href": "/api/v3/versions/11/form", "method": "POST" }
"updateImmediately": { "href": "/api/v3/versions/11", "method": "PATCH" }
"update": { "href": "/api/v3/versions/11/form", "method": "POST" },
"updateImmediately": { "href": "/api/v3/versions/11", "method": "PATCH" },
"definingProject": { "href": "/api/v3/projects/11" },
"availableInProjects": { "href": "/api/v3/versions/11/projects" }
"availableInProjects": { "href": "/api/v3/versions/11/projects" },
"customField4": {
"href": "/api/v3/custom_options/5",
"title" "Custom field option"
"title": "Custom field option"
}
},
"_type": "Version",
"id": 11,
@ -61,7 +62,7 @@ Depending on custom fields defined for versions, additional properties might exi
"endDate": null,
"status": "open",
"sharing": "system",
"customField14": "1234567890",
"customField14": "1234567890"
}
## View version [GET]
@ -332,6 +333,7 @@ You can use the form and schema to be retrieve the valid attribute values and by
"customField4": {
"href": "/api/v3/custom_options/5",
"title" "Custom field option"
}
},
"name": "v3.0 Alpha",
"description": {
@ -829,6 +831,10 @@ For more details and all possible responses see the general specification of [Fo
## Version update form [POST]
+ Parameters
+ id (required, integer, `1`) ... Project id
+ Request Update version form
+ Body
@ -1269,8 +1275,8 @@ Note that due to sharing this might be more than the versions *defined* by that
"createdAt": "2016-02-29T12:50:20+00:00",
"updatedAt": "2016-02-29T12:50:20+00:00",
"type": null
}]
}
}
]
}
}

@ -36,10 +36,8 @@ Represents an individual page in a project's wiki.
"title": "A wiki page with a name",
"_embedded": {
"project": {
"_type": "Project",
"id": 12,
<-- abbreviated -->
}
"_type": "Project...",
"id": 12
}
},
"_links": {

@ -496,22 +496,16 @@ Deletes the work package, as well as:
"_embedded": {
"elements": [
{
"_type": "Schema",
"_type": "Schema...",
"_links": {
"self": { "href": "/api/v3/work_packages/schemas/13-1" }
}
<snip>
},
{
"_type": "Schema",
"_type": "Schema...",
"_links": {
"self": { "href": "/api/v3/work_packages/schemas/7-6" }
}
<snip>
}
]
}
@ -637,7 +631,7 @@ For more details and all possible responses see the general specification of [Fo
}
}
## List Work Packages [GET]
## List work packages [GET]
+ Parameters
+ offset = `1` (optional, integer, `25`) ... Page number inside the requested collection.
@ -843,7 +837,7 @@ Returns the form resource that was created. See [Forms](/api/forms) section for
}
}
## List Work Packages [GET]
## List work packages by project [GET]
+ Parameters
+ id (required, integer, `1`) ... Project id
@ -909,7 +903,7 @@ Returns the form resource that was created. See [Forms](/api/forms) section for
"message": "The specified project does not exist."
}
## Create Work Package [POST]
## Create work package in project [POST]
When calling this endpoint the client provides a single object, containing at least the properties and links that are required, in the body.
The required fields of a WorkPackage can be found in its schema, which is embedded in the respective form.
@ -992,14 +986,14 @@ Note that it is only allowed to provide properties or links supporting the write
}
}
## Work Package Create Form [/api/v3/projects/{id}/work_packages/form]
## Work Package Create Form For Project [/api/v3/projects/{id}/work_packages/form]
This endpoint returns a form to allow a guided creation of a new work package.
The returned form will be pre-filled with default values for every property, if available.
For more details and all possible responses see the general specification of [Forms](/api/forms).
## Work Package Create Form [POST]
## Work Package Create Form For Project [POST]
+ Parameters
+ id (required, integer, `1`) ... ID of the project in which the work package will be created
@ -1284,16 +1278,16 @@ Lists all relations this work package is involved in.
},
"showUser": {
"href": "/users/1",
"type": 'text/html'
"type": "text/html"
},
"lock": {
"href": "/api/v3/users/1/lock",
"title": "Set lock on j.sheppard"
"title": "Set lock on j.sheppard",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/1",
"title": "Delete j.sheppard"
"title": "Delete j.sheppard",
"method": "DELETE"
}
},
@ -1316,12 +1310,12 @@ Lists all relations this work package is involved in.
},
"lock": {
"href": "/api/v3/users/2/lock",
"title": "Set lock on j.sheppard2"
"title": "Set lock on j.sheppard2",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/2",
"title": "Delete j.sheppard2"
"title": "Delete j.sheppard2",
"method": "DELETE"
}
},
@ -1590,6 +1584,8 @@ While the endpoint does support the pageSize parameter to limit the number of re
## Available relation candidates [GET]
+ Parameters
+ id (required, integer, `1`) ... Project id
+ pageSize (optional, integer, `25`) ... Maximum number of candidates to list (default 10)
+ filters (optional, string, `[{ "status_id": { "operator": "o", "values": null } }]`) ... JSON specifying filter conditions.
@ -1626,12 +1622,12 @@ While the endpoint does support the pageSize parameter to limit the number of re
},
"lock": {
"href": "/api/v3/users/1/lock",
"title": "Set lock on j.sheppard"
"title": "Set lock on j.sheppard",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/1",
"title": "Delete j.sheppard"
"title": "Delete j.sheppard",
"method": "DELETE"
}
},
@ -1654,12 +1650,12 @@ While the endpoint does support the pageSize parameter to limit the number of re
},
"lock": {
"href": "/api/v3/users/2/lock",
"title": "Set lock on j.sheppard2"
"title": "Set lock on j.sheppard2",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/2",
"title": "Delete j.sheppard2"
"title": "Delete j.sheppard2",
"method": "DELETE"
}
},
@ -1717,7 +1713,7 @@ Gets a list of users that are able to be watchers of the specified work package.
"message": "The specified work package does not exist."
}
## Available Projects [/api/v3/work_packages/{id}/available_projects]
## Available projects for work package [/api/v3/work_packages/{id}/available_projects]
+ Model
+ Body
@ -1797,12 +1793,12 @@ Gets a list of users that are able to be watchers of the specified work package.
"createdAt": "2016-02-29T12:50:20+00:00",
"updatedAt": "2016-02-29T12:50:20+00:00",
"type": null
}]
}
}
]
}
}
## Available projects [GET]
## Available projects for work package [GET]
Gets a list of projects that are available as projects to which the work package can be moved.
@ -1811,7 +1807,7 @@ Gets a list of projects that are available as projects to which the work package
+ Response 200 (application/hal+json)
[Available Projects][]
[Available projects for work package][]
+ Response 403 (application/hal+json)
@ -1865,11 +1861,11 @@ Gets a list of projects that are available as projects to which the work package
"_type": "Revision",
"_links": {
"self": {
"href": "/api/v3/revisions/13",
"href": "/api/v3/revisions/13"
},
"project": {
"href": "/api/v3/projects/1",
"title": "A Test Project
"title": "A Test Project"
},
"author": {
"href": "/api/v3/users/1",
@ -1888,22 +1884,22 @@ Gets a list of projects that are available as projects to which the work package
"raw": "This revision provides new features\n\nAn elaborate description",
"html": "<p>This revision provides new features<br/><br/>An elaborate description</p>"
},
"createdAt": "2015-07-21T13:36:59Z",
"createdAt": "2015-07-21T13:36:59Z"
},
{
"_type": "Revision",
"_links": {
"self": {
"href": "/api/v3/revisions/14",
"href": "/api/v3/revisions/14"
},
"project": {
"href": "/api/v3/projects/1",
"title": "A Test Project
"title": "A Test Project"
},
"author": {
"href": "/api/v3/users/2",
"title": "Jim Sheppard - j.sheppard"
}
},
"showRevision": {
"href": "/projects/identifier/repository/revision/029ed72a"
}
@ -1917,8 +1913,9 @@ Gets a list of projects that are available as projects to which the work package
"raw": "This revision fixes some stuff\n\nMore information here",
"html": "<p>This revision fixes some stuff<br/><br/>More information here</p>"
},
"createdAt": "2015-06-30T08:47:00Z",
}]
"createdAt": "2015-06-30T08:47:00Z"
}
]
}
}
@ -2139,12 +2136,12 @@ Returns the activity resource
},
"lock": {
"href": "/api/v3/users/1/lock",
"title": "Set lock on j.sheppard"
"title": "Set lock on j.sheppard",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/1",
"title": "Delete j.sheppard"
"title": "Delete j.sheppard",
"method": "DELETE"
}
},
@ -2167,12 +2164,12 @@ Returns the activity resource
},
"lock": {
"href": "/api/v3/users/2/lock",
"title": "Set lock on j.sheppard2"
"title": "Set lock on j.sheppard2",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/2",
"title": "Delete j.sheppard2"
"title": "Delete j.sheppard2",
"method": "DELETE"
}
},
@ -2253,12 +2250,12 @@ Gets a list of users that can be assigned to work packages in the given project.
},
"lock": {
"href": "/api/v3/users/1/lock",
"title": "Set lock on j.sheppard"
"title": "Set lock on j.sheppard",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/1",
"title": "Delete j.sheppard"
"title": "Delete j.sheppard",
"method": "DELETE"
}
},
@ -2281,12 +2278,12 @@ Gets a list of users that can be assigned to work packages in the given project.
},
"lock": {
"href": "/api/v3/users/2/lock",
"title": "Set lock on j.sheppard2"
"title": "Set lock on j.sheppard2",
"method": "POST"
},
"delete": {
"href": "/api/v3/users/2",
"title": "Delete j.sheppard2"
"title": "Delete j.sheppard2",
"method": "DELETE"
}
},

@ -13,6 +13,7 @@
"@angular/language-service": "11.2.3",
"@jsdevtools/coverage-istanbul-loader": "3.0.5",
"@types/jasmine": "~3.6.0",
"@types/swagger-ui": "^3.47.0",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"codelyzer": "^6.0.0",
@ -108,6 +109,7 @@
"reflect-metadata": "^0.1.13",
"rxjs": "^6.6.6",
"screenfull": "^4.2.1",
"swagger-ui": "^3.50.0",
"tablesorter": "^2.31.3",
"tickety-tick-formatter": "github:bitcrowd/tickety-tick-formatter",
"typedjson": "^1.5.1",

@ -0,0 +1,3 @@
<div id="docs-wrapper">
<div id="swagger"></div>
</div>

@ -0,0 +1,5 @@
div code
all: initial
color: white
@import "~swagger-ui/dist/swagger-ui.css"

@ -0,0 +1,48 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
import { AfterViewInit, Component, ViewEncapsulation } from '@angular/core';
import * as SwaggerUI from 'swagger-ui';
export const docsSelector = 'docs';
@Component({
selector: docsSelector,
styleUrls: ['./docs.component.sass'],
templateUrl: './docs.component.html',
encapsulation: ViewEncapsulation.None
})
export class DocsComponent implements AfterViewInit {
ngAfterViewInit() {
SwaggerUI({
dom_id: '#swagger',
url: document.location.href.replace("docs", "api/v3/spec.json"),
filter: true
});
}
}

@ -156,6 +156,7 @@ import {
} from "core-app/modules/admin/editable-query-props/editable-query-props.component";
import { SlideToggleComponent, slideToggleSelector } from "core-app/modules/common/slide-toggle/slide-toggle.component";
import { BackupComponent, backupSelector } from "./components/admin/backup.component";
import { DocsComponent, docsSelector } from "./components/api/docs/docs.component";
export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: appBaseSelector, cls: ApplicationBaseComponent },
@ -203,7 +204,8 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: quickInfoMacroSelector, cls: WorkPackageQuickinfoMacroComponent, embeddable: true },
{ selector: editableQueryPropsSelector, cls: EditableQueryPropsComponent },
{ selector: slideToggleSelector, cls: SlideToggleComponent },
{ selector: backupSelector, cls: BackupComponent }
{ selector: backupSelector, cls: BackupComponent },
{ selector: docsSelector, cls: DocsComponent }
];

@ -0,0 +1,7 @@
module API
module OpenAPI
def self.spec(version: :stable)
API::OpenAPI::BlueprintImport.convert version: version
end
end
end

@ -0,0 +1,469 @@
module API
module OpenAPI
module BlueprintImport
extend self
def assemble_file(input_path:, output_path:)
File.open(output_path, "w") do |f|
f.write read_file(input_path).gsub(/\t/, ' ')
end
end
def read_file(path)
bp = File.read path
bp.gsub(include_directive_regex).each do |_match|
read_file Pathname(path).parent.join($1).to_s
end
end
def include_directive_regex
@include_directive_regex ||= /\<\!\-\-\s*include\((.*)\)\s*\-\-\>/
end
def convert(version: :stable)
input_file = Rails.application.root.join("docs/api/apiv3-doc-#{version}.apib")
md_file = Tempfile.new("apibp.md").path
assemble_file input_path: input_file, output_path: md_file
spec = YAML.load %x`api-spec-converter -f api_blueprint -t openapi_3 --syntax=yaml #{md_file}`
add_security! spec
amend_schemas! spec, apibp: File.read(md_file)
spec
ensure
FileUtils.rm_f md_file if File.exist? md_file
end
def add_security!(spec)
spec["components"]["securitySchemes"] = {
"BasicAuth" => {
"type" => "http",
"scheme" => "basic"
}
}
spec["security"] = [
{ "BasicAuth" => [] }
]
end
def amend_schemas!(spec, apibp:)
schemas = schema_names spec
spec["tags"].each do |tag|
schema = schema_from_tag tag, schema_names: schemas
if schema
key = schema.keys.first.underscore.split("_").map(&:capitalize).join("_") + "Model"
spec["components"]["schemas"][key] = schema.values.first
end
end
add_formattable_schema! spec
add_link_schema! spec
add_missing_models! spec, apibp: apibp
spec["components"]["schemas"] = spec["components"]["schemas"].sort.to_h
end
def add_formattable_schema!(spec)
spec["components"]["schemas"]["Formattable"] = {
"type" => "object",
"required" => ["format"],
"properties" => {
"format" => {
"type" => "string",
"enum" => ["plain", "markdown", "custom"],
"readOnly" => true,
"description" => "Indicates the formatting language of the raw text",
"example" => "markdown"
},
"raw" => {
"type" => "string",
"description" => "The raw text, as entered by the user",
"example" => "I **am** formatted!"
},
"html" => {
"type" => "string",
"readOnly" => true,
"description" => "The text converted to HTML according to the format",
"example" => "I <strong>am</strong> formatted!"
}
},
"example" => { "format" => "markdown", "raw" => "I am formatted!", "html" => "I am formatted!" }
}
end
def add_link_schema!(spec)
spec["components"]["schemas"]["Link"] = {
"type" => "object",
"required" => ["href"],
"properties" => {
"href" => {
"type" => "string",
"nullable" => true,
"format" => "uri",
"description" => "URL to the referenced resource (might be relative)"
},
"title" => {
"type" => "string",
"description" => " Representative label for the resource"
},
"templated" => {
"type" => "boolean",
"default" => false,
"description" => "If true the href contains parts that need to be replaced by the client"
},
"method" => {
"type" => "string",
"default" => "GET",
"description" => "The HTTP verb to use when requesting the resource",
},
"payload" => {
"type" => "string",
"description" => "The payload to send in the request to achieve the desired result"
},
"identifier" => {
"type" => "string",
"description" => " An optional unique identifier to the link object"
}
},
"examples" => [
{ "href" => nil },
{ "href" => "/api/v3/work_packages", "method" => "POST" },
{ "href" => "/api/v3/examples/{example_id}", "templated" => true },
{ "href" => "urn:openproject-org:api:v3:undisclosed" }
]
}
end
def add_missing_models!(spec, apibp:)
lines = apibp.lines.to_a
model_candidates = lines.select { |l| l.strip.start_with?("## ") && l.strip.end_with?("]") && l.include?("[/") }
model_candidates.each do |model|
extract_model_example! spec, model, lines
end
end
def extract_model_example!(spec, heading, lines)
model_lines = lines
.drop(lines.index(heading))
.drop(1)
.take_while { |l| not l.strip.start_with?("#") }
return unless model_lines.include? "+ Model\n"
model_name = heading[(heading.index(" "))..(heading.index("[") - 1)].strip
json = model_lines
.drop_while { |l| not l.start_with?(" " * 8) }
.take_while { |l| l.start_with?(" " * 8) || l.strip.blank? }
.join
begin
key = model_name.gsub(" ", "_") + "Model"
example = JSON.parse json
spec["components"]["schemas"][key] = Hash(spec["components"]["schemas"][key]).deep_merge({
"type" => "object",
"example" => example
})
unused_key = key.sub(/Model\Z/, "")
spec["components"]["schemas"].delete unused_key if spec["components"]["schemas"][unused_key].blank?
rescue => e
case model_name
when 'Markdown', 'Plain Text'
spec["components"]["schemas"][key] = Hash(spec["components"]["schemas"][key]).deep_merge({
"type" => "string",
"format" => "html",
"example" => json.strip
})
else
STDERR.puts "Failed to parse model example for #{model_name}: #{e.message}"
end
end
end
def schema_names(spec)
names = spec["paths"]
.values
.flat_map { |p|
p.values.flat_map { |v| v["tags"] }
}
.uniq
.map(&:singularize)
.map { |n| n.gsub(" ", "") }
.reject { |n| n == 'Actions&Capability' }
names << 'ActionsAndCapabilities'
names
end
def schema_from_tag(tag, schema_names:)
name = tag["name"].singularize.gsub(" ", "")
return nil unless schema_names.include? name
{
name => schema_object(name, tag["description"], schema_names: schema_names)
}
end
def schema_object(name, description, schema_names:)
properties, required_properties = local_properties description: description, schema_names: schema_names
actions, _ = link_properties description, heading: "Actions", read_only: true
links, required_links = link_properties description, heading: "Linked Properties"
links = Hash(actions).merge Hash(links)
if links.present?
properties ||= {}
properties["_links"] = {
"type" => "object",
"required" => required_links,
"properties" => links
}
.reject { |k, v| v.nil? }
end
{
"type" => "object",
"required" => required_properties,
"properties" => properties
}
.reject { |k, v| v.nil? }
end
def link_properties(description, heading:, read_only: nil)
lines = description
.lines
.drop_while { |l| not l =~ /## #{heading}/i }
.drop_while { |l| not l =~ /\A\|\s*Link\s*\|/ }
.take_while { |l| l =~ /\A\|/ }
lines.delete_at 1 # delete header line
data = lines.map { |l| l.split("|")[1..-2].map(&:strip) }
return nil if data.empty?
header = data.first
name_index = header.index "Link"
desc_index = header.index "Description"
type_index = header.index "Type"
cons_index = header.index "Constraints"
sops_index = header.index "Supported operations"
cond_index = header.index "Condition"
required = []
properties = data[1..-1].map do |row|
name = row[name_index]
type = (type_index && String(row[type_index].presence)) || 'object'
link = {}
value = {
"allOf" => [{ "$ref" => "#/components/schemas/Link" }, link]
}
set_description! link, row, desc_index
set_read_write! link, row, sops_index
set_constraints! link, row, cons_index
if !read_only.nil?
link["readOnly"] = true
end
if type_index
if link["description"].present?
link["description"] = "#{link['description']}\n\n**Resource**: #{row[type_index]}"
else
link["description"] = "**Resource**: #{row[type_index]}"
end
end
required << name if property_required?(row, cons_index)
add_conditions! link, row, cond_index
[name, value]
end
[properties.to_h, required.presence]
end
def local_properties(description:, schema_names:)
lines = description
.lines
.drop_while { |l| not l =~ /## Local Properties/i }
.drop_while { |l| not l =~ /\A\|\s*Property\s*\|/ }
.take_while { |l| l =~ /\A\|/ }
lines.delete_at 1 # delete header line
data = lines.map { |l| l.split("|")[1..-2].map(&:strip) }
return nil if data.empty?
header = data.first
name_index = header.index "Property"
desc_index = header.index "Description"
type_index = header.index "Type"
cons_index = header.index "Constraints"
sops_index = header.index "Supported operations"
cond_index = header.index "Condition"
required = []
properties = data[1..-1].map do |row|
name = row[name_index]
type = (type_index && String(row[type_index].presence)) || 'object'
if schema_names.include? type
next [name, { '$ref' => '#/components/schemas/#{type}' }]
end
value = map_type type
set_description! value, row, desc_index
set_read_write! value, row, sops_index
set_constraints! value, row, cons_index
required << name if property_required?(row, cons_index)
if name == "language"
if value.include? "description"
value["description"] = "#{value['description']} | ISO 639-1 format"
else
value["description"] = "ISO 639-1 format"
end
end
add_conditions! value, row, cond_index
[name, value]
end
[properties.to_h, required.presence]
end
def type_in_schemas?(type)
["formattable"].include? type
end
def add_conditions!(data, row, index)
value = index && String(row[index]).presence
return unless value
if data.include? "description"
data["description"] = "#{data['description']}\n\n# Conditions\n\n#{value}"
else
data["description"] = "# Conditions\n\n#{value}"
end
end
def set_constraints!(data, row, index)
return if index.nil?
value = String(row[index])
set_minimum! data, value
set_maximum! data, value
set_min_max_length! data, value
end
def set_enum!(data, value)
return unless value.downcase.strip.starts_with? "in: "
values = value.split(":").last.strip
values = "[#{values}]" unless values.starts_with? "["
data["enum"] = YAML.load values
end
def set_min_max_length!(data, value)
return unless data["type"] == "string"
if value.downcase.include?('not empty')
data["minLength"] = 1
elsif value =~ /(\d+)\s+min\s+length/i
data["minLength"] = $1.to_i
elsif value =~ /(\d+)\s+max\s+length/i
data["maxLength"] = $1.to_i
end
end
def set_minimum!(data, value)
return unless value =~ /x\s+>(=)?\s+(\d+)/
data["minimum"] = $2.to_i
data["exclusiveMinimum"] = true unless $1
end
def set_maximum!(data, value)
return unless value =~ /x\s+<(=)?\s+(\d+)/
data["maximum"] = $2.to_i
data["exclusiveMaximum"] = true unless $1
end
def property_required?(row, index)
return false if index.nil?
String(row[index]).downcase.include? 'not null'
end
def set_read_write!(data, row, sops_index)
return if sops_index.nil?
value = String(row[sops_index]).downcase
read = value.include? "read"
write = value.include? "write"
if read and not write
data["readOnly"] = true
elsif write and not read
data["writeOnly"] = true
end
end
def set_description!(data, row, index)
return nil unless index
value = String(row[index])
data["description"] = value if value.present?
end
def map_type(type)
value = type.downcase
case value
when 'date'
{ 'type' => 'string', 'format' => 'date' }
when 'datetime'
{ 'type' => 'string', 'format' => 'date-time' }
when 'url'
{ 'type' => 'string', 'format' => 'uri' }
when 'duration'
{ 'type' => 'string', 'format' => 'duration' }
else
{ 'type' => value }
end
end
end
end
end

@ -79,6 +79,16 @@ module API
get '/' do
RootRepresenter.new({}, current_user: current_user)
end
get '/spec.json' do
API::OpenAPI.spec
end
get '/spec.yml' do
content_type 'text/vnd.yaml'
API::OpenAPI.spec.to_yaml
end
end
end
end

@ -45,4 +45,14 @@ namespace :api do
puts "#{method} #{path}"
end
end
desc 'Saves the API spec (OAS3.0) to ./docs/api/openproject-apiv3-<branch>.yml'
task :update_spec, [:branch] => [:environment] do |task, args|
branch = (args[:branch] || "stable").to_sym
spec = API::OpenAPI::BlueprintImport.convert version: branch
File.open(Rails.application.root.join("docs/api/apiv3-oas-#{branch}.yml"), "w") do |f|
f.write spec.to_yaml
end
end
end

Loading…
Cancel
Save