Compare commits

...

6 Commits
stage ... mika

Author SHA1 Message Date
Acid Chicken (硫酸鶏) a63ff4f660
Update definition
refs: 5a0a297634
2019-03-10 17:17:25 +09:00
Acid Chicken (硫酸鶏) 98ec5d0a9a
Merge branch 'develop' into mika 2019-02-24 12:57:31 +09:00
Acid Chicken (硫酸鶏) 43a9a080b3
Update CONTRIBUTING.md 2019-02-10 03:51:15 +09:00
Acid Chicken (硫酸鶏) bb2135c655
Update tsconfig.json 2019-02-10 03:47:55 +09:00
Acid Chicken (硫酸鶏) 803ed2cc32
Update tsconfig.json 2019-02-10 03:47:43 +09:00
Acid Chicken (硫酸鶏) 9d328784ec
Create Mika 2019-02-10 03:19:43 +09:00
5 changed files with 394 additions and 25 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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}`;
}
}

230
src/sanctuary/mika.ts Normal file
View 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;
}
}

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"strictNullChecks": true
}
}