groupware: update README to new API documentation generation process, and remove defunct files

This commit is contained in:
Pascal Bleser
2026-01-22 14:17:42 +01:00
parent 1408c3debc
commit 4778cd52e7
5 changed files with 11 additions and 530 deletions

View File

@@ -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.

View File

@@ -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'

View File

@@ -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

View File

@@ -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")
}
}
})

View File

@@ -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