mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
/api/docs finished
- made into separate, lazy loaded js/css bundle in angular - added setting allowing to disable API docs - added spec lint workflow - fixed styling (line-height) - renamed/restructured
This commit is contained in:
@@ -84,3 +84,13 @@ jobs:
|
||||
run: |
|
||||
docker-compose -f docker-compose.ci.yml down --remove-orphans -t 10
|
||||
sudo chown -R $(whoami):$(id -ng) $CI_CACHE_PATH
|
||||
api-spec:
|
||||
name: APIv3 specification (OpenAPI 3.0)
|
||||
if: github.repository == 'opf/openproject'
|
||||
runs-on: [self-hosted-autoscale]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- run: npx @redocly/openapi-cli lint docs/api/apiv3/openapi-spec.yml
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.
|
||||
# See https://redoc.ly/docs/cli/ for more information.
|
||||
docs/api/apiv3/openapi-spec.yml:
|
||||
no-empty-servers:
|
||||
- '#/servers'
|
||||
info-license:
|
||||
- '#/info'
|
||||
no-ambiguous-paths:
|
||||
- '#/paths/~1api~1v3~1queries~1{id}~1star'
|
||||
- '#/paths/~1api~1v3~1queries~1{id}~1unstar'
|
||||
- '#/paths/~1api~1v3~1relations~1{id}~1form'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{id}~1activities'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{id}~1attachments'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{id}~1available_projects'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{id}~1available_relation_candidates'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{id}~1available_watchers'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{id}~1form'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{id}~1revisions'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{work_package_id}~1relations'
|
||||
- '#/paths/~1api~1v3~1work_packages~1{work_package_id}~1watchers'
|
||||
docs/api/apiv3/paths/root.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
docs/api/apiv3/components/schemas/activity_model.yml:
|
||||
spec:
|
||||
- '#/properties/details/type'
|
||||
docs/api/apiv3/paths/activity.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/components/schemas/attachment_model.yml:
|
||||
spec:
|
||||
- '#/properties/digest/type'
|
||||
docs/api/apiv3/components/schemas/link.yml:
|
||||
spec:
|
||||
- '#/examples'
|
||||
docs/api/apiv3/paths/attachments.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/filesize
|
||||
- '#/post/responses/200/content/application~1hal+json/examples/response/value'
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/staticDownloadLocation
|
||||
docs/api/apiv3/paths/attachment.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/filesize
|
||||
- '#/get/responses/200/content/application~1hal+json/examples/response/value'
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/staticDownloadLocation
|
||||
docs/api/apiv3/paths/budget.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/id
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/subject
|
||||
docs/api/apiv3/paths/category.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/components/schemas/configuration_model.yml:
|
||||
spec:
|
||||
- '#/properties/perPageOptions/type'
|
||||
docs/api/apiv3/paths/configuration.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/custom_object.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/document.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_embedded
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/description
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/addAttachment
|
||||
docs/api/apiv3/components/schemas/grid_model.yml:
|
||||
spec:
|
||||
- '#/properties/widgets/type'
|
||||
docs/api/apiv3/paths/grids.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/scope
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
docs/api/apiv3/paths/grid.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/scope
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
docs/api/apiv3/paths/news_item.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_embedded
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/description
|
||||
docs/api/apiv3/paths/post.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_embedded
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/attachments
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/project
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/self
|
||||
docs/api/apiv3/paths/priority.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/project_copy.yml:
|
||||
operation-2xx-response:
|
||||
- '#/post/responses'
|
||||
docs/api/apiv3/paths/project_work_packages.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/customField1
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/customField2
|
||||
- '#/post/responses/200/content/application~1hal+json/examples/response/value'
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/startDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/dueDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/derivedStartDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/derivedDueDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/changeParent
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/customField3
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/delete
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/logTime
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/move
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/addWatcher/payload
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/customActions
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/watch/payload
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/ancestors
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/children
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/timeEntries/type
|
||||
docs/api/apiv3/components/schemas/query_model.yml:
|
||||
spec:
|
||||
- '#/properties/filters/type'
|
||||
- '#/properties/timelineLabels/type'
|
||||
docs/api/apiv3/paths/queries.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_embedded
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/columns
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/groupBy
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/highlightedAttributes
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/project
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/results
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/self
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/sortBy
|
||||
- >-
|
||||
#/post/responses/201/content/application~1hal+json/examples/response/value/_links/user
|
||||
docs/api/apiv3/paths/queries_column.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/components/schemas/query_filter_instance_schema_model.yml:
|
||||
spec:
|
||||
- '#/properties/name/type'
|
||||
- '#/properties/filter/type'
|
||||
docs/api/apiv3/paths/queries_filter.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/queries_operator.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/queries_sort_by.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/query.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_embedded
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/columns
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/groupBy
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/highlightedAttributes
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/project
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/results
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/self
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/sortBy
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/user
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_embedded
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/columns
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/groupBy
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/highlightedAttributes
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/project
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/results
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/self
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/sortBy
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/user
|
||||
docs/api/apiv3/paths/relation.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/delay
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/delay
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links
|
||||
docs/api/apiv3/paths/revision.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/role.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/status.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/components/schemas/type_model.yml:
|
||||
spec:
|
||||
- '#/properties/color/type'
|
||||
docs/api/apiv3/paths/type.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
docs/api/apiv3/paths/version.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/customField14
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/endDate
|
||||
- '#/get/responses/200/content/application~1hal+json/examples/response/value'
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/customField4
|
||||
docs/api/apiv3/paths/wiki_page.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_embedded
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/attachments
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/project
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/self
|
||||
docs/api/apiv3/paths/work_packages.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/customField1
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/customField2
|
||||
- '#/post/responses/200/content/application~1hal+json/examples/response/value'
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/startDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/dueDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/derivedStartDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/derivedDueDate
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/changeParent
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/customField3
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/delete
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/logTime
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/move
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/addWatcher/payload
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/customActions
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/watch/payload
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/ancestors
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/children
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/timeEntries/type
|
||||
docs/api/apiv3/paths/work_packages_schemas.yml:
|
||||
no-path-trailing-slash:
|
||||
- '#/'
|
||||
docs/api/apiv3/paths/work_package.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/customField1
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/customField2
|
||||
- '#/get/responses/200/content/application~1hal+json/examples/response/value'
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/startDate
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/dueDate
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/derivedStartDate
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/derivedDueDate
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/changeParent
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/customField3
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/delete
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/logTime
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/move
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/addWatcher/payload
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/customActions
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/watch/payload
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/ancestors
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/children
|
||||
- >-
|
||||
#/get/responses/200/content/application~1hal+json/examples/response/value/_links/timeEntries/type
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/customField1
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/customField2
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/startDate
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/dueDate
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/derivedStartDate
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/derivedDueDate
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/changeParent
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/customField3
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/delete
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/logTime
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/move
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/addWatcher/payload
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/customActions
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/watch/payload
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/ancestors
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/children
|
||||
- >-
|
||||
#/patch/responses/200/content/application~1hal+json/examples/response/value/_links/timeEntries/type
|
||||
docs/api/apiv3/paths/work_package_attachments.yml:
|
||||
no-invalid-media-type-examples:
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_type
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/filesize
|
||||
- '#/post/responses/200/content/application~1hal+json/examples/response/value'
|
||||
- >-
|
||||
#/post/responses/200/content/application~1hal+json/examples/response/value/_links/staticDownloadLocation
|
||||
docs/api/apiv3/paths/work_package_relations.yml:
|
||||
operation-2xx-response:
|
||||
- '#/get/responses'
|
||||
docs/api/apiv3/components/schemas/schema_model.yml:
|
||||
spec:
|
||||
- '#/properties/_dependencies/type'
|
||||
docs/api/apiv3/components/schemas/time_entry_model.yml:
|
||||
spec:
|
||||
- '#/properties/hours/type'
|
||||
@@ -28,9 +28,12 @@
|
||||
# See docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
class DocsController < ApplicationController
|
||||
class APIDocsController < ApplicationController
|
||||
before_action :require_login
|
||||
|
||||
layout 'angular'
|
||||
|
||||
def index
|
||||
render_404 unless Setting.apiv3_docs_enabled?
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,17 @@ See docs/COPYRIGHT.rdoc for more details.
|
||||
<%= toolbar title: t(:label_api_access_key_type) %>
|
||||
|
||||
<%= styled_form_tag(admin_settings_update_api_path, method: :patch) do %>
|
||||
<section class="form--section">
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t("setting_apiv3_docs") %></legend>
|
||||
</fieldset>
|
||||
<div class="form--field">
|
||||
<%= setting_check_box :apiv3_docs_enabled %>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t(:setting_apiv3_docs_enabled_instructions_html, link: api_docs_url) %></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="form--section">
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend">Cross-Origin Resource Sharing (CORS)</legend>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
FOOBAR
|
||||
@@ -1 +0,0 @@
|
||||
<docs></docs>
|
||||
@@ -2343,6 +2343,11 @@ en:
|
||||
If CORS is enabled, these are the origins that are allowed to access OpenProject API.
|
||||
<br/>
|
||||
Please check the <a href="%{origin_link}" target="_blank">Documentation on the Origin header</a> on how to specify the expected values.
|
||||
setting_apiv3_docs: "Documentation"
|
||||
setting_apiv3_docs_enabled: "Enable docs page"
|
||||
setting_apiv3_docs_enabled_instructions_html: >
|
||||
If the docs page is enabled you can get an interactive view of the APIv3 documentation under
|
||||
<a href="%{link}" target="_blank">%{link}</a>.
|
||||
setting_email_delivery_method: "Email delivery method"
|
||||
setting_sendmail_location: "Location of the sendmail executable"
|
||||
setting_smtp_enable_starttls_auto: "Automatically use STARTTLS if available"
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ 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'
|
||||
get "/api/docs" => 'api_docs#index'
|
||||
|
||||
# Redirect deprecated issue links to new work packages uris
|
||||
get '/issues(/)' => redirect("#{rails_relative_url_root}/work_packages")
|
||||
|
||||
@@ -379,6 +379,9 @@ apiv3_cors_enabled:
|
||||
apiv3_cors_origins:
|
||||
serialized: true
|
||||
default: []
|
||||
apiv3_docs_enabled:
|
||||
default: true
|
||||
format: boolean
|
||||
notification_retention_period_days:
|
||||
default: 30
|
||||
format: int
|
||||
|
||||
@@ -36,6 +36,10 @@ class Apiv3Paths {
|
||||
this.apiV3Base = basePath + '/api/v3';
|
||||
}
|
||||
|
||||
public get openApiSpecPath():string {
|
||||
return this.apiV3Base + '/spec.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview markup path
|
||||
*
|
||||
|
||||
@@ -57,6 +57,12 @@ export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
|
||||
flash_message: { dynamic: true, value: null, inherit: false }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'api-docs.**',
|
||||
parent: 'root',
|
||||
url: '/api/docs',
|
||||
loadChildren: () => import('../../features/api-docs/openproject-api-docs.module').then(m => m.OpenprojectApiDocsModule)
|
||||
},
|
||||
{
|
||||
name: 'boards.**',
|
||||
parent: 'root',
|
||||
|
||||
@@ -135,7 +135,6 @@ import {
|
||||
slideToggleSelector
|
||||
} from "core-app/shared/components/slide-toggle/slide-toggle.component";
|
||||
import { BackupComponent, backupSelector } from "core-app/core/setup/globals/components/admin/backup.component";
|
||||
import { DocsComponent, docsSelector } from "core-app/core/setup/globals/components/docs/docs.component";
|
||||
import {
|
||||
EnterpriseBaseComponent,
|
||||
enterpriseBaseSelector,
|
||||
@@ -222,7 +221,6 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
|
||||
{ selector: slideToggleSelector, cls: SlideToggleComponent },
|
||||
{ selector: backupSelector, cls: BackupComponent },
|
||||
{ selector: opInAppNotificationBellSelector, cls: InAppNotificationBellComponent },
|
||||
{ selector: docsSelector, cls: DocsComponent }
|
||||
];
|
||||
|
||||
|
||||
|
||||
+16
-17
@@ -26,23 +26,22 @@
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { AfterViewInit, Component, ViewEncapsulation } from '@angular/core';
|
||||
import * as SwaggerUI from 'swagger-ui';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { UIRouterModule } from "@uirouter/angular";
|
||||
import { SwaggerUIComponent } from './swagger-ui/swagger-ui.component';
|
||||
import { API_DOCS_ROUTES } from './openproject-api-docs.routes';
|
||||
|
||||
export const docsSelector = 'docs';
|
||||
|
||||
@Component({
|
||||
selector: docsSelector,
|
||||
styleUrls: ['./docs.component.sass'],
|
||||
templateUrl: './docs.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
@NgModule({
|
||||
imports: [
|
||||
// Routes for /backlogs
|
||||
UIRouterModule.forChild({
|
||||
states: API_DOCS_ROUTES
|
||||
}),
|
||||
],
|
||||
declarations: [
|
||||
SwaggerUIComponent
|
||||
]
|
||||
})
|
||||
export class DocsComponent implements AfterViewInit {
|
||||
ngAfterViewInit() {
|
||||
SwaggerUI({
|
||||
dom_id: '#swagger',
|
||||
url: document.location.href.replace("docs", "api/v3/spec.json"),
|
||||
filter: true
|
||||
});
|
||||
}
|
||||
export class OpenprojectApiDocsModule {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//-- 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 { Ng2StateDeclaration, UIRouter } from "@uirouter/angular";
|
||||
import { SwaggerUIComponent } from "./swagger-ui/swagger-ui.component";
|
||||
|
||||
export const API_DOCS_ROUTES:Ng2StateDeclaration[] = [
|
||||
{
|
||||
name: 'api-docs',
|
||||
parent: 'root',
|
||||
url: '/api/docs',
|
||||
component: SwaggerUIComponent
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,57 @@
|
||||
//-- 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 { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import * as SwaggerUI from 'swagger-ui';
|
||||
|
||||
@Component({
|
||||
selector: 'op-api-docs',
|
||||
styleUrls: ['./swagger-ui.component.sass'],
|
||||
templateUrl: './swagger-ui.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SwaggerUIComponent implements AfterViewInit {
|
||||
constructor(private pathHelperService: PathHelperService) {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
SwaggerUI({
|
||||
dom_id: '#swagger',
|
||||
url: this.pathHelperService.api.v3.openApiSpecPath,
|
||||
filter: true,
|
||||
requestInterceptor: (req) => {
|
||||
if (!req.loadSpec) {
|
||||
// required to make session-based authentication work for POST requests with APIv3
|
||||
req.headers["X-Requested-With"] = "XMLHttpRequest";
|
||||
}
|
||||
return req;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
-1
@@ -204,7 +204,6 @@ $acronym-underline: 1px dotted #ddd !default;
|
||||
small {
|
||||
font-size: $small-font-size;
|
||||
color: $small-font-color;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,588 +0,0 @@
|
||||
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, single_file: false)
|
||||
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)
|
||||
|
||||
if !single_file
|
||||
split_up_schemas! spec
|
||||
split_up_paths! spec
|
||||
split_up_tags! spec
|
||||
end
|
||||
|
||||
spec
|
||||
ensure
|
||||
FileUtils.rm_f md_file if File.exist? md_file
|
||||
end
|
||||
|
||||
def split_up_schemas!(spec)
|
||||
file_path = Rails.application.root.join "docs/api/apiv3/components/schemas"
|
||||
|
||||
FileUtils.mkdir_p file_path.to_s
|
||||
|
||||
new_schemas = spec["components"]["schemas"].map do |name, content|
|
||||
identifier = name.underscore
|
||||
file_name = "#{identifier}.yml"
|
||||
|
||||
File.open(file_path.join(file_name), "w") do |f|
|
||||
f.write "# Schema: #{name}\n"
|
||||
f.write content.to_yaml
|
||||
end
|
||||
|
||||
[name, { "$ref" => "./components/schemas/#{file_name}"}]
|
||||
end
|
||||
|
||||
spec["components"]["schemas"] = new_schemas.to_h
|
||||
end
|
||||
|
||||
def split_up_tags!(spec)
|
||||
file_path = Rails.application.root.join "docs/api/apiv3/tags"
|
||||
|
||||
FileUtils.mkdir_p file_path.to_s
|
||||
|
||||
new_tags = spec["tags"].map do |value|
|
||||
identifier = value["name"].downcase.gsub("&", "and").gsub(" ", "_")
|
||||
file_name = "#{identifier}.yml"
|
||||
|
||||
File.open(file_path.join(file_name), "w") do |f|
|
||||
f.write value.to_yaml
|
||||
end
|
||||
|
||||
{ "$ref" => "./tags/#{file_name}"}
|
||||
end
|
||||
|
||||
spec["tags"] = new_tags
|
||||
end
|
||||
|
||||
def split_up_paths!(spec)
|
||||
file_path = Rails.application.root.join "docs/api/apiv3/paths"
|
||||
|
||||
FileUtils.mkdir_p file_path.to_s
|
||||
|
||||
new_paths = spec["paths"].map do |path, content|
|
||||
segments = path.sub("/api/v3", "").split("/").reject(&:blank?)
|
||||
|
||||
(0..(segments.size - 1)).each do |i|
|
||||
if i > 0 && segments[i].end_with?("id}")
|
||||
before = segments[i - 1]
|
||||
after = before.singularize
|
||||
|
||||
# certain words like 'news' can't be singularized
|
||||
if before == after
|
||||
segments[i - 1] = "#{before}_item"
|
||||
else
|
||||
segments[i - 1] = after
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
identifier = segments.reject { |s| s.end_with?("id}") }.join("_").presence || "root"
|
||||
file_name = "#{identifier}.yml"
|
||||
|
||||
File.open(file_path.join(file_name), "w") do |f|
|
||||
f.write "# #{path}\n"
|
||||
f.write fix_operation_ids!(fix_references!(content.dup, context: spec)).to_yaml
|
||||
end
|
||||
|
||||
[path, { "$ref" => "./paths/#{file_name}"}]
|
||||
end
|
||||
|
||||
raise "Splitting up into paths failed! Expected same number of paths. " unless new_paths.size == spec["paths"].size
|
||||
|
||||
spec["paths"] = new_paths.to_h
|
||||
end
|
||||
|
||||
def fix_operation_ids!(spec)
|
||||
spec.each do |key, value|
|
||||
if value.is_a? Hash
|
||||
fix_operation_ids! value
|
||||
elsif key == "operationId"
|
||||
spec[key] = spec[key].gsub " ", "_"
|
||||
end
|
||||
end
|
||||
|
||||
spec
|
||||
end
|
||||
|
||||
def fix_references!(spec, context:)
|
||||
spec.each do |key, value|
|
||||
if value.is_a? Hash
|
||||
fix_references! value, context: context
|
||||
elsif value.is_a? Array
|
||||
spec[key] = value.map { |v| v.is_a?(Hash) ? fix_references!(v.dup, context: context) : v }
|
||||
elsif key == "$ref" && value.start_with?("#/components")
|
||||
spec[key] = '.' + context.dig(*(value.split("/").drop(1) + ['$ref']))
|
||||
end
|
||||
end
|
||||
|
||||
spec
|
||||
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,
|
||||
"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" => "./link.yml" }, 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
|
||||
|
||||
if type == "Formattable"
|
||||
value.delete "type"
|
||||
|
||||
value = {
|
||||
"allOf" => [
|
||||
{ "$ref" => "./formattable.yml" },
|
||||
value
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
[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
|
||||
Reference in New Issue
Block a user