Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
Acid Chicken (硫酸鶏) | a63ff4f660 | |
Acid Chicken (硫酸鶏) | 98ec5d0a9a | |
Acid Chicken (硫酸鶏) | 43a9a080b3 | |
Acid Chicken (硫酸鶏) | bb2135c655 | |
Acid Chicken (硫酸鶏) | 803ed2cc32 | |
Acid Chicken (硫酸鶏) | 9d328784ec |
|
@ -58,17 +58,15 @@ export function something(foo: string): string {
|
|||
```
|
||||
|
||||
## Directory structure
|
||||
```
|
||||
src ... ソースコード
|
||||
@types ... 外部ライブラリなどの型定義
|
||||
prelude ... Misskeyに関係ないかつ副作用なし
|
||||
misc ... 副作用なしのユーティリティ処理
|
||||
service ... 副作用ありの共通処理
|
||||
queue ... ジョブキューとジョブ
|
||||
server ... Webサーバー
|
||||
client ... クライアント
|
||||
mfm ... MFM
|
||||
|
||||
test ... テスト
|
||||
|
||||
```
|
||||
<dl><dt>src</dt><dd>ソースコード
|
||||
<dl><dt>@types</dt><dd>外部ライブラリなどの型定義</dd></dl>
|
||||
<dl><dt>prelude</dt><dd>Misskeyに関係ないかつ副作用なし</dd></dl>
|
||||
<dl><dt>misc</dt><dd>副作用なしのユーティリティ処理</dd></dl>
|
||||
<dl><dt>service</dt><dd>副作用ありの共通処理</dd></dl>
|
||||
<dl><dt>queue</dt><dd>ジョブキューとジョブ</dd></dl>
|
||||
<dl><dt>server</dt><dd>Webサーバー</dd></dl>
|
||||
<dl><dt>client</dt><dd>クライアント</dd></dl>
|
||||
<dl><dt>mfm</dt><dd>MFM</dd></dl>
|
||||
<dl><dt>sanctuary</dt><dd>TypeScriptの制約を強くしたエリア <strike>乃々の聖域ではない</strike></dd></dl></dd></dl>
|
||||
<dl><dt>test</dt><dd>テスト</dd></dl>
|
||||
|
|
|
@ -6,7 +6,9 @@ import * as fs from 'fs';
|
|||
import { URL } from 'url';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { Source, Mixin } from './types';
|
||||
import Mika, { optional } from '../sanctuary/mika';
|
||||
import * as pkg from '../../package.json';
|
||||
import Logger from '../misc/logger';
|
||||
|
||||
/**
|
||||
* Path of configuration directory
|
||||
|
@ -21,23 +23,140 @@ const path = process.env.NODE_ENV == 'test'
|
|||
: `${dir}/default.yml`;
|
||||
|
||||
export default function load() {
|
||||
const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source;
|
||||
const config: Source = yaml.safeLoad(fs.readFileSync(path, 'utf-8'));
|
||||
|
||||
const mixin = {} as Mixin;
|
||||
const logger = new Logger('config');
|
||||
|
||||
const errors = new Mika({
|
||||
repository_url: 'string!?',
|
||||
feedback_url: 'string!?',
|
||||
url: 'string!',
|
||||
port: 'number!',
|
||||
https: {
|
||||
[optional]: true,
|
||||
key: 'string!',
|
||||
cert: 'string!'
|
||||
},
|
||||
disableHsts: 'boolean!?',
|
||||
mongodb: {
|
||||
host: 'string!',
|
||||
port: 'number!',
|
||||
db: 'string!',
|
||||
user: 'string!?',
|
||||
pass: 'string!?'
|
||||
},
|
||||
elasticsearch: {
|
||||
[optional]: true,
|
||||
host: 'string!',
|
||||
port: 'number!',
|
||||
pass: 'string!'
|
||||
},
|
||||
drive: {
|
||||
[optional]: true,
|
||||
storage: 'string!?'
|
||||
},
|
||||
redis: {
|
||||
[optional]: true,
|
||||
host: 'string!',
|
||||
port: 'number!',
|
||||
pass: 'string!',
|
||||
db: 'number!?',
|
||||
prefix: 'string!?'
|
||||
},
|
||||
autoAdmin: 'boolean!?',
|
||||
proxy: 'string!?',
|
||||
accesslog: 'string!?',
|
||||
clusterLimit: 'number!?',
|
||||
// The below properties are defined for backward compatibility.
|
||||
name: 'string!?',
|
||||
description: 'string!?',
|
||||
localDriveCapacityMb: 'number!?',
|
||||
remoteDriveCapacityMb: 'number!?',
|
||||
preventCacheRemoteFiles: 'boolean!?',
|
||||
recaptcha: {
|
||||
[optional]: true,
|
||||
enableRecaptcha: 'boolean!?',
|
||||
recaptchaSiteKey: 'string!?',
|
||||
recaptchaSecretKey: 'string!?'
|
||||
},
|
||||
ghost: 'string!?',
|
||||
maintainer: {
|
||||
[optional]: true,
|
||||
name: 'string!',
|
||||
email: 'string!?'
|
||||
},
|
||||
twitter: {
|
||||
[optional]: true,
|
||||
consumer_key: 'string!?',
|
||||
consumer_secret: 'string!?'
|
||||
},
|
||||
github: {
|
||||
[optional]: true,
|
||||
client_id: 'string!?',
|
||||
client_secret: 'string!?'
|
||||
},
|
||||
user_recommendation: {
|
||||
[optional]: true,
|
||||
engine: 'string!?',
|
||||
timeout: 'number!?'
|
||||
},
|
||||
sw: {
|
||||
[optional]: true,
|
||||
public_key: 'string!',
|
||||
private_key: 'string!'
|
||||
}
|
||||
}).validate(config);
|
||||
|
||||
if (!errors && config.drive.storage === 'minio') {
|
||||
const minioErrors = new Mika({
|
||||
bucket: 'string!',
|
||||
prefix: 'string!',
|
||||
baseUrl: 'string!',
|
||||
config: {
|
||||
endPoint: 'string!',
|
||||
accessKey: 'string!',
|
||||
secretKey: 'string!',
|
||||
useSSL: 'boolean!?',
|
||||
port: 'number!?',
|
||||
region: 'string!?',
|
||||
transport: 'string!?',
|
||||
sessionToken: 'string!?',
|
||||
}
|
||||
}).validate(config.drive);
|
||||
|
||||
if (minioErrors)
|
||||
for (const error of minioErrors)
|
||||
errors.push(error);
|
||||
}
|
||||
|
||||
if (errors) {
|
||||
for (const { path, excepted, actual } of errors)
|
||||
logger.error(`Invalid config value detected at ${path}: excepted type is ${typeof excepted === 'string' ? excepted : 'object'}, but actual value type is ${actual}`);
|
||||
|
||||
throw 'The configuration is invalid. Check your .config/default.yml';
|
||||
}
|
||||
|
||||
const url = validateUrl(config.url);
|
||||
|
||||
config.url = normalizeUrl(config.url);
|
||||
|
||||
mixin.host = url.host;
|
||||
mixin.hostname = url.hostname;
|
||||
mixin.scheme = url.protocol.replace(/:$/, '');
|
||||
mixin.wsScheme = mixin.scheme.replace('http', 'ws');
|
||||
mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
|
||||
mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
|
||||
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
||||
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
||||
mixin.userAgent = `Misskey/${pkg.version} (${config.url})`;
|
||||
const scheme = url.protocol.replace(/:$/, '');
|
||||
const ws_scheme = scheme.replace('http', 'ws');
|
||||
|
||||
const mixin: Mixin = {
|
||||
host: url.host,
|
||||
hostname: url.hostname,
|
||||
scheme,
|
||||
wsScheme,
|
||||
wsUrl: `${wsScheme}://${url.host}`,
|
||||
apiUrl: `${scheme}://${url.host}/api`,
|
||||
authUrl: `${scheme}://${url.host}/auth`,
|
||||
devUrl: `${scheme}://${url.host}/dev`,
|
||||
docsUrl: `${scheme}://${url.host}/docs`,
|
||||
statsUrl: `${scheme}://${url.host}/stats`,
|
||||
statusUrl: `${scheme}://${url.host}/status`,
|
||||
driveUrl: `${scheme}://${url.host}/files`,
|
||||
userAgent: `Misskey/${pkg.version} (${config.url})`
|
||||
};
|
||||
|
||||
if (config.autoAdmin == null) config.autoAdmin = false;
|
||||
|
||||
|
|
|
@ -115,3 +115,18 @@ export function cumulativeSum(xs: number[]): number[] {
|
|||
for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
|
||||
return ys;
|
||||
}
|
||||
|
||||
export function toEnglishString(x: string[], n = 'and'): string {
|
||||
switch (x.length) {
|
||||
case 0:
|
||||
return '';
|
||||
|
||||
case 1:
|
||||
return x[0];
|
||||
|
||||
default:
|
||||
const y = [...x];
|
||||
const z = y.pop();
|
||||
return `${y.join(', ')}, ${n} ${z}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
/* tslint:enable:strict-type-predicates triple-equals */
|
||||
|
||||
import { toEnglishString } from '../prelude/array';
|
||||
|
||||
/* KEYWORD DEFINITION
|
||||
* { sakura: null } // 'sakura' is null.
|
||||
* { izumi: undefined } // 'izumi' is undefined.
|
||||
* {} // 'ako' is unprovided (not undefined in here).
|
||||
*
|
||||
* Reason: The undefined is a type, so you can define undefined.
|
||||
*/
|
||||
|
||||
export const additional = Symbol('Allows additional properties.');
|
||||
export const optional = Symbol('Allows unprovided (not undefined).');
|
||||
export const nullable = Symbol('Allows null.');
|
||||
|
||||
type Type = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';
|
||||
type ExtendedType = Type | 'null' | 'unprovided';
|
||||
|
||||
type Everything = string | number | bigint | boolean | symbol | undefined | object | Function | null;
|
||||
|
||||
/**
|
||||
* Manifest
|
||||
* Information for
|
||||
* Reliance
|
||||
* Identify
|
||||
* Analysis
|
||||
*/
|
||||
type Miria = {
|
||||
/** STRING TYPED SYNTAX
|
||||
* type1 : Allows the type1 and null.
|
||||
* type1|type2 : Allows type1, type2, and null.
|
||||
* type1! : Allows type1 only.
|
||||
* type1? : Allows type1, null, and unprovided.
|
||||
* type1!? : Allows type1, and unprovided.
|
||||
*
|
||||
* (! and ? are suffix.)
|
||||
* (The spaces(U+0020) are ignored.)
|
||||
*/
|
||||
[x: string]: string | Miria;
|
||||
[additional]?: boolean;
|
||||
[optional]?: boolean;
|
||||
[nullable]?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reason
|
||||
* Information for
|
||||
* Keeping safety by
|
||||
* Analysis
|
||||
*/
|
||||
type Rika = {
|
||||
path: string;
|
||||
excepted: string | Miria | null;
|
||||
actual: ExtendedType;
|
||||
};
|
||||
|
||||
type MiriaInternal = {
|
||||
[x: string]: string | MiriaInternal | undefined;
|
||||
[additional]?: boolean;
|
||||
[optional]?: boolean;
|
||||
[nullable]?: boolean;
|
||||
};
|
||||
|
||||
const $ = () => {}; // trash
|
||||
|
||||
// ^ https://github.com/Microsoft/TypeScript/issues/7061
|
||||
|
||||
/**
|
||||
* Manifest based
|
||||
* Identify objects for
|
||||
* Keeping safety by this
|
||||
* Analyzer class
|
||||
*/
|
||||
export default class Mika {
|
||||
private static readonly fuhihi = 'Miria'; // < https://github.com/Microsoft/TypeScript/issues/1579
|
||||
|
||||
protected static readonly types = ['string', 'number', 'bigint', 'boolean', 'symbol', 'undefined', 'object', 'function'];
|
||||
|
||||
protected static readonly syntax = new RegExp(`^\\s*(?:${Mika.types.join('|')})(?:\\s*\\|\\s*(?:${Mika.types.join('|')}))*\\s*!?\\s*\\??\\s*$`);
|
||||
|
||||
public static readonly additional = additional;
|
||||
|
||||
public static readonly optional = optional;
|
||||
|
||||
public static readonly nullable = nullable;
|
||||
|
||||
constructor(
|
||||
readonly miria: Miria
|
||||
) {
|
||||
Mika.ensure(miria, [], Object.keys({ miria })[0]);
|
||||
// ^~~ #1579 (see above) ~~^
|
||||
}
|
||||
|
||||
private static ensure(
|
||||
source: Miria,
|
||||
location: string[],
|
||||
nameof: string
|
||||
) {
|
||||
const errorMessage = `Specified ${nameof} is invalid.`;
|
||||
|
||||
for (const [k, v] of Object.entries(source)) {
|
||||
const invalidType = !['string', 'object'].includes(typeof v);
|
||||
const header = () => `${errorMessage} ${[...location, k].join('.')} is`;
|
||||
|
||||
if (invalidType || v === null)
|
||||
throw `${header()} ${invalidType ? `${typeof v === 'undefined' ? 'an' : 'a'} ${typeof v}, neither string or object(: ${Mika.fuhihi})` : 'is null'}.`;
|
||||
|
||||
if (typeof v === 'string' && !Mika.syntax.test(v))
|
||||
throw `${header()} '${v}', neither ${toEnglishString([...Mika.types, 'combined theirs'], 'or')}.`;
|
||||
|
||||
if (typeof v === 'object')
|
||||
this.ensure(v, [...location, k], errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
protected static validateFromString(
|
||||
target: Everything,
|
||||
source: string,
|
||||
location: string[] = []
|
||||
) {
|
||||
const path = location.join('.');
|
||||
const rikas: Rika[] = [];
|
||||
const excepted = source.replace(/\s/, '');
|
||||
/*let it move! v
|
||||
*/let lastSpan = excepted.length - 1;
|
||||
const optional = excepted[lastSpan] === '?';
|
||||
const nullable = excepted[optional ? --lastSpan : lastSpan] !== '!';
|
||||
const allowing = excepted.slice(0, nullable ? --lastSpan : lastSpan).split('|');
|
||||
const pushRika = (actual: ExtendedType) => rikas.push({ path, excepted, actual });
|
||||
|
||||
if (target === null)
|
||||
(nullable || pushRika('null'), $)();
|
||||
else if (!allowing.includes(typeof target))
|
||||
pushRika(typeof target);
|
||||
|
||||
return rikas;
|
||||
}
|
||||
|
||||
protected dive(
|
||||
target: object,
|
||||
source: MiriaInternal,
|
||||
location: string[] = []
|
||||
) {
|
||||
const rikas: Rika[] = [];
|
||||
/** DEFINITION
|
||||
* | values | specs | given |
|
||||
* |-----------:|:------|:------|
|
||||
* | true | true | true |
|
||||
* | false | false | true |
|
||||
* | null | true | false |
|
||||
* | unprovided | false | false |
|
||||
*/
|
||||
const keys = Object.keys(source).reduce<Record<string, boolean | null>>((a, c) => (a[c] = null, a), {});
|
||||
|
||||
for (const [k, v] of Object.entries(target) as [string, Everything][]) {
|
||||
const inclusion = keys[k] !== undefined;
|
||||
const x = source[k];
|
||||
const miria = x as Miria;
|
||||
const here = [...location, k];
|
||||
const path = here.join('.');
|
||||
const pushRika = (actual: ExtendedType, excepted?: string | Miria | null) => rikas.push({
|
||||
path,
|
||||
excepted: excepted === undefined ? miria : excepted,
|
||||
actual
|
||||
});
|
||||
const pushRikas = (iterable: Iterable<Rika>) => {
|
||||
for (const rika of iterable)
|
||||
rikas.push(rika);
|
||||
};
|
||||
|
||||
keys[k] = inclusion;
|
||||
|
||||
if (!inclusion && !source[additional])
|
||||
pushRika(v === null ? 'null' : typeof v, null);
|
||||
else if (typeof x === 'undefined')
|
||||
continue;
|
||||
else if (typeof x === 'string')
|
||||
pushRikas(Mika.validateFromString(v, x, here));
|
||||
else if (v === undefined)
|
||||
(x[optional] || pushRika(inclusion ? 'undefined' : 'unprovided'), $)();
|
||||
else if (v === null)
|
||||
(x[nullable] || pushRika('null'), $)();
|
||||
else if (typeof v === 'object')
|
||||
pushRikas(this.dive(v, x, here));
|
||||
else
|
||||
pushRika(typeof v);
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(source as Miria).filter(([k]) => keys[k] === null)) {
|
||||
const rika: Rika = {
|
||||
path: [...location, k].join('.'),
|
||||
excepted: v,
|
||||
actual: 'unprovided'
|
||||
};
|
||||
|
||||
if (typeof v === 'string')
|
||||
(v.endsWith('?') || rikas.push(rika), $)();
|
||||
else if (!v[optional])
|
||||
rikas.push(rika);
|
||||
}
|
||||
|
||||
return rikas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates object.
|
||||
* @param x The source object.
|
||||
* @returns The difference points when the source object fails validation, otherwise null.
|
||||
*/
|
||||
public validate(
|
||||
x: object
|
||||
) {
|
||||
const root = (actual: ExtendedType): Rika[] => [{
|
||||
path: '',
|
||||
excepted: this.miria,
|
||||
actual
|
||||
}];
|
||||
|
||||
if (typeof x !== 'object')
|
||||
return root(typeof x);
|
||||
|
||||
if (x === null)
|
||||
return this.miria[nullable] ? null : root('null');
|
||||
|
||||
const rikas = this.dive(x, this.miria);
|
||||
|
||||
return rikas.length ? rikas : null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue