Added Swagger
This commit is contained in:
parent
5c6f37eaf7
commit
af76cbca87
|
@ -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!
|
|
@ -3,60 +3,86 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
return view("user.create");
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
//
|
||||
$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.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
//
|
||||
$user = User::find($id);
|
||||
|
||||
return view("user.show", [
|
||||
"user" => $user
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ class User extends Authenticatable
|
|||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name', 'email', 'password',
|
||||
'name_first', "name_last", 'email', 'password', "phone"
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,4 +17,8 @@ use Illuminate\Support\Facades\Route;
|
|||
// return view('welcome');
|
||||
//});
|
||||
|
||||
Route::middleware(["auth"])->group(function () {
|
||||
|
||||
});
|
||||
|
||||
Route::get("/", "RootController@index")->name("root.index");
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/.git
|
||||
/.github
|
||||
/dev-helpers
|
||||
/docs
|
||||
/src
|
||||
/swagger-editor-dist-package
|
||||
/test
|
||||
/node_modules
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
node_modules
|
||||
.idea
|
||||
.deps_check
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
test/e2e/screenshots
|
||||
test/e2e/videos
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
semi: false
|
||||
trailingComma: es5
|
||||
endOfLine: lf
|
||||
requirePragma: true
|
||||
insertPragma: true
|
|
@ -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"]
|
|
@ -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.
|
|
@ -0,0 +1,114 @@
|
|||
# <img src="https://raw.githubusercontent.com/swagger-api/swagger.io/wordpress/images/assets/SWE-logo-clr.png" height="80">
|
||||
[](http://badge.fury.io/js/swagger-editor)
|
||||
[](https://jenkins.swagger.io/job/oss-swagger-editor-master/)
|
||||
[](https://codeclimate.com/github/swagger-api/swagger-editor)
|
||||
[](https://david-dm.org/swagger-api/swagger-editor)
|
||||
[](https://david-dm.org/swagger-api/swagger-editor-#info=devDependencies)
|
||||
[](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.
|
|
@ -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.
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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/"
|
||||
}
|
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 738 B |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
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
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
|
@ -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 |
|
@ -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,
|
||||
}
|
||||
```
|
|
@ -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.
|
||||
|
||||

|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.";
|
|
@ -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/^/* /'
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": [
|
||||
2,
|
||||
{
|
||||
"devDependencies": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import * as AST from "./ast"
|
||||
|
||||
export default function() {
|
||||
return {
|
||||
fn: { AST }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestKeywordsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}])
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestOAS3KeywordsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}])
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 []
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestRefsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}])
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import * as wrapActions from "./wrap-actions"
|
||||
|
||||
export default function EditorAutosuggestSnippetsPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
editor: {
|
||||
wrapActions,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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)
|
||||
}
|
||||
}])
|
||||
}
|
|
@ -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 []
|
||||
}
|
|
@ -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] : ""
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = () => () => {}
|
|
@ -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"
|
||||
})
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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([])
|
||||
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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, " ")
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
JUMP_TO_LINE
|
||||
} from "./actions"
|
||||
|
||||
export default {
|
||||
[JUMP_TO_LINE]: (state, { payload } ) =>{
|
||||
return state.set("gotoLine", { line: payload })
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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] } }
|
||||
```
|
|
@ -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
|
@ -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 }
|
||||
}
|
||||
})
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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: {}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import spec from "./spec"
|
||||
import * as components from "./components"
|
||||
|
||||
export default function JumpToPathPlugin() {
|
||||
return [
|
||||
spec,
|
||||
{
|
||||
components,
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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 |
|
@ -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)
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue