From f42d9b847ddd7048a7750b4b3a0c59a89cda861d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 24 Feb 2019 09:45:27 +0900
Subject: [PATCH] Improve type definitions

---
 src/prelude/schema.ts                    | 38 +++++++++++
 src/server/api/endpoints.ts              |  3 +-
 src/server/api/endpoints/charts/notes.ts | 10 +--
 src/server/api/endpoints/notes/create.ts |  6 +-
 src/server/api/openapi/gen-spec.ts       | 29 +--------
 src/services/chart/index.ts              | 16 +++++
 src/services/chart/notes.ts              | 81 ++++++++++++++----------
 7 files changed, 110 insertions(+), 73 deletions(-)
 create mode 100644 src/prelude/schema.ts

diff --git a/src/prelude/schema.ts b/src/prelude/schema.ts
new file mode 100644
index 000000000..7c3d6aac8
--- /dev/null
+++ b/src/prelude/schema.ts
@@ -0,0 +1,38 @@
+export type Schema = {
+	type: 'number' | 'string' | 'array' | 'object' | any;
+	optional?: boolean;
+	items?: Schema;
+	properties?: Obj;
+	description?: string;
+};
+
+export type Obj = { [key: string]: Schema };
+
+export type ObjType<s extends Obj> = { [P in keyof s]: SchemaType<s[P]> };
+
+// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2
+type MyType<T extends Schema> = {
+	0: any;
+	1: SchemaType<T>;
+}[T extends Schema ? 1 : 0];
+
+export type SchemaType<p extends Schema> =
+	p['type'] extends 'number' ? number :
+	p['type'] extends 'string' ? string :
+	p['type'] extends 'array' ? MyType<p['items']>[] :
+	p['type'] extends 'object' ? ObjType<p['properties']> :
+	any;
+
+export function convertOpenApiSchema(schema: Schema) {
+	const x = JSON.parse(JSON.stringify(schema)); // copy
+	if (!['string', 'number', 'boolean', 'array', 'object'].includes(x.type)) {
+		x['$ref'] = `#/components/schemas/${x.type}`;
+	}
+	if (x.type === 'object' && x.properties) {
+		x.required = Object.entries(x.properties).filter(([k, v]: any) => !v.isOptional).map(([k, v]: any) => k);
+		for (const k of Object.keys(x.properties)) {
+			x.properties[k] = convertOpenApiSchema(x.properties[k]);
+		}
+	}
+	return x;
+}
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 2873dd3c1..7abee95a5 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -1,6 +1,7 @@
 import { Context } from 'cafy';
 import * as path from 'path';
 import * as glob from 'glob';
+import { Schema } from '../../prelude/schema';
 
 export type Param = {
 	validator: Context<any>;
@@ -29,7 +30,7 @@ export interface IEndpointMeta {
 		};
 	};
 
-	res?: any;
+	res?: Schema;
 
 	/**
 	 * このエンドポイントにリクエストするのにユーザー情報が必須か否か
diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts
index d254bb854..cc0ca8bef 100644
--- a/src/server/api/endpoints/charts/notes.ts
+++ b/src/server/api/endpoints/charts/notes.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import notesChart from '../../../../services/chart/notes';
+import notesChart, { notesLogSchema } from '../../../../services/chart/notes';
+import { convertLog } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -28,12 +29,7 @@ export const meta = {
 		},
 	},
 
-	res: {
-		type: 'array',
-		items: {
-			type: 'object',
-		},
-	},
+	res: convertLog(notesLogSchema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index a4f262bda..7df86625d 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -175,12 +175,10 @@ export const meta = {
 
 	res: {
 		type: 'object',
-		props: {
+		properties: {
 			createdNote: {
 				type: 'Note',
-				desc: {
-					'ja-JP': '作成した投稿'
-				}
+				description: '作成した投稿'
 			}
 		}
 	},
diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts
index ad46eb20a..748791823 100644
--- a/src/server/api/openapi/gen-spec.ts
+++ b/src/server/api/openapi/gen-spec.ts
@@ -4,6 +4,7 @@ import config from '../../../config';
 import { errors as basicErrors } from './errors';
 import { schemas } from './schemas';
 import { description } from './description';
+import { convertOpenApiSchema } from '../../../prelude/schema';
 
 export function genOpenapiSpec(lang = 'ja-JP') {
 	const spec = {
@@ -104,33 +105,7 @@ export function genOpenapiSpec(lang = 'ja-JP') {
 
 		const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : [];
 
-		const resSchema = endpoint.meta.res ? renderType(endpoint.meta.res) : {};
-
-		function renderType(x: any) {
-			const res = {} as any;
-
-			if (['User', 'Note', 'DriveFile'].includes(x.type)) {
-				res['$ref'] = `#/components/schemas/${x.type}`;
-			} else if (x.type === 'object') {
-				res['type'] = 'object';
-				if (x.props) {
-					const props = {} as any;
-					for (const kv of Object.entries(x.props)) {
-						props[kv[0]] = renderType(kv[1]);
-					}
-					res['properties'] = props;
-				}
-			} else if (x.type === 'array') {
-				res['type'] = 'array';
-				if (x.items) {
-					res['items'] = renderType(x.items);
-				}
-			} else {
-				res['type'] = x.type;
-			}
-
-			return res;
-		}
+		const resSchema = endpoint.meta.res ? convertOpenApiSchema(endpoint.meta.res) : {};
 
 		const info = {
 			operationId: endpoint.name,
diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts
index 30ef2847d..1e6ff0ca9 100644
--- a/src/services/chart/index.ts
+++ b/src/services/chart/index.ts
@@ -9,6 +9,7 @@ import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 import { ICollection } from 'monk';
 import Logger from '../../misc/logger';
+import { Schema } from '../../prelude/schema';
 
 const logger = new Logger('chart');
 
@@ -346,3 +347,18 @@ export default abstract class Chart<T extends Obj> {
 		return res;
 	}
 }
+
+export function convertLog(logSchema: Schema): Schema {
+	const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
+	if (v.type === 'number') {
+		v.type = 'array';
+		v.items = {
+			type: 'number'
+		};
+	} else if (v.type === 'object') {
+		for (const k of Object.keys(v.properties)) {
+			v.properties[k] = convertLog(v.properties[k]);
+		}
+	}
+	return v;
+}
diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts
index 8f95f6363..d3704fed9 100644
--- a/src/services/chart/notes.ts
+++ b/src/services/chart/notes.ts
@@ -2,48 +2,61 @@ import autobind from 'autobind-decorator';
 import Chart, { Obj } from '.';
 import Note, { INote } from '../../models/note';
 import { isLocalUser } from '../../models/user';
+import { SchemaType } from '../../prelude/schema';
 
-/**
- * 投稿に関するチャート
- */
-type NotesLog = {
-	local: {
-		/**
-		 * 集計期間時点での、全投稿数
-		 */
-		total: number;
+const logSchema = {
+	total: {
+		type: 'number' as 'number',
+		description: '集計期間時点での、全投稿数'
+	},
 
-		/**
-		 * 増加した投稿数
-		 */
-		inc: number;
+	inc: {
+		type: 'number' as 'number',
+		description: '増加した投稿数'
+	},
 
-		/**
-		 * 減少した投稿数
-		 */
-		dec: number;
+	dec: {
+		type: 'number' as 'number',
+		description: '減少した投稿数'
+	},
 
-		diffs: {
-			/**
-			 * 通常の投稿数の差分
-			 */
-			normal: number;
+	diffs: {
+		type: 'object' as 'object',
+		properties: {
+			normal: {
+				type: 'number' as 'number',
+				description: '通常の投稿数の差分'
+			},
 
-			/**
-			 * リプライの投稿数の差分
-			 */
-			reply: number;
+			reply: {
+				type: 'number' as 'number',
+				description: 'リプライの投稿数の差分'
+			},
 
-			/**
-			 * Renoteの投稿数の差分
-			 */
-			renote: number;
-		};
-	};
-
-	remote: NotesLog['local'];
+			renote: {
+				type: 'number' as 'number',
+				description: 'Renoteの投稿数の差分'
+			},
+		}
+	},
 };
 
+export const notesLogSchema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+type NotesLog = SchemaType<typeof notesLogSchema>;
+
 class NotesChart extends Chart<NotesLog> {
 	constructor() {
 		super('notes');