Added Swagger

This commit is contained in:
Sebastian Davaris 2020-06-10 08:25:21 +02:00
parent 5c6f37eaf7
commit af76cbca87
257 changed files with 48861 additions and 12 deletions

10
ROUTES.md Normal file
View File

@ -0,0 +1,10 @@
# Route Documentation
### [GET] /
Returns the index page...
### [GET] /users
Shows a list of all the users.
[REQUIREMENTS]
The user must be authenticated!

View File

@ -3,60 +3,86 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\User;
use Illuminate\Support\Facades\Auth;
class UserController extends Controller class UserController extends Controller
{ {
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @return \Illuminate\Http\Response * @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function index() public function index(Request $request)
{ {
// $users = User::query()->paginate($request->query("page", 1));
return view("user.index", [ "users" => $users ]);
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function create() public function create()
{ {
// return view("user.create");
} }
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function store(Request $request) public function store(Request $request)
{ {
// $data = $request->validate([
"name_first" => "required|max:255",
"name_last" => "required|max:255",
"email" => "required|email|unique:users",
"password" => "required|max:60",
"phone" => "required|unique:users",
]);
$user = new User($data);
$user->save();
return view("user.success");
} }
/** /**
* Display the specified resource. * Display the specified resource.
* *
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function show($id) public function show($id)
{ {
// $user = User::find($id);
return view("user.show", [
"user" => $user
]);
} }
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function edit($id) public function edit($id)
{ {
// $user = User::find($id);
return view("user.edit", [
"user" => $user
]);
} }
/** /**
@ -81,4 +107,18 @@ class UserController extends Controller
{ {
// //
} }
/*******************************************/
/* Authentication */
/*******************************************/
public function login(Request $request) {
$data = $request->only("email", "password");
if(Auth::attempt($data)) {
return view("user.home", [ "user" => Auth::user() ]);
}
return redirect()->back(400);
}
} }

View File

@ -20,7 +20,7 @@ class User extends Authenticatable
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'name', 'email', 'password', 'name_first', "name_last", 'email', 'password', "phone"
]; ];
/** /**

View File

@ -17,4 +17,8 @@ use Illuminate\Support\Facades\Route;
// return view('welcome'); // return view('welcome');
//}); //});
Route::middleware(["auth"])->group(function () {
});
Route::get("/", "RootController@index")->name("root.index"); Route::get("/", "RootController@index")->name("root.index");

1
swagger/.agignore Normal file
View File

@ -0,0 +1 @@
dist/

49
swagger/.babelrc Normal file
View File

@ -0,0 +1,49 @@
{
"presets": [
[
"@babel/env",
{
"targets": {
"browsers": [
/* benefit of C/S/FF/Edge only? */
"> 1%",
"last 2 versions",
"Firefox ESR",
"not dead",
]
},
"useBuiltIns": "entry",
"corejs": "2"
}
],
"@babel/preset-react"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": "2"
}
],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-chaining",
[
"transform-react-remove-prop-types",
{
"additionalLibraries": [
"react-immutable-proptypes"
]
}
],
[
"babel-plugin-module-resolver",
{
"alias": {
"plugins": "./src/plugins",
"test": "./test",
"src": "./src"
}
}
]
]
}

8
swagger/.dockerignore Normal file
View File

@ -0,0 +1,8 @@
/.git
/.github
/dev-helpers
/docs
/src
/swagger-editor-dist-package
/test
/node_modules

10
swagger/.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

38
swagger/.eslintrc Normal file
View File

@ -0,0 +1,38 @@
{
"parser": "babel-eslint",
"env": {
"browser": true,
"node": true,
"es6": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"plugins": ["react", "import", "mocha"],
"rules": {
"semi": [2, "never"],
"strict": 0,
"quotes": [2, "double", { "allowTemplateLiterals": true }],
"no-unused-vars": 2,
"no-multi-spaces": 1,
"camelcase": 1,
"no-use-before-define": [2, "nofunc"],
"no-underscore-dangle": 0,
"no-unused-expressions": 1,
"comma-dangle": 0,
"no-console": ["error", { "allow": ["warn", "error"] }],
"react/jsx-no-bind": [1, { "allowArrowFunctions": true }], // TODO: make this an error
"react/display-name": 0,
"import/no-extraneous-dependencies": [2],
"no-useless-escape": 1,
"mocha/no-exclusive-tests": 2
}
}

10
swagger/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules
.idea
.deps_check
.DS_Store
npm-debug.log*
.eslintcache
# Cypress
test/e2e/screenshots
test/e2e/videos

10
swagger/.npmignore Normal file
View File

@ -0,0 +1,10 @@
*
*/
!README.md
!package.json
!dist/swagger-editor.js
!dist/swagger-editor.js.map
!dist/swagger-editor.css
!dist/swagger-editor.css.map
!dist/validation.worker.js
!dist/validation.worker.js.map

5
swagger/.prettierrc.yaml Normal file
View File

@ -0,0 +1,5 @@
semi: false
trailingComma: es5
endOfLine: lf
requirePragma: true
insertPragma: true

19
swagger/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM nginx:1.17-alpine
MAINTAINER fehguy
RUN mkdir -p /run/nginx
COPY nginx.conf /etc/nginx/
# copy swagger files to the `/js` folder
COPY ./index.html /usr/share/nginx/html/
ADD ./dist/*.js /usr/share/nginx/html/dist/
ADD ./dist/*.map /usr/share/nginx/html/dist/
ADD ./dist/*.css /usr/share/nginx/html/dist/
ADD ./dist/*.png /usr/share/nginx/html/dist/
ADD ./docker-run.sh /usr/share/nginx/
EXPOSE 8080
CMD ["sh", "/usr/share/nginx/docker-run.sh"]

201
swagger/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 SmartBear Software Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

114
swagger/README.md Normal file
View File

@ -0,0 +1,114 @@
# <img src="https://raw.githubusercontent.com/swagger-api/swagger.io/wordpress/images/assets/SWE-logo-clr.png" height="80">
[![NPM version](https://badge.fury.io/js/swagger-ui.svg)](http://badge.fury.io/js/swagger-editor)
[![Build Status](https://jenkins.swagger.io/buildStatus/icon?job=oss-swagger-editor-master)](https://jenkins.swagger.io/job/oss-swagger-editor-master/)
[![Code Climate](https://codeclimate.com/github/swagger-api/swagger-editor/badges/gpa.svg)](https://codeclimate.com/github/swagger-api/swagger-editor)
[![Dependency Status](https://david-dm.org/swagger-api/swagger-editor/status.svg)](https://david-dm.org/swagger-api/swagger-editor)
[![devDependency Status](https://david-dm.org/swagger-api/swagger-editor/dev-status.svg)](https://david-dm.org/swagger-api/swagger-editor-#info=devDependencies)
[![Build Status](https://jenkins.swagger.io/view/OSS%20-%20JavaScript/job/oss-swagger-editor-master/badge/icon?subject=jenkins%20build)](https://jenkins.swagger.io/view/OSS%20-%20JavaScript/job/oss-swagger-editor-master/)
**🕰️ Looking for the older version of Swagger Editor?** Refer to the [*2.x* branch](https://github.com/swagger-api/swagger-editor/tree/2.x).
Swagger Editor lets you edit [Swagger API specifications](https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md) in YAML inside your browser and to preview documentations in real time.
Valid Swagger JSON descriptions can then be generated and used with the full Swagger tooling (code generation, documentation, etc).
As a brand new version, written from the ground up, there are some known issues and unimplemented features. Check out the [Known Issues](#known-issues) section for more details.
This repository publishes to two different NPM modules:
* [swagger-editor](https://www.npmjs.com/package/swagger-editor) is a traditional npm module intended for use in single-page applications that are capable of resolving dependencies (via Webpack, Browserify, etc).
* [swagger-editor-dist](https://www.npmjs.com/package/swagger-editor-dist) is a dependency-free module that includes everything you need to serve Swagger Editor in a server-side project, or a web project that can't resolve npm module dependencies.
If you're building a single-page application, using `swagger-editor` is strongly recommended, since `swagger-editor-dist` is significantly larger.
For the older version of swagger-editor, refer to the [*2.x branch*](https://github.com/swagger-api/swagger-editor/tree/2.x).
## Running locally
##### Prerequisites
- NPM 6.x
Generally, we recommend following guidelines from [Node.js Releases](https://nodejs.org/en/about/releases/) to only use Active LTS or Maintenance LTS releases.
Current Node.js Active LTS:
- Node.js 12.x
- NPM 6.x
Current Node.js Maintenance LTS:
- Node.js 10.x
- NPM 6.x
If you have Node.js and npm installed, you can run `npm start` to spin up a static server.
Otherwise, you can open `index.html` directly from your filesystem in your browser.
If you'd like to make code changes to Swagger Editor, you can start up a Webpack hot-reloading dev server via `npm run dev`.
##### Browser support
Swagger Editor works in the latest versions of Chrome, Safari, Firefox, and Edge.
### Known Issues
To help with the migration, here are the currently known issues with 3.X. This list will update regularly, and will not include features that were not implemented in previous versions.
- Everything listed in [Swagger UI's Known Issues](https://github.com/swagger-api/swagger-ui/blob/master/README.md#known-issues).
- The integration with the codegen is still missing.
- Importing specs from a URL is not implemented.
## Docker
### Running the image from DockerHub
There is a docker image published in [DockerHub](https://hub.docker.com/r/swaggerapi/swagger-editor/).
To use this, run the following:
```
docker pull swaggerapi/swagger-editor
docker run -d -p 80:8080 swaggerapi/swagger-editor
```
This will run Swagger Editor (in detached mode) on port 80 on your machine, so you can open it by navigating to `http://localhost` in your browser.
* You can provide your own `json` or `yaml` definition file on your host
```
docker run -d -p 80:8080 -v $(pwd):/tmp -e SWAGGER_FILE=/tmp/swagger.json swaggerapi/swagger-editor
```
* You can provide a API document from your local machine — for example, if you have a file at `./bar/swagger.json`:
```
docker run -d -p 80:8080 -e URL=/foo/swagger.json -v /bar:/usr/share/nginx/html/foo swaggerapi/swagger-editor
```
### Building and running an image locally
To build and run a docker image with the code checked out on your machine, run the following from the root directory of the project:
```
# Install npm packages (if needed)
npm install
# Build the app
npm run build
# Build an image
docker build -t swagger-editor .
# Run the container
docker run -d -p 80:8080 swagger-editor
```
You can then view the app by navigating to `http://localhost` in your browser.
## Documentation
* [Importing your OpenAPI document](docs/import.md)
## Security contact
Please disclose any security-related issues or vulnerabilities by emailing [security@swagger.io](mailto:security@swagger.io), instead of using the public issue tracker.

21
swagger/SECURITY.md Normal file
View File

@ -0,0 +1,21 @@
# Security Policy
If you believe you've found an exploitable security issue in Swagger Editor,
**please don't create a public issue**.
## Supported versions
This is the list of versions of `swagger-editor` which are
currently being supported with security updates.
| Version | Supported | Notes |
| -------- | ------------------ | ---------------------- |
| 3.x | :white_check_mark: | |
| 2.x | :x: | End-of-life as of 2017 |
## Reporting a vulnerability
To report a vulnerability please send an email with the details to [security@swagger.io](mailto:security@swagger.io).
We'll acknowledge receipt of your report ASAP, and set expectations on how we plan to handle it.

View File

@ -0,0 +1,12 @@
if [ $DOCKER_HUB_USERNAME ]; then
docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD;
if [ ! -z "$TRAVIS_TAG" ]; then
DOCKER_IMAGE_TAG=${TRAVIS_TAG#?};
docker build -t $DOCKER_IMAGE_NAME .;
docker tag $DOCKER_IMAGE_NAME $DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG;
docker push $DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG;
docker tag $DOCKER_IMAGE_NAME $DOCKER_IMAGE_NAME:latest;
docker push $DOCKER_IMAGE_NAME:latest;
fi;
fi;

View File

@ -0,0 +1,28 @@
function extsToRegExp(exts) {
return new RegExp("\\.(" + exts.map(function(ext) {
return ext.replace(/\./g, "\\.");
}).join("|") + ")(\\?.*)?$");
}
module.exports = function loadersByExtension(obj) {
var loaders = [];
Object.keys(obj).forEach(function(key) {
var exts = key.split("|");
var value = obj[key];
var entry = {
extensions: exts,
test: extsToRegExp(exts)
};
if(Array.isArray(value)) {
entry.loaders = value;
} else if(typeof value === "string") {
entry.loader = value;
} else {
Object.keys(value).forEach(function(valueKey) {
entry[valueKey] = value[valueKey];
});
}
loaders.push(entry);
});
return loaders;
};

10
swagger/cypress.json Normal file
View File

@ -0,0 +1,10 @@
{
"fileServerFolder": "test/e2e/static",
"fixturesFolder": "test/e2e/fixtures",
"integrationFolder": "test/e2e/tests",
"pluginsFile": "test/e2e/plugins/index.js",
"screenshotsFolder": "test/e2e/screenshots",
"supportFile": "test/e2e/support/index.js",
"videosFolder": "test/e2e/videos",
"baseUrl": "http://localhost:3260/"
}

View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<!-- HTML for hot dev server -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger Editor</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Roboto,sans-serif;
font-size: 9px;
line-height: 1.42857143;
color: #444;
margin: 0px;
}
#swagger-editor {
font-size: 1.3em;
}
.container {
height: 100%;
max-width: 880px;
margin-left: auto;
margin-right: auto;
}
#editor-wrapper {
height: 100%;
border:1em solid #000;
border:none;
}
.Pane2 {
overflow-y: scroll;
}
</style>
<link href="./swagger-editor.css" rel="stylesheet">
<link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16" />
</head>
<body>
<div id="swagger-editor"></div>
<script src="/commons.js"> </script>
<script src="/swagger-editor-bundle.js"> </script>
<script src="/swagger-editor-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Webpack outputs library variables with file-names-like-this
window["SwaggerEditorBundle"] = window["SwaggerEditorBundle"] || window["swagger-editor-bundle"]
window["SwaggerEditorStandalonePreset"] = window["SwaggerEditorStandalonePreset"] || window["swagger-editor-standalone-preset"]
// Build a system
const editor = SwaggerEditorBundle({
dom_id: '#swagger-editor',
layout: 'StandaloneLayout',
presets: [
SwaggerEditorStandalonePreset
]
})
window.editor = editor
}
</script>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>
</defs>
</svg>
</body>
</html>

BIN
swagger/dist/favicon-16x16.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

BIN
swagger/dist/favicon-32x32.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

139
swagger/dist/swagger-editor-bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
swagger/dist/swagger-editor.css vendored Normal file

File diff suppressed because one or more lines are too long

1
swagger/dist/swagger-editor.css.map vendored Normal file

File diff suppressed because one or more lines are too long

2
swagger/dist/swagger-editor.js vendored Normal file

File diff suppressed because one or more lines are too long

1
swagger/dist/swagger-editor.js.map vendored Normal file

File diff suppressed because one or more lines are too long

20
swagger/docker-run.sh Normal file
View File

@ -0,0 +1,20 @@
#! /bin/sh
set -e
NGINX_ROOT=/usr/share/nginx/html
INDEX_FILE=$NGINX_ROOT/index.html
# TODO: this is empty but we'll be adding configuration values here
## Adding env var support for swagger file (json or yaml)
if [[ -f "$SWAGGER_FILE" ]]; then
cp -s "$SWAGGER_FILE" "$NGINX_ROOT"
REL_PATH="/$(basename $SWAGGER_FILE)"
sed -i "s|https://petstore.swagger.io/v2/swagger.json|$REL_PATH|g" $INDEX_FILE
fi
# Gzip after replacements
find /usr/share/nginx/html/ -type f -regex ".*\.\(html\|js\|css\)" -exec sh -c "gzip < {} > {}.gz" \;
exec nginx -g 'daemon off;'

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

22
swagger/docs/helpers.md Normal file
View File

@ -0,0 +1,22 @@
### `getEditorMetadata`
`getEditorMetadata` is a method that allows you to get information about the Editor's state without reaching directly into the plugin system.
Example:
```js
const editor = SwaggerEditor({ /* your configuration here */ })
SwaggerEditor.getEditorMetadata()
```
Result:
```js
{
contentString: String,
contentObject: Object,
isValid: Boolean,
errors: Array,
}
```

17
swagger/docs/import.md Normal file
View File

@ -0,0 +1,17 @@
# Importing OpenAPI documents
Swagger Editor can import your OpenAPI document, which can be formatted as JSON or YAML.
### File → Import File
Click **Choose File** and select import. The file you are importing has to be a valid JSON or YAML OpenAPI document. Swagger Editor will prompt you about validation errors, if any exist.
### File → Import URL
Paste the URL to your OpenAPI document.
### Drag and Drop
Simply drag and drop your OpenAPI JSON or YAML document into the Swagger Editor browser window.
![Swagger Editor drag and drop demo](./drag-and-drop.gif)

101
swagger/index.html Normal file
View File

@ -0,0 +1,101 @@
<!DOCTYPE html>
<!-- HTML for static distribution bundle build -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger Editor</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Roboto,sans-serif;
font-size: 9px;
line-height: 1.42857143;
color: #444;
margin: 0px;
}
#swagger-editor {
font-size: 1.3em;
}
.container {
height: 100%;
max-width: 880px;
margin-left: auto;
margin-right: auto;
}
#editor-wrapper {
height: 100%;
border:1em solid #000;
border:none;
}
.Pane2 {
overflow-y: scroll;
}
</style>
<link href="./dist/swagger-editor.css" rel="stylesheet">
<link rel="icon" type="image/png" href="./dist/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./dist/favicon-16x16.png" sizes="16x16" />
</head>
<body>
<div id="swagger-editor"></div>
<script src="./dist/swagger-editor-bundle.js"> </script>
<script src="./dist/swagger-editor-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Build a system
const editor = SwaggerEditorBundle({
dom_id: '#swagger-editor',
layout: 'StandaloneLayout',
presets: [
SwaggerEditorStandalonePreset
]
})
window.editor = editor
}
</script>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>
</defs>
</svg>
</body>
</html>

33
swagger/nginx.conf Normal file
View File

@ -0,0 +1,33 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_static on;
gzip_disable "msie6";
gzip_vary on;
gzip_types text/plain text/css application/javascript;
add_header X-Frame-Options deny;
server {
listen 8080;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
}

22297
swagger/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

167
swagger/package.json Normal file
View File

@ -0,0 +1,167 @@
{
"name": "swagger-editor",
"description": "Swagger Editor",
"version": "3.11.1",
"main": "dist/swagger-editor.js",
"repository": "git@github.com:swagger-api/swagger-editor.git",
"license": "Apache-2.0",
"contributors": [
"(in alphabetical order)",
"Anna Bodnia <anna.bodnia@gmail.com>",
"Buu Nguyen <buunguyen@gmail.com>",
"Josh Ponelat <jponelat@gmail.com>",
"Kyle Shockey <kyleshockey1@gmail.com>",
"Robert Barnwell <robert@robertismy.name>",
"Sahar Jafari <shr.jafari@gmail.com>"
],
"scripts": {
"automated-release": "release-it --config ./release/.release-it.json",
"build": "run-p --aggregate-output build:core build:bundle build:standalone build:stylesheets",
"build:bundle": "webpack --config webpack/bundle.babel.js --colors",
"build:core": "webpack --config webpack/core.babel.js --colors",
"build:standalone": "webpack --config webpack/standalone.babel.js --colors",
"build:stylesheets": "webpack --config webpack/stylesheets.babel.js --colors",
"deps-check": "run-s deps-check:license deps-check:size",
"deps-check:license": "license-checker --production --csv --out $npm_package_config_deps_check_dir/licenses.csv && license-checker --development --csv --out $npm_package_config_deps_check_dir/licenses-dev.csv",
"deps-check:size": "webpack -p --config webpack/test_deps_size.babel.js --json | webpack-bundle-size-analyzer >| $npm_package_config_deps_check_dir/sizes.txt",
"predev": "npm install",
"dev": "npm-run-all --parallel hot-server",
"hot-server": "webpack-dev-server --host 0.0.0.0 --config webpack/dev.babel.js --inline --hot --progress --content-base dev-helpers/",
"open-static": "node -e 'require(\"open\")(\"http://localhost:3001\")'",
"lint": "eslint --cache --ext '.js,.jsx' src test",
"lint-errors": "eslint --cache --quiet --ext '.js,.jsx' src test",
"lint-fix": "eslint --cache --ext '.js,.jsx' src test --fix",
"e2e": "run-p -r e2e:hot-server e2e:cypress-test",
"e2e:dev": "run-p -r e2e:hot-server e2e:cypress-open",
"e2e:hot-server": "webpack-dev-server --port 3260 --host 0.0.0.0 --config webpack/dev.babel.js --inline --hot --progress --content-base test/e2e/static",
"e2e:cypress-test": "cypress run",
"e2e:cypress-open": "cypress open",
"test": "run-s just-test-in-node e2e lint-errors",
"test-in-node": "run-s lint-errors just-test-in-node",
"just-test-in-node": "cross-env NODE_ENV=test mocha --require test/unit/setup.js --recursive --require @babel/register test/unit",
"just-test-in-node-watch": "npm run just-test-in-node -- -w",
"serve-static": "http-server -i -a 0.0.0.0 -p 3001",
"prestart": "npm install",
"security-audit": "run-s -sc security-audit:all security-audit:prod",
"security-audit:prod": "npm-audit-ci-wrapper -p -t low",
"security-audit:all": "npm-audit-ci-wrapper -t moderate",
"start": "npm-run-all --parallel serve-static open-static",
"watch": "webpack --config webpack/core.babel.js --watch --progress"
},
"dependencies": {
"ajv": "^5.2.2",
"ajv-errors": "^1.0.1",
"ajv-keywords": "^3.4.0",
"boron": "^0.2.3",
"brace": "^0.10.0",
"classnames": "^2.1.3",
"core-js": "^2.4.1",
"deepmerge": "^1.3.2",
"file-dialog": "0.0.7",
"immutable": "^3.x.x",
"is-json": "^2.0.1",
"js-yaml": "^3.13.1",
"json-beautify": "^1.0.1",
"json-refs": "^3.0.4",
"lodash": "^4.17.15",
"promise-worker": "^1.1.1",
"prop-types": "15.6.0",
"querystring-browser": "^1.0.4",
"react": "^15.6.2",
"react-ace": "^4.1.6",
"react-addons-css-transition-group": "^15.4.2",
"react-dd-menu": "^2.0.0",
"react-dom": "^15.6.2",
"react-dropzone": "4.2.11",
"react-file-download": "^0.3.2",
"react-immutable-proptypes": "^2.1.0",
"react-redux": "^4.x.x",
"react-split-pane": "^0.1.82",
"react-transition-group": "^1.1.1",
"redux": "^3.x.x",
"reselect": "^2.5.4",
"swagger-client": "^3.10.6",
"swagger-ui": "^3.26.0",
"traverse": "^0.6.6",
"whatwg-fetch": "^2.0.3",
"yaml-js": "^0.2.3"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.5.5",
"@babel/runtime-corejs2": "^7.0.0",
"@release-it/conventional-changelog": "^1.1.0",
"autoprefixer": "^6.6.1",
"babel-eslint": "^10.0.2",
"babel-loader": "^8.0.6",
"babel-plugin-module-alias": "^1.6.0",
"babel-plugin-module-resolver": "^3.2.0",
"babel-plugin-transform-es2015-constants": "^6.1.4",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"bundlesize": "^0.17.2",
"cross-env": "^5.1.3",
"css-loader": "^3.1.0",
"cssnano": "^4.1.10",
"cypress": "^3.4.1",
"dedent": "^0.7.0",
"deep-extend": "^0.6.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5",
"eslint": "^6.1.0",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-mocha": "^6.3.0",
"eslint-plugin-react": "^4.3.0",
"expect": "^1.20.2",
"extract-text-webpack-plugin": "0.8.2",
"file-loader": "0.8.4",
"git-describe": "^4.0.1",
"html-webpack-plugin": "^2.28.0",
"http-server": "^0.12.1",
"ignore-assets-webpack-plugin": "^2.0.1",
"jsdom": "11.12.0",
"jsdom-global": "3.0.2",
"json-loader": "0.5.3",
"less": "^3.9.0",
"less-loader": "^5.0.0",
"license-checker": "^8.0.4",
"matcher": "^0.1.2",
"mini-css-extract-plugin": "^0.8.0",
"mocha": "^6.2.0",
"mocha-jsdom": "^2.0.0",
"npm-audit-ci-wrapper": "^2.3.0",
"npm-run-all": "^4.1.3",
"null-loader": "0.1.1",
"open": "^6.4.0",
"postcss-loader": "^3.0.0",
"raw-loader": "0.5.1",
"react-hot-loader": "^1.3.1",
"react-test-renderer": "^15.6.2",
"release-it": "^12.3.5",
"rewiremock": "^3.7.4",
"rimraf": "^2.6.0",
"standard": "^8.6.0",
"style-loader": "0.13.0",
"url-loader": "^2.1.0",
"webpack": "^4.36.1",
"webpack-bundle-size-analyzer": "^2.5.0",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2",
"worker-loader": "^2.0.0"
},
"config": {
"deps_check_dir": ".deps_check"
},
"bundlesize": [
{
"path": "./dist/swagger-editor-bundle.js",
"maxSize": "2.8 MB",
"compression": "none"
}
]
}

View File

@ -0,0 +1,29 @@
{
"hooks": {
"before:bump": [
"./release/check-for-breaking-changes.sh ${latestVersion} ${version}",
"npm update swagger-client",
"npm update swagger-ui",
"npm test"
],
"after:bump": ["npm run build"],
"after:release": "export GIT_TAG=v${version} && echo GIT_TAG=v${version} > release/.version"
},
"git": {
"requireUpstream": false,
"changelog": "./release/get-changelog.sh",
"commitMessage": "release: v${version}",
"tagName": "v${version}",
"push": false
},
"github": {
"release": true,
"releaseName": "Swagger Editor %s Released!",
"draft": true
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": "angular"
}
}
}

View File

@ -0,0 +1,14 @@
#!/bin/bash
CURRENT_VERSION=$1
NEXT_VERSION=$2
CURRENT_MAJOR=${CURRENT_VERSION:0:1}
NEXT_MAJOR=${NEXT_VERSION:0:1}
if [ "$CURRENT_MAJOR" -ne "$NEXT_MAJOR" ] ;
then if [ "$BREAKING_OKAY" = "true" ];
then echo "breaking change detected but BREAKING_OKAY is set; continuing." && exit 0;
else echo "breaking change detected and BREAKING_OKAY is not set; aborting." && exit 1;
fi;
fi;
echo "next version is not a breaking change; continuing.";

View File

@ -0,0 +1,5 @@
echo "_No release summary included._\n\n#### Changelog\n"
PREV_RELEASE_REF=$(git log --pretty=oneline | grep ' release: ' | head -n 2 | tail -n 1 | cut -f 1 -d " ")
git log --pretty=oneline $PREV_RELEASE_REF..HEAD | awk '{ $1=""; print}' | sed -e 's/^[ \t]*//' | sed 's/^feat/0,feat/' | sed 's/^improve/1,improve/' | sed 's/^fix/2,fix/'| sort | sed 's/^[0-2],//' | sed 's/^/* /'

10
swagger/src/.eslintrc Normal file
View File

@ -0,0 +1,10 @@
{
"rules": {
"import/no-extraneous-dependencies": [
2,
{
"devDependencies": false
}
]
}
}

67
swagger/src/index.js Normal file
View File

@ -0,0 +1,67 @@
import deepMerge from "deepmerge"
import SwaggerUI from "swagger-ui"
import EditorLayout from "./layout"
import EditorPlugin from "./plugins/editor"
import LocalStoragePlugin from "./plugins/local-storage"
import ValidateBasePlugin from "./plugins/validate-base"
import ValidateSemanticPlugin from "./plugins/validate-semantic"
import ValidateJsonSchemaPlugin from "./plugins/json-schema-validator"
import EditorAutosuggestPlugin from "./plugins/editor-autosuggest"
import EditorAutosuggestSnippetsPlugin from "./plugins/editor-autosuggest-snippets"
import EditorAutosuggestKeywordsPlugin from "./plugins/editor-autosuggest-keywords"
import EditorAutosuggestOAS3KeywordsPlugin from "./plugins/editor-autosuggest-oas3-keywords"
import EditorAutosuggestRefsPlugin from "./plugins/editor-autosuggest-refs"
import PerformancePlugin from "./plugins/performance"
import JumpToPathPlugin from "./plugins/jump-to-path"
import SplitPaneModePlugin from "./plugins/split-pane-mode"
import ASTPlugin from "./plugins/ast"
// eslint-disable-next-line no-undef
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION } = buildInfo
window.versions = window.versions || {}
window.versions.swaggerEditor = `${PACKAGE_VERSION}/${GIT_COMMIT || "unknown"}${GIT_DIRTY ? "-dirty" : ""}`
const plugins = {
EditorPlugin,
ValidateBasePlugin,
ValidateSemanticPlugin,
ValidateJsonSchemaPlugin,
LocalStoragePlugin,
EditorAutosuggestPlugin,
EditorAutosuggestSnippetsPlugin,
EditorAutosuggestKeywordsPlugin,
EditorAutosuggestRefsPlugin,
EditorAutosuggestOAS3KeywordsPlugin,
PerformancePlugin,
JumpToPathPlugin,
SplitPaneModePlugin,
ASTPlugin,
}
const defaults = {
// we have the `dom_id` prop for legacy reasons
dom_id: "#swagger-editor", // eslint-disable-line camelcase
layout: "EditorLayout",
presets: [
SwaggerUI.presets.apis
],
plugins: Object.values(plugins),
components: {
EditorLayout
},
showExtensions: true,
swagger2GeneratorUrl: "https://generator.swagger.io/api/swagger.json",
oas3GeneratorUrl: "https://generator3.swagger.io/openapi.json",
swagger2ConverterUrl: "https://converter.swagger.io/api/convert",
}
export default function SwaggerEditor(options) {
let mergedOptions = deepMerge(defaults, options)
mergedOptions.presets = defaults.presets.concat(options.presets || [])
mergedOptions.plugins = defaults.plugins.concat(options.plugins || [])
return SwaggerUI(mergedOptions)
}
SwaggerEditor.plugins = plugins

80
swagger/src/layout.jsx Normal file
View File

@ -0,0 +1,80 @@
import React from "react"
import PropTypes from "prop-types"
import Dropzone from "react-dropzone"
Dropzone.displayName = "Dropzone" // For testing
export default class EditorLayout extends React.Component {
static propTypes = {
errSelectors: PropTypes.object.isRequired,
errActions: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
layoutSelectors: PropTypes.object.isRequired,
layoutActions: PropTypes.object.isRequired
}
onChange = (newYaml, origin="editor") => {
this.props.specActions.updateSpec(newYaml, origin)
}
onDrop = (acceptedFiles, rejectedFiles) => {
const someFilesWereRejected = rejectedFiles && rejectedFiles.length > 0
const thereIsExactlyOneAcceptedFile = acceptedFiles && acceptedFiles.length === 1
if ( someFilesWereRejected || !thereIsExactlyOneAcceptedFile) {
alert("Sorry, there was an error processing your file.\nPlease drag and drop exactly one .yaml or .json OpenAPI definition file.")
} else {
const file = acceptedFiles[0]
const reader = new FileReader()
reader.onloadend = () => {
const spec = reader.result
this.onChange(spec, "fileDrop")
}
reader.readAsText(file, "utf-8")
}
}
render() {
const { getComponent } = this.props
const UIBaseLayout = getComponent("BaseLayout", true)
const EditorContainer = getComponent("EditorContainer", true)
const SplitPaneMode = getComponent("SplitPaneMode", true)
const Container = getComponent("Container")
return (
<div className="swagger-editor">
<Container className="container">
<Dropzone
className="dropzone"
accept=".yaml,application/json"
multiple={false}
onDrop={this.onDrop}
disablePreview
disableClick
>
{({ isDragActive }) => {
if (isDragActive) {
return (
<div className="dropzone__overlay">
Please drop a .yaml or .json OpenAPI spec.
</div>
)
} else {
return (
<SplitPaneMode>
<EditorContainer onChange={this.onChange} />
<UIBaseLayout/>
</SplitPaneMode>
)
}
}}
</Dropzone>
</Container>
</div>
)
}
}

View File

@ -0,0 +1,308 @@
import YAML from "yaml-js"
import isArray from "lodash/isArray"
import lodashFind from "lodash/find"
import memoize from "lodash/memoize"
let cachedCompose = memoize(YAML.compose) // TODO: build a custom cache based on content
var MAP_TAG = "tag:yaml.org,2002:map"
var SEQ_TAG = "tag:yaml.org,2002:seq"
export function getLineNumberForPath(yaml, path) {
// Type check
if (typeof yaml !== "string") {
throw new TypeError("yaml should be a string")
}
if (!isArray(path)) {
throw new TypeError("path should be an array of strings")
}
var i = 0
let ast = cachedCompose(yaml)
// simply walks the tree using current path recursively to the point that
// path is empty
return find(ast, path)
function find(current, path, last) {
if(!current) {
// something has gone quite wrong
// return the last start_mark as a best-effort
if(last && last.start_mark)
return last.start_mark.line
return 0
}
if (path.length && current.tag === MAP_TAG) {
for (i = 0; i < current.value.length; i++) {
var pair = current.value[i]
var key = pair[0]
var value = pair[1]
if (key.value === path[0]) {
return find(value, path.slice(1), current)
}
if (key.value === path[0].replace(/\[.*/, "")) {
// access the array at the index in the path (example: grab the 2 in "tags[2]")
var index = parseInt(path[0].match(/\[(.*)\]/)[1])
if(value.value.length === 1 && index !== 0 && !!index) {
var nextVal = lodashFind(value.value[0], { value: index.toString() })
} else { // eslint-disable-next-line no-redeclare
var nextVal = value.value[index]
}
return find(nextVal, path.slice(1), value.value)
}
}
}
if (path.length && current.tag === SEQ_TAG) {
var item = current.value[path[0]]
if (item && item.tag) {
return find(item, path.slice(1), current.value)
}
}
if (current.tag === MAP_TAG && !Array.isArray(last)) {
return current.start_mark.line
} else {
return current.start_mark.line + 1
}
}
}
/**
* Get a position object with given
* @param {string} yaml
* YAML or JSON string
* @param {array} path
* an array of stings that constructs a
* JSON Path similar to JSON Pointers(RFC 6901). The difference is, each
* component of path is an item of the array instead of being separated with
* slash(/) in a string
*/
export function positionRangeForPath(yaml, path) {
// Type check
if (typeof yaml !== "string") {
throw new TypeError("yaml should be a string")
}
if (!isArray(path)) {
throw new TypeError("path should be an array of strings")
}
var invalidRange = {
start: {line: -1, column: -1},
end: {line: -1, column: -1}
}
var i = 0
let ast = cachedCompose(yaml)
// simply walks the tree using astValue path recursively to the point that
// path is empty.
return find(ast)
function find(astValue, astKeyValue) {
if (astValue.tag === MAP_TAG) {
for (i = 0; i < astValue.value.length; i++) {
var pair = astValue.value[i]
var key = pair[0]
var value = pair[1]
if (key.value === path[0]) {
path.shift()
return find(value, key)
}
}
}
if (astValue.tag === SEQ_TAG) {
var item = astValue.value[path[0]]
if (item && item.tag) {
path.shift()
return find(item, astKeyValue)
}
}
// if path is still not empty we were not able to find the node
if (path.length) {
return invalidRange
}
const range = {
start: {
line: astValue.start_mark.line,
column: astValue.start_mark.column,
pointer: astValue.start_mark.pointer,
},
end: {
line: astValue.end_mark.line,
column: astValue.end_mark.column,
pointer: astValue.end_mark.pointer,
}
}
if(astKeyValue) {
// eslint-disable-next-line camelcase
range.key_start = {
line: astKeyValue.start_mark.line,
column: astKeyValue.start_mark.column,
pointer: astKeyValue.start_mark.pointer,
}
// eslint-disable-next-line camelcase
range.key_end = {
line: astKeyValue.end_mark.line,
column: astKeyValue.end_mark.column,
pointer: astKeyValue.end_mark.pointer,
}
}
return range
}
}
/**
* Get a JSON Path for position object in the spec
* @param {string} yaml
* YAML or JSON string
* @param {object} position
* position in the YAML or JSON string with `line` and `column` properties.
* `line` and `column` number are zero indexed
*/
export function pathForPosition(yaml, position) {
// Type check
if (typeof yaml !== "string") {
throw new TypeError("yaml should be a string")
}
if (typeof position !== "object" || typeof position.line !== "number" ||
typeof position.column !== "number") {
throw new TypeError("position should be an object with line and column" +
" properties")
}
try {
var ast = cachedCompose(yaml)
} catch (e) {
console.error("Error composing AST", e)
const problemMark = e.problem_mark || {}
const errorTraceMessage = [
yaml.split("\n").slice(problemMark.line - 5, problemMark.line + 1).join("\n"),
Array(problemMark.column).fill(" ").join("") + `^----- ${e.name}: ${e.toString().split("\n")[0]}`,
yaml.split("\n").slice(problemMark.line + 1, problemMark.line + 5).join("\n")
].join("\n")
console.error(errorTraceMessage)
return null
}
var path = []
return find(ast)
/**
* recursive find function that finds the node matching the position
* @param {object} current - AST object to serach into
*/
function find(current) {
// algorythm:
// is current a promitive?
// // finish recursion without modifying the path
// is current a hash?
// // find a key or value that position is in their range
// // if key is in range, terminate recursion with exisiting path
// // if a value is in range push the corresponding key to the path
// // andcontinue recursion
// is current an array
// // find the item that position is in it"s range and push the index
// // of the item to the path and continue recursion with that item.
var i = 0
if (!current || [MAP_TAG, SEQ_TAG].indexOf(current.tag) === -1) {
return path
}
if (current.tag === MAP_TAG) {
for (i = 0; i < current.value.length; i++) {
var pair = current.value[i]
var key = pair[0]
var value = pair[1]
if (isInRange(key)) {
return path
} else if (isInRange(value)) {
path.push(key.value)
return find(value)
}
}
}
if (current.tag === SEQ_TAG) {
for (i = 0; i < current.value.length; i++) {
var item = current.value[i]
if (isInRange(item)) {
path.push(i.toString())
return find(item)
}
}
}
return path
/**
* Determines if position is in node"s range
* @param {object} node - AST node
* @return {Boolean} true if position is in node"s range
*/
function isInRange(node) {
/* jshint camelcase: false */
// if node is in a single line
if (node.start_mark.line === node.end_mark.line) {
return (position.line === node.start_mark.line) &&
(node.start_mark.column <= position.column) &&
(node.end_mark.column >= position.column)
}
// if position is in the same line as start_mark
if (position.line === node.start_mark.line) {
return position.column >= node.start_mark.column
}
// if position is in the same line as end_mark
if (position.line === node.end_mark.line) {
return position.column <= node.end_mark.column
}
// if position is between start and end lines return true, otherwise
// return false.
return (node.start_mark.line < position.line) &&
(node.end_mark.line > position.line)
}
}
}
// utility fns
export let pathForPositionAsync = promisifySyncFn(pathForPosition)
export let positionRangeForPathAsync = promisifySyncFn(positionRangeForPath)
export let getLineNumberForPathAsync = promisifySyncFn(getLineNumberForPath)
function promisifySyncFn(fn) {
return function(...args) {
return new Promise((resolve) => resolve(fn(...args)))
}
}

View File

@ -0,0 +1,7 @@
import * as AST from "./ast"
export default function() {
return {
fn: { AST }
}
}

View File

@ -0,0 +1,21 @@
import keywordMap from "./keyword-map"
import getKeywordsForPath from "./get-keywords-for-path"
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
const { fn: { getPathForPosition }, specSelectors } = system
const { isOAS3 } = specSelectors
if(isOAS3 && isOAS3()) {
// isOAS3 selector exists, and returns true
return cb(null, null)
}
const { AST } = ctx
var editorValue = editor.getValue()
const path = getPathForPosition({ pos, prefix, editorValue, AST})
const suggestions = getKeywordsForPath({ system, path, keywordMap })
cb(null, suggestions)
}

View File

@ -0,0 +1,176 @@
import isArray from "lodash/isArray"
import isObject from "lodash/isObject"
import mapValues from "lodash/mapValues"
import isPlainObject from "lodash/isPlainObject"
import toArray from "lodash/toArray"
import isString from "lodash/isString"
import get from "lodash/get"
export default function getKeywordsForPath({ system, path, keywordMap }) {
keywordMap = Object.assign({}, keywordMap)
// is getting path was not successful stop here and return no candidates
if (!isArray(path)) {
return [
{
name: "array",
value: " ",
score: 300,
meta: "Couldn't load suggestions"
}
]
}
if(path[path.length - 2] === "tags" && path.length > 2) {
// 'path.length > 2' excludes top-level 'tags'
return system.specSelectors.tags().map(tag => ({
score: 0,
meta: "local",
value: tag.get("name"),
})).toJS()
}
let reversePath = path.slice(0).reverse()
if(reversePath[1] === "security" && isNumeric(reversePath[0])) {
// **.security[x]
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
score: 0,
meta: "local",
caption: sec,
snippet: `${sec}: []`
})).toJS()
}
if(reversePath[0] === "security") {
// **.security:
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
score: 0,
meta: "local",
caption: sec,
snippet: `\n- ${sec}: []`
})).toJS()
}
// traverse down the keywordMap for each key in the path until there is
// no key in the path
var key = path.shift()
while (key && isObject(keywordMap)) {
keywordMap = getChild(keywordMap, key)
key = path.shift()
}
// if no keywordMap was found after the traversal return no candidates
if (!isObject(keywordMap)) {
return []
}
// if keywordMap is an array of strings, return the array as list of
// suggestions
if (isArray(keywordMap) && keywordMap.every(isString)) {
return keywordMap.map(constructAceCompletion.bind(null, "value"))
}
// If keywordMap is describing an array unwrap the inner map so we can
// suggest for array items
if (isArray(keywordMap)) {
if(isArray(keywordMap[0])) {
return keywordMap[0].map(item => {
return {
name: "array",
value: "- " + item,
score: 300,
meta: "array item"
}
})
} else {
return [{
name: "array",
value: "- ",
score: 300,
meta: "array item"
}]
}
}
// if keywordMap is not an object at this point return no candidates
if (!isObject(keywordMap)) {
return []
}
// for each key in keywordMap map construct a completion candidate and
// return the array
return suggestionFromSchema(keywordMap)
}
function getChild(object, key) {
var keys = Object.keys(object)
var regex
var isArrayAccess = /^\d+$/.test(key)
if(isArrayAccess && isArray(object)) {
return object[0]
}
for (var i = 0; i < keys.length; i++) {
let childVal = object[keys[i]]
if(!childVal) {
return null
}
regex = new RegExp(childVal.__regex || keys[i])
if (regex.test(key) && childVal) {
if(typeof childVal === "object" && !isArray(childVal)) {
return Object.assign({}, childVal)
} else {
return childVal
}
}
}
}
function suggestionFromSchema(map) {
const res = toArray(mapValues(map, (val, key) => {
const keyword = get(val, "__value", key)
const meta = isPlainObject(val) ? "object" : "keyword"
return constructAceCompletion(meta, keyword)
}))
return res
}
function constructAceCompletion(meta, keyword) {
if(keyword.slice(0, 2) === "__") {
return {}
}
// Give keywords, that extra colon
let snippet
switch(meta) {
case "keyword":
snippet = `${keyword}: `
break
case "object":
snippet = `${keyword}:\n `
break
default:
snippet = keyword
}
// snippet's treat `$` as special characters
snippet = snippet.replace("$", "\\$")
return {
snippet,
caption: keyword,
score: 300,
meta,
}
}
function isNumeric(obj) {
return !isNaN(obj)
}

View File

@ -0,0 +1,11 @@
import * as wrapActions from "./wrap-actions"
export default function EditorAutosuggestKeywordsPlugin() {
return {
statePlugins: {
editor: {
wrapActions,
}
}
}
}

View File

@ -0,0 +1,265 @@
var Bool = ["true", "false"]
var Anything = String
var combine = (...objs) => objs ? Object.assign({}, ...objs) : {}
var makeValue = (val = "") => {
return {
__value: val
}
}
var emptyValue = makeValue("")
var externalDocs = {
description: String,
url: String
}
var xml = {
name: String,
namespace: String,
prefix: String,
attribute: Bool,
wrapped: Bool
}
var schema = {
$ref: String,
format: String,
title: String,
description: String,
default: String,
maximum: Number,
minimum: Number,
exclusiveMaximum: Bool,
exclusiveMinimum: Bool,
maxLength: Number,
minLength: Number,
pattern: String,
maxItems: Number,
minItems: Number,
uniqueItems: Bool,
enum: [String],
multipleOf: Number,
maxProperties: Number,
minProperties: Number,
required: [String],
type: ["string", "number", "integer", "boolean", "array", "object"],
get items () { return this },
get allOf () { return [this] },
get properties () {
return {
".": this
}
},
get additionalProperties () { return this },
discriminator: String,
readOnly: Bool,
xml: xml,
externalDocs: externalDocs,
example: String
}
var schemes = [
"http",
"https",
"ws",
"wss"
]
var items = {
type: ["string", "number", "integer", "boolean", "array"],
format: String,
get items () { return this },
collectionFormat: ["csv"],
default: Anything,
minimum: String,
maximum: String,
exclusiveMinimum: Bool,
exclusiveMaximum: Bool,
minLength: String,
maxLength: String,
pattern: String,
minItems: String,
maxItems: String,
uniqueItems: Bool,
enum: [Anything],
multipleOf: String
}
var header = {
description: String,
type: String,
format: String,
items: items,
collectionFormat: ["csv"],
default: Anything,
enum: [String],
minimum: String,
maximum: String,
exclusiveMinimum: Bool,
exclusiveMaximum: Bool,
multipleOf: String,
maxLength: String,
minLength: String,
pattern: String,
minItems: String,
maxItems: String,
uniqueItems: Bool
}
var parameter = {
name: String,
description: String,
required: ["true", "false"],
type: [
"string",
"number",
"boolean",
"integer",
"array",
"file"
],
format: String,
schema: schema,
enum: [String],
minimum: String,
maximum: String,
exclusiveMinimum: Bool,
exclusiveMaximum: Bool,
multipleOf: String,
maxLength: String,
minLength: String,
pattern: String,
minItems: String,
maxItems: String,
uniqueItems: Bool,
allowEmptyValue: Bool,
collectionFormat: ["csv", "multi"],
default: String,
items: items,
in: [
"body",
"formData",
"header",
"path",
"query"
]
}
var reference = {
"$ref": String
}
var response = {
description: String,
schema: schema,
headers: {
".": combine(header, {
__value: ""
})
},
examples: String
}
var operation = {
summary: String,
description: String,
schemes: [schemes],
externalDocs: externalDocs,
operationId: String,
produces: [String],
consumes: [String],
deprecated: Bool,
security: [String],
parameters: [combine(reference, parameter)],
responses: {
"[2-6][0-9][0-9]": combine(reference, response, emptyValue),
"default": combine(reference, response)
},
tags: [String]
}
var securityScheme = {
type: ["oauth2", "apiKey", "basic"],
description: String,
name: String,
in: ["query", "header"],
flow: ["implicit", "password", "application", "accessCode"],
authorizationUrl: String,
tokenUrl: String,
scopes: String // actually an object, but this is equivalent
}
var info = {
version: String,
title: String,
description: String,
termsOfService: String,
contact: {
name: String,
url: String,
email: String
},
license: {
name: String,
url: String
}
}
var map = {
swagger: ["\'2.0\'"],
info: info,
host: String,
basePath: String,
schemes: [schemes],
produces: [String],
consumes: [String],
paths: {
//path
".": {
__value: "",
parameters: [combine(reference, parameter)],
"get": operation,
"put": operation,
"post": operation,
"delete": operation,
"options": operation,
"head": operation,
"patch": operation,
"$ref": String
}
},
definitions: {
// Definition name
".": combine(schema, emptyValue)
},
parameters: {
".": combine(reference, parameter, emptyValue)
},
responses: {
"[2-6][0-9][0-9]": combine(response, emptyValue)
},
securityDefinitions: {
".": combine(securityScheme, emptyValue)
},
security: [String],
tags: [{
name: String,
description: String,
externalDocs: externalDocs
}],
externalDocs: externalDocs
}
export default map

View File

@ -0,0 +1,11 @@
import getCompletions from "./get-completions"
// Add an autosuggest completer
export const addAutosuggestionCompleters = (ori, system) => (context) => {
return ori(context).concat([{
getCompletions(...args) {
// Add `context`, then `system` as the last args
return getCompletions(...args, context, system)
}
}])
}

View File

@ -0,0 +1,21 @@
import keywordMap from "./keyword-map"
import getKeywordsForPath from "./get-keywords-for-path"
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
const { fn: { getPathForPosition }, specSelectors } = system
const { isOAS3 } = specSelectors
if(isOAS3 && !isOAS3()) {
// isOAS3 selector exists, and returns false
return cb(null, null)
}
const { AST } = ctx
var editorValue = editor.getValue()
const path = getPathForPosition({ pos, prefix, editorValue, AST})
const suggestions = getKeywordsForPath({ system, path, keywordMap })
cb(null, suggestions)
}

View File

@ -0,0 +1,176 @@
import isArray from "lodash/isArray"
import isObject from "lodash/isObject"
import mapValues from "lodash/mapValues"
import isPlainObject from "lodash/isPlainObject"
import toArray from "lodash/toArray"
import isString from "lodash/isString"
import get from "lodash/get"
export default function getKeywordsForPath({ system, path, keywordMap}) {
keywordMap = Object.assign({}, keywordMap)
// is getting path was not successful stop here and return no candidates
if (!isArray(path)) {
return [
{
name: "array",
value: " ",
score: 300,
meta: "Couldn't load suggestions"
}
]
}
if(path[path.length - 2] === "tags" && path.length > 2) {
// 'path.length > 2' excludes top-level 'tags'
return system.specSelectors.tags().map(tag => ({
score: 0,
meta: "local",
value: tag.get("name"),
})).toJS()
}
let reversePath = path.slice(0).reverse()
if(reversePath[1] === "security" && isNumeric(reversePath[0])) {
// **.security[x]
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
score: 0,
meta: "local",
caption: sec,
snippet: `${sec}: []`
})).toJS()
}
if(reversePath[0] === "security") {
// **.security:
return system.specSelectors.securityDefinitions().keySeq().map(sec => ({
score: 0,
meta: "local",
caption: sec,
snippet: `\n- ${sec}: []`
})).toJS()
}
// traverse down the keywordMap for each key in the path until there is
// no key in the path
var key = path.shift()
while (key && isObject(keywordMap)) {
keywordMap = getChild(keywordMap, key)
key = path.shift()
}
// if no keywordMap was found after the traversal return no candidates
if (!isObject(keywordMap)) {
return []
}
// if keywordMap is an array of strings, return the array as list of
// suggestions
if (isArray(keywordMap) && keywordMap.every(isString)) {
return keywordMap.map(constructAceCompletion.bind(null, "value"))
}
// If keywordMap is describing an array unwrap the inner map so we can
// suggest for array items
if (isArray(keywordMap)) {
if(isArray(keywordMap[0])) {
return keywordMap[0].map(item => {
return {
name: "array",
value: "- " + item,
score: 300,
meta: "array item"
}
})
} else {
return [{
name: "array",
value: "- ",
score: 300,
meta: "array item"
}]
}
}
// if keywordMap is not an object at this point return no candidates
if (!isObject(keywordMap)) {
return []
}
// for each key in keywordMap map construct a completion candidate and
// return the array
return suggestionFromSchema(keywordMap)
}
function getChild(object, key) {
var keys = Object.keys(object)
var regex
var isArrayAccess = /^\d+$/.test(key)
if(isArrayAccess && isArray(object)) {
return object[0]
}
for (var i = 0; i < keys.length; i++) {
let childVal = object[keys[i]]
if (!childVal) {
return null
}
regex = new RegExp(childVal.__regex || keys[i])
if (regex.test(key) && childVal) {
if(typeof childVal === "object" && !isArray(childVal)) {
return Object.assign({}, childVal)
} else {
return childVal
}
}
}
}
function suggestionFromSchema(map) {
const res = toArray(mapValues(map, (val, key) => {
const keyword = get(val, "__value", key)
const meta = isPlainObject(val) ? "object" : "keyword"
return constructAceCompletion(meta, keyword)
}))
return res
}
function constructAceCompletion(meta, keyword) {
if(keyword.slice(0, 2) === "__") {
return {}
}
// Give keywords, that extra colon
let snippet
switch(meta) {
case "keyword":
snippet = `${keyword}: `
break
case "object":
snippet = `${keyword}:\n `
break
default:
snippet = keyword
}
// snippet's treat `$` as special characters
snippet = snippet.replace("$", "\\$")
return {
snippet,
caption: keyword,
score: 300,
meta,
}
}
function isNumeric(obj) {
return !isNaN(obj)
}

View File

@ -0,0 +1,11 @@
import * as wrapActions from "./wrap-actions"
export default function EditorAutosuggestOAS3KeywordsPlugin() {
return {
statePlugins: {
editor: {
wrapActions,
}
}
}
}

View File

@ -0,0 +1,20 @@
import {
ExternalDocumentation,
Info,
SecurityRequirement,
Server,
Tag,
Components,
Paths
} from "./oas3-objects.js"
export default {
openapi: String,
info: Info,
servers: [Server],
paths: Paths,
components: Components,
security: [SecurityRequirement],
tags: [Tag],
externalDocs: ExternalDocumentation
}

View File

@ -0,0 +1,331 @@
// Adapted from OAS 3.0.0-rc2
// comma dangles in this file = cleaner diffs
/*eslint comma-dangle: ["error", "always-multiline"]*/
// anyOf and combine are the same for now.
// they are seperated for semantics, and for possible future improvement
const anyOf = (...objs) => objs ? Object.assign({}, ...objs) : {}
const stringEnum = (arr) => arr
const Any = null
export const ExternalDocumentation = {
description: String,
url: String,
}
export const Contact = {
name: String,
url: String,
email: String,
}
export const License = {
name: String,
url: String,
}
export const Info = {
title: String,
description: String,
termsOfService: String,
contact: Contact,
license: License,
version: String,
}
export const ServerVariable = {
enum: [String],
default: String,
description: String,
}
export const XML = {
name: String,
namespace: String,
prefix: String,
attribute: Boolean,
wrapped: Boolean,
}
export const OAuthFlow = {
authorizationUrl: String,
tokenUrl: String,
refreshUrl: String,
scopes: {
".": String,
},
}
export const Reference = {
"$ref": String,
}
export const Example = {
summary: String,
description: String,
value: Any,
externalValue: String,
}
export const SecurityRequirement = {
".": [String],
}
export const Server = {
url: String,
description: String,
variables: {
".": ServerVariable,
},
}
export const Link = {
operationRef: String,
operationId: String,
parameters: {
".": Any,
},
requestBody: Any,
description: String,
server: Server,
}
export const Schema = {
// Lifted from JSONSchema
title: String,
multipleOf: String,
maximum: String,
exclusiveMaximum: String,
minimum: String,
exclusiveMinimum: String,
maxLength: String,
minLength: String,
pattern: RegExp,
maxItems: String,
minItems: String,
uniqueItems: Boolean,
maxProperties: String,
minProperties: String,
required: Boolean,
enum: String,
// Adapted from JSONSchema
type: String,
get allOf () { return this },
get oneOf () { return this },
get anyOf () { return this },
get not () { return this },
get items () { return this },
get properties () {
return {
".": this,
}
},
get additionalProperties () { return this },
description: String,
format: String,
default: Any,
nullable: Boolean,
readOnly: Boolean,
writeOnly: Boolean,
xml: XML,
externalDocs: ExternalDocumentation,
example: Any,
deprecated: Boolean,
}
export const Encoding = {
contentType: String,
headers: {
".": undefined,
},
style: stringEnum(["matrix", "label", "form", "simple", "spaceDelimited", "pipeDelimited", "deepObject"]),
explode: Boolean,
allowReserved: Boolean,
}
export const MediaType = {
schema: anyOf(Schema, Reference),
example: Any,
examples: {
".": anyOf(Example, Reference),
},
encoding: {
".": Encoding,
},
}
export const Parameter = {
name: String,
in: stringEnum(["query", "header", "path", "cookie"]),
description: String,
required: Boolean,
deprecated: Boolean,
allowEmptyValue: Boolean,
style: stringEnum(["matrix", "label", "form", "simple", "spaceDelimited", "pipeDelimited", "deepObject"]),
explode: String,
allowReserved: Boolean,
schema: anyOf(Schema, Reference),
example: Any,
examples: {
".": anyOf(Example, Reference),
},
content: {
".": MediaType,
},
}
export const Header = {
description: String,
required: Boolean,
deprecated: Boolean,
allowEmptyValue: Boolean,
style: stringEnum(["matrix", "label", "form", "simple", "spaceDelimited", "pipeDelimited", "deepObject"]),
explode: String,
allowReserved: Boolean,
schema: anyOf(Schema, Reference),
example: Any,
examples: {
".": anyOf(Example, Reference),
},
content: {
".": MediaType,
},
}
export const RequestBody = {
description: String,
content: {
".": MediaType,
},
}
export const Response = {
description: String,
headers: {
".": anyOf(Header, Reference),
},
content: {
".": MediaType,
},
links: {
".": anyOf(Link, Reference),
},
}
export const Responses = {
default: anyOf(Response, Reference),
"\\d\\d\\d|\\d\\dX|\\dXX": anyOf(Response, Reference),
}
export const Callback = {
// ".": PathItem,
}
export const Tag = {
name: String,
description: String,
externalDocs: ExternalDocumentation,
}
export const OAuthFlows = {
implicit: OAuthFlow,
password: OAuthFlow,
clientCredentials: OAuthFlow,
authorizationCode: OAuthFlow,
}
export const SecurityScheme = {
type: String,
description: String,
name: String,
in: String,
scheme: String,
bearerFormat: String,
flows: OAuthFlows,
openIdConnectUrl: String,
}
const ComponentFixedFieldRegex = "^[a-zA-Z0-9\.\-_]+$"
export const Components = {
schemas: {
[ComponentFixedFieldRegex]: anyOf(Schema, Reference),
},
responses: {
[ComponentFixedFieldRegex]: anyOf(Response, Reference),
},
parameters: {
[ComponentFixedFieldRegex]: anyOf(Parameter, Reference),
},
examples: {
[ComponentFixedFieldRegex]: anyOf(Example, Reference),
},
requestBodies: {
[ComponentFixedFieldRegex]: anyOf(RequestBody, Reference),
},
headers: {
[ComponentFixedFieldRegex]: anyOf(Header, Reference),
},
securitySchemes: {
[ComponentFixedFieldRegex]: anyOf(SecurityScheme, Reference),
},
links: {
[ComponentFixedFieldRegex]: anyOf(Link, Reference),
},
callbacks: {
get [ComponentFixedFieldRegex]() { return anyOf(Callback, Reference) },
},
}
export const Operation = {
tags: [String],
summary: String,
description: String,
externalDocs: ExternalDocumentation,
operationId: String,
parameters: [anyOf(Parameter, Reference)],
requestBody: anyOf(RequestBody, Reference),
responses: Responses,
get callbacks() {
return {
".": anyOf(Callback, Reference),
}
},
deprecated: Boolean,
security: [SecurityRequirement],
servers: [Server],
}
export const Discriminator = {
propertyName: String,
mapping: {
".": String,
},
}
export const PathItem = anyOf(Reference, {
summary: String,
description: String,
get: Operation,
put: Operation,
post: Operation,
delete: Operation,
options: Operation,
head: Operation,
patch: Operation,
trace: Operation,
servers: Server,
parameters: anyOf(Parameter, Reference),
})
export const Paths = {
"/.": PathItem,
}
// solves `PathItem -> Operation -> Callback -> PathItem` circular reference
Callback["."] = PathItem
// solves `Encoding -> Header -> MediaType -> Encoding` circular reference
Encoding.headers["."] = Header

View File

@ -0,0 +1,11 @@
import getCompletions from "./get-completions"
// Add an autosuggest completer
export const addAutosuggestionCompleters = (ori, system) => (context) => {
return ori(context).concat([{
getCompletions(...args) {
// Add `context`, then `system` as the last args
return getCompletions(...args, context, system)
}
}])
}

View File

@ -0,0 +1,12 @@
import getRefsForPath from "./get-refs-for-path"
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
const { fn: { getPathForPosition } } = system
const { AST } = ctx
var editorValue = editor.getValue()
const path = getPathForPosition({ pos, prefix, editorValue, AST})
const suggestions = getRefsForPath({ system, path})
cb(null, suggestions)
}

View File

@ -0,0 +1,24 @@
import isArray from "lodash/isArray"
import last from "lodash/last"
export default function getRefsForPath({ system, path }) {
// Note fellow ace hackers:
// we have to be weary of _what_ ace will filter on, see the order ( probably should be fixed, but... ): https://github.com/ajaxorg/ace/blob/b219b5584456534fbccb5fb20470c61011fa0b0a/lib/ace/autocomplete.js#L469
// Because of that, I'm matching on `caption` and using `snippet` instead of `value` for injecting
if(isArray(path) && last(path) === "$ref") {
const localRefs = system.specSelectors.localRefs()
const refType = system.specSelectors.getRefType(path)
return localRefs
.filter(r => r.get("type") == refType)
.toJS()
.map(r => ({
score: 100,
meta: "local",
snippet: `'${r.$ref}'`, // wrap in quotes
caption: r.name,
}))
}
return []
}

View File

@ -0,0 +1,11 @@
import * as wrapActions from "./wrap-actions"
export default function EditorAutosuggestRefsPlugin() {
return {
statePlugins: {
editor: {
wrapActions,
}
}
}
}

View File

@ -0,0 +1,11 @@
import getCompletions from "./get-completions"
// Add an autosuggest completer
export const addAutosuggestionCompleters = (ori, system) => (context) => {
return ori(context).concat([{
getCompletions(...args) {
// Add `context`, then `system` as the last args
return getCompletions(...args, context, system)
}
}])
}

View File

@ -0,0 +1,21 @@
import snippets from "./snippets"
import getSnippetsForPath from "./get-snippets-for-path"
export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) {
const { fn: { getPathForPosition }, specSelectors } = system
const { isOAS3 } = specSelectors
if(isOAS3 && isOAS3()) {
// isOAS3 selector exists, and returns true
return cb(null, null)
}
const { AST } = ctx
const editorValue = editor.getValue()
const path = getPathForPosition({ pos, prefix, editorValue, AST})
const suggestions = getSnippetsForPath({ path, snippets})
return cb(null, suggestions)
}

View File

@ -0,0 +1,49 @@
import isArray from "lodash/isArray"
export default function getSnippetsForPath({ path, snippets }) {
// find all possible snippets, modify them to be compatible with Ace and
// sort them based on their position. Sorting is done by assigning a score
// to each snippet, not by sorting the array
if (!isArray(path)) {
return []
}
return snippets
.filter(snippet => {
return snippet.path.length === path.length
})
.filter(snippet => {
return snippet.path.every((k, i) => {
return !!(new RegExp(k)).test(path[i])
})
})
.map(snippet => {
// change shape of snippets for ACE
return {
caption: snippet.name,
snippet: snippet.content,
meta: "snippet"
}
})
.map(snippetSorterForPos(path))
}
export function snippetSorterForPos(path) {
return function(snippet) {
// by default score is high
let score = 1000
// if snippets content has the keyword it will get a lower score because
// it's more likely less relevant
// (FIX) is this logic work for all cases?
path.forEach(function(keyword) {
if (snippet.snippet.indexOf(keyword)) {
score = 500
}
})
snippet.score = score
return snippet
}
}

View File

@ -0,0 +1,11 @@
import * as wrapActions from "./wrap-actions"
export default function EditorAutosuggestSnippetsPlugin() {
return {
statePlugins: {
editor: {
wrapActions,
}
}
}
}

View File

@ -0,0 +1,192 @@
const operationRegex = "get|put|post|delete|options|head|patch"
/**
* Makes an HTTP operation snippet's content based on operation name
*
* @param {string} operationName - the HTTP verb
*
* @return {string} - the snippet content for that operation
*/
function makeOperationSnippet(operationName) {
return [
"${1:" + operationName + "}:",
" summary: ${2}",
" description: ${2}",
" responses:",
" ${3:200:}",
" description: ${4:OK}",
"${6}"
].join("\n")
}
/**
* Makes an HTTP response code snippet's content based on code
*
* @param {string} code - HTTP Response Code
*
* @return {string} - Snippet content
*/
function makeResponseCodeSnippet(code) {
return [
"${1:" + code + "}:",
" description: ${2}",
"${3}"
].join("\n")
}
export default [
{
name: "swagger",
trigger: "sw",
path: [],
content: [
"swagger: \'2.0\'",
"${1}"
].join("\n")
},
{
name: "info",
trigger: "info",
path: [],
content: [
"info:",
" version: ${1:0.0.0}",
" title: ${2:title}",
" description: ${3:description}",
" termsOfService: ${4:terms}",
" contact:",
" name: ${5}",
" url: ${6}",
" email: ${7}",
" license:",
" name: ${8:MIT}",
" url: ${9:http://opensource.org/licenses/MIT}",
"${10}"
].join("\n")
},
{
name: "get",
trigger: "get",
path: ["paths", "."],
content: makeOperationSnippet("get")
},
{
name: "post",
trigger: "post",
path: ["paths", "."],
content: makeOperationSnippet("post")
},
{
name: "put",
trigger: "put",
path: ["paths", "."],
content: makeOperationSnippet("put")
},
{
name: "delete",
trigger: "delete",
path: ["paths", "."],
content: makeOperationSnippet("delete")
},
{
name: "patch",
trigger: "patch",
path: ["paths", "."],
content: makeOperationSnippet("patch")
},
{
name: "options",
trigger: "options",
path: ["paths", "."],
content: makeOperationSnippet("options")
},
// operation level parameter
{
name: "parameter",
trigger: "param",
path: ["paths", ".", ".", "parameters"],
content: [
"- name: ${1:parameter_name}",
" in: ${2:query}",
" description: ${3:description}",
" type: ${4:string}",
"${5}"
].join("\n")
},
// path level parameter
{
name: "parameter",
trigger: "param",
path: ["paths", ".", "parameters"],
content: [
"- name: ${1:parameter_name}",
" in: ${2:path}",
" required: true",
" description: ${3:description}",
" type: ${4:string}",
"${5}"
].join("\n")
},
{
name: "response",
trigger: "resp",
path: ["paths", ".", ".", "responses"],
content: [
"${1:code}:",
" description: ${2}",
" schema: ${3}",
"${4}"
].join("\n")
},
{
name: "200",
trigger: "200",
path: ["paths", ".", operationRegex, "responses"],
content: makeResponseCodeSnippet("200")
},
{
name: "300",
trigger: "300",
path: ["paths", ".", operationRegex, "responses"],
content: makeResponseCodeSnippet("300")
},
{
name: "400",
trigger: "400",
path: ["paths", ".", operationRegex, "responses"],
content: makeResponseCodeSnippet("400")
},
{
name: "500",
trigger: "500",
path: ["paths", ".", operationRegex, "responses"],
content: makeResponseCodeSnippet("500")
},
{
name: "model",
trigger: "mod|def",
regex: "mod|def",
path: ["definitions"],
content: [
"${1:ModelName}:",
" type: object",
" properties:",
" ${2}"
]
}
]

View File

@ -0,0 +1,11 @@
import getCompletions from "./get-completions"
// Add an autosuggest completer
export const addAutosuggestionCompleters = (ori, system) => (context) => {
return ori(context).concat([{
getCompletions(...args) {
// Add `context`, then `system` as the last args
return getCompletions(...args, context, system)
}
}])
}

View File

@ -0,0 +1,15 @@
// Enable Ace editor autocompletions
export const enableAutocompletions = ({editor}) => () => {
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true
})
}
// Add completers. Just override this method. And concat on your completer(s)
// see: https://github.com/ajaxorg/ace/blob/master/lib/ace/autocomplete.js
// eg: return ori(...args).concat({ getCompletions() {...}})
export const addAutosuggestionCompleters = () => () => {
return []
}

View File

@ -0,0 +1,73 @@
export function getPathForPosition({ pos: originalPos, prefix, editorValue, AST }) {
var pos = Object.assign({}, originalPos)
var lines = editorValue.split(/\r\n|\r|\n/)
var previousLine = lines[pos.row - 1] || ""
var currentLine = lines[pos.row]
var nextLine = lines[pos.row + 1] || ""
var prepared = false
// we're always at the document root when there's no indentation,
// so let's save some effort
if (pos.column === 1) {
return []
}
let prevLineIndent = getIndent(previousLine).length
let currLineIndent = getIndent(currentLine).length
const isCurrentLineEmpty = currentLine.replace(prefix, "").trim() === ""
if(
(previousLine.trim()[0] === "-" || nextLine.trim()[0] === "-")
&& currLineIndent >= prevLineIndent
&& isCurrentLineEmpty
) {
// for arrays with existing items under it, on blank lines
// example:
// myArray:
// - a: 1
// | <-- user cursor
currentLine += "- a: b" // fake array item
// pos.column += 1
prepared = true
}
// if current position is in at a free line with whitespace insert a fake
// key value pair so the generated AST in ASTManager has current position in
// editing node
if ( !prepared && isCurrentLineEmpty) {
currentLine += "a: b" // fake key value pair
pos.column += 1
prepared = true
}
if(currentLine[currentLine.length - 1] === ":") {
// Add a space if a user doesn't put one after a colon
// NOTE: this doesn't respect the "prepared" flag.
currentLine += " "
pos.column += 1
}
//if prefix is empty then add fake, empty value
if( !prepared && !prefix){
// for scalar values with no values
// i.e. "asdf: "
currentLine += "~"
}
// append inserted character in currentLine for better AST results
lines[originalPos.row] = currentLine
editorValue = lines.join("\n")
let path = AST.pathForPosition(editorValue, {
line: pos.row,
column: pos.column
})
return path
}
function getIndent(str) {
let match = str.match(/^ +/)
return match ? match[0] : ""
}

View File

@ -0,0 +1,46 @@
export function wrapCompleters(completers, cutoff = 100) {
let isLiveCompletionDisabled = false
let lastSpeeds = []
let isPerformant = () => lastSpeeds.every(speed => speed < cutoff)
if(cutoff === 0 || cutoff === "0") {
// never disable live autocomplete
return completers
}
return completers.map((completer, i) => {
let ori = completer.getCompletions
completer.getCompletions = function(editor, session, pos, prefix, callback) {
let startTime = Date.now()
try {
ori(editor, session, pos, prefix, (...args) => {
let msElapsed = Date.now() - startTime
lastSpeeds[i] = msElapsed
if(isLiveCompletionDisabled && isPerformant()) {
console.warn("Manual autocomplete was performant - re-enabling live autocomplete")
editor.setOptions({
enableLiveAutocompletion: true
})
isLiveCompletionDisabled = false
}
if(msElapsed > cutoff && editor.getOption("enableLiveAutocompletion")) {
console.warn("Live autocomplete is slow - disabling it")
editor.setOptions({
enableLiveAutocompletion: false
})
isLiveCompletionDisabled = true
}
callback(...args)
})
} catch(e) {
console.error("Autocompleter encountered an error")
console.error(e)
callback(null, [])
}
}
return completer
})
}

View File

@ -0,0 +1,36 @@
import * as actions from "./actions"
import * as fn from "./fn"
import * as specSelectors from "./spec-selectors"
import { wrapCompleters } from "./helpers"
export default function EditorAutosuggestPlugin() {
return {
fn,
statePlugins: {
spec: {
selectors: specSelectors,
},
editor: {
actions,
wrapActions: {
onLoad: (ori, sys) => (context) => {
const { editor } = context
// Any other calls for editor#onLoad
ori(context)
// Enable autosuggestions ( aka: autocompletions )
sys.editorActions.enableAutocompletions(context)
// Add completers ( for autosuggestions )
const completers = sys.editorActions.addAutosuggestionCompleters(context)
const cutoff = sys.getConfigs().liveAutocompleteCutoff
const wrappedCompleters = wrapCompleters(completers || [], cutoff)
editor.completers = wrappedCompleters
return
}
}
}
}
}
}

View File

@ -0,0 +1,61 @@
import { createSelector } from "reselect"
import { Set, Map } from "immutable"
import { escapeJsonPointerToken } from "../refs-util"
const SWAGGER2_REF_MAP = {
"paths": "pathitems",
"definitions": "definitions",
"schema": "definitions",
"parameters": "parameters",
"responses": "responses"
}
const OAS3_REF_MAP = {
schemas: "components/schemas", // for Schemas within Components
schema: "components/schemas", // for Schemas throughout document
parameters: "components/parameters",
requestBody: "components/requestBodies",
callbacks: "components/callbacks",
examples: "components/examples",
responses: "components/responses",
headers: "components/headers",
links: "components/links"
}
const SWAGGER2_TYPES = Set(Object.values(SWAGGER2_REF_MAP))
const OAS3_TYPES = Set(Object.values(OAS3_REF_MAP))
// Return a normalized "type" for a given path [a,b,c]
// eg: /definitions/bob => definition
// /paths/~1pets/responses/200/schema => definition ( because of schema )
export const getRefType = (state, path) => (sys) => createSelector(
() => {
for( var i=path.length-1; i>-1; i-- ) {
let tag = path[i]
if(sys.specSelectors.isOAS3 && sys.specSelectors.isOAS3()) {
if(OAS3_REF_MAP[tag]) {
return OAS3_REF_MAP[tag]
}
} else if( SWAGGER2_REF_MAP[tag] ) {
return SWAGGER2_REF_MAP[tag]
}
}
return null
})(state)
export const localRefs = (state) => (sys) => createSelector(
sys.specSelectors.spec,
sys.specSelectors.isOAS3 || (() => false),
(spec, isOAS3) => {
return (isOAS3 ? OAS3_TYPES : SWAGGER2_TYPES).toList().flatMap( type => {
return spec
.getIn(type.split("/"), Map({}))
.keySeq()
.map( name => Map({
name,
type,
$ref: `#/${type}/${escapeJsonPointerToken(name)}`,
}))
})
}
)(state)

View File

@ -0,0 +1,15 @@
export default function(system) {
return {
rootInjects: {
getEditorMetadata() {
const allErrors = system.errSelectors.allErrors()
return {
contentString: system.specSelectors.specStr(),
contentObject: system.specSelectors.specJson().toJS(),
isValid: allErrors.size === 0,
errors: allErrors.toJS()
}
}
}
}
}

View File

@ -0,0 +1,13 @@
export const JUMP_TO_LINE = "jump_to_line"
export function jumpToLine(line) {
return {
type: JUMP_TO_LINE,
payload: line
}
}
// This is a hook. Will have editor instance
// It needs to be an async-function, to avoid dispatching an object to the reducer
export const onLoad = () => () => {}

View File

@ -0,0 +1,6 @@
/* global ace */
ace.define("ace/snippets/yaml",
["require","exports","module"], function(e,t,n){ // eslint-disable-line no-unused-vars
t.snippetText=undefined
t.scope="yaml"
})

View File

@ -0,0 +1,65 @@
import React from "react"
import PropTypes from "prop-types"
export default class EditorContainer extends React.Component {
// This is already debounced by editor.jsx
onChange = (value) => {
this.props.onChange(value)
}
render() {
let { specSelectors, getComponent, errSelectors, fn, editorSelectors, configsSelectors } = this.props
let Editor = getComponent("Editor")
let wrapperClasses = ["editor-wrapper"]
const readOnly = !!configsSelectors.get("readOnly")
if(readOnly) {
wrapperClasses.push("read-only")
}
let propsForEditor = this.props
const editorOptions = {
enableLiveAutocompletion: configsSelectors.get("editorLiveAutocomplete"),
readOnly: readOnly,
highlightActiveLine: !readOnly,
highlightGutterLine: !readOnly,
}
return (
<div id='editor-wrapper' className={wrapperClasses.join(" ")}>
{ readOnly ? <h2 className="editor-readonly-watermark">Read Only</h2> : null }
<Editor
{...propsForEditor}
value={specSelectors.specStr()}
origin={specSelectors.specOrigin()}
editorOptions={editorOptions}
specObject={specSelectors.specJson().toJS()}
errors={errSelectors.allErrors()}
onChange={this.onChange}
goToLine={editorSelectors.gotoLine()}
AST={fn.AST}
/>
</div>
)
}
}
EditorContainer.defaultProps = {
onChange: Function.prototype
}
EditorContainer.propTypes = {
specActions: PropTypes.object.isRequired,
configsSelectors: PropTypes.object.isRequired,
onChange: PropTypes.func,
fn: PropTypes.object,
specSelectors: PropTypes.object.isRequired,
errSelectors: PropTypes.object.isRequired,
editorSelectors: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
}

View File

@ -0,0 +1,326 @@
import React from "react"
import PropTypes from "prop-types"
import AceEditor from "react-ace"
import editorPluginsHook from "../editor-plugins/hook"
import { placeMarkerDecorations } from "../editor-helpers/marker-placer"
import Im, { fromJS } from "immutable"
import ImPropTypes from "react-immutable-proptypes"
import win from "src/window"
import isUndefined from "lodash/isUndefined"
import omit from "lodash/omit"
import isEqual from "lodash/isEqual"
import debounce from "lodash/debounce"
import ace from "brace"
import "brace/mode/yaml"
import "brace/theme/tomorrow_night_eighties"
import "brace/ext/language_tools"
import "brace/ext/searchbox"
import "./brace-snippets-yaml"
const NOOP = Function.prototype // Apparently the best way to no-op
export default function makeEditor({ editorPluginsToRun }) {
class Editor extends React.Component {
constructor(props, context) {
super(props, context)
this.editor = null
this.debouncedOnChange = props.debounce > 0
? debounce(props.onChange, props.debounce)
: props.onChange
}
static propTypes = {
specId: PropTypes.string,
value: PropTypes.string,
editorOptions: PropTypes.object,
origin: PropTypes.string,
debounce: PropTypes.number,
onChange: PropTypes.func,
onMarkerLineUpdate: PropTypes.func,
markers: PropTypes.object,
goToLine: PropTypes.object,
specObject: PropTypes.object.isRequired,
editorActions: PropTypes.object,
AST: PropTypes.object.isRequired,
errors: ImPropTypes.list,
}
static defaultProps = {
value: "",
specId: "--unknown--",
origin: "not-editor",
onChange: NOOP,
onMarkerLineUpdate: NOOP,
markers: {},
goToLine: {},
errors: fromJS([]),
editorActions: {onLoad(){}},
editorOptions: {},
debounce: 800 // 0.5 imperial seconds
}
checkForSilentOnChange = (value) => {
if(!this.silent) {
this.debouncedOnChange(value)
}
}
onLoad = (editor) => {
const { props } = this
const { AST, specObject } = props
const langTools = ace.acequire("ace/ext/language_tools")
const session = editor.getSession()
this.editor = editor
// fixes a warning, see https://github.com/ajaxorg/ace/issues/2499
editor.$blockScrolling = Infinity
session.setUseWrapMode(true)
session.on("changeScrollLeft", xPos => { // eslint-disable-line no-unused-vars
session.setScrollLeft(0)
})
// TODO Remove this in favour of editorActions.onLoad
editorPluginsHook(editor, props, editorPluginsToRun || [], {
langTools, AST, specObject
})
editor.setHighlightActiveLine(false)
editor.setHighlightActiveLine(true)
this.syncOptionsFromState(props.editorOptions)
if(props.editorActions && props.editorActions.onLoad)
props.editorActions.onLoad({...props, langTools, editor})
this.updateMarkerAnnotations(this.props)
}
onResize = () => {
const { editor } = this
if(editor) {
let session = editor.getSession()
editor.resize()
let wrapLimit = session.getWrapLimit()
editor.setPrintMarginColumn(wrapLimit)
}
}
onClick = () => {
// onClick is deferred by 40ms, to give element resizes time to settle.
setTimeout(() => {
if(this.getWidth() !== this.width) {
this.onResize()
this.width = this.getWidth()
}
}, 40)
}
getWidth = () => {
let el = win.document.getElementById("editor-wrapper")
return el ? el.getBoundingClientRect().width : null
}
updateErrorAnnotations = (nextProps) => {
if(this.editor && nextProps.errors) {
let editorAnnotations = nextProps.errors.toJS().map(err => {
// Create annotation objects that ACE can use
return {
row: err.line - 1,
column: 0,
type: err.level,
text: err.message
}
})
this.editor.getSession().setAnnotations(editorAnnotations)
}
}
updateMarkerAnnotations = (props) => {
const { editor } = this
const markers = Im.Map.isMap(props.markers) ? props.markers.toJS() : {}
this._removeMarkers = placeMarkerDecorations({
editor,
markers,
onMarkerLineUpdate: props.onMarkerLineUpdate,
})
}
removeMarkers = () => {
if(this._removeMarkers) {
this._removeMarkers()
this._removeMarkers = null
}
}
shouldUpdateYaml = (props) => {
// No editor instance
if(!this.editor)
return false
// Origin is editor
if(props.origin === "editor")
return false
// Redundant
if(this.editor.getValue() === props.value)
return false
// Value and origin are same, no update.
if(this.props.value === props.value
&& this.props.origin === props.origin)
return false
return true
}
shouldUpdateMarkers = (props) => {
const { markers } = props
if(Im.Map.isMap(markers)) {
return !Im.is(markers, this.props.markers) // Different from previous?
}
return true // Not going to do a deep compare of object-like markers
}
updateYamlAndMarkers = (props) => {
// If we update the yaml, we need to "lift" the yaml first
if(this.shouldUpdateYaml(props)) {
this.removeMarkers()
this.updateYaml(props)
this.updateMarkerAnnotations(props)
} else if (this.shouldUpdateMarkers(props)) {
this.removeMarkers()
this.updateMarkerAnnotations(props)
}
}
updateYaml = (props) => {
if (props.origin === "insert") {
// Don't clobber the undo stack in this case.
this.editor.session.doc.setValue(props.value)
this.editor.selection.clearSelection()
} else {
// session.setValue does not trigger onChange, nor add to undo stack.
// Neither of which we want here.
this.editor.session.setValue(props.value)
}
}
syncOptionsFromState = (editorOptions={}) => {
const { editor } = this
if(!editor) {
return
}
const setOptions = omit(editorOptions, ["readOnly"])
editor.setOptions(setOptions)
const readOnly = isUndefined(editorOptions.readOnly)
? false
: editorOptions.readOnly // If its undefined, default to false.
editor.setReadOnly(readOnly)
}
componentWillMount() {
// add user agent info to document
// allows our custom Editor styling for IE10 to take effect
var doc = win.document.documentElement
doc.setAttribute("data-useragent", win.navigator.userAgent)
this.syncOptionsFromState(this.props.editorOptions)
}
componentDidMount() {
// eslint-disable-next-line react/no-did-mount-set-state
this.width = this.getWidth()
win.document.addEventListener("click", this.onClick)
}
componentWillUnmount() {
win.document.removeEventListener("click", this.onClick)
}
componentWillReceiveProps(nextProps) {
let hasChanged = (k) => !isEqual(nextProps[k], this.props[k])
const editor = this.editor
// Change the debounce value/func
if(this.props.debounce !== nextProps.debounce) {
if(this.debouncedOnChange.flush)
this.debouncedOnChange.flush()
this.debouncedOnChange = nextProps.debounce > 0
? debounce(nextProps.onChange, nextProps.debounce)
: nextProps.onChange
}
this.updateYamlAndMarkers(nextProps)
this.updateErrorAnnotations(nextProps)
if(hasChanged("editorOptions")) {
this.syncOptionsFromState(nextProps.editorOptions)
}
if(editor && nextProps.goToLine && nextProps.goToLine.line && hasChanged("goToLine")) {
editor.gotoLine(nextProps.goToLine.line)
nextProps.editorActions.jumpToLine(null)
}
}
shouldComponentUpdate() {
return false // Never update, see: componentWillRecieveProps and this.updateYaml for where we update things.
}
render() {
// NOTE: we're manually managing the value lifecycle, outside of react render
// This will only render once.
return (
<AceEditor
mode="yaml"
theme="tomorrow_night_eighties"
value={this.props.value /* This will only load once, thereafter it'll be via updateYaml */}
onLoad={this.onLoad}
onChange={this.checkForSilentOnChange}
name="ace-editor"
width="100%"
height="100%"
tabSize={2}
fontSize={14}
useSoftTabs="true"
wrapEnabled={true}
editorProps={{
"display_indent_guides": true,
folding: "markbeginandend"
}}
setOptions={{
cursorStyle: "smooth",
wrapBehavioursEnabled: true
}}
/>
)
}
}
return Editor
}

View File

@ -0,0 +1,51 @@
// This code is registered as a helper, not a plugin, because its lifecycle is
// unique to the needs of the marker placement logic.
import countBy from "lodash/countBy"
import map from "lodash/map"
let removers = []
function setRemovers(arr) {
removers.forEach(fn => fn()) // remove existing anchors & gutters
removers = arr // use parent scope to persist reference
}
export function placeMarkerDecorations({editor, markers, onMarkerLineUpdate}) {
if(typeof editor !== "object") {
return
}
let markerLines = countBy(Object.values(markers), "position")
let removeFns = map(markerLines, (count, line) => {
let className = `editor-marker-${count > 8 ? "9-plus" : count}`
let s = editor.getSession()
let anchor = s.getDocument().createAnchor(+line, 0)
anchor.setPosition(+line, 0) // noClip = true
s.addGutterDecoration(+line, className)
anchor.on("change", function (e) {
var oldLine = e.old.row
var newLine = e.value.row
s.removeGutterDecoration(oldLine, className)
s.addGutterDecoration(newLine, className)
onMarkerLineUpdate([oldLine, newLine, line])
})
return function () {
// // Remove the anchor & decoration
let currentLine = +anchor.getPosition().row
editor.getSession().removeGutterDecoration(currentLine, className)
anchor.detach()
}
})
setRemovers(removeFns)
// To manually remove them
return () => setRemovers([])
}

View File

@ -0,0 +1,16 @@
import isFunction from "lodash/isFunction"
export default function(editor, { onGutterClick }) {
editor.on("guttermousedown", (e) => {
let editor = e.editor
let line = e.getDocumentPosition().row
let region = editor.renderer.$gutterLayer.getRegion(e)
e.stop()
if(isFunction(onGutterClick)) {
onGutterClick({ region, line })
}
})
}

View File

@ -0,0 +1,22 @@
// TODO: Turn these into actions, that we can override
import GutterClick from "./gutter-click"
import JsonToYaml from "./json-to-yaml"
import TabHandler from "./tab-handler"
const plugins = [
{fn: GutterClick, name: "gutterClick"},
{fn: JsonToYaml, name: "jsonToYaml"},
{fn: TabHandler, name: "tabHandler"},
]
export default function (editor, props = {}, editorPluginsToRun = [], helpers = {}) {
plugins
.filter(plugin => ~editorPluginsToRun.indexOf(plugin.name))
.forEach( plugin => {
try {
plugin.fn(editor, props, helpers)
} catch(e) {
console.error(`${plugin.name || ""} plugin error:`, e)
}
})
}

View File

@ -0,0 +1,53 @@
import YAML from "js-yaml"
export default function(editor) {
editor.on("paste", e => {
const originalStr = e.text
if (!isJSON(originalStr)) {
return
}
let yamlString
try {
yamlString = YAML.safeDump(YAML.safeLoad(originalStr), {
lineWidth: -1 // don't generate line folds
})
} catch (e) {
return
}
if (!confirm("Would you like to convert your JSON into YAML?")) {
return
}
// using SelectionRange instead of CursorPosition, because:
// SR.start|end === CP when there's no selection
// and it catches indentation edge cases when there is one
const padding = makePadding(editor.getSelectionRange().start.column)
// update the pasted content
e.text = yamlString
.split("\n")
.map((line, i) => i == 0 ? line : padding + line) // don't pad first line, it's already indented
.join("\n")
.replace(/\t/g, " ") // tabs -> spaces, just to be sure
})
}
function isJSON (str){
// basic test: "does this look like JSON?"
let regex = /^[ \r\n\t]*[{\[]/
return regex.test(str)
}
function makePadding(len) {
let str = ""
while(str.length < len) {
str += " "
}
return str
}

View File

@ -0,0 +1,7 @@
export default function(editor) {
// NOTE: react-ace has an onPaste prop.. we could refactor to that.
editor.on("paste", e => {
// replace all U+0009 tabs in pasted string with two spaces
e.text = e.text.replace(/\t/g, " ")
})
}

View File

@ -0,0 +1,23 @@
import makeEditor from "./components/editor"
import EditorContainer from "./components/editor-container"
import * as actions from "./actions"
import reducers from "./reducers"
import * as selectors from "./selectors"
import EditorSpecPlugin from "./spec"
let Editor = makeEditor({
editorPluginsToRun: ["gutterClick", "jsonToYaml", "pasteHandler"]
})
export default function () {
return [EditorSpecPlugin, {
components: { Editor, EditorContainer },
statePlugins: {
editor: {
reducers,
actions,
selectors
}
}
}]
}

View File

@ -0,0 +1,9 @@
import {
JUMP_TO_LINE
} from "./actions"
export default {
[JUMP_TO_LINE]: (state, { payload } ) =>{
return state.set("gotoLine", { line: payload })
}
}

View File

@ -0,0 +1,13 @@
import { createSelector } from "reselect"
import Im from "immutable"
const state = state => {
return state || Im.Map()
}
export const gotoLine = createSelector(
state,
state => {
return state.get("gotoLine") || null
}
)

View File

@ -0,0 +1,34 @@
const SPEC_UPDATE_ORIGIN = "spec_update_spec_origin"
// wraps updateSpec to include the "origin" parameter, defaulting to "not-editor"
// Includes a selector to get the origin, specSelectors.specOrigin
export default function EditorSpecPlugin() {
return {
statePlugins: {
spec: {
wrapActions: {
updateSpec: (ori, system) => (specStr, origin) => {
system.specActions.updateSpecOrigin(origin)
ori(specStr)
}
},
reducers: {
[SPEC_UPDATE_ORIGIN]: (state, action) => {
return state.set("specOrigin", action.payload)
}
},
selectors: {
specOrigin: (state) => state.get("specOrigin") || "not-editor"
},
actions: {
updateSpecOrigin(origin="not-editor") {
return {
payload: origin+"",
type: SPEC_UPDATE_ORIGIN,
}
}
}
}
}
}
}

View File

@ -0,0 +1,44 @@
A guide to custom patterns used in the JSON Schema validation documents in this folder...
### Pivot key existential switch
Applies a schema to an object based on whether a key exists in an object.
> "If key A exists on the object, apply schema X. Else, apply schema Y."
```yaml
switch:
- if:
required: [a]
then:
description: schema X; within `then` can be any JSON Schema content
- then:
description: schema Y; within `then` can be any JSON Schema content
```
### Pivot key value switch
Applies a schema to an object based on the value of a specific, always-required key (the "pivot key").
> "If key A is foo, apply schema X. Else, if key A is bar, apply schema Y. Else, tell the user that key A must be foo or bar."
- The pivot key must be `required` in each `if` block, otherwise the switch may generate a false positive for the entire object when the key isn't provided at all.
- The default case (the last one, with `then` but no `if`) must always require the pivot key's presence and report all possible values back as an enum, otherwise a misleading error message may be shown to the user.
```yaml
switch:
- if:
required: [a]
properties: { a: { enum: [foo] } }
then:
description: schema X; within `then` can be any JSON Schema content
- if:
required: [a]
properties: { a: { enum: [bar] } }
then:
description: schema Y; within `then` can be any JSON Schema content
- then:
description: fallback schema; ensures the user is told the pivot key is needed and should have one of the enumerated values
required: [a]
properties: { a: { enum: [foo, bar] } }
```

View File

@ -0,0 +1,176 @@
// JSON-Schema ( draf04 ) validator
import JsonSchemaWebWorker from "./validator.worker.js"
import YAML from "js-yaml"
import PromiseWorker from "promise-worker"
import debounce from "lodash/debounce"
import swagger2SchemaYaml from "./swagger2-schema.yaml"
import oas3SchemaYaml from "./oas3-schema.yaml"
const swagger2Schema = YAML.safeLoad(swagger2SchemaYaml)
const oas3Schema = YAML.safeLoad(oas3SchemaYaml)
// Lazily created promise worker
let _promiseWorker
const promiseWorker = () => {
if (!_promiseWorker)
_promiseWorker = new PromiseWorker(new JsonSchemaWebWorker())
return _promiseWorker
}
export const addSchema = (schema, schemaPath = []) => () => {
promiseWorker().postMessage({
type: "add-schema",
payload: {
schemaPath,
schema
}
})
}
// Figure out what schema we need to use ( we're making provision to be able to do sub-schema validation later on)
// ...for now we just pick which base schema to use (eg: openapi-2-0, openapi-3.0, etc)
export const getSchemaBasePath = () => ({ specSelectors }) => {
// Eg: [openapi-3.0] or [openapi-2-0]
// later on... ["openapi-2.0", "paths", "get"]
const isOAS3 = specSelectors.isOAS3 ? specSelectors.isOAS3() : false
const isSwagger2 = specSelectors.isSwagger2
? specSelectors.isSwagger2()
: false
const isAmbiguousVersion = isOAS3 && isSwagger2
// Refuse to handle ambiguity
if (isAmbiguousVersion) return []
if (isSwagger2) return ["openapi-2.0"]
if (isOAS3) return ["openapi-3.0"]
}
export const setup = () => ({ jsonSchemaValidatorActions }) => {
// Add schemas , once off
jsonSchemaValidatorActions.addSchema(swagger2Schema, ["openapi-2.0"])
jsonSchemaValidatorActions.addSchema(oas3Schema, ["openapi-3.0"])
}
export const validate = ({ spec, path = [], ...rest }) => system => {
// stagger clearing errors, in case there is another debounced validation
// run happening, which can occur when the user's typing cadence matches
// the latency of validation
// TODO: instead of using a timeout, be aware of any pending validation
// promises, and use them to schedule error clearing.
setTimeout(() => {
system.errActions.clear({
source: system.jsonSchemaValidatorSelectors.errSource()
})
}, 50)
system.jsonSchemaValidatorActions.validateDebounced({ spec, path, ...rest })
}
// Create a debounced validate, that is lazy
let _debValidate
export const validateDebounced = (...args) => system => {
// Lazily create one...
if (!_debValidate) {
_debValidate = debounce((...args) => {
system.jsonSchemaValidatorActions.validateImmediate(...args)
}, 200)
}
return _debValidate(...args)
}
export const validateImmediate = ({ spec, path = [] }) => system => {
// schemaPath refers to type of schema, and later might refer to sub-schema
const baseSchemaPath = system.jsonSchemaValidatorSelectors.getSchemaBasePath()
// No base path? Then we're unable to do anything...
if (!baseSchemaPath.length)
throw new Error("Ambiguous schema path, unable to run validation")
return system.jsonSchemaValidatorActions.validateWithBaseSchema({
spec,
path: [...baseSchemaPath, ...path]
})
}
export const validateWithBaseSchema = ({ spec, path = [] }) => system => {
const errSource = system.jsonSchemaValidatorSelectors.errSource()
return promiseWorker()
.postMessage({
type: "validate",
payload: {
jsSpec: spec,
specStr: system.specSelectors.specStr(),
schemaPath: path,
source: errSource
}
})
.then(
({ results, path }) => {
system.jsonSchemaValidatorActions.handleResults(null, {
results,
path
})
},
err => {
system.jsonSchemaValidatorActions.handleResults(err, {})
}
)
}
export const handleResults = (err, { results }) => system => {
if (err) {
// Something bad happened with validation.
throw err
}
system.errActions.clear({
source: system.jsonSchemaValidatorSelectors.errSource()
})
if (!Array.isArray(results)) {
results = [results]
}
// Filter out anything funky
results = results.filter(val => typeof val === "object" && val !== null)
if (results.length) {
system.errActions.newSpecErrBatch(results)
}
}
export default function() {
return {
afterLoad: system => system.jsonSchemaValidatorActions.setup(),
statePlugins: {
jsonSchemaValidator: {
actions: {
addSchema,
validate,
handleResults,
validateDebounced,
validateImmediate,
validateWithBaseSchema,
setup
},
selectors: {
getSchemaBasePath,
errSource() {
// Used to identify the errors generated by this plugin
return "structural"
}
}
},
spec: {
wrapActions: {
validateSpec: (ori, system) => (...args) => {
ori(...args)
const [spec, path] = args
system.jsonSchemaValidatorActions.validate({ spec, path })
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
import "src/polyfills"
import registerPromiseWorker from "promise-worker/register"
import Validator from "./validator"
const validator = new Validator()
registerPromiseWorker(({ type, payload }) => {
if (type == "add-schema") {
const { schema, schemaPath } = payload
validator.addSchema(schema, schemaPath)
return
}
if (type == "validate") {
const { jsSpec, specStr, schemaPath, source } = payload
let validationResults = validator.validate({
jsSpec,
specStr,
schemaPath,
source
})
return { results: validationResults }
}
})

View File

@ -0,0 +1,118 @@
// Error condenser!
//
// 1. group all errors by path
// 2. score them by message frequency
// 3. select the most frequent messages (ties retain all equally-frequent messages)
// 4. concatenate the params of each occurrence of the most frequent message
// 5. create one condensed error for the path
// 6. return all condensed errors as an array
export function condenseErrors(errors) {
if (!Array.isArray(errors)) {
return []
}
const tree = {}
function countFor(dataPath, message) {
return tree[dataPath][message].length
}
errors.forEach(err => {
const { dataPath, message } = err
if (tree[dataPath] && tree[dataPath][message]) {
tree[dataPath][message].push(err)
} else if (tree[dataPath]) {
tree[dataPath][message] = [err]
} else {
tree[dataPath] = {
[message]: [err]
}
}
})
const dataPaths = Object.keys(tree)
return dataPaths.reduce((res, path) => {
const messages = Object.keys(tree[path])
const mostFrequentMessageNames = messages.reduce(
(obj, msg) => {
const count = countFor(path, msg)
if (count > obj.max) {
return {
messages: [msg],
max: count
}
} else if (count === obj.max) {
obj.messages.push(msg)
return obj
} else {
return obj
}
},
{ max: 0, messages: [] }
).messages
const mostFrequentMessages = mostFrequentMessageNames.map(
name => tree[path][name]
)
const condensedErrors = mostFrequentMessages.map(messages => {
return messages.reduce((prev, err) => {
const obj = Object.assign({}, prev, {
params: mergeParams(prev.params, err.params)
})
if (!prev.params && !err.params) {
delete obj.params
}
return obj
})
})
return res.concat(condensedErrors)
}, [])
}
// Helpers
function mergeParams(objA = {}, objB = {}) {
if (!objA && !objB) {
return undefined
}
const res = {}
for (let k in objA) {
if (Object.prototype.hasOwnProperty.call(objA, k)) {
res[k] = arrayify(objA[k])
}
}
for (let k in objB) {
if (Object.prototype.hasOwnProperty.call(objB, k)) {
if (res[k]) {
const curr = res[k]
res[k] = curr.concat(arrayify(objB[k]))
} else {
res[k] = arrayify(objB[k])
}
}
}
return res
}
function arrayify(thing) {
if (thing === undefined || thing === null) {
return thing
}
if (Array.isArray(thing)) {
return thing
} else {
return [thing]
}
}

View File

@ -0,0 +1,94 @@
import Ajv from "ajv"
import AjvErrors from "ajv-errors"
import AjvKeywords from "ajv-keywords"
import { getLineNumberForPath } from "./shared.js"
import { condenseErrors } from "./condense-errors.js"
import jsonSchema from "./jsonSchema"
const IGNORED_AJV_PARAMS = ["type", "errors"]
export default class JSONSchemaValidator {
constructor() {
this.ajv = new Ajv({
allErrors: true,
jsonPointers: true,
})
AjvKeywords(this.ajv, "switch")
AjvErrors(this.ajv)
this.addSchema(jsonSchema)
}
addSchema(schema, key) {
this.ajv.addSchema(schema, normalizeKey(key))
}
validate({ jsSpec, specStr, schemaPath, source }) {
this.ajv.validate(normalizeKey(schemaPath), jsSpec)
if (!this.ajv.errors || !this.ajv.errors.length) {
return null
}
const condensedErrors = condenseErrors(this.ajv.errors)
try {
const boundGetLineNumber = getLineNumberForPath.bind(null, specStr)
return condensedErrors.map(err => {
let preparedMessage = err.message
if (err.params) {
preparedMessage += "\n"
for (var k in err.params) {
if (IGNORED_AJV_PARAMS.indexOf(k) === -1) {
const ori = err.params[k]
const value = Array.isArray(ori) ? dedupe(ori).join(", ") : ori
preparedMessage += `${k}: ${value}\n`
}
}
}
const errorPathArray = jsonPointerStringToArray(err.dataPath)
return {
level: "error",
line: boundGetLineNumber(errorPathArray || []),
path: errorPathArray,
message: preparedMessage.trim(),
source,
original: err
}
})
}
catch (err) {
return {
level: "error",
line: err.problem_mark && err.problem_mark.line + 1 || 0,
message: err.problem,
source: "parser",
original: err
}
}
}
}
function dedupe(arr) {
return arr.filter((val, i) => {
return arr.indexOf(val) === i
})
}
function pathToJSONPointer(arr) {
return arr.map(a => (a + "").replace("~", "~0").replace("/", "~1")).join("/")
}
function jsonPointerStringToArray(str) {
return str.split("/")
.map(part => (part + "").replace(/~0/g, "~").replace(/~1/g, "/"))
.filter(str => str.length > 0)
}
// Convert arrays into a string. Safely, by using the JSONPath spec
function normalizeKey(key) {
if (!Array.isArray(key)) key = [key]
return pathToJSONPointer(key)
}

View File

@ -0,0 +1,147 @@
export default {
id: "http://json-schema.org/draft-04/schema#",
$schema: "http://json-schema.org/draft-04/schema#",
description: "Core schema meta-schema",
definitions: {
schemaArray: {
type: "array",
minItems: 1,
items: { $ref: "#" }
},
positiveInteger: {
type: "integer",
minimum: 0
},
positiveIntegerDefault0: {
allOf: [{ $ref: "#/definitions/positiveInteger" }, { default: 0 }]
},
simpleTypes: {
enum: [
"array",
"boolean",
"integer",
/* "null", */ // removed per https://github.com/swagger-api/swagger-editor/issues/1832#issuecomment-483717197
"number",
"object",
"string"
]
},
stringArray: {
type: "array",
items: { type: "string" },
minItems: 1,
uniqueItems: true
}
},
type: "object",
properties: {
id: {
type: "string",
format: "uri"
},
$schema: {
type: "string",
format: "uri"
},
title: {
type: "string"
},
description: {
type: "string"
},
default: {},
multipleOf: {
type: "number",
minimum: 0,
exclusiveMinimum: true
},
maximum: {
type: "number"
},
exclusiveMaximum: {
type: "boolean",
default: false
},
minimum: {
type: "number"
},
exclusiveMinimum: {
type: "boolean",
default: false
},
maxLength: { $ref: "#/definitions/positiveInteger" },
minLength: { $ref: "#/definitions/positiveIntegerDefault0" },
pattern: {
type: "string",
format: "regex"
},
additionalItems: {
anyOf: [{ type: "boolean" }, { $ref: "#" }],
default: {}
},
items: {
anyOf: [{ $ref: "#" }, { $ref: "#/definitions/schemaArray" }],
default: {}
},
maxItems: { $ref: "#/definitions/positiveInteger" },
minItems: { $ref: "#/definitions/positiveIntegerDefault0" },
uniqueItems: {
type: "boolean",
default: false
},
maxProperties: { $ref: "#/definitions/positiveInteger" },
minProperties: { $ref: "#/definitions/positiveIntegerDefault0" },
required: { $ref: "#/definitions/stringArray" },
additionalProperties: {
anyOf: [{ type: "boolean" }, { $ref: "#" }],
default: {}
},
definitions: {
type: "object",
additionalProperties: { $ref: "#" },
default: {}
},
properties: {
type: "object",
additionalProperties: { $ref: "#" },
default: {}
},
patternProperties: {
type: "object",
additionalProperties: { $ref: "#" },
default: {}
},
dependencies: {
type: "object",
additionalProperties: {
anyOf: [{ $ref: "#" }, { $ref: "#/definitions/stringArray" }]
}
},
enum: {
type: "array",
minItems: 1,
uniqueItems: true
},
type: {
$ref: "#/definitions/simpleTypes"
// anyOf: [
// { $ref: "#/definitions/simpleTypes" },
// {
// type: "array",
// items: { $ref: "#/definitions/simpleTypes" },
// minItems: 1,
// uniqueItems: true
// }
// ]
},
allOf: { $ref: "#/definitions/schemaArray" },
anyOf: { $ref: "#/definitions/schemaArray" },
oneOf: { $ref: "#/definitions/schemaArray" },
not: { $ref: "#" }
},
dependencies: {
exclusiveMaximum: ["maximum"],
exclusiveMinimum: ["minimum"]
},
default: {}
}

View File

@ -0,0 +1,77 @@
import get from "lodash/get"
export function transformPathToArray(property, jsSpec) {
if (property.slice(0, 9) === "instance.") {
var str = property.slice(9)
} else {
// eslint-disable-next-line no-redeclare
var str = property
}
var pathArr = []
// replace '.', '["', '"]' separators with pipes
str = str.replace(/\.(?![^["]*"\])|(\[\")|(\"\]\.?)/g, "|")
// handle single quotes as well
str = str.replace(/\[\'/g, "|")
str = str.replace(/\'\]/g, "|")
// split on our new delimiter, pipe
str = str.split("|")
str
.map(item => {
// "key[0]" becomes ["key", "0"]
if (item.indexOf("[") > -1) {
let index = parseInt(item.match(/\[(.*)\]/)[1])
let keyName = item.slice(0, item.indexOf("["))
return [keyName, index.toString()]
} else {
return item
}
})
.reduce(function(a, b) {
// flatten!
return a.concat(b)
}, [])
.concat([""]) // add an empty item into the array, so we don't get stuck with something in our buffer below
.reduce((buffer, curr) => {
let obj = pathArr.length ? get(jsSpec, pathArr) : jsSpec
if (get(obj, makeAccessArray(buffer, curr))) {
if (buffer.length) {
pathArr.push(buffer)
}
if (curr.length) {
pathArr.push(curr)
}
return ""
} else {
// attach key to buffer
return `${buffer}${buffer.length ? "." : ""}${curr}`
}
}, "")
if (typeof get(jsSpec, pathArr) !== "undefined") {
return pathArr
} else {
// if our path is not correct (there is no value at the path),
// return null
return null
}
}
function makeAccessArray(buffer, curr) {
let arr = []
if (buffer.length) {
arr.push(buffer)
}
if (curr.length) {
arr.push(curr)
}
return arr
}

View File

@ -0,0 +1,4 @@
// export * from './ast.js'
// These import/exports are shared code between worker and main bundle.
// Putting them here keeps the distiction clear
export { getLineNumberForPath } from "../../ast/ast.js"

View File

@ -0,0 +1,64 @@
import React from "react"
import PropTypes from "prop-types"
import JumpIcon from "./jump-icon.svg"
export class JumpToPath extends React.Component {
static propTypes = {
editorActions: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired,
fn: PropTypes.object.isRequired,
path: PropTypes.oneOfType([
PropTypes.array,
PropTypes.string
]),
content: PropTypes.element,
showButton: PropTypes.bool,
specPath: PropTypes.array, // The location within the spec. Used as a fallback if `path` doesn't exist
}
static defaultProps = {
path: "",
}
shouldComponentUpdate(nextProps) {
let { shallowEqualKeys } = nextProps.fn
return shallowEqualKeys(this.props, nextProps, [
"content", "showButton", "path", "specPath"
])
}
jumpToPath = (e) => {
e.stopPropagation()
const {
specPath=[],
path,
specSelectors,
editorActions
} = this.props
const jumpPath = specSelectors.bestJumpPath({path, specPath})
editorActions.jumpToLine(specSelectors.getSpecLineFromPath(jumpPath))
}
defaultJumpContent = <img src={JumpIcon} onClick={this.jumpToPath} className="view-line-link" title={"Jump to definition"} />
render() {
let { content, showButton } = this.props
if (content) {
// if we were given content to render, wrap it
return (
<span onClick={ this.jumpToPath }>
{ showButton ? this.defaultJumpContent : null }
{content}
</span>
)
} else {
// just render a link
return this.defaultJumpContent
}
}
}

View File

@ -0,0 +1,11 @@
import spec from "./spec"
import * as components from "./components"
export default function JumpToPathPlugin() {
return [
spec,
{
components,
}
]
}

View File

@ -0,0 +1,3 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@ -0,0 +1,77 @@
import { unescapeJsonPointerToken } from "../refs-util"
export default function spec() {
return {
statePlugins: {
spec: {
selectors: {
getSpecLineFromPath: (state, path) => ({fn: { AST }, specSelectors: { specStr }}) => {
return AST.getLineNumberForPath(specStr(), path.toJS ? path.toJS() : path)
},
// This will search return `path if it exists, else it'll look for the best $ref jump point
// There is one caveat, it'll not search _down_ for deeply nested $refs. In those cases, it'll bring you to the shallower $ref.
bestJumpPath: (state, {path, specPath}) => (system) => {
const {
specSelectors: { specJson },
fn: { transformPathToArray }
} = system
// We"ve been given an explicit path? Use that...
if(path) {
return typeof path === "string" ? transformPathToArray(path, specJson().toJS()) : path
}
// Try each path in the resolved spec, starting from the deepest
for(let i = specPath.length; i >= 0; i--) {
const tryPath = specPath.slice(0,i)
// A $ref exists in the source? ( ie: pre-resolver)
const $ref = specJson().getIn([...tryPath, "$ref"])
// We have a $ref in the source?
if($ref) {
if(!/^#\//.test($ref)) {
return [...tryPath, "$ref"]
} else { // Is local $ref
// Get rid of the trailing '#'
const pointer = $ref.charAt(0) === "#" ? $ref.substr(1) : $ref
return jsonPointerToArray(pointer)
}
}
// This path exists in the source spec?
if(specJson().hasIn(tryPath)) {
return tryPath
}
}
// ...else just specPath, which is hopefully close enough
return specPath
}
}
}
}
}
}
// Copied out of swagger-client, not sure if it should be exposed as a lib or as part of the public swagger-client api.
/**
* Converts a JSON pointer to array.
* @api public
*/
function jsonPointerToArray(pointer) {
if (typeof pointer !== "string") {
throw new TypeError(`Expected a string, got a ${typeof pointer}`)
}
if (pointer[0] === "/") {
pointer = pointer.substr(1)
}
if (pointer === "") {
return []
}
return pointer.split("/").map(unescapeJsonPointerToken)
}

View File

@ -0,0 +1,45 @@
import PetstoreYaml from "./petstore"
const CONTENT_KEY = "swagger-editor-content"
let localStorage = window.localStorage
export const updateSpec = (ori) => (...args) => {
let [spec] = args
ori(...args)
saveContentToStorage(spec)
}
export default function(system) {
// setTimeout runs on the next tick
setTimeout(() => {
if(localStorage.getItem(CONTENT_KEY)) {
system.specActions.updateSpec(localStorage.getItem(CONTENT_KEY), "local-storage")
} else if(localStorage.getItem("ngStorage-SwaggerEditorCache")) {
// Legacy migration for swagger-editor 2.x
try {
let obj = JSON.parse(localStorage.getItem("ngStorage-SwaggerEditorCache"))
let yaml = obj.yaml
system.specActions.updateSpec(yaml)
saveContentToStorage(yaml)
localStorage.setItem("ngStorage-SwaggerEditorCache", null)
} catch(e) {
system.specActions.updateSpec(PetstoreYaml)
}
} else {
system.specActions.updateSpec(PetstoreYaml)
}
}, 0)
return {
statePlugins: {
spec: {
wrapActions: {
updateSpec
}
}
}
}
}
function saveContentToStorage(str) {
return localStorage.setItem(CONTENT_KEY, str)
}

Some files were not shown because too many files have changed in this diff Show More