mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-06 20:18:58 -06:00
groupware: update README to new API documentation generation process, and remove defunct files
This commit is contained in:
@@ -4,9 +4,11 @@ The OpenCloud Groupware service provides a REST API for performing all the backe
|
||||
|
||||
## OpenAPI Documentation
|
||||
|
||||
To generate the OpenAPI ("Swagger") documentation of the REST API, [`pnpm`](https://pnpm.io/) is a pre-requisite.
|
||||
To generate the OpenAPI ("Swagger") documentation of the REST API, [`pnpm`](https://pnpm.io/) is a pre-requisite,
|
||||
as well as [`the groupware-apidocs tool`](https://github.com/opencloud-eu/groupware-apidocs).
|
||||
|
||||
Run the following command in this directory to generate the `swagger.yml` OpenAPI definition file:
|
||||
After building and installing `groupware-apidocs` somewhere in your `PATH`,
|
||||
run the following command in this directory to generate the `swagger.yml` OpenAPI definition file:
|
||||
|
||||
```bash
|
||||
make apidoc
|
||||
@@ -18,10 +20,6 @@ To generate a static HTML file using [Redocly](https://redocly.com/), which will
|
||||
make apidoc-static
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
Path parameters are documented in the file [`api-params.yaml`](file:api-params.yaml) and injected into the OpenAPI specification using the script [`apidoc-process.ts`](file:apidoc-process.ts) (which is done automatically when using the `Makefile` as described above.)
|
||||
|
||||
### Favicon
|
||||
|
||||
A [favicon](https://developer.mozilla.org/en-US/docs/Glossary/Favicon) is inserted into the static (Redocly) HTML file as part of the build process in the `Makefile`, using [`favicon.png`](file:favicon.png) as the source, computing its base64 to insert it as an image using a [data URL](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data) in order to embed it.
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
examples:
|
||||
refs:
|
||||
accountId: 'a'
|
||||
emailId: 'bmaaaaa2'
|
||||
threadId: 'b'
|
||||
mailboxIds: {"a": true}
|
||||
emailKeywords: ["$seen", "$notjunk"]
|
||||
emailSize: 3794
|
||||
emailReceivedAt: '2025-09-23T10:58:03Z'
|
||||
emailSentAt: '2025-09-23T12:58:03+02:00'
|
||||
blobId: 'cfz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcqjwaf'
|
||||
attachmentType: 'application/pdf'
|
||||
attachmentSize: 192128
|
||||
attachmentDisposition: 'attachment'
|
||||
attachmentPartId: '3'
|
||||
attachmentCharset: 'utf-8'
|
||||
attachmentCid: 'c1'
|
||||
emailAddressName: 'Camina Drummer'
|
||||
emailAddressEmail: 'drummer@opa.org'
|
||||
emailSenders:
|
||||
- name: 'Chrisjen Avasarala'
|
||||
email: 'secgen@earth.gov'
|
||||
emailFroms:
|
||||
- name: 'Chrissie'
|
||||
email: 'secgen@earth.gov'
|
||||
emailTos:
|
||||
- name: 'Camina Drummer'
|
||||
email: 'drummer@opa.org'
|
||||
emailCCs:
|
||||
- name: 'Naomi Nagata'
|
||||
email: 'nagata@opa.org'
|
||||
- name: 'James Holden'
|
||||
email: 'holden@earth.gov'
|
||||
emailBCCs:
|
||||
- name: 'Fred Johnson'
|
||||
email: 'johnson@opa.org'
|
||||
emailSubject: 'Food for thought'
|
||||
emailPreview: >-
|
||||
No one starts a war unless I say then can.
|
||||
emailAttachments:
|
||||
- partId: '2'
|
||||
blobId: 'cfz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcqjwaf'
|
||||
size: 1374
|
||||
type: 'application/pdf'
|
||||
name: 'the_path.pdf'
|
||||
disposition: 'attachment'
|
||||
- partId: '3'
|
||||
blobId: 'cnz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcq0wqo'
|
||||
charset: 'utf-8'
|
||||
size: 728
|
||||
type: 'text/plain'
|
||||
name: 'secrets.txt'
|
||||
disposition: 'attachment'
|
||||
- partId: '4'
|
||||
blobId: 'caqyey2wobo2bzjkkp2qlsn1ctitl02yylscnb77lc79nvubjihliaiadq'
|
||||
size: 787545
|
||||
name: 'molecule-design.png'
|
||||
type: 'image/png'
|
||||
disposition: 'inline'
|
||||
cid: 'c1'
|
||||
inject:
|
||||
Email:
|
||||
size: $emailSize
|
||||
EmailSummary:
|
||||
size: $emailSize
|
||||
EmailBodyPart:
|
||||
size: $attachmentSize
|
||||
ContactCardKind: 'individual'
|
||||
ContactCard:
|
||||
'@type': Contact
|
||||
addressBookIds:
|
||||
- aaabc2aa
|
||||
- c329aaze
|
||||
addresses:
|
||||
bu7icohc:
|
||||
'@type': Address
|
||||
components:
|
||||
- '@type': AddressComponent
|
||||
kind: number
|
||||
value: '12'
|
||||
- '@type': AddressComponent
|
||||
kind: separator
|
||||
value: ' '
|
||||
- '@type': AddressComponent
|
||||
kind: name
|
||||
value: 'Gravity Street'
|
||||
- '@type': AddressComponent
|
||||
kind: locality
|
||||
value: 'Medina Station'
|
||||
- '@type': AddressComponent
|
||||
kind: region
|
||||
value: 'Outer Belt'
|
||||
- '@type': AddressComponent
|
||||
kind: separator
|
||||
value: ' '
|
||||
- '@type': AddressComponent
|
||||
kind: postcode
|
||||
value: '618291'
|
||||
- '@type': AddressComponent
|
||||
kind: country
|
||||
value: 'Sol'
|
||||
isOrdered: true
|
||||
defaultSeparator: ', '
|
||||
countryCode: 'CA'
|
||||
coordinates: 'geo:43.6466107,-79.3889872'
|
||||
timeZone: EDT
|
||||
contexts:
|
||||
- delivery: true
|
||||
- work: true
|
||||
full: '12 Gravity Street, Medina Station, Outer Belt 618291, Sol'
|
||||
pref: 1
|
||||
anniversaries:
|
||||
yeex2wiu:
|
||||
'@type': Anniversary
|
||||
kind: birth
|
||||
date:
|
||||
'@type': PartialDate
|
||||
year: 1983
|
||||
month: 7
|
||||
day: 18
|
||||
calendarScale: iso8601
|
||||
calendars:
|
||||
uin5daen:
|
||||
'@type': Calendar
|
||||
kind: calendar
|
||||
uri: 'https://ceres.org/calendars/@cdrummer/c1'
|
||||
mediaType: application/jscontact+json
|
||||
contexts:
|
||||
private: true
|
||||
work: true
|
||||
pref: 1
|
||||
label: main
|
||||
created: '2025-09-30T11:00:12Z'
|
||||
cryptoKeys:
|
||||
iez1thoo:
|
||||
'@type': CryptoKey
|
||||
uri: 'https://opa.org/keys/@cdrummer.gpg'
|
||||
mediaType: application/pgp-keys
|
||||
contexts:
|
||||
private: true
|
||||
work: true
|
||||
pref: 10
|
||||
label: opa
|
||||
directories:
|
||||
cich5tah:
|
||||
'@type': Directory
|
||||
kind: entry
|
||||
uri: https://directory.opa.org/addrbook/cdrummer/Camina%20Drummer.vcf
|
||||
mediaType: text/vcard
|
||||
ju5iemoh:
|
||||
'@type': Directory
|
||||
kind: directory
|
||||
uri: ldap://ldap.opa.org/o=OPA,ou=Bosmangs
|
||||
pref: 1
|
||||
emails:
|
||||
xush7tae:
|
||||
'@type': EmailAddress
|
||||
address: cdrummer@opa.org
|
||||
contexts:
|
||||
work: true
|
||||
private: true
|
||||
pref: 10
|
||||
label: opa
|
||||
ra1ohjah:
|
||||
'@type': EmailAddress
|
||||
address: camina.drummer@ceres.net
|
||||
contexts:
|
||||
private: true
|
||||
pref: 20
|
||||
id: em8ahgha
|
||||
keywords:
|
||||
bosmang: true
|
||||
opa: true
|
||||
tycho: true
|
||||
rebel: true
|
||||
kind: 'individual'
|
||||
language: en-GB
|
||||
links:
|
||||
eech3oib:
|
||||
'@type': Link
|
||||
kind: contact
|
||||
uri: mailto:contact@opa.org
|
||||
pref: 1
|
||||
localizations: {}
|
||||
media:
|
||||
ohchae4a:
|
||||
'@type': Media
|
||||
kind: photo
|
||||
uri: https://static.wikia.nocookie.net/expanse/images/c/c7/Tycho-stn-14.png/revision/latest/scale-to-width-down/1000?cb=20170225140521
|
||||
mediaType: image/png
|
||||
members: {}
|
||||
name:
|
||||
'@type': Name
|
||||
components:
|
||||
- '@type': NameComponent
|
||||
kind: given
|
||||
value: Camina
|
||||
- '@type': NameComponent
|
||||
kind: surname
|
||||
value: Drummer
|
||||
isOrdered: true
|
||||
defaultSeparator: ' '
|
||||
full: 'Camina Drummer'
|
||||
nicknames:
|
||||
aumiez4y:
|
||||
'@type': Nickname
|
||||
name: Bosmang
|
||||
contexts:
|
||||
work: true
|
||||
pref: 1
|
||||
notes:
|
||||
aep1poov:
|
||||
'@type': Note
|
||||
created: '2025-09-30T11:00:12Z'
|
||||
author:
|
||||
'@type': Author
|
||||
name: 'expanse.fandom.com'
|
||||
uri: 'https://expanse.fandom.com/wiki/Camina_Drummer_(TV)'
|
||||
note: 'Cammina Drummer is a strong-willed, pragmatic, and no-nonsense Belter captain. Having a strong connection to her roots and her cultural identity, Drummer is a Belter through and through: She is resilient and adaptable, treats her crew with respect and equality, and is committed to the Belter way of life, which involves hard work, communal life shared with others, and not taking anything for granted.'
|
||||
onlineServices:
|
||||
ohne9oum:
|
||||
'@type': OnlineService
|
||||
service: 'Ring Network'
|
||||
uri: 'https://ring.example.com/contact/@cdrummer'
|
||||
user: '@cdrummer18219'
|
||||
contexts:
|
||||
private: true
|
||||
work: true
|
||||
label: ring
|
||||
organizations:
|
||||
eesa1aiv:
|
||||
'@type': Organization
|
||||
name: 'Outer Planets Alliance'
|
||||
sortAs: OPA
|
||||
contexts:
|
||||
work: true
|
||||
personalInfo:
|
||||
vibi6ine:
|
||||
'@type': PersonalInfo
|
||||
kind: expertise
|
||||
value: loyalty
|
||||
level: high
|
||||
phones:
|
||||
xaecie9e:
|
||||
'@type': Phone
|
||||
number: '+1-999-555-1234'
|
||||
features:
|
||||
main-number: true
|
||||
mobile: true
|
||||
voice: true
|
||||
text: true
|
||||
video: true
|
||||
contexts:
|
||||
private: true
|
||||
work: true
|
||||
pref: 1
|
||||
label: main
|
||||
preferredLanguages:
|
||||
en:
|
||||
'@type': LanguagePref
|
||||
language: en-GB
|
||||
contexts:
|
||||
private: true
|
||||
work: true
|
||||
prodId: 'Mock 0.0'
|
||||
relatedTo:
|
||||
'urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6':
|
||||
'@type': Relation
|
||||
relation:
|
||||
friend: true
|
||||
'8cacdfb7d1ffdb59@example.com':
|
||||
'@type': Relation
|
||||
relation: {}
|
||||
schedulingAddresses:
|
||||
xeith5qu:
|
||||
'@type': SchedulingAddress
|
||||
uri: 'https://scheduling.example.com/@cdrummer/c1'
|
||||
contexts:
|
||||
private: true
|
||||
work: true
|
||||
pref: 1
|
||||
label: main
|
||||
speakToAs:
|
||||
'@type': SpeakToAs
|
||||
grammaticalGender: feminine
|
||||
pronouns:
|
||||
taefie5a:
|
||||
'@type': Pronouns
|
||||
pronouns: 'she/her'
|
||||
contexts:
|
||||
privatr: true
|
||||
pref: 1
|
||||
titles:
|
||||
sheetei4:
|
||||
'@type': Title
|
||||
name: Bosmang
|
||||
kind: title
|
||||
organizationId: eesa1aiv
|
||||
uid: '05a6dd3b-f393-438e-a858-9024471fd9fc'
|
||||
updated: '2025-09-30T15:24:01Z'
|
||||
version: '1.0'
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
params:
|
||||
account:
|
||||
description: The identifier of the Account to use for this operation
|
||||
mailbox:
|
||||
description: The identifier of the Mailbox to perform this operation on
|
||||
emailid:
|
||||
description: The identifier of the Email to perform this operation on
|
||||
addressbookid:
|
||||
description: The identifier of the AddressBook to perform this operation on
|
||||
calendarid:
|
||||
description: The identifier of the Calendar to perform this operation on
|
||||
tasklistid:
|
||||
description: The identifier of the TaskList to perform this operation on
|
||||
@@ -1,207 +0,0 @@
|
||||
import * as fs from 'fs'
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
const API_PARAMS_CONFIG_FILE = 'api-params.yaml'
|
||||
const API_EXAMPLES_CONFIG_FILE = 'api-examples.yaml'
|
||||
|
||||
interface Response {
|
||||
$ref: string
|
||||
}
|
||||
|
||||
interface Parameter {
|
||||
type: string
|
||||
required: boolean
|
||||
format: string
|
||||
example: any
|
||||
name: string
|
||||
description: string
|
||||
in: string
|
||||
}
|
||||
|
||||
interface VerbData {
|
||||
tags: string[]
|
||||
summary: string
|
||||
description: string | undefined
|
||||
operationId: string
|
||||
parameters: Parameter[]
|
||||
responses: {[status:string]:Response}
|
||||
}
|
||||
|
||||
interface Item {
|
||||
$ref: string
|
||||
}
|
||||
|
||||
interface AdditionalProperties {
|
||||
$ref: string
|
||||
}
|
||||
|
||||
interface Property {
|
||||
description: string
|
||||
type: string
|
||||
items: Item
|
||||
example: any
|
||||
additionalProperties: AdditionalProperties
|
||||
}
|
||||
|
||||
interface Definition {
|
||||
type: string
|
||||
title: string
|
||||
required: string[]
|
||||
properties: {[property:string]:Property}
|
||||
example: string
|
||||
examples: string[]
|
||||
}
|
||||
|
||||
interface OpenApi {
|
||||
paths: {[path:string]:{[verb:string]:VerbData}}
|
||||
definitions: {[type:string]:Definition}
|
||||
}
|
||||
|
||||
interface Param {
|
||||
description: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface ParamsConfig {
|
||||
params: {[param:string]:Param}
|
||||
}
|
||||
|
||||
interface ExamplesConfigExamples {
|
||||
refs: {[id:string]:any}
|
||||
inject: {[id:string]:{[property:string]:any}}
|
||||
}
|
||||
|
||||
interface ExamplesConfig {
|
||||
examples: ExamplesConfigExamples
|
||||
}
|
||||
|
||||
let inputData = ''
|
||||
|
||||
process.stdin.on('data', (chunk) => {
|
||||
inputData += chunk.toString()
|
||||
})
|
||||
|
||||
const usedExamples = new Set<string>()
|
||||
const unresolvedExampleReferences = new Set<string>()
|
||||
|
||||
function processDescription(description: string|null|undefined): string|null|undefined {
|
||||
if (description !== null && description !== undefined) {
|
||||
return description.split("\n").map(line => line.replace(/^(\s*)![\*\-]?/, '$1*')).join("\n")
|
||||
} else {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
const paramsConfig = yaml.load(fs.readFileSync(API_PARAMS_CONFIG_FILE, 'utf8')) as ParamsConfig
|
||||
const params = paramsConfig.params || {}
|
||||
|
||||
const examplesConfig = yaml.load(fs.readFileSync(API_EXAMPLES_CONFIG_FILE, 'utf8')) as ExamplesConfig
|
||||
const exampleRefs = examplesConfig.examples.refs
|
||||
const exampleInjects = examplesConfig.examples.inject
|
||||
|
||||
const data = yaml.load(inputData) as OpenApi
|
||||
|
||||
for (const path in data.paths) {
|
||||
const pathData = data.paths[path]
|
||||
|
||||
for (const param in params) {
|
||||
if (path.includes(`{${param}}`)) {
|
||||
const paramsData = params[param] as Param
|
||||
for (const verb in pathData) {
|
||||
const verbData = pathData[verb]
|
||||
verbData.parameters ??= []
|
||||
verbData.parameters.push({
|
||||
name: param,
|
||||
required: true,
|
||||
type: paramsData.type !== undefined ? paramsData.type : 'string',
|
||||
in: 'path',
|
||||
description: paramsData.description,
|
||||
} as Parameter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do some magic with the formatting of endpoint descriptions:
|
||||
for (const verb in pathData) {
|
||||
const verbData = pathData[verb]
|
||||
verbData.description = processDescription(verbData.description)
|
||||
}
|
||||
}
|
||||
|
||||
for (const def in data.definitions) {
|
||||
const defData = data.definitions[def]
|
||||
|
||||
if (def.startsWith('TypeOf')) {
|
||||
const value = def.substring('TypeOf'.length)
|
||||
defData.title = value
|
||||
defData.example = value
|
||||
}
|
||||
|
||||
const injects = exampleInjects[def] || {}
|
||||
if (defData.properties !== null && defData.properties !== undefined) {
|
||||
for (const prop in defData.properties as any) {
|
||||
const propData = defData.properties[prop]
|
||||
|
||||
const inject = injects[prop]
|
||||
if (inject !== null && inject !== undefined) {
|
||||
propData.example = inject
|
||||
}
|
||||
|
||||
if (propData.example !== null && propData.example !== undefined) {
|
||||
if (typeof propData.example === 'string' && (propData.example as string).startsWith('$')) {
|
||||
const exampleId = propData.example.substring(1)
|
||||
const value = exampleRefs[exampleId]
|
||||
if (value === null || value === undefined) {
|
||||
unresolvedExampleReferences.add(exampleId)
|
||||
} else {
|
||||
usedExamples.add(exampleId)
|
||||
propData.example = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
propData.description = processDescription(propData.description)
|
||||
}
|
||||
} else {
|
||||
if (typeof(injects) === 'string') {
|
||||
defData.example = injects
|
||||
} else if (Array.isArray(injects)) {
|
||||
defData.examples = injects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(yaml.dump(data))
|
||||
process.stdout.write("\n")
|
||||
|
||||
if (unresolvedExampleReferences.size > 0) {
|
||||
console.error(`\x1b[33;1m⚠️ WARNING: unresolved example references not contained in ${API_PARAMS_CONFIG_FILE}:\x1b[0m`)
|
||||
unresolvedExampleReferences.forEach(item => {
|
||||
console.error(` - ${item}`)
|
||||
})
|
||||
console.error()
|
||||
}
|
||||
|
||||
const unusedExampleReferences = new Set<string>(Object.keys(exampleRefs))
|
||||
usedExamples.forEach(item => {
|
||||
unusedExampleReferences.delete(item)
|
||||
})
|
||||
|
||||
if (unusedExampleReferences.size > 0) {
|
||||
console.error(`\x1b[33;1m⚠️ WARNING: unused examples in ${API_EXAMPLES_CONFIG_FILE}:\x1b[0m`)
|
||||
unusedExampleReferences.forEach(item => {
|
||||
console.error(` - ${item}`)
|
||||
})
|
||||
console.error()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`Error occured while post-processing OpenAPI: ${error.message}`)
|
||||
} else {
|
||||
console.error("Unknown error occurred")
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,8 @@
|
||||
openapi: 3.0.4
|
||||
info:
|
||||
title: the title
|
||||
summary: the summary
|
||||
description: this is the description
|
||||
servers:
|
||||
- url: https://localhost:9200/
|
||||
description: Local Development Server
|
||||
@@ -8,7 +12,9 @@ tags:
|
||||
description: APIs that are not categorized yet
|
||||
- name: bootstrap
|
||||
x-displayName: Bootstrapping
|
||||
description: Initialization APIs
|
||||
description: |-
|
||||
APIs that are used to bootstrap clients, providing required initial information about the user,
|
||||
such as capabilities and limits.
|
||||
- name: account
|
||||
x-displayName: Accounts
|
||||
description: APIs for accounts
|
||||
|
||||
Reference in New Issue
Block a user