Browse Source

Implement logic

Add implementation of getting UUIDs from usernames.
master
TheoryOfNekomata 2 months ago
parent
commit
d707d44232
22 changed files with 984 additions and 326 deletions
  1. +2
    -2
      packages/cli/package.json
  2. +7
    -14
      packages/cli/src/commands.ts
  3. +0
    -82
      packages/cli/src/modules/adder/adder.controller.ts
  4. +0
    -21
      packages/cli/src/modules/adder/adder.service.ts
  5. +0
    -2
      packages/cli/src/modules/adder/index.ts
  6. +2
    -0
      packages/cli/src/modules/uuid/index.ts
  7. +42
    -0
      packages/cli/src/modules/uuid/uuid.controller.ts
  8. +19
    -0
      packages/cli/src/modules/uuid/uuid.service.ts
  9. +9
    -19
      packages/cli/test/index.test.ts
  10. +2
    -2
      packages/core/package.json
  11. +55
    -18
      packages/core/src/index.ts
  12. +54
    -9
      packages/core/test/index.test.ts
  13. +2
    -2
      packages/web-api/package.json
  14. +0
    -60
      packages/web-api/src/modules/adder/adder.controller.ts
  15. +0
    -21
      packages/web-api/src/modules/adder/adder.service.ts
  16. +0
    -2
      packages/web-api/src/modules/adder/index.ts
  17. +2
    -0
      packages/web-api/src/modules/uuid/index.ts
  18. +35
    -0
      packages/web-api/src/modules/uuid/uuid.controller.ts
  19. +19
    -0
      packages/web-api/src/modules/uuid/uuid.service.ts
  20. +7
    -20
      packages/web-api/src/routes.ts
  21. +9
    -25
      packages/web-api/test/index.test.ts
  22. +718
    -27
      pnpm-lock.yaml

+ 2
- 2
packages/cli/package.json View File

@@ -1,5 +1,5 @@
{
"name": "@modal-sh/cli",
"name": "@modal-sh/minecraft-uuid-cli",
"version": "0.0.0",
"files": [
"dist",
@@ -66,7 +66,7 @@
},
"dependencies": {
"@clack/prompts": "^0.6.3",
"@modal-sh/core": "workspace:*",
"@modal-sh/minecraft-uuid-core": "workspace:*",
"pino": "^8.14.1",
"yargs": "^17.7.2"
}


+ 7
- 14
packages/cli/src/commands.ts View File

@@ -1,22 +1,15 @@
import { Cli } from './packages/cli-wrapper';
import { AdderController, AdderControllerImpl } from './modules/adder';
import { UuidController, AdderControllerImpl } from './modules/uuid';

export const addCommands = (cli: Cli) => {
const adderController: AdderController = new AdderControllerImpl();
const uuidController: UuidController = new AdderControllerImpl();

return cli
.command({
aliases: ['a'],
command: 'add',
parameters: ['<a> <b>'],
describe: 'Add two numbers',
handler: adderController.addNumbers,
})
.command({
aliases: ['s'],
command: 'subtract',
parameters: ['<a> <b>'],
describe: 'Subtract two numbers',
handler: adderController.subtractNumbers,
aliases: ['u'],
command: 'uuid',
parameters: ['<username>'],
describe: 'Get equivalent UUID of username in Minecraft.',
handler: uuidController.getUsernameUuid,
});
};

+ 0
- 82
packages/cli/src/modules/adder/adder.controller.ts View File

@@ -1,82 +0,0 @@
import {
AdderService,
AdderServiceImpl,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from './adder.service';
import { CommandHandler } from '../../packages/cli-wrapper';

export interface AdderController {
addNumbers: CommandHandler;
subtractNumbers: CommandHandler;
}

export class AdderControllerImpl implements AdderController {
constructor(
private readonly adderService: AdderService = new AdderServiceImpl(),
) {
// noop
}

readonly addNumbers: CommandHandler = (params) => {
if (!params.interactive) {
const checkArgs = params.args as Record<string, unknown>;
if (typeof checkArgs.a === 'undefined') {
params.logger.error('Missing required argument: a');
return -1;
}
if (typeof checkArgs.b === 'undefined') {
params.logger.error('Missing required argument: b');
return -1;
}
}

const { a, b } = params.args;
try {
const response = this.adderService.addNumbers({ a: Number(a), b: Number(b) });
params.logger.info(response);
} catch (errorRaw) {
const error = errorRaw as Error;
params.logger.error(error.message);
if (error instanceof InvalidArgumentTypeError) {
return -1;
}
if (error instanceof ArgumentOutOfRangeError) {
return -2;
}
return -3;
}
return 0;
}

readonly subtractNumbers: CommandHandler = (params) => {
if (!params.interactive) {
const checkArgs = params.args as Record<string, unknown>;
if (typeof checkArgs.a === 'undefined') {
params.logger.error('Missing required argument: a');
return -1;
}
if (typeof checkArgs.b === 'undefined') {
params.logger.error('Missing required argument: b');
return -1;
}
}

const { a, b } = params.args;
try {
const response = this.adderService.addNumbers({ a: Number(a), b: -(Number(b)) });
params.logger.info(response);
} catch (errorRaw) {
const error = errorRaw as Error;
params.logger.error(error.message);
if (error instanceof InvalidArgumentTypeError) {
return -1;
}
if (error instanceof ArgumentOutOfRangeError) {
return -2;
}
return -3;
}
return 0;
}
}

+ 0
- 21
packages/cli/src/modules/adder/adder.service.ts View File

@@ -1,21 +0,0 @@
import {
add,
AddFunctionOptions,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '@modal-sh/core';

export interface AdderService {
addNumbers(options: AddFunctionOptions): number;
}

export class AdderServiceImpl implements AdderService {
addNumbers(options: AddFunctionOptions) {
return add(options);
}
}

export {
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
};

+ 0
- 2
packages/cli/src/modules/adder/index.ts View File

@@ -1,2 +0,0 @@
export * from './adder.controller';
export * from './adder.service';

+ 2
- 0
packages/cli/src/modules/uuid/index.ts View File

@@ -0,0 +1,2 @@
export * from './uuid.controller';
export * from './uuid.service';

+ 42
- 0
packages/cli/src/modules/uuid/uuid.controller.ts View File

@@ -0,0 +1,42 @@
import {
UuidService,
UuidServiceImpl,
InvalidArgumentTypeError,
} from './uuid.service';
import { CommandHandler } from '@/packages/cli-wrapper';

export interface UuidController {
getUsernameUuid: CommandHandler;
}

export class AdderControllerImpl implements UuidController {
constructor(
private readonly uuidService: UuidService = new UuidServiceImpl(),
) {
// noop
}

readonly getUsernameUuid: CommandHandler = async (params) => {
if (!params.interactive) {
const checkArgs = params.args as Record<string, unknown>;
if (typeof checkArgs.username === 'undefined') {
params.logger.error('Missing required argument: username');
return -1;
}
}

const { username, onlineMode } = params.args;
try {
const response = await this.uuidService.getUsernameUuid({ username, onlineMode });
params.logger.info(response);
} catch (errorRaw) {
const error = errorRaw as Error;
params.logger.error(error.message);
if (error instanceof InvalidArgumentTypeError) {
return -1;
}
return -3;
}
return 0;
}
}

+ 19
- 0
packages/cli/src/modules/uuid/uuid.service.ts View File

@@ -0,0 +1,19 @@
import {
getUsernameUuid,
GetUsernameUuidOptions,
InvalidArgumentTypeError,
} from '@modal-sh/minecraft-uuid-core';

export interface UuidService {
getUsernameUuid(options: GetUsernameUuidOptions): Promise<string>;
}

export class UuidServiceImpl implements UuidService {
getUsernameUuid(options: GetUsernameUuidOptions) {
return getUsernameUuid(options);
}
}

export {
InvalidArgumentTypeError,
};

+ 9
- 19
packages/cli/test/index.test.ts View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { createCli } from '../src/cli';
import { addCommands } from '../src/commands';
import { AdderServiceImpl, InvalidArgumentTypeError, ArgumentOutOfRangeError } from '../src/modules/adder';
import { UuidServiceImpl, InvalidArgumentTypeError } from '../src/modules/uuid';
import { Cli } from '../src/packages/cli-wrapper';

vi.mock('process');
@@ -17,39 +17,30 @@ describe('blah', () => {
});

it('returns result when successful', async () => {
const response = await cli.test().run(['add', '1', '2']);
const response = await cli.test().run(['uuid', 'username']);
expect(response.exitCode).toBe(0);
});

it('returns error when given invalid inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
vi.spyOn(UuidServiceImpl.prototype, 'getUsernameUuid').mockImplementationOnce(() => {
throw new InvalidArgumentTypeError('Invalid input');
});

const response = await cli.test().run(['add', '1', '2']);
const response = await cli.test().run(['uuid', 'username']);
expect(response.exitCode).toBe(-1);
});

it('returns error when given out-of-range inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new ArgumentOutOfRangeError('Out of range');
});

const response = await cli.test().run(['add', '1', '2']);
expect(response.exitCode).toBe(-2);
});

it('returns error when an unexpected error occurs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
vi.spyOn(UuidServiceImpl.prototype, 'getUsernameUuid').mockImplementationOnce(() => {
throw new Error('Unexpected error');
});

const response = await cli.test().run(['add', '1', '2']);
const response = await cli.test().run(['uuid', 'username']);
expect(response.exitCode).toBe(-3);
});

it('returns error when given insufficient arguments', async () => {
const response = await cli.test().run(['add', '1']);
const response = await cli.test().run(['uuid']);
expect(response.exitCode).toBe(-1);
});

@@ -58,10 +49,9 @@ describe('blah', () => {
const response = await cli
.test()
.promptValue({
a: '1',
b: '2',
username: 'username',
})
.run(['add', '-i']);
.run(['uuid', '-i']);
expect(response.exitCode).not.toBe(-1);
});
});


+ 2
- 2
packages/core/package.json View File

@@ -1,5 +1,5 @@
{
"name": "@modal-sh/core",
"name": "@modal-sh/minecraft-uuid-core",
"version": "0.0.0",
"files": [
"dist",
@@ -19,7 +19,7 @@
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vitest": "^0.28.1"
"vitest": "1.3.1"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",


+ 55
- 18
packages/core/src/index.ts View File

@@ -1,10 +1,13 @@
export interface AddFunctionOptions {
a: number;
b: number;
import { get } from 'https';
import { createHash } from 'crypto';

export interface GetUsernameUuidOptions {
username: string;
onlineMode?: boolean;
}

export interface AddFunction {
(options: AddFunctionOptions): number;
export interface GetUsernameUuid {
(options: GetUsernameUuidOptions): Promise<string>;
}

export class InvalidArgumentTypeError extends TypeError {
@@ -14,21 +17,55 @@ export class InvalidArgumentTypeError extends TypeError {
}
}

export class ArgumentOutOfRangeError extends RangeError {
constructor(message: string) {
super(message);
this.name = 'ArgumentOutOfRangeError';
}
}
const formatUuidBytes = (bytes: Buffer) => {
return [
bytes.slice(0, 4),
bytes.slice(4, 6),
bytes.slice(6, 8),
bytes.slice(8, 10),
bytes.slice(10)
]
.map((b) => b.toString('hex'))
.join('-');
};

export const getUsernameUuid: GetUsernameUuid = async (options: GetUsernameUuidOptions): Promise<string> => {
const { username, onlineMode = false } = options as unknown as Record<string, unknown>;

export const add: AddFunction = (options: AddFunctionOptions): number => {
const { a, b } = options as unknown as Record<string, unknown>;
if (typeof a !== 'number' || typeof b !== 'number') {
throw new InvalidArgumentTypeError('a and b must be numbers');
if (typeof username !== 'string') {
throw new InvalidArgumentTypeError('Username must be a string');
}
if (!Number.isFinite(a) || !Number.isFinite(b)) {
throw new ArgumentOutOfRangeError('a and b must be finite numbers');

if (!onlineMode) {
const md5 = createHash('md5');
md5.update(Buffer.from(`OfflinePlayer:${username}`, 'utf-8'));
const bytes = md5.digest();

bytes[6] &= 0x0f;
bytes[6] |= 0x30;

bytes[8] &= 0x3f;
bytes[8] |= 0x80;

return formatUuidBytes(bytes);
}

return a + b;
return new Promise((resolve, reject) => {
get(`https://api.mojang.com/users/profiles/minecraft/${encodeURIComponent(username)}`, res => {
let uuid = '';

res.on('data', (c) => {
const json = c.toString('utf-8');
const data = JSON.parse(json);
const bytes = Buffer.from(data.id, 'hex');

uuid = formatUuidBytes(bytes);
});

res.on('end', () => {
resolve(uuid);
});
})
.on('error', reject);
});
};

+ 54
- 9
packages/core/test/index.test.ts View File

@@ -1,16 +1,61 @@
import { describe, it, expect } from 'vitest';
import { add } from '../src';
import {describe, it, expect, vi} from 'vitest';
import { getUsernameUuid } from '../src';
import { EventEmitter } from 'events';

describe('blah', () => {
it('returns result', () => {
expect(add({ a: 1, b: 1 })).toEqual(2);
describe('minecraft-uuid', () => {
it('returns result (default offline mode)', async () => {
vi.mock('crypto', () => ({
createHash: (algo) => {
return {
update: () => {
// noop
},
digest: () => Buffer.alloc(16, 0),
};
},
}));

const uuid = await getUsernameUuid({ username: 'username' });
expect(uuid).toEqual('00000000-0000-3000-8000-000000000000');
});

it('throws when given invalid argument types', () => {
expect(() => add({ a: '1' as unknown as number, b: 1 })).toThrow(TypeError);
it('returns result (explicit offline mode)', async () => {
vi.mock('crypto', () => ({
createHash: (algo) => {
return {
update: () => {
// noop
},
digest: () => Buffer.alloc(16, 0),
};
},
}));

const uuid = await getUsernameUuid({ username: 'username', onlineMode: false });
expect(uuid).toEqual('00000000-0000-3000-8000-000000000000');
});

it('throws when given out-of-range arguments', () => {
expect(() => add({ a: Infinity, b: 1 })).toThrow(RangeError);
it('returns result (explicit online mode)', async () => {
vi.mock('https', () => ({
get: (url, cb) => {
const emitter = new EventEmitter();

if (typeof cb === 'function') {
(cb as unknown as (...args: unknown[]) => unknown)(emitter);
}

emitter.emit('data', Buffer.from(JSON.stringify({
id: '82c683b548333d6b8a2606352cd6f327',
name: 'username'
})));

emitter.emit('end');

return emitter;
},
}));

const uuid = await getUsernameUuid({ username: 'username', onlineMode: true });
expect(uuid).toEqual('82c683b5-4833-3d6b-8a26-06352cd6f327');
});
});

+ 2
- 2
packages/web-api/package.json View File

@@ -1,5 +1,5 @@
{
"name": "@modal-sh/web-api",
"name": "@modal-sh/minecraft-uuid-web-api",
"version": "0.0.0",
"files": [
"dist",
@@ -23,7 +23,7 @@
"vitest": "^0.28.1"
},
"dependencies": {
"@modal-sh/core": "workspace:*",
"@modal-sh/minecraft-uuid-core": "workspace:*",
"fastify": "^4.12.0"
},
"scripts": {


+ 0
- 60
packages/web-api/src/modules/adder/adder.controller.ts View File

@@ -1,60 +0,0 @@
import { RouteHandlerMethod } from 'fastify';
import {
AdderService,
AdderServiceImpl,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '@/modules/adder/adder.service';
import { constants } from 'http2';

export interface AdderController {
addNumbers: RouteHandlerMethod;
subtractNumbers: RouteHandlerMethod;
}

export class AdderControllerImpl implements AdderController {
constructor(
private readonly adderService: AdderService = new AdderServiceImpl(),
) {
// noop
}

readonly addNumbers: RouteHandlerMethod = async (request, reply) => {
const { a, b } = request.body as { a: number; b: number };
try {
const response = this.adderService.addNumbers({ a, b });
reply.send(response);
} catch (errorRaw) {
if (errorRaw instanceof InvalidArgumentTypeError) {
request.log.info(errorRaw);
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
if (errorRaw instanceof ArgumentOutOfRangeError) {
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
const error = errorRaw as Error;
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message);
}
}

readonly subtractNumbers: RouteHandlerMethod = async (request, reply) => {
const { a, b } = request.body as { a: number; b: number };
try {
const response = this.adderService.addNumbers({ a, b: -b });
reply.send(response);
} catch (errorRaw) {
if (errorRaw instanceof InvalidArgumentTypeError) {
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
if (errorRaw instanceof ArgumentOutOfRangeError) {
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
const error = errorRaw as Error;
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message);
}
}
}

+ 0
- 21
packages/web-api/src/modules/adder/adder.service.ts View File

@@ -1,21 +0,0 @@
import {
add,
AddFunctionOptions,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '@modal-sh/core';

export interface AdderService {
addNumbers(options: AddFunctionOptions): number;
}

export class AdderServiceImpl implements AdderService {
addNumbers(options: AddFunctionOptions) {
return add(options);
}
}

export {
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
};

+ 0
- 2
packages/web-api/src/modules/adder/index.ts View File

@@ -1,2 +0,0 @@
export * from './adder.controller';
export * from './adder.service';

+ 2
- 0
packages/web-api/src/modules/uuid/index.ts View File

@@ -0,0 +1,2 @@
export * from './uuid.controller';
export * from './uuid.service';

+ 35
- 0
packages/web-api/src/modules/uuid/uuid.controller.ts View File

@@ -0,0 +1,35 @@
import { RouteHandlerMethod } from 'fastify';
import {
UuidService,
UuidServiceImpl,
InvalidArgumentTypeError,
} from '@/modules/uuid/uuid.service';
import { constants } from 'http2';

export interface UuidController {
getUsernameUuid: RouteHandlerMethod;
}

export class UuidControllerImpl implements UuidController {
constructor(
private readonly uuidService: UuidService = new UuidServiceImpl(),
) {
// noop
}

readonly getUsernameUuid: RouteHandlerMethod = async (request, reply) => {
const { username, onlineMode } = request.body as { username: string; onlineMode?: boolean };
try {
const response = this.uuidService.getUsernameUuid({ username, onlineMode });
reply.send(response);
} catch (errorRaw) {
if (errorRaw instanceof InvalidArgumentTypeError) {
request.log.info(errorRaw);
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
const error = errorRaw as Error;
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message);
}
}
}

+ 19
- 0
packages/web-api/src/modules/uuid/uuid.service.ts View File

@@ -0,0 +1,19 @@
import {
GetUsernameUuidOptions,
getUsernameUuid,
InvalidArgumentTypeError,
} from '@modal-sh/minecraft-uuid-core';

export interface UuidService {
getUsernameUuid(options: GetUsernameUuidOptions): Promise<string>;
}

export class UuidServiceImpl implements UuidService {
async getUsernameUuid(options: GetUsernameUuidOptions) {
return getUsernameUuid(options);
}
}

export {
InvalidArgumentTypeError,
};

+ 7
- 20
packages/web-api/src/routes.ts View File

@@ -1,36 +1,23 @@
import { FastifyInstance } from 'fastify';
import { AdderController, AdderControllerImpl } from '@/modules/adder';
import { UuidController, UuidControllerImpl } from '@/modules/uuid';

export const addRoutes = (server: FastifyInstance) => {
const adderController: AdderController = new AdderControllerImpl();
const uuidController: UuidController = new UuidControllerImpl();

return server
.route({
method: 'POST',
url: '/add',
url: '/api/uuid',
schema: {
body: {
type: 'object',
required: ['username'],
properties: {
a: { type: 'number' },
b: { type: 'number' },
username: { type: 'string' },
onlineMode: { type: 'boolean' },
}
}
},
handler: adderController.addNumbers,
})
.route({
method: 'POST',
url: '/subtract',
schema: {
body: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
}
}
},
handler: adderController.subtractNumbers,
handler: uuidController.getUsernameUuid,
});
};

+ 9
- 25
packages/web-api/test/index.test.ts View File

@@ -11,14 +11,13 @@ import { constants } from 'http2';
import { createServer } from '../src/server';
import { addRoutes } from '../src/routes';
import {
AdderServiceImpl,
ArgumentOutOfRangeError,
UuidServiceImpl,
InvalidArgumentTypeError,
} from '../src/modules/adder';
} from '../src/modules/uuid';

describe('Example', () => {
let server: FastifyInstance;
const body = { a: 1, b: 2 };
const body = { username: 'username', onlineMode: false };

beforeAll(() => {
server = createServer();
@@ -32,7 +31,7 @@ describe('Example', () => {
it('returns result when successful', async () => {
const response = await server
.inject()
.post('/')
.post('/api/uuid')
.body(body)
.headers({
'Accept': 'application/json',
@@ -41,28 +40,13 @@ describe('Example', () => {
});

it('returns error when given invalid inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
vi.spyOn(UuidServiceImpl.prototype, 'getUsernameUuid').mockImplementationOnce(() => {
throw new InvalidArgumentTypeError('Invalid input');
});

const response = await server
.inject()
.post('/')
.body(body)
.headers({
'Accept': 'application/json',
});
expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST);
});

it('returns error when given out-of-range inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new ArgumentOutOfRangeError('Out of range');
});

const response = await server
.inject()
.post('/')
.post('/api/uuid')
.body(body)
.headers({
'Accept': 'application/json',
@@ -71,14 +55,14 @@ describe('Example', () => {
});

it('returns error when an unexpected error occurs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
vi.spyOn(UuidServiceImpl.prototype, 'getUsernameUuid').mockImplementationOnce(() => {
throw new Error('Unexpected error');
});

const response = await server
.inject()
.post('/')
.body({ a: 1, b: 2 })
.post('/api/uuid')
.body(body)
.headers({
'Accept': 'application/json',
});


+ 718
- 27
pnpm-lock.yaml
File diff suppressed because it is too large
View File


Loading…
Cancel
Save