├── .changeset ├── README.md └── config.json ├── .env ├── .eslintrc.cjs ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── README.md ├── index.html ├── lib ├── README.md ├── components │ ├── AletDialog.tsx │ ├── CodeEditor.tsx │ ├── Dialog.tsx │ ├── EmptyState.tsx │ ├── ToolTip.tsx │ └── index.ts ├── editor │ ├── AsideMenu.tsx │ ├── CodePreview.tsx │ ├── EndpointEdge.tsx │ ├── EndpointNode.tsx │ ├── FlowArena.tsx │ ├── FlowsList.tsx │ ├── MethodRenderer.tsx │ ├── PathButton.tsx │ └── consts.ts ├── index.css ├── index.ts ├── stores │ ├── Config.ts │ ├── Controller.tsx │ └── ModeProvider.tsx ├── types │ ├── Flow.ts │ └── Swagger.ts └── utils │ ├── cn.ts │ ├── create-safe-context.tsx │ ├── genId.ts │ ├── getDef.ts │ ├── helpers.ts │ ├── index.ts │ ├── transformEndpointNodes.ts │ ├── transformSwagger.ts │ └── updateNodePosition.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── logo-opencopilot.png ├── src ├── App.tsx ├── main.tsx └── vite-env.d.ts ├── styles └── index.css ├── tailwind.config.ts ├── test ├── README.md ├── __snapshots__ │ └── transformPaths.test.ts.snap ├── genId.test.ts ├── public │ ├── swagger-identity.json │ ├── swagger-metering-labels.json │ ├── swagger-os-flavor-access.json │ └── swagger-pet-store.json └── transformPaths.test.ts ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json ├── vite.config.lib.ts ├── vite.config.ts └── vitest.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": true, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SENTRY_AUTH_TOKEN=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 2 | SENTRY_DSN=https://f38dd7abb498fc03ce47acb1ea3003f6@o4505912426954752.ingest.sentry.io/4505912429117440 -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs", "node_modules"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | "react-hooks/exhaustive-deps": "warn", 18 | "@typescript-eslint/no-unused-vars": 0, 19 | "@typescript-eslint/no-explicit-any": 0, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v3 20 | 21 | - name: Install pnpm 22 | run: npm install -g pnpm 23 | 24 | - name: Install Dependencies 25 | run: pnpm install 26 | 27 | - name: Create Release Pull Request or Publish to npm 28 | id: changesets 29 | uses: changesets/action@v1 30 | with: 31 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 32 | publish: npm publish --access public 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore development files 2 | node_modules/ 3 | src/ 4 | lib/ 5 | *.ts 6 | *.tsx 7 | *.js 8 | *.jsx 9 | *.map 10 | vercel.json 11 | tailwind.config.ts 12 | vite.config.ts 13 | tsconfig.json 14 | tsconfig.build.json 15 | # Ignore build output 16 | dist/ 17 | build/ 18 | lib/ 19 | .eslintcache -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.17.0 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @openchatai/copilot-flows-editor 2 | 3 | ## 1.6.0 4 | 5 | ### Minor Changes 6 | 7 | - 5bd5285: added `getData` function to the controller in order to get standard opencopilot flow definition 8 | 9 | ## 1.5.2 10 | 11 | ### Patch Changes 12 | 13 | - 66a5e9b: FIXED: `maxFlows` bug. 14 | 15 | ## 1.5.1 16 | 17 | ### Patch Changes 18 | 19 | - 391eab8: bump version 20 | 21 | ## 1.5.0 22 | 23 | ### Minor Changes 24 | 25 | - 2a9674e: - Now we can limit the flows created by setting `maxFlows` prop. 26 | - auto externalize the peer deps in lib mode (ci-change). 27 | - Now we can delete flows (ui-change). 28 | - edited `./package.json` deps and peer deps. 29 | - we no longer load reset styles in lib mode (assuming that we use tailwindcss in the dashboard). 30 | 31 | ## 1.4.0 32 | 33 | ### Minor Changes 34 | 35 | - 5c82878: no need for tailwind reset in lib mode, some code formatting. 36 | 37 | ## 1.3.6 38 | 39 | ### Patch Changes 40 | 41 | - 66397d0: export `transformPaths`. 42 | 43 | ## 1.3.5 44 | 45 | ### Patch Changes 46 | 47 | - 9bc30eb: set draggable to false to nodes, to avoid shifting when dragged. 48 | 49 | ## 1.3.4 50 | 51 | ### Patch Changes 52 | 53 | - e806091: making some of path keys optional 54 | 55 | ## 1.3.3 56 | 57 | ### Patch Changes 58 | 59 | - 2beec39: export some useful utils 60 | 61 | ## 1.3.2 62 | 63 | ### Patch Changes 64 | 65 | - 424a35d: trial 1 66 | 67 | ## 1.3.1 68 | 69 | ### Patch Changes 70 | 71 | - c6987da: fixed u is undefinded 72 | 73 | ## 1.3.0 74 | 75 | ### Minor Changes 76 | 77 | - ac72030: we can pass initialState to the controller. 78 | 79 | ## 1.2.0 80 | 81 | ### Minor Changes 82 | 83 | - aed06ee: standalone and non standalone mode, for more control over the editor 84 | 85 | ## 1.1.3 86 | 87 | ### Patch Changes 88 | 89 | - f21312b: styling issues with the Api Nodes styles. 90 | 91 | ## 1.1.2 92 | 93 | ### Patch Changes 94 | 95 | - be13f38: fixes 96 | 97 | ## 1.1.1 98 | 99 | ### Patch Changes 100 | 101 | - 775b294: some style customizations 102 | 103 | ## 1.1.0 104 | 105 | ### Minor Changes 106 | 107 | - 1aa80aa: updated styles. 108 | 109 | ## 1.0.0 110 | 111 | ### Major Changes 112 | 113 | - 97d72bd: the first stable version, included with tests, and bug fixes. 114 | 115 | ## 0.0.6 116 | 117 | ### Patch Changes 118 | 119 | - c8aa0c2: fixed:styles issue 120 | 121 | ## 0.0.5 122 | 123 | ### Patch Changes 124 | 125 | - edeaf86: reactflow provider issues 126 | 127 | ## 0.0.4 128 | 129 | ### Patch Changes 130 | 131 | - 7011621: same issue 132 | 133 | ## 0.0.3 134 | 135 | ### Patch Changes 136 | 137 | - dc5f18c: updatd exported types from package.json 138 | 139 | ## 0.0.2 140 | 141 | ### Patch Changes 142 | 143 | - 7e2b904: changed typo in `./package.json` 144 | 145 | ## 0.0.1 146 | 147 | ### Patch Changes 148 | 149 | - 1e5c65b: added styles.css 150 | 151 | ## 0.0.0 152 | 153 | ### Minor Changes 154 | 155 | - 2973638: the first minor releas, to test the ci 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flows Editor 2 | 3 | 4 | By default, OpenCopilot attempts to map the user's request to an endpoint automatically, and this works well for most simple use cases. However, if your backend contains flows that are not straightforward or intuitive, you may need to define these flows using OpenCopilot's definitions. 5 | 6 | ## What is a Flow? 7 | 8 | A flow consists of a series of steps, where each step represents an API call to your backend. Each flow has a name and description to guide the copilot in determining when to invoke it based on the user's request. 9 | 10 | For example, imagine that your system includes an "add to cart" functionality, which involves calling multiple endpoints. You may want your copilot to handle user requests that require this functionality. To achieve this, you need to define a flow using OpenCopilot's flow definitions. 11 | 12 | ![example-flow](https://github.com/openchatai/editor/assets/32633162/5d8c0ddf-dbda-4c03-97d0-94d0aaf9128d) 13 | 14 | **In this example:** 15 | 16 | 1. The user requested the copilot to add 3 sunglasses to the cart. 17 | 18 | 2. OpenCopilot automatically mapped the user request to the pre-defined "create cart" flow, passing the necessary parameters based on the user request. 19 | 20 | 3. After making all the API calls, OpenCopilot responds to the user based on the results of the flow. 21 | 22 | ## How to Define Your OpenCopilot Flows File 23 | 24 | First, ensure that your copilot has a valid Swagger file. Typically, when you create a new copilot, you will need to upload a valid Swagger file. 25 | 26 | We have developed a [simple tool](https://editor.opencopilot.so) to assist you in writing your OpenCopilot flow definitions. In the following example, we have created a flow for cart creation. Take a look at it. 27 | 28 | 29 | ![flows-file-example](https://github.com/openchatai/editor/assets/32633162/90b0c6a0-80eb-4a8d-b7a7-6f122e4bba63) 30 | 31 | 32 | 33 | Notice that each step have `open_api_operation_id`, which refer to your swagger file, from there the copilot knows exactly what is the endpoint details, params, base url, etc.. 34 | 35 | We have prepared a set of endpoints to help you create these flows. 36 | 37 | ## Flows Best Practices 38 | 39 | Flows are still in beta, and there are some areas where they currently lack functionality. To make the most of flows in OpenCopilot, ensure that your flow definition files always align with your Swagger file in terms of `open_api_operation_id`. Additionally, try to minimize the response data from each step (only pass the important fields between steps, not the entire API response). 40 | 41 | If you have a specific use case and need support from our team, we would love to assist you. Please join our Discord server or schedule a call with us. 42 | 43 | ## Suggestions and Questions ❤️ 44 | 45 | OpenCopilot flow definition is new and still in beta. We highly appreciate your suggestions. You can join our [Discord server](https://discord.gg/yjEgCgvefr), [hey@openchat.so](mailto:hey@openchat.so)email us, or [book a call](https://calendly.com/hey-pk0) with us to share your feedback. 46 | 47 | 48 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenCopilot - Flows editor 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # copilot-flows-editor ( the library ) 2 | 3 | ## Installation 4 | 5 | ```bash 6 | pnpm add @openchatai/copilot-flows-editor 7 | ``` 8 | 9 | ## Usage 10 | 11 | ````tsx 12 | import { Controller, FlowArena,CodePreview } from "@openchatai/copilot-flows-editor"; // import the components 13 | import "@openchatai/copilot-flows-editor/dist/style.css"; // import the styles 14 | 15 | function FlowsEditorDemo() { 16 | return ( 17 | // wrap the components in the controller 18 |
19 |
20 | // add the flow arena (the aside menu and the canvas) 21 | // preview the flow code (based on our flow schema) 22 |
23 |
24 |
25 | ); 26 | } 27 | 28 | ``` 29 | ```` 30 | -------------------------------------------------------------------------------- /lib/components/AletDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | import cn from "../utils/cn"; 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root; 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 10 | 11 | const AlertDialogPortal = ({ 12 | className, 13 | ...props 14 | }: AlertDialogPrimitive.AlertDialogPortalProps) => ( 15 | 16 | ); 17 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName; 18 | 19 | const AlertDialogOverlay = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 31 | )); 32 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 33 | 34 | const AlertDialogContent = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 39 | 40 | 48 | 49 | 50 | )); 51 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 52 | 53 | const AlertDialogHeader = ({ 54 | className, 55 | ...props 56 | }: React.HTMLAttributes) => ( 57 |
64 | ); 65 | AlertDialogHeader.displayName = "AlertDialogHeader"; 66 | 67 | const AlertDialogFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 |
78 | ); 79 | AlertDialogFooter.displayName = "AlertDialogFooter"; 80 | 81 | const AlertDialogTitle = React.forwardRef< 82 | React.ElementRef, 83 | React.ComponentPropsWithoutRef 84 | >(({ className, ...props }, ref) => ( 85 | 90 | )); 91 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 92 | 93 | const AlertDialogDescription = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )); 103 | AlertDialogDescription.displayName = 104 | AlertDialogPrimitive.Description.displayName; 105 | 106 | const AlertDialogAction = React.forwardRef< 107 | React.ElementRef, 108 | React.ComponentPropsWithoutRef 109 | >(({ ...props }, ref) => ); 110 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 111 | 112 | const AlertDialogCancel = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, ...props }, ref) => ( 116 | 121 | )); 122 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 123 | 124 | export { 125 | AlertDialog, 126 | AlertDialogTrigger, 127 | AlertDialogContent, 128 | AlertDialogHeader, 129 | AlertDialogFooter, 130 | AlertDialogTitle, 131 | AlertDialogDescription, 132 | AlertDialogAction, 133 | AlertDialogCancel, 134 | }; 135 | -------------------------------------------------------------------------------- /lib/components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import CodeMirror from "@uiw/react-codemirror"; 2 | import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; 3 | import { EditorView, ViewUpdate } from "@codemirror/view"; 4 | import { json as jsonLang, jsonParseLinter } from "@codemirror/lang-json"; 5 | import { basicLight } from "@uiw/codemirror-themes-all"; 6 | import { linter } from "@codemirror/lint"; 7 | 8 | export function CodeEditor({ 9 | initialValue, 10 | onChange, 11 | }: { 12 | initialValue?: string; 13 | onChange?: (value: string, viewUpdate: ViewUpdate) => void; 14 | }) { 15 | return ( 16 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /lib/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import cn from "../utils/cn"; 6 | 7 | const Dialog = DialogPrimitive.Root; 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger; 10 | 11 | const DialogPortal = ({ 12 | className, 13 | ...props 14 | }: DialogPrimitive.DialogPortalProps) => ( 15 | 16 | ); 17 | DialogPortal.displayName = DialogPrimitive.Portal.displayName; 18 | 19 | const DialogOverlay = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 31 | )); 32 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 33 | 34 | const DialogContent = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, children, ...props }, ref) => ( 38 | 39 | 40 | 48 | {children} 49 | 50 | 51 | 52 | )); 53 | DialogContent.displayName = DialogPrimitive.Content.displayName; 54 | 55 | const DialogHeader = ({ 56 | className, 57 | ...props 58 | }: React.HTMLAttributes) => ( 59 |
66 | ); 67 | DialogHeader.displayName = "DialogHeader"; 68 | 69 | const DialogFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
80 | ); 81 | DialogFooter.displayName = "DialogFooter"; 82 | 83 | const DialogTitle = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )); 96 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 97 | 98 | const DialogDescription = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, ...props }, ref) => ( 102 | 107 | )); 108 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 109 | 110 | export { 111 | Dialog, 112 | DialogTrigger, 113 | DialogContent, 114 | DialogHeader, 115 | DialogFooter, 116 | DialogTitle, 117 | DialogDescription, 118 | }; 119 | -------------------------------------------------------------------------------- /lib/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function EmptyState({ children }: { children?: React.ReactNode }) { 4 | return ( 5 |
6 | ¯\_(ツ)_/¯ 7 | {children} 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /lib/components/ToolTip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | import cn from "../utils/cn"; 6 | 7 | const TooltipProvider = TooltipPrimitive.Provider; 8 | 9 | const Tooltip = TooltipPrimitive.Root; 10 | 11 | const TooltipTrigger = TooltipPrimitive.Trigger; 12 | 13 | const TooltipContent = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, sideOffset = 5, ...props }, ref) => ( 17 | 26 | )); 27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 28 | 29 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 30 | -------------------------------------------------------------------------------- /lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from './AletDialog' 2 | export { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from './Dialog' 3 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ToolTip' 4 | export { CodeEditor } from './CodeEditor' 5 | export { EmptyState } from './EmptyState' -------------------------------------------------------------------------------- /lib/editor/AsideMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useMode } from "../stores/ModeProvider"; 2 | import { PlusIcon } from "@radix-ui/react-icons"; 3 | import { PathButton } from "./PathButton"; 4 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; 5 | import { useEffect, useMemo, useState } from "react"; 6 | import { MethodBtn } from "./MethodRenderer"; 7 | import { FlowsList } from "./FlowsList"; 8 | import { useController } from "../stores/Controller"; 9 | import { EmptyState } from "../components"; 10 | import { cn, parse, transformPaths } from "../utils"; 11 | import { useSettings } from "../stores/Config"; 12 | 13 | function UploadSwagger() { 14 | const [file, setFile] = useState(null); 15 | const { loadPaths } = useController(); 16 | useEffect(() => { 17 | if (file && file.length > 0) { 18 | const $file = file.item(0); 19 | if ($file) { 20 | const reader = new FileReader(); 21 | reader.readAsText($file); 22 | reader.onload = (e) => { 23 | const text = e.target?.result; 24 | if (typeof text === "string") { 25 | const json = parse(text); 26 | loadPaths(transformPaths(json.paths)); 27 | console.log("paths loaded"); 28 | } 29 | }; 30 | } 31 | } 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, [file]); 34 | return ( 35 | 36 | setFile(ev.target.files)} 43 | /> 44 |
45 | 53 |
54 |
55 | ); 56 | } 57 | 58 | export function AsideMenu() { 59 | const { standalone } = useSettings(); 60 | const { 61 | state: { paths }, 62 | } = useController(); 63 | 64 | const { mode, isEdit } = useMode(); 65 | 66 | const [search, setSearch] = useState(""); 67 | 68 | const renderedPaths = useMemo( 69 | () => 70 | search.trim().length > 0 71 | ? paths.filter((path) => path.path.includes(search.trim())) 72 | : paths, 73 | [search, paths] 74 | ); 75 | 76 | return ( 77 | 155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /lib/editor/CodePreview.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { 3 | ChevronDownIcon, 4 | ChevronRightIcon, 5 | CodeIcon, 6 | } from "@radix-ui/react-icons"; 7 | import Ajv from "ajv"; 8 | import { js } from "js-beautify"; 9 | import cn from "../utils/cn"; 10 | import { useController } from "../stores/Controller"; 11 | import { CodeEditor } from "../components"; 12 | 13 | const ajv = new Ajv({ 14 | allErrors: true, 15 | verbose: true, 16 | strict: true, 17 | }); 18 | 19 | const validate = ajv.compile({ 20 | $schema: "http://json-schema.org/draft-07/schema#", 21 | type: "object", 22 | properties: { 23 | opencopilot: { 24 | type: "string", 25 | pattern: "^\\d+\\.\\d+$", 26 | }, 27 | info: { 28 | type: "object", 29 | properties: { 30 | title: { 31 | type: "string", 32 | }, 33 | version: { 34 | type: "string", 35 | }, 36 | }, 37 | required: ["title", "version"], 38 | }, 39 | flows: { 40 | type: "array", 41 | items: { 42 | type: "object", 43 | properties: { 44 | name: { 45 | type: "string", 46 | }, 47 | description: { 48 | type: "string", 49 | }, 50 | requires_confirmation: { 51 | type: "boolean", 52 | }, 53 | steps: { 54 | type: "array", 55 | items: { 56 | type: "object", 57 | properties: { 58 | stepId: { 59 | type: "string", 60 | }, 61 | operation: { 62 | type: "string", 63 | }, 64 | open_api_operation_id: { 65 | type: "string", 66 | }, 67 | parameters: { 68 | type: "object", 69 | }, 70 | }, 71 | required: ["operation", "open_api_operation_id"], 72 | }, 73 | }, 74 | on_success: { 75 | type: "array", 76 | items: { 77 | type: "object", 78 | properties: { 79 | handler: { 80 | type: "string", 81 | }, 82 | }, 83 | }, 84 | }, 85 | on_failure: { 86 | type: "array", 87 | items: { 88 | type: "object", 89 | properties: { 90 | handler: { 91 | type: "string", 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | required: [ 98 | "name", 99 | "description", 100 | "requires_confirmation", 101 | "steps", 102 | // "on_success", 103 | // "on_failure", 104 | ], 105 | }, 106 | }, 107 | }, 108 | required: ["opencopilot", "info", "flows"], 109 | }); 110 | 111 | export function CodePreview() { 112 | // this will preview the whole code for the flows. 113 | const { 114 | state: { flows }, 115 | getData, 116 | } = useController(); 117 | const [code, $setCode] = useState("{}"); 118 | const [codeExpanded, setCodeExpanded] = useState(false); 119 | const setCode = useCallback(() => { 120 | const $code = getData(); 121 | const $codeString = js(JSON.stringify($code), { 122 | indent_size: 1, 123 | }); 124 | $setCode($codeString); 125 | validate($code); 126 | }, [getData]); 127 | 128 | const [barOpen, setBarOpen] = useState(false); 129 | return ( 130 |
136 |
137 | 148 | 160 |
161 | 162 |
168 |
169 | 187 | 188 |
189 | {validate.errors ? ( 190 |
    191 | {validate.errors?.map((error) => { 192 | return ( 193 |
  • 194 | 195 | {error.instancePath} 196 | 197 |
    {error.message}
    198 |
  • 199 | ); 200 | })} 201 |
202 | ) : ( 203 |
204 | Your OpenCopilot flows definition is valid 205 |
206 | )} 207 |
208 |
209 |
210 | 211 |
212 | ); 213 | } 214 | -------------------------------------------------------------------------------- /lib/editor/EndpointEdge.tsx: -------------------------------------------------------------------------------- 1 | import { PlusIcon } from "@radix-ui/react-icons"; 2 | import { 3 | BaseEdge, 4 | EdgeLabelRenderer, 5 | EdgeProps, 6 | getStraightPath, 7 | } from "reactflow"; 8 | import { useMode } from "../stores/ModeProvider"; 9 | import { useMemo } from "react"; 10 | import { cn } from "../utils"; 11 | 12 | export function NodeEdge({ 13 | sourceX, 14 | sourceY, 15 | targetX, 16 | targetY, 17 | ...props 18 | }: EdgeProps) { 19 | const [edgePath, labelX, labelY] = getStraightPath({ 20 | sourceX, 21 | sourceY, 22 | targetX, 23 | targetY, 24 | }); 25 | const { mode } = useMode(); 26 | const activeEdge = useMemo(() => { 27 | if (mode.type === "add-node-between") return mode.edge; 28 | }, [mode]); 29 | return ( 30 | <> 31 | 32 |
40 | 49 |
50 |
51 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /lib/editor/EndpointNode.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Handle, 3 | NodeProps, 4 | Position, 5 | useNodeId, 6 | useNodes, 7 | NodeToolbar, 8 | useReactFlow, 9 | } from "reactflow"; 10 | import { useMode } from "../stores/ModeProvider"; 11 | import { memo, useCallback, useMemo } from "react"; 12 | import { Y, nodedimensions } from "./consts"; 13 | import { PlusIcon, TrashIcon } from "@radix-ui/react-icons"; 14 | import { MethodBtn } from "./MethodRenderer"; 15 | import { NodeData } from "../types/Swagger"; 16 | import { 17 | AlertDialog, 18 | AlertDialogAction, 19 | AlertDialogCancel, 20 | AlertDialogContent, 21 | AlertDialogFooter, 22 | AlertDialogHeader, 23 | AlertDialogTrigger, 24 | } from "../components"; 25 | import { cn, updateNodesPositions } from "../utils"; 26 | 27 | const HideHandleStyles = { 28 | background: "transparent", 29 | fill: "transparent", 30 | color: "transparent", 31 | border: "none", 32 | }; 33 | function EndpointNode({ data, zIndex }: NodeProps) { 34 | const nodes = useNodes(); 35 | const { setNodes } = useReactFlow(); 36 | const nodeId = useNodeId(); 37 | const nodeObj = nodes.find((n) => n.id === nodeId); 38 | const { mode, setMode, reset: resetMode } = useMode(); 39 | 40 | const isActive = useMemo(() => { 41 | if (mode.type === "edit-node") { 42 | return mode.node.id === nodeId; 43 | } else { 44 | return false; 45 | } 46 | // eslint-disable-next-line react-hooks/exhaustive-deps 47 | }, [mode]); 48 | 49 | const isFirstNode = nodes?.[0]?.id === nodeId; 50 | const isLastNode = nodes?.[nodes.length - 1]?.id === nodeId; 51 | const deleteNode = useCallback(() => { 52 | setTimeout(() => { 53 | setNodes( 54 | updateNodesPositions( 55 | nodes.filter((nd) => nd.id !== nodeId), 56 | Y 57 | ) 58 | ); 59 | resetMode(); 60 | }, 300); 61 | // eslint-disable-next-line react-hooks/exhaustive-deps 62 | }, []); 63 | return ( 64 | <> 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | Are you sure you want to delete this node? 75 | 76 | 77 | 81 | Yup! 82 | 83 | 84 | Nope! 85 | 86 | 87 | 88 | 89 | 90 | {!isFirstNode && ( 91 | 97 | )} 98 |
105 |
{ 107 | nodeObj && setMode({ type: "edit-node", node: nodeObj }); 108 | }} 109 | className={cn( 110 | "bg-white border group duration-300 ease-in-out rounded select-none cursor-pointer transition-all h-full w-full", 111 | isActive 112 | ? "border-indigo-500 [box-shadow:inset_0px_2.5px_0px_0px_theme(colors.indigo.500)]" 113 | : "border-slate-200 hover:shadow" 114 | )} 115 | > 116 |
117 | {data.path} 118 |

119 | {data.description} 120 |

121 | 125 | {data.method} 126 | 127 |
128 |
129 | {isLastNode && ( 130 |
138 | 148 |
149 | )} 150 |
151 | 152 | 158 | 159 | ); 160 | } 161 | 162 | const MemoizedEndpointNode = memo(EndpointNode); 163 | export default MemoizedEndpointNode; 164 | -------------------------------------------------------------------------------- /lib/editor/FlowArena.tsx: -------------------------------------------------------------------------------- 1 | import ReactFlow, { 2 | Background, 3 | OnConnect, 4 | addEdge, 5 | useEdgesState, 6 | MarkerType, 7 | Edge, 8 | applyNodeChanges, 9 | NodeChange, 10 | useReactFlow, 11 | } from "reactflow"; 12 | import { useCallback, useEffect, useMemo } from "react"; 13 | import "reactflow/dist/style.css"; 14 | import { NodeEdge } from "./EndpointEdge"; 15 | import EndpointNode from "./EndpointNode"; 16 | import { AsideMenu } from "./AsideMenu"; 17 | import { useMode } from "../stores/ModeProvider"; 18 | import { BUILDER_SCALE } from "./consts"; 19 | import { useController } from "../stores/Controller"; 20 | 21 | export function FlowArena() { 22 | const nodeTypes = useMemo( 23 | () => ({ 24 | endpointNode: EndpointNode, 25 | }), 26 | [] 27 | ); 28 | const edgeTypes = useMemo( 29 | () => ({ 30 | endpointEdge: NodeEdge, 31 | }), 32 | [] 33 | ); 34 | const { fitView } = useReactFlow(); 35 | const [edges, setEdges, onEdgesChange] = useEdgesState([]); 36 | const { 37 | activeNodes, 38 | setNodes, 39 | state: { activeFlowId }, 40 | } = useController(); 41 | const { setMode } = useMode(); 42 | // auto connect nodes 43 | useEffect(() => { 44 | if (!activeNodes) return; 45 | if (activeNodes.length === 0) { 46 | setMode({ type: "append-node" }); 47 | return; 48 | } 49 | fitView(); 50 | const newEdges = activeNodes 51 | .map((v, i, a) => { 52 | const curr = v; 53 | const next = a.at(i + 1); 54 | if (curr && next) { 55 | const id = curr.id + "-" + next.id; 56 | return { 57 | id: id, 58 | target: curr.id, 59 | source: next.id, 60 | type: "endpointEdge", 61 | markerStart: { 62 | type: MarkerType.ArrowClosed, 63 | }, 64 | }; 65 | } 66 | }) 67 | .filter((v) => typeof v !== "undefined") as Edge[]; 68 | setEdges(newEdges); 69 | // eslint-disable-next-line react-hooks/exhaustive-deps 70 | }, [activeNodes]); 71 | 72 | const onConnect: OnConnect = useCallback( 73 | (connection) => setEdges((eds) => addEdge(connection, eds)), 74 | [setEdges] 75 | ); 76 | const onNodesChange = useCallback( 77 | (changes: NodeChange[]) => { 78 | const _ = applyNodeChanges(changes, activeNodes || []); 79 | console.log("nodes changed", _); 80 | setNodes(_); 81 | }, 82 | [setNodes, activeNodes] 83 | ); 84 | const empty = useMemo(() => { 85 | return activeNodes?.length === 0 || activeFlowId === undefined; 86 | }, [activeNodes, activeFlowId]); 87 | return ( 88 | <> 89 |
90 | 91 |
92 | {empty && ( 93 |
97 |
98 |
99 | {activeFlowId 100 | ? "Start by selecting an endpoint from the menu" 101 | : "Start by creating a flow"} 102 |
103 |
104 |
105 | )} 106 | { 111 | event.stopPropagation(); 112 | event.bubbles = false; 113 | setMode({ 114 | type: "add-node-between", 115 | edge: edge, 116 | }); 117 | }} 118 | className="transition-all duration-300 origin-center w-full h-full" 119 | edgeTypes={edgeTypes} 120 | maxZoom={BUILDER_SCALE} 121 | minZoom={BUILDER_SCALE} 122 | onNodesChange={onNodesChange} 123 | onEdgesChange={onEdgesChange} 124 | onConnect={onConnect} 125 | deleteKeyCode={[]} 126 | fitView 127 | > 128 | 129 | 130 |
131 |
132 | 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /lib/editor/FlowsList.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { 3 | ChevronRightIcon, 4 | CubeIcon, 5 | Pencil1Icon, 6 | PlusIcon, 7 | TrashIcon, 8 | } from "@radix-ui/react-icons"; 9 | import { useController } from "../stores/Controller"; 10 | import { useMode } from "../stores/ModeProvider"; 11 | import { 12 | Dialog, 13 | DialogContent, 14 | DialogHeader, 15 | DialogTrigger, 16 | EmptyState, 17 | } from "../components"; 18 | import { cn } from "../utils"; 19 | import { useSettings } from "../stores/Config"; 20 | 21 | export function FlowsList() { 22 | const [flowsPanelOpened, setFlowsPanel] = useState(false); 23 | const [modalOpen, setModalOpen] = useState(false); 24 | const { reset } = useMode(); 25 | const { maxFlows } = useSettings(); 26 | const { 27 | createFlow, 28 | state: { flows, activeFlowId }, 29 | setActiveFlow, 30 | deleteFlow, 31 | } = useController(); 32 | console.log(flows.length); 33 | function onSubmit(e: React.FormEvent) { 34 | e.preventDefault(); 35 | const data = new FormData(e.currentTarget); 36 | const [name, description, focus] = [ 37 | data.get("name"), 38 | data.get("description"), 39 | data.get("focus"), 40 | ]; 41 | if (name && description) { 42 | createFlow({ 43 | createdAt: Date.now(), 44 | name: name.toString(), 45 | description: description.toString(), 46 | focus: focus === "on" ? true : false, 47 | }); 48 | setModalOpen(false); 49 | } 50 | } 51 | return ( 52 |
53 |
54 | 66 | 67 | 68 | 69 | New Flow 70 | 71 |
72 | 73 | 81 | 82 | 90 |
91 | 98 | 101 |
102 |
103 | 109 |
110 |
111 |
112 | { 115 | if (maxFlows && flows.length >= maxFlows) { 116 | alert(`You can only have ${maxFlows} flows at a time.`); 117 | ev.preventDefault(); 118 | return; 119 | } 120 | }} 121 | > 122 | 125 | 126 |
127 |
128 |
136 | {flows.length === 0 ? ( 137 | 138 | ) : ( 139 |
    140 | {flows?.map((flow, i) => { 141 | const isActive = flow.id === activeFlowId; 142 | return ( 143 |
  • 144 |
    150 |
    151 | 152 | {flow.name} 153 |
    154 |
    155 | 166 | 176 |
    177 |
    178 |
  • 179 | ); 180 | })} 181 |
182 | )} 183 |
184 |
185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /lib/editor/MethodRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react"; 2 | import { Method } from "../types/Swagger"; 3 | import { cn } from "../utils"; 4 | // BG colors for method buttons 5 | const methodStyles = (method: Method) => { 6 | switch (method.toUpperCase()) { 7 | case "GET": 8 | return "bg-green-400"; 9 | case "POST": 10 | return "bg-blue-400"; 11 | case "PUT": 12 | return "bg-yellow-400"; 13 | case "DELETE": 14 | return "bg-red-400"; 15 | default: 16 | return "bg-gray-400"; 17 | } 18 | }; 19 | export const MethodBtn = forwardRef< 20 | ElementRef<"button">, 21 | { method: Method } & ComponentPropsWithoutRef<"button"> 22 | >(({ method, className, ...props }, _ref) => ( 23 | 37 | 41 | 42 | 43 |
44 |
45 | 46 | ); 47 | } 48 | 49 | export default function FlowBuilder() { 50 | return ( 51 | 67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import FlowBuilder from "./App.tsx"; 3 | import "../styles/index.css"; 4 | import "@fontsource-variable/open-sans"; 5 | import * as Sentry from "@sentry/react"; 6 | Sentry.init({ 7 | dsn: import.meta.env.SENTRY_DSN, 8 | integrations: [ 9 | new Sentry.BrowserTracing({ 10 | tracePropagationTargets: ["localhost", "opencopilot.so"], 11 | }), 12 | new Sentry.Replay(), 13 | new Sentry.BrowserProfilingIntegration(), 14 | ], 15 | tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! 16 | replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. 17 | replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. 18 | }); 19 | ReactDOM.createRoot(document.getElementById("root")!).render(); 20 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | svg { 6 | width: 1em; 7 | height: 1em; 8 | aspect-ratio: 1/1; 9 | } 10 | .react-flow__node { 11 | @apply !transition-all !ease-out !duration-300 origin-center; 12 | } 13 | 14 | .btn, 15 | .a { 16 | @apply rounded text-sm border outline-none relative focus:outline-none whitespace-nowrap [&:active:not(:disabled)]:opacity-90 [&:active:not(:disabled)]:scale-[0.98] border-transparent transition duration-150 ease-in-out font-medium inline-flex items-center justify-center leading-5 shadow-sm disabled:cursor-not-allowed disabled:text-slate-400 disabled:bg-slate-100 disabled:border-slate-200 dark:disabled:border-slate-600 data-[loading=true]:text-slate-400 data-[loading=true]:bg-slate-100 data-[loading=true]:border-slate-200 disabled:shadow-none dark:disabled:bg-slate-800 dark:disabled:text-slate-600; 17 | } 18 | .small { 19 | @apply px-2 py-1; 20 | } 21 | .large { 22 | @apply px-4 py-3; 23 | } 24 | .danger { 25 | @apply text-white bg-rose-500 hover:bg-opacity-80 hover:border-opacity-80; 26 | } 27 | .secondary { 28 | @apply hover:border-opacity-80 dark:bg-slate-800 border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 text-indigo-500; 29 | } 30 | /* wrapper */ 31 | .wrapper { 32 | @apply border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 rounded-sm border shadow-lg; 33 | } 34 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./src/**/*.html", 5 | "./src/**/*.tsx", 6 | "./lib/**/*.tsx", 7 | ], 8 | darkMode: 'class', 9 | theme: { 10 | container: { 11 | center: true, 12 | }, 13 | extend: { 14 | fontFamily: { 15 | "system-ui": [ 16 | "-apple-system", 17 | "BlinkMacSystemFont", 18 | "Segoe UI", 19 | "Roboto", 20 | ], 21 | openSans: ['Open Sans Variable', 'sans-serif'] 22 | } 23 | }, 24 | }, 25 | plugins: [ 26 | require("tailwindcss-animate") 27 | ], 28 | } 29 | 30 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | to test the utilities and transformers we use 4 | SOON 5 | -------------------------------------------------------------------------------- /test/genId.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, describe, it } from 'vitest' 2 | import { genId } from '../lib/utils' 3 | import { expect } from 'vitest' 4 | 5 | describe('genId', () => { 6 | it("should generate a random string", () => { 7 | const id = genId() 8 | expect(id).to.be.a('string') 9 | expect(id).to.have.lengthOf.above(0) 10 | }) 11 | 12 | it("should generate a unique string", () => { 13 | const id1 = genId() 14 | const id2 = genId() 15 | assert(id1 !== id2) 16 | }) 17 | 18 | }) -------------------------------------------------------------------------------- /test/public/swagger-identity.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "identity", 5 | "version": "Unknown" 6 | }, 7 | "consumes": [ 8 | "application/json" 9 | ], 10 | "produces": [ 11 | "application/json" 12 | ], 13 | "paths": { 14 | "/": { 15 | "get": { 16 | "operationId": "getVersions-v2", 17 | "summary": "List versions", 18 | "description": "Lists information about all Identity API versions.\n", 19 | "produces": [ 20 | "application/json" 21 | ], 22 | "responses": { 23 | "200": { 24 | "description": "200 response", 25 | "examples": { 26 | "application/json": "{\n \"versions\": {\n \"values\": [\n {\n \"status\": \"stable\",\n \"updated\": \"2013-03-06T00:00:00Z\",\n \"media-types\": [\n {\n \"base\": \"application/json\",\n \"type\": \"application/vnd.openstack.identity-v3+json\"\n },\n {\n \"base\": \"application/xml\",\n \"type\": \"application/vnd.openstack.identity-v3+xml\"\n }\n ],\n \"id\": \"v3.0\",\n \"links\": [\n {\n \"href\": \"http://192.168.122.176:5000/v3/\",\n \"rel\": \"self\"\n }\n ]\n },\n {\n \"status\": \"stable\",\n \"updated\": \"2014-04-17T00:00:00Z\",\n \"media-types\": [\n {\n \"base\": \"application/json\",\n \"type\": \"application/vnd.openstack.identity-v2.0+json\"\n },\n {\n \"base\": \"application/xml\",\n \"type\": \"application/vnd.openstack.identity-v2.0+xml\"\n }\n ],\n \"id\": \"v2.0\",\n \"links\": [\n {\n \"href\": \"http://192.168.122.176:5000/v2.0/\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://docs.openstack.org/\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ]\n }\n ]\n }\n}" 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "/v2.0": { 33 | "get": { 34 | "operationId": "getVersionInfo-v2.0", 35 | "summary": "Show version details", 36 | "description": "Shows details for the Identity API v2.0.\n", 37 | "produces": [ 38 | "application/json" 39 | ], 40 | "responses": { 41 | "200": { 42 | "description": "200 203 response", 43 | "examples": { 44 | "application/json": "{\n \"version\": {\n \"status\": \"stable\",\n \"updated\": \"2013-03-06T00:00:00Z\",\n \"media-types\": [\n {\n \"base\": \"application/json\",\n \"type\": \"application/vnd.openstack.identity-v3+json\"\n },\n {\n \"base\": \"application/xml\",\n \"type\": \"application/vnd.openstack.identity-v3+xml\"\n }\n ],\n \"id\": \"v3.0\",\n \"links\": [\n {\n \"href\": \"http://23.253.228.211:35357/v3/\",\n \"rel\": \"self\"\n }\n ]\n }\n}" 45 | } 46 | }, 47 | "203": { 48 | "description": "200 203 response", 49 | "examples": { 50 | "application/json": "{\n \"version\": {\n \"status\": \"stable\",\n \"updated\": \"2013-03-06T00:00:00Z\",\n \"media-types\": [\n {\n \"base\": \"application/json\",\n \"type\": \"application/vnd.openstack.identity-v3+json\"\n },\n {\n \"base\": \"application/xml\",\n \"type\": \"application/vnd.openstack.identity-v3+xml\"\n }\n ],\n \"id\": \"v3.0\",\n \"links\": [\n {\n \"href\": \"http://23.253.228.211:35357/v3/\",\n \"rel\": \"self\"\n }\n ]\n }\n}" 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | "/v2.0/extensions": { 57 | "get": { 58 | "operationId": "listExtensions-v2.0", 59 | "summary": "List extensions", 60 | "description": "Lists available extensions.\n", 61 | "produces": [ 62 | "application/json" 63 | ], 64 | "responses": { 65 | "200": { 66 | "description": "200 203 response", 67 | "examples": { 68 | "application/json": "{\n \"extensions\": {\n \"values\": [\n {\n \"updated\": \"2013-07-07T12:00:0-00:00\",\n \"name\": \"OpenStack S3 API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/s3tokens/v1.0\",\n \"alias\": \"s3tokens\",\n \"description\": \"OpenStack S3 API.\"\n },\n {\n \"updated\": \"2013-07-23T12:00:0-00:00\",\n \"name\": \"OpenStack Keystone Endpoint Filter API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api/blob/master/openstack-identity-api/v3/src/markdown/identity-api-v3-os-ep-filter-ext.md\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-EP-FILTER/v1.0\",\n \"alias\": \"OS-EP-FILTER\",\n \"description\": \"OpenStack Keystone Endpoint Filter API.\"\n },\n {\n \"updated\": \"2013-12-17T12:00:0-00:00\",\n \"name\": \"OpenStack Federation APIs\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-FEDERATION/v1.0\",\n \"alias\": \"OS-FEDERATION\",\n \"description\": \"OpenStack Identity Providers Mechanism.\"\n },\n {\n \"updated\": \"2013-07-11T17:14:00-00:00\",\n \"name\": \"OpenStack Keystone Admin\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0\",\n \"alias\": \"OS-KSADM\",\n \"description\": \"OpenStack extensions to Keystone v2.0 API enabling Administrative Operations.\"\n },\n {\n \"updated\": \"2014-01-20T12:00:0-00:00\",\n \"name\": \"OpenStack Simple Certificate API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-SIMPLE-CERT/v1.0\",\n \"alias\": \"OS-SIMPLE-CERT\",\n \"description\": \"OpenStack simple certificate retrieval extension\"\n },\n {\n \"updated\": \"2013-07-07T12:00:0-00:00\",\n \"name\": \"OpenStack EC2 API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-EC2/v1.0\",\n \"alias\": \"OS-EC2\",\n \"description\": \"OpenStack EC2 Credentials backend.\"\n }\n ]\n }\n}" 69 | } 70 | }, 71 | "203": { 72 | "description": "200 203 response", 73 | "examples": { 74 | "application/json": "{\n \"extensions\": {\n \"values\": [\n {\n \"updated\": \"2013-07-07T12:00:0-00:00\",\n \"name\": \"OpenStack S3 API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/s3tokens/v1.0\",\n \"alias\": \"s3tokens\",\n \"description\": \"OpenStack S3 API.\"\n },\n {\n \"updated\": \"2013-07-23T12:00:0-00:00\",\n \"name\": \"OpenStack Keystone Endpoint Filter API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api/blob/master/openstack-identity-api/v3/src/markdown/identity-api-v3-os-ep-filter-ext.md\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-EP-FILTER/v1.0\",\n \"alias\": \"OS-EP-FILTER\",\n \"description\": \"OpenStack Keystone Endpoint Filter API.\"\n },\n {\n \"updated\": \"2013-12-17T12:00:0-00:00\",\n \"name\": \"OpenStack Federation APIs\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-FEDERATION/v1.0\",\n \"alias\": \"OS-FEDERATION\",\n \"description\": \"OpenStack Identity Providers Mechanism.\"\n },\n {\n \"updated\": \"2013-07-11T17:14:00-00:00\",\n \"name\": \"OpenStack Keystone Admin\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0\",\n \"alias\": \"OS-KSADM\",\n \"description\": \"OpenStack extensions to Keystone v2.0 API enabling Administrative Operations.\"\n },\n {\n \"updated\": \"2014-01-20T12:00:0-00:00\",\n \"name\": \"OpenStack Simple Certificate API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-SIMPLE-CERT/v1.0\",\n \"alias\": \"OS-SIMPLE-CERT\",\n \"description\": \"OpenStack simple certificate retrieval extension\"\n },\n {\n \"updated\": \"2013-07-07T12:00:0-00:00\",\n \"name\": \"OpenStack EC2 API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/OS-EC2/v1.0\",\n \"alias\": \"OS-EC2\",\n \"description\": \"OpenStack EC2 Credentials backend.\"\n }\n ]\n }\n}" 75 | } 76 | } 77 | } 78 | } 79 | }, 80 | "/v2.0/extensions/{alias}": { 81 | "parameters": [ 82 | { 83 | "name": "alias", 84 | "required": true, 85 | "in": "path", 86 | "type": "string", 87 | "description": "The extension name.\n" 88 | } 89 | ], 90 | "get": { 91 | "operationId": "getExtension-v2.0", 92 | "summary": "Get extension details", 93 | "description": "Gets detailed information for a specified extension.\n", 94 | "produces": [ 95 | "application/json" 96 | ], 97 | "responses": { 98 | "200": { 99 | "description": "200 203 response", 100 | "examples": { 101 | "application/json": "{\n \"extension\": {\n \"updated\": \"2013-07-07T12:00:0-00:00\",\n \"name\": \"OpenStack S3 API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/s3tokens/v1.0\",\n \"alias\": \"s3tokens\",\n \"description\": \"OpenStack S3 API.\"\n }\n}" 102 | } 103 | }, 104 | "203": { 105 | "description": "200 203 response", 106 | "examples": { 107 | "application/json": "{\n \"extension\": {\n \"updated\": \"2013-07-07T12:00:0-00:00\",\n \"name\": \"OpenStack S3 API\",\n \"links\": [\n {\n \"href\": \"https://github.com/openstack/identity-api\",\n \"type\": \"text/html\",\n \"rel\": \"describedby\"\n }\n ],\n \"namespace\": \"http://docs.openstack.org/identity/api/ext/s3tokens/v1.0\",\n \"alias\": \"s3tokens\",\n \"description\": \"OpenStack S3 API.\"\n }\n}" 108 | } 109 | } 110 | } 111 | } 112 | }, 113 | "/v2.0/tokens": { 114 | "post": { 115 | "operationId": "authenticate-v2.0", 116 | "summary": "Authenticate", 117 | "description": "Authenticates and generates a token.\n", 118 | "produces": [ 119 | "application/json" 120 | ], 121 | "responses": { 122 | "200": { 123 | "description": "200 203 response", 124 | "examples": { 125 | "application/json": "{\n \"access\": {\n \"token\": {\n \"issued_at\": \"2014-01-30T15:30:58.819584\",\n \"expires\": \"2014-01-31T15:30:58Z\",\n \"id\": \"aaaaa-bbbbb-ccccc-dddd\",\n \"tenant\": {\n \"enabled\": true,\n \"description\": null,\n \"name\": \"demo\",\n \"id\": \"fc394f2ab2df4114bde39905f800dc57\"\n }\n },\n \"serviceCatalog\": [\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8774/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8774/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8774/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"2dad48f09e2a447a9bf852bcd93548ef\"\n }\n ],\n \"type\": \"compute\",\n \"name\": \"nova\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:9696/\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:9696/\",\n \"internalURL\": \"http://23.253.72.207:9696/\",\n \"id\": \"97c526db8d7a4c88bbb8d68db1bdcdb8\"\n }\n ],\n \"type\": \"network\",\n \"name\": \"neutron\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8776/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8776/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8776/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"93f86dfcbba143a39a33d0c2cd424870\"\n }\n ],\n \"type\": \"volumev2\",\n \"name\": \"cinder\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8774/v3\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8774/v3\",\n \"internalURL\": \"http://23.253.72.207:8774/v3\",\n \"id\": \"3eb274b12b1d47b2abc536038d87339e\"\n }\n ],\n \"type\": \"computev3\",\n \"name\": \"nova\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:3333\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:3333\",\n \"internalURL\": \"http://23.253.72.207:3333\",\n \"id\": \"957f1e54afc64d33a62099faa5e980a2\"\n }\n ],\n \"type\": \"s3\",\n \"name\": \"s3\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:9292\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:9292\",\n \"internalURL\": \"http://23.253.72.207:9292\",\n \"id\": \"27d5749f36864c7d96bebf84a5ec9767\"\n }\n ],\n \"type\": \"image\",\n \"name\": \"glance\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8776/v1/fc394f2ab2df4114bde39905f800dc57\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8776/v1/fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8776/v1/fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"37c83a2157f944f1972e74658aa0b139\"\n }\n ],\n \"type\": \"volume\",\n \"name\": \"cinder\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8773/services/Admin\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8773/services/Cloud\",\n \"internalURL\": \"http://23.253.72.207:8773/services/Cloud\",\n \"id\": \"289b59289d6048e2912b327e5d3240ca\"\n }\n ],\n \"type\": \"ec2\",\n \"name\": \"ec2\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8080\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8080/v1/AUTH_fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8080/v1/AUTH_fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"16b76b5e5b7d48039a6e4cc3129545f3\"\n }\n ],\n \"type\": \"object-store\",\n \"name\": \"swift\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:35357/v2.0\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:5000/v2.0\",\n \"internalURL\": \"http://23.253.72.207:5000/v2.0\",\n \"id\": \"26af053673df4ef3a2340c4239e21ea2\"\n }\n ],\n \"type\": \"identity\",\n \"name\": \"keystone\"\n }\n ],\n \"user\": {\n \"username\": \"demo\",\n \"roles_links\": [],\n \"id\": \"9a6590b2ab024747bc2167c4e064d00d\",\n \"roles\": [\n {\n \"name\": \"Member\"\n },\n {\n \"name\": \"anotherrole\"\n }\n ],\n \"name\": \"demo\"\n },\n \"metadata\": {\n \"is_admin\": 0,\n \"roles\": [\n \"7598ac3c634d4c3da4b9126a5f67ca2b\",\n \"f95c0ab82d6045d9805033ee1fbc80d4\"\n ]\n }\n }\n}" 126 | } 127 | }, 128 | "203": { 129 | "description": "200 203 response", 130 | "examples": { 131 | "application/json": "{\n \"access\": {\n \"token\": {\n \"issued_at\": \"2014-01-30T15:30:58.819584\",\n \"expires\": \"2014-01-31T15:30:58Z\",\n \"id\": \"aaaaa-bbbbb-ccccc-dddd\",\n \"tenant\": {\n \"enabled\": true,\n \"description\": null,\n \"name\": \"demo\",\n \"id\": \"fc394f2ab2df4114bde39905f800dc57\"\n }\n },\n \"serviceCatalog\": [\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8774/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8774/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8774/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"2dad48f09e2a447a9bf852bcd93548ef\"\n }\n ],\n \"type\": \"compute\",\n \"name\": \"nova\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:9696/\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:9696/\",\n \"internalURL\": \"http://23.253.72.207:9696/\",\n \"id\": \"97c526db8d7a4c88bbb8d68db1bdcdb8\"\n }\n ],\n \"type\": \"network\",\n \"name\": \"neutron\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8776/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8776/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8776/v2/fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"93f86dfcbba143a39a33d0c2cd424870\"\n }\n ],\n \"type\": \"volumev2\",\n \"name\": \"cinder\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8774/v3\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8774/v3\",\n \"internalURL\": \"http://23.253.72.207:8774/v3\",\n \"id\": \"3eb274b12b1d47b2abc536038d87339e\"\n }\n ],\n \"type\": \"computev3\",\n \"name\": \"nova\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:3333\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:3333\",\n \"internalURL\": \"http://23.253.72.207:3333\",\n \"id\": \"957f1e54afc64d33a62099faa5e980a2\"\n }\n ],\n \"type\": \"s3\",\n \"name\": \"s3\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:9292\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:9292\",\n \"internalURL\": \"http://23.253.72.207:9292\",\n \"id\": \"27d5749f36864c7d96bebf84a5ec9767\"\n }\n ],\n \"type\": \"image\",\n \"name\": \"glance\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8776/v1/fc394f2ab2df4114bde39905f800dc57\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8776/v1/fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8776/v1/fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"37c83a2157f944f1972e74658aa0b139\"\n }\n ],\n \"type\": \"volume\",\n \"name\": \"cinder\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8773/services/Admin\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8773/services/Cloud\",\n \"internalURL\": \"http://23.253.72.207:8773/services/Cloud\",\n \"id\": \"289b59289d6048e2912b327e5d3240ca\"\n }\n ],\n \"type\": \"ec2\",\n \"name\": \"ec2\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:8080\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:8080/v1/AUTH_fc394f2ab2df4114bde39905f800dc57\",\n \"internalURL\": \"http://23.253.72.207:8080/v1/AUTH_fc394f2ab2df4114bde39905f800dc57\",\n \"id\": \"16b76b5e5b7d48039a6e4cc3129545f3\"\n }\n ],\n \"type\": \"object-store\",\n \"name\": \"swift\"\n },\n {\n \"endpoints_links\": [],\n \"endpoints\": [\n {\n \"adminURL\": \"http://23.253.72.207:35357/v2.0\",\n \"region\": \"RegionOne\",\n \"publicURL\": \"http://23.253.72.207:5000/v2.0\",\n \"internalURL\": \"http://23.253.72.207:5000/v2.0\",\n \"id\": \"26af053673df4ef3a2340c4239e21ea2\"\n }\n ],\n \"type\": \"identity\",\n \"name\": \"keystone\"\n }\n ],\n \"user\": {\n \"username\": \"demo\",\n \"roles_links\": [],\n \"id\": \"9a6590b2ab024747bc2167c4e064d00d\",\n \"roles\": [\n {\n \"name\": \"Member\"\n },\n {\n \"name\": \"anotherrole\"\n }\n ],\n \"name\": \"demo\"\n },\n \"metadata\": {\n \"is_admin\": 0,\n \"roles\": [\n \"7598ac3c634d4c3da4b9126a5f67ca2b\",\n \"f95c0ab82d6045d9805033ee1fbc80d4\"\n ]\n }\n }\n}" 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | "/v2.0/tenants": { 138 | "parameters": [ 139 | { 140 | "name": "X-Auth-Token", 141 | "required": true, 142 | "in": "header", 143 | "type": "string", 144 | "description": "A valid authentication token.\n" 145 | }, 146 | { 147 | "name": "marker", 148 | "required": false, 149 | "in": "query", 150 | "type": "string", 151 | "description": "The ID of the last item in the previous list.\n" 152 | }, 153 | { 154 | "name": "limit", 155 | "required": false, 156 | "in": "query", 157 | "type": "integer", 158 | "description": "The page size.\n" 159 | } 160 | ], 161 | "get": { 162 | "operationId": "listTenants", 163 | "summary": "List tenants", 164 | "description": "Lists tenants to which the specified token has access.\n", 165 | "produces": [ 166 | "application/json" 167 | ], 168 | "responses": { 169 | "200": { 170 | "description": "200 203 response", 171 | "examples": { 172 | "application/json": "{\n \"tenants_links\": [],\n \"tenants\": [\n {\n \"description\": \"A description ...\",\n \"enabled\": true,\n \"id\": \"1234\",\n \"name\": \"ACME Corp\"\n },\n {\n \"description\": \"A description ...\",\n \"enabled\": true,\n \"id\": \"3456\",\n \"name\": \"Iron Works\"\n }\n ]\n}" 173 | } 174 | }, 175 | "203": { 176 | "description": "200 203 response", 177 | "examples": { 178 | "application/json": "{\n \"tenants_links\": [],\n \"tenants\": [\n {\n \"description\": \"A description ...\",\n \"enabled\": true,\n \"id\": \"1234\",\n \"name\": \"ACME Corp\"\n },\n {\n \"description\": \"A description ...\",\n \"enabled\": true,\n \"id\": \"3456\",\n \"name\": \"Iron Works\"\n }\n ]\n}" 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /test/public/swagger-metering-labels.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "metering-labels", 5 | "version": "Unknown" 6 | }, 7 | "consumes": [ 8 | "application/json" 9 | ], 10 | "produces": [ 11 | "application/json" 12 | ], 13 | "paths": { 14 | "/v2.0/metering/metering-labels": { 15 | "get": { 16 | "operationId": "listMeteringLabels", 17 | "summary": "List metering labels", 18 | "description": "Lists all l3 metering labels that belong to the specified tenant.\n", 19 | "produces": [ 20 | "application/json" 21 | ], 22 | "responses": { 23 | "200": { 24 | "description": "200 response", 25 | "examples": { 26 | "application/json": "{\n \"metering_labels\": [\n {\n \"id\": \"a6700594-5b7a-4105-8bfe-723b346ce866\",\n \"tenant_id\": \"45345b0ee1ea477fac0f541b2cb79cd4\",\n \"description\": \"label1 description\",\n \"name\": \"label1\"\n },\n {\n \"id\": \"e131d186-b02d-4c0b-83d5-0c0725c4f812\",\n \"tenant_id\": \"45345b0ee1ea477fac0f541b2cb79cd4\",\n \"description\": \"label2 description\",\n \"name\": \"label2\"\n }\n ]\n}" 27 | } 28 | } 29 | } 30 | }, 31 | "post": { 32 | "operationId": "createMeteringLabel", 33 | "summary": "Create metering label", 34 | "description": "Creates a l3 metering label.\n", 35 | "produces": [ 36 | "application/json" 37 | ], 38 | "responses": { 39 | "201": { 40 | "description": "201 response", 41 | "examples": { 42 | "application/json": "{\n \"metering_label\": {\n \"id\": \"bc91b832-8465-40a7-a5d8-ba87de442266\",\n \"tenant_id\": \"45345b0ee1ea477fac0f541b2cb79cd4\",\n \"description\": \"description of label1\",\n \"name\": \"label1\"\n }\n}" 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | "/v2.0/metering/metering-labels/{metering_label_id}": { 49 | "parameters": [ 50 | { 51 | "name": "metering_label_id", 52 | "required": true, 53 | "in": "path", 54 | "type": "string", 55 | "description": "The unique identifier of the metering label.\n" 56 | } 57 | ], 58 | "get": { 59 | "operationId": "getMeteringLabel", 60 | "summary": "Show metering label", 61 | "description": "Shows informations for a specified metering label.\n", 62 | "produces": [ 63 | "application/json" 64 | ], 65 | "responses": { 66 | "200": { 67 | "description": "200 response", 68 | "examples": { 69 | "application/json": "{\n \"metering_label\": {\n \"id\": \"a6700594-5b7a-4105-8bfe-723b346ce866\",\n \"tenant_id\": \"45345b0ee1ea477fac0f541b2cb79cd4\",\n \"description\": \"label1 description\",\n \"name\": \"label1\"\n }\n}" 70 | } 71 | } 72 | } 73 | }, 74 | "delete": { 75 | "operationId": "deleteMeteringLabel", 76 | "summary": "Delete metering label", 77 | "description": "Deletes a l3 metering label.\n", 78 | "produces": [ 79 | "application/json" 80 | ], 81 | "responses": { 82 | "204": { 83 | "description": "204 response", 84 | "examples": {} 85 | } 86 | } 87 | } 88 | }, 89 | "/v2.0/metering/metering-label-rules": { 90 | "get": { 91 | "operationId": "listMeteringLabelRules", 92 | "summary": "List metering label rules", 93 | "description": "Lists a summary of all l3 metering label rules belonging to the specified tenant.\n", 94 | "produces": [ 95 | "application/json" 96 | ], 97 | "responses": { 98 | "200": { 99 | "description": "200 response", 100 | "examples": { 101 | "application/json": "{\n \"metering_label_rules\": [\n {\n \"remote_ip_prefix\": \"20.0.0.0/24\",\n \"direction\": \"ingress\",\n \"metering_label_id\": \"e131d186-b02d-4c0b-83d5-0c0725c4f812\",\n \"id\": \"9536641a-7d14-4dc5-afaf-93a973ce0eb8\",\n \"excluded\": false\n },\n {\n \"remote_ip_prefix\": \"10.0.0.0/24\",\n \"direction\": \"ingress\",\n \"metering_label_id\": \"e131d186-b02d-4c0b-83d5-0c0725c4f812\",\n \"id\": \"ffc6fd15-40de-4e7d-b617-34d3f7a93aec\",\n \"excluded\": false\n }\n ]\n}" 102 | } 103 | } 104 | } 105 | }, 106 | "post": { 107 | "operationId": "createMeteringLabelRule", 108 | "summary": "Create metering label rule", 109 | "description": "Creates a l3 metering label rule.\n", 110 | "produces": [ 111 | "application/json" 112 | ], 113 | "responses": { 114 | "201": { 115 | "description": "201 response", 116 | "examples": { 117 | "application/json": "{\n \"metering_label_rule\": {\n \"remote_ip_prefix\": \"10.0.1.0/24\",\n \"direction\": \"ingress\",\n \"metering_label_id\": \"e131d186-b02d-4c0b-83d5-0c0725c4f812\",\n \"id\": \"00e13b58-b4f2-4579-9c9c-7ac94615f9ae\",\n \"excluded\": false\n }\n}" 118 | } 119 | } 120 | } 121 | } 122 | }, 123 | "/v2.0/metering/metering-label-rules/{metering-label-rule-id}": { 124 | "parameters": [ 125 | { 126 | "name": "metering-label-rule-id", 127 | "required": true, 128 | "in": "path", 129 | "type": "string", 130 | "description": "The unique identifier of metering label rule.\n" 131 | } 132 | ], 133 | "get": { 134 | "operationId": "getMeteringLabelRule", 135 | "summary": "Show metering label rule", 136 | "description": "Shows detailed informations for a specified metering label rule.\n", 137 | "produces": [ 138 | "application/json" 139 | ], 140 | "responses": { 141 | "200": { 142 | "description": "200 response", 143 | "examples": { 144 | "application/json": "{\n \"metering_label_rule\": {\n \"remote_ip_prefix\": \"20.0.0.0/24\",\n \"direction\": \"ingress\",\n \"metering_label_id\": \"e131d186-b02d-4c0b-83d5-0c0725c4f812\",\n \"id\": \"9536641a-7d14-4dc5-afaf-93a973ce0eb8\",\n \"excluded\": false\n }\n}" 145 | } 146 | } 147 | } 148 | }, 149 | "delete": { 150 | "operationId": "deleteMeteringLabelRule", 151 | "summary": "Delete metering label rule", 152 | "description": "Deletes a specified l3 metering label rule.\n", 153 | "produces": [ 154 | "application/json" 155 | ], 156 | "responses": { 157 | "204": { 158 | "description": "204 response", 159 | "examples": {} 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /test/public/swagger-os-flavor-access.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "os-flavor-access", 5 | "version": "Unknown" 6 | }, 7 | "consumes": [ 8 | "application/json" 9 | ], 10 | "produces": [ 11 | "application/json" 12 | ], 13 | "paths": { 14 | "/v2/{tenant_id}/flavors": { 15 | "parameters": [ 16 | { 17 | "name": "tenant_id", 18 | "required": true, 19 | "in": "path", 20 | "type": "string", 21 | "description": "The ID for the tenant or account in a multi-tenancy cloud.\n" 22 | } 23 | ], 24 | "get": { 25 | "operationId": "detailAccess", 26 | "summary": "List flavors with access type", 27 | "description": "Lists flavors and includes the access type, which is public or private.\n", 28 | "produces": [ 29 | "application/json" 30 | ], 31 | "responses": { 32 | "200": { 33 | "description": "200 response", 34 | "examples": { 35 | "application/json": "{\n \"flavors\": [\n {\n \"name\": \"m1.tiny\",\n \"links\": [\n {\n \"href\": \"http://openstack.example.com/v2/openstack/flavors/1\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://openstack.example.com/openstack/flavors/1\",\n \"rel\": \"bookmark\"\n }\n ],\n \"ram\": 512,\n \"vcpus\": 1,\n \"os-flavor-access:is_public\": true,\n \"disk\": 1,\n \"id\": \"1\"\n },\n {\n \"name\": \"m1.small\",\n \"links\": [\n {\n \"href\": \"http://openstack.example.com/v2/openstack/flavors/2\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://openstack.example.com/openstack/flavors/2\",\n \"rel\": \"bookmark\"\n }\n ],\n \"ram\": 2048,\n \"vcpus\": 1,\n \"os-flavor-access:is_public\": true,\n \"disk\": 20,\n \"id\": \"2\"\n },\n {\n \"name\": \"m1.medium\",\n \"links\": [\n {\n \"href\": \"http://openstack.example.com/v2/openstack/flavors/3\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://openstack.example.com/openstack/flavors/3\",\n \"rel\": \"bookmark\"\n }\n ],\n \"ram\": 4096,\n \"vcpus\": 2,\n \"os-flavor-access:is_public\": true,\n \"disk\": 40,\n \"id\": \"3\"\n },\n {\n \"name\": \"m1.large\",\n \"links\": [\n {\n \"href\": \"http://openstack.example.com/v2/openstack/flavors/4\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://openstack.example.com/openstack/flavors/4\",\n \"rel\": \"bookmark\"\n }\n ],\n \"ram\": 8192,\n \"vcpus\": 4,\n \"os-flavor-access:is_public\": true,\n \"disk\": 80,\n \"id\": \"4\"\n },\n {\n \"name\": \"m1.xlarge\",\n \"links\": [\n {\n \"href\": \"http://openstack.example.com/v2/openstack/flavors/5\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://openstack.example.com/openstack/flavors/5\",\n \"rel\": \"bookmark\"\n }\n ],\n \"ram\": 16384,\n \"vcpus\": 8,\n \"os-flavor-access:is_public\": true,\n \"disk\": 160,\n \"id\": \"5\"\n }\n ]\n}" 36 | } 37 | } 38 | } 39 | }, 40 | "post": { 41 | "operationId": "createAccess", 42 | "summary": "Create private flavor", 43 | "description": "Creates a private flavor.\n", 44 | "produces": [ 45 | "application/json" 46 | ], 47 | "responses": { 48 | "200": { 49 | "description": "200 response", 50 | "examples": { 51 | "application/json": "{\n \"flavor\": {\n \"name\": \"test_flavor\",\n \"links\": [\n {\n \"href\": \"http://openstack.example.com/v2/openstack/flavors/10\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://openstack.example.com/openstack/flavors/10\",\n \"rel\": \"bookmark\"\n }\n ],\n \"ram\": 1024,\n \"vcpus\": 2,\n \"os-flavor-access:is_public\": false,\n \"disk\": 10,\n \"id\": \"10\"\n }\n}" 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "/v2/{tenant_id}/flavors/{flavor_id}": { 58 | "parameters": [ 59 | { 60 | "name": "tenant_id", 61 | "required": true, 62 | "in": "path", 63 | "type": "string", 64 | "description": "The ID for the tenant or account in a multi-tenancy cloud.\n" 65 | }, 66 | { 67 | "name": "flavor_id", 68 | "required": true, 69 | "in": "path", 70 | "type": "string", 71 | "description": "The ID of the flavor of interest to you.\n" 72 | } 73 | ], 74 | "get": { 75 | "operationId": "showAccess", 76 | "summary": "Show flavor access type", 77 | "description": "Gets the flavor access type, which is public or private.\n", 78 | "produces": [ 79 | "application/json" 80 | ], 81 | "responses": { 82 | "200": { 83 | "description": "200 response", 84 | "examples": { 85 | "application/json": "{\n \"flavor\": {\n \"name\": \"m1.tiny\",\n \"links\": [\n {\n \"href\": \"http://openstack.example.com/v2/openstack/flavors/1\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://openstack.example.com/openstack/flavors/1\",\n \"rel\": \"bookmark\"\n }\n ],\n \"ram\": 512,\n \"vcpus\": 1,\n \"os-flavor-access:is_public\": true,\n \"disk\": 1,\n \"id\": \"1\"\n }\n}" 86 | } 87 | } 88 | } 89 | } 90 | }, 91 | "/v2/{tenant_id}/flavors/{flavor_id}/os-flavor-access": { 92 | "parameters": [ 93 | { 94 | "name": "tenant_id", 95 | "required": true, 96 | "in": "path", 97 | "type": "string", 98 | "description": "The ID for the tenant or account in a multi-tenancy cloud.\n" 99 | }, 100 | { 101 | "name": "flavor_id", 102 | "required": true, 103 | "in": "path", 104 | "type": "string", 105 | "description": "The ID of the flavor of interest to you.\n" 106 | } 107 | ], 108 | "get": { 109 | "operationId": "listAccess", 110 | "summary": "List tenants with access to private flavor", 111 | "description": "Lists tenants with access to the specified private flavor.\n", 112 | "produces": [ 113 | "application/json" 114 | ], 115 | "responses": { 116 | "200": { 117 | "description": "200 response", 118 | "examples": { 119 | "application/json": "{\n \"flavor_access\": [\n {\n \"tenant_id\": \"fake_tenant\",\n \"flavor_id\": \"10\"\n },\n {\n \"tenant_id\": \"openstack\",\n \"flavor_id\": \"10\"\n }\n ]\n}" 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "/v2/{tenant_id}/flavors/{flavor_id}/action": { 126 | "parameters": [ 127 | { 128 | "name": "tenant_id", 129 | "required": true, 130 | "in": "path", 131 | "type": "string", 132 | "description": "The ID for the tenant or account in a multi-tenancy cloud.\n" 133 | }, 134 | { 135 | "name": "flavor_id", 136 | "required": true, 137 | "in": "path", 138 | "type": "string", 139 | "description": "The ID of the flavor of interest to you.\n" 140 | } 141 | ], 142 | "post": { 143 | "operationId": "addTenantAccess", 144 | "summary": "Add access to private flavor", 145 | "description": "Gives a specified tenant access to the specified private flavor.\n", 146 | "produces": [ 147 | "application/json" 148 | ], 149 | "responses": { 150 | "200": { 151 | "description": "200 response", 152 | "examples": { 153 | "application/json": "{\n \"flavor_access\": [\n {\n \"tenant_id\": \"fake_tenant\",\n \"flavor_id\": \"10\"\n },\n {\n \"tenant_id\": \"openstack\",\n \"flavor_id\": \"10\"\n }\n ]\n}" 154 | } 155 | } 156 | } 157 | }, 158 | "delete": { 159 | "operationId": "removeTenantAccess", 160 | "summary": "Delete access from private flavor", 161 | "description": "Revokes access from the specified tenant for the specified private flavor.\n", 162 | "produces": [ 163 | "application/json" 164 | ], 165 | "responses": { 166 | "200": { 167 | "description": "200 response", 168 | "examples": { 169 | "application/json": "{\n \"flavor_access\": [\n {\n \"tenant_id\": \"openstack\",\n \"flavor_id\": \"10\"\n }\n ]\n}" 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /test/public/swagger-pet-store.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "Swagger Petstore - OpenAPI 3.0", 5 | "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "email": "apiteam@swagger.io" 9 | }, 10 | "license": { 11 | "name": "Apache 2.0", 12 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | }, 14 | "version": "1.0.11" 15 | }, 16 | "externalDocs": { 17 | "description": "Find out more about Swagger", 18 | "url": "http://swagger.io" 19 | }, 20 | "servers": [ 21 | { 22 | "url": "https://petstore3.swagger.io/api/v3" 23 | } 24 | ], 25 | "tags": [ 26 | { 27 | "name": "pet", 28 | "description": "Everything about your Pets", 29 | "externalDocs": { 30 | "description": "Find out more", 31 | "url": "http://swagger.io" 32 | } 33 | }, 34 | { 35 | "name": "store", 36 | "description": "Access to Petstore orders", 37 | "externalDocs": { 38 | "description": "Find out more about our store", 39 | "url": "http://swagger.io" 40 | } 41 | }, 42 | { 43 | "name": "user", 44 | "description": "Operations about user" 45 | } 46 | ], 47 | "paths": { 48 | "/pet": { 49 | "put": { 50 | "tags": [ 51 | "pet" 52 | ], 53 | "summary": "Update an existing pet", 54 | "description": "Update an existing pet by Id", 55 | "operationId": "updatePet", 56 | "requestBody": { 57 | "description": "Update an existent pet in the store", 58 | "content": { 59 | "application/json": { 60 | "schema": { 61 | "$ref": "#/components/schemas/Pet" 62 | } 63 | }, 64 | "application/xml": { 65 | "schema": { 66 | "$ref": "#/components/schemas/Pet" 67 | } 68 | }, 69 | "application/x-www-form-urlencoded": { 70 | "schema": { 71 | "$ref": "#/components/schemas/Pet" 72 | } 73 | } 74 | }, 75 | "required": true 76 | }, 77 | "responses": { 78 | "200": { 79 | "description": "Successful operation", 80 | "content": { 81 | "application/json": { 82 | "schema": { 83 | "$ref": "#/components/schemas/Pet" 84 | } 85 | }, 86 | "application/xml": { 87 | "schema": { 88 | "$ref": "#/components/schemas/Pet" 89 | } 90 | } 91 | } 92 | }, 93 | "400": { 94 | "description": "Invalid ID supplied" 95 | }, 96 | "404": { 97 | "description": "Pet not found" 98 | }, 99 | "405": { 100 | "description": "Validation exception" 101 | } 102 | }, 103 | "security": [ 104 | { 105 | "petstore_auth": [ 106 | "write:pets", 107 | "read:pets" 108 | ] 109 | } 110 | ] 111 | }, 112 | "post": { 113 | "tags": [ 114 | "pet" 115 | ], 116 | "summary": "Add a new pet to the store", 117 | "description": "Add a new pet to the store", 118 | "operationId": "addPet", 119 | "requestBody": { 120 | "description": "Create a new pet in the store", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Pet" 125 | } 126 | }, 127 | "application/xml": { 128 | "schema": { 129 | "$ref": "#/components/schemas/Pet" 130 | } 131 | }, 132 | "application/x-www-form-urlencoded": { 133 | "schema": { 134 | "$ref": "#/components/schemas/Pet" 135 | } 136 | } 137 | }, 138 | "required": true 139 | }, 140 | "responses": { 141 | "200": { 142 | "description": "Successful operation", 143 | "content": { 144 | "application/json": { 145 | "schema": { 146 | "$ref": "#/components/schemas/Pet" 147 | } 148 | }, 149 | "application/xml": { 150 | "schema": { 151 | "$ref": "#/components/schemas/Pet" 152 | } 153 | } 154 | } 155 | }, 156 | "405": { 157 | "description": "Invalid input" 158 | } 159 | }, 160 | "security": [ 161 | { 162 | "petstore_auth": [ 163 | "write:pets", 164 | "read:pets" 165 | ] 166 | } 167 | ] 168 | } 169 | }, 170 | "/pet/findByStatus": { 171 | "get": { 172 | "tags": [ 173 | "pet" 174 | ], 175 | "summary": "Finds Pets by status", 176 | "description": "Multiple status values can be provided with comma separated strings", 177 | "operationId": "findPetsByStatus", 178 | "parameters": [ 179 | { 180 | "name": "status", 181 | "in": "query", 182 | "description": "Status values that need to be considered for filter", 183 | "required": false, 184 | "explode": true, 185 | "schema": { 186 | "type": "string", 187 | "default": "available", 188 | "enum": [ 189 | "available", 190 | "pending", 191 | "sold" 192 | ] 193 | } 194 | } 195 | ], 196 | "responses": { 197 | "200": { 198 | "description": "successful operation", 199 | "content": { 200 | "application/json": { 201 | "schema": { 202 | "type": "array", 203 | "items": { 204 | "$ref": "#/components/schemas/Pet" 205 | } 206 | } 207 | }, 208 | "application/xml": { 209 | "schema": { 210 | "type": "array", 211 | "items": { 212 | "$ref": "#/components/schemas/Pet" 213 | } 214 | } 215 | } 216 | } 217 | }, 218 | "400": { 219 | "description": "Invalid status value" 220 | } 221 | }, 222 | "security": [ 223 | { 224 | "petstore_auth": [ 225 | "write:pets", 226 | "read:pets" 227 | ] 228 | } 229 | ] 230 | } 231 | }, 232 | "/pet/findByTags": { 233 | "get": { 234 | "tags": [ 235 | "pet" 236 | ], 237 | "summary": "Finds Pets by tags", 238 | "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 239 | "operationId": "findPetsByTags", 240 | "parameters": [ 241 | { 242 | "name": "tags", 243 | "in": "query", 244 | "description": "Tags to filter by", 245 | "required": false, 246 | "explode": true, 247 | "schema": { 248 | "type": "array", 249 | "items": { 250 | "type": "string" 251 | } 252 | } 253 | } 254 | ], 255 | "responses": { 256 | "200": { 257 | "description": "successful operation", 258 | "content": { 259 | "application/json": { 260 | "schema": { 261 | "type": "array", 262 | "items": { 263 | "$ref": "#/components/schemas/Pet" 264 | } 265 | } 266 | }, 267 | "application/xml": { 268 | "schema": { 269 | "type": "array", 270 | "items": { 271 | "$ref": "#/components/schemas/Pet" 272 | } 273 | } 274 | } 275 | } 276 | }, 277 | "400": { 278 | "description": "Invalid tag value" 279 | } 280 | }, 281 | "security": [ 282 | { 283 | "petstore_auth": [ 284 | "write:pets", 285 | "read:pets" 286 | ] 287 | } 288 | ] 289 | } 290 | }, 291 | "/pet/{petId}": { 292 | "get": { 293 | "tags": [ 294 | "pet" 295 | ], 296 | "summary": "Find pet by ID", 297 | "description": "Returns a single pet", 298 | "operationId": "getPetById", 299 | "parameters": [ 300 | { 301 | "name": "petId", 302 | "in": "path", 303 | "description": "ID of pet to return", 304 | "required": true, 305 | "schema": { 306 | "type": "integer", 307 | "format": "int64" 308 | } 309 | } 310 | ], 311 | "responses": { 312 | "200": { 313 | "description": "successful operation", 314 | "content": { 315 | "application/json": { 316 | "schema": { 317 | "$ref": "#/components/schemas/Pet" 318 | } 319 | }, 320 | "application/xml": { 321 | "schema": { 322 | "$ref": "#/components/schemas/Pet" 323 | } 324 | } 325 | } 326 | }, 327 | "400": { 328 | "description": "Invalid ID supplied" 329 | }, 330 | "404": { 331 | "description": "Pet not found" 332 | } 333 | }, 334 | "security": [ 335 | { 336 | "api_key": [] 337 | }, 338 | { 339 | "petstore_auth": [ 340 | "write:pets", 341 | "read:pets" 342 | ] 343 | } 344 | ] 345 | }, 346 | "post": { 347 | "tags": [ 348 | "pet" 349 | ], 350 | "summary": "Updates a pet in the store with form data", 351 | "description": "", 352 | "operationId": "updatePetWithForm", 353 | "parameters": [ 354 | { 355 | "name": "petId", 356 | "in": "path", 357 | "description": "ID of pet that needs to be updated", 358 | "required": true, 359 | "schema": { 360 | "type": "integer", 361 | "format": "int64" 362 | } 363 | }, 364 | { 365 | "name": "name", 366 | "in": "query", 367 | "description": "Name of pet that needs to be updated", 368 | "schema": { 369 | "type": "string" 370 | } 371 | }, 372 | { 373 | "name": "status", 374 | "in": "query", 375 | "description": "Status of pet that needs to be updated", 376 | "schema": { 377 | "type": "string" 378 | } 379 | } 380 | ], 381 | "responses": { 382 | "405": { 383 | "description": "Invalid input" 384 | } 385 | }, 386 | "security": [ 387 | { 388 | "petstore_auth": [ 389 | "write:pets", 390 | "read:pets" 391 | ] 392 | } 393 | ] 394 | }, 395 | "delete": { 396 | "tags": [ 397 | "pet" 398 | ], 399 | "summary": "Deletes a pet", 400 | "description": "delete a pet", 401 | "operationId": "deletePet", 402 | "parameters": [ 403 | { 404 | "name": "api_key", 405 | "in": "header", 406 | "description": "", 407 | "required": false, 408 | "schema": { 409 | "type": "string" 410 | } 411 | }, 412 | { 413 | "name": "petId", 414 | "in": "path", 415 | "description": "Pet id to delete", 416 | "required": true, 417 | "schema": { 418 | "type": "integer", 419 | "format": "int64" 420 | } 421 | } 422 | ], 423 | "responses": { 424 | "400": { 425 | "description": "Invalid pet value" 426 | } 427 | }, 428 | "security": [ 429 | { 430 | "petstore_auth": [ 431 | "write:pets", 432 | "read:pets" 433 | ] 434 | } 435 | ] 436 | } 437 | }, 438 | "/pet/{petId}/uploadImage": { 439 | "post": { 440 | "tags": [ 441 | "pet" 442 | ], 443 | "summary": "uploads an image", 444 | "description": "", 445 | "operationId": "uploadFile", 446 | "parameters": [ 447 | { 448 | "name": "petId", 449 | "in": "path", 450 | "description": "ID of pet to update", 451 | "required": true, 452 | "schema": { 453 | "type": "integer", 454 | "format": "int64" 455 | } 456 | }, 457 | { 458 | "name": "additionalMetadata", 459 | "in": "query", 460 | "description": "Additional Metadata", 461 | "required": false, 462 | "schema": { 463 | "type": "string" 464 | } 465 | } 466 | ], 467 | "requestBody": { 468 | "content": { 469 | "application/octet-stream": { 470 | "schema": { 471 | "type": "string", 472 | "format": "binary" 473 | } 474 | } 475 | } 476 | }, 477 | "responses": { 478 | "200": { 479 | "description": "successful operation", 480 | "content": { 481 | "application/json": { 482 | "schema": { 483 | "$ref": "#/components/schemas/ApiResponse" 484 | } 485 | } 486 | } 487 | } 488 | }, 489 | "security": [ 490 | { 491 | "petstore_auth": [ 492 | "write:pets", 493 | "read:pets" 494 | ] 495 | } 496 | ] 497 | } 498 | }, 499 | "/store/inventory": { 500 | "get": { 501 | "tags": [ 502 | "store" 503 | ], 504 | "summary": "Returns pet inventories by status", 505 | "description": "Returns a map of status codes to quantities", 506 | "operationId": "getInventory", 507 | "responses": { 508 | "200": { 509 | "description": "successful operation", 510 | "content": { 511 | "application/json": { 512 | "schema": { 513 | "type": "object", 514 | "additionalProperties": { 515 | "type": "integer", 516 | "format": "int32" 517 | } 518 | } 519 | } 520 | } 521 | } 522 | }, 523 | "security": [ 524 | { 525 | "api_key": [] 526 | } 527 | ] 528 | } 529 | }, 530 | "/store/order": { 531 | "post": { 532 | "tags": [ 533 | "store" 534 | ], 535 | "summary": "Place an order for a pet", 536 | "description": "Place a new order in the store", 537 | "operationId": "placeOrder", 538 | "requestBody": { 539 | "content": { 540 | "application/json": { 541 | "schema": { 542 | "$ref": "#/components/schemas/Order" 543 | } 544 | }, 545 | "application/xml": { 546 | "schema": { 547 | "$ref": "#/components/schemas/Order" 548 | } 549 | }, 550 | "application/x-www-form-urlencoded": { 551 | "schema": { 552 | "$ref": "#/components/schemas/Order" 553 | } 554 | } 555 | } 556 | }, 557 | "responses": { 558 | "200": { 559 | "description": "successful operation", 560 | "content": { 561 | "application/json": { 562 | "schema": { 563 | "$ref": "#/components/schemas/Order" 564 | } 565 | } 566 | } 567 | }, 568 | "405": { 569 | "description": "Invalid input" 570 | } 571 | } 572 | } 573 | }, 574 | "/store/order/{orderId}": { 575 | "get": { 576 | "tags": [ 577 | "store" 578 | ], 579 | "summary": "Find purchase order by ID", 580 | "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", 581 | "operationId": "getOrderById", 582 | "parameters": [ 583 | { 584 | "name": "orderId", 585 | "in": "path", 586 | "description": "ID of order that needs to be fetched", 587 | "required": true, 588 | "schema": { 589 | "type": "integer", 590 | "format": "int64" 591 | } 592 | } 593 | ], 594 | "responses": { 595 | "200": { 596 | "description": "successful operation", 597 | "content": { 598 | "application/json": { 599 | "schema": { 600 | "$ref": "#/components/schemas/Order" 601 | } 602 | }, 603 | "application/xml": { 604 | "schema": { 605 | "$ref": "#/components/schemas/Order" 606 | } 607 | } 608 | } 609 | }, 610 | "400": { 611 | "description": "Invalid ID supplied" 612 | }, 613 | "404": { 614 | "description": "Order not found" 615 | } 616 | } 617 | }, 618 | "delete": { 619 | "tags": [ 620 | "store" 621 | ], 622 | "summary": "Delete purchase order by ID", 623 | "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", 624 | "operationId": "deleteOrder", 625 | "parameters": [ 626 | { 627 | "name": "orderId", 628 | "in": "path", 629 | "description": "ID of the order that needs to be deleted", 630 | "required": true, 631 | "schema": { 632 | "type": "integer", 633 | "format": "int64" 634 | } 635 | } 636 | ], 637 | "responses": { 638 | "400": { 639 | "description": "Invalid ID supplied" 640 | }, 641 | "404": { 642 | "description": "Order not found" 643 | } 644 | } 645 | } 646 | }, 647 | "/user": { 648 | "post": { 649 | "tags": [ 650 | "user" 651 | ], 652 | "summary": "Create user", 653 | "description": "This can only be done by the logged in user.", 654 | "operationId": "createUser", 655 | "requestBody": { 656 | "description": "Created user object", 657 | "content": { 658 | "application/json": { 659 | "schema": { 660 | "$ref": "#/components/schemas/User" 661 | } 662 | }, 663 | "application/xml": { 664 | "schema": { 665 | "$ref": "#/components/schemas/User" 666 | } 667 | }, 668 | "application/x-www-form-urlencoded": { 669 | "schema": { 670 | "$ref": "#/components/schemas/User" 671 | } 672 | } 673 | } 674 | }, 675 | "responses": { 676 | "default": { 677 | "description": "successful operation", 678 | "content": { 679 | "application/json": { 680 | "schema": { 681 | "$ref": "#/components/schemas/User" 682 | } 683 | }, 684 | "application/xml": { 685 | "schema": { 686 | "$ref": "#/components/schemas/User" 687 | } 688 | } 689 | } 690 | } 691 | } 692 | } 693 | }, 694 | "/user/createWithList": { 695 | "post": { 696 | "tags": [ 697 | "user" 698 | ], 699 | "summary": "Creates list of users with given input array", 700 | "description": "Creates list of users with given input array", 701 | "operationId": "createUsersWithListInput", 702 | "requestBody": { 703 | "content": { 704 | "application/json": { 705 | "schema": { 706 | "type": "array", 707 | "items": { 708 | "$ref": "#/components/schemas/User" 709 | } 710 | } 711 | } 712 | } 713 | }, 714 | "responses": { 715 | "200": { 716 | "description": "Successful operation", 717 | "content": { 718 | "application/json": { 719 | "schema": { 720 | "$ref": "#/components/schemas/User" 721 | } 722 | }, 723 | "application/xml": { 724 | "schema": { 725 | "$ref": "#/components/schemas/User" 726 | } 727 | } 728 | } 729 | }, 730 | "default": { 731 | "description": "successful operation" 732 | } 733 | } 734 | } 735 | }, 736 | "/user/login": { 737 | "get": { 738 | "tags": [ 739 | "user" 740 | ], 741 | "summary": "Logs user into the system", 742 | "description": "", 743 | "operationId": "loginUser", 744 | "parameters": [ 745 | { 746 | "name": "username", 747 | "in": "query", 748 | "description": "The user name for login", 749 | "required": false, 750 | "schema": { 751 | "type": "string" 752 | } 753 | }, 754 | { 755 | "name": "password", 756 | "in": "query", 757 | "description": "The password for login in clear text", 758 | "required": false, 759 | "schema": { 760 | "type": "string" 761 | } 762 | } 763 | ], 764 | "responses": { 765 | "200": { 766 | "description": "successful operation", 767 | "headers": { 768 | "X-Rate-Limit": { 769 | "description": "calls per hour allowed by the user", 770 | "schema": { 771 | "type": "integer", 772 | "format": "int32" 773 | } 774 | }, 775 | "X-Expires-After": { 776 | "description": "date in UTC when token expires", 777 | "schema": { 778 | "type": "string", 779 | "format": "date-time" 780 | } 781 | } 782 | }, 783 | "content": { 784 | "application/xml": { 785 | "schema": { 786 | "type": "string" 787 | } 788 | }, 789 | "application/json": { 790 | "schema": { 791 | "type": "string" 792 | } 793 | } 794 | } 795 | }, 796 | "400": { 797 | "description": "Invalid username/password supplied" 798 | } 799 | } 800 | } 801 | }, 802 | "/user/logout": { 803 | "get": { 804 | "tags": [ 805 | "user" 806 | ], 807 | "summary": "Logs out current logged in user session", 808 | "description": "", 809 | "operationId": "logoutUser", 810 | "parameters": [], 811 | "responses": { 812 | "default": { 813 | "description": "successful operation" 814 | } 815 | } 816 | } 817 | }, 818 | "/user/{username}": { 819 | "get": { 820 | "tags": [ 821 | "user" 822 | ], 823 | "summary": "Get user by user name", 824 | "description": "", 825 | "operationId": "getUserByName", 826 | "parameters": [ 827 | { 828 | "name": "username", 829 | "in": "path", 830 | "description": "The name that needs to be fetched. Use user1 for testing. ", 831 | "required": true, 832 | "schema": { 833 | "type": "string" 834 | } 835 | } 836 | ], 837 | "responses": { 838 | "200": { 839 | "description": "successful operation", 840 | "content": { 841 | "application/json": { 842 | "schema": { 843 | "$ref": "#/components/schemas/User" 844 | } 845 | }, 846 | "application/xml": { 847 | "schema": { 848 | "$ref": "#/components/schemas/User" 849 | } 850 | } 851 | } 852 | }, 853 | "400": { 854 | "description": "Invalid username supplied" 855 | }, 856 | "404": { 857 | "description": "User not found" 858 | } 859 | } 860 | }, 861 | "put": { 862 | "tags": [ 863 | "user" 864 | ], 865 | "summary": "Update user", 866 | "description": "This can only be done by the logged in user.", 867 | "operationId": "updateUser", 868 | "parameters": [ 869 | { 870 | "name": "username", 871 | "in": "path", 872 | "description": "name that need to be deleted", 873 | "required": true, 874 | "schema": { 875 | "type": "string" 876 | } 877 | } 878 | ], 879 | "requestBody": { 880 | "description": "Update an existent user in the store", 881 | "content": { 882 | "application/json": { 883 | "schema": { 884 | "$ref": "#/components/schemas/User" 885 | } 886 | }, 887 | "application/xml": { 888 | "schema": { 889 | "$ref": "#/components/schemas/User" 890 | } 891 | }, 892 | "application/x-www-form-urlencoded": { 893 | "schema": { 894 | "$ref": "#/components/schemas/User" 895 | } 896 | } 897 | } 898 | }, 899 | "responses": { 900 | "default": { 901 | "description": "successful operation" 902 | } 903 | } 904 | }, 905 | "delete": { 906 | "tags": [ 907 | "user" 908 | ], 909 | "summary": "Delete user", 910 | "description": "This can only be done by the logged in user.", 911 | "operationId": "deleteUser", 912 | "parameters": [ 913 | { 914 | "name": "username", 915 | "in": "path", 916 | "description": "The name that needs to be deleted", 917 | "required": true, 918 | "schema": { 919 | "type": "string" 920 | } 921 | } 922 | ], 923 | "responses": { 924 | "400": { 925 | "description": "Invalid username supplied" 926 | }, 927 | "404": { 928 | "description": "User not found" 929 | } 930 | } 931 | } 932 | } 933 | }, 934 | "components": { 935 | "schemas": { 936 | "Order": { 937 | "type": "object", 938 | "properties": { 939 | "id": { 940 | "type": "integer", 941 | "format": "int64", 942 | "example": 10 943 | }, 944 | "petId": { 945 | "type": "integer", 946 | "format": "int64", 947 | "example": 198772 948 | }, 949 | "quantity": { 950 | "type": "integer", 951 | "format": "int32", 952 | "example": 7 953 | }, 954 | "shipDate": { 955 | "type": "string", 956 | "format": "date-time" 957 | }, 958 | "status": { 959 | "type": "string", 960 | "description": "Order Status", 961 | "example": "approved", 962 | "enum": [ 963 | "placed", 964 | "approved", 965 | "delivered" 966 | ] 967 | }, 968 | "complete": { 969 | "type": "boolean" 970 | } 971 | }, 972 | "xml": { 973 | "name": "order" 974 | } 975 | }, 976 | "Customer": { 977 | "type": "object", 978 | "properties": { 979 | "id": { 980 | "type": "integer", 981 | "format": "int64", 982 | "example": 100000 983 | }, 984 | "username": { 985 | "type": "string", 986 | "example": "fehguy" 987 | }, 988 | "address": { 989 | "type": "array", 990 | "xml": { 991 | "name": "addresses", 992 | "wrapped": true 993 | }, 994 | "items": { 995 | "$ref": "#/components/schemas/Address" 996 | } 997 | } 998 | }, 999 | "xml": { 1000 | "name": "customer" 1001 | } 1002 | }, 1003 | "Address": { 1004 | "type": "object", 1005 | "properties": { 1006 | "street": { 1007 | "type": "string", 1008 | "example": "437 Lytton" 1009 | }, 1010 | "city": { 1011 | "type": "string", 1012 | "example": "Palo Alto" 1013 | }, 1014 | "state": { 1015 | "type": "string", 1016 | "example": "CA" 1017 | }, 1018 | "zip": { 1019 | "type": "string", 1020 | "example": "94301" 1021 | } 1022 | }, 1023 | "xml": { 1024 | "name": "address" 1025 | } 1026 | }, 1027 | "Category": { 1028 | "type": "object", 1029 | "properties": { 1030 | "id": { 1031 | "type": "integer", 1032 | "format": "int64", 1033 | "example": 1 1034 | }, 1035 | "name": { 1036 | "type": "string", 1037 | "example": "Dogs" 1038 | } 1039 | }, 1040 | "xml": { 1041 | "name": "category" 1042 | } 1043 | }, 1044 | "User": { 1045 | "type": "object", 1046 | "properties": { 1047 | "id": { 1048 | "type": "integer", 1049 | "format": "int64", 1050 | "example": 10 1051 | }, 1052 | "username": { 1053 | "type": "string", 1054 | "example": "theUser" 1055 | }, 1056 | "firstName": { 1057 | "type": "string", 1058 | "example": "John" 1059 | }, 1060 | "lastName": { 1061 | "type": "string", 1062 | "example": "James" 1063 | }, 1064 | "email": { 1065 | "type": "string", 1066 | "example": "john@email.com" 1067 | }, 1068 | "password": { 1069 | "type": "string", 1070 | "example": "12345" 1071 | }, 1072 | "phone": { 1073 | "type": "string", 1074 | "example": "12345" 1075 | }, 1076 | "userStatus": { 1077 | "type": "integer", 1078 | "description": "User Status", 1079 | "format": "int32", 1080 | "example": 1 1081 | } 1082 | }, 1083 | "xml": { 1084 | "name": "user" 1085 | } 1086 | }, 1087 | "Tag": { 1088 | "type": "object", 1089 | "properties": { 1090 | "id": { 1091 | "type": "integer", 1092 | "format": "int64" 1093 | }, 1094 | "name": { 1095 | "type": "string" 1096 | } 1097 | }, 1098 | "xml": { 1099 | "name": "tag" 1100 | } 1101 | }, 1102 | "Pet": { 1103 | "required": [ 1104 | "name", 1105 | "photoUrls" 1106 | ], 1107 | "type": "object", 1108 | "properties": { 1109 | "id": { 1110 | "type": "integer", 1111 | "format": "int64", 1112 | "example": 10 1113 | }, 1114 | "name": { 1115 | "type": "string", 1116 | "example": "doggie" 1117 | }, 1118 | "category": { 1119 | "$ref": "#/components/schemas/Category" 1120 | }, 1121 | "photoUrls": { 1122 | "type": "array", 1123 | "xml": { 1124 | "wrapped": true 1125 | }, 1126 | "items": { 1127 | "type": "string", 1128 | "xml": { 1129 | "name": "photoUrl" 1130 | } 1131 | } 1132 | }, 1133 | "tags": { 1134 | "type": "array", 1135 | "xml": { 1136 | "wrapped": true 1137 | }, 1138 | "items": { 1139 | "$ref": "#/components/schemas/Tag" 1140 | } 1141 | }, 1142 | "status": { 1143 | "type": "string", 1144 | "description": "pet status in the store", 1145 | "enum": [ 1146 | "available", 1147 | "pending", 1148 | "sold" 1149 | ] 1150 | } 1151 | }, 1152 | "xml": { 1153 | "name": "pet" 1154 | } 1155 | }, 1156 | "ApiResponse": { 1157 | "type": "object", 1158 | "properties": { 1159 | "code": { 1160 | "type": "integer", 1161 | "format": "int32" 1162 | }, 1163 | "type": { 1164 | "type": "string" 1165 | }, 1166 | "message": { 1167 | "type": "string" 1168 | } 1169 | }, 1170 | "xml": { 1171 | "name": "##default" 1172 | } 1173 | } 1174 | }, 1175 | "requestBodies": { 1176 | "Pet": { 1177 | "description": "Pet object that needs to be added to the store", 1178 | "content": { 1179 | "application/json": { 1180 | "schema": { 1181 | "$ref": "#/components/schemas/Pet" 1182 | } 1183 | }, 1184 | "application/xml": { 1185 | "schema": { 1186 | "$ref": "#/components/schemas/Pet" 1187 | } 1188 | } 1189 | } 1190 | }, 1191 | "UserArray": { 1192 | "description": "List of user object", 1193 | "content": { 1194 | "application/json": { 1195 | "schema": { 1196 | "type": "array", 1197 | "items": { 1198 | "$ref": "#/components/schemas/User" 1199 | } 1200 | } 1201 | } 1202 | } 1203 | } 1204 | }, 1205 | "securitySchemes": { 1206 | "petstore_auth": { 1207 | "type": "oauth2", 1208 | "flows": { 1209 | "implicit": { 1210 | "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", 1211 | "scopes": { 1212 | "write:pets": "modify pets in your account", 1213 | "read:pets": "read your pets" 1214 | } 1215 | } 1216 | } 1217 | }, 1218 | "api_key": { 1219 | "type": "apiKey", 1220 | "name": "api_key", 1221 | "in": "header" 1222 | } 1223 | } 1224 | } 1225 | } -------------------------------------------------------------------------------- /test/transformPaths.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest' 2 | import petStoreSwagger from './public/swagger-pet-store.json' 3 | import identitySwagger from './public/swagger-identity.json' 4 | import meteringLablesSwagger from './public/swagger-metering-labels.json' 5 | import osFlavourSwagger from './public/swagger-os-flavor-access.json' 6 | import { transformPaths } from '../lib/utils' 7 | import { expect } from 'vitest' 8 | describe('transformPaths', it => { 9 | it('should match snapshot example 1', () => { 10 | expect(transformPaths(petStoreSwagger.paths as unknown as any)).toMatchSnapshot() 11 | 12 | }) 13 | 14 | it('should match snapshot example 2', () => { 15 | expect(transformPaths(identitySwagger.paths as unknown as any)).toMatchSnapshot() 16 | }) 17 | 18 | it('should match snapshot example 3', () => { 19 | expect(transformPaths(osFlavourSwagger.paths as unknown as any)).toMatchSnapshot() 20 | }) 21 | 22 | it('should match snapshot example 4', () => { 23 | expect(transformPaths(meteringLablesSwagger.paths as unknown as any)).toMatchSnapshot() 24 | }) 25 | } 26 | ) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "moduleResolution": "Node", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | "noUncheckedIndexedAccess": true, 16 | "strict": true, 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false, 19 | "noFallthroughCasesInSwitch": true, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": ["./src/**/*.ts", "./src/**/*.tsx", "lib"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /vite.config.lib.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import dts from "vite-plugin-dts"; 4 | import { resolve } from "node:path"; 5 | import { name, peerDependencies } from "./package.json"; 6 | const formattedName = name.match(/[^/]+$/)?.[0] ?? name; 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | dts({ 11 | include: ["lib"], 12 | insertTypesEntry: true, 13 | }), 14 | ], 15 | mode: "lib", 16 | base: "./", 17 | build: { 18 | copyPublicDir: false, 19 | lib: { 20 | entry: resolve(__dirname, "lib/index.ts"), 21 | name: formattedName, 22 | formats: ["umd", "es"], 23 | fileName: (format) => `index.${format}.js`, 24 | }, 25 | rollupOptions: { 26 | external: ["react/jsx-runtime", ...Object.keys(peerDependencies ?? {})], 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { sentryVitePlugin } from "@sentry/vite-plugin"; 4 | 5 | export default defineConfig(({ mode }) => { 6 | const { SENTRY_AUTH_TOKEN } = Object.assign(process.env, loadEnv(mode, process.cwd(), '')) 7 | return { 8 | root: __dirname, 9 | base: './', 10 | plugins: [react(), sentryVitePlugin({ 11 | authToken: SENTRY_AUTH_TOKEN, 12 | org: "openchat-ai-e588264b7", 13 | project: "javascript-react" 14 | })], 15 | build: { 16 | sourcemap: true, 17 | }, 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['test/**/*.test.ts', 'test/**/*.test.tsx'], 6 | env: { 7 | NODE_ENV: 'test', 8 | }, 9 | }, 10 | }) --------------------------------------------------------------------------------