diff --git a/services/groupware/README.md b/services/groupware/README.md index a476e53422..0f525b1901 100644 --- a/services/groupware/README.md +++ b/services/groupware/README.md @@ -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. diff --git a/services/groupware/api-examples.yaml b/services/groupware/api-examples.yaml deleted file mode 100644 index ddc474c6cc..0000000000 --- a/services/groupware/api-examples.yaml +++ /dev/null @@ -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' - - diff --git a/services/groupware/api-params.yaml b/services/groupware/api-params.yaml deleted file mode 100644 index 20a2e78c63..0000000000 --- a/services/groupware/api-params.yaml +++ /dev/null @@ -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 diff --git a/services/groupware/apidoc-process.ts b/services/groupware/apidoc-process.ts deleted file mode 100644 index a85a63b62b..0000000000 --- a/services/groupware/apidoc-process.ts +++ /dev/null @@ -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() -const unresolvedExampleReferences = new Set() - -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(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") - } - } -}) diff --git a/services/groupware/apidoc.yml b/services/groupware/apidoc.yml index 42da082b2a..d268807018 100644 --- a/services/groupware/apidoc.yml +++ b/services/groupware/apidoc.yml @@ -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