diff --git a/packages/rest/package.json b/packages/rest/package.json index 6e3a83d31..503f89679 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -24,6 +24,7 @@ "test:test-type": "tsc --project tsconfig.test.json" }, "dependencies": { + "@discordeno/transformer": "18.0.0-alpha.1", "dotenv": "^16.0.3" }, "devDependencies": { diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 39ac75552..40dba0ef8 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ +import TRANSFORMERS from '@discordeno/transformer' import type { BigString, Camelize, DiscordUser } from '@discordeno/types' +// TODO: make dynamic based on package.json file +const version = '18.0.0-alpha.1' + export function createRestManager (options: CreateRestManagerOptions): RestManager { const rest: RestManager = { token: options.token, @@ -9,15 +13,49 @@ export function createRestManager (options: CreateRestManagerOptions): RestManag routes: { // User endpoints - user: (userId: BigString) => { + user (userId: BigString) { return `/users/${userId}` } }, + createRequest (options) { + const headers: Record = { + 'user-agent': `DiscordBot (https://github.com/discordeno/discordeno, v${version})` + } + + if (!options.unauthorized) headers.authorization = `Bot ${rest.token}` + + // SOMETIMES SPECIAL HEADERS (E.G. CUSTOM AUTHORIZATION) NEED TO BE USED + if (options.headers) { + for (const key in options.headers) { + headers[key.toLowerCase()] = options.headers[key] + } + } + + // GET METHODS SHOULD NOT HAVE A BODY + if (options.method === 'GET') { + options.body = undefined + } + + // IF A REASON IS PROVIDED ENCODE IT IN HEADERS + if (options.body?.reason) { + headers['X-Audit-Log-Reason'] = encodeURIComponent( + options.body.reason as string + ) + options.body.reason = undefined + } + + return { + headers, + body: (options.body?.file ?? JSON.stringify(options.body)) as + | FormData + | string, + method: options.method + } + }, + async makeRequest (method, url) { - return await fetch(`${rest.baseUrl}/v${rest.version}/${url}`, { - method - }).then(async res => await res.json()) + return await fetch(`${rest.baseUrl}/v${rest.version}/${url}`, rest.createRequest({ method, url })).then(async res => await res.json()) }, async get (url) { @@ -25,8 +63,8 @@ export function createRestManager (options: CreateRestManagerOptions): RestManag }, async getUser (id) { - // TODO: camelize - return await rest.get(rest.routes.user(id)) + const result = await rest.get(rest.routes.user(id)) + return TRANSFORMERS.user(result) } } @@ -65,6 +103,8 @@ export interface RestManager { /** A specific user route. */ user: (id: BigString) => string } + /** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */ + createRequest: (options: CreateRequestBodyOptions) => RequestBody /** Make a request to the api. */ makeRequest: (method: RequestMethods, url: string) => Promise /** Make a get request to the api */ @@ -80,3 +120,17 @@ export interface RestManager { export type RequestMethods = 'GET' export type ApiVersions = 9 | 10 + +export interface CreateRequestBodyOptions { + headers?: Record + method: RequestMethods + body?: Record + unauthorized?: boolean + url?: string +} + +export interface RequestBody { + headers: Record + body: string | FormData + method: RequestMethods +} diff --git a/packages/rest/tests/e2e/user.spec.ts b/packages/rest/tests/e2e/user.spec.ts new file mode 100644 index 000000000..88805232f --- /dev/null +++ b/packages/rest/tests/e2e/user.spec.ts @@ -0,0 +1,45 @@ +import { expect } from 'chai' +import { describe, it } from 'mocha' +import { rest } from '../utils.js' + +describe('[rest] manager', () => { + describe('Get a user from the api', () => { + it('With a valid user id', async () => { + const user = await rest.getUser('130136895395987456') + + describe('User has correct shape and form', () => { + it('Has correct id', () => { + expect(user.id).to.be.equal('130136895395987456') + }) + + it('Has a valid username', () => { + expect(user.username.length).to.be.greaterThanOrEqual(1) + }) + + it('Has a valid discriminator', () => { + expect(user.discriminator.length).to.be.equal(4) + }) + + it('Has been camelized', () => { + const keys = Object.keys(user) + + expect(keys.includes('mfa_enabled')).to.be.false + expect(keys.includes('accent_color')).to.be.false + expect(keys.includes('premium_type')).to.be.false + expect(keys.includes('public_flags')).to.be.false + expect(keys.includes('mfaEnabled')).to.be.true + expect(keys.includes('accentColor')).to.be.true + expect(keys.includes('premiumType')).to.be.true + expect(keys.includes('publicFlags')).to.be.true + }) + }) + }) + + // TODO fix this test, needs proper error handling in rest + // it('With an invalid user id', async () => { + // const user = await rest.getUser('123') + // console.log('user fail', user) + // expect(await rest.getUser('123')).to.throw + // }) + }) +}) diff --git a/packages/rest/tests/utils.ts b/packages/rest/tests/utils.ts new file mode 100644 index 000000000..4f54c9985 --- /dev/null +++ b/packages/rest/tests/utils.ts @@ -0,0 +1,12 @@ +import type { Camelize, DiscordGuild } from '@discordeno/types' +import { createRestManager } from '../src/manager.js' +import { token } from './constants.js' + +// eslint-disable-next-line prefer-const +export let cached = { + guild: undefined as Camelize | undefined +} + +export const rest = createRestManager({ + token +}) diff --git a/packages/transformer/.c8rc.json b/packages/transformer/.c8rc.json new file mode 100644 index 000000000..f135073e4 --- /dev/null +++ b/packages/transformer/.c8rc.json @@ -0,0 +1,7 @@ +{ + "all": true, + "src": "src", + "reporter": ["text", "lcov"], + "include": ["src/**/*.ts"], + "exclude": ["tests"] +} diff --git a/packages/transformer/.mocharc.json b/packages/transformer/.mocharc.json new file mode 100644 index 000000000..4689d4a04 --- /dev/null +++ b/packages/transformer/.mocharc.json @@ -0,0 +1,10 @@ +{ + "require": "ts-node/register", + "loader": "ts-node/esm", + "recursive": true, + "timeout": 2000, + "watch-extensions": "ts", + "watch-files": ["src", "tests"], + "enable-source-maps": true, + "parallel": false +} diff --git a/packages/transformer/.swcrc b/packages/transformer/.swcrc new file mode 100644 index 000000000..5ae43bb99 --- /dev/null +++ b/packages/transformer/.swcrc @@ -0,0 +1,31 @@ +{ + "minify": true, + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "target": "es2022", + "keepClassNames": true, + "loose": true, + "minify": { + "compress": { + "unused": true + }, + "mangle": true + } + }, + "module": { + "type": "es6", + "strict": false, + "strictMode": true, + "lazy": false, + "noInterop": false + }, + "sourceMaps": "inline" +} \ No newline at end of file diff --git a/packages/transformer/bench/index.ts b/packages/transformer/bench/index.ts new file mode 100644 index 000000000..27a9ea2e0 --- /dev/null +++ b/packages/transformer/bench/index.ts @@ -0,0 +1,145 @@ +import Benchmark from 'benchmark' + +import TRANSFORMERS from '../src/index.js' + +const suite = new Benchmark.Suite() + +// Fetch the cached guild +const discordGuild = JSON.parse( + await ( + await fetch( + 'https://raw.githubusercontent.com/discordeno/discordeno/benchies/cache/cachedObject/guild.json' + ) + ).text() +) + +const guild = TRANSFORMERS.guild(discordGuild) + +suite.add('[Transformer] snake Guild to a camel Guild', () => { + TRANSFORMERS.guild(discordGuild) +}) + +suite.add('[Transformer] camel Guild to a snake Guild ', () => { + TRANSFORMERS.reverse.guild(guild) +}) + +// Fetch the cached user +const discordUser = JSON.parse( + await ( + await fetch( + 'https://raw.githubusercontent.com/discordeno/discordeno/benchies/cache/cachedObject/user.json' + ) + ).text() +) + +const user = TRANSFORMERS.user(discordUser) + +suite.add('[Transformer] snake User to a camel User', () => { + TRANSFORMERS.user(discordUser) +}) + +suite.add('[Transformer] camel User to a snake User', () => { + TRANSFORMERS.reverse.user(user) +}) + +for (const channelType of [ + 'rules', + 'announcement-channel', + 'moderator-channel', + 'text-channel', + 'stage-channel', + 'voice-channel' +]) { + const discordChannel = JSON.parse( + await ( + await fetch( + `https://raw.githubusercontent.com/discordeno/discordeno/benchies/cache/cachedObject/${channelType}.json` + ) + ).text() + ) + let formattedChannelType = channelType + .split('-') + .map((word) => word[0].toUpperCase() + word.slice(1)) + .join(' ') + if (formattedChannelType === 'Rules') formattedChannelType = 'Rules Channel' + + suite.add( + `[Transformer] snake ${formattedChannelType} to a camel ${formattedChannelType}`, + () => { + TRANSFORMERS.channel(discordChannel) + } + ) + + const channel = TRANSFORMERS.channel(discordChannel) + + suite.add( + `[Transformer] camel ${formattedChannelType} to a snake ${formattedChannelType}`, + () => { + TRANSFORMERS.reverse.channel(channel) + } + ) +} + +// Fetch the cached member +const discordMember = JSON.parse( + await ( + await fetch( + 'https://raw.githubusercontent.com/discordeno/discordeno/benchies/cache/cachedObject/member.json' + ) + ).text() +) + +const member = TRANSFORMERS.member(discordMember) + +suite.add('[Transformer] snake Member to a camel Member', () => { + TRANSFORMERS.member(discordMember) +}) + +suite.add('[Transformer] camel Member to a snake Member', () => { + TRANSFORMERS.reverse.member(member) +}) + +// Fetch the cached role +const discordRole = JSON.parse( + await ( + await fetch( + 'https://raw.githubusercontent.com/discordeno/discordeno/benchies/cache/cachedObject/role.json' + ) + ).text() +) + +const role = TRANSFORMERS.role(discordRole) + +suite.add('[Transformer] snake Role to a camel Role', () => { + TRANSFORMERS.role(discordRole) +}) + +suite.add('[Transformer] camel Role to a snake Role', () => { + TRANSFORMERS.reverse.role(role) +}) + +/* +// Fetch the cached message +const discordMessage = JSON.parse( + await (await fetch( + 'https://raw.githubusercontent.com/discordeno/discordeno/benchies/cache/cachedObject/message.json' + )) + .text() +) + +const message = TRANSFORMERS.message(discordMessage) + +suite.add('[Transformer] snake Message to a camel Message', () => { + TRANSFORMERS.message(discordMessage) +}) + +suite.add('[Transformer] camel User to a snake User', () => { + TRANSFORMERS.reverse.message(message) +}) +*/ + +suite + .on('cycle', function (event: any) { + console.log(String(event.target)) + }) + .run() diff --git a/packages/transformer/package.json b/packages/transformer/package.json new file mode 100644 index 000000000..283bc29a5 --- /dev/null +++ b/packages/transformer/package.json @@ -0,0 +1,46 @@ +{ + "name": "@discordeno/transformer", + "version": "18.0.0-alpha.1", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/discordeno/discordeno.git" + }, + "scripts": { + "build": "swc --delete-dir-on-start src --out-dir dist", + "build:type": "tsc --declaration --emitDeclarationOnly --declarationDir dist", + "release-build": "yarn build && yarn build:type", + "fmt": "eslint --fix \"src/**/*.ts*\"", + "lint": "eslint \"src/**/*.ts*\"", + "test:unit-coverage": "c8 mocha --no-warnings 'tests/**/*.spec.ts'", + "test:unit": "c8 --r lcov mocha --no-warnings 'tests/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js transformer", + "test:deno-unit": "swc tests --delete-dir-on-start --out-dir denoTestsDist && node ../../scripts/fixDenoTestExtension.js && deno test -A --import-map ../../denoImportMap.json denoTestsDist", + "test:unit:watch": "mocha --no-warnings --watch --parallel 'tests/**/*.spec.ts'", + "test:type": "tsc --noEmit", + "test:test-type": "tsc --project tsconfig.test.json", + "bench:build": "swc bench --delete-dir-on-start --out-dir benchDist && node ../../scripts/fixBenchExtension.js" + }, + "devDependencies": { + "@discordeno/types": "18.0.0-alpha.1", + "@swc/cli": "^0.1.57", + "@swc/core": "^1.3.21", + "@types/benchmark": "^2.1.2", + "@types/chai": "^4", + "@types/mocha": "^10", + "@types/node": "^18.11.9", + "@types/sinon": "^10.0.13", + "benchmark": "^2.1.4", + "c8": "^7.12.0", + "chai": "^4.3.7", + "eslint": "^8.0.1", + "eslint-config-discordeno": "*", + "microtime": "^3.1.1", + "mocha": "^10.1.0", + "sinon": "^15.0.0", + "tsconfig": "*", + "typescript": "^4.9.3" + } +} diff --git a/packages/transformer/src/casing/index.ts b/packages/transformer/src/casing/index.ts new file mode 100644 index 000000000..72e50f894 --- /dev/null +++ b/packages/transformer/src/casing/index.ts @@ -0,0 +1 @@ +export * from './user.js' diff --git a/packages/transformer/src/casing/user.ts b/packages/transformer/src/casing/user.ts new file mode 100644 index 000000000..fea90fad5 --- /dev/null +++ b/packages/transformer/src/casing/user.ts @@ -0,0 +1,43 @@ +import type { Camelize, DiscordUser } from '@discordeno/types' + +export function c1amelize1User (payload: DiscordUser): Camelize { + return { + id: payload.id, + bot: payload.bot, + email: payload.email, + flags: payload.flags, + avatar: payload.avatar, + system: payload.system, + banner: payload.banner, + locale: payload.locale, + username: payload.username, + verified: payload.verified, + discriminator: payload.discriminator, + + mfaEnabled: payload.mfa_enabled, + accentColor: payload.accent_color, + premiumType: payload.premium_type, + publicFlags: payload.public_flags + } +} + +export function s1nakelize1User (payload: Camelize): DiscordUser { + return { + id: payload.id, + bot: payload.bot, + email: payload.email, + flags: payload.flags, + avatar: payload.avatar, + system: payload.system, + banner: payload.banner, + locale: payload.locale, + username: payload.username, + verified: payload.verified, + discriminator: payload.discriminator, + + mfa_enabled: payload.mfaEnabled, + accent_color: payload.accentColor, + premium_type: payload.premiumType, + public_flags: payload.publicFlags + } +} diff --git a/packages/transformer/src/index.ts b/packages/transformer/src/index.ts new file mode 100644 index 000000000..0a561b008 --- /dev/null +++ b/packages/transformer/src/index.ts @@ -0,0 +1,13 @@ +import { c1amelize1User, s1nakelize1User } from './casing/user.js' + +export * from './casing/index.js' + +export const TRANSFORMERS = { + user: c1amelize1User, + + reverse: { + user: s1nakelize1User + } +} + +export default TRANSFORMERS diff --git a/packages/transformer/tests/empty.spec.ts b/packages/transformer/tests/empty.spec.ts new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/packages/transformer/tests/empty.spec.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/transformer/tsconfig.json b/packages/transformer/tsconfig.json new file mode 100644 index 000000000..50e6ef6eb --- /dev/null +++ b/packages/transformer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "outDir": "./dist", + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.tsx" + ], + "exclude": [ + "node_modules", + "dist", + "test", + "tests" + ] +} \ No newline at end of file diff --git a/packages/transformer/tsconfig.test.json b/packages/transformer/tsconfig.test.json new file mode 100644 index 000000000..eeb80c521 --- /dev/null +++ b/packages/transformer/tsconfig.test.json @@ -0,0 +1,11 @@ +{ + "extends": "tsconfig/test.json", + "include": [ + "tests", + ], + "exclude": [ + "node_modules", + "dist", + "src" + ] +} \ No newline at end of file diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json index d46e530f6..753d5ef02 100644 --- a/packages/tsconfig/base.json +++ b/packages/tsconfig/base.json @@ -18,7 +18,8 @@ "skipLibCheck": true, "strict": true, "incremental": true, - "importsNotUsedAsValues": "error" + "importsNotUsedAsValues": "error", + "resolveJsonModule": true }, "exclude": [ "node_modules", diff --git a/yarn.lock b/yarn.lock index 9c30de1cd..0a063e53b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,6 +25,7 @@ __metadata: version: 0.0.0-use.local resolution: "@discordeno/rest@workspace:packages/rest" dependencies: + "@discordeno/transformer": 18.0.0-alpha.1 "@discordeno/types": 18.0.0-alpha.1 "@swc/cli": ^0.1.57 "@swc/core": ^1.3.21 @@ -47,6 +48,31 @@ __metadata: languageName: unknown linkType: soft +"@discordeno/transformer@18.0.0-alpha.1, @discordeno/transformer@workspace:packages/transformer": + version: 0.0.0-use.local + resolution: "@discordeno/transformer@workspace:packages/transformer" + dependencies: + "@discordeno/types": 18.0.0-alpha.1 + "@swc/cli": ^0.1.57 + "@swc/core": ^1.3.21 + "@types/benchmark": ^2.1.2 + "@types/chai": ^4 + "@types/mocha": ^10 + "@types/node": ^18.11.9 + "@types/sinon": ^10.0.13 + benchmark: ^2.1.4 + c8: ^7.12.0 + chai: ^4.3.7 + eslint: ^8.0.1 + eslint-config-discordeno: "*" + microtime: ^3.1.1 + mocha: ^10.1.0 + sinon: ^15.0.0 + tsconfig: "*" + typescript: ^4.9.3 + languageName: unknown + linkType: soft + "@discordeno/types@18.0.0-alpha.1, @discordeno/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@discordeno/types@workspace:packages/types" @@ -411,6 +437,13 @@ __metadata: languageName: node linkType: hard +"@types/benchmark@npm:^2.1.2": + version: 2.1.2 + resolution: "@types/benchmark@npm:2.1.2" + checksum: dc5e544ffca59bdc61008d89f007c14c281c17e80d50385da81eb9182e4466a9208ceb7af72ddfb4df1e2445ea35f2af36854168f698edd4c015a4262232462d + languageName: node + linkType: hard + "@types/chai-as-promised@npm:^7": version: 7.1.5 resolution: "@types/chai-as-promised@npm:7.1.5" @@ -860,6 +893,16 @@ __metadata: languageName: node linkType: hard +"benchmark@npm:^2.1.4": + version: 2.1.4 + resolution: "benchmark@npm:2.1.4" + dependencies: + lodash: ^4.17.4 + platform: ^1.3.3 + checksum: aa466561d4f2b0a2419a3069b8f90fd35ffacf26849697eea9de525ecfbd10b44da11070cc51c88d772076db8cb2415641b493de7d6c024fdf8551019c6fcf1c + languageName: node + linkType: hard + "bin-check@npm:^4.1.0": version: 4.1.0 resolution: "bin-check@npm:4.1.0" @@ -3313,6 +3356,13 @@ __metadata: languageName: node linkType: hard +"lodash@npm:^4.17.4": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 + languageName: node + linkType: hard + "log-symbols@npm:4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" @@ -3473,6 +3523,17 @@ __metadata: languageName: node linkType: hard +"microtime@npm:^3.1.1": + version: 3.1.1 + resolution: "microtime@npm:3.1.1" + dependencies: + node-addon-api: ^5.0.0 + node-gyp: latest + node-gyp-build: ^4.4.0 + checksum: 1161571d9c994070139d8dd65160bb5ebb87044cb0ea9828a65fde201fcf9b90da9c2897e352a26e852a7fa9dd7b6774128b50854462d117be1b019abff41887 + languageName: node + linkType: hard + "mime-db@npm:^1.28.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" @@ -3733,6 +3794,26 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^5.0.0": + version: 5.0.0 + resolution: "node-addon-api@npm:5.0.0" + dependencies: + node-gyp: latest + checksum: 7c5e2043ac37f6108784d94ed73a44ae6d3e68eb968de60680922fc6bc3d17fa69448c0feb4e0c9d3f4c74a0324822e566a8340a56916d9d6f23cb3e85620334 + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.4.0": + version: 4.5.0 + resolution: "node-gyp-build@npm:4.5.0" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: d888bae0fb88335f69af1b57a2294a931c5042f36e413d8d364c992c9ebfa0b96ffe773179a5a2c8f04b73856e8634e09cce108dbb9804396d3cc8c5455ff2db + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 9.3.1 resolution: "node-gyp@npm:9.3.1" @@ -4116,6 +4197,13 @@ __metadata: languageName: node linkType: hard +"platform@npm:^1.3.3": + version: 1.3.6 + resolution: "platform@npm:1.3.6" + checksum: 6f472a09c61d418c7e26c1c16d0bdc029549d512dbec6526216a1e59ec68100d07007d0097dcba69dddad883d6f2a83361b4bdfe0094a3d9a2af24158643d85e + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1"