diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 139acac4..79841efe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -455,6 +455,9 @@ importers: tailwind-merge: specifier: ^1.14.0 version: 1.14.0 + yaml: + specifier: ^2.3.4 + version: 2.3.4 widgets: dependencies: @@ -11911,7 +11914,7 @@ packages: dependencies: lilconfig: 2.1.0 postcss: 8.4.26 - yaml: 2.3.3 + yaml: 2.3.4 /postcss-loader@7.3.3(postcss@8.4.26)(typescript@5.2.2)(webpack@5.89.0): resolution: {integrity: sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==} @@ -14372,8 +14375,8 @@ packages: engines: {node: '>= 6'} dev: true - /yaml@2.3.3: - resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==} + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} /yargs-parser@20.2.9: diff --git a/web/app/openapi.yml/openapi.yml.liquid b/web/app/openapi.yml/openapi.yml.liquid new file mode 100644 index 00000000..6d62ddf7 --- /dev/null +++ b/web/app/openapi.yml/openapi.yml.liquid @@ -0,0 +1,90 @@ +openapi: 3.0.0 +info: + title: OSSInsight widgets api document + version: 1.0.0 + description: TODO + +servers: + - url: https://next.ossinsight.io + +tags: + - name: widgets + description: All public widgets + +paths: + {%- for widget in widgets %} + /widgets/official/{{ widget.normalized_name }}/manifest.json: + get: + tags: + - widgets + operationId: {{ widget.manifest.id | quote }} + summary: {{ widget.manifest.summary | quote }} + description: {{ widget.manifest.description | quote }} + parameters: + {%- for parameter in widget.parameters %} + - name: {{ parameter.name | quote }} + in: {{ parameter.in }} + required: {{ parameter.required }} + description: {{ parameter.description | quote }} + schema: + {{- parameter.schema | yaml: 6 }} + {{- parameter.extra | yaml: 5 }} + {%- endfor %} + responses: + 200: + description: 'Successful response for manifest request' + content: + application/json: + schema: + $ref: '#/components/schemas/WidgetManifest' + 400: + $ref: '#/components/responses/bad_request' + 404: + $ref: '#/components/responses/not_found' + {%- endfor%} + +components: + schemas: + WidgetManifest: + type: object + description: Manifest for the analyze + properties: + imageUrl: + type: string + format: uri + description: URL of the thumbnail image. + pageUrl: + type: string + format: uri + description: URL of the analysis page. + title: + type: string + description: Title of the analysis. + description: + type: string + description: Description of the analysis. + keywords: + type: array + items: + type: string + description: Keywords related to the analysis. + + responses: + bad_request: + description: Bad request - parameters missing or invalid. + not_found: + description: Resource not found. + + examples: + tidb: + description: "The id of GitHub Repository pingcap/tidb is `41986369`" + value: 41986369 + tikv: + description: "The id of GitHub Repository pingcap/tikv is `48833910`" + value: 48833910 + pingcap: + description: "The id of GitHub Organization @pingcap is `11855343`" + value: 11855343 + torvalds: + description: "The id of GitHub User @pingcap is `11855343`" + value: 1024025 \ No newline at end of file diff --git a/web/app/openapi.yml/route.ts b/web/app/openapi.yml/route.ts new file mode 100644 index 00000000..12215248 --- /dev/null +++ b/web/app/openapi.yml/route.ts @@ -0,0 +1,132 @@ +import { filterWidgetUrlParameters } from '@/app/widgets/[vendor]/[name]/utils'; +import widgets from '@ossinsight/widgets'; +import { createWidgetContext } from '@ossinsight/widgets-core/src/utils/context'; +import { NextRequest, NextResponse } from 'next/server'; +import { compile, TemplateParameter, TemplateScope } from './utils'; + +export async function GET (req: NextRequest) { + const names = req.nextUrl.searchParams.getAll('names').map(decodeURIComponent); + + const scopes = await Promise.allSettled( + Object.entries(widgets) + .filter(([name]) => { + if (names.length > 0) { + return names.includes(name); + } else { + return true; + } + }) + .filter(([_, widget]) => { + return !widget.meta.private; + }) + .map(async ([widgetName, widget]) => { + const { description, keywords, private: isPrive } = widget.meta; + + const generateMetadata = await widget.metadataGenerator(); + const parameterDefinitions = await widget.parameterDefinition(); + + const metadata = await generateMetadata({ + ...createWidgetContext('client', {}, null as any), + getCollection () { return { id: 0, name: 'Collection', public: true }; }, + getRepo () { return { id: 0, fullName: 'Repository' }; }, + getUser () { return { id: 0, login: 'Developer' };}, + getOrg () { return { id: 0, login: 'Organization' }; }, + getTimeParams () { return { zone: 'TimeZone', period: 'Period' }; }, + }); + + const parsedWidgetName = widgetName.replaceAll('@ossinsight/widget-', ''); + const finalTitle = metadata.title ?? parsedWidgetName; + const finalDescription = metadata.description ?? description ?? parsedWidgetName; + + // TODO: generate more detailed schema for special parameters + const parameters = Object.entries(parameterDefinitions) + .filter(([param]) => filterWidgetUrlParameters(widgetName, param)) + .filter(([, def]) => !('expression' in def)) + .map(([name, def]) => { + let schema: any; + let extra: any; + + switch (def.type) { + case 'repo-id': + case 'repo-ids': + case 'user-id': + case 'collection-id': + case 'owner-id': + case 'limit': + case 'time-zone': + schema = { type: 'number' }; + break; + case 'month': + case 'day': + schema = { type: 'string' }; + break; + default: + schema = { type: 'string' }; + break; + } + + if (def.array) { + schema = { type: 'array', items: { oneOf: [schema] } }; + } + + if ('enums' in def) { + schema.enum = def.enums; + } + + switch (def.type) { + case 'repo-id': + extra = { + examples: { + 'pingcap/tidb': { $ref: '#/components/examples/tidb' }, + 'tikv/tikv': { $ref: '#/components/examples/tikv' }, + }, + }; + break; + case 'user-id': + extra = { + examples: { + 'torvalds': { $ref: '#/components/examples/torvalds' }, + }, + }; + break; + case 'owner-id': + extra = { + examples: { + 'pingcap': { $ref: '#/components/examples/pingcap' }, + }, + }; + break; + } + + return { + name, + schema, + extra, + in: 'query', + description: [def.description, def.title].filter(Boolean).join(' - '), + required: def.required ? 'true' : 'false', + } satisfies TemplateParameter; + }); + + return { + normalized_name: parsedWidgetName, + group: `widgets/${finalTitle}`, + title: finalTitle, + description: finalDescription, + manifest: { + id: 'getManifest', + summary: 'Manifest for ' + finalTitle, + description: finalDescription, + }, + parameters, + } satisfies TemplateScope; + })); + + const yml = await compile({ widgets: scopes.flatMap(s => s.status === 'fulfilled' ? [s.value] : []) }); + + return new NextResponse(yml, { + headers: { + 'Content-Type': 'application/openapi+yaml', + }, + }); +} \ No newline at end of file diff --git a/web/app/openapi.yml/utils.ts b/web/app/openapi.yml/utils.ts new file mode 100644 index 00000000..1f7b46e2 --- /dev/null +++ b/web/app/openapi.yml/utils.ts @@ -0,0 +1,50 @@ +import { Liquid } from 'liquidjs'; +import { stringify } from 'yaml'; +import template from './openapi.yml.liquid'; + +export type TemplateParameter = { + name: string + in: 'query' | 'path' + required: 'true' | 'false' + description: string + schema: object + extra?: object +} + +export type TemplateScope = { + group: string + title: string + description: string + normalized_name: string + manifest: { + id: string + summary: string + description: string + } + parameters: TemplateParameter[] +} + +const liquid = new Liquid(); +liquid.registerFilter('quote', value => { + if (typeof value === 'string') { + return JSON.stringify(value); + } + return JSON.stringify(String(value)); +}); + +liquid.registerFilter('yaml', function (value, tabs) { + if (!value) { + return ''; + } + + const pfx = ' '.repeat(parseInt(tabs || 0)); + return '\n' + stringify(value) + .split('\n') + .map(line => line ? `${pfx}${line}` : line) + .join('\n'); +}); + +export async function compile (scope: { widgets: TemplateScope[] }): Promise { + const tmpl = await liquid.parse(template); + return await liquid.render(tmpl, scope); +} diff --git a/web/app/widgets/[vendor]/[name]/manifest.json/route.ts b/web/app/widgets/[vendor]/[name]/manifest.json/route.ts index aa3832a4..f534c697 100644 --- a/web/app/widgets/[vendor]/[name]/manifest.json/route.ts +++ b/web/app/widgets/[vendor]/[name]/manifest.json/route.ts @@ -17,8 +17,7 @@ export async function GET (request: NextRequest, { params: { vendor, name: param notFound(); } - const { description, keywords } = widgetMeta(name) - console.log(description, keywords) + const { description, keywords } = widgetMeta(name); const generateMetadata = await widgetMetadataGenerator(name); @@ -30,7 +29,6 @@ export async function GET (request: NextRequest, { params: { vendor, name: param parameters[key] = value; }); - const paramDef = await widgetParameterDefinitions(name); Object.assign(parameters, resolveExpressions(paramDef)); const linkedData = await resolveParameters(paramDef, parameters); diff --git a/web/env.d.ts b/web/env.d.ts index d4d25bfc..47c274ad 100644 --- a/web/env.d.ts +++ b/web/env.d.ts @@ -1,2 +1,7 @@ /// /// + +declare module '*.liquid' { + declare const template: string + export default template; +} diff --git a/web/next.config.mjs b/web/next.config.mjs index 2560e68f..e2f1289c 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -19,8 +19,11 @@ const nextConfig = { }, webpack: config => { config.module.rules.push({ - test: /\.sql$/, - use: 'raw-loader', + test: /\.sql$/, + use: 'raw-loader', + }, { + test: /\.liquid$/, + use: 'raw-loader', }) config.externals.push('@napi-rs/canvas') return config; diff --git a/web/package.json b/web/package.json index 8d97eb91..aa8e8bb0 100644 --- a/web/package.json +++ b/web/package.json @@ -45,6 +45,7 @@ "luxon": "^3.4.3", "raw-loader": "^4.0.2", "sass": "^1.68.0", - "tailwind-merge": "^1.14.0" + "tailwind-merge": "^1.14.0", + "yaml": "^2.3.4" } }