269 lines
7.7 KiB
JavaScript
269 lines
7.7 KiB
JavaScript
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const YAML = require('yaml');
|
|||
|
|
|
|||
|
|
const repoRoot = process.cwd();
|
|||
|
|
const rootFile = path.join(repoRoot, 'pocket-base', 'spec', 'openapi-wx.yaml');
|
|||
|
|
const splitDir = path.join(repoRoot, 'pocket-base', 'spec', 'openapi-wx');
|
|||
|
|
|
|||
|
|
const filePaths = [
|
|||
|
|
rootFile,
|
|||
|
|
...fs
|
|||
|
|
.readdirSync(splitDir)
|
|||
|
|
.filter((name) => name.endsWith('.yaml'))
|
|||
|
|
.map((name) => path.join(splitDir, name)),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const docs = new Map(
|
|||
|
|
filePaths.map((filePath) => [filePath, YAML.parse(fs.readFileSync(filePath, 'utf8'))]),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
function decodePointerSegment(segment) {
|
|||
|
|
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getByPointer(documentData, pointer) {
|
|||
|
|
if (!pointer || pointer === '#' || pointer === '') {
|
|||
|
|
return documentData;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const segments = pointer
|
|||
|
|
.replace(/^#/, '')
|
|||
|
|
.split('/')
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.map(decodePointerSegment);
|
|||
|
|
|
|||
|
|
let current = documentData;
|
|||
|
|
for (const segment of segments) {
|
|||
|
|
if (current == null) {
|
|||
|
|
return undefined;
|
|||
|
|
}
|
|||
|
|
current = current[segment];
|
|||
|
|
}
|
|||
|
|
return current;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveRef(ref, currentFile) {
|
|||
|
|
const [filePart, hashPart = ''] = ref.split('#');
|
|||
|
|
const targetFile = filePart
|
|||
|
|
? path.resolve(path.dirname(currentFile), filePart)
|
|||
|
|
: currentFile;
|
|||
|
|
const targetDoc = docs.get(targetFile);
|
|||
|
|
if (!targetDoc) {
|
|||
|
|
return { targetFile, targetKey: `${targetFile}#${hashPart}`, schema: undefined };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const pointer = hashPart ? `#${hashPart}` : '#';
|
|||
|
|
return {
|
|||
|
|
targetFile,
|
|||
|
|
targetKey: `${targetFile}${pointer}`,
|
|||
|
|
schema: getByPointer(targetDoc, pointer),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function pickType(typeValue) {
|
|||
|
|
if (Array.isArray(typeValue)) {
|
|||
|
|
return typeValue.find((item) => item !== 'null') || typeValue[0];
|
|||
|
|
}
|
|||
|
|
return typeValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function cleanLabelText(text, fallback) {
|
|||
|
|
if (!text || typeof text !== 'string') {
|
|||
|
|
return fallback;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const normalized = text
|
|||
|
|
.split(/\r?\n/)
|
|||
|
|
.map((line) => line.trim())
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.map((line) => line.replace(/^[\-\d\.\)\s]+/, '').trim())
|
|||
|
|
.find((line) => line && !/^(可选|必填|说明|注意)[。::]?$/u.test(line));
|
|||
|
|
|
|||
|
|
if (!normalized) {
|
|||
|
|
return fallback;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return normalized.replace(/[`"'<>]/g, '').trim() || fallback;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function placeholderKey(key) {
|
|||
|
|
return typeof key === 'string' && key.includes('|');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function deepMerge(baseValue, nextValue) {
|
|||
|
|
if (
|
|||
|
|
baseValue &&
|
|||
|
|
nextValue &&
|
|||
|
|
typeof baseValue === 'object' &&
|
|||
|
|
typeof nextValue === 'object' &&
|
|||
|
|
!Array.isArray(baseValue) &&
|
|||
|
|
!Array.isArray(nextValue)
|
|||
|
|
) {
|
|||
|
|
const merged = { ...baseValue };
|
|||
|
|
const nextKeys = Object.keys(nextValue);
|
|||
|
|
const hasConcreteNextKeys = nextKeys.some((key) => !placeholderKey(key));
|
|||
|
|
|
|||
|
|
if (hasConcreteNextKeys) {
|
|||
|
|
for (const key of Object.keys(merged)) {
|
|||
|
|
if (placeholderKey(key)) {
|
|||
|
|
delete merged[key];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const [key, value] of Object.entries(nextValue)) {
|
|||
|
|
if (key in merged) {
|
|||
|
|
merged[key] = deepMerge(merged[key], value);
|
|||
|
|
} else {
|
|||
|
|
merged[key] = value;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return merged;
|
|||
|
|
}
|
|||
|
|
return nextValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildLabel(schemaLike, fallback) {
|
|||
|
|
return cleanLabelText(schemaLike?.description || schemaLike?.title, fallback);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function fallbackScalar(label, fieldType) {
|
|||
|
|
return `${label}|${fieldType || 'string'}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildExample(schemaLike, currentFile, fieldName = 'field', seenRefs = new Set()) {
|
|||
|
|
if (schemaLike == null) {
|
|||
|
|
return fallbackScalar(fieldName, 'string');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (typeof schemaLike !== 'object' || Array.isArray(schemaLike)) {
|
|||
|
|
return fallbackScalar(fieldName, 'string');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (schemaLike.$ref) {
|
|||
|
|
const { targetFile, targetKey, schema } = resolveRef(schemaLike.$ref, currentFile);
|
|||
|
|
if (!schema) {
|
|||
|
|
return fallbackScalar(fieldName, 'string');
|
|||
|
|
}
|
|||
|
|
if (seenRefs.has(targetKey)) {
|
|||
|
|
return fallbackScalar(fieldName, 'object');
|
|||
|
|
}
|
|||
|
|
const nextSeen = new Set(seenRefs);
|
|||
|
|
nextSeen.add(targetKey);
|
|||
|
|
return buildExample(schema, targetFile, fieldName, nextSeen);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (schemaLike.allOf) {
|
|||
|
|
return schemaLike.allOf.reduce((accumulator, item) => {
|
|||
|
|
const nextValue = buildExample(item, currentFile, fieldName, seenRefs);
|
|||
|
|
return deepMerge(accumulator, nextValue);
|
|||
|
|
}, {});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (schemaLike.oneOf && schemaLike.oneOf.length > 0) {
|
|||
|
|
return buildExample(schemaLike.oneOf[0], currentFile, fieldName, seenRefs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (schemaLike.anyOf && schemaLike.anyOf.length > 0) {
|
|||
|
|
return buildExample(schemaLike.anyOf[0], currentFile, fieldName, seenRefs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const fieldType =
|
|||
|
|
pickType(schemaLike.type) ||
|
|||
|
|
(schemaLike.properties || schemaLike.additionalProperties ? 'object' : undefined) ||
|
|||
|
|
(schemaLike.items ? 'array' : undefined) ||
|
|||
|
|
'string';
|
|||
|
|
const fieldLabel = buildLabel(schemaLike, fieldName);
|
|||
|
|
|
|||
|
|
if (fieldType === 'object') {
|
|||
|
|
const exampleObject = {};
|
|||
|
|
|
|||
|
|
if (schemaLike.properties && typeof schemaLike.properties === 'object') {
|
|||
|
|
for (const [propertyName, propertySchema] of Object.entries(schemaLike.properties)) {
|
|||
|
|
exampleObject[propertyName] = buildExample(
|
|||
|
|
propertySchema,
|
|||
|
|
currentFile,
|
|||
|
|
propertyName,
|
|||
|
|
seenRefs,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (Object.keys(exampleObject).length === 0 && schemaLike.additionalProperties) {
|
|||
|
|
exampleObject[`${fieldLabel}字段|string`] = `${fieldLabel}值|string`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (Object.keys(exampleObject).length === 0) {
|
|||
|
|
exampleObject[`${fieldLabel}|string`] = `${fieldLabel}|string`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return exampleObject;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (fieldType === 'array') {
|
|||
|
|
const itemFieldName = fieldName.endsWith('s') ? fieldName.slice(0, -1) : `${fieldName}_item`;
|
|||
|
|
return [buildExample(schemaLike.items || {}, currentFile, itemFieldName, seenRefs)];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return fallbackScalar(fieldLabel, fieldType);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function httpMethods(pathItem) {
|
|||
|
|
return ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'].filter(
|
|||
|
|
(method) => pathItem && typeof pathItem[method] === 'object',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const filePath of filePaths.filter((file) => file !== rootFile)) {
|
|||
|
|
const documentData = docs.get(filePath);
|
|||
|
|
if (!documentData || typeof documentData !== 'object') {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (documentData.components && documentData.components.schemas) {
|
|||
|
|
for (const [schemaName, schemaValue] of Object.entries(documentData.components.schemas)) {
|
|||
|
|
schemaValue.example = buildExample(schemaValue, filePath, schemaName, new Set());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (documentData.paths) {
|
|||
|
|
for (const pathItem of Object.values(documentData.paths)) {
|
|||
|
|
for (const method of httpMethods(pathItem)) {
|
|||
|
|
const operation = pathItem[method];
|
|||
|
|
|
|||
|
|
const requestContent = operation?.requestBody?.content;
|
|||
|
|
if (requestContent && typeof requestContent === 'object') {
|
|||
|
|
for (const media of Object.values(requestContent)) {
|
|||
|
|
if (media && media.schema) {
|
|||
|
|
delete media.examples;
|
|||
|
|
media.example = buildExample(media.schema, filePath, 'request', new Set());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const responses = operation?.responses;
|
|||
|
|
if (responses && typeof responses === 'object') {
|
|||
|
|
for (const response of Object.values(responses)) {
|
|||
|
|
const responseContent = response?.content;
|
|||
|
|
if (!responseContent || typeof responseContent !== 'object') {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const media of Object.values(responseContent)) {
|
|||
|
|
if (media && media.schema) {
|
|||
|
|
delete media.examples;
|
|||
|
|
media.example = buildExample(media.schema, filePath, 'response', new Set());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs.writeFileSync(filePath, YAML.stringify(documentData, { lineWidth: 0 }), 'utf8');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`Updated examples in ${filePaths.length - 1} split OpenAPI files.`);
|