├── .gitignore ├── .prettierignore ├── .travis.yml ├── CODEOWNERS ├── LICENSE.md ├── README.md ├── config-overrides.ts ├── package-lock.json ├── package.json ├── public ├── img │ ├── datree-logo.png │ ├── datree-logo192x192.png │ ├── datree-logo512x512.png │ └── favicon-new-32x32.png ├── index.html ├── main.wasm ├── manifest.json └── robots.txt ├── scripts └── cloudfront.sh ├── src ├── App │ ├── App.test.tsx │ ├── App.tsx │ ├── Results │ │ ├── TestErrors.tsx │ │ └── ValidationResult.tsx │ ├── YamlEditor.tsx │ ├── appSlice │ │ ├── appSlice.ts │ │ ├── getValidationResult.ts │ │ └── index.ts │ └── services │ │ ├── LocalStorageService.ts │ │ └── compileJsonSchema.ts ├── LoadWasm │ ├── index.tsx │ ├── wasmTypes.d.ts │ └── wasm_exec.js ├── examples │ ├── customRuleKindEnum.yaml │ ├── defaultYamlInputValue.ts │ ├── defaultYamlSchemaValue.ts │ └── k8s-demo.yaml ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── redux │ ├── hooks.ts │ └── store.ts ├── reportWebVitals.ts ├── setupTests.ts └── utils │ ├── createUseClasses │ ├── Readme.md │ ├── createUseClasses.ts │ └── index.ts │ ├── functions.ts │ ├── getErrorMessage.ts │ └── utils.ts ├── tsconfig.json └── wasm ├── go.mod ├── go.sum ├── main.go ├── parseErrorToString.go └── validator.go /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # more 26 | .idea 27 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | LICENSE.md 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | language: node_js 4 | node_js: '16' 5 | install: 6 | - npm ci 7 | - pyenv local 3.6 8 | - pip install -U pip 9 | - pip install --user awscli 10 | script: 11 | - npm run build:ts && npm run test 12 | deploy: # on merge to main 13 | # deploy to production s3 bucket 14 | - provider: s3 15 | skip_cleanup: true 16 | access_key_id: '$AWS_ACCESS_KEY_ID' 17 | secret_access_key: '$AWS_SECRET_ACCESS_KEY' 18 | bucket: yaml-schema-validator 19 | local-dir: build 20 | acl: public_read 21 | on: 22 | branch: main 23 | # cloudfront invalidation 24 | - provider: script 25 | script: bash scripts/cloudfront.sh E3ESLGBVCM2QFT 26 | on: 27 | branch: main 28 | env: 29 | global: 30 | # AWS_ACCESS_KEY_ID 31 | - secure: oa8gX38x/vK4N+UoJVzvQBE6kF89F9Z8e0CDumYmn7EfRO5Xz+mEhwwUqRTku9d50ouyhtzXWHKq+9L4tIMwiRMrUU4gInAJXQq1M2XgDcyF0odR/pKa83tK6E76N7sRkgk9HK74jO4OoI/0YVnGuklUN4zfv3sC2LnIZyYyRjs0bz0W/xiYiOmb4Y0AFqt5CM++FZdx9QJ5vI0AP1aMIvB0Xzh3lnzlYGN7eltz+Ce8OgamaoeLb36FjpM2X0isWuW0o0jkgC9pbzSrmKpZPi5UE+APwcILUbk1J60ost+G1Tc5lEcTy2UIo1zleD8vx8yy+VIFzXQ16oV3K0YUsgjyKiIlUkuoHEkVjerAe/b+f+tc3IwIJujo8GIbvubOTPzdHKLMQHMCEcM7Ui2U6e/EsWg/9coA/flnj7wCT9bgLwnGic22hmLbUR8D4ZFd40M2rdTYh5DMWDBCwJ03F8DUt/pV+Rsa1X6KVH40LXJ0/lxWJwjrPekaEFqDJmOio+Kj+x9IN94cpfUY9cNDswSl+B/NEYZKlpw9ozYg/xVGRxRnN5vxxsZ/FfSgUTj2SMontit13qw6uxWh471oezAvrb+KwJRR1w7a/4dLYEML1HdNozb8chAoZ054vc84P2EXv4jiMzofZdi+sMRm/t2dz7+MTO7LkRhlonk1yP8= 32 | # AWS_SECRET_ACCESS_KEY 33 | - secure: nyf+dTxir5f5LRoO090NwD/CLZNdLy0/2PAYY++HgSnJHxDbU6xnRg7MpprsvmJgFewxghrD8z2cpX7BDJ24ZQo+k3mq1SS8+wVThW5dI8xsVR+MSKLh31hvSXXhKRIoBDXlh48xMXjjr7TskS0OTGpPX5C2AMskNB+BVT9gH2oPnxE8rSLrk5NAnFSBOMatFWcg2OkSu7sNHFkhZOZSlRzT2LNtENfnRZf6yBseXA/WeR0YeCfD6vLeiLHaBeWOPnBttT9aKU2vnj2FXoVN+VI1PR86j9ZVLmBdNfckijYbstrpgS+VvvxJiDyfT3x9qcqe4zzaGIHiMX/vTJFXSZ75PVhMPBXtqA+A60lGRrXfhgHADZ1qIelKxr/8nNnBkxHBieGY1/7Dtjsb9mJtG2ALTzP6hI1rDQyP2fKNNJ1t5YkBThrI5ge9ttaAaj+jgiZvV+iLY3Mx4xqf2THE4X/yFOB43hvSO+x/+sK2SHmHypZ7RS4b70ez1EffEumWZ2eoWyI2zhQzVvb/fS5ItmdDlqxYhT1D4SHLejevqE4zvvpFWM1hLcYS43T7ugAjEGBocBCdx3r2CfbiyWxPiRUDljewpXwyUOZ58hL01k1qVaMZaKdiKw3hv3vdA1ahVdVzydyKN4vSEahwzlz9pghhJq9s5qSY74/5ZJBOxY8= 34 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # by deafult 2 | * @datreeio/developers 3 | 4 | # specific files 5 | README.md @eyarz 6 | LICENSE.md @eyarz 7 | CONTRIBUTING.md @eyarz 8 | DEVELOPER_GUIDE.md @eyarz 9 | 10 | # specific directories 11 | public/img/ @eyarz 12 | .github/ISSUE_TEMPLATE/ @eyarz 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "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. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "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. 22 | 23 | "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). 24 | 25 | "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. 26 | 27 | "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." 28 | 29 | "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. 30 | 31 | 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. 32 | 33 | 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. 34 | 35 | 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: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | You must cause any modified files to carry prominent notices stating that You changed the files; and 39 | 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 40 | 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. 41 | 42 | 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. 43 | 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. 44 | 45 | 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. 46 | 47 | 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. 48 | 49 | 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. 50 | 51 | 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. 52 | 53 | END OF TERMS AND CONDITIONS 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yaml schema validator online 2 | 3 | For reference, here is a list of features & changes we might want to implement in the future: 4 | (important features & quick wins marked in bold). 5 | 6 | - **include build:wasm in CI/CD pipeline and put /public/main.wasm in .gitignore** 7 | - **docs, README.md, CONTRIBUTING.md, GitHub about, GitHub repository image** 8 | - **favicon & icons in general** 9 | - **general web page design** 10 | - **support ajv keywords** 11 | - **support multiple versions of json schema (currently only latest)** 12 | - save & share 13 | - export as custom rule option 14 | - better error line marker (currently only for yaml errors) 15 | - header and additional text on the page 16 | - improve error messages formats 17 | - support custom keys (resourceMinimum & resourceMaximum) 18 | - optimize the words & technical terms on the page to make it easier to understand 19 | - support multiple configuration files in the input yaml 20 | - support ctrl+z after applying prettier 21 | - change or add domains to host the app on 22 | - SEO improvements 23 | -------------------------------------------------------------------------------- /config-overrides.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const { override, addBabelPlugin } = require('customize-cra'); 3 | 4 | module.exports = override(addBabelPlugin(['@emotion', {}])); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yaml-schema-validator", 3 | "version": "0.1.0", 4 | "private": false, 5 | "description": "yaml schema validator online", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/datreeio/yaml-schema-validator.git" 9 | }, 10 | "license": "Apache-2.0", 11 | "author": "datree.io", 12 | "scripts": { 13 | "start": "concurrently \"npm run watch:ts\" \"npm run watch:wasm\"", 14 | "watch:ts": "react-app-rewired start", 15 | "watch:wasm": "watch \"npm run build:wasm\" ./wasm", 16 | "build": "npm run build:wasm && npm run build:ts", 17 | "build:ts": "react-app-rewired build", 18 | "build:wasm": "cd wasm && GOOS=js GOARCH=wasm go build -o ../public/main.wasm && cd .. && echo \"\\033[0;32mcompiled wasm successfully!\\033[0m\"", 19 | "test": "react-app-rewired test --watchAll=false", 20 | "test:watch": "react-app-rewired test", 21 | "eject": "react-scripts eject", 22 | "predeploy": "npm run build", 23 | "prettier": "prettier --write .", 24 | "lint": "eslint .", 25 | "lint:fix": "eslint --fix --ext .ts,.tsx,.js,.jsx .", 26 | "fix": "npm run prettier && npm run lint:fix" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "prettier": { 41 | "printWidth": 120, 42 | "singleQuote": true, 43 | "trailingComma": "all" 44 | }, 45 | "eslintConfig": { 46 | "plugins": [ 47 | "simple-import-sort" 48 | ], 49 | "extends": [ 50 | "react-app", 51 | "react-app/jest", 52 | "plugin:prettier/recommended" 53 | ], 54 | "rules": { 55 | "max-len": "off", 56 | "prettier/prettier": [ 57 | "warn", 58 | { 59 | "singleQuote": true 60 | } 61 | ], 62 | "simple-import-sort/exports": "warn", 63 | "simple-import-sort/imports": "warn" 64 | }, 65 | "ignorePatterns": [ 66 | "build/*" 67 | ] 68 | }, 69 | "dependencies": { 70 | "@emotion/css": "^11.5.0", 71 | "@material-ui/core": "^4.12.3", 72 | "@material-ui/icons": "^4.11.2", 73 | "@reduxjs/toolkit": "^1.6.2", 74 | "ajv": "^8.8.2", 75 | "codemirror": "^5.64.0", 76 | "js-yaml": "^4.1.0", 77 | "react": "^17.0.2", 78 | "react-codemirror2": "^7.2.1", 79 | "react-dom": "^17.0.2", 80 | "react-redux": "^7.2.6", 81 | "react-scripts": "4.0.3", 82 | "web-vitals": "^1.1.2" 83 | }, 84 | "devDependencies": { 85 | "@emotion/babel-plugin": "^11.3.0", 86 | "@testing-library/jest-dom": "^5.15.0", 87 | "@testing-library/react": "^11.2.7", 88 | "@testing-library/user-event": "^12.8.3", 89 | "@types/codemirror": "^5.60.5", 90 | "@types/customize-cra": "^1.0.2", 91 | "@types/jest": "^26.0.24", 92 | "@types/js-yaml": "^4.0.5", 93 | "@types/node": "^12.20.37", 94 | "@types/react": "^17.0.36", 95 | "@types/react-dom": "^17.0.11", 96 | "concurrently": "^7.1.0", 97 | "customize-cra": "^1.0.0", 98 | "eslint-config-prettier": "^8.3.0", 99 | "eslint-plugin-prettier": "^4.0.0", 100 | "eslint-plugin-simple-import-sort": "^7.0.0", 101 | "prettier": "^2.4.1", 102 | "react-app-rewired": "^2.1.8", 103 | "typescript": "^4.5.2", 104 | "watch": "^1.0.2" 105 | }, 106 | "config-overrides-path": "config-overrides.ts" 107 | } 108 | -------------------------------------------------------------------------------- /public/img/datree-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datreeio/yaml-schema-validator/304357c7f649edc51f156c92af1d32c1636908bc/public/img/datree-logo.png -------------------------------------------------------------------------------- /public/img/datree-logo192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datreeio/yaml-schema-validator/304357c7f649edc51f156c92af1d32c1636908bc/public/img/datree-logo192x192.png -------------------------------------------------------------------------------- /public/img/datree-logo512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datreeio/yaml-schema-validator/304357c7f649edc51f156c92af1d32c1636908bc/public/img/datree-logo512x512.png -------------------------------------------------------------------------------- /public/img/favicon-new-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datreeio/yaml-schema-validator/304357c7f649edc51f156c92af1d32c1636908bc/public/img/favicon-new-32x32.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | YAML Schema Validator 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /public/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datreeio/yaml-schema-validator/304357c7f649edc51f156c92af1d32c1636908bc/public/main.wasm -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "img/favicon-new-32x32.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "img/datree-logo192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "img/datree-logo512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/cloudfront.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | CLOUDFRONT_DISTRIBUTION_ID=$1 5 | echo "Cloudfront: Invalidating /* for ${CLOUDFRONT_DISTRIBUTION_ID}" 6 | aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*" 7 | -------------------------------------------------------------------------------- /src/App/App.test.tsx: -------------------------------------------------------------------------------- 1 | test('dummy test', () => { 2 | expect(1).toBe(1); 3 | }); 4 | 5 | export {}; 6 | -------------------------------------------------------------------------------- /src/App/App.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React from 'react'; 3 | 4 | import { useAppDispatch, useAppSelector } from '../redux/hooks'; 5 | import { createUseClasses } from '../utils/createUseClasses'; 6 | import { selectAppSlice, selectValidationResult, setYamlInput, setYamlSchema } from './appSlice'; 7 | import { ValidationResult } from './Results/ValidationResult'; 8 | import { YamlEditor } from './YamlEditor'; 9 | 10 | interface Props {} 11 | 12 | export function App(props: Props) { 13 | const classes = useClasses(props); 14 | const dispatch = useAppDispatch(); 15 | const { yamlSchema, yamlInput } = useAppSelector(selectAppSlice); 16 | const validationResult = useAppSelector(selectValidationResult); 17 | 18 | const dispatchYamlSchemaValue = (newValue: string): void => { 19 | dispatch(setYamlSchema(newValue)); 20 | }; 21 | const dispatchYamlInputValue = (newValue: string): void => { 22 | dispatch(setYamlInput(newValue)); 23 | }; 24 | 25 | return ( 26 |
27 |
28 |

YAML Schema Validator Online

29 |
30 |
31 |
32 |
33 |
YAML Schema
34 |
35 | { 38 | dispatchYamlSchemaValue(value); 39 | }} 40 | setValue={dispatchYamlSchemaValue} 41 | /> 42 |
43 |
44 |
45 |
Input YAML to test against
46 |
47 | { 50 | dispatchYamlInputValue(value); 51 | }} 52 | setValue={dispatchYamlInputValue} 53 | /> 54 |
55 |
56 |
57 | 58 |
59 |
60 | ); 61 | } 62 | 63 | const useClasses = createUseClasses((_props: Props) => ({ 64 | root: css``, 65 | header: css` 66 | padding: 1rem 3rem; 67 | `, 68 | mainAppContainer: css` 69 | margin: 1rem; 70 | `, 71 | textFieldsContainer: css` 72 | display: flex; 73 | flex-direction: row; 74 | align-items: stretch; 75 | justify-content: space-between; 76 | height: 60vh; 77 | `, 78 | textFieldContainer: css` 79 | height: 100%; 80 | width: 47.5%; 81 | display: flex; 82 | flex-direction: column; 83 | `, 84 | textAboveTextField: css` 85 | padding: 1rem 1rem 1rem 0; 86 | flex-grow: 0; 87 | `, 88 | codeEditorWrapper: css` 89 | flex-grow: 1; 90 | `, 91 | })); 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /src/App/Results/TestErrors.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | 3 | import { createUseClasses } from '../../utils/createUseClasses'; 4 | import { GolangTestError } from '../appSlice'; 5 | 6 | interface Props { 7 | errors: GolangTestError[]; 8 | } 9 | 10 | export function TestErrors(props: Props) { 11 | const classes = useClasses(props); 12 | const { errors } = props; 13 | 14 | const numberOfErrors = errors?.length ?? 0; 15 | const errorsListHeaderText = `Found ${numberOfErrors} error${numberOfErrors !== 1 ? 's' : ''}`; 16 | 17 | return ( 18 |
19 |
{errorsListHeaderText}
20 |
21 | {errors?.map((e) => ( 22 |
23 |
24 |
Message:
25 |
{e.description}
26 |
27 |
28 |
Instance path:
29 |
{e.context}
30 |
31 |
32 | ))} 33 |
34 |
35 | ); 36 | } 37 | 38 | const useClasses = createUseClasses((_props: Props) => ({ 39 | testErrorsContainer: css``, 40 | testErrorsHeader: css` 41 | color: red; 42 | margin-bottom: 0.5rem; 43 | `, 44 | testErrorFieldName: css` 45 | color: black; 46 | min-width: 10rem; 47 | `, 48 | testError: css` 49 | margin-bottom: 1rem; 50 | `, 51 | testErrorProperty: css` 52 | display: flex; 53 | flex-direction: row; 54 | `, 55 | })); 56 | -------------------------------------------------------------------------------- /src/App/Results/ValidationResult.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { Fragment } from 'react'; 3 | 4 | import { createUseClasses } from '../../utils/createUseClasses'; 5 | import { ErrorType, ValidationResult as ValidationResultType } from '../appSlice'; 6 | import { TestErrors } from './TestErrors'; 7 | 8 | interface Props { 9 | validationResult: ValidationResultType; 10 | } 11 | 12 | export function ValidationResult(props: Props) { 13 | const classes = useClasses(props); 14 | const { validationResult } = props; 15 | 16 | return ( 17 |
18 | {validationResult.isSuccess ? ( 19 | {validationResult.successMessage} 20 | ) : ( 21 | 22 |
{validationResult.error.errorTypeMessage}
23 |
24 | {validationResult.error.errorType === ErrorType.test ? ( 25 | 26 | ) : ( 27 | {validationResult.error.errorMessage} 28 | )} 29 |
30 |
31 | )} 32 |
33 | ); 34 | } 35 | 36 | const useClasses = createUseClasses((props: Props) => ({ 37 | validationResultContainer: css` 38 | color: ${props.validationResult.isSuccess ? 'green' : 'red'}; 39 | padding: 1rem 1rem; 40 | `, 41 | successMessage: css` 42 | font-weight: bold; 43 | `, 44 | errorTypeMessage: css` 45 | font-weight: bold; 46 | margin-bottom: 0.5rem; 47 | `, 48 | errorMessage: css``, 49 | })); 50 | -------------------------------------------------------------------------------- /src/App/YamlEditor.tsx: -------------------------------------------------------------------------------- 1 | import 'codemirror/lib/codemirror.css'; 2 | import 'codemirror/mode/yaml/yaml'; 3 | import 'codemirror/addon/lint/lint'; 4 | import 'codemirror/addon/lint/lint.css'; 5 | import 'codemirror/addon/lint/yaml-lint'; 6 | 7 | import { css } from '@emotion/css'; 8 | import { IconButton, Tooltip, withStyles } from '@material-ui/core'; 9 | import FormatIndentIncreaseIcon from '@material-ui/icons/FormatIndentIncrease'; 10 | import { Controlled as ControlledCodeMirror, IControlledCodeMirror } from 'react-codemirror2'; 11 | 12 | import { createUseClasses } from '../utils/createUseClasses'; 13 | import { emptyObject } from '../utils/utils'; 14 | import { formatYaml, isYamlValid } from './appSlice/getValidationResult'; 15 | 16 | // required by codemirror/addon/lint/yaml-lint 17 | (window as any).jsyaml = require('js-yaml'); 18 | 19 | interface Props extends IControlledCodeMirror { 20 | setValue: (value: string) => void; 21 | } 22 | 23 | export function YamlEditor(props: Props) { 24 | const classes = useClasses(emptyObject); 25 | const isFormattingDisabled = !isYamlValid(props.value); 26 | 27 | return ( 28 |
29 | 41 | 42 |
43 | props.setValue(formatYaml(props.value))} 45 | className={classes.formatButton} 46 | disabled={isFormattingDisabled} 47 | > 48 | 49 | 50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | const useClasses = createUseClasses((props) => ({ 57 | yamlEditorRoot: css` 58 | width: 100%; 59 | height: 100%; 60 | .CodeMirror { 61 | min-height: 100%; 62 | } 63 | `, 64 | yamlEditorWrapper: css` 65 | position: relative; 66 | width: 100%; 67 | height: 100%; 68 | border: 1px solid silver; 69 | `, 70 | formatButton: css` 71 | border: 1px solid lightgray !important; 72 | border-radius: 0 !important; 73 | padding: 0.3rem !important; 74 | `, 75 | formatButtonWrapper: css` 76 | position: absolute; 77 | bottom: 50px; 78 | right: 50px; 79 | z-index: 100; 80 | `, 81 | })); 82 | 83 | const PrettierFormatTooltip = withStyles({ 84 | tooltip: { 85 | background: 'lightgray', 86 | color: 'black', 87 | fontSize: '0.8rem', 88 | }, 89 | })(Tooltip); 90 | -------------------------------------------------------------------------------- /src/App/appSlice/appSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | import type { RootState } from '../../redux/store'; 4 | import { LocalStorageKey, LocalStorageService } from '../services/LocalStorageService'; 5 | import { getValidationResult } from './getValidationResult'; 6 | 7 | export type YamlSchema = string; 8 | export type YamlInput = string; 9 | export enum ErrorType { 10 | yamlSchemaToObj, 11 | objToJsonSchema, 12 | yamlInputToObj, 13 | test, 14 | } 15 | 16 | export type GolangTestError = { 17 | description: string; 18 | context: string; 19 | }; 20 | export type GolangResult = { 21 | valid: boolean; 22 | errors: GolangTestError[] | null; 23 | err: string; 24 | }; 25 | 26 | type ValidationResultSuccess = { 27 | isSuccess: true; 28 | successMessage: string; 29 | }; 30 | type ValidationResultFailure = { 31 | isSuccess: false; 32 | error: { 33 | errorType: ErrorType.objToJsonSchema | ErrorType.yamlSchemaToObj | ErrorType.yamlInputToObj; 34 | errorTypeMessage: string; 35 | errorMessage: string; 36 | }; 37 | }; 38 | type ValidationResultTestFailure = { 39 | isSuccess: false; 40 | error: { 41 | errorType: ErrorType.test; 42 | errorTypeMessage: string; 43 | errors: GolangTestError[]; 44 | }; 45 | }; 46 | 47 | export type ValidationResult = ValidationResultSuccess | ValidationResultFailure | ValidationResultTestFailure; 48 | 49 | interface AppState { 50 | yamlSchema: YamlSchema; 51 | yamlInput: YamlInput; 52 | } 53 | 54 | const initialState: AppState = { 55 | yamlSchema: LocalStorageService.getItem(LocalStorageKey.yamlSchema), 56 | yamlInput: LocalStorageService.getItem(LocalStorageKey.yamlInput), 57 | }; 58 | 59 | export const appSlice = createSlice({ 60 | name: 'appSlice', 61 | initialState, 62 | reducers: { 63 | setYamlSchema: (state, action: PayloadAction) => { 64 | state.yamlSchema = action.payload; 65 | LocalStorageService.setItem(LocalStorageKey.yamlSchema, action.payload); 66 | }, 67 | setYamlInput: (state, action: PayloadAction) => { 68 | state.yamlInput = action.payload; 69 | LocalStorageService.setItem(LocalStorageKey.yamlInput, action.payload); 70 | }, 71 | }, 72 | }); 73 | 74 | export const { setYamlSchema, setYamlInput } = appSlice.actions; 75 | 76 | export const selectAppSlice = (state: RootState) => state.app; 77 | export const selectYamlSchema = (state: RootState) => state.app.yamlSchema; 78 | export const selectYamlInput = (state: RootState) => state.app.yamlInput; 79 | export const selectValidationResult = createSelector([selectYamlSchema, selectYamlInput], getValidationResult); 80 | 81 | export const appReducer = appSlice.reducer; 82 | -------------------------------------------------------------------------------- /src/App/appSlice/getValidationResult.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'js-yaml'; 2 | import parserYaml from 'prettier/parser-yaml'; 3 | import prettier from 'prettier/standalone'; 4 | 5 | import { isObject } from '../../utils/functions'; 6 | import { getErrorMessage } from '../../utils/getErrorMessage'; 7 | import { ErrorType, GolangResult, ValidationResult, YamlInput, YamlSchema } from './index'; 8 | 9 | export function getValidationResult(yamlSchema: YamlSchema, yamlInput: YamlInput): ValidationResult { 10 | // validate that input & schema are valid yaml 11 | try { 12 | yamlToObject(yamlSchema); 13 | } catch (error) { 14 | return { 15 | isSuccess: false, 16 | error: { 17 | errorType: ErrorType.yamlSchemaToObj, 18 | errorTypeMessage: 'YAML schema: invalid yaml', 19 | errorMessage: getErrorMessage(error), 20 | }, 21 | }; 22 | } 23 | try { 24 | yamlToObject(yamlInput); 25 | } catch (error) { 26 | return { 27 | isSuccess: false, 28 | error: { 29 | errorType: ErrorType.yamlInputToObj, 30 | errorTypeMessage: 'YAML input: invalid yaml', 31 | errorMessage: getErrorMessage(error), 32 | }, 33 | }; 34 | } 35 | 36 | // golang validation 37 | const result = window.validate(yamlSchema, yamlInput); 38 | const golangResult: GolangResult = JSON.parse(result); 39 | 40 | if (golangResult.valid) { 41 | return { 42 | isSuccess: true, 43 | successMessage: 'Input PASSES validation against schema', 44 | }; 45 | } 46 | if (golangResult.err) { 47 | return { 48 | isSuccess: false, 49 | error: { 50 | errorType: ErrorType.objToJsonSchema, 51 | errorTypeMessage: 'YAML schema: invalid yaml schema', 52 | errorMessage: golangResult.err, 53 | }, 54 | }; 55 | } 56 | if (golangResult.errors) { 57 | return { 58 | isSuccess: false, 59 | error: { 60 | errorType: ErrorType.test, 61 | errorTypeMessage: 'Input does NOT pass validation against schema', 62 | errors: golangResult.errors, 63 | }, 64 | }; 65 | } 66 | throw new Error(`unexpected golangResult${JSON.stringify(golangResult)}`); 67 | } 68 | 69 | function yamlToObject(yamlInput: string): object { 70 | const json = yaml.load(yamlInput); 71 | if (!isObject(json)) { 72 | throw new Error('invalid yaml'); 73 | } 74 | return json; 75 | } 76 | 77 | export function isYamlValid(value: string): boolean { 78 | let isYamlValid = true; 79 | try { 80 | yaml.load(value); 81 | } catch (e) { 82 | isYamlValid = false; 83 | } 84 | return isYamlValid; 85 | } 86 | 87 | export function formatYaml(source: string): string { 88 | return prettier.format(source, { 89 | parser: 'yaml', 90 | plugins: [parserYaml], 91 | tabWidth: 2, 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /src/App/appSlice/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appSlice'; 2 | -------------------------------------------------------------------------------- /src/App/services/LocalStorageService.ts: -------------------------------------------------------------------------------- 1 | import { defaultYamlInputValue } from '../../examples/defaultYamlInputValue'; 2 | import { defaultYamlSchemaValue } from '../../examples/defaultYamlSchemaValue'; 3 | 4 | export enum LocalStorageKey { 5 | yamlSchema = 'yamlSchema', 6 | yamlInput = 'yamlInput', 7 | } 8 | 9 | const localStorageDefaultValues: Record = { 10 | [LocalStorageKey.yamlSchema]: defaultYamlSchemaValue, 11 | [LocalStorageKey.yamlInput]: defaultYamlInputValue, 12 | }; 13 | 14 | export class LocalStorageService { 15 | static setItem(key: LocalStorageKey, value: string): void { 16 | localStorage.setItem(key, value); 17 | } 18 | static getItem(key: LocalStorageKey): string { 19 | return localStorage.getItem(key) ?? localStorageDefaultValues[key]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/App/services/compileJsonSchema.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { ValidateFunction } from 'ajv'; 2 | 3 | // TODO maybe add keywords and custom keys 4 | class CustomAjv extends Ajv {} 5 | 6 | const ajv = new CustomAjv({ allErrors: true }); 7 | 8 | export function compileJsonSchema(input: object): ValidateFunction { 9 | return ajv.compile(input); 10 | } 11 | -------------------------------------------------------------------------------- /src/LoadWasm/index.tsx: -------------------------------------------------------------------------------- 1 | import './wasm_exec.js'; 2 | import './wasmTypes.d.ts'; 3 | 4 | import { css } from '@emotion/css'; 5 | import { CircularProgress } from '@material-ui/core/'; 6 | import React, { useEffect } from 'react'; 7 | 8 | import { createUseClasses } from '../utils/createUseClasses'; 9 | 10 | async function loadWasm(): Promise { 11 | const goWasm = new window.Go(); 12 | const result = await WebAssembly.instantiateStreaming(fetch('main.wasm'), goWasm.importObject); 13 | goWasm.run(result.instance); 14 | } 15 | 16 | interface Props {} 17 | 18 | export const LoadWasm: React.FC = (props) => { 19 | const classes = useClasses(props); 20 | const [isLoading, setIsLoading] = React.useState(true); 21 | 22 | useEffect(() => { 23 | loadWasm().then(() => { 24 | setIsLoading(false); 25 | }); 26 | }, []); 27 | 28 | if (isLoading) { 29 | return ( 30 |
31 | 32 |
33 | ); 34 | } else { 35 | return {props.children}; 36 | } 37 | }; 38 | 39 | const useClasses = createUseClasses((_props: Props) => ({ 40 | root: css` 41 | width: 100vw; 42 | height: 100vh; 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | `, 47 | })); 48 | -------------------------------------------------------------------------------- /src/LoadWasm/wasmTypes.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | export interface Window { 3 | Go: new () => { 4 | importObject: WebAssembly.Imports; 5 | run: (instance: WebAssemblyInstantiatedSource['instance']) => Promise; 6 | }; 7 | validate: (yamlSchema: string, inputYaml: string) => string; 8 | } 9 | } 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /src/LoadWasm/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | const globalThis = window; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error('not implemented'); 10 | err.code = 'ENOSYS'; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ''; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf('\n'); 21 | if (nl !== -1) { 22 | console.log(outputBuf.substr(0, nl)); 23 | outputBuf = outputBuf.substr(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { 36 | callback(enosys()); 37 | }, 38 | chown(path, uid, gid, callback) { 39 | callback(enosys()); 40 | }, 41 | close(fd, callback) { 42 | callback(enosys()); 43 | }, 44 | fchmod(fd, mode, callback) { 45 | callback(enosys()); 46 | }, 47 | fchown(fd, uid, gid, callback) { 48 | callback(enosys()); 49 | }, 50 | fstat(fd, callback) { 51 | callback(enosys()); 52 | }, 53 | fsync(fd, callback) { 54 | callback(null); 55 | }, 56 | ftruncate(fd, length, callback) { 57 | callback(enosys()); 58 | }, 59 | lchown(path, uid, gid, callback) { 60 | callback(enosys()); 61 | }, 62 | link(path, link, callback) { 63 | callback(enosys()); 64 | }, 65 | lstat(path, callback) { 66 | callback(enosys()); 67 | }, 68 | mkdir(path, perm, callback) { 69 | callback(enosys()); 70 | }, 71 | open(path, flags, mode, callback) { 72 | callback(enosys()); 73 | }, 74 | read(fd, buffer, offset, length, position, callback) { 75 | callback(enosys()); 76 | }, 77 | readdir(path, callback) { 78 | callback(enosys()); 79 | }, 80 | readlink(path, callback) { 81 | callback(enosys()); 82 | }, 83 | rename(from, to, callback) { 84 | callback(enosys()); 85 | }, 86 | rmdir(path, callback) { 87 | callback(enosys()); 88 | }, 89 | stat(path, callback) { 90 | callback(enosys()); 91 | }, 92 | symlink(path, link, callback) { 93 | callback(enosys()); 94 | }, 95 | truncate(path, length, callback) { 96 | callback(enosys()); 97 | }, 98 | unlink(path, callback) { 99 | callback(enosys()); 100 | }, 101 | utimes(path, atime, mtime, callback) { 102 | callback(enosys()); 103 | }, 104 | }; 105 | } 106 | 107 | if (!globalThis.process) { 108 | globalThis.process = { 109 | getuid() { 110 | return -1; 111 | }, 112 | getgid() { 113 | return -1; 114 | }, 115 | geteuid() { 116 | return -1; 117 | }, 118 | getegid() { 119 | return -1; 120 | }, 121 | getgroups() { 122 | throw enosys(); 123 | }, 124 | pid: -1, 125 | ppid: -1, 126 | umask() { 127 | throw enosys(); 128 | }, 129 | cwd() { 130 | throw enosys(); 131 | }, 132 | chdir() { 133 | throw enosys(); 134 | }, 135 | }; 136 | } 137 | 138 | if (!globalThis.crypto) { 139 | throw new Error('globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)'); 140 | } 141 | 142 | if (!globalThis.performance) { 143 | throw new Error('globalThis.performance is not available, polyfill required (performance.now only)'); 144 | } 145 | 146 | if (!globalThis.TextEncoder) { 147 | throw new Error('globalThis.TextEncoder is not available, polyfill required'); 148 | } 149 | 150 | if (!globalThis.TextDecoder) { 151 | throw new Error('globalThis.TextDecoder is not available, polyfill required'); 152 | } 153 | 154 | const encoder = new TextEncoder('utf-8'); 155 | const decoder = new TextDecoder('utf-8'); 156 | 157 | globalThis.Go = class { 158 | constructor() { 159 | this.argv = ['js']; 160 | this.env = {}; 161 | this.exit = (code) => { 162 | if (code !== 0) { 163 | console.warn('exit code:', code); 164 | } 165 | }; 166 | this._exitPromise = new Promise((resolve) => { 167 | this._resolveExitPromise = resolve; 168 | }); 169 | this._pendingEvent = null; 170 | this._scheduledTimeouts = new Map(); 171 | this._nextCallbackTimeoutID = 1; 172 | 173 | const setInt64 = (addr, v) => { 174 | this.mem.setUint32(addr + 0, v, true); 175 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 176 | }; 177 | 178 | const getInt64 = (addr) => { 179 | const low = this.mem.getUint32(addr + 0, true); 180 | const high = this.mem.getInt32(addr + 4, true); 181 | return low + high * 4294967296; 182 | }; 183 | 184 | const loadValue = (addr) => { 185 | const f = this.mem.getFloat64(addr, true); 186 | if (f === 0) { 187 | return undefined; 188 | } 189 | if (!isNaN(f)) { 190 | return f; 191 | } 192 | 193 | const id = this.mem.getUint32(addr, true); 194 | return this._values[id]; 195 | }; 196 | 197 | const storeValue = (addr, v) => { 198 | const nanHead = 0x7ff80000; 199 | 200 | if (typeof v === 'number' && v !== 0) { 201 | if (isNaN(v)) { 202 | this.mem.setUint32(addr + 4, nanHead, true); 203 | this.mem.setUint32(addr, 0, true); 204 | return; 205 | } 206 | this.mem.setFloat64(addr, v, true); 207 | return; 208 | } 209 | 210 | if (v === undefined) { 211 | this.mem.setFloat64(addr, 0, true); 212 | return; 213 | } 214 | 215 | let id = this._ids.get(v); 216 | if (id === undefined) { 217 | id = this._idPool.pop(); 218 | if (id === undefined) { 219 | id = this._values.length; 220 | } 221 | this._values[id] = v; 222 | this._goRefCounts[id] = 0; 223 | this._ids.set(v, id); 224 | } 225 | this._goRefCounts[id]++; 226 | let typeFlag = 0; 227 | switch (typeof v) { 228 | case 'object': 229 | if (v !== null) { 230 | typeFlag = 1; 231 | } 232 | break; 233 | case 'string': 234 | typeFlag = 2; 235 | break; 236 | case 'symbol': 237 | typeFlag = 3; 238 | break; 239 | case 'function': 240 | typeFlag = 4; 241 | break; 242 | default: 243 | throw new Error(`Unsupported type: ${typeof v}`); 244 | } 245 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 246 | this.mem.setUint32(addr, id, true); 247 | }; 248 | 249 | const loadSlice = (addr) => { 250 | const array = getInt64(addr + 0); 251 | const len = getInt64(addr + 8); 252 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 253 | }; 254 | 255 | const loadSliceOfValues = (addr) => { 256 | const array = getInt64(addr + 0); 257 | const len = getInt64(addr + 8); 258 | const a = new Array(len); 259 | for (let i = 0; i < len; i++) { 260 | a[i] = loadValue(array + i * 8); 261 | } 262 | return a; 263 | }; 264 | 265 | const loadString = (addr) => { 266 | const saddr = getInt64(addr + 0); 267 | const len = getInt64(addr + 8); 268 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 269 | }; 270 | 271 | const timeOrigin = Date.now() - performance.now(); 272 | this.importObject = { 273 | go: { 274 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 275 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 276 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 277 | // This changes the SP, thus we have to update the SP used by the imported function. 278 | 279 | // func wasmExit(code int32) 280 | 'runtime.wasmExit': (sp) => { 281 | sp >>>= 0; 282 | const code = this.mem.getInt32(sp + 8, true); 283 | this.exited = true; 284 | delete this._inst; 285 | delete this._values; 286 | delete this._goRefCounts; 287 | delete this._ids; 288 | delete this._idPool; 289 | this.exit(code); 290 | }, 291 | 292 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 293 | 'runtime.wasmWrite': (sp) => { 294 | sp >>>= 0; 295 | const fd = getInt64(sp + 8); 296 | const p = getInt64(sp + 16); 297 | const n = this.mem.getInt32(sp + 24, true); 298 | window.fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 299 | }, 300 | 301 | // func resetMemoryDataView() 302 | 'runtime.resetMemoryDataView': (sp) => { 303 | sp >>>= 0; 304 | this.mem = new DataView(this._inst.exports.mem.buffer); 305 | }, 306 | 307 | // func nanotime1() int64 308 | 'runtime.nanotime1': (sp) => { 309 | sp >>>= 0; 310 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 311 | }, 312 | 313 | // func walltime() (sec int64, nsec int32) 314 | 'runtime.walltime': (sp) => { 315 | sp >>>= 0; 316 | const msec = new Date().getTime(); 317 | setInt64(sp + 8, msec / 1000); 318 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 319 | }, 320 | 321 | // func scheduleTimeoutEvent(delay int64) int32 322 | 'runtime.scheduleTimeoutEvent': (sp) => { 323 | sp >>>= 0; 324 | const id = this._nextCallbackTimeoutID; 325 | this._nextCallbackTimeoutID++; 326 | this._scheduledTimeouts.set( 327 | id, 328 | setTimeout( 329 | () => { 330 | this._resume(); 331 | while (this._scheduledTimeouts.has(id)) { 332 | // for some reason Go failed to register the timeout event, log and try again 333 | // (temporary workaround for https://github.com/golang/go/issues/28975) 334 | console.warn('scheduleTimeoutEvent: missed timeout event'); 335 | this._resume(); 336 | } 337 | }, 338 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 339 | ), 340 | ); 341 | this.mem.setInt32(sp + 16, id, true); 342 | }, 343 | 344 | // func clearTimeoutEvent(id int32) 345 | 'runtime.clearTimeoutEvent': (sp) => { 346 | sp >>>= 0; 347 | const id = this.mem.getInt32(sp + 8, true); 348 | clearTimeout(this._scheduledTimeouts.get(id)); 349 | this._scheduledTimeouts.delete(id); 350 | }, 351 | 352 | // func getRandomData(r []byte) 353 | 'runtime.getRandomData': (sp) => { 354 | sp >>>= 0; 355 | crypto.getRandomValues(loadSlice(sp + 8)); 356 | }, 357 | 358 | // func finalizeRef(v ref) 359 | 'syscall/js.finalizeRef': (sp) => { 360 | sp >>>= 0; 361 | const id = this.mem.getUint32(sp + 8, true); 362 | this._goRefCounts[id]--; 363 | if (this._goRefCounts[id] === 0) { 364 | const v = this._values[id]; 365 | this._values[id] = null; 366 | this._ids.delete(v); 367 | this._idPool.push(id); 368 | } 369 | }, 370 | 371 | // func stringVal(value string) ref 372 | 'syscall/js.stringVal': (sp) => { 373 | sp >>>= 0; 374 | storeValue(sp + 24, loadString(sp + 8)); 375 | }, 376 | 377 | // func valueGet(v ref, p string) ref 378 | 'syscall/js.valueGet': (sp) => { 379 | sp >>>= 0; 380 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 381 | sp = this._inst.exports.getsp() >>> 0; // see comment above 382 | storeValue(sp + 32, result); 383 | }, 384 | 385 | // func valueSet(v ref, p string, x ref) 386 | 'syscall/js.valueSet': (sp) => { 387 | sp >>>= 0; 388 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 389 | }, 390 | 391 | // func valueDelete(v ref, p string) 392 | 'syscall/js.valueDelete': (sp) => { 393 | sp >>>= 0; 394 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 395 | }, 396 | 397 | // func valueIndex(v ref, i int) ref 398 | 'syscall/js.valueIndex': (sp) => { 399 | sp >>>= 0; 400 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 401 | }, 402 | 403 | // valueSetIndex(v ref, i int, x ref) 404 | 'syscall/js.valueSetIndex': (sp) => { 405 | sp >>>= 0; 406 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 407 | }, 408 | 409 | // func valueCall(v ref, m string, args []ref) (ref, bool) 410 | 'syscall/js.valueCall': (sp) => { 411 | sp >>>= 0; 412 | try { 413 | const v = loadValue(sp + 8); 414 | const m = Reflect.get(v, loadString(sp + 16)); 415 | const args = loadSliceOfValues(sp + 32); 416 | const result = Reflect.apply(m, v, args); 417 | sp = this._inst.exports.getsp() >>> 0; // see comment above 418 | storeValue(sp + 56, result); 419 | this.mem.setUint8(sp + 64, 1); 420 | } catch (err) { 421 | sp = this._inst.exports.getsp() >>> 0; // see comment above 422 | storeValue(sp + 56, err); 423 | this.mem.setUint8(sp + 64, 0); 424 | } 425 | }, 426 | 427 | // func valueInvoke(v ref, args []ref) (ref, bool) 428 | 'syscall/js.valueInvoke': (sp) => { 429 | sp >>>= 0; 430 | try { 431 | const v = loadValue(sp + 8); 432 | const args = loadSliceOfValues(sp + 16); 433 | const result = Reflect.apply(v, undefined, args); 434 | sp = this._inst.exports.getsp() >>> 0; // see comment above 435 | storeValue(sp + 40, result); 436 | this.mem.setUint8(sp + 48, 1); 437 | } catch (err) { 438 | sp = this._inst.exports.getsp() >>> 0; // see comment above 439 | storeValue(sp + 40, err); 440 | this.mem.setUint8(sp + 48, 0); 441 | } 442 | }, 443 | 444 | // func valueNew(v ref, args []ref) (ref, bool) 445 | 'syscall/js.valueNew': (sp) => { 446 | sp >>>= 0; 447 | try { 448 | const v = loadValue(sp + 8); 449 | const args = loadSliceOfValues(sp + 16); 450 | const result = Reflect.construct(v, args); 451 | sp = this._inst.exports.getsp() >>> 0; // see comment above 452 | storeValue(sp + 40, result); 453 | this.mem.setUint8(sp + 48, 1); 454 | } catch (err) { 455 | sp = this._inst.exports.getsp() >>> 0; // see comment above 456 | storeValue(sp + 40, err); 457 | this.mem.setUint8(sp + 48, 0); 458 | } 459 | }, 460 | 461 | // func valueLength(v ref) int 462 | 'syscall/js.valueLength': (sp) => { 463 | sp >>>= 0; 464 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 465 | }, 466 | 467 | // valuePrepareString(v ref) (ref, int) 468 | 'syscall/js.valuePrepareString': (sp) => { 469 | sp >>>= 0; 470 | const str = encoder.encode(String(loadValue(sp + 8))); 471 | storeValue(sp + 16, str); 472 | setInt64(sp + 24, str.length); 473 | }, 474 | 475 | // valueLoadString(v ref, b []byte) 476 | 'syscall/js.valueLoadString': (sp) => { 477 | sp >>>= 0; 478 | const str = loadValue(sp + 8); 479 | loadSlice(sp + 16).set(str); 480 | }, 481 | 482 | // func valueInstanceOf(v ref, t ref) bool 483 | 'syscall/js.valueInstanceOf': (sp) => { 484 | sp >>>= 0; 485 | this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 0); 486 | }, 487 | 488 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 489 | 'syscall/js.copyBytesToGo': (sp) => { 490 | sp >>>= 0; 491 | const dst = loadSlice(sp + 8); 492 | const src = loadValue(sp + 32); 493 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 494 | this.mem.setUint8(sp + 48, 0); 495 | return; 496 | } 497 | const toCopy = src.subarray(0, dst.length); 498 | dst.set(toCopy); 499 | setInt64(sp + 40, toCopy.length); 500 | this.mem.setUint8(sp + 48, 1); 501 | }, 502 | 503 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 504 | 'syscall/js.copyBytesToJS': (sp) => { 505 | sp >>>= 0; 506 | const dst = loadValue(sp + 8); 507 | const src = loadSlice(sp + 16); 508 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 509 | this.mem.setUint8(sp + 48, 0); 510 | return; 511 | } 512 | const toCopy = src.subarray(0, dst.length); 513 | dst.set(toCopy); 514 | setInt64(sp + 40, toCopy.length); 515 | this.mem.setUint8(sp + 48, 1); 516 | }, 517 | 518 | debug: (value) => { 519 | console.log(value); 520 | }, 521 | }, 522 | }; 523 | } 524 | 525 | async run(instance) { 526 | if (!(instance instanceof WebAssembly.Instance)) { 527 | throw new Error('Go.run: WebAssembly.Instance expected'); 528 | } 529 | this._inst = instance; 530 | this.mem = new DataView(this._inst.exports.mem.buffer); 531 | this._values = [ 532 | // JS values that Go currently has references to, indexed by reference id 533 | NaN, 534 | 0, 535 | null, 536 | true, 537 | false, 538 | globalThis, 539 | this, 540 | ]; 541 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 542 | this._ids = new Map([ 543 | // mapping from JS values to reference ids 544 | [0, 1], 545 | [null, 2], 546 | [true, 3], 547 | [false, 4], 548 | [globalThis, 5], 549 | [this, 6], 550 | ]); 551 | this._idPool = []; // unused ids that have been garbage collected 552 | this.exited = false; // whether the Go program has exited 553 | 554 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 555 | let offset = 4096; 556 | 557 | const strPtr = (str) => { 558 | const ptr = offset; 559 | const bytes = encoder.encode(str + '\0'); 560 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 561 | offset += bytes.length; 562 | if (offset % 8 !== 0) { 563 | offset += 8 - (offset % 8); 564 | } 565 | return ptr; 566 | }; 567 | 568 | const argc = this.argv.length; 569 | 570 | const argvPtrs = []; 571 | this.argv.forEach((arg) => { 572 | argvPtrs.push(strPtr(arg)); 573 | }); 574 | argvPtrs.push(0); 575 | 576 | const keys = Object.keys(this.env).sort(); 577 | keys.forEach((key) => { 578 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 579 | }); 580 | argvPtrs.push(0); 581 | 582 | const argv = offset; 583 | argvPtrs.forEach((ptr) => { 584 | this.mem.setUint32(offset, ptr, true); 585 | this.mem.setUint32(offset + 4, 0, true); 586 | offset += 8; 587 | }); 588 | 589 | // The linker guarantees global data starts from at least wasmMinDataAddr. 590 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 591 | const wasmMinDataAddr = 4096 + 8192; 592 | if (offset >= wasmMinDataAddr) { 593 | throw new Error('total length of command line and environment variables exceeds limit'); 594 | } 595 | 596 | this._inst.exports.run(argc, argv); 597 | if (this.exited) { 598 | this._resolveExitPromise(); 599 | } 600 | await this._exitPromise; 601 | } 602 | 603 | _resume() { 604 | if (this.exited) { 605 | throw new Error('Go program has already exited'); 606 | } 607 | this._inst.exports.resume(); 608 | if (this.exited) { 609 | this._resolveExitPromise(); 610 | } 611 | } 612 | 613 | _makeFuncWrapper(id) { 614 | const go = this; 615 | return function () { 616 | const event = { id: id, this: this, args: arguments }; 617 | go._pendingEvent = event; 618 | go._resume(); 619 | return event.result; 620 | }; 621 | } 622 | }; 623 | })(); 624 | -------------------------------------------------------------------------------- /src/examples/customRuleKindEnum.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | kind: 4 | type: string 5 | enum: 6 | - Ingress 7 | - Service 8 | -------------------------------------------------------------------------------- /src/examples/defaultYamlInputValue.ts: -------------------------------------------------------------------------------- 1 | export const defaultYamlInputValue = `--- 2 | apiVersion: apps/v1 3 | kind: Service 4 | metadata: 5 | name: pass-policy 6 | labels: 7 | owner: me 8 | environment: prod 9 | app: web 10 | spec: 11 | containers: 12 | - name: nginx 13 | image: nginx:1.14.2 14 | ports: 15 | - containerPort: 80 16 | `; 17 | -------------------------------------------------------------------------------- /src/examples/defaultYamlSchemaValue.ts: -------------------------------------------------------------------------------- 1 | export const defaultYamlSchemaValue = `type: object 2 | properties: 3 | kind: 4 | type: string 5 | enum: 6 | - Ingress 7 | - Service 8 | `; 9 | -------------------------------------------------------------------------------- /src/examples/k8s-demo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Service 4 | metadata: 5 | name: pass-policy 6 | labels: 7 | owner: me 8 | environment: prod 9 | app: web 10 | spec: 11 | containers: 12 | - name: nginx 13 | image: nginx:1.14.2 14 | ports: 15 | - containerPort: 80 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* eslint-ignore */ 2 | /*@import-normalize; !* bring in normalize.css styles *!*/ 3 | 4 | body { 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 7 | 'Droid Sans', 'Helvetica Neue', sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | box-sizing: border-box; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Provider } from 'react-redux'; 6 | 7 | import App from './App/App'; 8 | import { LoadWasm } from './LoadWasm'; 9 | import { store } from './redux/store'; 10 | import reportWebVitals from './reportWebVitals'; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('root'), 21 | ); 22 | 23 | // If you want to start measuring performance in your app, pass a function 24 | // to log results (for example: reportWebVitals(console.log)) 25 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 26 | reportWebVitals(); 27 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/redux/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | 3 | import type { AppDispatch, RootState } from './store'; 4 | 5 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 6 | export const useAppDispatch = () => useDispatch(); 7 | export const useAppSelector: TypedUseSelectorHook = useSelector; 8 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | 3 | import { appReducer } from '../App/appSlice'; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | app: appReducer, 8 | }, 9 | }); 10 | 11 | // Infer the `RootState` and `AppDispatch` types from the store itself 12 | export type RootState = ReturnType; 13 | 14 | export type AppDispatch = typeof store.dispatch; 15 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils/createUseClasses/Readme.md: -------------------------------------------------------------------------------- 1 | #example usage: 2 | 3 | interface Props { 4 | size: number; 5 | } 6 | 7 | export const App = (props: Props) => { 8 | const classes = useClasses(props); 9 | 10 | return ( 11 |
12 |
13 | im the inner component 14 |
15 | Hello Datree 16 |
17 | ); 18 | }; 19 | 20 | const useClasses = createUseClasses((props) => ({ 21 | mainAppContainer: css` 22 | border: ${props.size}px solid green; 23 | background-color: green; 24 | `, 25 | innerComponent: css` 26 | background-color: red; 27 | opacity: 30%; 28 | `, 29 | })); 30 | 31 | ##note: you can replace the theme provider inside createUseClasses "useTheme" hook 32 | -------------------------------------------------------------------------------- /src/utils/createUseClasses/createUseClasses.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { useMemo } from 'react'; 3 | 4 | type CssString = ReturnType; 5 | 6 | type ClassesObject = Record; 7 | 8 | type ClassesFactory = (props: Props) => Classes; 9 | 10 | type UseClassesHook = (props: Props) => Classes; 11 | 12 | export function createUseClasses( 13 | classesFactory: ClassesFactory, 14 | ): UseClassesHook { 15 | return (props: Props) => { 16 | return useMemo(() => classesFactory(props), [props]); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/createUseClasses/index.ts: -------------------------------------------------------------------------------- 1 | import { createUseClasses } from './createUseClasses'; 2 | 3 | export { createUseClasses }; 4 | -------------------------------------------------------------------------------- /src/utils/functions.ts: -------------------------------------------------------------------------------- 1 | export function isObject(input: unknown): input is object { 2 | return typeof input === 'object' && input !== null; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/getErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import { YAMLException } from 'js-yaml'; 2 | 3 | type ErrorWithMessage = { 4 | message: string; 5 | }; 6 | 7 | function isErrorWithMessage(error: unknown): error is ErrorWithMessage { 8 | return ( 9 | typeof error === 'object' && 10 | error !== null && 11 | 'message' in error && 12 | typeof (error as Record).message === 'string' 13 | ); 14 | } 15 | 16 | function toErrorWithMessage(maybeError: unknown): ErrorWithMessage { 17 | if (isErrorWithMessage(maybeError)) return maybeError; 18 | 19 | try { 20 | return new Error(JSON.stringify(maybeError)); 21 | } catch { 22 | // fallback in case there's an error stringifying the maybeError 23 | // like with circular references for example. 24 | return new Error(String(maybeError)); 25 | } 26 | } 27 | 28 | function getYamlExceptionErrorMessage(error: YAMLException): string { 29 | const STRING_TO_REMOVE_FROM_START = 'YAMLException: '; 30 | const originalError = error.toString(true); 31 | if (originalError.startsWith(STRING_TO_REMOVE_FROM_START)) { 32 | return originalError.slice(STRING_TO_REMOVE_FROM_START.length); 33 | } else { 34 | return originalError; 35 | } 36 | } 37 | 38 | export function getErrorMessage(error: unknown) { 39 | return error instanceof YAMLException ? getYamlExceptionErrorMessage(error) : toErrorWithMessage(error).message; 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const emptyObject = {}; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /wasm/go.mod: -------------------------------------------------------------------------------- 1 | module wasm 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/ghodss/yaml v1.0.0 7 | github.com/xeipuuv/gojsonschema v1.2.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.1.0 // indirect 13 | github.com/stretchr/testify v1.7.0 // indirect 14 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 15 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 16 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 17 | gopkg.in/yaml.v2 v2.4.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /wasm/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 5 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 6 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 15 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 16 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 17 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 18 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 19 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 20 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 21 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 22 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 25 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 27 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 28 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 30 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /wasm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "syscall/js" 6 | ) 7 | 8 | type myResult struct { 9 | Context string `json:"context"` 10 | Description string `json:"description"` 11 | } 12 | 13 | func validate() js.Func { 14 | validator := New() 15 | return js.FuncOf(func(this js.Value, args []js.Value) (returnValue interface{}) { 16 | defer func() { 17 | if r := recover(); r != nil { 18 | returnValue = "{ \"err\": \"Unexpected error: " + ParseErrorToString(r) + ". This type of error should never happen! Please report the bug by visiting https://github.com/datreeio/yaml-schema-validator/issues/new/choose" + "\" }" 19 | } 20 | }() 21 | 22 | result, err := validator.ValidateYamlSchema(args[0].String(), args[1].String()) 23 | 24 | // put the full results in a struct 25 | var res struct { 26 | Valid bool `json:"valid"` 27 | Errors []myResult `json:"errors"` 28 | Err string `json:"err"` 29 | } 30 | if result != nil { 31 | res.Errors = make([]myResult, 0) 32 | res.Valid = result.Valid() 33 | errorsFound := result.Errors() 34 | if errorsFound != nil { 35 | for _, errorFound := range errorsFound { 36 | res.Errors = append(res.Errors, myResult{ 37 | Context: errorFound.Context().String(), 38 | Description: errorFound.Description(), 39 | }) 40 | } 41 | } 42 | } 43 | if err != nil { 44 | res.Err = err.Error() 45 | } 46 | 47 | // convert the struct to json 48 | jsonResult, err := json.Marshal(res) 49 | if err != nil { 50 | // should never happen 51 | panic(err) 52 | } 53 | // return the json as string 54 | returnValue = string(jsonResult) 55 | return 56 | }) 57 | } 58 | 59 | func main() { 60 | ch := make(chan struct{}, 0) 61 | js.Global().Set("validate", validate()) 62 | <-ch 63 | } 64 | -------------------------------------------------------------------------------- /wasm/parseErrorToString.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func ParseErrorToString(err interface{}) string { 6 | switch panicErr := err.(type) { 7 | case string: 8 | return panicErr 9 | case error: 10 | return panicErr.Error() 11 | default: 12 | return fmt.Sprintf("%v", panicErr) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /wasm/validator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ghodss/yaml" 5 | "github.com/xeipuuv/gojsonschema" 6 | ) 7 | 8 | type JSONSchemaValidator struct { 9 | } 10 | 11 | func New() *JSONSchemaValidator { 12 | return &JSONSchemaValidator{} 13 | } 14 | 15 | type Result = gojsonschema.Result 16 | 17 | func (jsv *JSONSchemaValidator) ValidateYamlSchema(schemaContent string, yamlContent string) (*Result, error) { 18 | jsonSchema, _ := yaml.YAMLToJSON([]byte(schemaContent)) 19 | return jsv.Validate(string(jsonSchema), yamlContent) 20 | } 21 | 22 | func (jsv *JSONSchemaValidator) Validate(schemaContent string, yamlContent string) (*Result, error) { 23 | jsonContent, _ := yaml.YAMLToJSON([]byte(yamlContent)) 24 | 25 | schemaLoader := gojsonschema.NewStringLoader(schemaContent) 26 | documentLoader := gojsonschema.NewStringLoader(string(jsonContent)) 27 | 28 | return gojsonschema.Validate(schemaLoader, documentLoader) 29 | } 30 | --------------------------------------------------------------------------------