Using pino as a log writer + Append the Generics to make Routing more scalable

This commit is contained in:
Amaury 2021-06-25 16:19:43 +02:00
parent d1f1165136
commit 01ca99c4e7
9 changed files with 4341 additions and 464 deletions

4455
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,7 @@
"@types/nock": "^11.1.0", "@types/nock": "^11.1.0",
"@types/node": "^15.3.0", "@types/node": "^15.3.0",
"@types/node-fetch": "^2.5.10", "@types/node-fetch": "^2.5.10",
"@types/pino": "^6.3.8",
"@types/sinon-chai": "^3.2.5", "@types/sinon-chai": "^3.2.5",
"@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^4.23.0",
@ -44,6 +45,7 @@
"mocha": "^8.4.0", "mocha": "^8.4.0",
"nock": "^13.0.11", "nock": "^13.0.11",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"pino": "^6.11.3",
"prettier": "^2.3.0", "prettier": "^2.3.0",
"sinon-chai": "^3.6.0", "sinon-chai": "^3.6.0",
"tmp-promise": "^3.0.2", "tmp-promise": "^3.0.2",

View File

@ -1,5 +1,4 @@
import { Client, Message, TextChannel } from 'discord.js'; import { Client, Message, TextChannel } from 'discord.js';
import dedent from 'ts-dedent';
import { ImplementableApi } from './implementableApi'; import { ImplementableApi } from './implementableApi';
import { once as eventsOnce } from 'events'; import { once as eventsOnce } from 'events';
@ -29,7 +28,12 @@ type ChannelsType = {
ChannelPeerTube: TextChannel; ChannelPeerTube: TextChannel;
}; };
class DiscordParser extends ImplementableApi { type TBaseDiscordMessageType = ('addListener' | 'newEntriesNotify') &
ImplementableApi.TBaseMessageType;
class DiscordParser<
T extends TBaseDiscordMessageType
> extends ImplementableApi<T> {
readonly botPrefix: string; readonly botPrefix: string;
readonly channels: { readonly channels: {
[key: string]: TextChannel; [key: string]: TextChannel;
@ -46,9 +50,9 @@ class DiscordParser extends ImplementableApi {
this.settingEvents(); this.settingEvents();
} }
static async instanciate( static async instanciate<T extends TBaseDiscordMessageType>(
config: DiscordParser.Config config: DiscordParser.Config
): Promise<DiscordParser.ConstructorPattern> { ): Promise<DiscordParser<T>> {
const client = new Client(); const client = new Client();
client.login(config.token); client.login(config.token);
await eventsOnce(client, 'ready'); await eventsOnce(client, 'ready');
@ -63,12 +67,12 @@ class DiscordParser extends ImplementableApi {
), ),
}; };
return { return new DiscordParser<T>({
name: config.name, name: config.name,
channels: channels, channels: channels,
client: client, client: client,
botPrefix: config.keyWord, botPrefix: config.keyWord,
}; });
} }
private static getTextChannel(client: Client, id: string): TextChannel { private static getTextChannel(client: Client, id: string): TextChannel {
@ -78,7 +82,7 @@ class DiscordParser extends ImplementableApi {
return channel; return channel;
} }
public receivedMessage(message: ImplementableApi.Message) { public receivedMessage(message: ImplementableApi.Message<T>) {
switch (message.type) { switch (message.type) {
case 'newEntriesNotify': case 'newEntriesNotify':
this.sendMsgYtb( this.sendMsgYtb(
@ -114,13 +118,14 @@ class DiscordParser extends ImplementableApi {
if (msg_splitted[0] === this.botPrefix) { if (msg_splitted[0] === this.botPrefix) {
switch (msg_splitted[1]) { switch (msg_splitted[1]) {
case 'add': case 'add':
const send_message: ImplementableApi.Message = { const send_message: ImplementableApi.Message<TBaseDiscordMessageType> =
{
rawContent: { rawContent: {
address: msg_splitted[2], address: msg_splitted[2],
user: message.author.toString(), user: message.author.toString(),
date: message.createdAt.toUTCString(), date: message.createdAt.toUTCString(),
}, },
type: 'newListener', type: 'addListener',
}; };
message.channel.send('Ceci est un feedback'); message.channel.send('Ceci est un feedback');
this.emit('addListener', send_message); this.emit('addListener', send_message);

View File

@ -10,14 +10,17 @@ namespace ImplementableApi {
* the type field permit to know which type of message it is * the type field permit to know which type of message it is
* rawContent field is the string content of the message * rawContent field is the string content of the message
*/ */
export type Message = { export type Message<T extends TBaseMessageType> = {
idListener?: number; idListener?: number;
type: 'newEntriesNotify' | 'newListener'; type: T | 'logging';
rawContent: any; rawContent: any;
}; };
export type TBaseMessageType = string;
} }
abstract class ImplementableApi extends EventEmitter { abstract class ImplementableApi<
T extends ImplementableApi.TBaseMessageType
> extends EventEmitter {
readonly name: string; readonly name: string;
constructor(readonly config: ImplementableApi.Config) { constructor(readonly config: ImplementableApi.Config) {
@ -25,7 +28,7 @@ abstract class ImplementableApi extends EventEmitter {
this.name = config.name; this.name = config.name;
} }
public abstract receivedMessage(message: ImplementableApi.Message): void; public abstract receivedMessage(message: ImplementableApi.Message<T>): void;
} }
export { ImplementableApi }; export { ImplementableApi };

View File

@ -1,11 +1,6 @@
import { ImplementableApi } from './implementableApi'; import { ImplementableApi } from './implementableApi';
import {
appendFileSync, import pino, { Logger } from 'pino';
closeSync,
openSync,
writeFile,
writeFileSync,
} from 'fs';
namespace LogWriter { namespace LogWriter {
export type Config = ImplementableApi.Config & { export type Config = ImplementableApi.Config & {
@ -17,33 +12,30 @@ namespace LogWriter {
*/ */
class LogWriter extends ImplementableApi { class LogWriter extends ImplementableApi {
readonly path: string; readonly path: string;
readonly logger: Logger;
constructor(readonly config: LogWriter.Config) { constructor(readonly config: LogWriter.Config) {
super(config); super(config);
this.path = config.path; this.path = config.path;
this.firstWrite(); this.logger = pino(pino.destination({ dest: config.path }));
}
private firstWrite() {
this.writeMsg('LogWriter is running');
} }
private writeMsg(message: string): void; private writeMsg(message: string): void;
private writeMsg(message: ImplementableApi.Message): void; private writeMsg(message: ImplementableApi.Message): void;
private writeMsg(message: ImplementableApi.Message | string) { private writeMsg(message: ImplementableApi.Message | string) {
if (typeof message !== 'string') if (typeof message !== 'string')
message = `[${message.type} :: ${new Date().toISOString()}] ${ message = `[${message.type}] ${
message.rawContent typeof message.rawContent === 'string'
? message.rawContent
: JSON.stringify(message.rawContent)
} ${ } ${
message.idListener ?? message.idListener ??
`( listener_id : ${message.idListener} )\n` `( listener_id : ${message.idListener} )\n`
}`; }`;
const fd = openSync(this.path, 'a'); // const str = `[${new Date().toISOString()}] ${message}\n`;
const str = `[${new Date().toISOString()}] ${message}\n`; this.logger.info(message);
appendFileSync(fd, str);
closeSync(fd);
} }
public receivedMessage(message: ImplementableApi.Message) { public receivedMessage(message: ImplementableApi.Message) {

View File

@ -1,8 +1,5 @@
import { ImplementableApi } from './implementableApi'; import { ImplementableApi } from './implementableApi';
import { DiscordParser } from './discordParser';
import { LogWriter } from './logWriter';
import { PeerTubeRequester } from './peertubeRequester';
import EventEmitter from 'events'; import EventEmitter from 'events';
namespace Router { namespace Router {
@ -12,44 +9,57 @@ namespace Router {
}; };
export type InternalConfig = { export type InternalConfig = {
events: EventsType[]; events: EventsType[];
apis: {
apiName: string;
}[];
}; };
// export type GlobalConfig = {
// router: InternalConfig;
// };
} }
class Router extends EventEmitter { class Router<
api_array: { [key: string]: ImplementableApi } = {}; MessageType extends ImplementableApi.TBaseMessageType
routes: Router.InternalConfig; > extends EventEmitter {
api_array: { [key: string]: ImplementableApi<MessageType> } = {};
events: Router.EventsType[];
constructor(readonly config: Router.EventsType[]) { constructor(readonly config: Router.EventsType[]) {
super(); super();
this.routes = { events: config, apis: [] }; this.events = config;
} }
public injectDependency(api: ImplementableApi): void { public injectDependency(api: ImplementableApi<MessageType>): void {
if (api.name in this.api_array) if (api.name in this.api_array)
throw `The api name '${api.name}' is already take`; throw `The api name '${api.name}' is already take`;
this.routes.apis.push({ apiName: api.name });
this.api_array[api.name] = api; this.api_array[api.name] = api;
this.setEvents(api); this.setEvents(api);
this.receivedMessage({
rawContent: `The dependency \`${api.name}\` was well injected into the router`,
type: 'logging',
});
} }
private setEvents(api: ImplementableApi) { private setEvents(api: ImplementableApi<MessageType>) {
for (const eventName in this.routes.events.map( for (const event of this.events.filter(
(ev) => ev.type === 'received' (ev) => ev.type === 'received'
)) )) {
api.on(eventName, () => this.emit(eventName)); api.on(event.name, (obj: ImplementableApi.Message<MessageType>) => {
this.receivedMessage({
type: 'logging',
rawContent: `A message of type \`${obj.type}\` was emited by \`${api.name}\` with the event \`${event.name}\``,
});
this.emit(event.name, obj);
});
}
} }
public receivedMessage(message: ImplementableApi.Message) { public receivedMessage(message: ImplementableApi.Message<MessageType>) {
for (const apiName in this.routes.apis) this.redirectMessage({
this.api_array[apiName].receivedMessage(message); rawContent: `A message of type \`${message.type}\` was received`,
type: 'logging',
});
this.redirectMessage(message);
}
private redirectMessage(message: ImplementableApi.Message<MessageType>) {
for (const api in this.api_array)
this.api_array[api].receivedMessage(message);
} }
} }

View File

@ -1,77 +0,0 @@
import chai, { expect } from 'chai';
import sinon from 'ts-sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
import { DiscordParser } from '../src/discordParser';
//data
const config: DiscordParser.Config = {
channelsId: {
idChannelPeerTube: 'peertubeID',
idChannelYtb: 'ytbChannel',
},
keyWord: 'myDiscordKeyword',
name: 'DiscordTestedInterface',
token: 'mySecretDiscordToken',
};
//mockeded imports
import { Channel, ChannelManager, Client } from 'discord.js';
describe.skip('test DiscordParser', function () {
let clientMockOn: sinon.SinonStub,
clientMockLogin: sinon.SinonStub,
channelStub: sinon.SinonStubbedInstance<Channel>,
channelManagerMockResolve: sinon.SinonStub;
before(() => {
clientMockOn = sinon.stub(Client.prototype, 'on');
clientMockOn.withArgs('message', sinon.match.func).onFirstCall();
clientMockOn.throws('Error, bad parameter or too much call');
clientMockLogin = sinon.stub(Client.prototype, 'login');
clientMockLogin
.withArgs(config.token)
.onFirstCall()
.resolves('ok')
.returnsThis();
clientMockLogin.throws('Error, bad parameter or too much call');
channelStub = sinon.createStubInstance(Channel);
channelManagerMockResolve = sinon.stub(
ChannelManager.prototype,
'resolve'
);
channelManagerMockResolve
.withArgs(config.channelsId.idChannelPeerTube)
.onFirstCall()
.returns(channelStub);
channelManagerMockResolve
.withArgs(config.channelsId.idChannelYtb)
.onFirstCall()
.returns(channelStub);
channelManagerMockResolve.throws("Error Bad id's or too much call");
});
after(() => {
clientMockOn.restore();
clientMockLogin.restore();
channelManagerMockResolve.restore();
});
it('it will test the DiscordParser constructor', async function () {
//when
const discordParser = new DiscordParser(config);
// expect
expect(discordParser.token).to.be.eql(config.token);
await discordParser.channels.then((channels) => {
expect(channels.ChannelYtb.id).to.be.eql(
config.channelsId.idChannelYtb
);
});
});
});

View File

@ -5,42 +5,49 @@ import sinonChai from 'sinon-chai';
chai.use(sinonChai); chai.use(sinonChai);
const expect = chai.expect; const expect = chai.expect;
import { withFile } from 'tmp-promise'; import { ImplementableApi, Router } from '../src/index';
import { writeFileSync } from 'fs';
import { Router } from '../src/index'; // const well_build_routing_file: Router.GlobalConfig = require('./rsrc/wellBuildedRoutingFile.json');
import { DiscordParser } from '../src/discordParser';
import { LogWriter } from '../src/logWriter';
import { PeerTubeRequester } from '../src/peertubeRequester';
const well_build_routing_file: Router.GlobalConfig = require('./rsrc/wellBuildedRoutingFile.json'); const events: Router.EventsType[] = [
{
describe('testing the routing part', function () { name: 'newEntries',
describe('testing the building part', function () { type: 'emit',
it('it will test a normal building', async function () {
await withFile(
async (file) => {
const edit_config = {
...well_build_routing_file,
logWriter: {
...well_build_routing_file.logWriter,
...{ path: file.path },
}, },
}; {
const r = new Router(edit_config); name: 'addListener',
type: 'received',
// expect(r.api_array['Discord']).to.be.instanceOf(
// DiscordParser
// );
expect(r.api_array['logWriter']).to.be.instanceOf(
LogWriter
);
expect(r.api_array['peertubeRequester']).to.be.instanceOf(
PeerTubeRequester
);
}, },
{ postfix: '.log' } ];
); type AllowedEvents = 'newEntries' | 'addListener'; // need to match with the EventsType's names
class FakeApi extends ImplementableApi<AllowedEvents> {
constructor(conf: ImplementableApi.Config) {
super(conf);
}
receivedMessage(msg: ImplementableApi.Message<AllowedEvents>) {
// don't do anything
}
}
describe.only('testing the routing part', function () {
describe('testing the injection', function () {
it('will test the dependency injection', function () {
// given
const myRouter = new Router<AllowedEvents>(events);
const myObj = new FakeApi({ name: 'myFakeApi' });
// first expect
expect(myRouter.api_array).to.be.empty;
expect(myRouter.events).to.be.empty;
// when
myRouter.injectDependency(myObj);
// expect
expect(myRouter.api_array).to.have.keys(['myFakeApi']);
expect(myRouter.api_array['myFakeApi']).to.be.instanceOf(FakeApi);
expect(myRouter.events).to.be.empty;
}); });
}); });
describe('testing the data transmission', function () { describe('testing the data transmission', function () {
@ -48,6 +55,31 @@ describe('testing the routing part', function () {
it('it will emit a new listener request', function () {}); it('it will emit a new listener request', function () {});
}); });
describe('testing the data reception', function () { describe('testing the data reception', function () {
it('it will received a new entries notification', function () {}); it.only('it will received a new entries notification', function () {
//given
const myMessageWhoWouldBeSend: ImplementableApi.Message<AllowedEvents> =
{
rawContent: 'My content',
type: 'logging',
idListener: 5,
};
const myStub = sinon.stub(FakeApi.prototype, 'receivedMessage');
const myRouter = new Router([]);
const myObj = new FakeApi({ name: 'myFakeApi' });
myRouter.injectDependency(myObj);
// when
myRouter.receivedMessage(myMessageWhoWouldBeSend);
// expect
expect(myStub.firstCall).to.have.been.calledWith(
myMessageWhoWouldBeSend
);
expect(myStub.secondCall).to.have.been.calledWith({
rawContent:
'The dependency `myFakeApi` was well injected into the router',
type: 'logging',
});
});
}); });
}); });

View File

@ -1,43 +0,0 @@
import chai, { expect } from 'chai';
import sinon from 'ts-sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
import { withDir } from 'tmp-promise';
import { existsSync, readFileSync } from 'fs';
import { LogWriter } from '../src/logWriter';
const config: LogWriter.Config = {
name: 'logWirterTested',
path: 'it will be set by tmp-promise',
};
describe('test logWriter', function () {
describe('constructor', function () {
it('will test the constructor with a new file', async function () {
await withDir(
async (dir) => {
const log_writer = new LogWriter({
...config,
...{ path: dir.path + '/toto.log' },
});
expect(existsSync(dir.path + '/toto.log')).to.be.true;
expect(
readFileSync(dir.path + '/toto.log', {
encoding: 'utf-8',
})
).to.match(/LogWriter is running\n+$/g);
},
{ unsafeCleanup: true }
);
});
});
describe('received message', function () {
it('will test the print function', function () {});
});
});
//presenter le projet . listener -> import sous forme de plugin