Create Mika
This commit is contained in:
parent
6683d50bae
commit
9d328784ec
5 changed files with 382 additions and 16 deletions
|
@ -68,6 +68,7 @@ src ... ソースコード
|
||||||
server ... Webサーバー
|
server ... Webサーバー
|
||||||
client ... クライアント
|
client ... クライアント
|
||||||
mfm ... MFM
|
mfm ... MFM
|
||||||
|
sanctuary ... TypeScriptの制約を強くしたエリア ~~乃々の聖域ではない~~
|
||||||
|
|
||||||
test ... テスト
|
test ... テスト
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,9 @@ import * as fs from 'fs';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import { Source, Mixin } from './types';
|
import { Source, Mixin } from './types';
|
||||||
|
import Mika, { optional } from '../sanctuary/mika';
|
||||||
import * as pkg from '../../package.json';
|
import * as pkg from '../../package.json';
|
||||||
|
import Logger from '../misc/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path of configuration directory
|
* Path of configuration directory
|
||||||
|
@ -21,27 +23,138 @@ const path = process.env.NODE_ENV == 'test'
|
||||||
: `${dir}/default.yml`;
|
: `${dir}/default.yml`;
|
||||||
|
|
||||||
export default function load() {
|
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!'
|
||||||
|
},
|
||||||
|
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);
|
const url = validateUrl(config.url);
|
||||||
|
|
||||||
config.url = normalizeUrl(config.url);
|
config.url = normalizeUrl(config.url);
|
||||||
|
|
||||||
mixin.host = url.host;
|
const scheme = url.protocol.replace(/:$/, '');
|
||||||
mixin.hostname = url.hostname;
|
const ws_scheme = scheme.replace('http', 'ws');
|
||||||
mixin.scheme = url.protocol.replace(/:$/, '');
|
|
||||||
mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
|
const mixin: Mixin = {
|
||||||
mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
|
host: url.host,
|
||||||
mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
|
hostname: url.hostname,
|
||||||
mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`;
|
scheme,
|
||||||
mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`;
|
ws_scheme,
|
||||||
mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`;
|
ws_url: `${ws_scheme}://${url.host}`,
|
||||||
mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`;
|
api_url: `${scheme}://${url.host}/api`,
|
||||||
mixin.status_url = `${mixin.scheme}://${mixin.host}/status`;
|
auth_url: `${scheme}://${url.host}/auth`,
|
||||||
mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
|
dev_url: `${scheme}://${url.host}/dev`,
|
||||||
mixin.user_agent = `Misskey/${pkg.version} (${config.url})`;
|
docs_url: `${scheme}://${url.host}/docs`,
|
||||||
|
stats_url: `${scheme}://${url.host}/stats`,
|
||||||
|
status_url: `${scheme}://${url.host}/status`,
|
||||||
|
drive_url: `${scheme}://${url.host}/files`,
|
||||||
|
user_agent: `Misskey/${pkg.version} (${config.url})`
|
||||||
|
};
|
||||||
|
|
||||||
if (config.autoAdmin == null) config.autoAdmin = false;
|
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];
|
for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
|
||||||
return ys;
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
230
src/sanctuary/mika.ts
Normal file
230
src/sanctuary/mika.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
7
src/sanctuary/tsconfig.json
Normal file
7
src/sanctuary/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"strictNullChecks": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue