├── .editorconfig ├── .eslintrc ├── .gitignore ├── .htaccess ├── .jsbeautifyrc ├── .travis.yml ├── .travis ├── travis-ssh-key.enc └── travis-ssh-key.pub ├── LICENSE ├── README.md ├── e2e-tests ├── .eslintrc ├── data.spec.js ├── interaction.spec.js ├── lib.js ├── protractor.conf.js ├── samples.spec.js └── smoke.spec.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── server.js ├── src ├── app.js ├── components │ ├── ValidationMessages.js │ ├── Validator.js │ ├── ValidatorView.js │ ├── index.js │ ├── validation-messages.html │ ├── validator-view.html │ └── validator.html ├── css │ └── main.css ├── dialogs │ └── About.html ├── service-worker.js ├── services │ ├── AlertService.js │ ├── Gist.js │ ├── MarkupJson.js │ ├── MarkupYaml.js │ ├── MetaSchemaLoader.js │ ├── TextService.js │ ├── ValidatorFactoryAJV.js │ ├── ValidatorFactoryJSV.js │ └── index.js └── vendor.js ├── tests ├── .eslintrc └── components │ ├── ValidationMessages.spec.js │ └── Validator.spec.js ├── webpack.common.config.js ├── webpack.dev.config.js ├── webpack.prod.config.js └── www ├── .htaccess ├── draft3 └── index.html ├── draft4 └── index.html ├── draft5 └── index.html ├── ga.js ├── index.html ├── samples ├── draft4 │ ├── invalid.document.json │ ├── invalid.schema.json │ ├── valid.document.json │ └── valid.schema.json ├── draft6 │ ├── invalid.document.json │ ├── invalid.schema.json │ ├── valid.document.json │ └── valid.schema.json ├── experimental │ ├── invalid.document.json │ ├── invalid.schema.json │ ├── valid.document.json │ └── valid.schema.json └── v5-unofficial │ ├── invalid.document.json │ ├── invalid.schema.json │ ├── valid.document.json │ └── valid.schema.json └── translations └── locale-en.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # 4 space indentation 16 | [**.{css,html}] 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // I want to use babel-eslint for parsing! 3 | "parser": "babel-eslint", 4 | "env": { 5 | // I write for browser 6 | "browser": true, 7 | // in CommonJS 8 | "node": true 9 | }, 10 | "extends": "eslint:recommended", 11 | // To give you an idea how to override rule options: 12 | "rules": { 13 | "quotes": [2, "single"], 14 | "eol-last": [0], 15 | "no-mixed-requires": [0], 16 | "no-underscore-dangle": [0], 17 | "indent": ["error", 2] 18 | }, 19 | "globals": { 20 | "angular": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /artifacts 2 | /dist 3 | /node_modules 4 | /.tmp 5 | /npm-debug.log 6 | /.env -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Order Allow,Deny 3 | Deny from all 4 | 5 | 6 | # Some things should never be served up 7 | RedirectMatch 404 /\.git 8 | RedirectMatch 404 /\.travis 9 | 10 | RewriteEngine On 11 | 12 | # Not letsencrypt challenge responses! 13 | RewriteCond %{REQUEST_URI} !^/\.well-known 14 | # Serve up everything in dist/ 15 | RewriteRule !^dist(/|$) dist%{REQUEST_URI} [L] 16 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "html": { 3 | "brace_style": "collapse", 4 | "indent_scripts": "normal", 5 | "max_preserve_newlines": 1, 6 | "preserve_newlines": true, 7 | "unformatted": ["a", "sub", "sup", "b", "i", "u"], 8 | "wrap_line_length": 0 9 | }, 10 | "js": { 11 | "indent_level": 0, 12 | "indent_with_tabs": false, 13 | "preserve_newlines": true, 14 | "max_preserve_newlines": 2, 15 | "jslint_happy": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - SAUCE_USERNAME=nickcmaynard-oss 3 | addons: 4 | ssh_known_hosts: 5 | - jsonschemalint.com # For deployment 6 | language: node_js 7 | node_js: 8 | - 12 9 | install: 10 | - npm ci 11 | script: 12 | - npm run ci:travis 13 | cache: 14 | directories: 15 | - node_modules 16 | deploy: 17 | # Deploy via rsync - only the dist/ folder 18 | provider: script 19 | skip_cleanup: true 20 | script: rsync -vrc --perms --chmod=u=rwX,g=rXs,o=rX -e 'ssh -o IdentitiesOnly=yes -o IdentityFile=$TRAVIS_BUILD_DIR/.travis/travis-ssh-key' $TRAVIS_BUILD_DIR/dist/ ncm@jsonschemalint.com:/ 21 | on: 22 | branch: master 23 | before_deploy: 24 | # Make the SSH key available 25 | - umask 0077 26 | - openssl aes-256-cbc -K $encrypted_ab3dc958fbe9_key -iv $encrypted_ab3dc958fbe9_iv -in .travis/travis-ssh-key.enc -out .travis/travis-ssh-key -d 27 | after_deploy: 28 | # Remove the SSH key 29 | - rm .travis/travis-ssh-key 30 | - npm run e2e-live 31 | notifications: 32 | email: 33 | - secure: "V8Lc5za7Oqi0osojMEFkmrt9C6bqedVOGIjF6YeVVpUKH61sgYrxU2ICHvPu1exu1c8aa4gs5kBuDGLsm74miikE3Z7YFRrYR8XpTo0Dd7rxeMT8GLV/UGjFrZ3NAxV/oFQzC6TBCNrD9swZnqNGnnTFRU09Ili+ARGkS8bCCO0=" 34 | -------------------------------------------------------------------------------- /.travis/travis-ssh-key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickcmaynard/jsonschemalint/0632de2ebb3bf4b6e13a1089b934e734ec57313f/.travis/travis-ssh-key.enc -------------------------------------------------------------------------------- /.travis/travis-ssh-key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCzLVIU9BK6iOOWvxM2JAPjdBNesIWWu4KfcYDgiCsXdT+mbaWx72zuTroK7Rcb8epr3Ubg8KOPvrSQnze3Os3YV9Koh5ycSz0DrsXsBZX+FtpBcW2T7WLgnZt26JgXqL4gUSXPhMkMJII7cS/Gs0lGL5ES7y46nDSYI20AOe9SlJRaRnpDpgl3UkBWmIZTJX+7Za9/Cctc3Sq6uTLG51a6CmzbH6+YICD3uSI5Aa3qrh7YcrAIe4ff3oHIsZ9PlwxhhUWVRqPelxZC0A3w3IkqkQ4dJEWzCSrSq8Ri/adTb1yNrLwWUTSM4avFzmk40Xx/ASBq82MBwl57I1BzOOCjS1b4OfzS3OwYEd3jIfceAMbfP04Gp1CAfBy7X1YCVU6l0Ln9pXesNgVkyc2+eW0Vqz1iEGXhkAu1Cu86bHyn5xQIFXzZohab2AMN4Szu9SlTlJqfo8yiI8uJZ4C/7WYNwOqrcMj7AchOSgoDZSYSw9oeCuDyT5IxLQ0nxM49iblwY7mPhr5UNMPC5oCBP5+zI7bmiSwZNJFeEVEpoh62MlG9NEZu0N10by5SJJQw6wdr2Mr2X00HQOVHjqh78U4bMTuQWsXtrxBsvM9lHQPwPHgLqoZCfl4/LGo+j+VQvEm9/XZB3MgypXjrBk7gTvs5ubNh3L1BoPyTVGbCebMJqw== nick@nmaynard-RH-OSX.local 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nick Maynard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Schema Lint 2 | 3 | [![Build Status](https://travis-ci.com/nickcmaynard/jsonschemalint.svg?branch=master)](https://travis-ci.com/nickcmaynard/jsonschemalint) 4 | 5 | ## Setup 6 | ```sh 7 | npm install 8 | ``` 9 | 10 | ## Run tests 11 | 12 | ```sh 13 | # Unit tests 14 | npm run test 15 | 16 | # End-to-end tests (build:dev is faster, but Travis uses build:production) 17 | npm run build:production 18 | npm run e2e 19 | 20 | # Run limited set of end-to-end tests on Chrome only - 2 terminals required 21 | npm run preview # terminal 1 22 | npm run build:dev # terminal 2 23 | npm run protractor -- --specs e2e-tests/smoke.spec.js --params.browsers="chrome" # terminal 2 24 | ``` 25 | 26 | ## Preview 27 | 28 | ### "live reload" webpack dev server 29 | 30 | ```sh 31 | npm run dev-server 32 | ``` 33 | Open [http://localhost:8080/webpack-dev-server](http://localhost:8080/webpack-dev-server). 34 | 35 | ### Static express preview (of dist/) 36 | ```sh 37 | npm run preview 38 | ``` 39 | 40 | Open [http://localhost:3001/](http://localhost:3001/). 41 | 42 | ## Building dist/ 43 | 44 | ### Production 45 | 46 | ```sh 47 | npm run build:production 48 | ``` 49 | 50 | ### Development (with sourcemap) 51 | ```sh 52 | npm run build:dev 53 | ``` 54 | -------------------------------------------------------------------------------- /e2e-tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-vars": [0] 4 | }, 5 | "globals": { 6 | "beforeEach": true, 7 | "expect": true, 8 | "describe": true, 9 | "it": true, 10 | "element": true, 11 | "by": true, 12 | "browser": true, 13 | "inject": true, 14 | "register": true, 15 | "chai": true, 16 | "protractor": true, 17 | "jasmine": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /e2e-tests/data.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EC = protractor.ExpectedConditions; 4 | var lib = require('./lib'); 5 | 6 | describe('data management', function() { 7 | 8 | it('should preserve schema and document across route changes', function() { 9 | browser.get('#/version/draft-04/markup/json'); 10 | 11 | var randomSchema = Math.random().toString(36).replace(/[^a-z]+/g, ''); 12 | var randomDocument = Math.random().toString(36).replace(/[^a-z]+/g, ''); 13 | 14 | // Reset 15 | element(by.buttonText('Reset')).click(); 16 | 17 | // Type something RANDOM in schema and document 18 | var schemaElement = element(by.css('validator[identifier=schema] textarea')); 19 | schemaElement.clear(); 20 | schemaElement.sendKeys(randomSchema); 21 | expect(schemaElement.evaluate('$ctrl.myDoc')).toEqual(randomSchema); 22 | var documentElement = element(by.css('validator[identifier=document] textarea')); 23 | documentElement.clear(); 24 | documentElement.sendKeys(randomDocument); 25 | expect(documentElement.evaluate('$ctrl.myDoc')).toEqual(randomDocument); 26 | 27 | // Select a different version (use the button!) 28 | // Open the spec version menu 29 | element(by.css('#specVersionDropdown')).click(); 30 | // SUSPICION - the samples menu displays outside angular's digest cycle. So protractor doesn't really know if it's there 31 | browser.wait(EC.visibilityOf(element(by.css('ul[aria-labelledby=specVersionDropdown]'))), 250); 32 | // Select a different version 33 | element(by.linkText('draft-03')).click(); 34 | browser.wait(lib.isDoneWorking, 2500); 35 | 36 | // Check the RANDOM stuff is still there 37 | expect(schemaElement.evaluate('$ctrl.myDoc')).toEqual(randomSchema); 38 | expect(documentElement.evaluate('$ctrl.myDoc')).toEqual(randomDocument); 39 | 40 | }); 41 | 42 | it('should preserve schema and document across page reloads', function() { 43 | browser.get('#/version/draft-04/markup/json'); 44 | 45 | var randomSchema = Math.random().toString(36).replace(/[^a-z]+/g, ''); 46 | var randomDocument = Math.random().toString(36).replace(/[^a-z]+/g, ''); 47 | 48 | // Reset 49 | element(by.buttonText('Reset')).click(); 50 | 51 | // Type something RANDOM in schema and document 52 | var schemaElement = element(by.css('validator[identifier=schema] textarea')); 53 | schemaElement.clear(); 54 | schemaElement.sendKeys(randomSchema); 55 | expect(schemaElement.evaluate('$ctrl.myDoc')).toEqual(randomSchema); 56 | var documentElement = element(by.css('validator[identifier=document] textarea')); 57 | documentElement.clear(); 58 | documentElement.sendKeys(randomDocument); 59 | expect(documentElement.evaluate('$ctrl.myDoc')).toEqual(randomDocument); 60 | 61 | // Reload it 62 | browser.refresh(); 63 | browser.wait(lib.isDoneWorking, 2500); 64 | 65 | // Check the RANDOM stuff is still there 66 | schemaElement = element(by.css('validator[identifier=schema] textarea')); 67 | documentElement = element(by.css('validator[identifier=document] textarea')); 68 | expect(schemaElement.evaluate('$ctrl.myDoc')).toEqual(randomSchema); 69 | expect(documentElement.evaluate('$ctrl.myDoc')).toEqual(randomDocument); 70 | 71 | }); 72 | 73 | }); -------------------------------------------------------------------------------- /e2e-tests/interaction.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var lib = require('./lib'); 4 | 5 | describe('document/spec interaction', function() { 6 | 7 | it('should show an explanatory error in the document errors when the schema is invalid', function() { 8 | browser.get('#/version/draft-04/markup/json'); 9 | 10 | var invalidSchema = `{ 11 | "type": "object", 12 | "properties": { 13 | "fooMin": { 14 | "type": "number", 15 | "minimum": "foo" 16 | } 17 | } 18 | }`; 19 | 20 | // Reset 21 | element(by.buttonText('Reset')).click(); 22 | 23 | // Type the invalid schema in the schema element 24 | var schemaElement = element(by.css('validator[identifier=schema] textarea')); 25 | schemaElement.sendKeys(invalidSchema); 26 | expect(schemaElement.evaluate('$ctrl.myDoc')).toEqual(invalidSchema); 27 | 28 | // And a basically valid JSON document 29 | var documentElement = element(by.css('validator[identifier=document] textarea')); 30 | documentElement.sendKeys('{}'); 31 | 32 | browser.wait(lib.isDoneWorking, 2500); 33 | 34 | var documentErrors = element.all(by.css('validator[identifier=document] validation-messages tbody tr')); 35 | 36 | expect(documentErrors.count()).toBeGreaterThan(0); 37 | 38 | }); 39 | 40 | }); -------------------------------------------------------------------------------- /e2e-tests/lib.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Check if the validator controllers have finished work 3 | isDoneWorking: function() { 4 | return element.all(by.css('validator > div')).evaluate('$ctrl.working').then(function(workingArr) { 5 | return workingArr.indexOf(true) === -1; 6 | }); 7 | } 8 | } -------------------------------------------------------------------------------- /e2e-tests/protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Allow ES6 in protractor tests 4 | require('babel-register'); 5 | 6 | // Dotenv 7 | require('dotenv').config() 8 | 9 | var SpecReporter = require('jasmine-spec-reporter').SpecReporter; 10 | 11 | // Standard capabilities to select 12 | var defaultCapabilities = [{ 13 | browserName: 'chrome' 14 | },{ 15 | browserName: 'firefox' 16 | }]; 17 | 18 | // Sauce Connect 19 | const useSauce = process.env.USE_SAUCE || process.env.TRAVIS; 20 | // Keep track 21 | let sauceConnectProcess; 22 | // Tunnel identifier 23 | const sauceTunnelId = useSauce ? (process.env.TRAVIS_JOB_NUMBER || 'cli-e2e') : undefined; 24 | 25 | //jshint strict: false 26 | var config = { 27 | 28 | allScriptsTimeout: 20000, 29 | 30 | specs: ['*.spec.js'], 31 | 32 | // Only resolves if environment is actually set up 33 | sauceUser: useSauce ? process.env.SAUCE_USERNAME : undefined, 34 | sauceKey: useSauce ? process.env.SAUCE_ACCESS_KEY : undefined, 35 | 36 | getMultiCapabilities: function() { 37 | // use --params.browsers='chrome,firefox' or --params.browsers='chrome' to select specific browsers 38 | var browsers = (this.params && this.params.browsers && this.params.browsers.split(',')) || defaultCapabilities.map((cap) => cap.browserName); 39 | let capabilities = defaultCapabilities.filter(function(cap) { 40 | return browsers.indexOf(cap.browserName) != -1; 41 | }); 42 | 43 | if (useSauce) { 44 | // In Travis, we test everything, so update the capabilities array 45 | capabilities = [ 46 | { 47 | 'browserName': 'chrome', 48 | 'version': 'latest', 49 | 'tunnel-identifier': sauceTunnelId, 50 | 'build': process.env.TRAVIS_BUILD_NUMBER 51 | }, { 52 | 'browserName': 'firefox', 53 | 'version': 'latest', 54 | 'tunnel-identifier': sauceTunnelId, 55 | 'build': process.env.TRAVIS_BUILD_NUMBER 56 | }, { 57 | 'browserName': 'MicrosoftEdge', 58 | 'version': 'latest', 59 | 'platform': 'Windows 10', 60 | 'tunnel-identifier': sauceTunnelId, 61 | 'build': process.env.TRAVIS_BUILD_NUMBER 62 | }, { 63 | 'browserName': 'internet explorer', 64 | 'version': 'latest', 65 | 'platform': 'Windows 10', 66 | 67 | // IE11 just doesn't play nice. Just run smoke tests 68 | 'exclude': ['data.spec.js','samples.spec.js', 'interaction.spec.js'], 69 | 70 | 'tunnel-identifier': sauceTunnelId, 71 | 'build': process.env.TRAVIS_BUILD_NUMBER 72 | } 73 | ]; 74 | 75 | // Launch Sauce Connect - we have to do it here because beforeLaunch doesn't wait for promises?! 76 | const SauceLabs = require('saucelabs').default; 77 | const account = new SauceLabs(); 78 | console.info('Launching Sauce Connect...'); 79 | const scp = account.startSauceConnect({ 80 | tunnelIdentifier: sauceTunnelId 81 | }).then(process => { 82 | console.info('...launched Sauce Connect.'); 83 | // Keep track 84 | sauceConnectProcess = process; 85 | }); 86 | 87 | // Wait for SCP then return capabilities for testing remotely 88 | return scp.then(() => { 89 | return capabilities; 90 | }); 91 | } else { 92 | // Capabilities for testing locally 93 | return capabilities; 94 | } 95 | }, 96 | 97 | baseUrl: process.env.LIVE ? 'https://jsonschemalint.com' : 'http://localhost:3001/', 98 | 99 | framework: 'jasmine2', 100 | 101 | jasmineNodeOpts: { 102 | defaultTimeoutInterval: 60000, 103 | print: function() {} 104 | }, 105 | 106 | onPrepare: function() { 107 | browser.driver.manage().window().setSize(1024, 700); 108 | jasmine.getEnv().addReporter(new SpecReporter({ 109 | spec: { 110 | displayStacktrace: true 111 | } 112 | })); 113 | }, 114 | 115 | afterLaunch: function() { 116 | if (sauceConnectProcess) { 117 | // Clean up Sauce Connect 118 | console.info('Closing Sauce Connect...'); 119 | // eslint-disable-next-line 120 | const p = sauceConnectProcess.close(); 121 | p.then(() => { 122 | console.info('...closed Sauce Connect.'); 123 | }); 124 | return p; 125 | } 126 | } 127 | 128 | }; 129 | 130 | exports.config = config; 131 | -------------------------------------------------------------------------------- /e2e-tests/samples.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EC = protractor.ExpectedConditions; 4 | var lib = require('./lib'); 5 | 6 | describe('samples', function() { 7 | 8 | var sampleTestMatrix = [ 9 | // Validating in draft-03 mode should force JSV to get loaded - the draft-04 samples (because they're simple) should be OK 10 | { mode: 'draft-03', sample: 'Sample draft-04 schema and valid document', schemaValid: true, documentValid: true }, 11 | { mode: 'draft-03', sample: 'Sample draft-04 schema and invalid document', schemaValid: true, documentValid: false }, 12 | // Validating in draft-04 mode to make sure Ajv's backwards compatible options are set properly 13 | { mode: 'draft-04', sample: 'Sample draft-04 schema and valid document', schemaValid: true, documentValid: true }, 14 | { mode: 'draft-04', sample: 'Sample draft-04 schema and invalid document', schemaValid: true, documentValid: false }, 15 | // Validating in draft-06 mode should force Ajv to get loaded 16 | { mode: 'draft-06', sample: 'Sample draft-06 schema and valid document', schemaValid: true, documentValid: true }, 17 | { mode: 'draft-06', sample: 'Sample draft-06 schema and invalid document', schemaValid: true, documentValid: false }, 18 | // Validating in draft-07 mode should force Ajv to get loaded 19 | { mode: 'draft-07', sample: 'Sample draft-06 schema and valid document', schemaValid: true, documentValid: true }, 20 | { mode: 'draft-07', sample: 'Sample draft-06 schema and invalid document', schemaValid: true, documentValid: false }, 21 | ]; 22 | var markupSampleTests = function(markup) { 23 | sampleTestMatrix.forEach(({mode, sample, schemaValid, documentValid}) => { 24 | it('in ' + mode + ' mode, should correctly validate the ' + sample, function() { 25 | browser.get('#/version/' + mode + '/markup/' + markup); 26 | 27 | // Open the samples menu 28 | element(by.buttonText('Samples')).click(); 29 | // SUSPICION - the samples menu displays outside angular's digest cycle. So protractor doesn't really know if it's there 30 | browser.wait(EC.visibilityOf(element(by.css('ul[aria-labelledby=sampleDropdown]'))), 250); 31 | 32 | // Click the sample to load it 33 | element(by.linkText(sample)).click(); 34 | // Wait for the validation processes to finish (samples to be loaded, any chunks to come in, etc.) 35 | browser.wait(lib.isDoneWorking, 2500); 36 | 37 | // Schema is valid/invalid 38 | expect(element(by.css('validator[identifier=schema] .panel.panel-' + (schemaValid ? 'success' : 'danger') )).isDisplayed()).toBeTruthy(); 39 | // Document is valid/invalid 40 | expect(element(by.css('validator[identifier=document] .panel.panel-' + (documentValid ? 'success' : 'danger') )).isDisplayed()).toBeTruthy(); 41 | }); 42 | }); 43 | }; 44 | 45 | describe('JSON samples', function() { 46 | markupSampleTests('json'); 47 | }); 48 | 49 | describe('YAML samples', function() { 50 | markupSampleTests('yaml'); 51 | }); 52 | 53 | }); -------------------------------------------------------------------------------- /e2e-tests/smoke.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('smoke tests', function() { 4 | 5 | it('should automatically redirect to a default version & markup when location hash/fragment is empty', function() { 6 | browser.get('index.html'); 7 | expect(browser.getCurrentUrl()).toMatch('/version/.*/markup/.*'); 8 | }); 9 | 10 | describe('about dialog', function() { 11 | it('should display when the button is clicked', function() { 12 | browser.get('index.html'); 13 | 14 | element(by.linkText('About')).click(); 15 | 16 | expect(element(by.cssContainingText('h3', 'About')).isDisplayed()).toBeTruthy(); 17 | }); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat May 23 2015 16:11:32 GMT+0200 (CEST) 3 | var webpackConfig = require('./webpack.dev.config'); 4 | 5 | // Remove CommonsChunkPlugin because it's incompatible with karma 6 | var commonsChunkPluginIndex = webpackConfig.plugins.findIndex(plugin => plugin.chunkNames); 7 | webpackConfig.plugins.splice(commonsChunkPluginIndex, 1); 8 | 9 | module.exports = function (config) { 10 | config.set({ 11 | 12 | // base path that will be used to resolve all patterns (eg. files, exclude) 13 | basePath: '', 14 | 15 | // frameworks to use 16 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 17 | frameworks: [ 18 | 'mocha', 'chai-spies', 'chai' 19 | ], 20 | 21 | // list of files / patterns to load in the browser 22 | files: [ 23 | // The app 24 | 'src/app.js', 25 | // The tests 26 | 'node_modules/angular-mocks/angular-mocks.js', 27 | 'tests/**/*.spec.js' 28 | ], 29 | 30 | // list of files to exclude 31 | exclude: [], 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | 'src/app.js': ['webpack'], 37 | 'tests/**/*.spec.js': ['babel'] 38 | }, 39 | 40 | babelPreprocessor: { 41 | options: { 42 | presets: [ 43 | ['env', { 44 | 'targets': { 45 | 'browsers': ['last 2 versions'] 46 | } 47 | }] 48 | ] 49 | } 50 | }, 51 | 52 | webpack: webpackConfig, 53 | 54 | webpackMiddleware: { 55 | // webpack-dev-middleware configuration 56 | // i. e. 57 | stats: 'errors-only' 58 | }, 59 | 60 | // test results reporter to use 61 | // possible values: 'dots', 'progress' 62 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 63 | reporters: ['spec'], 64 | 65 | specReporter: { 66 | maxLogLines: 5, // limit number of lines logged per test 67 | suppressErrorSummary: true, // do not print error summary 68 | suppressFailed: false, // do not print information about failed tests 69 | suppressPassed: false, // do not print information about passed tests 70 | suppressSkipped: false, // do not print information about skipped tests 71 | showSpecTiming: false // print the time elapsed for each spec 72 | }, 73 | 74 | // web server port 75 | port: 9876, 76 | 77 | // enable / disable colors in the output (reporters and logs) 78 | colors: true, 79 | 80 | // level of logging 81 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 82 | logLevel: config.LOG_INFO, 83 | 84 | // enable / disable watching file and executing tests whenever any file changes 85 | autoWatch: true, 86 | 87 | // start these browsers 88 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 89 | browsers: ['PhantomJS'], 90 | 91 | // Continuous Integration mode 92 | // if true, Karma captures browsers, runs the tests and exits 93 | singleRun: false 94 | }); 95 | }; 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nickcmaynard/jsonschemalint.com", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@bitbucket.org:nickcmaynard/json-schema-lint.git" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "scripts": { 13 | "lint": "./node_modules/.bin/eslint src tests e2e-tests", 14 | "preview": "node server.js", 15 | "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS", 16 | "dev-server": "./node_modules/.bin/webpack-dev-server --content-base dist/ --config webpack.dev.config.js", 17 | "update-webdriver": "./node_modules/.bin/webdriver-manager update", 18 | "preprotractor": "npm run update-webdriver", 19 | "protractor": "protractor e2e-tests/protractor.conf.js", 20 | "clean:dist": "npm run rimraf -- dist", 21 | "build:dev": "./node_modules/.bin/webpack --progress --config webpack.dev.config.js", 22 | "build:production": "./node_modules/.bin/webpack --progress --config webpack.prod.config.js", 23 | "ci:travis": "npm run lint && npm run clean:dist && npm run test && npm run build:production && npm run e2e", 24 | "e2e": "npm-run-all -p -r preview protractor", 25 | "e2e-live": "LIVE=true npm run protractor", 26 | "rimraf": "rimraf" 27 | }, 28 | "dependencies": { 29 | "JSV": "git://github.com/nickcmaynard/JSV.git#master", 30 | "ajv": "^6.12.2", 31 | "angular": "~1.8.0", 32 | "angular-route": "~1.8.0", 33 | "angular-sanitize": "~1.8.0", 34 | "angular-translate": "^2.18.2", 35 | "angular-translate-loader-static-files": "^2.18.2", 36 | "angular-ui-bootstrap": "^2.2.0", 37 | "compression": "^1.7.4", 38 | "es6-promise": "^4.2.8", 39 | "express": "^4.17.1", 40 | "winston": "^0.9.0", 41 | "yamljs": "^0.3.0" 42 | }, 43 | "devDependencies": { 44 | "angular-mocks": "~1.8.0", 45 | "babel-core": "^6.26.3", 46 | "babel-eslint": "^7.1.1", 47 | "babel-loader": "^7.1.5", 48 | "babel-polyfill": "^6.23.0", 49 | "babel-preset-env": "^1.7.0", 50 | "bootstrap": "^3.4.1", 51 | "chai": "^3.5.0", 52 | "chai-spies": "^0.7.1", 53 | "chunk-manifest-webpack-plugin": "^1.0.0", 54 | "copy-webpack-plugin": "^4.6.0", 55 | "css-loader": "^3.5.3", 56 | "dotenv": "^8.2.0", 57 | "eslint": "^6.8.0", 58 | "extract-text-webpack-plugin": "^3.0.2", 59 | "file-loader": "^0.10.0", 60 | "geckodriver": "^1.19.1", 61 | "html-loader": "^0.4.4", 62 | "html-webpack-plugin": "^2.28.0", 63 | "jasmine-spec-reporter": "^3.2.0", 64 | "karma": "^4.4.1", 65 | "karma-babel-preprocessor": "^6.0.1", 66 | "karma-chai": "^0.1.0", 67 | "karma-chai-spies": "^0.1.4", 68 | "karma-mocha": "^1.3.0", 69 | "karma-phantomjs-launcher": "^1.0.2", 70 | "karma-spec-reporter": "0.0.26", 71 | "karma-webpack": "^3.0.5", 72 | "mocha": "^5.2.0", 73 | "ng-annotate-webpack-plugin": "^0.1.3", 74 | "ngtemplate-loader": "^1.3.1", 75 | "npm-run-all": "^4.1.5", 76 | "phantomjs-prebuilt": "^2.1.14", 77 | "protractor": "^5.4.4", 78 | "protractor-jasmine2-screenshot-reporter": "^0.3.3", 79 | "rimraf": "^2.7.1", 80 | "saucelabs": "^4.5.0", 81 | "style-loader": "^0.13.1", 82 | "url-loader": "^1.1.2", 83 | "webpack": "^3.12.0", 84 | "webpack-dev-server": "^2.11.5", 85 | "webpack-md5-hash": "0.0.5", 86 | "webpack-merge": "^4.2.2" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var compression = require('compression'); 3 | 4 | var app = express(); 5 | 6 | // Logging 7 | var winston = require('winston'); 8 | winston.remove(winston.transports.Console); 9 | winston.add(winston.transports.Console, { 10 | colorize: true 11 | }); 12 | winston.level = 'info'; // make this "silly" to see all output 13 | var log = winston; 14 | 15 | // Basic logging 16 | app.use(function(req, res, next) { 17 | log.debug('%s %s', req.method, req.url); 18 | next(); 19 | }); 20 | 21 | app.use(compression()); 22 | 23 | app.use('/', express.static(__dirname + '/dist', { 24 | maxAge: '1h' 25 | })); 26 | 27 | app.listen(3001); 28 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | require('./css/main.css'); 2 | require('babel-polyfill'); 3 | 4 | var angular = require('angular'); 5 | 6 | angular.module('app', [require('angular-sanitize'), require('angular-route'), require('angular-ui-bootstrap'), require('angular-translate'), require('angular-translate-loader-static-files')]).config(function($routeProvider, $translateProvider) { 7 | 8 | $translateProvider.useSanitizeValueStrategy('sanitize'); 9 | // Bake in 'en' using webpack 10 | $translateProvider.translations('en', require('../www/translations/locale-en.json')); 11 | // For everything else, go to the static files loader 12 | $translateProvider.useStaticFilesLoader({ 13 | prefix: 'translations/locale-', 14 | suffix: '.json' 15 | }); 16 | $translateProvider.preferredLanguage('en'); 17 | 18 | $routeProvider.when('/version/draft-05/markup/:markupLanguage', { 19 | redirectTo: function(params) { 20 | return '/version/v5-unofficial/markup/' + params.markupLanguage; 21 | } 22 | }); 23 | $routeProvider.when('/version/:specVersion/markup/:markupLanguage', { 24 | template: '' 25 | }); 26 | $routeProvider.otherwise({ 27 | redirectTo: '/version/draft-07/markup/json' 28 | }); 29 | 30 | }).run(function($rootScope, $location, $window, $log) { 31 | $rootScope.$on('$routeChangeSuccess', function() { 32 | $log.info('Hash change, informing GA', $location.path()); 33 | $window.ga('send', 'pageview', $location.path()); 34 | }); 35 | }); 36 | 37 | require('./services'); 38 | require('./components'); 39 | -------------------------------------------------------------------------------- /src/components/ValidationMessages.js: -------------------------------------------------------------------------------- 1 | var templateUrl = require('ngtemplate-loader?relativeTo=/src/!html-loader!./validation-messages.html') 2 | 3 | //TODO: split this into two separate validation messages and validation errors component 4 | 5 | function ValidationMessagesController() { 6 | // Only the "error" messages 7 | this.errorMessages = function(messages) { 8 | return messages && messages.filter(a => typeof a.dataPath !== 'undefined'); 9 | }; 10 | 11 | // Only "simple" messages 12 | this.simpleMessages = function(messages) { 13 | return messages && messages.filter(a => typeof a.dataPath === 'undefined'); 14 | } 15 | } 16 | 17 | angular.module('app').component('validationMessages', { 18 | templateUrl: templateUrl, 19 | controller: ValidationMessagesController, 20 | bindings: { 21 | messages: '=' 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/Validator.js: -------------------------------------------------------------------------------- 1 | var templateUrl = require('ngtemplate-loader?relativeTo=/src/!html-loader!./validator.html') 2 | 3 | function ValidatorController($scope, $element, $attrs, $log, $q, $window) { 4 | 5 | var self = this; 6 | 7 | // Make a *copy* so we don't accidentally effect the parent's version 8 | this.$onInit = function() { 9 | this.myDoc = this.doc; 10 | }; 11 | 12 | this.$onChanges = function(changesObj) { 13 | // If any bound properties, such as validate, parse, doc, etc. change, rerun validation 14 | // $log.debug(this.identifier + ".$onChanges()", changesObj, JSON.stringify(this.myDoc)); 15 | 16 | // We need to be a bit careful - we can't trust this "changesObj" entirely, as it actually represents the projected current & previous values of the *external* model, not our internal model 17 | let doUpdate = false; 18 | if (Object.prototype.hasOwnProperty.call(changesObj, 'doc') && changesObj.doc.currentValue !== this.myDoc) { 19 | this.myDoc = changesObj.doc.currentValue; 20 | doUpdate = true; 21 | } 22 | if (Object.prototype.hasOwnProperty.call(changesObj, 'validate') || Object.prototype.hasOwnProperty.call(changesObj, 'parse')) { 23 | doUpdate = true; 24 | } 25 | 26 | if (doUpdate) { 27 | this.update(this.myDoc); 28 | } 29 | }; 30 | 31 | let previousDoc = undefined; 32 | this.update = function(doc) { 33 | $log.debug(this.identifier + '.update()'); 34 | if (previousDoc !== doc) { 35 | this.onUpdateDoc({value: doc}); 36 | } 37 | previousDoc = doc; 38 | 39 | if (!this.validate || !this.parse) { 40 | // Abort 41 | return this.messages = [{ 42 | message: 'Invalid setup, validator is ' + this.validate 43 | },{ 44 | message: 'Invalid setup, parse is ' + this.parse 45 | }]; 46 | } 47 | 48 | // KEEPME: Keep track of whether we're "working". This is used by the e2e tests 49 | $log.debug(self.identifier + '.update()', 'Begining work'); 50 | this.working = true; 51 | 52 | // Parse 53 | var parsePromise = this.parse(doc); 54 | parsePromise.then(function(obj) { 55 | $log.debug(self.identifier + '.update()', 'Successful parsing', obj); 56 | }, function(errors) { 57 | $log.debug(self.identifier + '.update()', 'Errors parsing', errors); 58 | }); 59 | 60 | // Validate, taking input from parse 61 | var validatePromise = parsePromise.then(angular.bind(this, this.validate)); 62 | validatePromise.then(function(obj) { 63 | $log.debug(self.identifier + '.update()', 'Successful validating', obj); 64 | }, function(errors) { 65 | $log.debug(self.identifier + '.update()', 'Errors validating', errors); 66 | }); 67 | 68 | // Combine the two, fail-fast (so if parse fails, we fail immediately rather than waiting for validate) 69 | var comboPromise = $q.all([parsePromise, validatePromise]).then(function(results) { 70 | // Successful validation 71 | $log.debug(self.identifier + '.update()', 'Successful parsing and validation', results); 72 | 73 | var obj = results[0]; 74 | 75 | self.onUpdateObj({value: obj}); 76 | self.isValid = true; 77 | return self.messages = [{ 78 | message: self.successMessage 79 | }]; 80 | }, function(errors) { 81 | // Something went wrong failures 82 | $log.debug(self.identifier + '.update()', 'Errors parsing/validating document', errors); 83 | 84 | self.onUpdateObj({value: null}); 85 | self.isValid = false; 86 | return self.messages = [].concat(errors); 87 | }); 88 | 89 | comboPromise.finally(function() { 90 | // KEEPME: Keep track of whether we're "working". This is used by the e2e tests 91 | $log.debug(self.identifier + '.update()', 'Done working'); 92 | self.working = false; 93 | }); 94 | }; 95 | 96 | this.format = function (doc) { 97 | $log.debug(this.identifier + '.format()'); 98 | var p = this.parse(doc).then(angular.bind(this, this.prettyPrint)); 99 | // Function 100 | p.then(function (text) { 101 | self.myDoc = text; 102 | self.update(self.myDoc); 103 | }); 104 | // Analytics 105 | p.then(function() { 106 | $window.ga('send', { 107 | hitType: 'event', 108 | eventCategory: 'Content', 109 | eventAction: 'Format', 110 | eventLabel: self.identifier 111 | }); 112 | }, function(error) { 113 | $window.ga('send', 'exception', { 114 | exDescription: 'content-format-error :: ' + error.message 115 | }); 116 | }); 117 | }; 118 | 119 | } 120 | 121 | angular.module('app').component('validator', { 122 | templateUrl: templateUrl, 123 | controller: ValidatorController, 124 | bindings: { 125 | 'identifier': '@', 126 | 'title': '@', 127 | 'doc': '<', 128 | 'validate': '<', 129 | 'parse': '<', 130 | 'prettyPrint': '<', 131 | 'successMessage': '<', 132 | 'onUpdateDoc': '&', 133 | 'onUpdateObj': '&', 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /src/components/ValidatorView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var aboutDialogTemplateUrl = require('ngtemplate-loader?relativeTo=/src/!html-loader!../dialogs/About.html'); 4 | 5 | var templateUrl = require('ngtemplate-loader?relativeTo=/src/!html-loader!./validator-view.html'); 6 | 7 | function ValidatorViewController($scope, $rootScope, $log, $http, $window, $q, $route, $location, $uibModal, $templateCache, gist, markupJson, markupYaml, validatorFactoryJSV, validatorFactoryAJV, alertService, textService) { 8 | 9 | var self = this; 10 | 11 | this.parseMarkup = null; 12 | this.validateSchema = null; 13 | this.validateDocument = null; 14 | 15 | // Set up spec versions 16 | this.validators = { 17 | 'draft-01': { 18 | service: validatorFactoryJSV('draft-01'), 19 | name: 'draft-01' 20 | }, 21 | 'draft-02': { 22 | service: validatorFactoryJSV('draft-02'), 23 | name: 'draft-02' 24 | }, 25 | 'draft-03': { 26 | service: validatorFactoryJSV('draft-03'), 27 | name: 'draft-03' 28 | }, 29 | 'draft-04': { 30 | service: validatorFactoryAJV('draft-04'), 31 | name: 'draft-04' 32 | }, 33 | 'draft-06': { 34 | service: validatorFactoryAJV('draft-06'), 35 | name: 'draft-06' 36 | }, 37 | 'draft-07': { 38 | service: validatorFactoryAJV('draft-07'), 39 | name: 'draft-07', 40 | label: 'draft-07 (latest)' 41 | }, 42 | // RETIRED schema versions 43 | 'v5-unofficial': { 44 | name: 'v5-unofficial', 45 | alerts: [{ 46 | className: 'alert-danger', 47 | content_tid: 'WARNING_V5_UNOFFICIAL' 48 | }], 49 | hidden: true 50 | }, 51 | 'experimental': { 52 | name: 'experimental', 53 | alerts: [{ 54 | className: 'alert-danger', 55 | content_tid: 'WARNING_EXPERIMENTAL' 56 | }], 57 | hidden: true 58 | } 59 | }; 60 | this.validatorsArr = function() { 61 | return Object.values(this.validators); 62 | }; 63 | 64 | // Set up markup languages 65 | this.markupLanguages = { 66 | 'json': { 67 | service: markupJson, 68 | name: 'JSON' 69 | }, 70 | 'yaml': { 71 | service: markupYaml, 72 | name: 'YAML' 73 | } 74 | }; 75 | 76 | this.about = function(event) { 77 | // Stop the link redirecting us to # 78 | event.preventDefault(); 79 | event.stopPropagation(); 80 | 81 | alertService.alert({title: '{{ "ABOUT" | translate }}', message: $templateCache.get(aboutDialogTemplateUrl), btnClass: 'btn-primary', size: 'lg'}); 82 | 83 | return false; 84 | }; 85 | 86 | // Getters 87 | this.getDocument = textService.getDocument; 88 | this.getSchema = textService.getSchema; 89 | 90 | // Reset everything 91 | this.reset = textService.reset; 92 | 93 | // Load a sample 94 | this.sample = function(ref) { 95 | $log.debug('sample', ref); 96 | 97 | var p = this.getCurrentMarkupService(); 98 | // Function 99 | p.then(function(markupService) { 100 | $http.get('samples/' + ref + '.document.json').then(function(response) { 101 | markupService.prettyPrint(response.data).then(function(text) { 102 | textService.setDocument(text); 103 | }); 104 | }); 105 | $http.get('samples/' + ref + '.schema.json').then(function(response) { 106 | markupService.prettyPrint(response.data).then(function(text) { 107 | textService.setSchema(text); 108 | }); 109 | }); 110 | }, function(errors) { 111 | alertService.alert({title: '{{ "ERROR_SAMPLE_LOADING" | translate }}', message: errors[0].message, btnClass: 'btn-danger'}); 112 | }); 113 | // Analytics 114 | p.then(function() { 115 | $window.ga('send', { 116 | hitType: 'event', 117 | eventCategory: 'Samples', 118 | eventAction: 'Load', 119 | eventLabel: ref 120 | }); 121 | }, function(errors) { 122 | $window.ga('send', 'exception', { 123 | exDescription: 'sample-load-error :: ' + errors[0].message 124 | }); 125 | }); 126 | }; 127 | 128 | // Load a Gist by ID 129 | this.loadGist = function(gistId) { 130 | this.loadedGistId = gistId; 131 | 132 | var p = gist.retrieve(gistId); 133 | // Function 134 | p.then(function(gist) { 135 | $log.info('Retrieved gist', gistId, gist); 136 | 137 | self.loadedGist = gist; 138 | 139 | textService.setSchema(gist.schema); 140 | textService.setDocument(gist.document); 141 | 142 | // Register a once-off listener - if schema or document change, clobber the gist param 143 | var canceller, 144 | documentListener, 145 | schemaListener; 146 | canceller = function() { 147 | $log.info('Content changed from loaded gist, altering state to allow for this'); 148 | // Don't show the gist ID in the URL 149 | $route.updateParams({gist: null}); 150 | // Clear the watch 151 | schemaListener && schemaListener(); 152 | documentListener && documentListener(); 153 | // Clobber the local "we're looking at a gist" variables 154 | delete self.loadedGist; 155 | delete self.loadedGistId; 156 | }; 157 | schemaListener = $scope.$watch('$ctrl.getSchema()', function(newValue) { 158 | if (self.loadedGist && newValue !== self.loadedGist.schema) { 159 | canceller(); 160 | } 161 | }); 162 | documentListener = $scope.$watch('$ctrl.getDocument()', function(newValue) { 163 | if (self.loadedGist && newValue !== self.loadedGist.document) { 164 | canceller(); 165 | } 166 | }); 167 | 168 | }, function(error) { 169 | $log.error(error); 170 | alertService.alert({title: '{{ "ERROR_GIST_LOADING" | translate }}', message: error, btnClass: 'btn-danger'}); 171 | }); 172 | // Analytics 173 | p.then(function() { 174 | $window.ga('send', { 175 | hitType: 'event', 176 | eventCategory: 'Gists', 177 | eventAction: 'Load' 178 | }); 179 | }, function(error) { 180 | $window.ga('send', 'exception', { 181 | exDescription: 'gist-load-error :: ' + error 182 | }); 183 | }); 184 | }; 185 | 186 | // Save a Gist and inform of success 187 | this.saveGist = function() { 188 | var p = gist.save(textService.getSchema(), textService.getDocument()); 189 | // Function 190 | p.then(function(gistId) { 191 | $route.updateParams({gist: gistId}); 192 | var url = $location.absUrl(); 193 | alertService.alert({ 194 | title: '{{ "GIST_SAVED" | translate }}', 195 | message: '{{ "GIST_VISIT" | translate }}' 196 | }); 197 | }, function(error) { 198 | $log.error(error); 199 | alertService.alert({title: '{{ "ERROR_GIST_SAVING" | translate }}', message: error, btnClass: 'btn-danger'}); 200 | 201 | }); 202 | // Analytics 203 | p.then(function() { 204 | $window.ga('send', { 205 | hitType: 'event', 206 | eventCategory: 'Gists', 207 | eventAction: 'Save' 208 | }); 209 | }, function(error) { 210 | $window.ga('send', 'exception', { 211 | exDescription: 'gist-save-error :: ' + error 212 | }); 213 | }); 214 | }; 215 | 216 | // Change the selected spec version 217 | this.setSpecVersion = function(specVersion) { 218 | $route.updateParams({specVersion: specVersion}); 219 | }; 220 | 221 | // Change the selected markup 222 | this.setMarkupLanguage = function(markupLanguage) { 223 | $route.updateParams({markupLanguage: markupLanguage}); 224 | }; 225 | 226 | // Wrapper functions to be bound to the Validator inputs 227 | this._parseMarkup = function(text) { 228 | $log.debug('_parseMarkup'); 229 | return this.getCurrentMarkupService().then(function(service) { 230 | return service.parse(text); 231 | }); 232 | }; 233 | this._prettyPrint = function(obj) { 234 | $log.debug('_prettyPrint', obj); 235 | return this.getCurrentMarkupService().then(function(service) { 236 | return service.prettyPrint(obj); 237 | }); 238 | }; 239 | this._validateSchema = function(obj) { 240 | $log.debug('_validateSchema', obj); 241 | return this.getCurrentValidationService().then(function(service) { 242 | return service.validateSchema(obj); 243 | }); 244 | }; 245 | this._validateDocument = function(schemaObj, obj) { 246 | $log.debug('_validateDocument', schemaObj, obj); 247 | if (!schemaObj) { 248 | return $q.reject([ 249 | { 250 | message_tid: 'ERROR_INVALID_SCHEMA' 251 | } 252 | ]); 253 | } 254 | return this.getCurrentValidationService().then(function(service) { 255 | return service.validate(schemaObj, obj); 256 | }); 257 | }; 258 | 259 | // Get currently referred-to validation service object 260 | this.getCurrentValidationService = function() { 261 | $log.debug('getCurrentValidationService'); 262 | if (!this.currentValidator) { 263 | // Abort 264 | return $q.reject([ 265 | { 266 | message_tid: 'ERROR_INVALID_VERSION', 267 | message_params: self.currentParams 268 | } 269 | ]); 270 | } 271 | return $q.when(this.currentValidator.service); 272 | } 273 | 274 | // Get currently referred-to markup service object 275 | this.getCurrentMarkupService = function() { 276 | $log.debug('getCurrentMarkupService'); 277 | if (!this.currentMarkup) { 278 | return $q.reject([ 279 | { 280 | message_tid: 'ERROR_INVALID_MARKUP', 281 | message_params: self.currentParams 282 | } 283 | ]); 284 | } 285 | return $q.when(this.currentMarkup.service); 286 | } 287 | 288 | // When the route changes, register the new versions 289 | $log.info('Selected JSON Schema version :: ' + $route.current.params.specVersion); 290 | self.currentValidator = self.validators[$route.current.params.specVersion]; 291 | self.validateSchema = angular.bind(self, self._validateSchema); 292 | self.validateDocument = angular.bind(self, self._validateDocument, null); 293 | 294 | $log.info('Selected markup language :: ' + $route.current.params.markupLanguage); 295 | self.currentMarkup = self.markupLanguages[$route.current.params.markupLanguage]; 296 | self.parseMarkup = angular.bind(self, self._parseMarkup); 297 | self.prettyPrint = angular.bind(self, self._prettyPrint); 298 | 299 | if ($route.current.params.gist && self.loadedGistId != $route.current.params.gist) { 300 | $log.info('Loading gist :: ' + $route.current.params.gist, self.loadedGistId); 301 | self.loadGist($route.current.params.gist); 302 | } 303 | 304 | // Notice when Validator components tell us things have changed 305 | this.onUpdateSchemaObj = function(obj) { 306 | // Re-bind validateDocument so an update happens 307 | $log.debug('Schema object changed'); 308 | this.validateDocument = angular.bind(this, this._validateDocument, obj); 309 | }; 310 | this.onUpdateDocumentString = function(doc) { 311 | $log.debug('Document string changed'); 312 | textService.setDocument(doc); 313 | }; 314 | this.onUpdateSchemaString = function(doc) { 315 | $log.debug('Schema string changed'); 316 | textService.setSchema(doc); 317 | }; 318 | 319 | } 320 | 321 | angular.module('app').component('validatorView', { 322 | templateUrl: templateUrl, 323 | controller: ValidatorViewController 324 | }); 325 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | require('./ValidationMessages'); 2 | require('./Validator'); 3 | require('./ValidatorView'); -------------------------------------------------------------------------------- /src/components/validation-messages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 |
{{ 'MESSAGE' | translate }}
{{ 'FIELD' | translate }}{{ 'ERROR' | translate }}{{ 'VALUE' | translate }}
22 |

23 |             
28 | -------------------------------------------------------------------------------- /src/components/validator-view.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 86 | 87 |
88 |
89 |
90 |
91 |
92 |
93 | 94 |
95 | 96 |
97 | 98 | 99 | 100 |
101 | 102 |
103 | 104 | 105 | 106 |
107 | 108 |
109 |
110 | 111 |
112 | -------------------------------------------------------------------------------- /src/components/validator.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{$ctrl.title}} 4 | 8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 2 | display: none !important; 3 | } 4 | 5 | body { 6 | padding-top: 60px; 7 | } 8 | 9 | textarea.document-text { 10 | font-family: Monaco, Andale Mono, monospace; 11 | font-size: smaller; 12 | resize: none; 13 | height: 300px; 14 | 15 | border: 0; 16 | border-bottom: 1px solid #ccc; 17 | border-radius: 0; 18 | } 19 | 20 | pre.error-value { 21 | border: 0; 22 | padding: 0; 23 | } 24 | 25 | table.validation-messages-table { 26 | table-layout: fixed; 27 | } 28 | 29 | td.error-path { 30 | word-break: break-word; 31 | } 32 | 33 | validation-messages .table { 34 | margin: 1em 0 0; 35 | } -------------------------------------------------------------------------------- /src/dialogs/About.html: -------------------------------------------------------------------------------- 1 |

JSON Schema Lint is a JSON schema validator to help you write and test JSON Schemas that conform with various specification versions.

2 |

The author/maintainer is Nick Maynard. Fork this project on Github.

3 |

Thanks 4 |

10 |

-------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | const CACHE_NAME = (new Date()).toISOString(); 2 | 3 | // Eagerly cache everything when service worker is installed 4 | self.addEventListener('install', function (e) { 5 | e.waitUntil( 6 | caches.open(CACHE_NAME).then(function (cache) { 7 | return cache.addAll(global.serviceWorkerOption.assets); 8 | }) 9 | ); 10 | }); 11 | 12 | // Fetch from cache if it's there 13 | self.addEventListener('fetch', function (event) { 14 | event.respondWith( 15 | caches.match(event.request).then(function (response) { 16 | return response || fetch(event.request); 17 | }) 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /src/services/AlertService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('app', false); 4 | 5 | app.service('alertService', function ($q, $uibModal) { 6 | 7 | this.alert = function (params) { 8 | if (!params) { 9 | return $q.reject('params required'); 10 | } 11 | return $uibModal.open({ 12 | animation: false, 13 | template: ``, 14 | ariaLabelledBy: 'modal-title-top', 15 | ariaDescribedBy: 'modal-body', 16 | controller: function () {}, 17 | size: params.size || 'sm' 18 | }).result; 19 | }; 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /src/services/Gist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('app', false); 4 | 5 | var base = 'https://api.github.com/gists'; 6 | 7 | app.service('gist', function ($q, $http, $log, $translate) { 8 | 9 | // Save the schema and document as a secret anonymous gist 10 | this.save = function(schema, doc) { 11 | return $http({ 12 | method: 'POST', 13 | url: base, 14 | data: { 15 | 'description': 'jsonschemalint.com ' + (new Date().toISOString()), 16 | 'public': false, 17 | 'files': { 18 | 'schema': { 19 | 'content': schema 20 | }, 21 | 'document': { 22 | 'content': doc 23 | } 24 | } 25 | } 26 | }).then(function(result) { 27 | $log.info('Saved gist successfully with ID', result.data.id); 28 | return result.data.id; 29 | }, function(error) { 30 | $log.error('Could not save gist', error); 31 | throw error.statusText; 32 | }); 33 | }; 34 | 35 | // Get the schema and document 36 | this.retrieve = function(gistId) { 37 | return $http({ 38 | method: 'GET', 39 | url: base + '/' + gistId 40 | }).then(function(result) { 41 | // Sanity check! 42 | if(!result || !result.data || !result.data.files || !result.data.files['schema'] || !result.data.files['document']) { 43 | return $translate('ERROR_GIST_FORMAT').then(function(errorStr) { 44 | return $q.reject(errorStr); 45 | }); 46 | } 47 | return { 48 | 'schema': result.data.files['schema'].content, 49 | 'document': result.data.files['document'].content 50 | }; 51 | }, function(error) { 52 | $log.error('Could not retrieve gist', error); 53 | throw error.statusText; 54 | }); 55 | }; 56 | 57 | }); -------------------------------------------------------------------------------- /src/services/MarkupJson.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('app', false); 4 | 5 | app.service('markupJson', function ($q) { 6 | 7 | this.parse = function (text) { 8 | return $q(function (resolve, reject) { 9 | try { 10 | var obj = JSON.parse(text); 11 | resolve(obj); 12 | } catch (err) { 13 | reject([{ 14 | message_tid: 'ERROR_INVALID_JSON' 15 | }]); 16 | } 17 | }); 18 | }; 19 | 20 | this.prettyPrint = function(obj) { 21 | return $q.when(JSON.stringify(obj, null, ' ')); 22 | }; 23 | 24 | }); -------------------------------------------------------------------------------- /src/services/MarkupYaml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('app', false); 4 | 5 | app.service('markupYaml', function ($window, $q, alertService, $log) { 6 | 7 | var yaml; 8 | 9 | var _setupPromise; 10 | var setup = function() { 11 | return _setupPromise || (_setupPromise = $q(function(resolve, reject) { 12 | try { 13 | require.ensure([], function(require) { 14 | yaml = require('yamljs'); 15 | $log.debug('MarkupYAML.setup()', 'Loaded yamljs'); 16 | resolve(true); 17 | }); 18 | } catch (error) { 19 | $log.error('MarkupYAML.setup()', 'Could not load yamljs', error); 20 | alertService.alert({ 21 | title: '{{ "ERROR_MODULE_LOADING_FAILED_TITLE" | translate }}', 22 | message: '{{ "ERROR_MODULE_LOADING_FAILED_CONTENT" | translate }}' 23 | }); 24 | reject(error); 25 | } 26 | })); 27 | }; 28 | 29 | this.parse = function (text) { 30 | return setup().then(function () { 31 | try { 32 | var obj = yaml.parse(text); 33 | return obj; 34 | } catch (err) { 35 | throw [{ 36 | message_tid: 'ERROR_INVALID_YAML' 37 | }]; 38 | } 39 | }); 40 | }; 41 | 42 | this.prettyPrint = function (obj) { 43 | // We wrap this in $q otherwise the digest doesn't fire correctly 44 | return setup().then(function () { 45 | return yaml.stringify(obj, 4, 2); 46 | }); 47 | }; 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /src/services/MetaSchemaLoader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.draft4 = require('../../node_modules/ajv/lib/refs/json-schema-draft-04.json'); 4 | exports.draft6 = require('../../node_modules/ajv/lib/refs/json-schema-draft-06.json'); 5 | exports.draft7 = require('../../node_modules/ajv/lib/refs/json-schema-draft-07.json'); 6 | -------------------------------------------------------------------------------- /src/services/TextService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('app', false); 4 | 5 | app.service('textService', function($q, $window, $log) { 6 | 7 | var self = this; 8 | 9 | this._schemaString; 10 | this._documentString; 11 | 12 | // Accessors 13 | this.getSchema = function() { 14 | return self._schemaString; 15 | }; 16 | this.setSchema = function(str) { 17 | if (str !== self._schemaString) { 18 | $window.ga('send', { 19 | hitType: 'event', 20 | eventCategory: 'Content', 21 | eventAction: 'Schema-Change' 22 | }); 23 | } 24 | self._schemaString = str; 25 | }; 26 | this.getDocument = function() { 27 | return self._documentString; 28 | }; 29 | this.setDocument = function(str) { 30 | if (str !== self._documentString) { 31 | $window.ga('send', { 32 | hitType: 'event', 33 | eventCategory: 'Content', 34 | eventAction: 'Document-Change' 35 | }); 36 | } 37 | self._documentString = str; 38 | }; 39 | 40 | this.reset = function() { 41 | // Analytics 42 | $window.ga('send', { 43 | hitType: 'event', 44 | eventCategory: 'Content', 45 | eventAction: 'Reset' 46 | }); 47 | 48 | delete self._schemaString; 49 | delete self._documentString; 50 | ls.removeItem('data'); 51 | ls.removeItem('schema'); 52 | }; 53 | 54 | // Load document & schema from localstorage 55 | var ls = $window['localStorage']; 56 | if (ls.getItem('data')) { 57 | $log.info('Loading document from local storage'); 58 | self._documentString = ls.getItem('data'); 59 | } 60 | if (ls.getItem('schema')) { 61 | $log.info('Loading schema from local storage'); 62 | self._schemaString = ls.getItem('schema'); 63 | } 64 | 65 | // Save form data to localstorage before unload 66 | $window.addEventListener('beforeunload', function() { 67 | if (self._documentString) { 68 | ls.setItem('data', self._documentString); 69 | } else { 70 | ls.removeItem('data'); 71 | } 72 | if (self._schemaString) { 73 | ls.setItem('schema', self._schemaString); 74 | } else { 75 | ls.removeItem('schema'); 76 | } 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /src/services/ValidatorFactoryAJV.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('app', false); 4 | 5 | // Handles draft-04 and up 6 | app.factory('validatorFactoryAJV', function ($window, $q, alertService, $log) { 7 | 8 | var Validator = function (version) { 9 | // Initially unset for lazy-loading 10 | var validator; 11 | 12 | var _setupPromise; 13 | var setup = function () { 14 | return _setupPromise || (_setupPromise = $q(function(resolve, reject) { 15 | try { 16 | require.ensure([], function(require) { 17 | var ajv = require('ajv'); 18 | var schemas = require('./MetaSchemaLoader'); 19 | $log.debug('ValidatorFactoryAJV.setup()', 'Loaded AJV'); 20 | 21 | validator = ajv({ 22 | verbose: true, 23 | allErrors: true, 24 | meta: false, // Don't load a meta-schema by default 25 | // 26 | // Loading logic follows guidance at 27 | // https://github.com/epoberezkin/ajv/releases/tag/5.0.0 28 | // and 29 | // https://github.com/epoberezkin/ajv/releases/tag/v6.0.0 30 | // 31 | unknownFormats: 'draft-04' === version ? 'ignore' : undefined, 32 | extendRefs: 'draft-04' === version ? true : undefined, 33 | schemaId: 'draft-04' === version ? 'id': undefined 34 | }); 35 | 36 | if (version === 'draft-04') { 37 | validator.addMetaSchema(schemas.draft4); 38 | validator._opts.defaultMeta = schemas.draft4.id; 39 | validator._refs['http://json-schema.org/schema'] = 'http://json-schema.org/draft-04/schema'; 40 | validator.removeKeyword('propertyNames'); 41 | validator.removeKeyword('contains'); 42 | validator.removeKeyword('const'); 43 | } 44 | else if (version === 'draft-06') { 45 | validator.addMetaSchema(schemas.draft6); 46 | validator._opts.defaultMeta = schemas.draft6.$id; 47 | } 48 | else if (version === 'draft-07') { 49 | validator.addMetaSchema(schemas.draft7); 50 | validator._opts.defaultMeta = schemas.draft7.$id; 51 | } 52 | resolve(true); 53 | }); 54 | } catch (error) { 55 | $log.error('ValidatorFactoryAJV.setup()', 'Could not load AJV', error); 56 | alertService.alert({ 57 | title: '{{ "ERROR_MODULE_LOADING_FAILED_TITLE" | translate }}', 58 | message: '{{ "ERROR_MODULE_LOADING_FAILED_CONTENT" | translate }}' 59 | }); 60 | reject(error); 61 | } 62 | })); 63 | }; 64 | 65 | this.validateSchema = function (schemaObject) { 66 | $log.debug('ValidatorFactoryAJV.validateSchema()'); 67 | return setup().then(function () { 68 | if (validator.validateSchema(schemaObject)) { 69 | $log.debug('ValidatorFactoryAJV.validateSchema()', validator.errorsText(validator.errors)); 70 | return true; 71 | } else { 72 | $log.debug('ValidatorFactoryAJV.validateSchema()', validator.errorsText(validator.errors)); 73 | throw validator.errors; 74 | } 75 | }); 76 | }; 77 | 78 | this.validate = function (schemaObject, documentObject) { 79 | $log.debug('ValidatorFactoryAJV.validate()'); 80 | return setup().then(function () { 81 | var result; 82 | try { 83 | result = validator.validate(schemaObject, documentObject); 84 | } catch (e) { 85 | // Some errors are thrown by Ajv, not wrapped up in its nice validator.errors interface 86 | $log.error('ValidatorFactoryAJV.validate()', e.message); 87 | // Wrap the exception into our standard format 88 | throw [{ message: e.message }]; 89 | } 90 | // Validation completed - check the results 91 | if(result) { 92 | $log.debug('ValidatorFactoryAJV.validate()', 'success'); 93 | return true; 94 | } else { 95 | $log.error('ValidatorFactoryAJV.validate()', validator.errorsText(validator.errors)); 96 | throw validator.errors; 97 | } 98 | }); 99 | }; 100 | }; 101 | 102 | // Factory for AJV validators 103 | return function (version) { 104 | return new Validator(version); 105 | }; 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /src/services/ValidatorFactoryJSV.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('app', false); 4 | 5 | // Handles draft-01, draft-02, draft-03 6 | app.factory('validatorFactoryJSV', function ($window, $q, alertService, $log) { 7 | 8 | var Validator = function (version) { 9 | // Initially unset for lazy-loading 10 | var validator; 11 | var schemaSchema; 12 | 13 | var _setupPromise; 14 | var setup = function () { 15 | // We wrap this in $q otherwise the digest doesn't fire correctly 16 | return _setupPromise || (_setupPromise = $q(function(resolve, reject) { 17 | try { 18 | require.ensure([], function(require) { 19 | var JSV = require('JSV'); 20 | $log.debug('ValidatorFactoryJSV.setup()', 'Loaded JSV'); 21 | var jsv = JSV.JSV; 22 | // 23 | // VERSION DETERMINATION LOGIC 24 | // 25 | validator = jsv.createEnvironment('json-schema-' + version); 26 | schemaSchema = validator.getDefaultSchema(); 27 | resolve(true); 28 | }); 29 | } catch (error) { 30 | $log.error('ValidatorFactoryJSV.setup()', 'Could not load JSV', error); 31 | alertService.alert({ 32 | title: '{{ "ERROR_MODULE_LOADING_FAILED_TITLE" | translate }}', 33 | message: '{{ "ERROR_MODULE_LOADING_FAILED_CONTENT" | translate }}' 34 | }); 35 | reject(error); 36 | } 37 | })); 38 | }; 39 | 40 | // Convert JSV errors into something the tables understand 41 | var mapError = function (e) { 42 | var field = e.uri.substring(e.uri.indexOf('#') + 1); 43 | return { 44 | dataPath: field, 45 | message: e.message, 46 | data: e.details.length ? e.details[0] : '' 47 | }; 48 | }; 49 | 50 | this.validateSchema = function (schemaObject) { 51 | $log.debug('ValidatorFactoryJSV.validateSchema()'); 52 | return setup().then(angular.bind(this, function () { 53 | var results = validator.validate(schemaObject, schemaSchema); 54 | if (!results.errors || !results.errors.length) { 55 | $log.debug('ValidatorFactoryJSV.validateSchema()', 'success'); 56 | return true; 57 | } else { 58 | $log.debug('ValidatorFactoryJSV.validateSchema()', 'failure', results.errors); 59 | throw results.errors.map(mapError); 60 | } 61 | })); 62 | }; 63 | 64 | this.validate = function (schemaObject, documentObject) { 65 | $log.debug('ValidatorFactoryJSV.validate()'); 66 | return setup().then(angular.bind(this, function () { 67 | var results = validator.validate(documentObject, schemaObject); 68 | if (!results.errors || !results.errors.length) { 69 | $log.debug('ValidatorFactoryJSV.validate()', 'success'); 70 | return true; 71 | } else { 72 | $log.debug('ValidatorFactoryJSV.validate()', 'failure', results.errors); 73 | throw results.errors.map(mapError); 74 | } 75 | })); 76 | }; 77 | }; 78 | 79 | // Factory for JSV validators 80 | return function (version) { 81 | return new Validator(version); 82 | }; 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | require('./AlertService'); 2 | require('./Gist'); 3 | require('./MarkupJson'); 4 | require('./MarkupYaml'); 5 | require('./MetaSchemaLoader'); 6 | require('./TextService'); 7 | require('./ValidatorFactoryAJV'); 8 | require('./ValidatorFactoryJSV'); -------------------------------------------------------------------------------- /src/vendor.js: -------------------------------------------------------------------------------- 1 | // This file contains "vendor" packages that should be pulled out of the app.js chunk created by webpack 2 | require('babel-polyfill'); 3 | require('bootstrap/dist/css/bootstrap.min.css'); 4 | require('angular'); 5 | require('angular-route'); 6 | require('angular-sanitize'); 7 | require('angular-translate'); 8 | require('angular-translate-loader-static-files'); 9 | require('angular-ui-bootstrap'); 10 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-vars": [0] 4 | }, 5 | "globals": { 6 | "beforeEach": true, 7 | "expect": true, 8 | "describe": true, 9 | "it": true, 10 | "element": true, 11 | "by": true, 12 | "browser": true, 13 | "inject": true, 14 | "register": true, 15 | "chai": true, 16 | "protractor": true, 17 | "jasmine": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/components/ValidationMessages.spec.js: -------------------------------------------------------------------------------- 1 | describe('ValidationMessages', function() { 2 | var $compile, $rootScope, $scope; 3 | 4 | // Load the app module, which contains the component 5 | beforeEach(module('app')); 6 | 7 | // Store references to $rootScope and $compile 8 | // so they are available to all tests in this describe block 9 | beforeEach(inject(function(_$compile_, _$rootScope_){ 10 | // The injector unwraps the underscores (_) from around the parameter names when matching 11 | $compile = _$compile_; 12 | $rootScope = _$rootScope_; 13 | $scope = $rootScope.$new(); 14 | })); 15 | 16 | it('mounts the component', function() { 17 | var element = $compile('')($scope); 18 | $rootScope.$digest(); 19 | 20 | // Must have compiled and inserted a scope properly - check that $scope has a child scope 21 | expect($scope.$$childHead.$ctrl).to.be.ok; 22 | }); 23 | 24 | it('doesn\'t mount a misnamed component', function() { 25 | var element = $compile('')($scope); 26 | $rootScope.$digest(); 27 | 28 | // Should not have compiled and inserted a scope properly 29 | expect($scope.$$childHead).not.to.be.ok; 30 | }); 31 | 32 | it('is empty when given no data', function() { 33 | var element = $compile('')($scope); 34 | $rootScope.$digest(); 35 | 36 | expect(element[0].querySelectorAll('*')).to.have.length(0); 37 | }); 38 | 39 | it('has a row for each message', function() { 40 | var element = $compile('')($scope); 41 | $rootScope.$digest(); 42 | 43 | var $ctrl = $scope.$$childHead.$ctrl; 44 | 45 | // Simple messages 46 | $scope.scopeProp = [ { 'message':'foo' }, { 'message':'foo' } ]; 47 | $rootScope.$digest(); 48 | 49 | expect(element[0].querySelectorAll('tbody tr')).to.have.length(2); 50 | 51 | // Complex messages 52 | $scope.scopeProp = [ { 'message':'foo', 'dataPath': 'bar' }, { 'message':'foo', 'dataPath': 'bar2' }, { 'message':'foo', 'dataPath': 'bar3' } ]; 53 | $rootScope.$digest(); 54 | 55 | expect(element[0].querySelectorAll('tbody tr')).to.have.length(3); 56 | 57 | }); 58 | 59 | it('correctly classifies simple and error messages', function() { 60 | var element = $compile('')($scope); 61 | $rootScope.$digest(); 62 | 63 | var $ctrl = $scope.$$childHead.$ctrl; 64 | 65 | expect($ctrl.simpleMessages([{ message: 'foo' }])).to.have.length(1); 66 | expect($ctrl.errorMessages([{ message: 'foo' }])).to.have.length(0); 67 | 68 | expect($ctrl.simpleMessages([{ message: 'foo', 'dataPath': '' }])).to.have.length(0); 69 | expect($ctrl.errorMessages([{ message: 'foo', 'dataPath': '' }])).to.have.length(1); 70 | 71 | expect($ctrl.simpleMessages([{ message: 'foo', 'dataPath': 'bar' }])).to.have.length(0); 72 | expect($ctrl.errorMessages([{ message: 'foo', 'dataPath': 'bar' }])).to.have.length(1); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/components/Validator.spec.js: -------------------------------------------------------------------------------- 1 | describe('Validator', function() { 2 | var $compile, 3 | $rootScope, 4 | $scope, 5 | $q; 6 | 7 | // Load the app module, which contains the component 8 | beforeEach(module('app')); 9 | 10 | // Store references to $rootScope and $compile 11 | // so they are available to all tests in this describe block 12 | beforeEach(inject(function(_$compile_, _$rootScope_, _$q_) { 13 | // The injector unwraps the underscores (_) from around the parameter names when matching 14 | $q = _$q_; 15 | $compile = _$compile_; 16 | $rootScope = _$rootScope_; 17 | $scope = $rootScope.$new(); 18 | })); 19 | 20 | // // Debug logging logic 21 | // var $log; 22 | // 23 | // // Inject the $log service 24 | // beforeEach(inject(function(_$log_){ 25 | // $log = _$log_; 26 | // })); 27 | // 28 | // // Log debug messages in Karma 29 | // afterEach(function(){ 30 | // console.log($log.debug.logs); 31 | // }); 32 | 33 | // Utility method to create an element and get a reference to its controller, from some injected HTML 34 | const _createInstance = (html) => { 35 | let element = $compile(html)($scope); 36 | $rootScope.$digest(); 37 | 38 | let $ctrl = $scope.$$childHead.$ctrl; 39 | 40 | return {element, $ctrl}; 41 | }; 42 | 43 | it('mounts the component', function() { 44 | const {element, $ctrl} = _createInstance(''); 45 | 46 | // Must have compiled and inserted a scope properly - check that $scope has a child scope 47 | expect($scope.$$childHead.$ctrl).to.be.ok; 48 | }); 49 | 50 | it('accepts a validate function', function() { 51 | $scope.echo = function(a) { 52 | return a; 53 | }; 54 | 55 | const {element, $ctrl} = _createInstance(''); 56 | 57 | expect($ctrl).to.be.ok; 58 | expect($ctrl.validate).to.be.a.function; 59 | expect($ctrl.validate('foo')).to.eql('foo'); 60 | }); 61 | 62 | it('accepts a parse function', function() { 63 | $scope.echo = function(a) { 64 | return a; 65 | }; 66 | 67 | const {element, $ctrl} = _createInstance(''); 68 | 69 | expect($ctrl).to.be.ok; 70 | expect($ctrl.parse).to.be.a.function; 71 | expect($ctrl.parse('foo')).to.eql('foo'); 72 | }); 73 | 74 | it('contains a form with a document text area', function() { 75 | const {element, $ctrl} = _createInstance(''); 76 | 77 | expect(element[0].querySelector('form textarea.validator-document')).not.to.be.empty; 78 | }); 79 | 80 | it('binds its document textarea to its document model', function() { 81 | $scope.scopeProp = 'foo'; 82 | 83 | const {element, $ctrl} = _createInstance(''); 84 | 85 | var textarea = element[0].querySelector('form textarea.validator-document'); 86 | expect(textarea.value).to.eql('foo'); 87 | }); 88 | 89 | it('doesn\'t call update when nothing is changed', function() { 90 | const {element, $ctrl} = _createInstance(''); 91 | 92 | var spy = chai.spy.on($ctrl, 'update'); 93 | 94 | // Run a digest 95 | $rootScope.$digest(); 96 | 97 | expect(spy).to.not.have.been.called(); 98 | }); 99 | 100 | it('calls update when the validate function is changed', function() { 101 | const {element, $ctrl} = _createInstance(''); 102 | 103 | var spy = chai.spy.on($ctrl, 'update'); 104 | 105 | // Change the function 106 | $scope.fn = function() { 107 | // blah 108 | }; 109 | $rootScope.$digest(); 110 | 111 | expect(spy).to.have.been.called.once; 112 | }); 113 | 114 | it('calls update when the parse function is changed', function() { 115 | const {element, $ctrl} = _createInstance(''); 116 | 117 | var spy = chai.spy.on($ctrl, 'update'); 118 | 119 | // Change the function 120 | $scope.fn = function() { 121 | // blah 122 | }; 123 | $rootScope.$digest(); 124 | 125 | expect(spy).to.have.been.called.once; 126 | }); 127 | 128 | it('calls update when the injected document changes', function() { 129 | const {element, $ctrl} = _createInstance(''); 130 | 131 | var spy = chai.spy.on($ctrl, 'update'); 132 | 133 | // Change the injected document 134 | $scope.upperDoc = 'flibble!' 135 | $rootScope.$digest(); 136 | 137 | expect(spy).to.have.been.called.once; 138 | }); 139 | 140 | it('doesn\'t call update when the injected document changes, but it\'s identical to the internal document', function() { 141 | const {element, $ctrl} = _createInstance(''); 142 | 143 | // Set up the internal state 144 | $ctrl.myDoc = 'flibble!'; 145 | $ctrl.update($ctrl.myDoc); 146 | $rootScope.$digest(); 147 | 148 | var spy = chai.spy.on($ctrl, 'update'); 149 | 150 | // Change the injected document 151 | $scope.upperDoc = 'flibble!' 152 | $rootScope.$digest(); 153 | 154 | expect(spy).to.not.have.been.called(); 155 | }); 156 | 157 | it('calls on-update-obj with null when the injected document doesn\'t validate', function() { 158 | const spy = chai.spy(function(params) {}); 159 | $scope.onUpdateObj = spy; 160 | 161 | const {element, $ctrl} = _createInstance(''); 162 | 163 | // We *must* use $q else the promises don't resolve properly :/ 164 | // Pretend it parses 165 | $ctrl.parse = () => $q.resolve({}); 166 | // But that it doesn't validate 167 | $ctrl.validate = () => $q.reject([]); 168 | $rootScope.$digest(); 169 | 170 | // Change the injected document 171 | $scope.upperDoc = 'flibble!' 172 | $rootScope.$digest(); 173 | 174 | expect(spy).to.have.been.called.once; 175 | expect(spy).to.have.been.always.with.exactly(null); 176 | }); 177 | 178 | it('calls on-update-obj with null when the injected document doesn\'t parse', function() { 179 | const spy = chai.spy(function(params) {}); 180 | $scope.onUpdateObj = spy; 181 | 182 | const {element, $ctrl} = _createInstance(''); 183 | 184 | // We *must* use $q else the promises don't resolve properly :/ 185 | // Pretend it doesn't parse 186 | $ctrl.parse = () => $q.reject([]); 187 | // But that it does (somehow! magic!) validate 188 | $ctrl.validate = () => $q.resolve({}); 189 | $rootScope.$digest(); 190 | 191 | // Change the injected document 192 | $scope.upperDoc = 'flibble!' 193 | $rootScope.$digest(); 194 | 195 | expect(spy).to.have.been.called.once; 196 | expect(spy).to.have.been.always.with.exactly(null); 197 | }); 198 | 199 | it('calls on-update-obj with a value when the injected document parses and validates', function() { 200 | const spy = chai.spy(function(params) {}); 201 | $scope.onUpdateObj = spy; 202 | 203 | const {element, $ctrl} = _createInstance(''); 204 | 205 | // We *must* use $q else the promises don't resolve properly :/ 206 | // Pretend it parses 207 | $ctrl.parse = () => $q.resolve({foo: 'bar'}); 208 | // And that it validates 209 | $ctrl.validate = () => $q.resolve({}); 210 | $rootScope.$digest(); 211 | 212 | // Change the injected document 213 | $scope.upperDoc = 'flibble!' 214 | $rootScope.$digest(); 215 | 216 | expect(spy).to.have.been.called.once; 217 | expect(spy).to.have.been.always.with.exactly({foo: 'bar'}); 218 | }); 219 | 220 | it('calls on-update-doc with a value when the injected document changes', function() { 221 | const spy = chai.spy(function(params) {}); 222 | $scope.onUpdateDoc = spy; 223 | 224 | const {element, $ctrl} = _createInstance(''); 225 | $rootScope.$digest(); 226 | 227 | expect($ctrl.onUpdateDoc).to.be.a.function; 228 | 229 | // Change the injected document 230 | $scope.upperDoc = 'flibble!' 231 | $rootScope.$digest(); 232 | 233 | expect(spy).to.have.been.called.once; 234 | expect(spy).to.have.been.always.with.exactly('flibble!'); 235 | }); 236 | 237 | }); 238 | -------------------------------------------------------------------------------- /webpack.common.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | var WebpackMd5Hash = require('webpack-md5-hash'); 7 | var path = require('path'); 8 | 9 | module.exports = { 10 | cache: true, 11 | entry: { 12 | app: './src/app.js', 13 | vendor: './src/vendor.js', 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, 'dist'), 17 | publicPath: '', 18 | filename: '[name].[chunkhash].js', 19 | chunkFilename: '[id].[chunkhash].js' 20 | }, 21 | stats: { 22 | // Configure the console output 23 | colors: true, 24 | modules: true, 25 | reasons: true 26 | }, 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.js?$/, 31 | loader: 'babel-loader', 32 | exclude: /node_modules/, 33 | query: { 34 | presets: [ 35 | ['env', { 36 | 'targets': { 37 | 'browsers': ['last 2 versions'] 38 | } 39 | }] 40 | ] 41 | } 42 | }, 43 | { test: /\.css$/, loader: ExtractTextPlugin.extract({fallback: 'style-loader', use: 'css-loader' })}, 44 | { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'file-loader?name=fonts/[name].[ext]' }, 45 | ] 46 | }, 47 | plugins: [ 48 | new WebpackMd5Hash(), 49 | new webpack.optimize.CommonsChunkPlugin({ 50 | name: ['app', 'vendor'] 51 | }), 52 | new CopyWebpackPlugin([ 53 | { from: 'www' } 54 | ]), 55 | new HtmlWebpackPlugin({ 56 | template: 'html-loader!./www/index.html', 57 | }), 58 | new ExtractTextPlugin('[name].[chunkhash].css'), 59 | new webpack.ProvidePlugin({ 60 | 'Promise': 'es6-promise' // Thanks Aaron (https://gist.github.com/Couto/b29676dd1ab8714a818f#gistcomment-1584602) 61 | }), 62 | new ngAnnotatePlugin({add: true}), 63 | new webpack.optimize.UglifyJsPlugin({ 64 | compress: { 65 | warnings: false 66 | }, 67 | sourceMap: true 68 | }) 69 | ], 70 | node: { 71 | fs: 'empty' 72 | }, 73 | profile: true 74 | }; 75 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | var webpackMerge = require('webpack-merge'); 2 | var commonConfig = require('./webpack.common.config.js'); 3 | 4 | module.exports = webpackMerge(commonConfig, { 5 | devtool: 'cheap-module-eval-source-map', 6 | devServer: { 7 | historyApiFallback: true, 8 | stats: 'minimal', 9 | contentBase: __dirname + "/www/", 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackMerge = require('webpack-merge'); 3 | var commonConfig = require('./webpack.common.config.js'); 4 | 5 | module.exports = webpackMerge(commonConfig, { 6 | plugins: [ 7 | new webpack.optimize.UglifyJsPlugin({ 8 | compress: { 9 | warnings: false 10 | }, 11 | sourceMap: true 12 | }) 13 | ] 14 | }); 15 | -------------------------------------------------------------------------------- /www/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Order Allow,Deny 3 | Deny from all 4 | 5 | 6 | ExpiresActive On 7 | ExpiresDefault "access plus 1 week" 8 | -------------------------------------------------------------------------------- /www/draft3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 | Please update your bookmarks - this is not a valid URL. 11 |

12 |

13 | Redirecting you in 10 seconds. 14 |

15 | 16 | 17 | -------------------------------------------------------------------------------- /www/draft4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 | Please update your bookmarks - this is not a valid URL. 11 |

12 |

13 | Redirecting you in 10 seconds. 14 |

15 | 16 | 17 | -------------------------------------------------------------------------------- /www/draft5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 | Please update your bookmarks - this is not a valid URL. 11 |

12 |

13 | Redirecting you in 10 seconds. 14 |

15 | 16 | 17 | -------------------------------------------------------------------------------- /www/ga.js: -------------------------------------------------------------------------------- 1 | window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date; 2 | ga('create', 'UA-30596645-1', 'auto'); 3 | ga('set', 'anonymizeIp', true); // Anonymise IP 4 | ga('set', 'allowAdFeatures', false); // Disable ad features regardless of dashboard settings 5 | ga('set', 'forceSSL', true); 6 | // ga('send', 'pageview'); // We do this on hashchange -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSON Schema Lint :: JSON Schema Validator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /www/samples/draft4/invalid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "this is a string, not a number", 3 | "bar": "this is a string that isn't allowed" 4 | } 5 | -------------------------------------------------------------------------------- /www/samples/draft4/invalid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Any validation failures are shown in the right-hand Messages pane.", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "number" 7 | }, 8 | "bar": { 9 | "type": "string", 10 | "enum": [ 11 | "a", 12 | "b", 13 | "c" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /www/samples/draft4/valid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": 12345, 3 | "bar": "a" 4 | } 5 | -------------------------------------------------------------------------------- /www/samples/draft4/valid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Any validation failures are shown in the right-hand Messages pane.", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "number" 7 | }, 8 | "bar": { 9 | "type": "string", 10 | "enum": [ 11 | "a", 12 | "b", 13 | "c" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /www/samples/draft6/invalid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "Not a number", 3 | "bar": "Doesn't equal constant", 4 | "baz": { 5 | "staticProperty": ["This array needs at least one number"], 6 | "property1": "The propertyNames keyword is an alternative to patternProperties", 7 | "pr()perty2": "All property names must match supplied conditions (in this case, it's a regex)" 8 | } 9 | } -------------------------------------------------------------------------------- /www/samples/draft6/invalid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": { 5 | "type": "number" 6 | }, 7 | "bar": { 8 | "const": "Must equal this value" 9 | }, 10 | "baz": { 11 | "type": "object", 12 | "properties": { 13 | "staticProperty": { 14 | "type": "array", 15 | "contains": { 16 | "type": "number" 17 | } 18 | } 19 | }, 20 | "propertyNames": { 21 | "pattern": "^([0-9a-zA-Z]*)$" 22 | }, 23 | "additionalProperties": { 24 | "type": "string" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /www/samples/draft6/valid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": 32, 3 | "bar": "Must equal this value", 4 | "baz": { 5 | "staticProperty": ["This needs at least one number", 32], 6 | "property1": "The propertyNames keyword is an alternative to patternProperties", 7 | "property2": "All property names must match supplied conditions (in this case, it's a regex)" 8 | } 9 | } -------------------------------------------------------------------------------- /www/samples/draft6/valid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": { 5 | "type": "number" 6 | }, 7 | "bar": { 8 | "const": "Must equal this value" 9 | }, 10 | "baz": { 11 | "type": "object", 12 | "properties": { 13 | "staticProperty": { 14 | "type": "array", 15 | "contains": { 16 | "type": "number" 17 | } 18 | } 19 | }, 20 | "propertyNames": { 21 | "pattern": "^([0-9a-zA-Z]*)$" 22 | }, 23 | "additionalProperties": { 24 | "type": "string" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /www/samples/experimental/invalid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "Should be equal to bar", 3 | "bar": "These should be identical", 4 | "baz": { 5 | "staticProperty": ["This needs at least one number"], 6 | "prop erty1": "The propertyNames keyword is an alternative to patternProperties", 7 | "pr()perty2": "All property names must match supplied conditions (in this case, it's a regex)" 8 | }, 9 | "rangeExample": 6, 10 | "ifExample": { 11 | "foo": true, 12 | "bar": "Where is baz?" 13 | } 14 | } -------------------------------------------------------------------------------- /www/samples/experimental/invalid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": { 5 | "const": { 6 | "$data": "/bar" 7 | } 8 | }, 9 | "bar": { 10 | "type": "string" 11 | }, 12 | "baz": { 13 | "type": "object", 14 | "properties": { 15 | "staticProperty": { 16 | "type": "array", 17 | "contains": { 18 | "type": "number" 19 | } 20 | } 21 | }, 22 | "propertyNames": { 23 | "pattern": "^([0-9a-zA-Z]*)$" 24 | }, 25 | "additionalProperties": { 26 | "type": "string" 27 | } 28 | }, 29 | "rangeExample": { 30 | "type": "number", 31 | "range": [0,5] 32 | }, 33 | "ifExample": { 34 | "type": "object", 35 | "if": { 36 | "enum": [true] 37 | }, 38 | "then": { 39 | "required": ["bar"] 40 | }, 41 | "else": { 42 | "required": ["baz"] 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /www/samples/experimental/valid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "These should be identical", 3 | "bar": "These should be identical", 4 | "baz": { 5 | "staticProperty": ["This needs at least one number", 32], 6 | "property1": "The propertyNames keyword is an alternative to patternProperties", 7 | "property2": "All property names must match supplied conditions (in this case, it's a regex)" 8 | }, 9 | "rangeExample": 3, 10 | "ifExample": { 11 | "foo": true, 12 | "baz": 32 13 | } 14 | } -------------------------------------------------------------------------------- /www/samples/experimental/valid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": { 5 | "const": { 6 | "$data": "/bar" 7 | } 8 | }, 9 | "bar": { 10 | "type": "string" 11 | }, 12 | "baz": { 13 | "type": "object", 14 | "properties": { 15 | "staticProperty": { 16 | "type": "array", 17 | "contains": { 18 | "type": "number" 19 | } 20 | } 21 | }, 22 | "propertyNames": { 23 | "pattern": "^([0-9a-zA-Z]*)$" 24 | }, 25 | "additionalProperties": { 26 | "type": "string" 27 | } 28 | }, 29 | "rangeExample": { 30 | "type": "number", 31 | "range": [0,5] 32 | }, 33 | "ifExample": { 34 | "type": "object", 35 | "if": { 36 | "enum": [true] 37 | }, 38 | "then": { 39 | "required": ["bar"] 40 | }, 41 | "else": { 42 | "required": ["baz"] 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /www/samples/v5-unofficial/invalid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": 12345, 3 | "moreThanFoo": 12344, 4 | "bar": "I love the constant keyword!", 5 | "sameAsBar": "I hate the constant keyword.", 6 | "baz": { 7 | "foo": 1234, 8 | "foobaz": "This is one of many ways to incorrectly format data with this switch schema." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /www/samples/v5-unofficial/invalid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Any validation failures are shown in the right-hand Messages pane.", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "number" 7 | }, 8 | "moreThanFoo": { 9 | "type": "number", 10 | "minimum": { 11 | "$data": "1/foo" 12 | }, 13 | "exclusiveMinimum": true 14 | }, 15 | "bar": { 16 | "type":"string" 17 | }, 18 | "sameAsBar": { 19 | "constant": { 20 | "$data": "1/bar" 21 | } 22 | }, 23 | "baz": { 24 | "type": "object", 25 | "switch": [ 26 | { 27 | "if": { 28 | "properties": { 29 | "foo": { 30 | "constant": { 31 | "$data": "2/foo" 32 | } 33 | } 34 | }, 35 | "required": ["foo"] 36 | }, 37 | "then": { 38 | "properties": { 39 | "foobaz": { 40 | "type": "string" 41 | } 42 | }, 43 | "required": ["foobaz"] 44 | } 45 | }, 46 | { 47 | "if": { 48 | "properties": { 49 | "bar": { 50 | "constant": { 51 | "$data": "2/bar" 52 | } 53 | } 54 | }, 55 | "required": ["bar"] 56 | }, 57 | "then": { 58 | "properties": { 59 | "barbaz": { 60 | "type": "number" 61 | } 62 | }, 63 | "required": ["barbaz"] 64 | } 65 | }, 66 | { 67 | "then": false 68 | } 69 | ] 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /www/samples/v5-unofficial/valid.document.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": 12345, 3 | "moreThanFoo": 12346, 4 | "bar": "I love the constant keyword!", 5 | "sameAsBar": "I love the constant keyword!", 6 | "baz": { 7 | "foo": 12345, 8 | "foobaz": "Switch statements can get really complicated." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /www/samples/v5-unofficial/valid.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Any validation failures are shown in the right-hand Messages pane.", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "number" 7 | }, 8 | "moreThanFoo": { 9 | "type": "number", 10 | "minimum": { 11 | "$data": "1/foo" 12 | }, 13 | "exclusiveMinimum": true 14 | }, 15 | "bar": { 16 | "type":"string" 17 | }, 18 | "sameAsBar": { 19 | "constant": { 20 | "$data": "1/bar" 21 | } 22 | }, 23 | "baz": { 24 | "type": "object", 25 | "switch": [ 26 | { 27 | "if": { 28 | "properties": { 29 | "foo": { 30 | "constant": { 31 | "$data": "2/foo" 32 | } 33 | } 34 | }, 35 | "required": ["foo"] 36 | }, 37 | "then": { 38 | "properties": { 39 | "foobaz": { 40 | "type": "string" 41 | } 42 | }, 43 | "required": ["foobaz"] 44 | } 45 | }, 46 | { 47 | "if": { 48 | "properties": { 49 | "bar": { 50 | "constant": { 51 | "$data": "2/bar" 52 | } 53 | } 54 | }, 55 | "required": ["bar"] 56 | }, 57 | "then": { 58 | "properties": { 59 | "barbaz": { 60 | "type": "number" 61 | } 62 | }, 63 | "required": ["barbaz"] 64 | } 65 | }, 66 | { 67 | "then": false 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /www/translations/locale-en.json: -------------------------------------------------------------------------------- 1 | { 2 | "FORMAT": "Format", 3 | "SAMPLES": "Samples", 4 | "RESET": "Reset", 5 | "SAVE_AS_GIST": "Save as Gist", 6 | "MARKUP_LANGUAGE": "Markup language", 7 | "OFFICIAL_SPEC_VERSION": "Official spec version", 8 | "UNOFFICIAL_SPEC_VERSION": "Unofficial spec version", 9 | "SCHEMA": "Schema", 10 | "DOCUMENT": "Document", 11 | "MESSAGE": "Message", 12 | "FIELD": "Field", 13 | "ERROR": "Error", 14 | "VALUE": "Value", 15 | "TOGGLE_NAVIGATION": "Toggle navigation", 16 | "ABOUT": "About", 17 | 18 | "OK": "OK", 19 | 20 | "GIST_SAVED": "Saved as Gist", 21 | "GIST_VISIT": "Visit saved schema/document pair", 22 | 23 | "sample_titles": { 24 | "draft-04-valid": "Sample draft-04 schema and valid document", 25 | "draft-04-invalid": "Sample draft-04 schema and invalid document", 26 | "draft-06-valid": "Sample draft-06 schema and valid document", 27 | "draft-06-invalid": "Sample draft-06 schema and invalid document", 28 | "v5-unofficial-valid": "Sample v5-unofficial schema and valid document", 29 | "v5-unofficial-invalid": "Sample v5-unofficial schema and invalid document", 30 | "experimental-valid": "Sample experimental schema and valid document", 31 | "experimental-invalid": "Sample experimental schema and invalid document" 32 | }, 33 | 34 | "WARNING_V5_UNOFFICIAL": "RETIRED schema version. Please use the latest official version instead.

This schema version, v5-unofficial, was a prototype for a draft version that was eventually scrapped. Previously, JSON Schema Lint offered this prototype as \"draft-05\", as it was originally meant to supersede draft-04.
This was not, and will never be, an official JSON Schema version.", 35 | "WARNING_EXPERIMENTAL": "RETIRED schema version. Please use the latest official version instead.

This was an experimental meta-schema used for testing new features.", 36 | 37 | "SCHEMA_VALID_MESSAGE": "Schema is valid according to {{name}}.", 38 | "DOCUMENT_VALID_MESSAGE": "Document validates against the schema, spec version {{name}}.", 39 | 40 | "ERROR_GIST_SAVING": "Error saving Gist", 41 | "ERROR_GIST_LOADING": "Error loading Gist", 42 | 43 | "ERROR_GIST_FORMAT": "Gist is not in JSON Schema Lint format", 44 | 45 | "ERROR_SAMPLE_LOADING": "Error loading sample", 46 | 47 | "ERROR_INVALID_JSON": "Document is invalid JSON. Try JSONLint to fix it.", 48 | "ERROR_INVALID_YAML": "Document is invalid YAML. Try YAML Validator to fix it.", 49 | 50 | "ERROR_INVALID_MARKUP": "Invalid markup language '{{markupLanguage}}'.", 51 | "ERROR_INVALID_VERSION": "Invalid schema version '{{specVersion}}'.", 52 | 53 | "ERROR_INVALID_SCHEMA": "Invalid schema.", 54 | 55 | "ERROR_INVALID_MARKUP_BUTTON": "???", 56 | "ERROR_INVALID_VERSION_BUTTON": "???", 57 | 58 | "ERROR_MODULE_LOADING_FAILED_TITLE": "Could not load module", 59 | "ERROR_MODULE_LOADING_FAILED_CONTENT": "We couldn't load a vital part of the application. This is probably due to network conditions. We recommend reloading the page once conditions improve." 60 | 61 | } 62 | --------------------------------------------------------------------------------