refactor path handling

This commit is contained in:
Malte Teichert
2024-05-22 00:22:55 +02:00
parent b60931881d
commit 4819f609ae
6 changed files with 241 additions and 132 deletions

View File

@@ -1,6 +1,6 @@
- [x] Add path renaming - [x] Add path renaming
- [x] Add path sub-page for editing a specific route - [x] Add path sub-page for editing a specific route
- [ ] Check that a path variable can only be included once - [x] Check that a path variable can only be included once
- [ ] Add schema editor component - [ ] Add schema editor component
- [ ] Add route-parameters - [ ] Add route-parameters
- [ ] path - [ ] path
@@ -55,6 +55,10 @@
--- ---
- [ ] Replace prompt, alert and confirms with skeleton-modals - [ ] Replace prompt, alert and confirms with skeleton-modals
- [x] Create path prompt
- [x] Edit path prompt
- [x] Rename path prompt
- [x] Delete path confirm
- [ ] Add extensions - [ ] Add extensions
--- ---

View File

@@ -1,106 +1,50 @@
<script lang="ts"> <script lang="ts">
import { pathTemplate } from '$lib/pathTemplate'; import { pathTemplate } from '$lib/pathTemplate';
import { openApiStore, pathRegex, sortPathsAlphabetically } from '$lib'; import {
addPath,
deletePath,
openApiStore,
pathRegex,
renamePath,
sortPathsAlphabetically
} from '$lib';
import { getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
/** The actual pathname */ /** The actual pathname */
export let pathName: string; export let pathName: string;
/** The index of the pathname in the paths object */ /** The index of the pathname in the paths object */
export let id: number; export let id: number;
const removeRoute = () => {
if (!confirm('Are you sure you want to delete this route?')) return;
let tempPathObject = {
...$openApiStore.paths
};
delete tempPathObject[pathName];
$openApiStore.paths = tempPathObject;
};
const addSubRoute = () => {
// prompt user to enter path
const path = prompt(
`Enter path. Wrap path parameters in curly braces.\nIs directly appended to "${pathName}".`
);
if (!path) return;
let newPath = pathName + path;
// check that path is valid
if (!newPath.match(pathRegex)) {
alert('Invalid path name');
return;
}
// check if path already exists
// @ts-expect-error - we are creating a new key
if ($openApiStore.paths[newPath]) {
alert('Path already exists');
return;
}
// TODO: check if variables are duplicated
// add path to paths object
$openApiStore.paths = {
...$openApiStore.paths,
[newPath]: pathTemplate
};
// sort paths object
sortPathsAlphabetically();
};
const renamePath = () => {
const newName = prompt('Edit the path name', pathName);
if (!newName) return;
// check that path is valid
if (!newName.match(pathRegex)) {
alert('Invalid path name');
return;
}
// check if path already exists
// @ts-expect-error - we are creating a new key
if ($openApiStore.paths[newName]) {
alert('Path already exists');
return;
}
// if pathName is empty, show error
// @ts-expect-error - we are checking if the value is defined
if (!$openApiStore.paths[pathName]) {
alert('Previous value cannot be empty');
return;
}
// create new object with new key and old value
$openApiStore.paths = {
...$openApiStore.paths,
[newName]: $openApiStore.paths![pathName]
};
// delete old key
delete $openApiStore.paths[pathName];
$openApiStore.paths = {
...$openApiStore.paths
};
// sort paths object
sortPathsAlphabetically();
};
</script> </script>
<div class="flex justify-between"> <div class="flex justify-between">
<h3 class="h3">{pathName}</h3> <h3 class="h3">{pathName}</h3>
<div class="flex gap-4"> <div class="flex gap-4">
<a href="/path-{id}" class="btn btn-sm variant-filled-primary">Edit</a> <a href="/path-{id}" class="btn btn-sm variant-filled-primary">Edit</a>
<button type="button" class="btn btn-sm variant-filled-warning" on:click={renamePath} <button
>Rename</button type="button"
class="btn btn-sm variant-filled-warning"
on:click={() => {
renamePath(modalStore, pathName);
}}
>
Rename
</button>
<button
type="button"
class="btn btn-sm variant-filled-secondary"
on:click={() => {
addPath(modalStore, pathName);
}}
> >
<button type="button" class="btn btn-sm variant-filled-secondary" on:click={addSubRoute}>
Add Sub-Route Add Sub-Route
</button> </button>
<button <button
type="button" type="button"
class="btn btn-sm variant-ringed-error hover:variant-filled-error" class="btn btn-sm variant-ringed-error hover:variant-filled-error"
on:click={removeRoute} on:click={() => {
deletePath(modalStore, pathName);
}}
> >
Remove Remove
</button> </button>

View File

@@ -1,40 +1,44 @@
<script lang="ts"> <script lang="ts">
import { addPath } from '$lib';
import { openApiStore, pathRegex, sortPathsAlphabetically } from '$lib'; import { openApiStore, pathRegex, sortPathsAlphabetically } from '$lib';
import { pathTemplate } from '$lib/pathTemplate'; import { pathTemplate } from '$lib/pathTemplate';
import PathListItem from '../atoms/PathListItem.svelte'; import PathListItem from '../atoms/PathListItem.svelte';
import { getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
// match path with parameters // match path with parameters
// add path // add path
const addPath = () => { // const addPath = () => {
// prompt user to enter path // // prompt user to enter path
const path = prompt( // const path = prompt(
'Enter path. Wrap path parameters in curly braces. Example: /users/{userId}' // 'Enter path. Wrap path parameters in curly braces. Example: /users/{userId}'
); // );
if (!path) return; // if (!path) return;
// check if path is valid // // check if path is valid
if (!pathRegex.test(path)) { // if (!pathRegex.test(path)) {
alert('Invalid path'); // alert('Invalid path');
return; // return;
} // }
// check if path already exists // // check if path already exists
// @ts-expect-error - we are working with an initially empty object // // @ts-expect-error - we are working with an initially empty object
if ($openApiStore.paths[path]) { // if ($openApiStore.paths[path]) {
alert('Path already exists'); // alert('Path already exists');
return; // return;
} // }
// create a temporary object to store paths // // create a temporary object to store paths
// add path to paths object // // add path to paths object
$openApiStore.paths = { // $openApiStore.paths = {
...$openApiStore.paths, // ...$openApiStore.paths,
[path]: pathTemplate // [path]: pathTemplate
}; // };
// sort paths alphabetically // // sort paths alphabetically
sortPathsAlphabetically(); // sortPathsAlphabetically();
}; // };
</script> </script>
<div <div
@@ -44,7 +48,13 @@
<PathListItem {pathName} id={index} /> <PathListItem {pathName} id={index} />
{/each} {/each}
<span class="w-full flex justify-center"> <span class="w-full flex justify-center">
<button type="button" class="btn variant-filled-primary" on:click={addPath}>Add Path</button> <button
type="button"
class="btn variant-filled-primary"
on:click={() => {
addPath(modalStore);
}}>Add Path</button
>
</span> </span>
<span class="w-full flex justify-center"> <span class="w-full flex justify-center">
<button <button

View File

@@ -1,6 +1,5 @@
import { persisted } from 'svelte-persisted-store'; import { persisted } from 'svelte-persisted-store';
import type { OpenAPIV3_1 } from './openAPITypes'; import type { OpenAPIV3_1 } from './openAPITypes';
import { get } from 'svelte/store';
export const localStoragePrefix = 'openapigen-'; export const localStoragePrefix = 'openapigen-';
@@ -40,22 +39,5 @@ export const openApiStore = persisted<OpenAPIV3_1.Document>(`${localStoragePrefi
} }
}); });
export const pathRegex = /\/([/]*[{]?[a-zA-Z]+[}]?)*/gm; // export path handling functions
export * from './pathHandling';
export const sortPathsAlphabetically = () => {
const tempPathObject = {};
const store = get(openApiStore);
// @ts-expect-error - we are working with an initially empty object
Object.keys(store.paths)
.sort()
.forEach((key) => {
// @ts-expect-error - we are working with initially empty objects
tempPathObject[key] = store.paths[key];
});
// update path object
openApiStore.update((data) => {
data.paths = tempPathObject;
return data;
});
};

163
src/lib/pathHandling.ts Normal file
View File

@@ -0,0 +1,163 @@
import { get } from 'svelte/store';
import type { ModalSettings, ModalStore } from '@skeletonlabs/skeleton';
import { pathTemplate } from './pathTemplate';
import { openApiStore } from '$lib';
export const pathRegex = /\/([/]*[{]?[a-zA-Z]+[}]?)*/gm;
/// Adds a new path to the OpenAPI document
export const addPath = (modalStore: ModalStore, startingPoint: string = '/') => {
let userPath: string = startingPoint.trim();
const modal: ModalSettings = {
type: 'prompt',
// Data
title: 'Enter Pathname',
body: 'Wrap path parameters in curly braces. Example: /users/{userId}',
// Populates the input value and attributes
value: userPath,
valueAttr: { type: 'text', minlength: 1, required: true },
// Returns the updated response value
response: (r: string | boolean | undefined) => {
if (r === false) return;
if (typeof r !== 'string') return;
userPath = r;
// Check if path already exists
if (!pathExists(userPath)) {
alert('Path already exists');
return;
}
// Check if path is valid
if (!isValidPath(userPath)) {
alert('Invalid path');
return;
}
// create path object
const store = get(openApiStore);
if (!store.paths) store.paths = {};
store.paths[userPath] = pathTemplate;
// add path to store
openApiStore.set(store);
// sort paths alphabetically
sortPathsAlphabetically();
}
};
modalStore.trigger(modal);
};
export const renamePath = (modalStore: ModalStore, oldPath: string) => {
let userPath: string = oldPath;
const modal: ModalSettings = {
type: 'prompt',
// Data
title: 'Rename path',
body: 'Wrap path parameters in curly braces. Example: /users/{userId}',
// Populates the input value and attributes
value: userPath,
valueAttr: { type: 'text', minlength: 1, required: true },
// Returns the updated response value
response: (r: string | boolean | undefined) => {
if (r === false) return;
if (typeof r !== 'string') return;
userPath = r;
// Check if path already exists
if (!pathExists(userPath)) {
alert('Path already exists');
return;
}
// Check if path is valid
if (!isValidPath(userPath)) {
alert('Invalid path');
return;
}
// create path object
const store = get(openApiStore);
if (!store.paths) store.paths = {};
// copy old path object to new path object
store.paths[userPath] = store.paths[oldPath];
// delete old path object
delete store.paths[oldPath];
// add path to store
openApiStore.set(store);
// sort paths alphabetically
sortPathsAlphabetically();
}
};
modalStore.trigger(modal);
};
/// Removes a path from the OpenAPI document
export const deletePath = (modalStore: ModalStore, path: string) => {
const modal: ModalSettings = {
type: 'confirm',
// Data
title: `Delete ${path}?`,
body: 'Are you sure you wish to delete the path?',
// TRUE if confirm pressed, FALSE if cancel pressed
response: (r: boolean) => {
if (r === false) return;
const store = get(openApiStore);
// check if path exists
if (!store.paths) return;
if (!(path in store.paths)) return;
delete store.paths[path];
openApiStore.set(store);
}
};
modalStore.trigger(modal);
};
/// checks if a given path already exists
export const pathExists = (path: string) => {
const store = get(openApiStore);
return !(path in store.paths!);
};
/// checks if a given path is valid
export const isValidPath = (path: string) => {
// check if path variables aren't duplicated
const variableRegex = /\{([^}]+)\}/gm;
const variables = path.match(variableRegex);
if (variables) {
const uniqueVariables = new Set(variables);
if (variables.length !== uniqueVariables.size) return false;
}
// check if path is a valid path
const pathWithoutVariables = path.replace('{', '').replace('}', '');
const pathRegex = /(\/[a-zA-Z-]+)+|\//gm;
const pathParts = pathWithoutVariables.match(pathRegex);
// the fallback is to return false if the pathParts array is null
return pathParts?.length === 1 ?? false;
};
/// sorts the paths in the OpenAPI document alphabetically
export const sortPathsAlphabetically = () => {
const tempPathObject = {};
const store = get(openApiStore);
// @ts-expect-error - we are working with an initially empty object
Object.keys(store.paths)
.sort()
.forEach((key) => {
// @ts-expect-error - we are working with initially empty objects
tempPathObject[key] = store.paths[key];
});
// update path object
openApiStore.update((data) => {
data.paths = tempPathObject;
return data;
});
};

View File

@@ -7,8 +7,14 @@
import { storePopup } from '@skeletonlabs/skeleton'; import { storePopup } from '@skeletonlabs/skeleton';
import { localStoragePrefix } from '$lib'; import { localStoragePrefix } from '$lib';
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow }); storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
// Modal
import { initializeStores, Modal } from '@skeletonlabs/skeleton';
initializeStores();
</script> </script>
<Modal />
<div class="w-full h-full"> <div class="w-full h-full">
<AppBar> <AppBar>
<svelte:fragment slot="lead"> <svelte:fragment slot="lead">