├── .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 | 
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 | 
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 |
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 |
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 |
32 | ));
33 |
34 | MethodBtn.displayName = "MethodBtn";
35 |
--------------------------------------------------------------------------------
/lib/editor/PathButton.tsx:
--------------------------------------------------------------------------------
1 | import { useReactFlow, type Node, Edge } from "reactflow";
2 | import { MethodBtn } from "./MethodRenderer";
3 | import { Y } from "./consts";
4 | import { useMode } from "../stores/ModeProvider";
5 | import { useCallback } from "react";
6 | import { useController } from "../stores/Controller";
7 | import { Method, NodeData, TransformedPath } from "../types/Swagger";
8 | import {
9 | Tooltip,
10 | TooltipContent,
11 | TooltipProvider,
12 | TooltipTrigger,
13 | } from "../components";
14 | import { genId, updateNodesPositions } from "../utils";
15 |
16 | export function PathButton({ path }: { path: TransformedPath }) {
17 | const { mode } = useMode();
18 | const { setNodes, getNodes, setEdges } = useReactFlow();
19 | const nodes = getNodes();
20 | const {
21 | state: { activeFlowId },
22 | } = useController();
23 | const appendNode = useCallback(
24 | (payload: NodeData) => {
25 | const id = genId();
26 | const newNode: Node = {
27 | id: id,
28 | type: "endpointNode",
29 | data: payload,
30 | draggable: false,
31 | position: { x: 0, y: Y * nodes.length },
32 | };
33 | setNodes((nds) => updateNodesPositions([...nds, newNode], Y));
34 | },
35 | [nodes.length, setNodes]
36 | );
37 |
38 | const addNodeBetween = useCallback(
39 | (edge: Edge, payload: NodeData) => {
40 | const targetNode = nodes.find((node) => node.id === edge.target);
41 | const sourceNode = nodes.find((node) => node.id === edge.source);
42 | if (!targetNode || !sourceNode) {
43 | return;
44 | }
45 | // delete the edge
46 | setEdges((eds) => eds.filter((ed) => ed.id !== edge.id));
47 | // add the new node
48 | const id = genId();
49 | const newNode: Node = {
50 | id: id,
51 | type: "endpointNode",
52 | data: payload,
53 | draggable: false,
54 | position: {
55 | x: 0,
56 | y: (sourceNode.position.y + targetNode.position.y) / 2,
57 | },
58 | };
59 | // put the new node in the middle of the two nodes that were connected (make sure the node is sorted in array too)
60 | const sourceIndex = nodes.findIndex((node) => node.id === sourceNode.id);
61 | const newNodes = nodes
62 | .slice(0, sourceIndex)
63 | .concat(newNode)
64 | .concat(nodes.slice(sourceIndex));
65 | setNodes(updateNodesPositions(newNodes, Y));
66 | },
67 | [nodes, setEdges, setNodes]
68 | );
69 |
70 | const isPresentInNodes = useCallback(
71 | (method: Method) => {
72 | return !!nodes.find((node) => {
73 | return (
74 | node.data.path === path.path &&
75 | node.data.method.toLowerCase() === method.toLowerCase()
76 | );
77 | });
78 | },
79 | [nodes, path]
80 | );
81 |
82 | return (
83 |
84 |
85 | {path.path}
86 |
87 |
88 | {path.methods.map((method, i) => {
89 | return (
90 |
91 | {method.description}
92 |
93 | {
99 | if (isPresentInNodes(method.method)) {
100 | return;
101 | }
102 | if (!activeFlowId) {
103 | alert("Please create a flow first");
104 | return;
105 | }
106 | const newNode: NodeData = {
107 | path: path.path,
108 | ...method,
109 | };
110 | if (mode.type === "append-node") {
111 | appendNode(newNode);
112 | } else if (mode.type === "add-node-between") {
113 | addNodeBetween(mode.edge, newNode);
114 | }
115 | }}
116 | >
117 | {method.method.toUpperCase()}
118 |
119 |
120 |
121 | );
122 | })}
123 |
124 |
125 |
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/lib/editor/consts.ts:
--------------------------------------------------------------------------------
1 | export const Y = 150;
2 | export const nodedimensions = {
3 | width: 250,
4 | height: 100,
5 | }
6 | export const BUILDER_SCALE = 1.25;
--------------------------------------------------------------------------------
/lib/index.css:
--------------------------------------------------------------------------------
1 | @tailwind components;
2 | @tailwind utilities;
3 |
4 | svg {
5 | width: 1em;
6 | height: 1em;
7 | aspect-ratio: 1/1;
8 | }
9 | .react-flow__node {
10 | @apply !transition-all !ease-out !duration-300 origin-center;
11 | }
12 |
13 | .btn,
14 | .a {
15 | @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;
16 | }
17 | .small {
18 | @apply px-2 py-1;
19 | }
20 | .large {
21 | @apply px-4 py-3;
22 | }
23 | .danger {
24 | @apply text-white bg-rose-500 hover:bg-opacity-80 hover:border-opacity-80;
25 | }
26 | .secondary {
27 | @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;
28 | }
29 | /* wrapper */
30 | .wrapper {
31 | @apply border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 rounded-sm border shadow-lg;
32 | }
33 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | import "./index.css";
2 | export { FlowArena } from "./editor/FlowArena";
3 | export { CodePreview } from "./editor/CodePreview";
4 | export { Controller, useController } from "./stores/Controller";
5 | export type { Paths, TransformedPath } from "./types/Swagger";
6 | export type { Flow, FlowSchema, Step } from "./types/Flow";
7 | // some exported utils
8 | export {
9 | transformaEndpointToNode,
10 | trasnformEndpointNodesData,
11 | } from "./utils/transformEndpointNodes";
12 | export { transformPaths } from "./utils/transformSwagger";
--------------------------------------------------------------------------------
/lib/stores/Config.ts:
--------------------------------------------------------------------------------
1 | import { createSafeContext } from "../utils/create-safe-context";
2 | export type Settings = {
3 | standalone?: boolean;
4 | maxFlows?: number;
5 | };
6 |
7 | const [SettingsProvider, useSettings] = createSafeContext(
8 | {} as Settings
9 | );
10 |
11 | export { SettingsProvider, useSettings };
12 |
--------------------------------------------------------------------------------
/lib/stores/Controller.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useReducer,
3 | type ReactNode,
4 | useCallback,
5 | useMemo,
6 | useEffect,
7 | } from "react";
8 | import { produce } from "immer";
9 | import { Node } from "reactflow";
10 | import { ModeProvider } from "./ModeProvider";
11 | import { TransformedPath } from "../types/Swagger";
12 | import { createSafeContext } from "../utils/create-safe-context";
13 | import { genId } from "../utils";
14 | import { ReactFlowProvider } from "reactflow";
15 | import { EndpointNodeType, FlowSchema, FlowType } from "../types/Flow";
16 | import { Settings, SettingsProvider } from "./Config";
17 | import { getDef } from "../utils/getDef";
18 |
19 | type StateShape = {
20 | paths: TransformedPath[];
21 | activeFlowId?: string;
22 | flows: FlowType[];
23 | };
24 |
25 | type ControllerContextType = {
26 | loadPaths: (paths: TransformedPath[]) => void;
27 | state: StateShape;
28 | activeNodes?: EndpointNodeType[];
29 | createFlow: (data: CreateFlowPayload) => void;
30 | setActiveFlow: (id: string) => void;
31 | setNodes: (nodes: Node[]) => void;
32 | reset: () => void;
33 | deleteFlow: (id: string) => void;
34 | getData: () => ReturnType;
35 | };
36 |
37 | type ActionType =
38 | | { type: "reset" }
39 | | { type: "load-paths"; pyload: TransformedPath[] }
40 | | { type: "set-active-flow"; pyload: string }
41 | | {
42 | type: "create-flow";
43 | pyload: {
44 | name: string;
45 | description?: string;
46 | createdAt: number;
47 | focus: boolean;
48 | };
49 | }
50 | | {
51 | type: "delete-flow";
52 | pyload: string;
53 | }
54 | | { type: "set-flows"; pyload: StateShape["flows"] }
55 | | { type: "set-nodes"; payload: Node[] };
56 |
57 | type CreateFlowPayload = Extract["pyload"];
58 |
59 | const initialStateValue: StateShape = {
60 | paths: [],
61 | flows: [],
62 | activeFlowId: undefined,
63 | };
64 | const [SafeProvider, useController] = createSafeContext(
65 | {} as ControllerContextType
66 | );
67 | function stateReducer(state: StateShape, action: ActionType) {
68 | if (action.type === "reset") return initialStateValue;
69 | return produce(state, (draft) => {
70 | switch (action.type) {
71 | case "load-paths":
72 | draft.paths = action.pyload;
73 | break;
74 | case "set-active-flow":
75 | draft.activeFlowId = action.pyload;
76 | break;
77 | case "create-flow": {
78 | const id = genId();
79 | const $newFlow = {
80 | ...action.pyload,
81 | steps: [],
82 | id,
83 | };
84 | draft.flows.push($newFlow);
85 | if (action.pyload.focus) draft.activeFlowId = id;
86 | break;
87 | }
88 | case "set-nodes":
89 | {
90 | const flow = draft.flows.find((f) => f.id === state.activeFlowId);
91 | if (!flow) return;
92 | flow.steps = action.payload;
93 | flow.updatedAt = Date.now();
94 | }
95 | break;
96 | case "delete-flow":
97 | draft.flows = draft.flows.filter((f) => f.id !== action.pyload);
98 | if (draft.activeFlowId === action.pyload) {
99 | draft.activeFlowId = undefined;
100 | }
101 | break;
102 | default:
103 | break;
104 | }
105 | });
106 | }
107 |
108 | function Controller({
109 | children,
110 | onChange,
111 | initialState,
112 | ...settings
113 | }: {
114 | children: ReactNode;
115 | initialState?: StateShape;
116 | onChange?: (state: StateShape) => void;
117 | } & Settings) {
118 | const [state, dispatch] = useReducer(
119 | stateReducer,
120 | initialState ? initialState : initialStateValue
121 | );
122 |
123 | useEffect(() => {
124 | onChange?.(state);
125 | }, [state, onChange]);
126 | const loadPaths = useCallback(
127 | (paths: TransformedPath[]) =>
128 | dispatch({
129 | type: "load-paths",
130 | pyload: paths,
131 | }),
132 | []
133 | );
134 | const createFlow = useCallback((data: CreateFlowPayload) => {
135 | if (settings.maxFlows && state.flows.length >= settings.maxFlows) return;
136 | dispatch({
137 | type: "create-flow",
138 | pyload: data,
139 | });
140 | }, []);
141 | const setActiveFlow = useCallback(
142 | (id: string) =>
143 | dispatch({
144 | type: "set-active-flow",
145 | pyload: id,
146 | }),
147 | []
148 | );
149 | const activeNodes = useMemo(() => {
150 | if (!state.activeFlowId) return undefined;
151 | const flow = state.flows.find((f) => f.id === state.activeFlowId);
152 | if (!flow) return undefined;
153 | return flow.steps;
154 | }, [state]);
155 |
156 | const setNodes = useCallback(
157 | (nodes: Node[]) =>
158 | dispatch({
159 | type: "set-nodes",
160 | payload: nodes,
161 | }),
162 | []
163 | );
164 | const deleteFlow = useCallback(
165 | (id: string) =>
166 | dispatch({
167 | type: "delete-flow",
168 | pyload: id,
169 | }),
170 | []
171 | );
172 | // TODO: @bug: when we reset, the nodes(in the arena) are not reset
173 | const reset = useCallback(() => dispatch({ type: "reset" }), []);
174 | const getData = useCallback(() => getDef(state.flows), [state]);
175 |
176 | return (
177 |
178 |
179 |
180 |
193 | {children}
194 |
195 |
196 |
197 |
198 | );
199 | }
200 |
201 | // eslint-disable-next-line react-refresh/only-export-components
202 | export { useController, Controller };
203 |
--------------------------------------------------------------------------------
/lib/stores/ModeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { type Edge, type Node } from "reactflow";
2 | import { useCallback, useMemo, useState } from "react";
3 | import { NodeData } from "../types/Swagger";
4 | import { createSafeContext } from "../utils/create-safe-context";
5 | type IdleMode = {
6 | type: "idle";
7 | };
8 | type AppendNodeMode = {
9 | type: "append-node";
10 | };
11 | type AddNodeBetweenMode = {
12 | type: "add-node-between";
13 | edge: Edge;
14 | };
15 | type EditNodeMode = {
16 | type: "edit-node";
17 | node: Node;
18 | };
19 |
20 | export type Mode =
21 | | AppendNodeMode
22 | | AddNodeBetweenMode
23 | | EditNodeMode
24 | | IdleMode;
25 | const DEFAULT: Mode = { type: "append-node" };
26 | type ModeContextType = {
27 | mode: Mode;
28 | setMode: React.Dispatch>;
29 | reset: () => void;
30 | isAdd: boolean;
31 | isEdit: boolean;
32 | isIdle: boolean;
33 | };
34 |
35 | const [ModeSafeProvider, useMode] = createSafeContext(
36 | {} as ModeContextType
37 | );
38 | function ModeProvider({ children }: { children: React.ReactNode }) {
39 | const [mode, $setMode] = useState(DEFAULT);
40 | const reset = useCallback(() => $setMode(DEFAULT), []);
41 | const setMode = useCallback($setMode, [$setMode]);
42 | const isAdd = useMemo(
43 | () => mode.type === "append-node" || mode.type === "add-node-between",
44 | [mode]
45 | );
46 | const isIdle = useMemo(() => mode.type === "idle", [mode]);
47 | const isEdit = useMemo(() => mode.type === "edit-node", [mode]);
48 | return (
49 |
50 | {children}
51 |
52 | );
53 | }
54 |
55 | // eslint-disable-next-line react-refresh/only-export-components
56 | export { ModeProvider, useMode };
57 |
--------------------------------------------------------------------------------
/lib/types/Flow.ts:
--------------------------------------------------------------------------------
1 | import type { Node } from "reactflow";
2 | import type { NodeData } from "./Swagger";
3 |
4 | export type Step = {
5 | stepId?: string;
6 | operation: string;
7 | open_api_operation_id: string;
8 | parameters?: Record[];
9 | };
10 |
11 | export type Flow = {
12 | name: string;
13 | description?: string;
14 | requires_confirmation: boolean;
15 | steps: Step[];
16 | };
17 |
18 | export type FlowSchema = {
19 | opencopilot: string;
20 | info: {
21 | title: string;
22 | version: string;
23 | };
24 | flows: Flow[];
25 | on_success: {
26 | handler: string;
27 | }[];
28 | on_failure: {
29 | handler: string;
30 | }[];
31 | };
32 | export type FlowType = {
33 | id: string;
34 | name: string;
35 | description?: string;
36 | createdAt: number;
37 | updatedAt?: number;
38 | steps: EndpointNodeType[];
39 | };
40 | export type EndpointNodeType = Node;
41 |
--------------------------------------------------------------------------------
/lib/types/Swagger.ts:
--------------------------------------------------------------------------------
1 | interface Info {
2 | title: string;
3 | version: string;
4 | }
5 |
6 | interface Response {
7 | description: string;
8 | content: {
9 | [contentType: string]: any;
10 | };
11 | }
12 |
13 | interface RequestBody {
14 | required?: boolean;
15 | content: {
16 | [contentType: string]: {
17 | schema: any;
18 | };
19 | };
20 | }
21 |
22 | // Operation Object
23 | interface Operation {
24 | tags?: string[];
25 | summary?: string;
26 | description?: string;
27 | operationId?: string;
28 | parameters?: Array<{
29 | name: string;
30 | in: "query" | "header" | "path" | "cookie";
31 | }>;
32 | requestBody?: RequestBody;
33 | responses: {
34 | [statusCode: string]: Response;
35 | };
36 | }
37 |
38 | export const methods = [
39 | "get",
40 | "post",
41 | "put",
42 | "delete",
43 | "options",
44 | "head",
45 | "patch",
46 | "trace",
47 | ] as const;
48 | export type Method = (typeof methods)[number];
49 | type PathItem = Record;
50 | // Paths Object
51 | export type Paths = Record;
52 | // Definitions Object (Schema)
53 | interface Definitions {
54 | [name: string]: any; // You can define more specific types based on your needs
55 | }
56 |
57 | // The main Swagger Object
58 | export interface Swagger<
59 | TPaths extends Paths = Paths,
60 | TDefinitions extends Definitions = Definitions,
61 | > {
62 | openapi: string;
63 | info: Info;
64 | servers?: Array<{
65 | url: string;
66 | description?: string;
67 | }>;
68 | tags?: Array<{
69 | name: string;
70 | description?: string;
71 | }>;
72 | paths: TPaths;
73 | definitions?: TDefinitions;
74 | }
75 |
76 | // transformation types
77 |
78 | export type ExtendedOperation = Omit<
79 | Operation,
80 | "responses" | "requestBody" | "parameters"
81 | > & {
82 | method: Method;
83 | };
84 |
85 | export type TransformedPath = {
86 | path: string;
87 | methods: ExtendedOperation[];
88 | };
89 |
90 | export type NodeData = ExtendedOperation & {
91 | path: string;
92 | };
93 |
--------------------------------------------------------------------------------
/lib/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 |
3 | /**
4 | * @description
5 | * A utility function to merge tailwind classes.
6 | * used for conditional merging of tailwind classes.
7 | * @param classNames - An array of tailwind classes
8 | * @returns A string of tailwind classes.
9 | * @example
10 | * cn('text-red-500', 'text-red-600') => text-red-600
11 | * cn('text-red-500', undefined, 'bg-red-500')
12 | * **/
13 | export default function cn(...classNames: Array): string {
14 | return twMerge(classNames)
15 | }
--------------------------------------------------------------------------------
/lib/utils/create-safe-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 |
3 | export function createSafeContext(defaultValue: DT) {
4 | const Context = createContext(defaultValue);
5 | const SafeProvider = Context.Provider;
6 | const useSafeContext = () => {
7 | const context = useContext(Context);
8 | if (context === undefined) {
9 | throw new Error("useSafeContext must be used within a SafeProvider");
10 | }
11 | return context;
12 | };
13 | return [SafeProvider, useSafeContext] as const;
14 | }
15 |
--------------------------------------------------------------------------------
/lib/utils/genId.ts:
--------------------------------------------------------------------------------
1 | export function genId() {
2 | return Date.now().toString(36) + Math.random().toString(36).slice(2);
3 | }
4 |
--------------------------------------------------------------------------------
/lib/utils/getDef.ts:
--------------------------------------------------------------------------------
1 | import type { FlowType } from "../types/Flow";
2 | import { trasnformEndpointNodesData } from "./transformEndpointNodes";
3 |
4 | export function getDef(flows: FlowType[]) {
5 | return {
6 | opencopilot: "0.1",
7 | info: {
8 | title: "My OpenCopilot definition",
9 | version: "1.0.0",
10 | },
11 | flows: flows.map((flow) => {
12 | return {
13 | name: flow.name,
14 | description: flow.description,
15 | steps: trasnformEndpointNodesData(flow.steps),
16 | requires_confirmation: true,
17 | on_success: [{}],
18 | on_failure: [{}],
19 | };
20 | }),
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/lib/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | export function parse(value: string) {
2 | if (value === null || value === undefined) return undefined;
3 | try {
4 | return JSON.parse(value);
5 | } catch (error) {
6 | return value;
7 | }
8 | }
9 | export function stringify(value: any): string {
10 | try {
11 | return JSON.stringify(value);
12 | } catch (error) {
13 | return value || undefined;
14 | }
15 | }
16 | export function getStorageValue(key: string, initialValue: any) {
17 | const value = window.localStorage.getItem(key);
18 | if (value) {
19 | return parse(value);
20 | }
21 | return initialValue;
22 | }
23 | export function setStorageValue(key: string, value: any) {
24 | localStorage.setItem(key, stringify(value));
25 | }
26 |
--------------------------------------------------------------------------------
/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { genId } from './genId';
2 | export { default as cn } from './cn';
3 | export { transformPaths } from './transformSwagger';
4 | export { updateNodesPositions } from './updateNodePosition';
5 | export { parse, getStorageValue, setStorageValue, stringify } from './helpers';
--------------------------------------------------------------------------------
/lib/utils/transformEndpointNodes.ts:
--------------------------------------------------------------------------------
1 | import { Y } from "../editor/consts";
2 | import type { EndpointNodeType } from "../types/Flow";
3 | import { genId } from "./genId";
4 | export function trasnformEndpointNodesData(nodes: EndpointNodeType[]) {
5 | return nodes
6 | .map((node) => node.data)
7 | .map((data) => ({
8 | operation: "call",
9 | stepId: data.operationId,
10 | open_api_operation_id: data.operationId,
11 | }));
12 | }
13 | // the reverse of the above function
14 | export function transformaEndpointToNode(
15 | data: EndpointNodeType["data"][]
16 | ): EndpointNodeType[] {
17 | return data.map((nodeData, index) => ({
18 | id: genId(),
19 | type: "endpointNode",
20 | draggable: false,
21 | position: {
22 | x: 0,
23 | y: index * Y,
24 | },
25 | data: nodeData,
26 | }));
27 | }
28 |
--------------------------------------------------------------------------------
/lib/utils/transformSwagger.ts:
--------------------------------------------------------------------------------
1 | import type { ExtendedOperation, Method, Paths, TransformedPath } from "../types/Swagger";
2 | import { methods as methodsArray } from "../types/Swagger";
3 |
4 | /**
5 | * @description Transforms the paths object from the swagger file into a more usable format
6 | */
7 | export function transformPaths(paths: Paths): TransformedPath[] {
8 | const trasnformedPaths = new Set();
9 | Object.keys(paths).forEach((pathString) => {
10 | const endpoint = paths[pathString];
11 | const methods = new Set()
12 | endpoint && Object.keys(endpoint).forEach((method) => {
13 | if (methodsArray.includes(method as Method)) {
14 | const operation = endpoint[method as Method];
15 | operation && methods.add({
16 | method: method as Method,
17 | ...operation
18 | });
19 | }
20 |
21 | });
22 | trasnformedPaths.add({
23 | path: pathString,
24 | methods: Array.from(methods)
25 | });
26 | });
27 | return Array.from(trasnformedPaths);
28 | }
--------------------------------------------------------------------------------
/lib/utils/updateNodePosition.ts:
--------------------------------------------------------------------------------
1 | import type { Node } from 'reactflow';
2 | export function updateNodesPositions(nodes: Node[], Y: number): Node[] {
3 | // Choose a suitable distance
4 | const updatedNodes = nodes.map((node, index) => ({
5 | ...node,
6 | position: { x: 0, y: index * Y },
7 | }));
8 | return updatedNodes;
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openchatai/copilot-flows-editor",
3 | "version": "1.6.0",
4 | "private": false,
5 | "description": "OpenChat Flow Builder for openCopilot",
6 | "type": "module",
7 | "main": "./dist/index.umd.js",
8 | "types": "./dist/index.d.ts",
9 | "module": "./dist/index.es.js",
10 | "exports": {
11 | "./dist/style.css": "./dist/style.css",
12 | ".": {
13 | "import": "./dist/index.es.js",
14 | "require": "./dist/index.umd.js",
15 | "types": "./dist/index.d.ts"
16 | }
17 | },
18 | "scripts": {
19 | "test:watch": "vitest",
20 | "test": "vitest --run",
21 | "dev": "vite",
22 | "build": "tsc && vite build",
23 | "build:lib": "vite build -c vite.config.lib.ts",
24 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
25 | "preview": "vite preview",
26 | "prepublishOnly": "npm run test && npm run build:lib",
27 | "semantic-release": "semantic-release",
28 | "cs": "changeset",
29 | "cs:v": "changeset version"
30 | },
31 | "peerDependencies": {
32 | "@radix-ui/react-alert-dialog": "^1.0.4",
33 | "@radix-ui/react-dialog": "^1.0.4",
34 | "@radix-ui/react-tooltip": "^1.0.6",
35 | "react": "^18.x",
36 | "react-dom": "^18.x"
37 | },
38 | "devDependencies": {
39 | "@changesets/cli": "^2.26.2",
40 | "@codemirror/autocomplete": "^6.9.0",
41 | "@codemirror/lang-json": "^6.0.1",
42 | "@codemirror/lint": "^6.4.1",
43 | "@codemirror/view": "^6.17.1",
44 | "@fontsource-variable/open-sans": "^5.0.13",
45 | "@radix-ui/react-icons": "^1.3.0",
46 | "@sentry/react": "^7.69.0",
47 | "@sentry/vite-plugin": "^2.7.1",
48 | "@types/js-beautify": "^1.14.0",
49 | "@types/node": "^20.6.0",
50 | "@types/react": "^18.2.21",
51 | "@types/react-dom": "^18.2.7",
52 | "@typescript-eslint/eslint-plugin": "^6.5.0",
53 | "@typescript-eslint/parser": "^6.5.0",
54 | "@uiw/codemirror-extensions-basic-setup": "^4.21.13",
55 | "@uiw/codemirror-themes-all": "^4.21.13",
56 | "@uiw/react-codemirror": "^4.21.13",
57 | "@vitejs/plugin-react": "^4.0.4",
58 | "ajv": "^8.12.0",
59 | "autoprefixer": "^10.4.15",
60 | "eslint": "^8.48.0",
61 | "eslint-plugin-react-hooks": "^4.6.0",
62 | "eslint-plugin-react-refresh": "^0.4.3",
63 | "immer": "^10.0.2",
64 | "js-beautify": "^1.14.9",
65 | "postcss": "^8.4.29",
66 | "prettier": "^3.0.3",
67 | "react": "^18.2.0",
68 | "react-dom": "^18.2.0",
69 | "reactflow": "^11.8.3",
70 | "tailwind-merge": "^1.14.0",
71 | "tailwindcss": "^3.3.3",
72 | "tailwindcss-animate": "^1.0.7",
73 | "typescript": "^5.2.2",
74 | "vite": "^4.4.9",
75 | "vite-plugin-dts": "^3.5.4",
76 | "vitest": "^0.34.5"
77 | },
78 | "dependencies": {
79 | "@radix-ui/react-alert-dialog": "^1.0.4",
80 | "@radix-ui/react-dialog": "^1.0.4",
81 | "@radix-ui/react-tooltip": "^1.0.6"
82 | },
83 | "files": [
84 | "dist"
85 | ],
86 | "sideEffects": [
87 | "**/*.css"
88 | ],
89 | "repository": {
90 | "type": "git",
91 | "url": "https://github.com/ah7255703/schema-parser-ui.git"
92 | }
93 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/logo-opencopilot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openchatai/copilot-flows-editor/c0e5137ae0e224cb81f5c070ae9da777dfc05686/public/logo-opencopilot.png
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { GitHubLogoIcon, ResetIcon } from "@radix-ui/react-icons";
2 | import { CodePreview, Controller, FlowArena, useController } from "../lib";
3 |
4 | function Header() {
5 | const {
6 | reset,
7 | state: { activeFlowId, flows },
8 | } = useController();
9 | const activeFlow = flows.find((f) => f.id === activeFlowId);
10 | return (
11 |
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 | })
--------------------------------------------------------------------------------