starlight-server 1.2.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +11 -1
  2. package/dist/demo/index.js +41 -27
  3. package/dist/http/cors.d.ts +33 -0
  4. package/dist/http/cors.js +33 -0
  5. package/dist/http/index.d.ts +1 -0
  6. package/dist/http/index.js +1 -0
  7. package/dist/http/mime-types.js +1 -20
  8. package/dist/http/request.d.ts +18 -2
  9. package/dist/http/request.js +28 -7
  10. package/dist/http/response.d.ts +9 -5
  11. package/dist/http/response.js +16 -8
  12. package/dist/http/server.d.ts +1 -1
  13. package/dist/http/server.js +1 -1
  14. package/dist/index.d.ts +3 -1
  15. package/dist/index.js +3 -2
  16. package/dist/logging.d.ts +4 -3
  17. package/dist/logging.js +7 -19
  18. package/dist/router/helpers.d.ts +6 -0
  19. package/dist/router/helpers.js +24 -0
  20. package/dist/router/index.d.ts +67 -38
  21. package/dist/router/index.js +107 -36
  22. package/dist/router/match-path.d.ts +13 -0
  23. package/dist/router/match-path.js +160 -0
  24. package/dist/swagger/factories.d.ts +143 -0
  25. package/dist/swagger/factories.js +231 -0
  26. package/dist/swagger/index.d.ts +54 -6
  27. package/dist/swagger/index.js +94 -58
  28. package/dist/swagger/specification.d.ts +802 -0
  29. package/dist/swagger/specification.js +130 -0
  30. package/package.json +10 -10
  31. package/src/demo/index.ts +45 -42
  32. package/src/http/cors.ts +59 -0
  33. package/src/http/index.ts +1 -0
  34. package/src/http/mime-types.ts +1 -20
  35. package/src/http/request.ts +38 -10
  36. package/src/http/response.ts +18 -17
  37. package/src/http/server.ts +2 -2
  38. package/src/index.ts +3 -2
  39. package/src/logging.ts +7 -21
  40. package/src/router/helpers.ts +27 -0
  41. package/src/router/index.ts +141 -38
  42. package/src/router/match-path.ts +178 -0
  43. package/src/swagger/factories.ts +345 -0
  44. package/src/swagger/index.ts +113 -80
  45. package/src/swagger/specification.ts +823 -0
  46. package/dist/router/cors.d.ts +0 -24
  47. package/dist/router/cors.js +0 -35
  48. package/dist/router/match.d.ts +0 -23
  49. package/dist/router/match.js +0 -172
  50. package/dist/router/parameters.d.ts +0 -50
  51. package/dist/router/parameters.js +0 -118
  52. package/dist/router/router.d.ts +0 -128
  53. package/dist/router/router.js +0 -97
  54. package/dist/swagger/factory.d.ts +0 -97
  55. package/dist/swagger/factory.js +0 -144
  56. package/dist/swagger/openapi-spec.d.ts +0 -261
  57. package/dist/swagger/openapi-spec.js +0 -5
  58. package/dist/validators/array.d.ts +0 -9
  59. package/dist/validators/array.js +0 -28
  60. package/dist/validators/boolean.d.ts +0 -4
  61. package/dist/validators/boolean.js +0 -28
  62. package/dist/validators/common.d.ts +0 -23
  63. package/dist/validators/common.js +0 -25
  64. package/dist/validators/index.d.ts +0 -20
  65. package/dist/validators/index.js +0 -38
  66. package/dist/validators/number.d.ts +0 -10
  67. package/dist/validators/number.js +0 -30
  68. package/dist/validators/object.d.ts +0 -13
  69. package/dist/validators/object.js +0 -36
  70. package/dist/validators/string.d.ts +0 -11
  71. package/dist/validators/string.js +0 -29
  72. package/src/.DS_Store +0 -0
  73. package/src/router/cors.ts +0 -54
  74. package/src/router/match.ts +0 -194
  75. package/src/router/parameters.ts +0 -175
  76. package/src/router/router.ts +0 -234
  77. package/src/swagger/factory.ts +0 -169
  78. package/src/swagger/openapi-spec.ts +0 -312
  79. package/src/validators/array.ts +0 -33
  80. package/src/validators/boolean.ts +0 -23
  81. package/src/validators/common.ts +0 -46
  82. package/src/validators/index.ts +0 -50
  83. package/src/validators/number.ts +0 -36
  84. package/src/validators/object.ts +0 -41
  85. package/src/validators/string.ts +0 -38
@@ -1,36 +1,107 @@
1
- /**
2
- * 实现路由注册
3
- * Implement route registration
4
- *
5
- * Usage:
6
- * const router = new DefaultRouter()
7
- * router.register('GET', '/hello/:name', async ({ request, response }: { request: Request, response: ResponseUtils }, params: { name: string }) => {
8
- * response.json({ hello: params.name })
9
- * })
10
- * startHTTPServer({
11
- * handler: router.handle
12
- * ...
13
- * })
14
- *
15
- * Usage With Custom Context:
16
- * interface MyContext extends BaseContext {
17
- * foo: () => void
18
- * }
19
- * class MyRouter extends BaseRouter<MyContext> {
20
- * executeWithContext(baseContext: BaseContext, route: Route<MyContext>, params: PathParamaters) {
21
- * function foo() { console.log('bar') }
22
- * const context = { ...baseContext, foo }
23
- * route.handle(context)
24
- * }
25
- * }
26
- * const router = new MyRouter()
27
- * router.register('GET', '/foobar', async ({ request, response, foo }) => {
28
- * foo()
29
- * response.json({})
30
- * })
31
- * startHTTPServer({
32
- * handler: router.handle
33
- * ...
34
- * })
35
- */
36
- export * from './router.js';
1
+ import { joinPath } from '@anjianshi/utils';
2
+ import { getPreflightRequestMethod, handleCORS } from '../http/cors.js';
3
+ import { HTTPError } from '../http/index.js';
4
+ import { Swagger } from '../swagger/index.js';
5
+ import * as helpers from './helpers.js';
6
+ import { matchPath } from './match-path.js';
7
+ export class Router {
8
+ /**
9
+ * ----------------------
10
+ * 路由定义
11
+ * ----------------------
12
+ */
13
+ routes = [];
14
+ register(route) {
15
+ this.routes.push(route);
16
+ this.registerRouteToSwagger(route);
17
+ }
18
+ /**
19
+ * ----------------------
20
+ * 全局 CORS 配置
21
+ * ----------------------
22
+ */
23
+ cors = false;
24
+ setCors(rule) {
25
+ this.cors = rule;
26
+ }
27
+ /**
28
+ * ----------------------
29
+ * Swagger 配置
30
+ * ----------------------
31
+ */
32
+ _swagger = null;
33
+ /**
34
+ * 访问绑定到这个 router 的 Swagger 实例
35
+ * (必须事先调用过 `router.bindSwagger()`)
36
+ */
37
+ get swagger() {
38
+ if (!this._swagger)
39
+ throw new Error('必须先绑定 Swagger 实例');
40
+ return this._swagger;
41
+ }
42
+ bindSwagger(swagger = new Swagger(), endpoint = '/swagger') {
43
+ if (this._swagger)
44
+ throw new Error('不能重复绑定 Swagger 实例');
45
+ this._swagger = swagger;
46
+ this.routes.forEach(this.registerRouteToSwagger.bind(this));
47
+ this.routes.push({
48
+ method: 'GET',
49
+ path: joinPath(endpoint, '/*'),
50
+ handler: async (context) => {
51
+ if (context.pathParameters['*'] === undefined && !context.request.path.endsWith('/')) {
52
+ context.response.redirect(joinPath(endpoint, '/'), true);
53
+ }
54
+ else {
55
+ await swagger.output(context.response, context.pathParameters['*']);
56
+ }
57
+ },
58
+ });
59
+ }
60
+ registerRouteToSwagger(route) {
61
+ if (this._swagger)
62
+ this._swagger.registerOperation(route.method, route.path, route.document ?? {});
63
+ }
64
+ /**
65
+ * -------------------------------
66
+ * route 调用 / context 配置
67
+ * -------------------------------
68
+ */
69
+ executor = async (basicContext, route) => {
70
+ await route.handler(basicContext);
71
+ };
72
+ setExecutor(executor) {
73
+ this.executor = executor;
74
+ }
75
+ /**
76
+ * ----------------------
77
+ * 请求处理
78
+ * ----------------------
79
+ */
80
+ handle = async (request, response) => {
81
+ const pathMatchedRoutes = matchPath(this.routes.map(route => route.path), request.path).map(result => ({ route: this.routes[result.index], parameters: result.parameters }));
82
+ if (!pathMatchedRoutes.length)
83
+ throw new HTTPError(404); // 没有路径匹配的路由
84
+ const method = request.method; // 请求的实际 method
85
+ const matched = pathMatchedRoutes.find(match => match.route.method === method);
86
+ if (!matched) {
87
+ const preflightTargetMethod = getPreflightRequestMethod(request); // 对于 CORS Preflight 请求,此为客户端原本希望请求的 method
88
+ const preflightMatched = pathMatchedRoutes.find(match => match.route.method === preflightTargetMethod);
89
+ if (preflightMatched) {
90
+ const corsRule = preflightMatched.route.cors ?? this.cors;
91
+ handleCORS(request, response, typeof corsRule === 'function' ? corsRule(request) : corsRule);
92
+ response.nodeResponse.end('');
93
+ return;
94
+ }
95
+ throw new HTTPError(405); // 有路径匹配的路由,但是没有 method 匹配的
96
+ }
97
+ const basicContext = {};
98
+ Object.assign(basicContext, {
99
+ request,
100
+ response,
101
+ pathParameters: matched.parameters,
102
+ validateQuery: helpers.validateQuery.bind(basicContext),
103
+ validateBody: helpers.validateBody.bind(basicContext),
104
+ });
105
+ await this.executor(basicContext, matched.route);
106
+ };
107
+ }
@@ -0,0 +1,13 @@
1
+ export interface MatchResult {
2
+ index: number;
3
+ pattern: string;
4
+ parameters: PathParameters;
5
+ }
6
+ export type PathParameters = {
7
+ [name: string]: string | undefined;
8
+ '*'?: string;
9
+ };
10
+ /**
11
+ * 返回与路径匹配的 pattern 及匹配得到的参数值
12
+ */
13
+ export declare function matchPath(patterns: string[], path: string): MatchResult[];
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 实现路径的模式匹配
3
+ * match(pattern, path) => match-result
4
+ *
5
+ * [支持的模式]
6
+ * 普通模式 | /abc/ | /abc/ |
7
+ * 命名参数 | /abc/:foo/bar | /abc/123/bar => { foo: '123' } |
8
+ * 可选的命名参数 | /abc/:foo?/bar | /abc/bar => {} | 可选参数若未传值,不会出现在匹配结果里
9
+ * 后续路径 | /abc/* | /abc/123/456 => { *: '123/456' } | * 只有出现在路由最后面时生效,可匹配任意长度内容,匹配结果不含开头的“/”
10
+ *
11
+ * [通用规则]
12
+ * - “模式”和“待匹配路径”开头结尾的 "/" 均不影响匹配。例如模式 /abc 可以和路径 abc/ 匹配。
13
+ * - 不区分大小写
14
+ */
15
+ import { clearSlash } from '@anjianshi/utils';
16
+ import escapeRegExp from 'lodash/escapeRegExp.js';
17
+ var NodeType;
18
+ (function (NodeType) {
19
+ NodeType["Text"] = "text";
20
+ NodeType["Named"] = "named";
21
+ NodeType["Rest"] = "rest";
22
+ })(NodeType || (NodeType = {}));
23
+ // -----------------------------------------------------
24
+ /**
25
+ * 返回与路径匹配的 pattern 及匹配得到的参数值
26
+ */
27
+ export function matchPath(patterns, path) {
28
+ // 解析 pattern 并按优先级排序
29
+ const parsedPatterns = patterns
30
+ .map((pattern, index) => {
31
+ if (patternCache.has(pattern))
32
+ return { index, pattern, ...patternCache.get(pattern) };
33
+ const nodes = parsePattern(pattern);
34
+ const regexp = patternToRegExp(nodes);
35
+ patternCache.set(pattern, { nodes, regexp });
36
+ return { index, pattern, nodes, regexp };
37
+ })
38
+ .sort((a, b) => patternSorter(a.nodes, b.nodes));
39
+ path = clearSlash(path);
40
+ return parsedPatterns
41
+ .map(({ index, pattern, nodes, regexp }) => {
42
+ const match = regexp.exec(path);
43
+ if (!match)
44
+ return null;
45
+ const parameters = formatMatchParameters(nodes, [...match].slice(1));
46
+ return { index, pattern, parameters };
47
+ })
48
+ .filter((matched) => matched !== null);
49
+ }
50
+ const patternCache = new Map();
51
+ /**
52
+ * 整理 pattern regexp 与路径匹配得到的参数值,按 pat节点定义,把正则匹配结果转换成参数值对象。
53
+ * 对于 `{ type: 'named', optional: true }` 或 `{ type: 'rest }` 的节点,若没有匹配到的值,则值为 undefined。
54
+ */
55
+ function formatMatchParameters(pattern, values) {
56
+ const parameters = {};
57
+ for (const node of pattern) {
58
+ if (node.type === NodeType.Text)
59
+ continue;
60
+ else if (node.type === NodeType.Named)
61
+ parameters[node.name] = values.shift();
62
+ else
63
+ parameters['*'] = values.shift();
64
+ }
65
+ return parameters;
66
+ }
67
+ /**
68
+ * 解析 pattern
69
+ * '/abc/:foo?/*' => [
70
+ * { type: 'text', text: 'abc' },
71
+ * { type: 'named', name: 'foo', optional: true },
72
+ * { type: 'rest' }
73
+ * ]
74
+ */
75
+ function parsePattern(pattern) {
76
+ pattern = clearSlash(pattern);
77
+ const nodes = [];
78
+ const patternParts = pattern.split('/');
79
+ for (const [i, part] of patternParts.entries()) {
80
+ if (part.startsWith(':') && part !== ':' && part !== ':?') {
81
+ const optional = part.endsWith('?');
82
+ const name = optional ? part.slice(1, -1) : part.slice(1);
83
+ if (name === '*')
84
+ throw new Error('pattern parameter name cannot be "*"');
85
+ nodes.push({ type: NodeType.Named, name, optional });
86
+ }
87
+ else if (part === '*' && i === patternParts.length - 1) {
88
+ nodes.push({ type: NodeType.Rest });
89
+ }
90
+ else {
91
+ nodes.push({ type: NodeType.Text, text: part });
92
+ }
93
+ }
94
+ return nodes;
95
+ }
96
+ /**
97
+ * 把 pattern 转换成正则表达式
98
+ *
99
+ * Nodes | Pattern | RegExp
100
+ * -------------------------- | ------------- | ----------------------------
101
+ * [] | | ^$
102
+ * ['abc'] | abc | ^abc$
103
+ * ['abc', named:foo, 'xyz'] | abc/:foo/xyz | ^abc/([^/]+?)/xyz$
104
+ * ['abc', named:foo?, 'xyz'] | abc/:foo?/xyz | ^abc(?:/([^/]+?))?/xyz$
105
+ * ['abc', named:foo?, rest] | abc/:foo?/* | ^abc(?:/([^/]+?))?(?:/(.+))?$
106
+ */
107
+ function patternToRegExp(pattern) {
108
+ const regexpParts = [];
109
+ for (const node of pattern) {
110
+ const prefix = node === pattern[0] ? '' : '/';
111
+ if (node.type === NodeType.Text) {
112
+ regexpParts.push(prefix + escapeRegExp(node.text));
113
+ }
114
+ else if (node.type === NodeType.Named) {
115
+ regexpParts.push(node.optional ? `(?:${prefix}([^/]+?))?` : `${prefix}([^/]+?)`);
116
+ }
117
+ else {
118
+ regexpParts.push(`(?:${prefix}(.+))?`);
119
+ }
120
+ }
121
+ return new RegExp('^' + regexpParts.join('') + '$', 'i');
122
+ }
123
+ /**
124
+ * 比较 pattern 的优先级(两个 pattern 都与路径匹配时,优先级更高的生效)
125
+ *
126
+ * 返回值:
127
+ * - patternA 优先:-1
128
+ * - patternB 优先:1
129
+ * - 优先级完全一样,返回 1,即后出现的优先
130
+ *
131
+ * 规则:
132
+ * 1. 挨个 node 比对
133
+ * 2. 先匹配各节点的类型,text > named > named optional > rest
134
+ * 3. 节点类型都一致,节点数量多的优先级更高
135
+ * 4. 节点类型、数量都一样,后出现的覆盖先出现的
136
+ */
137
+ function patternSorter(patternA, patternB) {
138
+ const typePriorities = [NodeType.Text, NodeType.Named, NodeType.Rest];
139
+ const length = Math.max(patternA.length, patternB.length);
140
+ for (let i = 0; i < length; i++) {
141
+ const a = patternA[i];
142
+ const b = patternB[i];
143
+ // 若 nodes 长度不一样,长的优先
144
+ if (a === undefined)
145
+ return -1;
146
+ else if (b === undefined)
147
+ return 1;
148
+ // node 类型不一样,按类型排序
149
+ const aTypePriority = typePriorities.indexOf(a.type);
150
+ const bTypePriority = typePriorities.indexOf(b.type);
151
+ if (aTypePriority !== bTypePriority)
152
+ return aTypePriority - bTypePriority;
153
+ // 若都是 named 类型但 optional 不同
154
+ if (a.type === NodeType.Named && b.type === NodeType.Named && a.optional !== b.optional) {
155
+ return a.optional ? 1 : -1;
156
+ }
157
+ }
158
+ // 长度、类型都一样,后出现的优先
159
+ return 1;
160
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * 用更符合日常习惯的参数格式,快捷生成各类 OpenAPI 定义
3
+ */
4
+ import { type OptionalFields } from '@anjianshi/utils';
5
+ import type { OpenAPI, Schema, Operation, Responses, Response, Reference, Parameter, RequestBody } from './specification.js';
6
+ type SchemaOrRef = Schema | Reference;
7
+ /**
8
+ * ---------------------
9
+ * OpenAPI Document
10
+ * ---------------------
11
+ */
12
+ export type DocumentOptions = Omit<OptionalFields<OpenAPI, 'openapi' | 'paths'>, 'info'> & {
13
+ info?: Partial<OpenAPI['info']>;
14
+ };
15
+ export declare function makeDocument(options?: DocumentOptions): OpenAPI;
16
+ /**
17
+ * ---------------------
18
+ * Operation
19
+ * ---------------------
20
+ */
21
+ export declare function makeOperation(options?: OperationOptions): Operation;
22
+ export interface OperationOptions extends Pick<Operation, 'summary' | 'description'> {
23
+ category?: string;
24
+ query?: (Parameter | Reference)[] | {
25
+ [name: string]: Schema | Omit<ParameterOptions, 'name'>;
26
+ };
27
+ /** 用来构建 RequestBody 的选项值,或引用某个 **RequestBody** 的 Reference */
28
+ body?: Parameters<typeof makeBody>[0] | Reference;
29
+ /** 用来构建 Response 的选项值,或引用某个 **Response** 的 Reference */
30
+ response?: Parameters<typeof makeResponse>[0] | Reference;
31
+ }
32
+ /** 判断一个对象是不是 OperationOptions */
33
+ export declare function isOperationOptions(value: unknown): value is OperationOptions;
34
+ /**
35
+ * -------------------------------
36
+ * Request
37
+ * -------------------------------
38
+ */
39
+ interface ParameterOptions {
40
+ name: string;
41
+ description?: string;
42
+ required?: boolean;
43
+ schema: SchemaOrRef;
44
+ }
45
+ /** 生成 query 定义 */
46
+ export declare function makeQuery(options: ParameterOptions): Parameter;
47
+ /** 生成 header 定义 */
48
+ export declare function makeHeader(options: ParameterOptions): Parameter;
49
+ /**
50
+ * 生成 RequestBody 定义,假定 body 通过 JSON 传递且内容是一个对象。
51
+ * input 支持的传值:
52
+ * 1. 生成 object Schema 所需的 properties 定义或完整选项值(ObjectOptions)
53
+ * 2. 已生成的 object Schema
54
+ *
55
+ */
56
+ export declare function makeBody(input: Record<string, SchemaOrRef> | ObjectOptions | Schema, description?: string): RequestBody;
57
+ /**
58
+ * -------------------------------
59
+ * Responses / Response
60
+ * -------------------------------
61
+ */
62
+ /** 传入 Schema 来生成 Responses 定义 */
63
+ export declare function makeResponses(schema: Schema, description?: string): Responses;
64
+ /** 传入已存在的 Response 定义来生成 Responses 定义 */
65
+ export declare function makeResponsesBy(response: Response | Reference): Responses;
66
+ /** 生成 Response 定义 */
67
+ export declare function makeResponse(schema: Schema | Record<string, SchemaOrRef>, description?: string): Response;
68
+ /**
69
+ * -------------------------------
70
+ * Schema
71
+ * -------------------------------
72
+ */
73
+ /** 判断一个对象是不是 Schema */
74
+ export declare function isSchema(value: unknown): value is Schema;
75
+ /** Schema 通用参数 */
76
+ type SchemaCommonOptions = Pick<Schema, 'title' | 'description' | 'default' | 'enum'>;
77
+ /** 生成 string Schema */
78
+ export declare function makeString(options?: StringOptions): Schema;
79
+ interface StringOptions extends SchemaCommonOptions {
80
+ min?: number;
81
+ max?: number;
82
+ pattern?: string;
83
+ }
84
+ /** 生成 number Schema */
85
+ export declare function makeNumber(options?: NumberOptions): Schema;
86
+ /** 生成 integer Schema */
87
+ export declare function makeInteger(options?: NumberOptions): Schema;
88
+ interface NumberOptions extends SchemaCommonOptions {
89
+ min?: number;
90
+ max?: number;
91
+ }
92
+ /** 生成 boolean Schema */
93
+ export declare function makeBoolean(options?: SchemaCommonOptions): Schema;
94
+ /** 生成 null Schema */
95
+ export declare function makeNull(options?: SchemaCommonOptions): Schema;
96
+ /**
97
+ * 生成 array Schema
98
+ * 可以直接传入 item 定义,也可以传入完整 options 对象
99
+ */
100
+ export declare function makeArray(rawOptions: SchemaOrRef | ArrayOptions): Schema;
101
+ interface ArrayOptions extends SchemaCommonOptions {
102
+ items: SchemaOrRef;
103
+ min?: number;
104
+ max?: number;
105
+ unique?: boolean;
106
+ }
107
+ /**
108
+ * 生成 object Schema
109
+ * 可以直接传入 properties 定义,也可以传入完整 options 对象
110
+ */
111
+ export declare function makeObject(rawOptions: Record<string, SchemaOrRef> | ObjectOptions): Schema;
112
+ interface ObjectOptions extends SchemaCommonOptions {
113
+ properties: Record<string, SchemaOrRef>;
114
+ required?: string[];
115
+ }
116
+ /** 指明一个值可能为 null,即转换为 { oneOf: [Schema, nullSchema] } */
117
+ export declare function makeNullable(schema: SchemaOrRef): Schema;
118
+ /**
119
+ * 生成内容是 MaySuccess 格式的 object Schema
120
+ * (对 MaySuccess 的定义见 @anjianshi/utils/lang/may-success.ts)
121
+ */
122
+ interface MaySuccessOptions extends SchemaCommonOptions {
123
+ /** 成功时的数据格式 */
124
+ data: SchemaOrRef;
125
+ /** 失败时的 data 字段格式(通常用不到) */
126
+ failed?: SchemaOrRef;
127
+ }
128
+ export declare function makeMaySuccess(options: SchemaOrRef | MaySuccessOptions): Schema;
129
+ /**
130
+ * -------------------------------
131
+ * Ref
132
+ * -------------------------------
133
+ */
134
+ declare function makeReference(target: keyof typeof referenceTargets, name: string): Reference;
135
+ declare function makeReference(uri: string): Reference;
136
+ export { makeReference };
137
+ declare const referenceTargets: {
138
+ schema: string;
139
+ response: string;
140
+ parameter: string;
141
+ body: string;
142
+ header: string;
143
+ };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * 用更符合日常习惯的参数格式,快捷生成各类 OpenAPI 定义
3
+ */
4
+ import { truthy } from '@anjianshi/utils';
5
+ import defaultsDeep from 'lodash/defaultsDeep.js';
6
+ import pick from 'lodash/pick.js';
7
+ export function makeDocument(options) {
8
+ return defaultsDeep(options ?? {}, {
9
+ openapi: '3.1.0',
10
+ paths: {},
11
+ info: { title: 'API Document', version: '0.0.1' },
12
+ });
13
+ }
14
+ /**
15
+ * ---------------------
16
+ * Operation
17
+ * ---------------------
18
+ */
19
+ export function makeOperation(options = {}) {
20
+ const { category, query, body, response } = options;
21
+ const parameters = Array.isArray(query)
22
+ ? query
23
+ : query
24
+ ? Object.entries(query).reduce((result, [name, options]) => {
25
+ const query = makeQuery({ name, ...('schema' in options ? options : { schema: options }) });
26
+ return [...result, query];
27
+ }, [])
28
+ : undefined;
29
+ const requestBody = body ? ('$ref' in body ? body : makeBody(body)) : undefined;
30
+ const responses = response
31
+ ? '$ref' in response
32
+ ? makeResponsesBy(response)
33
+ : makeResponses(response)
34
+ : undefined;
35
+ return {
36
+ tags: category !== undefined ? [category] : [],
37
+ summary: options.summary,
38
+ description: options.description,
39
+ parameters,
40
+ requestBody,
41
+ responses,
42
+ };
43
+ }
44
+ /** 判断一个对象是不是 OperationOptions */
45
+ export function isOperationOptions(value) {
46
+ if (typeof value !== 'object' || value === null)
47
+ return false;
48
+ return (['category', 'query', 'body', 'response'].find(key => truthy(value[key])) !== undefined);
49
+ }
50
+ /** 生成 query 定义 */
51
+ export function makeQuery(options) {
52
+ return { ...options, in: 'query' };
53
+ }
54
+ /** 生成 header 定义 */
55
+ export function makeHeader(options) {
56
+ return { ...options, in: 'header' };
57
+ }
58
+ /**
59
+ * 生成 RequestBody 定义,假定 body 通过 JSON 传递且内容是一个对象。
60
+ * input 支持的传值:
61
+ * 1. 生成 object Schema 所需的 properties 定义或完整选项值(ObjectOptions)
62
+ * 2. 已生成的 object Schema
63
+ *
64
+ */
65
+ export function makeBody(input, description) {
66
+ return {
67
+ description,
68
+ content: {
69
+ 'application/json': {
70
+ schema: isSchema(input) ? input : makeObject(input),
71
+ },
72
+ },
73
+ required: true,
74
+ };
75
+ }
76
+ /**
77
+ * -------------------------------
78
+ * Responses / Response
79
+ * -------------------------------
80
+ */
81
+ /** 传入 Schema 来生成 Responses 定义 */
82
+ export function makeResponses(schema, description) {
83
+ return {
84
+ 200: makeResponse(schema, description),
85
+ };
86
+ }
87
+ /** 传入已存在的 Response 定义来生成 Responses 定义 */
88
+ export function makeResponsesBy(response) {
89
+ return {
90
+ 200: response,
91
+ };
92
+ }
93
+ /** 生成 Response 定义 */
94
+ export function makeResponse(schema, description) {
95
+ return {
96
+ description,
97
+ content: {
98
+ 'application/json': {
99
+ schema: isSchema(schema) ? schema : makeObject(schema),
100
+ },
101
+ },
102
+ };
103
+ }
104
+ /**
105
+ * -------------------------------
106
+ * Schema
107
+ * -------------------------------
108
+ */
109
+ /** 判断一个对象是不是 Schema */
110
+ export function isSchema(value) {
111
+ if (typeof value !== 'object' || value === null)
112
+ return false;
113
+ return (['type', 'allOf', 'anyOf', 'oneOf', 'not'].find(key => truthy(value[key])) !== undefined);
114
+ }
115
+ function pickSchemaCommon(options) {
116
+ return pick(options, 'title', 'description', 'default', 'enum');
117
+ }
118
+ /** 生成 string Schema */
119
+ export function makeString(options = {}) {
120
+ return {
121
+ ...pickSchemaCommon(options),
122
+ type: 'string',
123
+ minLength: options.min,
124
+ maxLength: options.max,
125
+ pattern: options.pattern,
126
+ };
127
+ }
128
+ /** 生成 number Schema */
129
+ export function makeNumber(options = {}) {
130
+ return {
131
+ ...pickSchemaCommon(options),
132
+ type: 'number',
133
+ minimum: options.min,
134
+ maximum: options.max,
135
+ };
136
+ }
137
+ /** 生成 integer Schema */
138
+ export function makeInteger(options = {}) {
139
+ return {
140
+ ...makeNumber(options),
141
+ type: 'integer',
142
+ };
143
+ }
144
+ /** 生成 boolean Schema */
145
+ export function makeBoolean(options) {
146
+ return { ...options, type: 'boolean' };
147
+ }
148
+ /** 生成 null Schema */
149
+ export function makeNull(options) {
150
+ return { ...options, type: 'null' };
151
+ }
152
+ /**
153
+ * 生成 array Schema
154
+ * 可以直接传入 item 定义,也可以传入完整 options 对象
155
+ */
156
+ export function makeArray(rawOptions) {
157
+ const options = !('items' in rawOptions) ? { items: rawOptions } : rawOptions;
158
+ return {
159
+ ...pickSchemaCommon(options),
160
+ type: 'array',
161
+ items: options.items,
162
+ minItems: options.min,
163
+ maxItems: options.max,
164
+ uniqueItems: options.unique,
165
+ };
166
+ }
167
+ /**
168
+ * 生成 object Schema
169
+ * 可以直接传入 properties 定义,也可以传入完整 options 对象
170
+ */
171
+ export function makeObject(rawOptions) {
172
+ const options = !('properties' in rawOptions)
173
+ ? { properties: rawOptions }
174
+ : rawOptions;
175
+ return {
176
+ ...pickSchemaCommon(options),
177
+ type: 'object',
178
+ properties: options.properties,
179
+ required: options.required,
180
+ };
181
+ }
182
+ /** 指明一个值可能为 null,即转换为 { oneOf: [Schema, nullSchema] } */
183
+ export function makeNullable(schema) {
184
+ if ('$ref' in schema)
185
+ return { oneOf: [schema, makeNull()] };
186
+ const { title, description, ...restSchema } = schema;
187
+ return { title, description, oneOf: [restSchema, makeNull()] };
188
+ }
189
+ export function makeMaySuccess(options) {
190
+ const { data, failed, ...restOptions } = 'data' in options ? options : { data: options, failed: undefined };
191
+ const successSchema = makeObject({
192
+ title: '成功结果',
193
+ properties: {
194
+ success: makeBoolean({ enum: [true] }),
195
+ data,
196
+ },
197
+ });
198
+ const failedSchema = makeObject({
199
+ title: '失败结果',
200
+ properties: {
201
+ success: makeBoolean({ enum: [false] }),
202
+ message: makeString(),
203
+ code: { oneOf: [makeString(), makeNumber()] },
204
+ ...(failed ? { data: failed } : {}),
205
+ },
206
+ });
207
+ return {
208
+ ...pickSchemaCommon(restOptions),
209
+ oneOf: [successSchema, failedSchema],
210
+ };
211
+ }
212
+ function makeReference(target, uri) {
213
+ if (typeof uri === 'string') {
214
+ const name = uri;
215
+ return {
216
+ $ref: `#/components/${referenceTargets[target]}/${name}`,
217
+ };
218
+ }
219
+ else {
220
+ const uri = target;
221
+ return { $ref: uri };
222
+ }
223
+ }
224
+ export { makeReference };
225
+ const referenceTargets = {
226
+ schema: 'schemas',
227
+ response: 'responses',
228
+ parameter: 'parameters',
229
+ body: 'requestBodies',
230
+ header: 'headers',
231
+ };