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

View File

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

View File

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

View File

@ -1,8 +1,5 @@
import { ImplementableApi } from './implementableApi';
import { DiscordParser } from './discordParser';
import { LogWriter } from './logWriter';
import { PeerTubeRequester } from './peertubeRequester';
import EventEmitter from 'events';
namespace Router {
@ -12,44 +9,57 @@ namespace Router {
};
export type InternalConfig = {
events: EventsType[];
apis: {
apiName: string;
}[];
};
// export type GlobalConfig = {
// router: InternalConfig;
// };
}
class Router extends EventEmitter {
api_array: { [key: string]: ImplementableApi } = {};
routes: Router.InternalConfig;
class Router<
MessageType extends ImplementableApi.TBaseMessageType
> extends EventEmitter {
api_array: { [key: string]: ImplementableApi<MessageType> } = {};
events: Router.EventsType[];
constructor(readonly config: Router.EventsType[]) {
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)
throw `The api name '${api.name}' is already take`;
this.routes.apis.push({ apiName: api.name });
this.api_array[api.name] = api;
this.setEvents(api);
this.receivedMessage({
rawContent: `The dependency \`${api.name}\` was well injected into the router`,
type: 'logging',
});
}
private setEvents(api: ImplementableApi) {
for (const eventName in this.routes.events.map(
private setEvents(api: ImplementableApi<MessageType>) {
for (const event of this.events.filter(
(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) {
for (const apiName in this.routes.apis)
this.api_array[apiName].receivedMessage(message);
public receivedMessage(message: ImplementableApi.Message<MessageType>) {
this.redirectMessage({
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);
const expect = chai.expect;
import { withFile } from 'tmp-promise';
import { writeFileSync } from 'fs';
import { ImplementableApi, Router } from '../src/index';
import { Router } from '../src/index';
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 well_build_routing_file: Router.GlobalConfig = require('./rsrc/wellBuildedRoutingFile.json');
const events: Router.EventsType[] = [
{
name: 'newEntries',
type: 'emit',
},
{
name: 'addListener',
type: 'received',
},
];
type AllowedEvents = 'newEntries' | 'addListener'; // need to match with the EventsType's names
describe('testing the routing part', function () {
describe('testing the building part', function () {
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);
class FakeApi extends ImplementableApi<AllowedEvents> {
constructor(conf: ImplementableApi.Config) {
super(conf);
}
// 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' }
);
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 () {
@ -48,6 +55,31 @@ describe('testing the routing part', function () {
it('it will emit a new listener request', 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