├── .gitignore ├── .jscsrc ├── .travis.yml ├── .versions ├── LICENSE ├── README.md ├── json-simple-schema-tests.js ├── json-simple-schema.js └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.meteor 2 | .build* -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "fileExtensions": [ ".js", "jscs" ], 4 | 5 | "requireParenthesesAroundIIFE": true, 6 | "maximumLineLength": 120, 7 | "validateLineBreaks": "LF", 8 | "validateIndentation": "\t", 9 | 10 | "disallowKeywords": ["with"], 11 | "disallowSpacesInsideObjectBrackets": null, 12 | "disallowImplicitTypeConversion": ["string"], 13 | 14 | "safeContextKeyword": "_this", 15 | 16 | "excludeFiles": [ 17 | ".meteor/**" 18 | ] 19 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | node_js: 4 | - "0.10" 5 | before_install: 6 | - "curl -L http://git.io/ejPSng | /bin/sh" -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | aldeed:autoform@5.3.2 2 | aldeed:simple-schema@1.3.2 3 | babel-compiler@5.8.24_1 4 | babel-runtime@0.1.4 5 | base64@1.0.4 6 | binary-heap@1.0.4 7 | blaze@2.1.3 8 | blaze-tools@1.0.4 9 | boilerplate-generator@1.0.4 10 | bshamblen:json-simple-schema@0.1.3 11 | caching-compiler@1.0.0 12 | caching-html-compiler@1.0.2 13 | callback-hook@1.0.4 14 | check@1.1.0 15 | ddp@1.2.2 16 | ddp-client@1.2.1 17 | ddp-common@1.2.2 18 | ddp-server@1.2.2 19 | deps@1.0.9 20 | diff-sequence@1.0.1 21 | ecmascript@0.1.6 22 | ecmascript-runtime@0.2.6 23 | ejson@1.0.7 24 | geojson-utils@1.0.4 25 | html-tools@1.0.5 26 | htmljs@1.0.5 27 | id-map@1.0.4 28 | jquery@1.11.4 29 | livedata@1.0.15 30 | local-test:bshamblen:json-simple-schema@0.1.3 31 | logging@1.0.8 32 | meteor@1.1.10 33 | minifiers@1.1.7 34 | minimongo@1.0.10 35 | momentjs:moment@2.8.4 36 | mongo@1.1.3 37 | mongo-id@1.0.1 38 | npm-mongo@1.4.39_1 39 | observe-sequence@1.0.7 40 | ordered-dict@1.0.4 41 | promise@0.5.1 42 | random@1.0.5 43 | reactive-dict@1.1.3 44 | reactive-var@1.0.6 45 | retry@1.0.4 46 | routepolicy@1.0.6 47 | spacebars@1.0.7 48 | spacebars-compiler@1.0.7 49 | templating@1.1.5 50 | templating-tools@1.0.0 51 | tinytest@1.0.6 52 | tracker@1.0.9 53 | ui@1.0.8 54 | underscore@1.0.4 55 | webapp@1.2.3 56 | webapp-hashing@1.0.5 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Brian Shamblen 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/bshamblen/meteor-json-simple-schema.svg)](https://travis-ci.org/bshamblen/meteor-json-simple-schema) 2 | 3 | # JSON Schema to SimpleSchema Converter 4 | 5 | Converts a JSON schema document to a SimpleSchema object, for use with Collection2 and AutoForm. 6 | 7 | ## Install 8 | ```cli 9 | meteor add bshamblen:json-simple-schema 10 | ``` 11 | 12 | ## Use 13 | Simply load the contents of your JSON schema document from your local file system, or from a URL, and pass the parsed JSON object to the JSONSchema constructor: 14 | 15 | #### toSimpleSchema 16 | 17 | Generates an instance of `SimpleSchema`, ready to attach to your Meteor collection. 18 | 19 | ```javascript 20 | var jsonSchemaDoc = JSON.parse($.ajax({ 21 | type: 'GET', 22 | url: 'http://example.com/path-to-json-schema-file', 23 | async: false 24 | }).responseText); 25 | 26 | var jsonSchema = new JSONSchema(jsonSchemaDoc); 27 | var simpleSchema = jsonSchema.toSimpleSchema(); 28 | 29 | YourModel = new Mongo.Collection('somecollection'); 30 | YourModel.attachSchema(simpleSchema); 31 | ``` 32 | 33 | #### toSimpleSchemaProps 34 | 35 | Generates just the schema properties object, which can be modified prior to manually creating an instance of `SimpleSchema`. 36 | 37 | ```javascript 38 | var jsonSchemaDoc = JSON.parse($.ajax({ 39 | type: 'GET', 40 | url: 'http://example.com/path-to-json-schema-file', 41 | async: false 42 | }).responseText); 43 | 44 | var jsonSchema = new JSONSchema(jsonSchemaDoc); 45 | var props = jsonSchema.toSimpleSchemaProps(); 46 | 47 | props.extraProperty = { 48 | type: String, 49 | optional: true 50 | } 51 | 52 | var simpleSchema = new SimpleSchema(props); 53 | 54 | YourModel = new Mongo.Collection('somecollection'); 55 | YourModel.attachSchema(simpleSchema); 56 | ``` 57 | 58 | ## Disclaimer 59 | This is the first iteration of this project, with minimal functionality. It currently supports base data types (including arrays), inline sub-objects and many of the validation options: 60 | * title 61 | * minimum 62 | * maximum 63 | * exclusiveMinimum 64 | * exclusiveMaximum 65 | * minLength 66 | * maxLength 67 | * enum 68 | * minItems 69 | * maxItems 70 | * default 71 | * pattern 72 | * required 73 | * Now supports internal `$ref` (thanks [@bpatridge](https://github.com/bpartridge)) 74 | 75 | ## TODO 76 | * Add support for external `$ref` schemas, from a URI. 77 | 78 | ## Contributing 79 | Please feel free to contribute by sumbitting a pull request. 80 | -------------------------------------------------------------------------------- /json-simple-schema-tests.js: -------------------------------------------------------------------------------- 1 | var packageJsonSchema = { 2 | '$schema': 'http://json-schema.org/draft-04/schema#', 3 | 'title': 'Product', 4 | 'description': 'A product from Acme\'s catalog', 5 | 'type': 'object', 6 | 'properties': { 7 | 'id': { 8 | 'description': 'The unique identifier for a product', 9 | 'type': 'integer' 10 | }, 11 | 'name': { 12 | 'description': 'Name of the product', 13 | 'type': 'string' 14 | }, 15 | 'price': { 16 | 'type': 'number', 17 | 'minimum': '0', //test parsing of string values for number 18 | 'exclusiveMinimum': 1 //test parsing string values for boolean 19 | }, 20 | 'tags': { 21 | 'type': 'array', 22 | 'items': { 23 | 'type': 'string' 24 | }, 25 | 'minItems': 1, 26 | 'uniqueItems': true 27 | }, 28 | 'arrayOfObjects': { 29 | 'type': 'array', 30 | 'items': { 31 | 'type': 'object', 32 | 'properties': { 33 | 'foo': {'type': 'string'} 34 | } 35 | } 36 | }, 37 | 'objectWithAdditionalProps': { 38 | 'type': 'object', 39 | 'properties': { 40 | 'blah': { 41 | 'type': 'string' 42 | } 43 | }, 44 | 'additionalProperties': true 45 | }, 46 | 'arrayWithAdditionalProperties': { 47 | 'type': 'array', 48 | 'items': { 49 | 'type': 'object', 50 | 'additionalProperties': true, 51 | 'properties': { 52 | 'test': {'type': 'string'} 53 | } 54 | } 55 | }, 56 | 'color': { 57 | 'type': 'string', 58 | 'enum': ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', null] 59 | }, 60 | 'emailAddress': { 61 | 'type': 'string', 62 | 'format': 'email' 63 | } 64 | }, 65 | 'required': ['id', 'name', 'price'], 66 | 'additionalProperties': true //ignore additionalProperties option at root level since SimpleSchema doesn't support blackbox at root level. 67 | }; 68 | 69 | Tinytest.add('JSONSchema - convert a basic JSON schema object to a SimpleSchema object', function(test) { 70 | var jsonSchema = new JSONSchema(packageJsonSchema); 71 | var simpleSchema = jsonSchema.toSimpleSchema(); 72 | 73 | //Make sure .toSimpleSchema returned a SimpleSchema object 74 | test.instanceOf(simpleSchema, SimpleSchema); 75 | 76 | var rawSchema = simpleSchema._schema; 77 | 78 | test.equal(rawSchema.id.type, Number); 79 | test.equal(rawSchema.id.optional, false); 80 | 81 | test.equal(rawSchema.name.type, String); 82 | test.equal(rawSchema.name.optional, false); 83 | 84 | test.equal(rawSchema.price.type, Number); 85 | test.equal(rawSchema.price.min, 0); 86 | test.equal(rawSchema.price.optional, false); 87 | test.equal(rawSchema.price.exclusiveMin, true); 88 | test.equal(rawSchema.price.decimal, true); 89 | 90 | test.equal(rawSchema.tags.type, Array); 91 | test.equal(rawSchema.tags.minCount, 1); 92 | test.equal(rawSchema['tags.$'].type, String); 93 | 94 | test.equal(rawSchema['arrayOfObjects.$.foo'].type, String); 95 | 96 | test.equal(rawSchema['arrayWithAdditionalProperties.$'].type, Object); 97 | test.equal(rawSchema['arrayWithAdditionalProperties.$'].blackbox, true); 98 | 99 | test.equal(rawSchema.objectWithAdditionalProps.blackbox, true); 100 | 101 | test.equal(rawSchema.color.type, String); 102 | test.equal(rawSchema.color.allowedValues.length, 7); 103 | 104 | test.equal(rawSchema.emailAddress.type, String); 105 | test.equal(rawSchema.emailAddress.regEx, SimpleSchema.RegEx.Email); 106 | }); 107 | 108 | Tinytest.add('JSONSchema - uses toSimpleSchemaProps to manually add a property before creating SimpleSchema instance', function(test) { 109 | var jsonSchema = new JSONSchema(packageJsonSchema); 110 | var props = jsonSchema.toSimpleSchemaProps(); 111 | 112 | props.extraProperty = { 113 | type: String, 114 | optional: true 115 | } 116 | 117 | var simpleSchema = new SimpleSchema(props); 118 | 119 | //Make sure .toSimpleSchema returned a SimpleSchema object 120 | test.instanceOf(simpleSchema, SimpleSchema); 121 | 122 | var rawSchema = simpleSchema._schema; 123 | 124 | test.equal(rawSchema.id.type, Number); 125 | test.equal(rawSchema.id.optional, false); 126 | 127 | test.equal(rawSchema.name.type, String); 128 | test.equal(rawSchema.name.optional, false); 129 | 130 | test.equal(rawSchema.price.type, Number); 131 | test.equal(rawSchema.price.min, 0); 132 | test.equal(rawSchema.price.optional, false); 133 | test.equal(rawSchema.price.exclusiveMin, true); 134 | test.equal(rawSchema.price.decimal, true); 135 | 136 | test.equal(rawSchema.tags.type, Array); 137 | test.equal(rawSchema.tags.minCount, 1); 138 | test.equal(rawSchema['tags.$'].type, String); 139 | 140 | test.equal(rawSchema['arrayOfObjects.$.foo'].type, String); 141 | 142 | test.equal(rawSchema['arrayWithAdditionalProperties.$'].type, Object); 143 | test.equal(rawSchema['arrayWithAdditionalProperties.$'].blackbox, true); 144 | 145 | test.equal(rawSchema.objectWithAdditionalProps.blackbox, true); 146 | 147 | test.equal(rawSchema.color.type, String); 148 | test.equal(rawSchema.color.allowedValues.length, 7); 149 | 150 | test.equal(rawSchema.emailAddress.type, String); 151 | test.equal(rawSchema.emailAddress.regEx, SimpleSchema.RegEx.Email); 152 | 153 | test.equal(rawSchema.extraProperty.type, String); 154 | test.equal(rawSchema.extraProperty.optional, true); 155 | }); 156 | 157 | // https://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-04 158 | var packageJsonSchemaWithInternalRef = { 159 | '$schema': 'http://json-schema.org/draft-04/schema#', 160 | 'title': 'Thing', 161 | 'type': 'object', 162 | 'properties': { 163 | 'prop': {'$ref': '#/definitions/definitionWithSpecialChars~0~1%25'}, 164 | 'prop2': {'$ref': '#/definitions/arrayOfDefs/1'}, 165 | 'prop3': {'$ref': '#'}, 166 | 'refItems': { 167 | 'type': 'array', 168 | 'items': {'$ref': '#/definitions/arrayOfDefs/1'} 169 | } 170 | }, 171 | 'definitions': { 172 | 'definitionWithSpecialChars~/%': { 173 | 'type': 'string', 174 | 'format': 'email' 175 | }, 176 | 'arrayOfDefs': [ 177 | {'type': 'string'}, 178 | {'type': 'number'} 179 | ] 180 | } 181 | } 182 | 183 | Tinytest.add('JSONSchema - convert a JSON schema object with internal references to a SimpleSchema object', function(test) { 184 | var jsonSchema = new JSONSchema(packageJsonSchemaWithInternalRef); 185 | var simpleSchema = jsonSchema.toSimpleSchema(); 186 | 187 | //Make sure .toSimpleSchema returned a SimpleSchema object 188 | test.instanceOf(simpleSchema, SimpleSchema); 189 | 190 | var rawSchema = simpleSchema._schema; 191 | 192 | test.equal(rawSchema.prop.type, String); 193 | test.equal(rawSchema.prop.regEx, SimpleSchema.RegEx.Email); 194 | test.equal(rawSchema.prop2.type, Number); 195 | test.equal(rawSchema.prop3.type, Object); 196 | 197 | test.equal(rawSchema.refItems.type, Array); 198 | test.equal(rawSchema['refItems.$'].type, Number); 199 | }); 200 | -------------------------------------------------------------------------------- /json-simple-schema.js: -------------------------------------------------------------------------------- 1 | JSONSchema = function(schema, options) { 2 | var root = this; 3 | this.rawSchema = schema; 4 | 5 | if (typeof schema !== 'object') { 6 | throw new Error('schema parameter must be an object'); 7 | } 8 | 9 | var jsonSchema = schema; 10 | 11 | this.toSimpleSchemaProps = function toSimpleSchemaProps() { 12 | var props = jsonSchema.properties || jsonSchema; 13 | return translateProperties(props, getRequiredFromProperty(jsonSchema)); 14 | } 15 | 16 | this.toSimpleSchema = function toSimpleSchema() { 17 | var simpleSchemaProps = this.toSimpleSchemaProps(); 18 | return new SimpleSchema(simpleSchemaProps); 19 | } 20 | 21 | function translateProperties(properties, required) { 22 | var schema = {}; 23 | 24 | _.each(properties, function(prop, key) { 25 | prop = resolveReference(prop); 26 | var blackbox = getBlackboxFromProperty(prop); 27 | var ssProp = {}; 28 | addRules(ssProp, prop, required.indexOf(key) !== -1, blackbox); 29 | 30 | if (Meteor.isClient) { 31 | addAutoformAttributes(ssProp, prop); 32 | } 33 | 34 | schema[key] = ssProp; 35 | var subProps = getSubPropertiesFromProperty(prop); 36 | 37 | if (subProps && !blackbox) { 38 | var subSchema = translateProperties(subProps, getRequiredFromProperty(prop)); 39 | 40 | _.each(subSchema, function(subProp, subKey) { 41 | schema[key + '.' + (prop.type === 'array' ? '$.' : '') + subKey] = subProp; 42 | }); 43 | } 44 | }); 45 | 46 | return schema; 47 | } 48 | 49 | function getTypeFromProperty(prop) { 50 | var propType = prop.type === 'array' ? prop.items.type : prop.type; 51 | var format = prop.format; 52 | var ssType = String; 53 | 54 | switch (propType) { 55 | case 'integer': 56 | case 'number': 57 | ssType = Number; 58 | break; 59 | case 'boolean': 60 | ssType = Boolean; 61 | break; 62 | case 'object': 63 | ssType = Object; 64 | break; 65 | default: 66 | if (format === 'date' || format === 'date-time') { 67 | ssType = Date; 68 | } else { 69 | ssType = String; 70 | } 71 | break; 72 | } 73 | 74 | return (prop.type === 'array' ? [ssType] : ssType); 75 | } 76 | 77 | function getSubPropertiesFromProperty(prop) { 78 | if (prop.type === 'object' && prop.properties) { 79 | return prop.properties; 80 | } else if (prop.type === 'array' && prop.items && prop.items.type === 'object' && prop.items.properties) { 81 | return prop.items.properties; 82 | } 83 | 84 | return null; 85 | } 86 | 87 | function getRequiredFromProperty(prop) { 88 | if (prop.type === 'object' && prop.properties) { 89 | return prop.required || []; 90 | } else if (prop.type === 'array' && prop.items && prop.items.type === 'object' && prop.items.properties) { 91 | return prop.items.required || []; 92 | } 93 | 94 | return []; 95 | } 96 | 97 | function getBlackboxFromProperty(prop) { 98 | if (prop.type === 'object' && prop.additionalProperties) { 99 | return true; 100 | } else if (prop.type === 'array' && prop.items && prop.items.type === 'object' && prop.items.additionalProperties) { 101 | return true; 102 | } 103 | 104 | return false; 105 | } 106 | 107 | var translationMap = { 108 | title: {key: 'label'}, 109 | minimum: {key: 'min', type: Number}, 110 | maximum: {key: 'max', type: Number}, 111 | exclusiveMinimum: {key: 'exclusiveMin', type: Boolean}, 112 | exclusiveMaximum: {key: 'exclusiveMax', type: Boolean}, 113 | minLength: {key: 'min', type: Number}, 114 | maxLength: {key: 'max', type: Number}, 115 | minItems: {key: 'minCount', type: Number}, 116 | maxItems: {key: 'maxCount', type: Number}, 117 | 'default': {key: 'defaultValue'} 118 | } 119 | 120 | function addRules(target, source, isRequired, isBlackbox) { 121 | target.type = getTypeFromProperty(source); 122 | 123 | _.each(translationMap, function(sKey, jKey) { 124 | if (typeof source[jKey] !== 'undefined') { 125 | target[sKey.key] = sKey.type ? sKey.type(source[jKey]) : source[jKey]; 126 | } 127 | }); 128 | 129 | target.optional = !isRequired; 130 | 131 | if (isBlackbox) { 132 | target.blackbox = isBlackbox; 133 | } 134 | 135 | if (source.enum) { 136 | target.allowedValues = source.enum.filter(function(item) {return item !== null;}); 137 | } 138 | 139 | if (source.pattern) { 140 | target.regEx = new RegExp(source.pattern); 141 | } 142 | 143 | if (!source.pattern && source.format === 'email') { 144 | target.regEx = SimpleSchema.RegEx.Email; 145 | } else if (!source.pattern && (source.format === 'host-name' || source.format === 'hostname')) { 146 | target.regEx = SimpleSchema.RegEx.Domain; 147 | } else if (!source.pattern && source.format === 'ipv4') { 148 | target.rexEx = SimpleSchema.RegEx.IPv4; 149 | } else if (!source.pattern && source.format === 'ipv6') { 150 | target.rexEx = SimpleSchema.RegEx.IPv6; 151 | } else if (source.type === 'number' || (source.type === 'array' && source.items && source.items.type === 'number')) { 152 | target.decimal = true; 153 | } 154 | } 155 | 156 | function attachAutoformObject(target) { 157 | if (!target.autoform) { 158 | target.autoform = {} 159 | } 160 | 161 | if (!target.autoform.afFieldInput) { 162 | target.autoform.afFieldInput = {}; 163 | } 164 | } 165 | 166 | function addAutoformAttributes(target, source) { 167 | if (source.description) { 168 | attachAutoformObject(target); 169 | target.autoform.afFieldInput.title = source.description; 170 | } 171 | 172 | if (source.format === 'date-time') { 173 | attachAutoformObject(target); 174 | target.autoform.afFieldInput.type = 'datetime'; 175 | } 176 | 177 | if (target.allowedValues && target.optional) { 178 | attachAutoformObject(target); 179 | target.autoform.afFieldInput.firstOption = '(None)'; 180 | } 181 | } 182 | 183 | // https://tools.ietf.org/id/draft-pbryan-zyp-json-ref-03.html 184 | // https://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-04 185 | function resolveReference(prop) { 186 | var $ref; 187 | if ($ref = prop.$ref) { 188 | if ($ref === '#') { 189 | // Prevent infinite recursion. 190 | return {type: jsonSchema.type || 'object'}; 191 | } else if ($ref.substring(0, 2) === '#/') { 192 | var refParts = decodeURIComponent($ref).substring(2).split('/'); 193 | var out = _.reduce(refParts, function(memo, refPart) { 194 | if (_.isArray(memo)) { 195 | return memo[parseInt(refPart)]; 196 | } else { 197 | refPart = refPart.replace('~1', '/').replace('~0', '~'); 198 | return memo[refPart]; 199 | } 200 | }, jsonSchema); 201 | 202 | return resolveReference(out); 203 | } else { 204 | throw new Error('Non-internal or relative JSON references not yet implemented'); 205 | } 206 | } else { 207 | if (prop.items && prop.items.$ref) { 208 | return _.defaults({items: resolveReference(prop.items)}, prop); 209 | } 210 | 211 | return prop; 212 | } 213 | } 214 | }; 215 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'bshamblen:json-simple-schema', 3 | version: '0.1.3', 4 | summary: 'Converts a JSON Schema to a SimpleSchema, for use with Collection2 and AutoForm.', 5 | git: 'https://github.com/bshamblen/meteor-json-simple-schema.git', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom(['METEOR@0.9.3', 'METEOR@0.9.4', 'METEOR@1.0']); 11 | api.use(['aldeed:simple-schema@1.3.2', 'aldeed:autoform@5.3.1']); 12 | api.use('underscore'); 13 | api.addFiles('json-simple-schema.js'); 14 | api.export(['JSONSchema'], ['client', 'server']); 15 | }); 16 | 17 | Package.onTest(function(api) { 18 | api.use('bshamblen:json-simple-schema', ['client', 'server']); 19 | api.use(['tinytest', 'underscore', 'aldeed:simple-schema@1.3.2']); 20 | api.addFiles('json-simple-schema-tests.js'); 21 | }); 22 | --------------------------------------------------------------------------------