From f05d21508dc6dea9a5e5cb8b70cd55731b5027ba Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 15 Sep 2024 13:30:45 +0545 Subject: [PATCH 1/2] feat: apply script result --- CONTRIBUTING.md | 115 +++++++++++++++- README.md | 19 +-- apps/yasumu/src-tauri/runtime/02_runtime.ts | 60 +++++++-- apps/yasumu/src-tauri/runtime/_common.ts | 67 +++------- .../request/input/request-input.tsx | 90 ++++++++----- apps/yasumu/src/lib/scripts/script.ts | 15 +-- .../src/stores/api-testing/console.store.ts | 7 +- .../src/core/api/workspace/YasumuWorkspace.ts | 18 ++- .../api/workspace/modules/rest/YasumuRest.ts | 4 + .../rest/YasumuScriptResultEvaluator.ts | 126 ++++++++++++++++++ .../core/api/workspace/modules/rest/index.ts | 2 + 11 files changed, 406 insertions(+), 117 deletions(-) create mode 100644 packages/core/src/core/api/workspace/modules/rest/YasumuScriptResultEvaluator.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb92ae7..52c6340 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,116 @@ # Contributing to Yasumu -TODO +We appreciate your interest in contributing to Yasumu! Whether you're fixing bugs, adding new features, improving documentation, or anything else, your contributions are valuable. Follow the steps below to set up your development environment and start contributing. + +## Getting Started + +### 1. Clone the Repository + +Begin by cloning the Yasumu repository to your local machine. This will create a local copy of the project where you can work on your contributions. + +```sh +git clone https://github.com/yasumu-org/yasumu.git +``` + +### 2. Navigate to the Project Directory + +Once the repository is cloned, change into the project directory. + +```sh +cd yasumu +``` + +### 3. Install Dependencies + +Before working on the project, you will need to install all necessary dependencies. Yasumu uses Yarn v4 as its package manager. Run the following command to install dependencies: + +```sh +yarn install +``` + +This will install all the required packages and dependencies specified in the `package.json` file. Make sure you have [Yarn](https://yarnpkg.com/getting-started/install) installed on your machine before proceeding. + +### 4. Build the Project + +Yasumu is structured as a multi-package repository (monorepo), meaning it contains several packages. You need to build the project before running it. Building ensures that all source files are compiled and ready for execution. + +```sh +yarn build +``` + +The build process compiles the TypeScript files and ensures everything is in sync across packages. If you make any changes to the source code, be sure to run this command again to reflect those updates. + +### 5. Run the Yasumu Application + +To start the Yasumu application, use the following command: + +```sh +yarn workspace @yasumu/app app +``` + +This command runs the application located in the `@yasumu/app` workspace, which is the main application of the Yasumu project. + +## Running Tests + +To ensure that everything works correctly, you should run the test suite. Yasumu has a set of automated tests to maintain code quality and catch potential issues. To run the tests, use: + +```sh +yarn test +``` + +This will execute all the unit tests and report any errors. Make sure to run the tests after making changes to ensure that your contribution doesn’t break existing functionality. + +## Code Style and Linting + +Yasumu follows specific code formatting and linting rules to maintain a clean and readable codebase. Please make sure that your code adheres to these standards. + +To check for linting errors and automatically fix them (where possible), run: + +```sh +yarn lint --fix +``` + +This will run ESLint on the project and fix any auto-fixable issues. Be sure to fix any remaining linting errors manually before submitting your changes. + +## Submitting a Pull Request + +Once you've made your changes, you're ready to submit a pull request (PR). Follow these steps: + +1. **Create a New Branch** + It's a good practice to create a new branch for each feature or bug fix you work on. Use descriptive branch names that explain the purpose of the branch, e.g., `fix-bug-123` or `feature-new-component`. + + ```sh + git checkout -b feature/your-feature-name + ``` + +2. **Commit Your Changes** + Make sure your commit messages are descriptive and follow conventional commit guidelines (e.g., `feat: add new feature`, `fix: resolve issue with something`). You can take a look at [Conventional Commits](https://www.conventionalcommits.org/) for more information. + + ```sh + git add . + git commit -m "feat: add feature X" + ``` + +3. **Push the Changes to Your Fork** + Push your branch to your forked repository. + + ```sh + git push origin feature/your-feature-name + ``` + +4. **Open a Pull Request** + Go to the Yasumu repository on GitHub and open a new pull request. Provide a detailed description of your changes, the problem you're solving, and any additional context that may be helpful. + + Once the PR is submitted, it will be reviewed by the maintainers. If any changes are requested, please address them in a timely manner to help get your contribution merged quickly. + +## Additional Resources + +- **Documentation**: Ensure you're familiar with the [Yasumu documentation](https://docs.yasumu.dev) to understand how the system works and where your contributions will fit. +- **Issue Tracker**: If you're unsure what to work on, check the open issues on the [GitHub Issue Tracker](https://github.com/yasumu-org/yasumu/issues). Feel free to claim any issue or start a discussion if you need more clarification. +- **Coding Guidelines**: Make sure to review the project's coding standards and best practices to ensure consistency across the codebase. + +## Need Help? + +If you have any questions, feel free to reach out by opening an issue or joining the discussion in the project's community forum or [Discord](https://discord.yasumu.dev) server. + +We look forward to your contributions! diff --git a/README.md b/README.md index 58012b9..74fc579 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ Yasumu is a customizable, free and open-source application to test various types The following table shows the status of the features in the project. -| Feature | Status | -| ----------------- | ----------- | -| REST API Testing | In Progress | -| SMTP Server | In Progress | -| GraphQL Testing | In Progress | -| WebSocket Testing | Planned | -| gRPC Testing | Planned | -| Custom Plugins | Planned | -| Custom Themes | Planned | +| Feature | Status | +| ----------------- | ------- | +| REST API Testing | ✅ | +| SMTP Server | ✅ | +| GraphQL Testing | Planned | +| WebSocket Testing | Planned | +| gRPC Testing | Planned | +| Custom Plugins | Planned | +| Custom Themes | Planned | ## About @@ -77,3 +77,4 @@ For any queries, you can contact the maintainers at [contact@yasumu.dev](mailto: | Bibek Raj Ghimire | Developer | [GitHub](https://github.com/ghimirebibek) | | Samir Paudyal | Developer | [GitHub](https://github.com/samir-byte) | | Sulav Niroula | Frontend Developer | [GitHub](https://github.com/sulav7) | +| Anish | Frontend Developer | [GitHub](https://github.com/novanish) | diff --git a/apps/yasumu/src-tauri/runtime/02_runtime.ts b/apps/yasumu/src-tauri/runtime/02_runtime.ts index c128df0..f92c426 100644 --- a/apps/yasumu/src-tauri/runtime/02_runtime.ts +++ b/apps/yasumu/src-tauri/runtime/02_runtime.ts @@ -6,6 +6,10 @@ return Yasumu.context.data; } + public get id(): string { + return this.data.request.id; + } + public get url(): string { return this.data.request.url; } @@ -15,12 +19,16 @@ } public get headers(): Headers { - Yasumu.context.__meta.requestHeaders = new Headers(this.data.request.headers); - return Yasumu.context.__meta.requestHeaders; + const headers = new Headers(this.data.request.headers as HeadersInit); + + // @ts-ignore + Yasumu.context.__meta.request.headers = headers; + + return headers; } public cancel(): void { - Yasumu.context.__meta.requestCanceled = true; + Yasumu.context.__meta.request.canceled = true; } } @@ -29,6 +37,26 @@ return Yasumu.context.data; } + public get contentLength(): number { + return this.data.response.contentLength; + } + + public get redirected(): boolean { + return this.data.response.redirected; + } + + public get type(): string { + return this.data.response.type; + } + + public get ok(): boolean { + return this.data.response.ok; + } + + public get cookies(): Cookie[] { + return this.data.response.cookies; + } + public get url(): string { return this.data.response.url; } @@ -38,7 +66,7 @@ } public get headers(): Headers { - return new Headers(this.data.response.headers); + return new Headers(this.data.response.headers as HeadersInit); } public get status(): number { @@ -60,37 +88,53 @@ class YasumuStoreModel implements YasumuStore { private get store() { + return Yasumu.context.data.store; + } + + private get changes() { return Yasumu.context.__meta.store; } public get(key: string): any { - return this.store[key]; + return this.store[key] ?? this.changes.find((change) => change.key === key)?.value; } public set(key: string, value: any): void { this.store[key] = value; + this.changes.push({ op: 'set', key, value }); } public remove(key: string): void { delete this.store[key]; + this.changes.push({ op: 'delete', key }); } public has(key: string): boolean { - return key in this.store; + return key in this.store || this.changes.some((change) => change.key === key); } public count(): number { - return Object.keys(this.store).length; + const keys = Object.keys(this.store); + return keys.length + this.changes.filter((change) => change.op === 'set' && !keys.includes(change.key)).length; } public clear(): void { for (const key in this.store) { + this.changes.push({ op: 'delete', key }); delete this.store[key]; } } public entries(): [string, any][] { - return Object.entries(this.store); + const main = Object.entries(this.store); + + const res = [ + this.changes + .filter((change) => change.op === 'set' && !main.some((v) => v[0] === change.key)) + .map((v) => [v.key, v.value]), + ]; + + return res.flat() as [string, any][]; } } diff --git a/apps/yasumu/src-tauri/runtime/_common.ts b/apps/yasumu/src-tauri/runtime/_common.ts index 5f1f726..c1ab42b 100644 --- a/apps/yasumu/src-tauri/runtime/_common.ts +++ b/apps/yasumu/src-tauri/runtime/_common.ts @@ -1,3 +1,13 @@ +import { + LogType as YasumuLogType, + LogStream as YasumuLogStream, + YasumuResponseContextData, + YasumuRequestContextData, + YasumuContextData as YasumuContextDataCore, + YasumuContextMeta as YasumuContextMetaCore, + YasumuCookie, +} from '@yasumu/core'; + declare global { interface YasumuRequire { (id: string): any; @@ -5,6 +15,8 @@ declare global { cache: Record; } + type Cookie = YasumuCookie; + var __require: YasumuRequire; // @ts-ignore @@ -19,14 +31,8 @@ declare global { // @ts-ignore var crypto: YasumuCrypto; - type LogType = readonly ['log', 'error', 'warn', 'info', 'clear']; - - type LogStream = { - type: LogType[number]; - args: any[]; - timestamp: number; - test?: boolean; - }; + type LogType = YasumuLogType; + type LogStream = YasumuLogStream; interface NamedConsoleMethods { clear(): void; @@ -40,23 +46,6 @@ declare global { > & NamedConsoleMethods; - interface YasumuRequest { - url: string; - method: string; - headers: Headers; - cancel(): void; - } - - interface YasumuResponse { - url: string; - method: string; - headers: Headers; - status: number; - statusText: string; - bodyText: string; - responseTime: number; - } - interface YasumuStore { get(key: string): any; set(key: string, value: any): void; @@ -67,28 +56,16 @@ declare global { entries(): [string, any][]; } - interface YasumuContextData { - request: { - url: string; - method: string; - headers: Record; - }; - response: { - url: string; - method: string; - headers: Record; - status: number; - statusText: string; - bodyText: string; - responseTime: number; - }; + type YasumuContextData = YasumuContextDataCore; + type YasumuContextMeta = YasumuContextMetaCore; + + interface YasumuRequest extends Omit { + headers: Headers; + cancel(): void; } - interface YasumuContextMeta { - store: Record; - console: LogStream[]; - requestHeaders: Headers; - requestCanceled: boolean; + interface YasumuResponse extends Omit { + headers: Headers; } interface YasumuFeatures { diff --git a/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx b/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx index a413520..594ffd0 100644 --- a/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx +++ b/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { BodyMode, HttpMethods, HttpMethodsArray } from '@yasumu/core'; +import { BodyMode, HttpMethods, HttpMethodsArray, YasumuResponseContextData } from '@yasumu/core'; import { cn, IS_AUDIO, IS_BINARY_DATA, IS_IMAGE, IS_VIDEO } from '@/lib/utils'; import { useRequestConfig, useRequestStore } from '@/stores/api-testing/request-config.store'; import { ICookie, useResponse } from '@/stores/api-testing/response.store'; @@ -87,19 +87,31 @@ export default function RequestInput() { const dispatchRequest = useCallback(async () => { try { + if (!Yasumu.workspace) return; const controller = new AbortController(); responseStore.setAbortController(controller); const h = new Headers(); - const contextData: Record = { - response: {}, + const kv = Yasumu.workspace.openKV(); + const storeData = await kv.entries(); + const storeRecord = storeData.reduce( + (acc, [key, value]) => { + acc[key] = value; + return acc; + }, + {} as Record, + ); + + const contextData: YasumuContextData = { + response: {} as YasumuResponseContextData, request: { id, url, method, - headers: h, + headers: h as any, }, + store: storeRecord, }; headers.forEach((header) => { @@ -171,52 +183,54 @@ export default function RequestInput() { break; } - contextData.request.body = bodyData; - if (!!preRequestScript?.trim().length) { const result = await Yasumu.scripts.run(preRequestScript, Yasumu.scripts.createContextData(contextData), { test: false, }); - if (canEvaluateResult(result) && result.console && result.console.length) { - add(result.console); - } else if (result && typeof result === 'object' && '$error' in result) { - add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); - } else if (canEvaluateResult(result)) { - if (result.requestHeaders?.length) { + if (canEvaluateResult(result)) { + if (result.request.canceled) { + controller.abort(); + } + + if (result.console && result.console.length) { + add(result.console); + } + + await Yasumu.workspace.rest.scriptResults.applyContext(result, contextData); + + if (result.request.headers) { try { - result.requestHeaders.forEach(([key, value]) => { - h.set(key, value); + const headers = Object.entries(result.request.headers); + headers.forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => h.append(key, v)); + } else { + h.set(key, value); + } }); } catch { // } } - // if (Object.keys(result.store).length) { - // const id = Yasumu.workspace?.metadata.id; - - // if (id) { - // const store = Yasumu.createStore(id); - - // for (const key in result.store) { - // await store.set(key, result.store[key]); - // } - // } - // } + if (result.request.url && typeof result.request.url === 'string') { + setUrl(result.request.url); + } + } else if (result && typeof result === 'object' && '$error' in result) { + add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); } } const start = Date.now(); - const res = await Yasumu.fetch(url, { + const res = await Yasumu.fetch(contextData.request.url || url, { method: method.toUpperCase(), body: bodyData, redirect: 'follow', // @ts-ignore - maxRedirections: 20, + maxRedirections: 10, cache: 'no-cache', - credentials: 'omit', connectTimeout: 60_000, headers: h, signal: controller.signal, @@ -265,7 +279,7 @@ export default function RequestInput() { const bodySize = Number(res.headers.get('Content-Length')) || value.byteLength; - contextData.response.body = str; + contextData.response.bodyText = str; contextData.response.contentLength = bodySize; responseStore.setBody(str); @@ -277,7 +291,7 @@ export default function RequestInput() { responseStore.setResponseSize(len); } - contextData.response.headers = res.headers; + contextData.response.headers = Object.fromEntries(res.headers.entries()); contextData.response.status = res.status; contextData.response.statusText = res.statusText; contextData.response.redirected = res.redirected; @@ -296,8 +310,12 @@ export default function RequestInput() { }, ); - if (canEvaluateResult(result) && result.console && result.console.length) { - add(result.console); + if (canEvaluateResult(result)) { + if (result.console && result.console.length) { + add(result.console); + } + + await Yasumu.workspace.rest.scriptResults.applyContext(result, contextData); } else if (result && typeof result === 'object' && '$error' in result) { add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); } @@ -308,8 +326,12 @@ export default function RequestInput() { test: true, }); - if (canEvaluateResult(result) && result.console && result.console.length) { - add(result.console); + if (canEvaluateResult(result)) { + if (result.console && result.console.length) { + add(result.console); + } + + await Yasumu.workspace.rest.scriptResults.applyContext(result, contextData); } else if (result && typeof result === 'object' && '$error' in result) { add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); } diff --git a/apps/yasumu/src/lib/scripts/script.ts b/apps/yasumu/src/lib/scripts/script.ts index 260ebed..a41ebe1 100644 --- a/apps/yasumu/src/lib/scripts/script.ts +++ b/apps/yasumu/src/lib/scripts/script.ts @@ -1,17 +1,4 @@ -export type ConsoleLogLevel = 'log' | 'warn' | 'error' | 'info'; - -export interface ConsoleStream { - type: ConsoleLogLevel; - args: string[]; - timestamp: number; - test?: boolean; -} - -export interface YasumuPostEvaluationData { - store: Record; - requestHeaders: Array<[string, string]> | null; - console: ConsoleStream[]; -} +export type YasumuPostEvaluationData = YasumuContextMeta; export function canEvaluateResult(data: YasumuPostEvaluationData | unknown): data is YasumuPostEvaluationData { return !!(data && typeof data === 'object'); diff --git a/apps/yasumu/src/stores/api-testing/console.store.ts b/apps/yasumu/src/stores/api-testing/console.store.ts index 8a9833b..fb26caf 100644 --- a/apps/yasumu/src/stores/api-testing/console.store.ts +++ b/apps/yasumu/src/stores/api-testing/console.store.ts @@ -1,15 +1,14 @@ -import { ConsoleStream } from '@/lib/scripts/script'; import { create } from 'zustand'; export interface IConsole { - logs: ConsoleStream[]; - add: (log: ConsoleStream | ConsoleStream[]) => void; + logs: LogStream[]; + add: (log: LogStream | LogStream[]) => void; clear: () => void; } export const useConsole = create((set) => ({ logs: [], - add: (log: ConsoleStream | ConsoleStream[]) => + add: (log: LogStream | LogStream[]) => set((state) => ({ logs: [...state.logs, ...(Array.isArray(log) ? log : [log])] })), clear: () => set({ logs: [] }), })); diff --git a/packages/core/src/core/api/workspace/YasumuWorkspace.ts b/packages/core/src/core/api/workspace/YasumuWorkspace.ts index 0b36fc2..ac8bbc3 100644 --- a/packages/core/src/core/api/workspace/YasumuWorkspace.ts +++ b/packages/core/src/core/api/workspace/YasumuWorkspace.ts @@ -8,9 +8,9 @@ import { YasumuSmtp } from './modules/smtp/YasumuSmtp.js'; import { YasumuStoreKeys, YasumuWorkspaceFiles } from './constants.js'; import { Commands, - type CommandInvocation, type CommandsInvocationMap, } from '@/core/common/commands.js'; +import type { StoreCommon } from '@/externals/index.js'; export interface YasumuWorkspaceInit { path: string; @@ -22,6 +22,7 @@ export interface YasumuWorkspaceHistory { } export class YasumuWorkspace { + private _kv: StoreCommon | null = null; public metadata!: YasumuWorkspaceMetadata; public readonly rest: YasumuRest; public readonly smtp: YasumuSmtp; @@ -39,6 +40,19 @@ export class YasumuWorkspace { this.smtp = new YasumuSmtp(this); } + /** + * Open the key-value store for this workspace + */ + public openKV() { + if (this._kv) return this._kv; + + const name = this.metadata.id; + + this._kv = this.yasumu.createStore(name); + + return this._kv; + } + /** * Retrieve the path of the workspace */ @@ -153,7 +167,7 @@ export class YasumuWorkspace { */ public async send< Cmd extends Commands, - InvocationData extends CommandsInvocationMap[Cmd] + InvocationData extends CommandsInvocationMap[Cmd], >(command: Cmd, data: InvocationData[0]): Promise { return this.yasumu.commands.invoke(command, data); } diff --git a/packages/core/src/core/api/workspace/modules/rest/YasumuRest.ts b/packages/core/src/core/api/workspace/modules/rest/YasumuRest.ts index 32da48c..2930637 100644 --- a/packages/core/src/core/api/workspace/modules/rest/YasumuRest.ts +++ b/packages/core/src/core/api/workspace/modules/rest/YasumuRest.ts @@ -3,6 +3,7 @@ import { YasumuWorkspace } from '../../YasumuWorkspace.js'; import { YasumuWorkspaceFiles } from '../../constants.js'; import { YasumuRestEntity } from './YasumuRestEntity.js'; import { YasumuRestImports } from './YasumuRestImports.js'; +import { YasumuScriptResultEvaluator } from './YasumuScriptResultEvaluator.js'; export interface YasumuRestRequest { path: string; @@ -19,6 +20,9 @@ export interface TreeViewElement { export class YasumuRest { public readonly imports = new YasumuRestImports(this); + public readonly scriptResults = new YasumuScriptResultEvaluator( + this.workspace + ); /** * Create a new YasumuRest instance diff --git a/packages/core/src/core/api/workspace/modules/rest/YasumuScriptResultEvaluator.ts b/packages/core/src/core/api/workspace/modules/rest/YasumuScriptResultEvaluator.ts new file mode 100644 index 0000000..a86ee71 --- /dev/null +++ b/packages/core/src/core/api/workspace/modules/rest/YasumuScriptResultEvaluator.ts @@ -0,0 +1,126 @@ +import type { YasumuWorkspace } from '../../YasumuWorkspace.js'; + +export type LogType = readonly ['log', 'warn', 'error', 'info', 'clear']; +export interface LogStream { + type: LogType[number]; + args: any[]; + timestamp: number; +} + +export interface YasumuRequestContextData { + id: string; + url: string; + method: string; + headers: Record; + canceled?: boolean; +} + +export interface YasumuResponseContextData { + url: string; + method: string; + headers: Record; + status: number; + statusText: string; + bodyText: string; + responseTime: number; + contentLength: number; + redirected: boolean; + type: string; + ok: boolean; + cookies: YasumuCookie[]; +} + +export interface YasumuCookie { + name: string; + value: string; + domain: string; + path: string; + expires: string; + httpOnly: boolean; + secure: boolean; + sameSite: string; +} + +export interface YasumuContextData { + store: Record; + request: YasumuRequestContextData; + response: YasumuResponseContextData; +} + +export type TestStatus = 'pass' | 'fail' | 'skip'; + +export interface TestResult { + name: string; + status: TestStatus; + time: number; + message?: string; +} + +export interface YasumuContextMeta { + store: KV[]; + console: LogStream[]; + test: TestResult[]; + request: YasumuRequestContextData; + response: YasumuResponseContextData; +} + +export type KvOp = 'set' | 'delete'; + +export interface KV { + op: KvOp; + key: K; + value?: V; +} + +export class YasumuScriptResultEvaluator { + /** + * Create a new instance of the evaluator + * @param workspace The workspace to use + */ + public constructor(public readonly workspace: YasumuWorkspace) {} + + /** + * Apply the changes to the context + * @param changes The changes to apply + * @param ctx The context to apply the changes to + */ + public async applyContext( + changes: YasumuContextMeta, + ctx: YasumuContextData + ) { + if ('request' in changes && 'request' in ctx) { + Object.assign(ctx.request, changes.request); + } + + if ('response' in changes && 'response' in ctx) { + Object.assign(ctx.response, changes.response); + } + + if ('store' in changes && changes.store.length > 0) { + await this.updateStore(changes.store); + } + } + + /** + * Update the store with the given changes + * @param store The changes to apply to the store + */ + public async updateStore(store: KV[]) { + if (store.length < 1) return; + + const kv = this.workspace.openKV(); + + for (const action of store) { + switch (action.op) { + case 'set': + await kv.set(action.key, action.value); + break; + case 'delete': + await kv.delete(action.key); + break; + default: + break; + } + } + } +} diff --git a/packages/core/src/core/api/workspace/modules/rest/index.ts b/packages/core/src/core/api/workspace/modules/rest/index.ts index 3d93faf..a0e82b4 100644 --- a/packages/core/src/core/api/workspace/modules/rest/index.ts +++ b/packages/core/src/core/api/workspace/modules/rest/index.ts @@ -1,2 +1,4 @@ export * from './YasumuRest.js'; export * from './YasumuRestEntity.js'; +export * from './YasumuRestImports.js'; +export * from './YasumuScriptResultEvaluator.js'; From bd539632628c80850344028acd48e3b40e12daa7 Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:41:15 +0545 Subject: [PATCH 2/2] fix: apply lots of improvements --- apps/yasumu/src-tauri/Cargo.lock | 40 +++++++++- apps/yasumu/src-tauri/Cargo.toml | 2 +- apps/yasumu/src-tauri/capabilities/main.json | 14 +++- apps/yasumu/src-tauri/runtime/02_runtime.ts | 80 ++++++++++++++----- apps/yasumu/src-tauri/runtime/03_console.ts | 26 ++++-- apps/yasumu/src-tauri/runtime/04_test.ts | 41 +++++----- apps/yasumu/src-tauri/runtime/_common.ts | 3 + apps/yasumu/src-tauri/src/commands/scripts.rs | 31 ++++++- .../request/input/request-input.tsx | 31 ++++++- .../response/response-attachment-guard.tsx | 13 ++- .../(components)/response/response-viewer.tsx | 28 ++++++- .../response/stats/network-info.tsx | 2 +- .../response/stats/response-time.tsx | 30 +++++-- .../{console => results}/yasumu-console.tsx | 22 ++--- .../components/results/yasumu-test-result.tsx | 76 ++++++++++++++++++ apps/yasumu/src/lib/scripts/script.ts | 2 +- .../api-testing/request-config.store.ts | 10 +-- .../src/stores/api-testing/response.store.ts | 14 +++- .../stores/api-testing/script-time.store.ts | 19 +++++ .../src/stores/api-testing/test.store.ts | 15 ++++ .../rest/YasumuScriptResultEvaluator.ts | 3 +- test/http/Echo.GET | 2 +- 22 files changed, 404 insertions(+), 100 deletions(-) rename apps/yasumu/src/components/{console => results}/yasumu-console.tsx (68%) create mode 100644 apps/yasumu/src/components/results/yasumu-test-result.tsx create mode 100644 apps/yasumu/src/stores/api-testing/script-time.store.ts create mode 100644 apps/yasumu/src/stores/api-testing/test.store.ts diff --git a/apps/yasumu/src-tauri/Cargo.lock b/apps/yasumu/src-tauri/Cargo.lock index 215cc43..ea13fc6 100644 --- a/apps/yasumu/src-tauri/Cargo.lock +++ b/apps/yasumu/src-tauri/Cargo.lock @@ -1864,8 +1864,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -3026,6 +3028,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nid" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abdf1789932b85dc39446e27f45a1064a30f9e19a2b872b1d09bd59283f85f3" +dependencies = [ + "rand 0.8.5", + "thiserror", +] + [[package]] name = "nix" version = "0.27.1" @@ -5339,9 +5351,9 @@ dependencies = [ [[package]] name = "tanxium" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a78074a6843e5de930769336a5b70aa7916f7b9874edd5d53ca36bb2e53e4c" +checksum = "c87760f2c28d2e1046a8bfbd8367c094e94d2e7d067338fbb9606553cdd4031c" dependencies = [ "boa_engine", "boa_gc", @@ -5349,8 +5361,11 @@ dependencies = [ "boa_runtime", "deno_ast", "futures-util", + "nid", + "rand 0.8.5", "reqwest", "smol", + "ulid", "uuid", ] @@ -6223,6 +6238,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "ulid" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f903f293d11f31c0c29e4148f6dc0d033a7f80cebc0282bea147611667d289" +dependencies = [ + "getrandom 0.2.15", + "rand 0.8.5", + "web-time", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -6552,6 +6578,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.1" diff --git a/apps/yasumu/src-tauri/Cargo.toml b/apps/yasumu/src-tauri/Cargo.toml index 39a7c8e..5134bbf 100644 --- a/apps/yasumu/src-tauri/Cargo.toml +++ b/apps/yasumu/src-tauri/Cargo.toml @@ -38,5 +38,5 @@ tauri-plugin-shell = "2.0.0-rc.1" tauri-plugin-process = "2.0.0-rc.0" mailparse = "0.15.0" smol = "2.0.2" -tanxium = "0.1.2" +tanxium = "0.1.3" uuid = { version = "1.10.0", features = ["fast-rng"] } diff --git a/apps/yasumu/src-tauri/capabilities/main.json b/apps/yasumu/src-tauri/capabilities/main.json index 7a67f3c..f8f697a 100644 --- a/apps/yasumu/src-tauri/capabilities/main.json +++ b/apps/yasumu/src-tauri/capabilities/main.json @@ -2,9 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-capability", "description": "Capability for main window", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:app:allow-version", "core:app:allow-name", @@ -54,6 +52,14 @@ "store:allow-set", "store:allow-save", "store:allow-load", + "store:allow-clear", + "store:allow-delete", + "store:allow-entries", + "store:allow-has", + "store:allow-keys", + "store:allow-length", + "store:allow-reset", + "store:allow-values", { "identifier": "fs:allow-write-text-file", "allow": [ @@ -153,4 +159,4 @@ }, "process:default" ] -} \ No newline at end of file +} diff --git a/apps/yasumu/src-tauri/runtime/02_runtime.ts b/apps/yasumu/src-tauri/runtime/02_runtime.ts index f92c426..c469522 100644 --- a/apps/yasumu/src-tauri/runtime/02_runtime.ts +++ b/apps/yasumu/src-tauri/runtime/02_runtime.ts @@ -86,50 +86,68 @@ } } - class YasumuStoreModel implements YasumuStore { - private get store() { - return Yasumu.context.data.store; + const getStore = () => { + const val = Yasumu.context.data.store; + + if (!val) { + return (Yasumu.context.data.store = {}); } - private get changes() { - return Yasumu.context.__meta.store; + return val; + }; + const getChanges = () => { + const val = Yasumu.context.__meta.store; + + if (!Array.isArray(val)) { + return (Yasumu.context.__meta.store = []); } + return val; + }; + + class YasumuStoreModel implements YasumuStore { public get(key: string): any { - return this.store[key] ?? this.changes.find((change) => change.key === key)?.value; + return getStore()[key] ?? getChanges().find((change) => change.key === key)?.value; } public set(key: string, value: any): void { - this.store[key] = value; - this.changes.push({ op: 'set', key, value }); + if (value === undefined) { + throw new Error('Value cannot be undefined'); + } + + getStore()[key] = value; + getChanges().push({ op: 'set', key, value }); } public remove(key: string): void { - delete this.store[key]; - this.changes.push({ op: 'delete', key }); + delete getStore()[key]; + getChanges().push({ op: 'delete', key }); } public has(key: string): boolean { - return key in this.store || this.changes.some((change) => change.key === key); + return key in getStore() || getChanges().some((change) => change.key === key); } public count(): number { - const keys = Object.keys(this.store); - return keys.length + this.changes.filter((change) => change.op === 'set' && !keys.includes(change.key)).length; + const keys = Object.keys(getStore()); + return keys.length + getChanges().filter((change) => change.op === 'set' && !keys.includes(change.key)).length; } public clear(): void { - for (const key in this.store) { - this.changes.push({ op: 'delete', key }); - delete this.store[key]; + const store = getStore(); + const changes = getChanges(); + for (const key in store) { + changes.push({ op: 'delete', key }); + delete store[key]; } } public entries(): [string, any][] { - const main = Object.entries(this.store); + const main = Object.entries(getStore()); + const changes = getChanges(); const res = [ - this.changes + changes .filter((change) => change.op === 'set' && !main.some((v) => v[0] === change.key)) .map((v) => [v.key, v.value]), ]; @@ -142,10 +160,12 @@ context: { data: {}, __meta: { - store: {}, - requestHeaders: {}, + store: [], + request: {} as any, + response: {} as any, console: [], - }, + test: [], + } satisfies YasumuContextMeta, }, setContextData(data: string | Record) { if (typeof data === 'string') { @@ -159,8 +179,24 @@ store: new YasumuStoreModel(), }; + function deeplyClean(obj: any) { + if (!obj) return; + + for (const key in obj) { + if (obj[key] === undefined) { + delete obj[key]; + } else if (typeof obj[key] === 'object') { + deeplyClean(obj[key]); + } + } + } + Yasumu.serialize = function () { - return JSON.stringify(this.context.__meta); + const obj = this.context.__meta; + + deeplyClean(obj); + + return JSON.stringify(obj); }; Object.assign(Yasumu, context); diff --git a/apps/yasumu/src-tauri/runtime/03_console.ts b/apps/yasumu/src-tauri/runtime/03_console.ts index 8583abc..408d52a 100644 --- a/apps/yasumu/src-tauri/runtime/03_console.ts +++ b/apps/yasumu/src-tauri/runtime/03_console.ts @@ -1,10 +1,13 @@ /// (() => { const COMMON_OBJECTS = new Set(['Map', 'Set', 'WeakMap', 'WeakSet']); - function inspect(value: any, visited = new WeakSet(), indentLevel = 0): string { + const MAX_DEPTH = 2; + + function inspect(value: any, visited = new WeakSet(), indentLevel = 0, root = true, depth = 0): string { try { if (typeof value === 'string') { - return `"${value}"`; + if (!root) return `"${value}"`; + return value; } if (typeof value === 'bigint') { @@ -27,14 +30,27 @@ } if (typeof value === 'object') { + if (depth > MAX_DEPTH) { + return '{ ... }'; + } if (visited.has(value)) { return '[Circular]'; } visited.add(value); + if (value instanceof Error) { + let msg = value.stack || String(value); + + if (!value.stack) { + msg += `\n${Yasumu.utils.getStackTrace()}`; + } + + return msg; + } + if (Array.isArray(value)) { - return `[${value.map((item) => inspect(item, visited, indentLevel + 2)).join(', ')}]`; + return `[${value.map((item) => inspect(item, visited, indentLevel + 2, false, depth + 1)).join(', ')}]`; } if (value && 'constructor' in value && value.constructor && COMMON_OBJECTS.has(value.constructor.name)) { @@ -50,7 +66,7 @@ for (const key in value) { if (Object.prototype.propertyIsEnumerable.call(value, key)) { enumerableFound++; - result += `${nestedIndent}${key}: ${inspect(value[key], visited, indentLevel + 2)},\n`; + result += `${nestedIndent}${key}: ${inspect(value[key], visited, indentLevel + 2, false, depth + 1)},\n`; } } @@ -71,7 +87,7 @@ } } - const CONSOLE_METHODS: LogType = ['log', 'error', 'warn', 'info', 'clear']; + const CONSOLE_METHODS: LogType = ['log', 'warn', 'error', 'info', 'clear']; const console = new Proxy({} as YasumuConsole, { get(target, prop, receiver) { diff --git a/apps/yasumu/src-tauri/runtime/04_test.ts b/apps/yasumu/src-tauri/runtime/04_test.ts index 5ff0f2e..de3dedf 100644 --- a/apps/yasumu/src-tauri/runtime/04_test.ts +++ b/apps/yasumu/src-tauri/runtime/04_test.ts @@ -1,5 +1,7 @@ /// +import type { TestResult } from '@yasumu/core'; + (() => { const isTestEnvironment = Yasumu.features.test; @@ -15,12 +17,16 @@ return; } - const warnLog = (arg: string) => - Yasumu.context.__meta.console.push({ type: 'warn', args: [arg], timestamp: Date.now(), test: true }); - const writeSuccess = (arg: string) => - Yasumu.context.__meta.console.push({ type: 'log', args: [arg], timestamp: Date.now(), test: true }); - const writeError = (arg: string) => - Yasumu.context.__meta.console.push({ type: 'error', args: [arg], timestamp: Date.now(), test: true }); + const addTestResult = (result: Omit) => { + Yasumu.context.__meta.test.push({ ...result, id: crypto.randomUUID() }); + }; + + const writeSuccess = (name: string, time: number, message: string | null) => + addTestResult({ name, status: 'pass', time, message }); + const writeError = (name: string, time: number, message: string | null) => + addTestResult({ name, status: 'fail', time, message }); + const writeSkip = (name: string, time: number, message: string | null) => + addTestResult({ name, status: 'skip', time, message }); const YASUMU_ASSERTION_ERROR = 'YasumuAssertionError'; const TEST_CONTEXT_THROWAWAY_ERROR = 'TestContextThrowawayError'; @@ -218,21 +224,16 @@ }, }; - const formatTime = (duration: number) => { - if (duration < 1000) return `${duration.toFixed(2)}ms`; - return `${duration.toFixed(2)}s`; - }; - - const evaluateState = (duration: string) => { + const evaluateState = (duration: number) => { switch (state) { case TestState.Passed: - writeSuccess(`[PASSED] (${duration}) ${name}: ${stateReason}`); + writeSuccess(name, duration, stateReason || null); break; case TestState.Failed: - writeError(`[FAILED] (${duration}) Yasumu.test(${name}): ${stateReason}`); + writeError(name, duration, stateReason || null); break; case TestState.Skipped: - warnLog(`[SKIPPED] (${duration}) Yasumu.test(${name}): ${stateReason}`); + writeSkip(name, duration, stateReason || null); break; default: break; @@ -245,20 +246,18 @@ try { fn(testContext); end = performance.now(); - writeSuccess(`[PASSED] (${formatTime(end - start)}) ${name}`); + writeSuccess(name, end - start, null); } catch (_error) { end = performance.now(); - const timeTaken = formatTime(end - start); + const timeTaken = end - start; if (_error && _error instanceof TestContextThrowawayError && state != null) return void evaluateState(timeTaken); if (_error && _error instanceof YasumuAssertionError) { - writeError(`[FAILED] (${timeTaken}) Yasumu.test(${name}):\n${_error.message}`); + writeError(name, timeTaken, _error.message || String(_error)); return; } - writeError( - `[ERROR] (${timeTaken}) Yasumu.test(${name}) failed with error:\n${(_error as Error).message || _error}`, - ); + writeError(name, timeTaken, (_error as Error).message || String(_error)); } } diff --git a/apps/yasumu/src-tauri/runtime/_common.ts b/apps/yasumu/src-tauri/runtime/_common.ts index c1ab42b..dc555ec 100644 --- a/apps/yasumu/src-tauri/runtime/_common.ts +++ b/apps/yasumu/src-tauri/runtime/_common.ts @@ -82,6 +82,9 @@ declare global { response: YasumuResponse; store: YasumuStore; features: YasumuFeatures; + utils: { + getStackTrace(): string; + }; serialize(): string; nanoseconds(): bigint; sleep(ms: number): Promise; diff --git a/apps/yasumu/src-tauri/src/commands/scripts.rs b/apps/yasumu/src-tauri/src/commands/scripts.rs index 159cd38..9cab5b2 100644 --- a/apps/yasumu/src-tauri/src/commands/scripts.rs +++ b/apps/yasumu/src-tauri/src/commands/scripts.rs @@ -2,7 +2,7 @@ use boa_engine::{ js_str, js_string, object::ObjectInitializer, property::{Attribute, PropertyDescriptorBuilder}, - JsValue, + JsString, JsValue, NativeFunction, }; use tanxium::tanxium; use tauri::{path::BaseDirectory, Manager}; @@ -19,6 +19,7 @@ fn polyfill_yasumu_api( workspace_id: String, ) { let ctx = &mut tanxium.context; + // Yasumu object let package = app.package_info(); let app_version = format!( @@ -74,6 +75,31 @@ fn polyfill_yasumu_api( .configurable(false); yasumu_obj.insert_property(js_str!("workspace"), workspace); + + let yasumu_utils = ObjectInitializer::new(ctx) + .function( + NativeFunction::from_fn_ptr(|_, _, context| { + let stack = context + .stack_trace() + .map(|frame| format!(" at {}", frame.code_block().name().to_std_string_escaped())) + .collect::>() + .join("\n"); + + Ok(JsValue::String(JsString::from(stack))) + }), + js_string!("getStackTrace"), + 0, + ) + .build(); + + let yasumu_utils_obj = PropertyDescriptorBuilder::new() + .value(yasumu_utils) + .enumerable(true) + .writable(false) + .configurable(false) + .build(); + + yasumu_obj.insert_property(js_str!("utils"), yasumu_utils_obj); } #[tauri::command] @@ -157,7 +183,6 @@ pub async fn evaluate_javascript( .unwrap(); tanxium.initialize_runtime().unwrap(); - tanxium.load_extensions(extensions).unwrap(); polyfill_yasumu_api( &mut tanxium, @@ -167,6 +192,8 @@ pub async fn evaluate_javascript( id, ); + tanxium.load_extensions(extensions).unwrap(); + let prepare_script = if ts_supported { let res = tanxium.transpile(&prepare); diff --git a/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx b/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx index 594ffd0..0c91af8 100644 --- a/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx +++ b/apps/yasumu/src/app/api-testing/(components)/request/input/request-input.tsx @@ -14,10 +14,14 @@ import { HttpMethodColors } from '@/lib/constants'; import { Yasumu } from '@/lib/yasumu'; import { useConsole } from '@/stores/api-testing/console.store'; import { canEvaluateResult } from '@/lib/scripts/script'; +import { useTest } from '@/stores/api-testing/test.store'; +import { useScriptTime } from '@/stores/api-testing/script-time.store'; export default function RequestInput() { const { current } = useRequestStore(); const { add } = useConsole(); + const { add: addTest } = useTest(); + const { setPostResponse, setPreRequest, setTestScript } = useScriptTime(); const { method, setMethod, url, setUrl, body, headers, bodyMode, id, script: preRequestScript } = useRequestConfig(); const save = useDebounceCallback(() => { @@ -184,9 +188,13 @@ export default function RequestInput() { } if (!!preRequestScript?.trim().length) { + const preScriptStart = performance.now(); const result = await Yasumu.scripts.run(preRequestScript, Yasumu.scripts.createContextData(contextData), { test: false, }); + const preScriptFinish = Math.abs(performance.now() - preScriptStart); + + setPreRequest(preScriptFinish); if (canEvaluateResult(result)) { if (result.request.canceled) { @@ -221,10 +229,15 @@ export default function RequestInput() { add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); } } + const finalUrl = contextData.request.url || url; + + if (!finalUrl) { + throw new Error('No url provided'); + } - const start = Date.now(); + const start = performance.now(); - const res = await Yasumu.fetch(contextData.request.url || url, { + const res = await Yasumu.fetch(finalUrl, { method: method.toUpperCase(), body: bodyData, redirect: 'follow', @@ -238,7 +251,7 @@ export default function RequestInput() { if (!res) throw new Error('Failed to fetch response'); - const end = Math.abs(Date.now() - start); + const end = Math.abs(performance.now() - start); responseStore.setUrl(res.url); responseStore.setResponseStatus(res.status); @@ -302,6 +315,7 @@ export default function RequestInput() { contextData.response.responseTime = end; if (!!responseStore.postRequestScript?.trim().length) { + const postScriptStart = performance.now(); const result = await Yasumu.scripts.run( responseStore.postRequestScript, Yasumu.scripts.createContextData(contextData), @@ -309,6 +323,9 @@ export default function RequestInput() { test: false, }, ); + const postScriptFinish = Math.abs(performance.now() - postScriptStart); + + setPostResponse(postScriptFinish); if (canEvaluateResult(result)) { if (result.console && result.console.length) { @@ -322,15 +339,23 @@ export default function RequestInput() { } if (!!responseStore.test?.trim().length) { + const testScriptStart = performance.now(); const result = await Yasumu.scripts.run(responseStore.test, Yasumu.scripts.createContextData(contextData), { test: true, }); + const testScriptFinish = Math.abs(performance.now() - testScriptStart); + + setTestScript(testScriptFinish); if (canEvaluateResult(result)) { if (result.console && result.console.length) { add(result.console); } + if (result.test.length) { + addTest(result.test); + } + await Yasumu.workspace.rest.scriptResults.applyContext(result, contextData); } else if (result && typeof result === 'object' && '$error' in result) { add({ args: [result.$error as string], timestamp: Date.now(), type: 'error' }); diff --git a/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx b/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx index cd76a0e..93c80d0 100644 --- a/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx +++ b/apps/yasumu/src/app/api-testing/(components)/response/response-attachment-guard.tsx @@ -12,12 +12,19 @@ export function ResponseAttachmentGuard({ const { url } = useResponse(); if (!contentType) return <>{children}; - if (IS_AUDIO(contentType)) return