This commit is contained in:
2023-01-03 00:04:38 +00:00
commit 3f002872e3
925 changed files with 181049 additions and 0 deletions
@@ -0,0 +1,55 @@
# High level architecture of SwaggerEditor@5
This document briefly describes high level architecture of SwaggerEditor@5.
## Plugin architecture
SwaggerEditor@5 is composed of number of plugins. These plugins are divided into four
distinct categories
1. Plugins providing editor implementations
2. Plugins providing preview of editor content
3. Editor implementation support plugins
4. Generic features plugins
### Plugins providing editor implementations
These plugins include `editor-textarea` and `editor-moanco`.
`editor-textarea` is a base plugin that other editor implementations build on.
This plugin provides basic editing experience using `<textarea />` HTML tag.
`editor-monaco` builds on top of `editor-textarea` and provide advanced editing
experience using [Monaco Editor](https://microsoft.github.io/monaco-editor/).
### Plugins providing preview of editor content
These plugins include `editor-preview-swagger-ui`, `editor-preview-asyncapi` and others.
The single responsibility of editor preview plugin is to render the editor content (text)
to a set of UI components.
### Editor implementation support plugins
These plugins include `editor-persistence`, `editor-read-only` and others.
Plugins in this category are responsible to support plugins providing editor implementation
with additional enhanced capabilities in generic way.
### Generic features plugins
These plugins include `dialogs`, `modals`, `layout` and others. Each plugin in this
category provide features or enhancements to plugins in previous three categories.
## Future architectural changes
This section describes opportunities to make the architecture of SwaggerEditor@5 better.
### State management
Currently, editor content (text) is stored in SwaggerUI `spec` plugin. This causes various issues
which manifests in decreased user experience of editing text. Every time the editor content
changes the `spec` plugin will try to parse it as JSON or YAML, resolve it and store it in
redux state. This is absolutely not necessary. We can store editor content in editor implementation plugin
and have preview plugins work as finite state machines and detect if they should process the editor content in any way.
When this architectural change is implemented, the typing lag on big amount of editor content (text)
will significantly decrease.
@@ -0,0 +1,3 @@
# Plug points
- [editor-monaco](./editor-monaco.md)
@@ -0,0 +1,169 @@
# editor-monaco plug points
## Extending web worker capabilities
`editor-monaco` plugin is using monaco editor, which is using web workers to provide
editor capabilities. `editor-monaco` comes with a web worker called `apidom.worker`.
This worker contains all the language service utilizing ApiDOM capabilities.
`apidom.worker` can be extended in two ways: dynamic and static.
### Dynamic extension
Dynamic extension happens during runtime, and we recommend to use it only for simple use-cases.
First thing we need to do is to pass a `customApiDOMWorkerPath` option to the `EditorMonaco` plugin.
```js
EditorMonaco({
createData: {
customApiDOMWorkerPath: 'https://example.com/index.js',
},
})
```
`customApiDOMWorkerPath` is a URL (absolute or relative) of extending script. When the `apidom.worker`
is bootstrapping it, it imports this URL using [importScripts](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts).
The `apidom.worker` then expects the script to have the following signature:
**https://example.com/index.js**
```js
globalThis.customApiDOMWorkerFactory = (ApiDOMWorkerClass, toolbelt) => {
return ApiDOMWorkerClass;
};
```
The script must expose `customApiDOMWorkerFactory` function on global object. This function will
receive two arguments:
- ApiDOMWorkerClass - the class that implements the editor capabilities
- toolbelt - an object containing various library exports
Here is a simple **example** demonstrating changing the log level of language service:
```js
globalThis.customApiDOMWorkerFactory = (ApiDOMWorkerClass, toolbelt) => {
const { apidomLS } = toolbelt;
class ApiDOMWorkerLogLevelErrorClass extends ApiDOMWorkerClass {
static apiDOMContext = {
...ApiDOMWorkerClass.apiDOMContext,
logLevel: apidomLS.LogLevel.ERROR,
};
}
return ApiDOMWorkerLogLevelErrorClass;
};
```
### Static extension
Static extension involves need to use a build system like webpack.
**my-custom-apidom.worker.js**
```js
import { initialize, makeCreate, ApiDOMWorker } from '@swagger-api/swagger-editor/apidom.worker';
class ApiDOMWorkerExtended extends ApiDOMWorker {
// implementation of extensions
}
const create = makeCreate(ApiDOMWorkerExtended);
globalThis.onmessage = () => {
initialize((ctx, createData) => {
return create(ctx, createData);
});
};
export { initialize, create, makeCreate, ApiDOMWorkerExtended as ApiDOMWorker };
```
Next please have a look at the [usage section](../../../README.md#usage) of the documentation
specifically the **webpack.config.js** part. Given that we now extended the default worker,
we need to reconfigure the webpack and provide it with the path to the extended worker.
This is the part of the configuration that will change.
```js
entry: {
app: './index.js',
'apidom.worker': './my-custom-apidom.worker.js',
'editor.worker': '@swagger-api/swagger-editor/editor.worker',
}
```
## Passing data to web workers
Often when extending web worker capabilities it is the case that we need to pass additional
data to web worker. These data may include any arbitrary data compatible with
[the structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
Let's consider the following use-case. We need to extend the `apidom.worker` in a way that
it will be fetching data on demand from authorized REST endpoint.
### Dynamic extension
`EditorMonaco` plugin configuration.
```js
EditorMonaco({
createData: {
authToken: 'c32d8b45-92fe-44f6-8b61-42c2107dfe87',
customApiDOMWorkerPath: 'https://example.com/index.js',
},
})
```
**https://example.com/index.js**
```js
globalThis.customApiDOMWorkerFactory = (ApiDOMWorkerClass, toolbelt) => {
const { apidomLS } = toolbelt;
class ApiDOMWorkerLogLevelErrorClass extends ApiDOMWorkerClass {
static apiDOMContext = {
...ApiDOMWorkerClass.apiDOMContext,
logLevel: apidomLS.LogLevel.ERROR,
};
async loadData() {
// createData passed as plugin option is available in worker as this._createData
const { authToken } = this._createData;
return await fetch(`https://example.com/data?authToken=${authToken}`)
}
}
return ApiDOMWorkerLogLevelErrorClass;
};
```
### Static extension
Whenever you extend the `ApiDOMWorker` class you will have `_createData` public property available.
**my-custom-apidom.worker.js**
```js
import { initialize, makeCreate, ApiDOMWorker } from '@swagger-api/swagger-editor/apidom.worker';
class ApiDOMWorkerExtended extends ApiDOMWorker {
async loadData() {
// createData passed as plugin option is available in worker as this._createData
const { authToken } = this._createData;
return await fetch(`https://example.com/data?authToken=${authToken}`)
}
}
const create = makeCreate(ApiDOMWorkerExtended);
globalThis.onmessage = () => {
initialize((ctx, createData) => {
return create(ctx, createData);
});
};
export { initialize, create, makeCreate, ApiDOMWorkerExtended as ApiDOMWorker };
```
@@ -0,0 +1,133 @@
## Swagger Editor Legacy (v3/v4) Migration Summary
### Topbar Features
[x] Import from URL
[x] Import from File
[x] Save As Json
[x] Save As Yaml
[x] Convert from Json to Yaml, then download
[x] Convert from Yaml to Json, then download
[x] Convert from Json to Yaml, in editor only
[x] Clear Editor (moved from File Menu to Edit Menu)
[x] Convert from OAS2 to OAS3
[x] Generate Servers list - OAS2
[x] Generate Servers list - OAS3.0
[x] Generate Clients list - OAS2
[x] Generate Clients list - OAS3.0
[x] Display/Hide menu items based on combination of json/yaml, oas2/oas3
[x] NEW: consistent use of styled Modal components throughout (react-modal)
[x] NEW: Load default OAS2/OAS3/OAS3.1/AsyncApi2 definition; w/keyboard shortcuts
[x] Removed: topbar-insert (plugin); no replacement planned
### Editor Features
[x] NEW: Monaco Editor
[x] NEW: validation from apidom-ls library
[x] Removed: Ace Editor
[x] Removed: internal validation from swagger-editor v3/v4
[x] Drag-n-drop local file to editor (react-dropzone)
[x] Persist definition on browser refresh/reload (localStorage)
[x] Display validation pane results
### Visual UI Features
[x] SwaggerUI v4; React 17
### Implementation Changes
[x] React Hooks support, e.g. menu item display toggle, modal system
[x] Behavioral testing of components, e.g. not the implementation details
[x] Simplify and reorganize `presets`, `layouts`, and `plugins`
[x] Extract menu action methods from Components to Actions
[x] Extract menu logic helpers to Actions
[x] Extract shared menu logic helpers to `utils/editor-converter`
[x] Replace `fetch` with `axios` http helpers to `utils/topbar-http`
[x] deprecate plugins with direct implementation: Convert to OAS3, Import File
[x] Removed: unnecessary state where possible
[x] Removed: window.alert and window.confirm popups
[x] Removed: swagger-client library dependency
[x] Tests: testing-library + jest (unit), cypress (e2e)
### Possible additional/expanded docs/contributing topics
[x] Separation of actions from components. tldr: easier to test, possibly more re-usable
[x] React Hooks. tldr: modern practices
[x] testing-library. tldr: focus on user behaviors, more durable/maintainable tests
[x] reorganizing the presets. e.g. what happened to `standalone` and other plugins? The new recommended structure. tldr: multi-spec, multi-layouts
### List of migrated legacy methods
Note: likely to remove this section from final doc
Extract menu action methods from React to Actions
[x] importFromURL
[x] saveAsYaml
[x] saveAsJson
[x] convertToYaml
[x] downloadGeneratedFile
[x] importFile
[x] onDocumentLoad prop - removed. handled now in actions. Theoretically, we could expose as a user-overwritable function. Maybe SH needed it?
[x] updateEditorContent prop - will be removed, and handled in actions.
[x] clearEditor
Deprecate methods from React
[x] saveAsText
[x] handleResponse
Extract menu logic helpers to Actions
[x] getGeneratorUrl
[x] instantiateGeneratorClient
[x] shouldReInstantiateGeneratorClient (new)
Extract menu logic helpers to utils-converter
[x] hasParserErrors
[x] getFileName
[x] getDefinitionLanguage
[x] getDefinitionVersion
Create (axios) http method to utils-http
[x] getDefinitionFromUrl
[x] getGenerator2Definition
[x] postPerformOasConversion
Remove unnecessary state:
[x] swaggerClient
[x] definitionVersion
Remove use of alert and confirm via new modal system
[x] alert
[x] confirm
### Additional notes (remove from final doc)
[x] swagger-ui redux state
[x] connect monaco state to swagger-ui redux state
[x] connect actions with monaco state, e.g. updateEditorContent
[x] init oas spec via swagger-ui
[x] connect & sync generic-editor updates to swagger-ui
[x] init oas spec via import File (finish action) - Json
[x] init oas spec via import File (finish action) - Yaml
[x] init oas spec via import Url (finish action)
[x] CSS/Less/Saas styling
[x] remove use of mock data in topbarActions. (mock fixtures not removed yet)
[x] try react-modal lib instead of creating internal version
[x] modify generic-editor plugin to have a default editor placeholder (instead of calling GenericEditorContainer directly)
[x] add disposables array, and ability to dispose()
[x] make generator servers/clients toggleable; enable oas3.1/others when supported
[x] File Menu dropdown - add json/yaml detection to display appropriate link
[x] Edit menu dropdown - render convert to OAS3 only if currently 'isSwagger2`
[x] Edit menu dropdown - render convert to Yaml only if currently 'json'
[x] onboard - default definition should be in YAML format
### Resolved Issues (remove from final doc)
[x] ~~fix exploding styling/rendering when using in-browser (ctrl+f) find text within monaco; monaco's find+replace feature~~ USING P2M/M2P FIXES THIS ISSUE
[x] ~~warning: overlapping semantic tokens~~ THIS IS APIDOM-LS ISSUE
[x] fix editor configuration/onChange to always word-wrap.
[x] remove "dev mode" case when editor content is undefined
[x] remove mock data/fixtures/configuration from `actions.js`
[x] in codeActionUi, sometimes recieve an Uncaught promise TypeError: d.dispose is not a funtion
[x] asyncapi support: anywhere we should be detecting async api (or any other supported spec); e.g. clearEditor
[x] handle apidom parser throw, when unable to detect langugage. e.g. empty string, or oas2
[x] load default (oas3). note the topbar generate server/client exists. -> clear all. user may type random string. note that the topbar generate server/client did NOT disappear
[x] load default -> import OAS2. note the topbar generate server/client exists. -> clear all. user may type random string. note that the topbar generate server/client did NOT disappear
@@ -0,0 +1,30 @@
# Migration Status
* [Swagger Editor Legacy (v3/v4) Migration Summary](./migration-legacy-summary.md)
### Known Issues
[ ] handle case(s) when specSelectors.method returns undefined
### Integration Tasks
[ ] match and extend configurability options for editor & monaco
### Backlog
[ ] update "minimal" specs/fixtures for `clearEditor`; oas2, oas3, oas3_1, asycapi2
[ ] memory performance benchmarks
[ ] if refresh empty monaco, ~~should~~ MAY load a default definition
[ ] jump-to-path (plugin)
### Ideas
[ ] topbar: topbar-insert (plugin); this should be integrated into the `Topbar` plugin
[ ] topbar: update generator (plugin?) for oas3.1, asyncapi, etc. support when available
[ ] monaco: update syntax highlighting as needed
[ ] monaco - additional initial style and configuration of monaco editor, as needed
### Test Coverage Ideas
[ ] should add unit tests with bad/invalid urls; generator, import Url
[ ] should add unit tests when both swagger2 and oas3 flags set to same value (both true, both false)
[ ] should add unit tests allowing exclusion of both swagger2 and oas3, e.g. future asynapi, graphql, etc.
[ ] e2e: monaco-editor features
@@ -0,0 +1,134 @@
# Migrate from SwaggerEditor@4 to SwaggerEditor@next
## Table of Contents
- [Introduction](#introduction)
- [Development](#development)
- [Start script with hot-reloading](#start-script-with-hot-reloading)
- [Static build script](#static-build-script)
- [Local & remote definitions](#local--remote-definitions)
- [Linting](#linting)
- [Dependencies](#dependencies)
- [Testing](#testing)
- [Design](#design)
- [Plugins](#plugins)
- [plugins available at release](#plugins-available-at-release)
- [plugins not yet migrated](#plugins-not-yet-migrated)
- [Layout](#layout)
### Introduction
SwaggerEditor@next is a ground-up rewrite of SwaggerEditor@4. However, SwaggerEditor@4 is still fundamentally based on the SwaggerUI plugin system, and continues to be developed within the React ecosystem. For users without custom plugins or other forked customizations, SwaggerEditor@next is intended to be mostly drop-in compatible with the existing SwaggerEditor@4.
SwaggerEditor@4 introduces integration with ApiDOM and Microsoft's Monaco Editor. ApiDOM replaces the parser from SwaggerClient, as well as existing custom validation from SwaggerEditor@4. Monaco Editor replaces Ace Editor. Additional documentation on features and usage of [ApiDOM](https://github.com/swagger-api/apidom) and [Monaco Editor](https://github.com/microsoft/monaco-editor) can be found within their respective documention. Also available are the [ApiDOM Playground](https://swagger-api.github.io/apidom/) and [Monaco Editor Playground](https://microsoft.github.io/monaco-editor/playground.html)
This migration guide is intended to highlight setup changes and key customization features for SwaggerEditor@next.
### Development
SwaggerEditor@next is using **forked** Create React App as it's building infrastructure. Therefore there are a few minor differences to develop SwaggerEditor@next
#### Start script with hot-reloading
new:
```
$ npm start
```
old:
```
$ npm run dev
```
#### Static build script
new:
```
$ npm run build:app
$ npm run build:app:serve
```
old:
```
$ npm start
```
#### Local & remote definitions
Only files inside `public` can be used from `public/index.html`. JS files must be put inside `src`.
new:
```
<SwaggerEditor url={url} />
```
old:
In `dev-helpers/index.html`, add a `url` key like this:
```
const editor = SwaggerEditorBundle({
dom_id: '#swagger-editor',
url: "some-local-or-remote-path-to-definition.yaml" # define path here
layout: 'StandaloneLayout',
presets: [
SwaggerEditorStandalonePreset
]
})
```
#### Linting
Automatic linting checks remain part of the commit process. SwaggerEditor@4 now generally follows the eslint recommendations from the AirBnb and React teams.
There is currently a known issue between compatibility of `eslint-config-airbnb` and `eslint-config-react-app`. The workaround is to set `DISABLE_ESLINT_PLUGIN=false` in the npm script.
#### Dependencies
SwaggerEditor@next is now based on `swagger-ui-react` instead of `swagger-ui`. In addition, unlike SwaggerEditor@4, SwaggerEditor@next does not have a dependency on `SwaggerClient`.
#### Testing
Unit tests are now covered by `jest` and `@testing-library`. Unit tests are now co-located next to their component. Component tests follow the `@testing-library` philosophy to avoid including implementation details, so that changes to implementation but not functionality doesn't break existing tests.
E2E Cypress tests remain in the `test/cypress` directory.
### Design
#### Plugins
SwaggerEditor@next maintains its core as an extension of SwaggerUI's plugin system. SwaggerEditor@next exports itself as a fully realized set of plugins, each of which can be extended and wrapped as needed. This even includes the new Monaco Editor feature as a plugin!
Compared to SwaggerEditor@4, there is also no change to precedence with regards to the order of loading plugins.
SwaggerEditor@next provides a `modals` plugin for a unified modal system. The `modals` plugin is further extended via the `dialogs` plugin which provides a set of `alert` and `confirm` modal dialogs.
Any existing custom plugins for SwaggerEditor@4 that modified the behavior of, or directly interfaced with, the Ace Editor will likely need to be heavily refactored. Custom validation rules should be migrated to ApiDOM as needed.
##### plugins available at release
- `dialogs`
- `dropzone`
- `editor-monaco`
- `editor-persistence`
- `editor-preview-asyncapi`
- `editor-preview-swagger-ui`
- `editor-read-only`
- `editor-spec-origin`
- `editor-textarea`
- `layout`
- `modals`
- `topbar`
##### plugins not yet migrated
At the time of this writing there are two plugins from SwaggerEditor@4 that have not been migrated:
1. `jump-to-path`. Note: expect to leverage ApiDOM and `editor-monaco` plugin to replace AST and Ace Editor.
2. `topbar-insert`. Note: expect to leverage and wrap components of various existing plugins: `topbar`, `editor-monaco`, `modals`, and `dialogs`.
#### Layout
SwaggerEditor@next comes with a plugin for a "core" layout. The "core" layout provides plug points that are analogous to "panes" and/or "bars" in other IDEs. These plug points make it easier for developers to create plugins that customize their UX with as minimal changes to the "core" layout as possible.
The "core" layout includes contains `EditorPane`, `EditorPreviewPane`, and `Topbar`. These three components represent the basic UX wireframe for SwaggerEditor@next. In addition, the `EditorPane` is provided with four surrounding `bars` (top, bottom, left, right) that can each be customized with their own wrapped implementations.
Using itself as a reference design, SwaggerEditor@next provides implementation of two different `EditorPane` (`editor-monaco` and `editor-textarea`), and two different `EditorPreviewPane` (`editor-preview-swagger-ui` and `editor-preview-asyncapi). The `editor-monaco` plugin also further extends the base `EditorPaneBarTop` and `EditorPaneBarBottom` with its own wrapped version of each.
@@ -0,0 +1,24 @@
Monaco Client (React)
- setup a Monaco instance
- setup monaco environment use of language worker(s) to use
- instantiate web workers
- register providers via Adapters
- create a Monaco instance
Adapter
- an adapter for each service or provider
- generally expect a `provide[Description]` method that matches monaco interface
- offloads language service functions to language web worker
- run and/or return a monaco editor "effect". e.g. hover, completion, based on web worker result
- not required to use Typescript
WebWorker
- called via an adapter
- call a language service method in separate thread
- recommended use by Monaco
LanguageService
- called via a web worker
- any lib, in our case, apidom
- examples include typescript/javascript, json, css
notes:
- some minor naming differences across libs
@@ -0,0 +1,31 @@
Swagger-client is used as middleware to transform `generator` responses,
which also includes function methods alongside data objects.
fetch GET call to `SwaggerClient(url, [options])` will return `response.apis` methods.
- reminder to attach `.catch()` to any `response.apis.[method]` call
```
response.apis.clients:
- clientOptions: ƒ(parameters)
- downloadFile: ƒ(parameters)
- generateClient: ƒ(parameters)
- getClientOptions: ƒ(parameters)
response.apis.servers:
- downloadFile: ƒ(parameters)
- generateServerForLanguage: ƒ(parameters)
- getServerOptions: ƒ(parameters)
- serverOptions: ƒ(parameters)
```
### Summary
In order to download a generated definition, `legacy swagger-editor` made two calls to swagger-client.
1. Given OAS3 or Swagger2/OAS2 definition, swagger-editor makes a request to Generator3 or Generator2 respectively.
Generator3/Generator2 returns a response that includes it's own definition, which `swagger-client`, as middleware, translates the defintion included in Generator's response into a more usable `response.apis.clients` format. This format also includes new `execute` methods for `swagger-editor` to call.
2. `swagger-editor` calls the appropriate `execute` method that was created by `swagger-client` to download the generated definition. Note, Generator3 returns the Blob directly, while Generator2 returns a json object containing an URI link to the Blob.
Additionally, `legacy swagger-editor` saved these methods to React state for general purpose use, e.g. downloadFile vs clientOptions.
In this new version of swagger-editor, use of `swagger-client` has been deprecated, so these methods are no longer available or saved. Equivalent functionality to directly interface with Generator3/Generator2 now exists in `topbarActions`. This change also should now always yield a deterministic final result.