diff --git a/.babelrc b/.babelrc index c13c5f6..0711f59 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,39 @@ { - "presets": ["es2015"] + "env": { + "cjs": { + "presets": [ + [ + "env", + { + "targets": { + "node": "current" + }, + "modules": "commonjs", + "useBuiltIns": true + } + ] + ], + "plugins": [ + "transform-es2015-modules-commonjs" + ] + }, + "es6": { + "presets": [ + [ + "env", + { + "targets": { + "node": "current" + }, + "modules": false, + "useBuiltIns": true + } + ] + ], + "plugins": [ + "lodash", + "./build/use-lodash-es" + ] + } + } } diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..9eafe3a --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,10 @@ +engines: + eslint: + enabled: true + +ratings: + paths: + - '**.js' + +exclude_paths: + - '/node_modules/*' diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1a900ed --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/dist/ +/*.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e1c41c9 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "extends": "airbnb", + "globals": { + "document": true, + "window": true + }, + "env": { + "browser": true + }, + "parserOptions": { + "ecmaVersion": 8, + "sourceType": "module", + "ecmaFeatures": { + "impliedStrict": true + } + }, + "rules": { + "jsx-a11y/img-has-alt": "off", + "react/jsx-space-before-closing": "off" + } +} diff --git a/.gitignore b/.gitignore index 649d37e..0662814 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,57 @@ -dist/es6/ -node_modules/ +# Node App Files # +############ +/node_modules/ +/dist/ +/*.js +/enums +/exceptions +/serializers +/types +/well-known + +# Deployment/IDE Tools # +############ +.buildpath +*.iml +.idea/ +.project +.settings + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..d922a2d --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +/.idea/ +/build/ +/dist/ +/src/ +/tests/ +.babelrc +.codeclimate.yml +.editorconfig +.eslint* +.nvmrc +.travis.yml diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a00f43e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v8 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..245900f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 8 diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index f399543..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,78 +0,0 @@ -/*! - * usage: - * - * developing: $ grunt - * testing: $ grunt test - * deploy: $ grunt deploy - */ -module.exports = function(grunt) { - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - uglify: { - options: { - banner: '/*! <%= pkg.name %> v<%= pkg.version %>, <%= grunt.template.today("yyyy-mm-dd HH:MM:ss Z") %> */\n', - mangle: false - }, - build: { - src: 'dist/pbj.min.js', - dest: 'dist/pbj.min.js' - } - }, - babel: { - dist: { - options: { - moduleIds: true, - getModuleId: function(moduleName) { - return 'gdbots/pbj/' + moduleName.substr(4); - }, - plugins: ['transform-es2015-modules-amd'] - }, - files: [{ - expand: true, - cwd: 'src', - src: ['**/*.js'], - dest: 'dist/es6' - }] - } - }, - concat: { - dist: { - src: [ - 'dist/es6/**/*.js' - ], - dest: 'dist/pbj.min.js' - } - }, - shell: { - 'cleanup': { - command: 'rm -rf ./dist/es6/' - } - }, - watch: { - scripts: { - files: ['Gruntfile.js', 'src/**/*.js', 'tests/**/*.js'], - tasks: ['babel', 'concat', 'shell:cleanup', 'mochaTest'] - } - }, - mochaTest: { - test: { - options: { - require: 'tests/bootstrap.js' - }, - src: ['tests/**/*.js'] - } - } - }); - - grunt.loadNpmTasks('grunt-babel'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-shell'); - grunt.loadNpmTasks('grunt-mocha-test'); - - grunt.registerTask('deploy', ['babel', 'concat', 'uglify', 'shell:cleanup']); - grunt.registerTask('default', ['watch']); - grunt.registerTask('test', ['mochaTest']); -}; diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8dada3e..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8d53e9f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,141 @@ +# Apache License +Version 2.0, January 2004 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +## 1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 +through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the +License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled +by, or are under common control with that entity. For the purposes of this definition, "control" means +(i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract +or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial +ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software +source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, +including but not limited to compiled object code, generated documentation, and conversions to other media +types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, +as indicated by a copyright notice that is included in or attached to the work (an example is provided in the +Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) +the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, +as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not +include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work +and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any +modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to +Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to +submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of +electronic, verbal, or written communication sent to the Licensor or its representatives, including but not +limited to communication on electronic mailing lists, source code control systems, and issue tracking systems +that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been +received by Licensor and subsequently incorporated within the Work. + +## 2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare +Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +## 3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such +license applies only to those patent claims licensable by such Contributor that are necessarily infringed by +their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such +Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim +or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent licenses granted to You under this +License for that Work shall terminate as of the date such litigation is filed. + +## 4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and + + 2. You must cause any modified files to carry prominent notices stating that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, + trademark, and attribution notices from the Source form of the Work, excluding those notices that do + not pertain to any part of the Derivative Works; and + + 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that + You distribute must include a readable copy of the attribution notices contained within such NOTICE + file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with the Derivative Works; or, within a display + generated by the Derivative Works, if and wherever such third-party notices normally appear. The + contents of the NOTICE file are for informational purposes only and do not modify the License. You may + add Your own attribution notices within Derivative Works that You distribute, alongside or as an + addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be + construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license +terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative +Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the +conditions stated in this License. + +## 5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by +You to the Licensor shall be under the terms and conditions of this License, without any additional terms or +conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate +license agreement you may have executed with Licensor regarding such Contributions. + +## 6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of +the Licensor, except as required for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +## 7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor +provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, +MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +## 8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless +required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any +Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential +damages of any character arising as a result of this License or out of the use or inability to use the Work +(including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has been advised of the possibility +of such damages. + +## 9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole +responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold +each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index c028cc9..5d9cff9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# pbj-js +pbj-js +============= +[![Build Status](https://api.travis-ci.org/gdbots/pbj-js.svg)](https://travis-ci.org/gdbots/pbj-js) Pbj library for es6. diff --git a/bower.json b/bower.json deleted file mode 100755 index 03f6fd2..0000000 --- a/bower.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "gdbots-pbj", - "version": "0.1.0", - "description": "Pbj library for es6", - "main": "dist/pbj.min.js", - "license": "Apache-2.0", - "dependencies": { - "gdbots-common": "*" - } -} diff --git a/build/use-lodash-es.js b/build/use-lodash-es.js new file mode 100644 index 0000000..384f46c --- /dev/null +++ b/build/use-lodash-es.js @@ -0,0 +1,11 @@ +/* eslint-disable */ +module.exports = function () { + return { + visitor: { + ImportDeclaration(path) { + const source = path.node.source; + source.value = source.value.replace(/^lodash($|\/)/, 'lodash-es$1'); + } + } + } +}; diff --git a/dist/pbj.min.js b/dist/pbj.min.js deleted file mode 100644 index 14f0c08..0000000 --- a/dist/pbj.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/*! @gdbots/pbj v0.1.0, 2016-08-25 15:11:01 PDT */ -define("gdbots/pbj/codec",["exports","gdbots/pbj/well-known/dynamic-field","gdbots/pbj/well-known/geo-point","gdbots/pbj/message","gdbots/pbj/message-ref"],function(exports,_dynamicField,_geoPoint,_message,_messageRef){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var _dynamicField2=_interopRequireDefault(_dynamicField),_geoPoint2=_interopRequireDefault(_geoPoint),_message2=_interopRequireDefault(_message),_messageRef2=_interopRequireDefault(_messageRef),_createClass=function(){function defineProperties(target,props){for(var i=0;i "+schema.getClassName()}),_this=_possibleConstructorReturn(this,(MoreThanOneMessageForMixin.__proto__||Object.getPrototypeOf(MoreThanOneMessageForMixin)).call(this,"MessageResolver returned multiple messages using ["+mixin.getId().getCurieMajor()+"] when one was expected. Messages found: \n"+ids.join("\n")));return privateProps.set(_this,{mixin:mixin,messages:messages}),_this}return _inherits(MoreThanOneMessageForMixin,_SystemUtils$mixinCla),_createClass(MoreThanOneMessageForMixin,[{key:"getMixin",value:function(){return privateProps.get(this).mixin}},{key:"getMessage",value:function(){return privateProps.get(this).messages}}]),MoreThanOneMessageForMixin}(_systemUtils2["default"].mixinClass(_gdbotsPbjException2["default"]));exports["default"]=MoreThanOneMessageForMixin}),define("gdbots/pbj/exception/no-message-for-curie",["exports","gdbots/common/util/system-utils","gdbots/pbj/exception/gdbots-pbj-exception"],function(exports,_systemUtils,_gdbotsPbjException){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_gdbotsPbjException2=_interopRequireDefault(_gdbotsPbjException),_createClass=function(){function defineProperties(target,props){for(var i=0;i0?(privateProps.get(this).maxLength=maxLength,privateProps.get(this).minLength=_numberUtils2["default"].bound(minLength,0,privateProps.get(this).maxLength)):privateProps.get(this).minLength=_numberUtils2["default"].bound(minLength,0,privateProps.get(this).type.getMaxBytes()),null!==pattern&&(privateProps.get(this).pattern=pattern.trim().replace("/","")),null!==format&&_format2["default"].enumValueOf(format)?privateProps.get(this).format=_format2["default"].enumValueOf(format):privateProps.get(this).format=_format2["default"].UNKNOWN}function applyNumericOptions(){var min=arguments.length<=0||void 0===arguments[0]?null:arguments[0],max=arguments.length<=1||void 0===arguments[1]?null:arguments[1],precision=arguments.length<=2||void 0===arguments[2]?10:arguments[2],scale=arguments.length<=3||void 0===arguments[3]?2:arguments[3];null!==max&&(privateProps.get(this).max=parseInt(max)),null!==min&&(privateProps.get(this).min=parseInt(min),null!==privateProps.get(this).max&&privateProps.get(this).min>privateProps.get(this).max&&(privateProps.get(this).min=privateProps.get(this).max)),privateProps.get(this).precision=_numberUtils2["default"].bound(parseInt(precision),1,65),privateProps.get(this).scale=_numberUtils2["default"].bound(parseInt(scale),0,privateProps.get(this).precision)}function applyDefault(){var defaultValue=arguments.length<=0||void 0===arguments[0]?null:arguments[0];if(privateProps.get(this).defaultValue=defaultValue,privateProps.get(this).type.isScalar())privateProps.get(this).type.getTypeName()!==_typeName2["default"].TIMESTAMP&&(privateProps.get(this).useTypeDefault=!0);else{var decodeDefault=null!==privateProps.get(this).defaultValue&&"function"!=typeof privateProps.get(this).defaultValue;switch(privateProps.get(this).type.getTypeName()){case _typeName2["default"].IDENTIFIER:if(null===privateProps.get(this).instance)throw new Error("Field ["+privateProps.get(this).name+"] requires an instance.");decodeDefault&&!privateProps.get(this).defaultValue.hasTrait("Identifier")&&(privateProps.get(this).defaultValue=privateProps.get(this).type.decode(privateProps.get(this).defaultValue,this));break;case _typeName2["default"].INT_ENUM:case _typeName2["default"].STRING_ENUM:if(null===privateProps.get(this).instance)throw new Error("Field ["+privateProps.get(this).name+"] requires an instance.");decodeDefault&&!privateProps.get(this).defaultValue.hasTrait("Enum")&&(privateProps.get(this).defaultValue=privateProps.get(this).type.decode(privateProps.get(this).defaultValue,this))}}null!==privateProps.get(this).defaultValue&&"function"!=typeof privateProps.get(this).defaultValue&&guardDefault.bind(this)(privateProps.get(this).defaultValue)}function guardDefault(defaultValue){if(this.isASingleValue())return void this.guardValue(defaultValue);if(null!==defaultValue||!Array.isArray(defaultValue))throw new Error("Field ["+privateProps.get(this).name+"] default must be an array.");if(null!==defaultValue){if(this.isAMap()&&!_arrayUtils2["default"].isAssoc(defaultValue))throw new Error("Field ["+privateProps.get(this).name+"] default must be an associative array.");_arrayUtils2["default"].each(defaultValue,function(value,key){if(null===value)throw new Error("Field ["+privateProps.get(this).name+"] default for key ["+value+"] cannot be null.");this.guardValue(value)}.bind(this))}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.VALID_NAME_PATTERN=void 0;var _toArray2=_interopRequireDefault(_toArray),_arrayUtils2=_interopRequireDefault(_arrayUtils),_numberUtils2=_interopRequireDefault(_numberUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_typeName2=_interopRequireDefault(_typeName),_fieldRule2=_interopRequireDefault(_fieldRule),_format2=_interopRequireDefault(_format),_createClass=function(){function defineProperties(target,props){for(var i=0;iname.length||name.length>127)throw new Error("Name length must be between 1 to 127.");if(!VALID_NAME_PATTERN.test(name))throw new Error("Field ["+name+"] must match pattern ["+VALID_NAME_PATTERN+"].");if(!type||!type.hasTrait("Type"))throw new Error('Class "'+type+'" was expected to be instanceof of "Type" but is not.');if("boolean"!=typeof required)throw new Error("Required value must be boolean.");if("boolean"!=typeof useTypeDefault)throw new Error("UseTypeDefault value must be boolean.");if("boolean"!=typeof overridable)throw new Error("Overridable value must be boolean.");return type.getTypeName()!==_typeName2["default"].MESSAGE&&(anyOfInstances=null),privateProps.set(_this,{name:name,type:type,rule:null,required:required||!1,minLength:null,maxLength:null,pattern:null,format:null,min:null,max:null,precision:10,scale:2,defaultValue:null,useTypeDefault:useTypeDefault,instance:instance,anyOfInstances:anyOfInstances,assertion:assertion,overridable:overridable||!1}),applyFieldRule.bind(_this)(rule),applyStringOptions.bind(_this)(minLength,maxLength,pattern,format),applyNumericOptions.bind(_this)(min,max,precision,scale),applyDefault.bind(_this)(defaultValue),_this}return _inherits(Field,_SystemUtils$mixinCla),_createClass(Field,[{key:"getName",value:function(){return privateProps.get(this).name}},{key:"getType",value:function(){return privateProps.get(this).type}},{key:"getRule",value:function(){return privateProps.get(this).rule}},{key:"isASingleValue",value:function(){return _fieldRule2["default"].A_SINGLE_VALUE===privateProps.get(this).rule}},{key:"isASet",value:function(){return _fieldRule2["default"].A_SET===privateProps.get(this).rule}},{key:"isAList",value:function(){return _fieldRule2["default"].A_LIST===privateProps.get(this).rule}},{key:"isAMap",value:function(){return _fieldRule2["default"].A_MAP===privateProps.get(this).rule}},{key:"isRequired",value:function(){return privateProps.get(this).required}},{key:"getMinLength",value:function(){return privateProps.get(this).minLength||0}},{key:"getMaxLength",value:function(){return privateProps.get(this).maxLength?privateProps.get(this).maxLength:privateProps.get(this).type.getMaxBytes()}},{key:"getPattern",value:function(){return privateProps.get(this).pattern}},{key:"getFormat",value:function(){return privateProps.get(this).format}},{key:"getMin",value:function(){return privateProps.get(this).min?privateProps.get(this).min:privateProps.get(this).type.getMin()}},{key:"getMax",value:function(){return privateProps.get(this).max?privateProps.get(this).max:privateProps.get(this).type.getMax()}},{key:"getPrecision",value:function(){return privateProps.get(this).precision}},{key:"getScale",value:function(){return privateProps.get(this).scale}},{key:"getDefault",value:function(){var message=arguments.length<=0||void 0===arguments[0]?null:arguments[0];if(null===privateProps.get(this).defaultValue)return privateProps.get(this).useTypeDefault?this.isASingleValue()?privateProps.get(this).type.getDefault():[]:this.isASingleValue()?null:[];if("function"==typeof privateProps.get(this).defaultValue){var defaultValue=privateProps.get(this).defaultValue(message,this);return guardDefault.bind(this)(defaultValue),null===defaultValue?privateProps.get(this).useTypeDefault?this.isASingleValue()?privateProps.get(this).type.getDefault():[]:this.isASingleValue()?null:[]:defaultValue}return privateProps.get(this).defaultValue}},{key:"hasInstance",value:function(){return null!==privateProps.get(this).instance}},{key:"getInstance",value:function(){return privateProps.get(this).instance}},{key:"hasAnyOfInstances",value:function(){return null!==privateProps.get(this).anyOfInstances}},{key:"getAnyOfInstances",value:function(){return privateProps.get(this).anyOfInstances}},{key:"isOverridable",value:function(){return privateProps.get(this).overridable}},{key:"guardValue",value:function(value){if(privateProps.get(this).required&&null===value)throw new Error("Field ["+privateProps.get(this).name+"] is required and cannot be null.");null!==value&&privateProps.get(this).type.guard(value,this),null!==privateProps.get(this).assertion&&privateProps.get(this).assertion(value,this)}},{key:"toArray",value:function(){return{name:privateProps.get(this).name,type:privateProps.get(this).type.getTypeValue(),rule:privateProps.get(this).rule.getName(),required:privateProps.get(this).required,min_length:privateProps.get(this).minLength,max_length:privateProps.get(this).maxLength,pattern:privateProps.get(this).pattern,format:privateProps.get(this).format.getValue(),min:privateProps.get(this).min,max:privateProps.get(this).max,precision:privateProps.get(this).precision,scale:privateProps.get(this).scale,"default":this.getDefault(),use_type_default:privateProps.get(this).useTypeDefault,instance:privateProps.get(this).instance,any_of_instances:privateProps.get(this).anyOfInstances,has_assertion:null!==privateProps.get(this).assertion,overridable:privateProps.get(this).overridable}}},{key:"isCompatibleForMerge",value:function(other){return privateProps.get(this).name!==other.name?!1:privateProps.get(this).type!==other.type?!1:privateProps.get(this).rule!==other.rule?!1:privateProps.get(this).instance!==other.instance?!1:0===privateProps.get(this).anyOfInstances.filter(function(k){return-1!=other.anyOfInstances.indexOf(k)}).length?!1:!0}},{key:"isCompatibleForOverride",value:function(other){return privateProps.get(this).overridable?privateProps.get(this).name!==other.name?!1:privateProps.get(this).type!==other.type?!1:privateProps.get(this).rule!==other.rule?!1:privateProps.get(this).required!==other.required?!1:!0:!1}}]),Field}(_systemUtils2["default"].mixinClass(null,_toArray2["default"]));exports["default"]=Field}),define("gdbots/pbj/message-ref",["exports","gdbots/common/util/system-utils","gdbots/common/from-array","gdbots/common/to-array","gdbots/pbj/exception/invalid-argument-exception","gdbots/pbj/exception/logic-exception","gdbots/pbj/schema-curie"],function(exports,_systemUtils,_fromArray,_toArray,_invalidArgumentException,_logicException,_schemaCurie){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_fromArray2=_interopRequireDefault(_fromArray),_toArray2=_interopRequireDefault(_toArray),_invalidArgumentException2=_interopRequireDefault(_invalidArgumentException),_logicException2=_interopRequireDefault(_logicException),_schemaCurie2=_interopRequireDefault(_schemaCurie),_createClass=function(){function defineProperties(target,props){for(var i=0;i145)throw new Error("Schema curie cannot be greater than 145 chars.");var matches=curie.match(VALID_PATTERN);if(null===matches)throw new _invalidSchemaCurie2["default"]("Schema curie ["+curie+"] is invalid. It must match the pattern ["+VALID_PATTERN+"].");return _instances[curie]=new this(matches[1],matches[2],matches[3],matches[4]),_instances[curie]}}]),SchemaCurie}();exports["default"]=SchemaCurie}),define("gdbots/pbj/schema-id",["exports","gdbots/pbj/exception/invalid-schema-id","gdbots/pbj/schema-curie","gdbots/pbj/schema-version"],function(exports,_invalidSchemaId,_schemaCurie,_schemaVersion){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0}),exports.VALID_PATTERN=void 0;var _invalidSchemaId2=_interopRequireDefault(_invalidSchemaId),_schemaCurie2=_interopRequireDefault(_schemaCurie),_schemaVersion2=_interopRequireDefault(_schemaVersion),_createClass=function(){function defineProperties(target,props){for(var i=0;i145)throw new Error("Schema id cannot be greater than 150 chars.");var matches=schemaId.match(VALID_PATTERN);if(null===matches)throw new _invalidSchemaId2["default"]("Schema id ["+schemaId+"] is invalid. It must match the pattern ["+VALID_PATTERN+"].");return _instances[schemaId]=new this(matches[1],matches[2],matches[3],matches[4],matches[5]),_instances[schemaId]}}]),SchemaId}();exports["default"]=SchemaId}),define("gdbots/pbj/schema-q-name",["exports","gdbots/pbj/exception/invalid-schema-q-name"],function(exports,_invalidSchemaQName){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0}),exports.VALID_PATTERN=void 0;var _createClass=(_interopRequireDefault(_invalidSchemaQName),function(){function defineProperties(target,props){for(var i=0;i=minLength&&maxLength>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between ["+minLength+"] and ["+maxLength+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value=value.trim(),""===value?null:privateProps.get(this).encodeToBase64?_urlUtils2["default"].base64Encode(value):value}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(value=value.trim(),""===value)return null;if(!privateProps.get(this).decodeFromBase64)return value;if(value=_urlUtils2["default"].base64Decode(value),!1===value)throw new _decodeValueFailed2["default"](value,field,"Strict base64_decode failed.");return value}},{key:"isBinary",value:function(){return!0}},{key:"isString",value:function(){return!0}}]),AbstractBinaryType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=AbstractBinaryType}),define("gdbots/pbj/type/abstract-int-type",["exports","gdbots/common/util/number-utils","gdbots/common/util/system-utils","gdbots/pbj/type/type"],function(exports,_numberUtils,_systemUtils,_type){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _numberUtils2=_interopRequireDefault(_numberUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_type2=_interopRequireDefault(_type),_createClass=function(){function defineProperties(target,props){for(var i=0;i=min&&max>=value;if(!okay)throw new Error("Field ["+field.getName()+"] value must be between ["+min+"] and ["+max+"], ["+value+"] given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return parseInt(value)}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return parseInt(value)}},{key:"getDefault",value:function(){return 0}},{key:"isNumeric",value:function(){return!0}}]),AbstractIntType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=AbstractIntType}),define("gdbots/pbj/type/abstract-string-type",["exports","gdbots/common/util/number-utils","gdbots/common/util/system-utils","gdbots/pbj/type/type"],function(exports,_numberUtils,_systemUtils,_type){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _numberUtils2=_interopRequireDefault(_numberUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_type2=_interopRequireDefault(_type),_createClass=function(){function defineProperties(target,props){for(var i=0;i=minLength&&maxLength>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between ["+minLength+"] and ["+maxLength+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value=value.trim(),""===value?null:value}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value=value.trim(),""===value?null:value}},{key:"isString",value:function(){return!0}}]),AbstractStringType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=AbstractStringType}),define("gdbots/pbj/type/big-int-type",["exports","gdbots/common/util/system-utils","gdbots/pbj/well-known/big-number","gdbots/pbj/type/type"],function(exports,_systemUtils,_bigNumber,_type){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_bigNumber2=_interopRequireDefault(_bigNumber),_type2=_interopRequireDefault(_type),_createClass=function(){function defineProperties(target,props){for(var i=0;i0&&maxBytes>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between [1] and ["+maxBytes+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value.hasTrait("Identifier")?value.toString():null}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(!value||0===value.length)return null;var instance=field.getInstance();try{return instance.fromString(value)}catch(e){throw new _decodeValueFailed2["default"](value,field,e)}}},{key:"isScalar",value:function(){return!1}},{key:"isString",value:function(){return!0}},{key:"getMaxBytes",value:function(){return 100}}]),IdentifierType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=IdentifierType}),define("gdbots/pbj/type/int-enum-type",["exports","gdbots/common/util/array-utils","gdbots/common/util/system-utils","gdbots/pbj/type/type","gdbots/pbj/exception/decode-value-failed"],function(exports,_arrayUtils,_systemUtils,_type,_decodeValueFailed){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _arrayUtils2=_interopRequireDefault(_arrayUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_type2=_interopRequireDefault(_type),_decodeValueFailed2=_interopRequireDefault(_decodeValueFailed),_createClass=function(){function defineProperties(target,props){for(var i=0;ithis.getMax())throw new Error('Number "'+value.getValue()+'" was expected to be at least "'+value.getMin()+'" and at most "'+value.getMax()+'".')}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value.hasTrait("Enum")?parseInt(value.getValue()):0}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(null===value)return null;var instance=field.getInstance(),enumValue=null;if(_arrayUtils2["default"].each(instance.enumValues,function(item){value===parseInt(item.getValue())&&(enumValue=item)}),null===enumValue)throw new _decodeValueFailed2["default"](value,field,e);return enumValue}},{key:"isScalar",value:function(){return!1}},{key:"isNumeric",value:function(){return!0}},{key:"getMin",value:function(){return 0}},{key:"getMax",value:function(){return 65535}}]),IntEnumType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=IntEnumType}),define("gdbots/pbj/type/int-type",["exports","gdbots/common/util/system-utils","gdbots/pbj/type/abstract-int-type"],function(exports,_systemUtils,_abstractIntType){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_abstractIntType2=_interopRequireDefault(_abstractIntType),_createClass=function(){function defineProperties(target,props){for(var i=0;i0&&maxBytes>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between [1] and ["+maxBytes+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value.hasTrait("Enum")?String(value.getValue()):null}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(null===value)return null;var instance=field.getInstance(),enumValue=null;if(_arrayUtils2["default"].each(instance.enumValues,function(item){value===String(item.getValue())&&(enumValue=item)}),null===enumValue)throw new _decodeValueFailed2["default"](value,field,e);return enumValue}},{key:"isScalar",value:function(){return!1}},{key:"isString",value:function(){return!0}},{key:"getMaxBytes",value:function(){return 100}}]),StringEnumType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=StringEnumType}),define("gdbots/pbj/type/string-type",["exports","gdbots/common/util/date-utils","gdbots/common/util/hashtag-utils","gdbots/common/util/system-utils","gdbots/pbj/type/abstract-string-type","gdbots/pbj/enum/format"],function(exports,_dateUtils,_hashtagUtils,_systemUtils,_abstractStringType,_format){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _dateUtils2=_interopRequireDefault(_dateUtils),_hashtagUtils2=_interopRequireDefault(_hashtagUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_abstractStringType2=_interopRequireDefault(_abstractStringType),_format2=_interopRequireDefault(_format),_createClass=function(){function defineProperties(target,props){for(var i=0;i()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].HASHTAG:if(!_hashtagUtils2["default"].isValid(value))throw new Error("Field ["+field.getName()+"] must be a valid hashtag. @see HashtagUtils.isValid.");break;case _format2["default"].IPV4:if(!/^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].IPV6:if(!/^((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].HOSTNAME:case _format2["default"].URI:case _format2["default"].URL:if(!/(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].UUID:if(!/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].")}}},{key:"getMaxBytes",value:function(){return 255}}]),StringType}(_systemUtils2["default"].mixinClass(_abstractStringType2["default"]));exports["default"]=StringType}),define("gdbots/pbj/type/text-type",["exports","gdbots/common/util/system-utils","gdbots/pbj/type/abstract-string-type"],function(exports,_systemUtils,_abstractStringType){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call; -}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_abstractStringType2=_interopRequireDefault(_abstractStringType),_createClass=function(){function defineProperties(target,props){for(var i=0;iname.length||name.length>127)throw new Error("DynamicField name length must be between 1 to 127.");if(!VALID_NAME_PATTERN.test(name))throw new Error("DynamicField name ["+name+"] must match pattern ["+VALID_NAME_PATTERN+"].");var field=createField(kind.getValue());return privateProps.set(_this,{name:name,kind:kind.getValue(),value:field.getType().decode(value,field)}),field.guardValue(privateProps.get(_this).value),_this}return _inherits(DynamicField,_SystemUtils$mixinCla),_createClass(DynamicField,[{key:"toArray",value:function(){var field=createField(privateProps.get(this).kind),data={name:privateProps.get(this).name};return data[privateProps.get(this).kind]=field.getType().encode(privateProps.get(this).value,field),data}},{key:"toString",value:function(){return JSON.stringify(this)}},{key:"getName",value:function(){return privateProps.get(this).name}},{key:"getKind",value:function(){return privateProps.get(this).kind}},{key:"getField",value:function(){return createField(privateProps.get(this).kind)}},{key:"getValue",value:function(){return privateProps.get(this).value}},{key:"equals",value:function(other){return privateProps.get(this).name===privateProps.get(other).name&&privateProps.get(this).kind===privateProps.get(other).kind&&privateProps.get(this).value===privateProps.get(other).value}}],[{key:"createBoolVal",value:function(name){var value=arguments.length<=1||void 0===arguments[1]?!1:arguments[1];return new this(name,_dynamicFieldKind2["default"].BOOL_VAL,value)}},{key:"createDateVal",value:function(name,value){return new this(name,_dynamicFieldKind2["default"].DATE_VAL,value)}},{key:"createFloatVal",value:function(name){var value=arguments.length<=1||void 0===arguments[1]?0:arguments[1];return new this(name,_dynamicFieldKind2["default"].FLOAT_VAL,value)}},{key:"createIntVal",value:function(name){var value=arguments.length<=1||void 0===arguments[1]?0:arguments[1];return new this(name,_dynamicFieldKind2["default"].INT_VAL,value)}},{key:"createStringVal",value:function(name,value){return new this(name,_dynamicFieldKind2["default"].STRING_VAL,value)}},{key:"createTextVal",value:function(name,value){return new this(name,_dynamicFieldKind2["default"].TEXT_VAL,value)}},{key:"fromArray",value:function(){var data=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];if(void 0===data.name)throw new _invalidArgumentException2["default"]('DynamicField "name" property must be set.');var name=data.name;delete data.name;var kind=Array.keys(data)[0];try{kind=_dynamicFieldKind2["default"][kind.toUpperCase()]}catch(e){throw new _invalidArgumentException2["default"]('DynamicField "'+kind+'" is not a valid kind.')}return new this(name,kind,data[kind.getValue()])}}]),DynamicField}(_systemUtils2["default"].mixinClass(null,_fromArray2["default"],_toArray2["default"]));exports["default"]=DynamicField}),define("gdbots/pbj/well-known/generates-identifier",["exports"],function(exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i90||privateProps.get(_this).latitude<-90)throw new _invalidArgumentException2["default"]("Latitude must be within range [-90.0, 90.0]");if(privateProps.get(_this).longitude>180||privateProps.get(_this).longitude<-180)throw new _invalidArgumentException2["default"]("Longitude must be within range [-180.0, 180.0]");return _this}return _inherits(GeoPoint,_SystemUtils$mixinCla),_createClass(GeoPoint,[{key:"getLatitude",value:function(){return privateProps.get(this).latitude}},{key:"getLongitude",value:function(){return privateProps.get(this).longitude}},{key:"toArray", -value:function(){return{type:"Point",coordinates:[privateProps.get(this).longitude,privateProps.get(this).latitude]}}},{key:"toString",value:function(){return privateProps.get(this).latitude+","+privateProps.get(this).longitude}}],[{key:"fromArray",value:function(){var data=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];if(void 0!==data.coordinates)return new this(data.coordinates[1],data.coordinates[0]);throw new _invalidArgumentException2["default"]('Payload must be a GeoJson "Point" type.')}},{key:"fromString",value:function(string){return string=string.split(","),new this(string[0],string[1])}}]),GeoPoint}(_systemUtils2["default"].mixinClass(null,_fromArray2["default"],_toArray2["default"]));exports["default"]=GeoPoint}),define("gdbots/pbj/well-known/identifier",["exports"],function(exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;ilen||len>16)throw new _invalidArgumentException2["default"]("Input ["+int+"] must be between 13 and 16 digits, ["+len+"] given.");16>len&&(int=_stringUtils2["default"].strPad(int,16,"0"));var m=new this;return privateProps.get(m)["int"]=parseInt(int),privateProps.get(m).sec=parseInt(int.substring(0,10)),privateProps.get(m).usec=parseInt(int.slice(-6)),m}}]),Microtime}();exports["default"]=Microtime}),define("gdbots/pbj/well-known/slug-identifier",["exports","gdbots/common/util/slug-utils","gdbots/common/util/string-utils","gdbots/common/util/system-utils","gdbots/pbj/exception/invalid-argument-exception","gdbots/pbj/well-known/identifier"],function(exports,_slugUtils,_stringUtils,_systemUtils,_invalidArgumentException,_identifier){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _slugUtils2=_interopRequireDefault(_slugUtils),_stringUtils2=_interopRequireDefault(_stringUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_invalidArgumentException2=_interopRequireDefault(_invalidArgumentException),_identifier2=_interopRequireDefault(_identifier),_createClass=function(){function defineProperties(target,props){for(var i=0;i 0) { + this.minLength = clamp(this.minLength, 0, this.maxLength); + } else { + this.minLength = clamp(this.minLength, 0, this.type.getMaxBytes()); + } + + this.pattern = pattern ? new RegExp(trim(pattern, '/')) : null; + + if (format) { + try { + this.format = Format.create(`${format}`); + } catch (e) { + this.format = Format.UNKNOWN; + } + } else { + this.format = Format.UNKNOWN; + } + } + + /** + * @private + * + * @param {?number} min + * @param {?number} max + * @param {?number} precision + * @param {?number} scale + */ + applyNumericOptions(min = null, max = null, precision = 10, scale = 2) { + this.min = min; + this.max = max; + + if (this.max !== null) { + this.max = toInteger(this.max); + } + + if (this.min !== null) { + this.min = toInteger(this.min); + if (this.max !== null && this.min > this.max) { + this.min = this.max; + } + } + + this.precision = clamp(precision, 1, 65); + this.scale = clamp(scale, 0, this.precision); + } + + /** + * @private + * + * @param {*} defaultValue + */ + applyDefault(defaultValue = null) { + this.defaultValue = defaultValue; + const defaultValueIsAFunction = isFunction(this.defaultValue); + + if (this.type.isScalar()) { + if (this.type.getTypeName() !== TypeName.TIMESTAMP) { + this.useTypeDefault = true; + } + } else { + const decodeDefault = this.defaultValue !== null && !defaultValueIsAFunction; + switch (this.type.getTypeValue()) { + case TypeName.IDENTIFIER.getValue(): + if (!this.hasClassProto()) { + throw new AssertionFailed(`Field [${this.name}] requires a classProto.`); + } + + if (decodeDefault && !(this.defaultValue instanceof Identifier)) { + this.defaultValue = this.type.decode(this.defaultValue, this); + } + + break; + + case TypeName.INT_ENUM.getValue(): + case TypeName.STRING_ENUM.getValue(): + if (!this.hasClassProto()) { + throw new AssertionFailed(`Field [${this.name}] requires a classProto.`); + } + + if (decodeDefault && !(this.defaultValue instanceof Enum)) { + this.defaultValue = this.type.decode(this.defaultValue, this); + } + + break; + + default: + break; + } + } + + if (this.defaultValue !== null && !defaultValueIsAFunction) { + this.guardDefault(this.defaultValue); + } + } + + /** + * @returns {string} + */ + getName() { + return this.name; + } + + /** + * @returns {Type} + */ + getType() { + return this.type; + } + + /** + * @returns FieldRule + */ + getRule() { + return this.rule; + } + + /** + * @returns {boolean} + */ + isASingleValue() { + return FieldRule.A_SINGLE_VALUE === this.rule; + } + + /** + * @returns {boolean} + */ + isASet() { + return FieldRule.A_SET === this.rule; + } + + /** + * @returns {boolean} + */ + isAList() { + return FieldRule.A_LIST === this.rule; + } + + /** + * @returns {boolean} + */ + isAMap() { + return FieldRule.A_MAP === this.rule; + } + + /** + * @returns {boolean} + */ + isRequired() { + return this.required; + } + + /** + * @returns {number} + */ + getMinLength() { + return this.minLength; + } + + /** + * @returns {number} + */ + getMaxLength() { + return this.maxLength > 0 ? this.maxLength : this.type.getMaxBytes(); + } + + /** + * @returns {?RegExp} + */ + getPattern() { + return this.pattern; + } + + /** + * @returns {Format} + */ + getFormat() { + return this.format; + } + + /** + * @returns {number} + */ + getMin() { + return this.min === null ? this.type.getMin() : this.min; + } + + /** + * @returns {number} + */ + getMax() { + return this.max === null ? this.type.getMax() : this.max; + } + + /** + * @returns {number} + */ + getPrecision() { + return this.precision; + } + + /** + * @returns {number} + */ + getScale() { + return this.scale; + } + + /** + * @param {?Message} message + */ + getDefault(message = null) { + if (this.defaultValue === null) { + if (this.useTypeDefault) { + return this.isASingleValue() ? this.type.getDefault() : []; + } + + return this.isASingleValue() ? null : []; + } + + if (!isFunction(this.defaultValue)) { + return this.defaultValue; + } + + const dynamicDefault = this.defaultValue(message, this); + this.guardDefault(dynamicDefault); + if (dynamicDefault === null) { + if (this.useTypeDefault) { + return this.isASingleValue() ? this.type.getDefault() : []; + } + + return this.isASingleValue() ? null : []; + } + + return dynamicDefault; + } + + /** + * @private + * + * @param {*} defaultValue + * + * @throws {AssertionFailed} + */ + guardDefault(defaultValue) { + if (defaultValue === null) { + return; + } + + if (this.isASingleValue()) { + this.guardValue(defaultValue); + return; + } + + if (this.isAMap() && !isMap(defaultValue)) { + throw new AssertionFailed(`Field [${this.name}] default must be a Map.`); + } else if (this.isASet() && !isSet(defaultValue)) { + throw new AssertionFailed(`Field [${this.name}] default must be a Set.`); + } else if (this.isAList() && !isArray(defaultValue)) { + throw new AssertionFailed(`Field [${this.name}] default must be an Array.`); + } + + defaultValue.forEach((v, k) => { + if (v === null) { + throw new AssertionFailed(`Field [${this.name}] default for key [${k}] cannot be null.`); + } + + this.guardValue(v); + }); + } + + /** + * @returns {boolean} + */ + hasClassProto() { + return this.classProto !== null && isObject(this.classProto) && !isPlainObject(this.classProto); + } + + /** + * @returns {?Enum|Object|Message|Identifier} + */ + getClassProto() { + return this.classProto; + } + + /** + * @returns {string[]} + */ + getAnyOfCuries() { + return this.anyOfCuries; + } + + /** + * @returns {boolean} + */ + isOverridable() { + return this.overridable; + } + + /** + * @param {*} value + * + * @throws {AssertionFailed} + */ + guardValue(value) { + if (this.required && value === null) { + throw new AssertionFailed(`Field [${this.name}] is required and cannot be null.`); + } + + if (value !== null) { + this.type.guard(value, this); + } + + if (this.assertion) { + this.assertion(value, this); + } + } + + /** + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * @returns {Object} + */ + toObject() { + return { + name: this.name, + type: this.type.getTypeValue(), + rule: this.rule.getName(), + required: this.required, + min_length: this.minLength, + max_length: this.maxLength, + pattern: `${this.pattern}`, + format: this.format.getValue(), + min: this.min, + max: this.max, + precision: this.precision, + scale: this.scale, + default_value: this.defaultValue, + use_type_default: this.useTypeDefault, + class_proto: this.classProto, + any_of_curies: this.anyOfCuries, + has_assertion: this.assertion !== null, + overridable: this.overridable, + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * Returns true if this field is likely compatible with the + * provided field during a mergeFrom operation. + * + * todo: implement/test isCompatibleForMerge + * + * @param {Field} other + * + * @returns {boolean} + */ + isCompatibleForMerge(other) { + if (this.name !== other.name) { + return false; + } + + if (this.type !== other.type) { + return false; + } + + if (this.rule !== other.rule) { + return false; + } + + return intersection(this.anyOfCuries, other.anyOfCuries).length; + } + + /** + * Returns true if the provided field can be used as an + * override to this field. + * + * @param {Field} other + * + * @returns {boolean} + */ + isCompatibleForOverride(other) { + if (!this.overridable) { + return false; + } + + if (this.name !== other.name) { + return false; + } + + if (this.type !== other.type) { + return false; + } + + if (this.rule !== other.rule) { + return false; + } + + return this.required === other.required; + } +} diff --git a/src/FieldBuilder.js b/src/FieldBuilder.js new file mode 100644 index 0000000..bb5aaf5 --- /dev/null +++ b/src/FieldBuilder.js @@ -0,0 +1,227 @@ +import toInteger from 'lodash/toInteger'; +import FieldRule from './enums/FieldRule'; +import Field from './Field'; + +export default class FieldBuilder { + /** + * @param {string} name + * @param {Type} type + */ + constructor(name, type) { + this.config = { + name, + type, + rule: FieldRule.A_SINGLE_VALUE, + required: false, + precision: 10, + scale: 2, + useTypeDefault: true, + overridable: false, + }; + } + + /** + * @param {string} name + * @param {Type} type + * + * @returns {FieldBuilder} + */ + static create(name, type) { + return new FieldBuilder(name, type); + } + + /** + * @returns {FieldBuilder} + */ + required() { + this.config.required = true; + return this; + } + + /** + * @returns {FieldBuilder} + */ + optional() { + this.config.required = false; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asASingleValue() { + this.config.rule = FieldRule.A_SINGLE_VALUE; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asASet() { + this.config.rule = FieldRule.A_SET; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asAList() { + this.config.rule = FieldRule.A_LIST; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asAMap() { + this.config.rule = FieldRule.A_MAP; + return this; + } + + /** + * @param {number} minLength + * + * @returns {FieldBuilder} + */ + minLength(minLength) { + this.config.minLength = toInteger(minLength); + return this; + } + + /** + * @param {number} maxLength + * + * @returns {FieldBuilder} + */ + maxLength(maxLength) { + this.config.maxLength = toInteger(maxLength); + return this; + } + + /** + * @param {string} pattern + * + * @returns {FieldBuilder} + */ + pattern(pattern) { + this.config.pattern = pattern; + return this; + } + + /** + * @param {string} format + * + * @returns {FieldBuilder} + */ + format(format) { + this.config.format = format; + return this; + } + + /** + * @param {number} min + * + * @returns {FieldBuilder} + */ + min(min) { + this.config.min = toInteger(min); + return this; + } + + /** + * @param {number} max + * + * @returns {FieldBuilder} + */ + max(max) { + this.config.max = toInteger(max); + return this; + } + + /** + * @param {number} precision + * + * @returns {FieldBuilder} + */ + precision(precision) { + this.config.precision = toInteger(precision); + return this; + } + + /** + * @param {number} scale + * + * @returns {FieldBuilder} + */ + scale(scale) { + this.config.scale = toInteger(scale); + return this; + } + + /** + * @param {*} defaultValue + * + * @returns {FieldBuilder} + */ + withDefault(defaultValue) { + this.config.defaultValue = defaultValue; + return this; + } + + /** + * @param {boolean} useTypeDefault + * + * @returns {FieldBuilder} + */ + useTypeDefault(useTypeDefault) { + this.config.useTypeDefault = useTypeDefault; + return this; + } + + /** + * @param {Function} classProto + * + * @returns {FieldBuilder} + */ + classProto(classProto) { + this.config.classProto = classProto; + return this; + } + + /** + * @param {string[]} anyOfCuries + * + * @returns {FieldBuilder} + */ + anyOfCuries(anyOfCuries) { + this.config.anyOfCuries = anyOfCuries; + return this; + } + + /** + * @param {Function} assertion + * + * @returns {FieldBuilder} + */ + assertion(assertion) { + this.config.assertion = assertion; + return this; + } + + /** + * @param {boolean} overridable + * + * @returns {FieldBuilder} + */ + overridable(overridable) { + this.config.overridable = overridable; + return this; + } + + /** + * @returns {Field} + */ + build() { + return new Field(Object.assign({}, this.config)); + } +} diff --git a/src/Message.js b/src/Message.js new file mode 100644 index 0000000..d95243a --- /dev/null +++ b/src/Message.js @@ -0,0 +1,852 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ +import isArray from 'lodash/isArray'; +import isBoolean from 'lodash/isBoolean'; +import isEmpty from 'lodash/isEmpty'; +import isMap from 'lodash/isMap'; +import toSafeInteger from 'lodash/toSafeInteger'; +import trim from 'lodash/trim'; +import md5 from 'md5'; +import AssertionFailed from './exceptions/AssertionFailed'; +import FrozenMessageIsImmutable from './exceptions/FrozenMessageIsImmutable'; +import LogicException from './exceptions/LogicException'; +import RequiredFieldNotSet from './exceptions/RequiredFieldNotSet'; +import SchemaNotDefined from './exceptions/SchemaNotDefined'; +import MessageRef from './MessageRef'; +import Schema, { PBJ_FIELD_NAME } from './Schema'; +import JsonSerializer from './serializers/JsonSerializer'; +import ObjectSerializer from './serializers/ObjectSerializer'; + +/** + * Stores all message instances so data is kept + * private and cannot be mutated directly. + * + * @type {WeakMap} + */ +const msgs = new WeakMap(); + +/** + * Schemas only need to be defined once per message. + * This maps contains all references, keyed by the + * message class itself. + * + * @type {Map} + */ +const schemas = new Map(); + +/** + * Ensures a frozen message can't be modified. + * + * @param {Message} message + * + * @throws {FrozenMessageIsImmutable} + */ +function guardFrozenMessage(message) { + if (message.isFrozen()) { + throw new FrozenMessageIsImmutable(message); + } +} + +/** + * Populates the default on a single field if it's not already set + * and the default generated is not a null value or empty array. + * + * @param {Message} message + * @param {Field} field + * + * @returns {boolean} Returns true if a non null/empty default was applied or already present. + */ +function populateDefault(message, field) { + const fieldName = field.getName(); + if (message.has(fieldName)) { + return true; + } + + const defaultValue = field.getDefault(message); + if (defaultValue === null) { + return false; + } + + const msg = msgs.get(message); + + if (field.isASingleValue()) { + msg.data.set(fieldName, defaultValue); + msg.clearedFields.delete(fieldName); + return true; + } + + if (isEmpty(defaultValue)) { + return false; + } + + if (field.isASet()) { + message.addToSet(fieldName, Array.from(defaultValue)); + return true; + } + + msg.data.set(fieldName, defaultValue); + msg.clearedFields.delete(fieldName); + return true; +} + +export default class Message { + /** + * Nothing fancy on new messages... we let the serializers or application code get fancy. + */ + constructor() { + msgs.set(this, { + /** @var {Map} */ + data: new Map(), + + /** + * A set of fields that have been cleared or set to null that + * must be included when serialized so it's clear that the + * value has been unset. + * + * @var {Set} + */ + clearedFields: new Set(), + + /** + * @see Message.freeze + * + * @var {boolean} + */ + isFrozen: false, + + /** + * @see Message.isReplay + * + * @var {?boolean} + */ + isReplay: null, + }); + } + + /** + * @returns {Schema} + * + * @throws {SchemaNotDefined} + */ + static schema() { + if (!schemas.has(this)) { + const schema = this.defineSchema(); + + if (!(schema instanceof Schema)) { + throw new SchemaNotDefined( + `Message [${this.name}] must return a Schema from the defineSchema method.`, + ); + } + + if (schema.getClassProto() !== this) { + throw new SchemaNotDefined( + `Schema [${schema.getId()}] returned from defineSchema must be for class [${this.name}], not [${schema.getClassProto().name}].`, + ); + } + + schemas.set(this, schema); + return schema; + } + + return schemas.get(this); + } + + /** + * @private + * + * @returns {Schema} + * + * @throws {SchemaNotDefined} + */ + static defineSchema() { + throw new SchemaNotDefined(`Message [${this.constructor.name}] must return a Schema from the defineSchema method.`); + } + + /** + * @returns {Schema} + */ + schema() { + return this.constructor.schema(); + } + + /** + * Creates a new message with the defaults populated. + * + * @returns {Message} + */ + static create() { + const message = new this(); + return message.populateDefaults(); + } + + /** + * Returns a new message from the provided object using the ObjectSerializer. + * @see ObjectSerializer.deserialize + * + * @param {Object} obj + * + * @returns {Message} + * + * @throws {AssertionFailed} + */ + static fromObject(obj = {}) { + if (!obj[PBJ_FIELD_NAME]) { + // eslint-disable-next-line no-param-reassign + obj[PBJ_FIELD_NAME] = this.schema().getId().toString(); + } + + return ObjectSerializer.deserialize(obj); + } + + /** + * Generates an md5 hash of the json representation of the current message. + * + * @param {string[]} ignoredFields + * + * @returns {string} + */ + generateEtag(ignoredFields = []) { + const obj = ObjectSerializer.serialize(this, { includeAllFields: true }); + if (!ignoredFields.length) { + return md5(JSON.stringify(obj)); + } + + ignoredFields.forEach(f => delete obj[f]); + return md5(JSON.stringify(obj)); + } + + /** + * Generates a reference to this message with an optional tag. + * This method must be implemented in the concrete class or a mixin. + * + * @param {?string} tag + * + * @returns {MessageRef} + */ + generateMessageRef(tag = null) { + throw new LogicException('You must implement "generateMessageRef" in your schema.'); + } + + /** + * Returns an object that can be used in a uri template to generate + * a uri/url for this message. + * + * @link https://tools.ietf.org/html/rfc6570 + * @link https://medialize.github.io/URI.js/uri-template.html + * + * @returns {Object} + */ + getUriTemplateVars() { + return {}; + } + + /** + * Verifies all required fields have been populated. + * todo: recursively validate nested messages? + * + * @returns {Message} + * + * @throws {RequiredFieldNotSet} + */ + validate() { + this.schema().getRequiredFields().forEach((field) => { + if (!this.has(field.getName())) { + throw new RequiredFieldNotSet(this, field); + } + }); + + return this; + } + + /** + * Freezes the message, making it immutable. The message must be valid + * before it can be frozen so this may throw an exception if some required + * fields have not been populated. + * + * @returns {Message} + * + * @throws {RequiredFieldNotSet} + */ + freeze() { + if (this.isFrozen()) { + return this; + } + + this.validate(); + const msg = msgs.get(this); + msg.isFrozen = true; + + this.schema().getFields().forEach((field) => { + if (!field.getType().isMessage()) { + return; + } + + /** @var {Message|Message[]} value */ + const value = this.get(field.getName()); + if (value instanceof Message) { + value.freeze(); + return; + } + + if (isEmpty(value)) { + return; + } + + if (field.isAMap()) { + Object.keys(value).forEach(k => value[k].freeze()); + return; + } + + value.forEach(m => m.freeze()); + }); + + return this; + } + + /** + * Returns true if the message has been frozen. A frozen message is + * immutable and cannot be modified. + * + * @returns {boolean} + */ + isFrozen() { + return msgs.get(this).isFrozen; + } + + /** + * Returns true if the data of the message matches. + * + * @param {Message} other + * + * @returns {boolean} + */ + equals(other) { + // This could probably use some work. :) low level serialization string match. + return `${this}` === `${other}`; + } + + /** + * Returns true if this message is being replayed. Providing a value + * will set the flag but this can only be done once. Note that + * setting a message as being "replayed" will also freeze the message. + * + * @param {?boolean} replay + * + * @returns {boolean} + * + * @throws {LogicException} + */ + isReplay(replay = null) { + const msg = msgs.get(this); + + if (replay === null) { + if (msg.isReplay === null) { + msg.isReplay = false; + } + + return msg.isReplay; + } + + if (msg.isReplay === null) { + msg.isReplay = isBoolean(replay) ? replay : false; + if (msg.isReplay) { + this.freeze(); + } + + return msg.isReplay; + } + + throw new LogicException('You can only set the replay mode "on" one time.'); + } + + /** + * Populates the defaults on all fields or just the fieldName provided. + * Operation will NOT overwrite any fields already set. + * + * @param {?string} fieldName + * + * @returns {Message} + */ + populateDefaults(fieldName = null) { + guardFrozenMessage(this); + + if (!isEmpty(fieldName)) { + populateDefault(this, this.schema().getField(fieldName)); + return this; + } + + this.schema().getFields().forEach(field => populateDefault(this, field)); + return this; + } + + /** + * Returns true if the field has been populated. + * + * @param {string} fieldName + * + * @returns {boolean} + */ + has(fieldName) { + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + return false; + } + + const value = msg.data.get(fieldName); + if (isArray(value) || isMap(value)) { + return !isEmpty(value); + } + + return value !== null && value !== undefined; + } + + /** + * Returns the value for the given field. If the field has not + * been set you will get a null value. + * + * @param {string} fieldName + * @param {*} defaultValue + * + * @returns {*} + */ + get(fieldName, defaultValue = null) { + if (!this.has(fieldName)) { + return defaultValue; + } + + const field = this.schema().getField(fieldName); + const msg = msgs.get(this); + + if (field.isASingleValue()) { + return msg.data.get(fieldName); + } + + if (field.isAList()) { + return [...msg.data.get(fieldName)]; + } + + // a set is stored as a Map internally but really + // is just a simple array when serialized. + if (field.isASet()) { + return Array.from(msg.data.get(fieldName).values()); + } + + // maps must return as a plain object. + const obj = {}; + msg.data.get(fieldName).forEach((v, k) => obj[k] = v); // eslint-disable-line no-return-assign + return obj; + } + + /** + * Clears the value of a field. + * + * @param {string} fieldName + * + * @returns {Message} + */ + clear(fieldName) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + const msg = msgs.get(this); + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + populateDefault(this, field); + return this; + } + + /** + * Returns true if the field has been cleared. + * + * @param {string} fieldName + * + * @returns {boolean} + */ + hasClearedField(fieldName) { + return msgs.get(this).clearedFields.has(fieldName); + } + + /** + * Returns an array of field names that have been cleared. + * + * @returns {string[]} + */ + getClearedFields() { + return Array.from(msgs.get(this).clearedFields.values()); + } + + /** + * Sets a single value field. + * + * @param {string} fieldName + * @param {*} value + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + set(fieldName, value) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isASingleValue()) { + throw new AssertionFailed(`Field [${fieldName}] must be a single value.`); + } + + if (value === null) { + return this.clear(fieldName); + } + + field.guardValue(value); + const msg = msgs.get(this); + msg.data.set(fieldName, value); + msg.clearedFields.delete(fieldName); + + return this; + } + + /** + * Returns true if the provided value is in the set of values. + * + * @param {string} fieldName + * @param {*} value + * + * @returns {boolean} + */ + isInSet(fieldName, value) { + if (!this.has(fieldName)) { + return false; + } + + /** @var {string} key */ + const key = trim(value); + if (!key.length) { + return false; + } + + return msgs.get(this).data.get(fieldName).has(key.toLowerCase()); + } + + /** + * Adds an array of unique values to an unsorted set of values. + * + * @param {string} fieldName + * @param {Array} values + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + addToSet(fieldName, values) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isASet()) { + throw new AssertionFailed(`Field [${fieldName}] must be a set.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.data.set(fieldName, new Map()); + } + + const store = msg.data.get(fieldName); + values.forEach((value) => { + /** @var {string} key */ + const key = trim(value); + if (!key.length) { + return; + } + + field.guardValue(value); + store.set(key.toLowerCase(), value); + }); + + if (store.size) { + msg.clearedFields.delete(fieldName); + } + + return this; + } + + /** + * Removes an array of values from a set. + * + * @param {string} fieldName + * @param {Array} values + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + removeFromSet(fieldName, values) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isASet()) { + throw new AssertionFailed(`Field [${fieldName}] must be a set.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.clearedFields.add(fieldName); + return this; + } + + const store = msg.data.get(fieldName); + values.forEach((value) => { + /** @var {string} key */ + const key = trim(value); + if (!key.length) { + return; + } + + store.delete(key.toLowerCase()); + }); + + if (!store.size) { + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + } + + return this; + } + + /** + * Returns true if the provided value is in the list of values. + * Uses SameValueZero for comparison. + * + * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes?v=control + * + * @param {string} fieldName + * @param {*} value + * + * @returns {boolean} + */ + isInList(fieldName, value) { + if (!this.has(fieldName)) { + return false; + } + + return msgs.get(this).data.get(fieldName).includes(value); + } + + /** + * Returns an item in a list or null if it doesn't exist. + * + * @param {string} fieldName + * @param {number} index + * @param {*} defaultValue + * + * @returns {*} + */ + getFromListAt(fieldName, index, defaultValue = null) { + if (!this.has(fieldName)) { + return defaultValue; + } + + const value = msgs.get(this).data.get(fieldName)[toSafeInteger(index)]; + return value === undefined ? defaultValue : value; + } + + /** + * Adds an array of values to an unsorted list/array (not unique). + * + * @param {string} fieldName + * @param {Array} values + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + addToList(fieldName, values) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAList()) { + throw new AssertionFailed(`Field [${fieldName}] must be a list.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.data.set(fieldName, []); + } + + const store = msg.data.get(fieldName); + values.forEach((value) => { + field.guardValue(value); + store.push(value); + }); + + if (store.length) { + msg.clearedFields.delete(fieldName); + } + + return this; + } + + /** + * Removes the element from the array at the index. + * + * @param {string} fieldName + * @param {number} index + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + removeFromListAt(fieldName, index) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAList()) { + throw new AssertionFailed(`Field [${fieldName}] must be a list.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.clearedFields.add(fieldName); + return this; + } + + const store = msg.data.get(fieldName); + store.splice(toSafeInteger(index), 1); + + if (!store.length) { + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + } + + return this; + } + + /** + * Returns true if the map contains the provided key. + * + * @param {string} fieldName + * @param {string} key + * + * @returns {boolean} + */ + isInMap(fieldName, key) { + if (!this.has(fieldName)) { + return false; + } + + return msgs.get(this).data.get(fieldName).has(key); + } + + /** + * Returns the value of a key in a map or null if it doesn't exist. + * + * @param {string} fieldName + * @param {string} key + * @param {*} defaultValue + * + * @returns {*} + */ + getFromMap(fieldName, key, defaultValue = null) { + if (!this.isInMap(fieldName, key)) { + return defaultValue; + } + + return msgs.get(this).data.get(fieldName).get(key); + } + + /** + * Adds a key/value pair to a map. + * + * @param {string} fieldName + * @param {string} key + * @param {*} value + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + addToMap(fieldName, key, value) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAMap()) { + throw new AssertionFailed(`Field [${fieldName}] must be a map.`); + } + + if (value === null) { + return this.removeFromMap(fieldName, key); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.data.set(fieldName, new Map()); + } + + const store = msg.data.get(fieldName); + field.guardValue(value); + store.set(key, value); + msg.clearedFields.delete(fieldName); + + return this; + } + + /** + * Removes a key/value pair from a map. + * + * @param {string} fieldName + * @param {string} key + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + removeFromMap(fieldName, key) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAMap()) { + throw new AssertionFailed(`Field [${fieldName}] must be a map.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.clearedFields.add(fieldName); + return this; + } + + const store = msg.data.get(fieldName); + store.delete(key); + + if (!store.size) { + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + } + + return this; + } + + /** + * @returns {string} + */ + toString() { + return JsonSerializer.serialize(this); + } + + /** + * @returns {Object} + */ + toObject() { + return ObjectSerializer.serialize(this); + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @returns {Message} + */ + clone() { + return ObjectSerializer.deserialize(ObjectSerializer.serialize(this)); + } +} diff --git a/src/MessageRef.js b/src/MessageRef.js new file mode 100644 index 0000000..a46a27f --- /dev/null +++ b/src/MessageRef.js @@ -0,0 +1,175 @@ +/* eslint-disable no-useless-escape */ + +import trim from 'lodash/trim'; +import AssertionFailed from './exceptions/AssertionFailed'; +import SchemaCurie from './SchemaCurie'; + +/** + * Regular expression pattern for matching a valid id string. + * @type {RegExp} + */ +export const VALID_ID_PATTERN = /^[\w\/\.:-]+$/; + +/** + * Represents a reference to a message. Typically used to link messages + * together via a correlator or "links". Format for a reference: + * vendor:package:category:message:id#tag (tag is optional) + */ +export default class MessageRef { + /** + * @param {SchemaCurie} curie - A curie which fully qualifies what this reference is linked to. + * @param {string} id - Identifier to the message. + * @param {?string} tag - Tag/relationship qualifier for this ref. + * NOTE: Tag is automatically normalized to a slug-formatted-string. + * + * @throws {AssertionFailed} + */ + constructor(curie, id, tag = null) { + this.curie = curie; + // note: this is left to match php lib which at one point had literal 'null' values + // in the id to account for (de)serialization failures. in some future version this + // should be removed, it's very rare. + this.id = trim(id) || 'null'; + this.tag = trim(tag) || null; + + if (!VALID_ID_PATTERN.test(this.id)) { + throw new AssertionFailed(`MessageRef.id [${this.id}] is invalid. It must match the pattern [${VALID_ID_PATTERN}].`); + } + + if (this.tag !== null) { + this.tag = this.tag.replace(/[^\w\.-]/g, '-').toLowerCase(); + } + + if (this.curie.isMixin()) { + throw new AssertionFailed('Mixins cannot be used in a MessageRef.'); + } + + Object.freeze(this); + } + + /** + * @param {string} str + * + * @returns {MessageRef} + */ + static fromString(str) { + const [ref, tag = null] = str.split('#'); + const [vendor, pkg, category, message, ...id] = ref.split(':'); + const curie = SchemaCurie.fromString(`${vendor}:${pkg}:${category}:${message}`); + return new MessageRef(curie, id.join(':'), tag); + } + + /** + * @param {string} json + * + * @returns {MessageRef} + * + * @throws {AssertionFailed} + */ + static fromJSON(json) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return MessageRef.fromObject(obj); + } + + /** + * @param {Object} obj + * + * @returns {MessageRef} + * + * @throws {AssertionFailed} + */ + static fromObject(obj = {}) { + if (obj.curie && obj.id) { + return new MessageRef(SchemaCurie.fromString(obj.curie), obj.id, obj.tag || null); + } + + throw new AssertionFailed('MessageRef is invalid.'); + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.curie; + } + + /** + * @returns {boolean} + */ + hasId() { + return this.id !== 'null'; + } + + /** + * @returns {?string} + */ + getId() { + return this.hasId() ? this.id : null; + } + + /** + * @returns {boolean} + */ + hasTag() { + return this.tag !== null; + } + + /** + * @returns {?string} + */ + getTag() { + return this.tag; + } + + /** + * @returns {string} + */ + toString() { + if (this.hasTag()) { + return `${this.curie}:${this.id}#${this.tag}`; + } + + return `${this.curie}:${this.id}`; + } + + /** + * @returns {Object} + */ + toObject() { + if (this.hasTag()) { + return { curie: this.curie.toString(), id: this.id, tag: this.tag }; + } + + return { curie: this.curie.toString(), id: this.id }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {MessageRef} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/MessageResolver.js b/src/MessageResolver.js new file mode 100644 index 0000000..cb9d2e6 --- /dev/null +++ b/src/MessageResolver.js @@ -0,0 +1,219 @@ +import MoreThanOneMessageForMixin from './exceptions/MoreThanOneMessageForMixin'; +import NoMessageForCurie from './exceptions/NoMessageForCurie'; +import NoMessageForMixin from './exceptions/NoMessageForMixin'; +import NoMessageForQName from './exceptions/NoMessageForQName'; +import NoMessageForSchemaId from './exceptions/NoMessageForSchemaId'; +import SchemaCurie from './SchemaCurie'; +import SchemaId from './SchemaId'; + +/** + * A map of all the available messages keyed by the schema resolver key + * and curies for resolution that is only major version specific. + * + * @type {Map} + */ +const messages = new Map(); + +/** + * An map of resolved messages in this request/process. + * + * @type {Map} + */ +const resolved = new Map(); + +/** + * A map of resolved lookups by mixin, keyed by the mixin id with major rev + * and optionally a package and category (for faster lookups) + * @see SchemaId.getCurieMajor + * + * @type {Map} + */ +const resolvedMixins = new Map(); + +/** + * A map of resolved lookups by qname. + * + * @type {Map} + */ +const resolvedQnames = new Map(); + +export default class MessageResolver { + /** + * Returns all of the registered messages. + * + * @returns {Message[]} + */ + static all() { + return Array.from(messages.values()); + } + + /** + * Returns the message to be used for the provided schema id. + * + * @param {SchemaId} id + * + * @returns {Message} + * + * @throws {NoMessageForSchemaId} + */ + static resolveId(id) { + const curieMajor = id.getCurieMajor(); + if (resolved.has(curieMajor)) { + return resolved.get(curieMajor); + } + + if (messages.has(curieMajor)) { + const message = messages.get(curieMajor); + resolved.set(curieMajor, message); + return message; + } + + const curie = id.getCurie().toString(); + if (messages.has(curie)) { + const message = messages.get(curie); + resolved.set(curieMajor, message); + resolved.set(curie, message); + return message; + } + + throw new NoMessageForSchemaId(id); + } + + /** + * Returns the message to be used for the provided curie. + * + * @param {SchemaCurie} curie + * + * @returns {Message} + * + * @throws {NoMessageForCurie} + */ + static resolveCurie(curie) { + const key = curie.toString(); + if (resolved.has(key)) { + return resolved.get(key); + } + + if (messages.has(key)) { + const message = messages.get(key); + resolved.set(key, message); + return message; + } + + throw new NoMessageForCurie(curie); + } + + /** + * Returns the SchemaCurie for the given SchemaQName. + * + * @param {SchemaQName} qname + * + * @returns {SchemaCurie} + * + * @throws {NoMessageForQName} + */ + static resolveQName(qname) { + const key = qname.toString(); + if (resolvedQnames.has(key)) { + return resolvedQnames.get(key); + } + + const qvendor = qname.getVendor(); + const qmessage = qname.getMessage(); + + const keys = Array.from(messages.keys()); + const l = keys.length; + for (let i = 0; i < l; i += 1) { + const [vendor, pkg, category, message] = keys[i].split(':'); + if (qvendor === vendor && qmessage === message) { + const curie = SchemaCurie.fromString(`${vendor}:${pkg}:${category}:${message}`); + resolvedQnames.set(key, curie); + return curie; + } + } + + throw new NoMessageForQName(qname); + } + + /** + * Adds a single schema and class proto to the resolver. + * @see SchemaId.getCurieMajor + * + * @param {SchemaId|string} id - A SchemaId instance, curie string or curie major string. + * @param {Message} classProto - The Message class itself, not an instance. + */ + static register(id, classProto) { + const key = id instanceof SchemaId ? id.getCurieMajor() : `${id}`; + messages.set(key, classProto); + } + + /** + * Return the one schema expected to be using the provided mixin. + * + * @param {Mixin} mixin + * @param {?string} inPackage + * @param {?string} inCategory + * + * @returns {Schema} + * + * @throws {MoreThanOneMessageForMixin} + * @throws {NoMessageForMixin} + */ + static findOneUsingMixin(mixin, inPackage = null, inCategory = null) { + const schemas = this.findAllUsingMixin(mixin, inPackage, inCategory); + if (schemas.length !== 1) { + throw new MoreThanOneMessageForMixin(mixin, schemas); + } + + return schemas[0]; + } + + /** + * Returns an array of Schemas expected to be using the provided mixin. + * + * @param {Mixin} mixin + * @param {?string} inPackage + * @param {?string} inCategory + * + * @return {Schema[]} + * + * @throws {NoMessageForMixin} + */ + static findAllUsingMixin(mixin, inPackage = null, inCategory = null) { + const mixinId = mixin.getId().getCurieMajor(); + const key = `${mixinId}${inPackage}${inCategory}`; + let schemas; + + if (!resolvedMixins.has(key)) { + const filtered = inPackage || inCategory; + schemas = []; + messages.forEach((message, id) => { + if (filtered) { + const [, pkg, category] = id.split(':'); + if (inPackage && inPackage !== pkg) { + return; + } + + if (inCategory && inCategory !== category) { + return; + } + } + + const schema = message.schema(); + if (schema.hasMixin(mixinId)) { + schemas.push(schema); + } + }); + + resolvedMixins.set(key, schemas); + } else { + schemas = resolvedMixins.get(key); + } + + if (!schemas.length) { + throw new NoMessageForMixin(mixin); + } + + return schemas; + } +} diff --git a/src/Mixin.js b/src/Mixin.js new file mode 100644 index 0000000..a3705fc --- /dev/null +++ b/src/Mixin.js @@ -0,0 +1,82 @@ +/* eslint-disable class-methods-use-this */ +import LogicException from './exceptions/LogicException'; + +/** + * We store all Mixin instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory create method to create mixins. + * + * @type {Map} + */ +const instances = new Map(); + +export default class Mixin { + constructor() { + Object.freeze(this); + } + + /** + * @returns {Mixin} + */ + static create() { + if (!instances.has(this)) { + instances.set(this, new this()); + } + + return instances.get(this); + } + + /** + * @returns {SchemaId} + */ + getId() { + throw new LogicException('You must implement "getId" in your Mixin.'); + } + + /** + * @returns {Field[]} + */ + getFields() { + return []; + } + + /** + * @returns {string} + */ + toString() { + return this.getId().toString(); + } + + /** + * @returns {Object} + */ + toObject() { + return { + id: this.getId(), + fields: this.getFields(), + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {Mixin} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/Schema.js b/src/Schema.js new file mode 100644 index 0000000..f617315 --- /dev/null +++ b/src/Schema.js @@ -0,0 +1,310 @@ +import camelCase from 'lodash/camelCase'; +import isEmpty from 'lodash/isEmpty'; +import upperFirst from 'lodash/upperFirst'; +import FieldAlreadyDefined from './exceptions/FieldAlreadyDefined'; +import FieldNotDefined from './exceptions/FieldNotDefined'; +import FieldOverrideNotCompatible from './exceptions/FieldOverrideNotCompatible'; +import MixinAlreadyAdded from './exceptions/MixinAlreadyAdded'; +import MixinNotDefined from './exceptions/MixinNotDefined'; +import Fb from './FieldBuilder'; +import SchemaId, { VALID_PATTERN } from './SchemaId'; +import StringType from './types/StringType'; + +export const PBJ_FIELD_NAME = '_schema'; + +export default class Schema { + /** + * @param {SchemaId|string} id + * @param {Message} classProto + * @param {Field[]} fields + * @param {Mixin[]} mixins + */ + constructor(id, classProto, fields = [], mixins = []) { + this.id = id instanceof SchemaId ? id : SchemaId.fromString(id); + this.classProto = classProto; + this.fields = new Map(); + this.requiredFields = new Map(); + this.mixins = new Map(); + this.mixinsByCurie = new Map(); + this.classNameMethod = camelCase(this.id.getCurie().getMessage()); + this.classNameMethodMajor = `${this.classNameMethod}V${this.id.getVersion().getMajor()}`; + this.className = upperFirst(this.classNameMethodMajor); + + this.addField( + Fb.create(PBJ_FIELD_NAME, StringType.create()) + .required() + .pattern(VALID_PATTERN) + .withDefault(this.id.toString()) + .build(), + ); + + mixins.forEach(m => this.addMixin(m)); + fields.forEach(f => this.addField(f)); + + this.mixinIds = Array.from(this.mixins.keys()); + this.mixinCuries = Array.from(this.mixinsByCurie.keys()); + + Object.freeze(this); + } + + /** + * @private + * + * @param {Field} field + * + * @throws {FieldAlreadyDefined} + * @throws {FieldOverrideNotCompatible} + */ + addField(field) { + const fieldName = field.getName(); + + if (this.hasField(fieldName)) { + const existingField = this.getField(fieldName); + if (!existingField.isOverridable()) { + throw new FieldAlreadyDefined(this, fieldName); + } + + if (!existingField.isCompatibleForOverride(field)) { + throw new FieldOverrideNotCompatible(this, fieldName, field); + } + } + + this.fields.set(fieldName, field); + if (field.isRequired()) { + this.requiredFields.set(fieldName, field); + } + } + + /** + * @private + * + * @param {Mixin} mixin + * + * @throws {MixinAlreadyAdded} + */ + addMixin(mixin) { + const id = mixin.getId(); + const curieStr = id.getCurie().toString(); + + if (this.mixinsByCurie.has(curieStr)) { + throw new MixinAlreadyAdded(this, this.mixinsByCurie.get(curieStr), mixin); + } + + this.mixins.set(id.getCurieMajor(), mixin); + this.mixinsByCurie.set(curieStr, mixin); + mixin.getFields().forEach(f => this.addField(f)); + } + + /** + * @returns {Message} + */ + getClassProto() { + return this.classProto; + } + + /** + * @returns {string} + */ + getClassName() { + return this.className; + } + + /** + * Convenience method to return the name of the method that should + * exist to handle this message. + * + * For example, an ImportUserV1 message would be handled by: + * SomeClass.importUserV1(command) + * + * @param {boolean} withMajor + * + * @returns {string} + */ + getHandlerMethodName(withMajor = false) { + return withMajor ? this.classNameMethodMajor : this.classNameMethod; + } + + /** + * @returns {SchemaId} + */ + getId() { + return this.id; + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.id.getCurie(); + } + + /** + * @returns {string} + */ + getCurieMajor() { + return this.id.getCurieMajor(); + } + + /** + * @returns {SchemaQName} + */ + getQName() { + return this.id.getCurie().getQName(); + } + + /** + * Convenience method that creates a message instance with this schema. + * + * @param {Object} data + * + * @returns {Message} + */ + createMessage(data = {}) { + if (isEmpty(data)) { + return this.classProto.create(); + } + + return this.classProto.fromObject(data); + } + + /** + * @param {string} fieldName + * + * @returns {boolean} + */ + hasField(fieldName) { + return this.fields.has(fieldName); + } + + /** + * @param {string} fieldName + * + * @returns {Field} + * + * @throws {FieldNotDefined} + */ + getField(fieldName) { + if (!this.fields.has(fieldName)) { + throw new FieldNotDefined(this, fieldName); + } + + return this.fields.get(fieldName); + } + + /** + * @returns {Field[]} + */ + getFields() { + return Array.from(this.fields.values()); + } + + /** + * @returns {Field[]} + */ + getRequiredFields() { + return Array.from(this.requiredFields.values()); + } + + /** + * Returns true if the mixin is on this schema. Id provided can be + * qualified to major rev or just the curie. + * @see SchemaId.getCurieMajor + * + * @param {string} mixinId + * + * @returns {boolean} + */ + hasMixin(mixinId) { + return this.mixins.has(mixinId) || this.mixinsByCurie.has(mixinId); + } + + /** + * @param {string} mixinId + * + * @returns {Mixin} + * + * @throws {MixinNotDefined} + */ + getMixin(mixinId) { + if (this.mixins.has(mixinId)) { + return this.mixins.get(mixinId); + } + + if (this.mixinsByCurie.has(mixinId)) { + return this.mixinsByCurie.get(mixinId); + } + + throw new MixinNotDefined(this, mixinId); + } + + /** + * @returns {Mixin[]} + */ + getMixins() { + return Array.from(this.mixins.values()); + } + + /** + * Returns an array of curies with the major rev. + * @see SchemaId.getCurieMajor + * + * @returns {string[]} + */ + getMixinIds() { + return this.mixinIds; + } + + /** + * Returns an array of curies (string version). + * + * @returns {string[]} + */ + getMixinCuries() { + return this.mixinCuries; + } + + /** + * @returns {string} + */ + toString() { + return this.id.toString(); + } + + /** + * @returns {Object} + */ + toObject() { + return { + id: this.id.toString(), + curie: this.getCurie().toString(), + curie_major: this.getCurieMajor(), + qname: this.getQName().toString(), + class_name: this.className, + mixins: this.getMixins().map(m => m.getId()), + fields: this.getFields(), + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {Schema} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/SchemaCurie.js b/src/SchemaCurie.js new file mode 100644 index 0000000..2dbaef9 --- /dev/null +++ b/src/SchemaCurie.js @@ -0,0 +1,157 @@ +/* eslint-disable no-useless-escape */ + +import InvalidSchemaCurie from './exceptions/InvalidSchemaCurie'; +import SchemaQName from './SchemaQName'; + +/** + * We store all SchemaCurie instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory from* methods to create curies. + * + * @type {Map} + */ +const instances = new Map(); + +/** + * Regular expression pattern for matching a valid SchemaCurie string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+)$/; + +/** + * Schemas can be fully qualified by the schema id (which includes the version) + * or the short form which is called a CURIE or "compact uri". + * @link http://en.wikipedia.org/wiki/CURIE + * + * Schema Curie Format: + * vendor:package:category:message + * + * @see SchemaId + * + */ +export default class SchemaCurie { + /** + * @param {string} vendor + * @param {string} pkg + * @param {?string} category + * @param {string} message + * + * @throws {InvalidSchemaCurie} + */ + constructor(vendor, pkg, category, message) { + this.vendor = vendor || ''; + this.pkg = pkg || ''; + this.category = `${category}`.trim() || null; + this.message = message || ''; + this.curie = `${this.vendor}:${this.pkg}:${this.category || ''}:${this.message}`; + + if (!VALID_PATTERN.test(this.curie)) { + throw new InvalidSchemaCurie( + `SchemaCurie [${this.curie}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + if (this.curie.length > 145) { + throw new InvalidSchemaCurie('SchemaCurie cannot be greater than 145 chars.'); + } + + this.qname = SchemaQName.fromCurie(this); + Object.freeze(this); + instances.set(this.curie, this); + } + + /** + * @param {string} curie + * + * @returns {SchemaCurie} + */ + static fromString(curie) { + const key = `${curie}`; + if (instances.has(key)) { + return instances.get(key); + } + + return new SchemaCurie(...key.split(':')); + } + + /** + * @param {SchemaId} id + * + * @returns {SchemaCurie} + */ + static fromId(id) { + return SchemaCurie.fromString(id.toString().replace(`:${id.getVersion()}`, '').substr(4)); + } + + /** + * @returns {string} + */ + getVendor() { + return this.vendor; + } + + /** + * @returns {string} + */ + getPackage() { + return this.pkg; + } + + /** + * @returns {?string} + */ + getCategory() { + return this.category; + } + + /** + * @returns {string} + */ + getMessage() { + return this.message; + } + + /** + * @returns {boolean} + */ + isMixin() { + return this.category === 'mixin'; + } + + /** + * @return {SchemaQName} + */ + getQName() { + return this.qname; + } + + /** + * @returns {string} + */ + toString() { + return this.curie; + } + + /** + * @returns {string} + */ + toJSON() { + return this.toString(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {SchemaCurie} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/SchemaId.js b/src/SchemaId.js new file mode 100644 index 0000000..eb4e131 --- /dev/null +++ b/src/SchemaId.js @@ -0,0 +1,212 @@ +/* eslint-disable no-useless-escape */ + +import InvalidSchemaId from './exceptions/InvalidSchemaId'; +import SchemaCurie from './SchemaCurie'; +import SchemaVersion from './SchemaVersion'; + +/** + * We store all SchemaId instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory from* methods to create curies. + * + * @type {Map} + */ +const instances = new Map(); + +/** + * Regular expression pattern for matching a valid SchemaId string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^pbj:([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+):([0-9]+-[0-9]+-[0-9]+)$/; + +/** + * Schemas have fully qualified names, similar to a "urn". This is combination of ideas from: + * + * Amazon Resource Names (ARNs) and AWS Service Namespaces + * @link http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + * + * SnowPlow Analytics (Iglu) + * @link http://snowplowanalytics.com/blog/2014/07/01/iglu-schema-repository-released/ + * + * @link http://en.wikipedia.org/wiki/CURIE + * + * And of course the various package managers like composer, npm, etc. + * + * Schema Id Format: + * pbj:vendor:package:category:message:version + * + * Schema Curie Format: + * vendor:package:category:message + * + * Schema Curie Major Format: + * vendor:package:category:message:v# + * + * Schema QName Format: + * vendor:message + * + * Formats: + * VENDOR: [a-z0-9-]+ + * PACKAGE: [a-z0-9\.-]+ + * CATEGORY: ([a-z0-9-]+)? + * (clarifies the intent of the message, e.g. command, request, event, response, etc.) + * + * MESSAGE: [a-z0-9-]+ + * VERSION: @see SchemaVersion VALID_PATTERN + * + * Examples of fully qualified schema ids: + * pbj:acme:videos:event:video-uploaded:1-0-0 + * pbj:acme:users:command:register-user:1-1-0 + * pbj:acme:api.videos:request:get-video:1-0-0 + * + * The fully qualified schema identifier corresponds to a json schema implementing the + * Gdbots PBJ Json Schema. + * + * The schema id must be resolveable to a class that should be able to read and write + * messages with payloads that validate using the json schema. The target class is ideally + * major revision specific. As in GetVideoV1, GetVideoV2, etc. Only "major" revisions + * should require a unique class since all other schema changes should not break anything. + * + * @see SchemaVersion + * + */ +export default class SchemaId { + /** + * @param {string} vendor + * @param {string} pkg + * @param {?string} category + * @param {string} message + * @param {string} version + * + * @throws {InvalidSchemaId} + */ + constructor(vendor, pkg, category, message, version) { + this.vendor = vendor || ''; + this.pkg = pkg || ''; + this.category = `${category}`.trim() || null; + this.message = message || ''; + this.version = SchemaVersion.fromString(version); + this.id = `pbj:${this.vendor}:${this.pkg}:${this.category || ''}:${this.message}:${this.version}`; + + if (!VALID_PATTERN.test(this.id)) { + throw new InvalidSchemaId( + `SchemaId [${this.id}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + if (this.id.length > 150) { + throw new InvalidSchemaId('SchemaId cannot be greater than 150 chars.'); + } + + this.curie = SchemaCurie.fromId(this); + Object.freeze(this); + instances.set(this.id, this); + } + + /** + * @param {string} id + * + * @returns {SchemaId} + */ + static fromString(id) { + const key = `${id}`; + if (instances.has(key)) { + return instances.get(key); + } + + return new SchemaId(...key.substr(4).split(':')); + } + + /** + * @returns {string} + */ + getVendor() { + return this.vendor; + } + + /** + * @returns {string} + */ + getPackage() { + return this.pkg; + } + + /** + * @returns {?string} + */ + getCategory() { + return this.category; + } + + /** + * @returns {string} + */ + getMessage() { + return this.message; + } + + /** + * @returns {SchemaVersion} + */ + getVersion() { + return this.version; + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.curie; + } + + /** + * Returns the major version qualified curie. This should be used by the MessageResolver, + * event dispatchers, etc. where consumers will need to be able to reliably type hint or + * locate classes and provide functionality for a given message, with the expectation + * that a major revision is likely not compatible with another major revision of the + * same message. + * + * e.g. "vendor:package:category:message:v1" + * + * @returns {string} + */ + getCurieMajor() { + return `${this.curie}:v${this.version.getMajor()}`; + } + + /** + * @return {SchemaQName} + */ + getQName() { + return this.curie.getQName(); + } + + /** + * @returns {string} + */ + toString() { + return this.id; + } + + /** + * @returns {string} + */ + toJSON() { + return this.toString(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {SchemaId} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/SchemaQName.js b/src/SchemaQName.js new file mode 100644 index 0000000..46bedaa --- /dev/null +++ b/src/SchemaQName.js @@ -0,0 +1,126 @@ +import InvalidSchemaQName from './exceptions/InvalidSchemaQName'; + +/** + * We store all SchemaQName instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory from* methods to create qnames. + * + * @type {Map} + */ +const instances = new Map(); + +/** + * Regular expression pattern for matching a valid SchemaQName string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9-]+)$/; + +/** + * Schemas can be referenced in an extremely compact manner using a QName. + * This is NOT 100% reliably unique as the larger your app is the more likely the + * same message name will be duplicated in another service. + * @link https://en.wikipedia.org/wiki/QName + * + * Schema QName Format: + * vendor:message + * + * @see SchemaId + * @see SchemaCurie + * + */ +export default class SchemaQName { + /** + * @param {string} vendor + * @param {string} message + * + * @throws {InvalidSchemaVersion} + */ + constructor(vendor, message) { + this.vendor = vendor || ''; + this.message = message || ''; + this.qname = `${this.vendor}:${this.message}`; + + if (!VALID_PATTERN.test(this.qname)) { + throw new InvalidSchemaQName( + `SchemaQName [${this.qname}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + Object.freeze(this); + instances.set(this.qname, this); + } + + /** + * @param {string} qname - A valid SchemaQName as a string, e.g. vendor:message + * + * @returns {SchemaQName} + * + * @throws {InvalidSchemaQName} + */ + static fromString(qname) { + const key = `${qname}`; + if (instances.has(key)) { + return instances.get(key); + } + + return new SchemaQName(...key.split(':')); + } + + /** + * @param {SchemaId} id + * + * @returns {SchemaQName} + */ + static fromId(id) { + return SchemaQName.fromCurie(id.getCurie()); + } + + /** + * @param {SchemaCurie} curie + * + * @returns {SchemaQName} + */ + static fromCurie(curie) { + const qname = `${curie.getVendor()}:${curie.getMessage()}`; + if (instances.has(qname)) { + return instances.get(qname); + } + + return new SchemaQName(curie.getVendor(), curie.getMessage()); + } + + /** + * @returns {string} + */ + toString() { + return this.qname; + } + + /** + * @returns {string} + */ + toJSON() { + return this.qname; + } + + /** + * @returns {string} + */ + valueOf() { + return this.qname; + } + + /** + * @returns {string} + */ + getVendor() { + return this.vendor; + } + + /** + * @returns {string} + */ + getMessage() { + return this.message; + } +} diff --git a/src/SchemaVersion.js b/src/SchemaVersion.js new file mode 100644 index 0000000..4eeb6d9 --- /dev/null +++ b/src/SchemaVersion.js @@ -0,0 +1,115 @@ +import toInteger from 'lodash/toInteger'; +import InvalidSchemaVersion from './exceptions/InvalidSchemaVersion'; + +/** + * Regular expression pattern for matching a valid SchemaVersion string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^([0-9]+)-([0-9]+)-([0-9]+)$/; + +/** + * Similar to semantic versioning but with dashes and no "alpha, beta, etc." qualifiers. + * + * E.g. 1-0-0 (major-minor-patch) + * + * MAJOR + * Is incremented when a change is made which breaks the rules of Protobuf/Thrift backward + * compatibility, such as changing the type of a field. + * + * MINOR + * Is a change which is backward compatible but not forward compatible. Records created from + * the old version of the schema can be deserialized using the new schema, but not the other way + * around. Example: adding a new field to a union type. + * + * PATCH + * Is a change which is both backward compatible and forward compatible. The previous version of + * the schema can be used to deserialize records created from the new version of the schema, and + * vice versa. Example: adding a new optional field. + * + * @link http://semver.org/ + * @link http://snowplowanalytics.com/blog/2014/05/13/introducing-schemaver-for-semantic-versioning-of-schemas/ + * + */ +export default class SchemaVersion { + /** + * @param {number} major + * @param {number} minor + * @param {number} patch + * + * @throws {InvalidSchemaVersion} + */ + constructor(major = 1, minor = 0, patch = 0) { + this.major = toInteger(major); + this.minor = toInteger(minor); + this.patch = toInteger(patch); + this.version = `${major}-${minor}-${patch}`; + + if (!VALID_PATTERN.test(this.version)) { + throw new InvalidSchemaVersion( + `SchemaVersion [${this.version}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + Object.freeze(this); + } + + /** + * @param {string} version - A valid SchemaVersion as a string, e.g. 1-0-0 + * + * @returns {SchemaVersion} + * + * @throws {InvalidSchemaVersion} + */ + static fromString(version = '1-0-0') { + const matches = `${version}`.match(VALID_PATTERN); + if (matches === null) { + throw new InvalidSchemaVersion( + `SchemaVersion [${version}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + return new SchemaVersion(matches[1], matches[2], matches[3]); + } + + /** + * @returns {string} + */ + toString() { + return this.version; + } + + /** + * @returns {string} + */ + toJSON() { + return this.version; + } + + /** + * @returns {string} + */ + valueOf() { + return this.version; + } + + /** + * @returns {number} + */ + getMajor() { + return this.major; + } + + /** + * @returns {number} + */ + getMinor() { + return this.minor; + } + + /** + * @returns {number} + */ + getPatch() { + return this.patch; + } +} diff --git a/src/codec.js b/src/codec.js deleted file mode 100644 index 0459920..0000000 --- a/src/codec.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -import DynamicField from 'gdbots/pbj/well-known/dynamic-field'; -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import Message from 'gdbots/pbj/message'; -import MessageRef from 'gdbots/pbj/message-ref'; - -export default class Codec -{ - /** - * @param Message message - * @param Field field - * - * @return mixed - */ - encodeMessage(message, field) { - throw message.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return Message - */ - decodeMessage(value, field) { - return Message.fromArray(value); - } - - /** - * @param MessageRef messageRef - * @param Field field - * - * @return mixed - */ - encodeMessageRef(messageRef, field) { - return messageRef.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return MessageRef - */ - decodeMessageRef(value, field) { - return MessageRef.fromArray(value); - } - - /** - * @param GeoPoint geoPoint - * @param Field field - * - * @return mixed - */ - encodeGeoPoint(geoPoint, field) { - return [geoPoint.getLongitude(), geoPoint.getLatitude()]; - } - - /** - * @param mixed value - * @param Field field - * - * @return GeoPoint - */ - decodeGeoPoint(value, field) { - return new GeoPoint(value[1], value[0]); - } - - /** - * @param DynamicField dynamicField - * @param Field field - * - * @return mixed - */ - encodeDynamicField(dynamicField, field) { - dynamicField.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return DynamicField - */ - decodeDynamicField(value, field) { - DynamicField.fromArray(value); - } -} diff --git a/src/enum/dynamic-field-kind.js b/src/enum/dynamic-field-kind.js deleted file mode 100644 index a73b9bc..0000000 --- a/src/enum/dynamic-field-kind.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static DynamicFieldKind BOOL_VAL() - * @method static DynamicFieldKind DATE_VAL() - * @method static DynamicFieldKind FLOAT_VAL() - * @method static DynamicFieldKind INT_VAL() - * @method static DynamicFieldKind STRING_VAL() - * @method static DynamicFieldKind TEXT_VAL() - */ -export default class DynamicFieldKind extends SystemUtils.mixinClass(Enum) {} - -DynamicFieldKind.initEnum({ - BOOL_VAL: 'bool_val', - DATE_VAL: 'date_val', - FLOAT_VAL: 'float_val', - INT_VAL: 'int_val', - STRING_VAL: 'string_val', - TEXT_VAL: 'text_val' -}); diff --git a/src/enum/field-rule.js b/src/enum/field-rule.js deleted file mode 100644 index 28a5d57..0000000 --- a/src/enum/field-rule.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static FieldRule A_SINGLE_VALUE() - * @method static FieldRule A_SET() - * @method static FieldRule A_LIST() - * @method static FieldRule A_MAP() - */ -export default class FieldRule extends SystemUtils.mixinClass(Enum) {} - -FieldRule.initEnum({ - A_SINGLE_VALUE: 1, - A_SET: 2, - A_LIST: 3, - A_MAP: 4 -}); diff --git a/src/enum/format.js b/src/enum/format.js deleted file mode 100644 index 49d570c..0000000 --- a/src/enum/format.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#format - * - * @method static Format UNKNOWN() - * @method static Format DATE() - * @method static Format DATE_TIME() - * @method static Format EMAIL() - * @method static Format HASHTAG() - * @method static Format HOSTNAME() - * @method static Format IPV4() - * @method static Format IPV6() - * @method static Format SLUG() - * @method static Format URI() - * @method static Format URL() - * @method static Format UUID() - */ -export default class Format extends SystemUtils.mixinClass(Enum) {} - -Format.initEnum({ - UNKNOWN: 'unknown', - DATE: 'date', - DATE_TIME: 'date-time', - EMAIL: 'email', - HASHTAG: 'hashtag', - HOSTNAME: 'hostname', - IPV4: 'ipv4', - IPV6: 'ipv6', - SLUG: 'slug', - URI: 'uri', - URL: 'url', - UUID: 'uuid' -}); diff --git a/src/enum/type-name.js b/src/enum/type-name.js deleted file mode 100644 index 962fff4..0000000 --- a/src/enum/type-name.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static TypeName BIG_INT() - * @method static TypeName BINARY() - * @method static TypeName BLOB() - * @method static TypeName BOOLEAN() - * @method static TypeName DATE() - * @method static TypeName DATE_TIME() - * @method static TypeName DECIMAL() - * @method static TypeName DYNAMIC_FIELD() - * @method static TypeName FLOAT() - * @method static TypeName GEO_POINT() - * @method static TypeName IDENTIFIER() - * @method static TypeName INT() - * @method static TypeName INT_ENUM() - * @method static TypeName MEDIUM_BLOB() - * @method static TypeName MEDIUM_INT() - * @method static TypeName MEDIUM_TEXT() - * @method static TypeName MESSAGE() - * @method static TypeName MESSAGE_REF() - * @method static TypeName MICROTIME() - * @method static TypeName SIGNED_BIG_INT() - * @method static TypeName SIGNED_INT() - * @method static TypeName SIGNED_MEDIUM_INT() - * @method static TypeName SIGNED_SMALL_INT() - * @method static TypeName SIGNED_TINY_INT() - * @method static TypeName SMALL_INT() - * @method static TypeName STRING() - * @method static TypeName STRING_ENUM() - * @method static TypeName TEXT() - * @method static TypeName TIME_UUID() - * @method static TypeName TIMESTAMP() - * @method static TypeName TINY_INT() - * @method static TypeName TRINARY() - * @method static TypeName UUID() - */ -export default class TypeName extends SystemUtils.mixinClass(Enum) {} - -TypeName.initEnum({ - BIG_INT: 'big-int', - BINARY: 'binary', - BLOB: 'blob', - BOOLEAN: 'boolean', - DATE: 'date', - DATE_TIME: 'date-time', - DECIMAL: 'decimal', - DYNAMIC_FIELD: 'dynamic-field', - FLOAT: 'float', - GEO_POINT: 'geo-point', - IDENTIFIER: 'identifier', - INT: 'int', - INT_ENUM: 'int-enum', - MEDIUM_BLOB: 'medium-blob', - MEDIUM_INT: 'medium-int', - MEDIUM_TEXT: 'medium-text', - MESSAGE: 'message', - MESSAGE_REF: 'message-ref', - MICROTIME: 'microtime', - SIGNED_BIG_INT: 'signed-big-int', - SIGNED_INT: 'signed-int', - SIGNED_MEDIUM_INT: 'signed-medium-int', - SIGNED_SMALL_INT: 'signed-small-int', - SIGNED_TINY_INT: 'signed-tiny-int', - SMALL_INT: 'small-int', - STRING: 'string', - STRING_ENUM: 'string-enum', - TEXT: 'text', - TIME_UUID: 'time-uuid', - TIMESTAMP: 'timestamp', - TINY_INT: 'tiny-int', - TRINARY: 'trinary', - UUID: 'uuid' -}); diff --git a/src/enums/DynamicFieldKind.js b/src/enums/DynamicFieldKind.js new file mode 100644 index 0000000..df09e6a --- /dev/null +++ b/src/enums/DynamicFieldKind.js @@ -0,0 +1,13 @@ +import Enum from '@gdbots/common/Enum'; + +export default class DynamicFieldKind extends Enum { +} + +DynamicFieldKind.configure({ + BOOL_VAL: 'bool_val', + DATE_VAL: 'date_val', + FLOAT_VAL: 'float_val', + INT_VAL: 'int_val', + STRING_VAL: 'string_val', + TEXT_VAL: 'text_val', +}, 'gdbots:pbj:dynamic-field-kind'); diff --git a/src/enums/FieldRule.js b/src/enums/FieldRule.js new file mode 100644 index 0000000..46223fa --- /dev/null +++ b/src/enums/FieldRule.js @@ -0,0 +1,11 @@ +import Enum from '@gdbots/common/Enum'; + +export default class FieldRule extends Enum { +} + +FieldRule.configure({ + A_SINGLE_VALUE: 1, + A_SET: 2, + A_LIST: 3, + A_MAP: 4, +}, 'gdbots:pbj:field-rule'); diff --git a/src/enums/Format.js b/src/enums/Format.js new file mode 100644 index 0000000..9417dc0 --- /dev/null +++ b/src/enums/Format.js @@ -0,0 +1,22 @@ +import Enum from '@gdbots/common/Enum'; + +/** + * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#format + */ +export default class Format extends Enum { +} + +Format.configure({ + UNKNOWN: 'unknown', + DATE: 'date', + DATE_TIME: 'date-time', + EMAIL: 'email', + HASHTAG: 'hashtag', + HOSTNAME: 'hostname', + IPV4: 'ipv4', + IPV6: 'ipv6', + SLUG: 'slug', + URI: 'uri', + URL: 'url', + UUID: 'uuid', +}, 'gdbots:pbj:format'); diff --git a/src/enums/TypeName.js b/src/enums/TypeName.js new file mode 100644 index 0000000..23bcac2 --- /dev/null +++ b/src/enums/TypeName.js @@ -0,0 +1,40 @@ +import Enum from '@gdbots/common/Enum'; + +export default class TypeName extends Enum { +} + +TypeName.configure({ + BIG_INT: 'big-int', + BINARY: 'binary', + BLOB: 'blob', + BOOLEAN: 'boolean', + DATE: 'date', + DATE_TIME: 'date-time', + DECIMAL: 'decimal', + DYNAMIC_FIELD: 'dynamic-field', + FLOAT: 'float', + GEO_POINT: 'geo-point', + IDENTIFIER: 'identifier', + INT: 'int', + INT_ENUM: 'int-enum', + MEDIUM_BLOB: 'medium-blob', + MEDIUM_INT: 'medium-int', + MEDIUM_TEXT: 'medium-text', + MESSAGE: 'message', + MESSAGE_REF: 'message-ref', + MICROTIME: 'microtime', + SIGNED_BIG_INT: 'signed-big-int', + SIGNED_INT: 'signed-int', + SIGNED_MEDIUM_INT: 'signed-medium-int', + SIGNED_SMALL_INT: 'signed-small-int', + SIGNED_TINY_INT: 'signed-tiny-int', + SMALL_INT: 'small-int', + STRING: 'string', + STRING_ENUM: 'string-enum', + TEXT: 'text', + TIME_UUID: 'time-uuid', + TIMESTAMP: 'timestamp', + TINY_INT: 'tiny-int', + TRINARY: 'trinary', + UUID: 'uuid', +}, 'gdbots:pbj:type-name'); diff --git a/src/exception/decode-value-failed.js b/src/exception/decode-value-failed.js deleted file mode 100644 index f82e222..0000000 --- a/src/exception/decode-value-failed.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class DecodeValueFailed extends SystemUtils.mixinClass(InvalidArgumentException) -{ - /** - * @param mixed value - * @param Field field - * @param string message - */ - constructor(value, field, message) { - let str = '' + value; - if ('object' === typeof value) { - str = value.toString(); - } - - super('Failed to decode [' + str + '] for field [' + field.getName() + '] to a [' + field.getType().getTypeValue() + ']. Detail: ' + message + '.'); - - privateProps.set(this, { - /** @var mixed */ - value: value, - - /** @var Field */ - field: field - }); - } - - /** - * @return mixed - */ - getValue() { - return privateProps.get(this).value; - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/deserialize-message-failed.js b/src/exception/deserialize-message-failed.js deleted file mode 100644 index b0985c4..0000000 --- a/src/exception/deserialize-message-failed.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class DeserializeMessageFailed extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exception/encode-value-failed.js b/src/exception/encode-value-failed.js deleted file mode 100644 index 3c4d943..0000000 --- a/src/exception/encode-value-failed.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class EncodeValueFailed extends SystemUtils.mixinClass(InvalidArgumentException) -{ - /** - * @param mixed value - * @param Field field - * @param string message - */ - constructor(value, field, message) { - let str = '' + value; - if ('object' === typeof value) { - str = value.toString(); - } - - super('Failed to encode [' + str + '] for field [' + field.getName() + '] to a [' + field.getType().getTypeValue() + ']. Detail: ' + message + '.'); - - privateProps.set(this, { - /** @var mixed */ - value: value, - - /** @var Field */ - field: field - }); - } - - /** - * @return mixed - */ - getValue() { - return privateProps.get(this).value; - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/field-already-defined.js b/src/exception/field-already-defined.js deleted file mode 100644 index cc78f5a..0000000 --- a/src/exception/field-already-defined.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldAlreadyDefined extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string fieldName - */ - constructor(schema, fieldName) { - let field = schema.getField(fieldName); - - super('Field [' + field.getName() + '] is already defined on message [' + schema.getClassName() + '] and is not overridable.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var Field */ - field: field - }); - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/field-not-defined.js b/src/exception/field-not-defined.js deleted file mode 100644 index c4d16c2..0000000 --- a/src/exception/field-not-defined.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldNotDefined extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string fieldName - */ - constructor(schema, fieldName) { - super('Field [' + fieldName + '] is not defined on message [' + schema.getClassName() + '].'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var string */ - fieldName: fieldName - }); - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).fieldName; - } -} diff --git a/src/exception/field-override-not-compatible.js b/src/exception/field-override-not-compatible.js deleted file mode 100644 index 0122e8c..0000000 --- a/src/exception/field-override-not-compatible.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldOverrideNotCompatible extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string fieldName - * @param Field overrideField - */ - constructor(schema, fieldName, overrideField) { - let existingField = schema.getField(fieldName); - - super('Field [' + existingField.getName() + '] override for [' + schema.getClassName() + '] is not compatible. Name, Type, Rule and Required must match.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var Field */ - existingField: existingField, - - /** @var Field */ - overrideField: overrideField - }); - } - - /** - * @return Field - */ - getExistingField() { - return privateProps.get(this).existingField; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).existingField.getName(); - } - - /** - * @return Field - */ - getOverrideField() { - return privateProps.get(this).overrideField; - } -} diff --git a/src/exception/frozen-message-is-immutable.js b/src/exception/frozen-message-is-immutable.js deleted file mode 100644 index 586ad25..0000000 --- a/src/exception/frozen-message-is-immutable.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FrozenMessageIsImmutable extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Message type - */ - constructor(type) { - super('Message is frozen and cannot be modified.'); - - privateProps.set(this, { - /** @var Message */ - type: type - }); - } - - /** - * @return Message - */ - getType() { - return privateProps.get(this).type; - } -} diff --git a/src/exception/gdbots-pbj-exception.js b/src/exception/gdbots-pbj-exception.js deleted file mode 100644 index 62a1a96..0000000 --- a/src/exception/gdbots-pbj-exception.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; - -export default class GdbotsPbjException extends SystemUtils.mixinClass(Error) {} diff --git a/src/exception/invalid-argument-exception.js b/src/exception/invalid-argument-exception.js deleted file mode 100644 index 4f1a926..0000000 --- a/src/exception/invalid-argument-exception.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class InvalidArgumentException extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exception/invalid-resolved-schema.js b/src/exception/invalid-resolved-schema.js deleted file mode 100644 index 387f58f..0000000 --- a/src/exception/invalid-resolved-schema.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class InvalidResolvedSchema extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param SchemaId resolvedSchemaId - * @param string resolvedClassName - */ - constructor(schema, resolvedSchemaId, resolvedClassName) { - super('Schema id [' + resolvedSchemaId.toString() + '] with curie [' + resolvedSchemaId.getCurieMajor() + '] was resolved to [' + resolvedClassName + '] but that message has a curie of [' + schema.getId().getCurieMajor() + ']. They must match.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var SchemaId */ - resolvedSchemaId: resolvedSchemaId, - - /** @var string */ - resolvedClassName: resolvedClassName - }); - } - - /** - * @return SchemaId - */ - getResolvedSchemaId() { - return privateProps.get(this).resolvedSchemaId; - } - - /** - * @return string - */ - getResolvedClassName() { - return privateProps.get(this).resolvedClassName; - } -} diff --git a/src/exception/invalid-schema-curie.js b/src/exception/invalid-schema-curie.js deleted file mode 100644 index 8699dca..0000000 --- a/src/exception/invalid-schema-curie.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaCurie extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/invalid-schema-id.js b/src/exception/invalid-schema-id.js deleted file mode 100644 index cc0f1bf..0000000 --- a/src/exception/invalid-schema-id.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaId extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/invalid-schema-q-name.js b/src/exception/invalid-schema-q-name.js deleted file mode 100644 index e44fbf9..0000000 --- a/src/exception/invalid-schema-q-name.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaQName extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/invalid-schema-version.js b/src/exception/invalid-schema-version.js deleted file mode 100644 index 135963d..0000000 --- a/src/exception/invalid-schema-version.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaVersion extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/logic-exception.js b/src/exception/logic-exception.js deleted file mode 100644 index adc72c0..0000000 --- a/src/exception/logic-exception.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class LogicException extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exception/mixin-already-added.js b/src/exception/mixin-already-added.js deleted file mode 100644 index f20cca2..0000000 --- a/src/exception/mixin-already-added.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class MixinAlreadyAdded extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param Mixin originalMixin - * @param Mixin duplicateMixin - */ - constructor(schema, originalMixin, duplicateMixin) { - super('Mixin with id [' + duplicateMixin.getId().toString() + '] was already added from [' + originalMixin.getId().toString() + '] to message [' + schema.getClassName() + ']. You cannot add multiple versions of the same mixin.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var Mixin */ - originalMixin: originalMixin, - - /** @var Mixin */ - duplicateMixin: duplicateMixin - }); - } - - /** - * @return Mixin - */ - getOriginalMixin() { - return privateProps.get(this).originalMixin; - } - - /** - * @return Mixin - */ - getDuplicateMixin() { - return privateProps.get(this).duplicateMixin; - } -} diff --git a/src/exception/mixin-not-defined.js b/src/exception/mixin-not-defined.js deleted file mode 100644 index ce5a21b..0000000 --- a/src/exception/mixin-not-defined.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class MixinNotDefined extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string mixinId - */ - constructor(schema, mixinId) { - super('Mixin [' + mixinId + '] is not defined on message [' + schema.getClassName() + '].'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var string */ - mixinId: mixinId - }); - } - - /** - * @return string - */ - getMixinId() { - return privateProps.get(this).mixinId; - } -} diff --git a/src/exception/more-than-one-message-for-mixin.js b/src/exception/more-than-one-message-for-mixin.js deleted file mode 100644 index 2c9c44d..0000000 --- a/src/exception/more-than-one-message-for-mixin.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class MoreThanOneMessageForMixin extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Mixin mixin - * @param Message[] messages - */ - constructor(mixin, messages) { - let ids = messages.map(function(message) { - let schema = message.schema(); - return schema.getId().toString() + ' => ' + schema.getClassName(); - }); - - super('MessageResolver returned multiple messages using [' + mixin.getId().getCurieMajor() + '] when one was expected. Messages found: ' + "\n" + ids.join("\n")); - - privateProps.set(this, { - /** @var Mixin */ - mixin: mixin, - - /** @var Message[] */ - messages: messages - }); - } - - /** - * @return Mixin - */ - getMixin() { - return privateProps.get(this).mixin; - } - - /** - * @return Message[] - */ - getMessage() { - return privateProps.get(this).messages; - } -} diff --git a/src/exception/no-message-for-curie.js b/src/exception/no-message-for-curie.js deleted file mode 100644 index 771d8ab..0000000 --- a/src/exception/no-message-for-curie.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class NoMessageForCurie extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param SchemaCurie curie - */ - constructor(curie) { - super('MessageResolver is unable to resolve [' + curie.toString() + '] to a message.'); - - privateProps.set(this, { - /** @var SchemaCurie */ - curie: curie - }); - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).curie; - } -} diff --git a/src/exception/no-message-for-mixin.js b/src/exception/no-message-for-mixin.js deleted file mode 100644 index 3527d1f..0000000 --- a/src/exception/no-message-for-mixin.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class NoMessageForMixin extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Mixin mixin - */ - constructor(mixin) { - super('MessageResolver is unable to find any messages using [' + mixin.getId().getCurieMajor() + '].'); - - privateProps.set(this, { - /** @var Mixin */ - mixin: mixin - }); - } - - /** - * @return Mixin - */ - getMixin() { - return privateProps.get(this).mixin; - } -} diff --git a/src/exception/no-message-for-schema-id.js b/src/exception/no-message-for-schema-id.js deleted file mode 100644 index 623a339..0000000 --- a/src/exception/no-message-for-schema-id.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class NoMessageForSchemaId extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param SchemaId schemaId - */ - constructor(schemaId) { - super('MessageResolver is unable to resolve schema id [' + schemaId.toString() + '] using curie [' + schemaId.getCurieMajor() + '] to a message.'); - - privateProps.set(this, { - /** @var SchemaId */ - schemaId: schemaId - }); - } - - /** - * @return SchemaId - */ - getSchemaId() { - return privateProps.get(this).schemaId; - } -} diff --git a/src/exception/required-field-not-set.js b/src/exception/required-field-not-set.js deleted file mode 100644 index 6aa0ee2..0000000 --- a/src/exception/required-field-not-set.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class RequiredFieldNotSet extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Message type - * @param Field field - */ - constructor(type, field) { - super('Required field [' + field.getName() + '] must be set on message [' + type.constructor.schema().getClassName() + '].'); - - privateProps.set(this, { - /** @var Message */ - type: type, - - /** @var Schema */ - schema: type.constructor.schema(), - - /** @var Field */ - field: field - }); - } - - /** - * @return Message - */ - getType() { - return privateProps.get(this).type; - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/schema-not-defined.js b/src/exception/schema-not-defined.js deleted file mode 100644 index f8e2f9a..0000000 --- a/src/exception/schema-not-defined.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class SchemaNotDefined extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exceptions/AssertionFailed.js b/src/exceptions/AssertionFailed.js new file mode 100644 index 0000000..3c91c14 --- /dev/null +++ b/src/exceptions/AssertionFailed.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class AssertionFailed extends InvalidArgumentException { +} diff --git a/src/exceptions/DecodeValueFailed.js b/src/exceptions/DecodeValueFailed.js new file mode 100644 index 0000000..8e689d3 --- /dev/null +++ b/src/exceptions/DecodeValueFailed.js @@ -0,0 +1,36 @@ +import truncate from 'lodash/truncate'; +import InvalidArgumentException from './InvalidArgumentException'; + +export default class DecodeValueFailed extends InvalidArgumentException { + /** + * @param {*} value + * @param {Field} field + * @param {string} message + */ + constructor(value, field, message) { + super(`Failed to decode [${truncate(value)}] for field [${field.getName()}] to a [${field.getType().getTypeName()}]. ${message}`); + this.value = value; + this.field = field; + } + + /** + * @returns {*} + */ + getValue() { + return this.value; + } + + /** + * @returns {Field} + */ + getField() { + return this.field; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.field.getName(); + } +} diff --git a/src/exceptions/FieldAlreadyDefined.js b/src/exceptions/FieldAlreadyDefined.js new file mode 100644 index 0000000..5b48f4e --- /dev/null +++ b/src/exceptions/FieldAlreadyDefined.js @@ -0,0 +1,27 @@ +import SchemaException from './SchemaException'; + +export default class FieldAlreadyDefined extends SchemaException { + /** + * @param {Schema} schema + * @param {string} fieldName + */ + constructor(schema, fieldName) { + super(`Field [${fieldName}] is already defined on message [${schema.getId()}] and is not overridable.`); + this.schema = schema; + this.field = this.schema.getField(fieldName); + } + + /** + * @returns {Field} + */ + getField() { + return this.field; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.field.getName(); + } +} diff --git a/src/exceptions/FieldNotDefined.js b/src/exceptions/FieldNotDefined.js new file mode 100644 index 0000000..b77cf27 --- /dev/null +++ b/src/exceptions/FieldNotDefined.js @@ -0,0 +1,20 @@ +import SchemaException from './SchemaException'; + +export default class FieldNotDefined extends SchemaException { + /** + * @param {Schema} schema + * @param {string} fieldName + */ + constructor(schema, fieldName) { + super(`Field [${fieldName}] is not defined on message [${schema.getId()}].`); + this.schema = schema; + this.fieldName = fieldName; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.fieldName; + } +} diff --git a/src/exceptions/FieldOverrideNotCompatible.js b/src/exceptions/FieldOverrideNotCompatible.js new file mode 100644 index 0000000..61bce8e --- /dev/null +++ b/src/exceptions/FieldOverrideNotCompatible.js @@ -0,0 +1,36 @@ +import SchemaException from './SchemaException'; + +export default class FieldOverrideNotCompatible extends SchemaException { + /** + * @param {Schema} schema + * @param {string} fieldName + * @param {Field} overrideField + */ + constructor(schema, fieldName, overrideField) { + super(`Field [${fieldName}] override for [${schema.getId()}] is not compatible. Name, Type, Rule and Required must match.`); + this.schema = schema; + this.existingField = this.schema.getField(fieldName); + this.overrideField = overrideField; + } + + /** + * @returns {Field} + */ + getExistingField() { + return this.existingField; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.existingField.getName(); + } + + /** + * @returns {Field} + */ + getOverrideField() { + return this.overrideField; + } +} diff --git a/src/exceptions/FrozenMessageIsImmutable.js b/src/exceptions/FrozenMessageIsImmutable.js new file mode 100644 index 0000000..9ec9f02 --- /dev/null +++ b/src/exceptions/FrozenMessageIsImmutable.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class FrozenMessageIsImmutable extends LogicException { + /** + * @param {Message} pbj + */ + constructor(pbj) { + super('Message is frozen and cannot be modified.'); + this.pbj = pbj; + } + + /** + * @returns {Message} + */ + getPbj() { + return this.pbj; + } +} diff --git a/src/exceptions/GdbotsPbjException.js b/src/exceptions/GdbotsPbjException.js new file mode 100644 index 0000000..f9aced0 --- /dev/null +++ b/src/exceptions/GdbotsPbjException.js @@ -0,0 +1,4 @@ +import Exception from '@gdbots/common/Exception'; + +export default class GdbotsPbjException extends Exception { +} diff --git a/src/exceptions/InvalidArgumentException.js b/src/exceptions/InvalidArgumentException.js new file mode 100644 index 0000000..a09b71e --- /dev/null +++ b/src/exceptions/InvalidArgumentException.js @@ -0,0 +1,12 @@ +import GdbotsPbjException from './GdbotsPbjException'; + +export default class InvalidArgumentException extends GdbotsPbjException { + /** + * @param {string} message + */ + constructor(message) { + // 3 = INVALID_ARGUMENT + // @link https://github.com/gdbots/schemas/blob/master/schemas/gdbots/pbjx/enums.xml#L12 + super(message, 3); + } +} diff --git a/src/exceptions/InvalidResolvedSchema.js b/src/exceptions/InvalidResolvedSchema.js new file mode 100644 index 0000000..42f00fb --- /dev/null +++ b/src/exceptions/InvalidResolvedSchema.js @@ -0,0 +1,20 @@ +import SchemaException from './SchemaException'; + +export default class InvalidResolvedSchema extends SchemaException { + /** + * @param {Schema} schema + * @param {SchemaId} resolvedSchemaId + */ + constructor(schema, resolvedSchemaId) { + super(`Schema id [${resolvedSchemaId}] was resolved to [${schema.getCurieMajor()}]. Curie majors must match.`); + this.schema = schema; + this.resolvedSchemaId = resolvedSchemaId; + } + + /** + * @returns {SchemaId} + */ + getResolvedSchemaId() { + return this.resolvedSchemaId; + } +} diff --git a/src/exceptions/InvalidSchemaCurie.js b/src/exceptions/InvalidSchemaCurie.js new file mode 100644 index 0000000..da8b5ff --- /dev/null +++ b/src/exceptions/InvalidSchemaCurie.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaCurie extends InvalidArgumentException { +} diff --git a/src/exceptions/InvalidSchemaId.js b/src/exceptions/InvalidSchemaId.js new file mode 100644 index 0000000..9cdaf42 --- /dev/null +++ b/src/exceptions/InvalidSchemaId.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaId extends InvalidArgumentException { +} diff --git a/src/exceptions/InvalidSchemaQName.js b/src/exceptions/InvalidSchemaQName.js new file mode 100644 index 0000000..cb1a2cb --- /dev/null +++ b/src/exceptions/InvalidSchemaQName.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaQName extends InvalidArgumentException { +} diff --git a/src/exceptions/InvalidSchemaVersion.js b/src/exceptions/InvalidSchemaVersion.js new file mode 100644 index 0000000..a4bab55 --- /dev/null +++ b/src/exceptions/InvalidSchemaVersion.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaVersion extends InvalidArgumentException { +} diff --git a/src/exceptions/LogicException.js b/src/exceptions/LogicException.js new file mode 100644 index 0000000..111d425 --- /dev/null +++ b/src/exceptions/LogicException.js @@ -0,0 +1,12 @@ +import GdbotsPbjException from './GdbotsPbjException'; + +export default class LogicException extends GdbotsPbjException { + /** + * @param {string} message + */ + constructor(message) { + // 13 = INTERNAL + // @link https://github.com/gdbots/schemas/blob/master/schemas/gdbots/pbjx/enums.xml#L23 + super(message, 13); + } +} diff --git a/src/exceptions/MixinAlreadyAdded.js b/src/exceptions/MixinAlreadyAdded.js new file mode 100644 index 0000000..9ff5b6f --- /dev/null +++ b/src/exceptions/MixinAlreadyAdded.js @@ -0,0 +1,29 @@ +import SchemaException from './SchemaException'; + +export default class MixinAlreadyAdded extends SchemaException { + /** + * @param {Schema} schema + * @param {Mixin} originalMixin + * @param {Mixin} duplicateMixin + */ + constructor(schema, originalMixin, duplicateMixin) { + super(`Mixin with id [${duplicateMixin.getId()}] was already added from [${originalMixin.getId()}] to message [${schema.getId()}]. You cannot add multiple versions of the same mixin.`); + this.schema = schema; + this.originalMixin = originalMixin; + this.duplicateMixin = duplicateMixin; + } + + /** + * @returns {Mixin} + */ + getOriginalMixin() { + return this.originalMixin; + } + + /** + * @returns {Mixin} + */ + getDuplicateMixin() { + return this.duplicateMixin; + } +} diff --git a/src/exceptions/MixinNotDefined.js b/src/exceptions/MixinNotDefined.js new file mode 100644 index 0000000..bc3a42f --- /dev/null +++ b/src/exceptions/MixinNotDefined.js @@ -0,0 +1,20 @@ +import SchemaException from './SchemaException'; + +export default class MixinNotDefined extends SchemaException { + /** + * @param {Schema} schema + * @param {string} mixinId + */ + constructor(schema, mixinId) { + super(`Mixin [${mixinId}] is not defined on message [${schema.getId()}].`); + this.schema = schema; + this.mixinId = mixinId; + } + + /** + * @returns {string} + */ + getMixinId() { + return this.mixinId; + } +} diff --git a/src/exceptions/MoreThanOneMessageForMixin.js b/src/exceptions/MoreThanOneMessageForMixin.js new file mode 100644 index 0000000..f9ec419 --- /dev/null +++ b/src/exceptions/MoreThanOneMessageForMixin.js @@ -0,0 +1,28 @@ +import LogicException from './LogicException'; + +export default class MoreThanOneMessageForMixin extends LogicException { + /** + * @param {Mixin} mixin + * @param {Schema[]} schemas + */ + constructor(mixin, schemas) { + const ids = schemas.map(s => s.getId().toString()).join('\n - '); + super(`MessageResolver returned multiple schemas using [${mixin.getId().getCurieMajor()}] when one was expected. Schemas found:\n - ${ids}`); + this.mixin = mixin; + this.schemas = schemas; + } + + /** + * @returns {Mixin} + */ + getMixin() { + return this.mixin; + } + + /** + * @returns {Schema[]} + */ + getSchemas() { + return this.schemas; + } +} diff --git a/src/exceptions/NoMessageForCurie.js b/src/exceptions/NoMessageForCurie.js new file mode 100644 index 0000000..9820763 --- /dev/null +++ b/src/exceptions/NoMessageForCurie.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForCurie extends LogicException { + /** + * @param {SchemaCurie} curie + */ + constructor(curie) { + super(`MessageResolver is unable to resolve schema curie [${curie}] to a class.`); + this.curie = curie; + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.curie; + } +} diff --git a/src/exceptions/NoMessageForMixin.js b/src/exceptions/NoMessageForMixin.js new file mode 100644 index 0000000..dca144f --- /dev/null +++ b/src/exceptions/NoMessageForMixin.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForMixin extends LogicException { + /** + * @param {Mixin} mixin + */ + constructor(mixin) { + super(`MessageResolver is unable to find any messages using [${mixin.getId().getCurieMajor()}].`); + this.mixin = mixin; + } + + /** + * @returns {Mixin} + */ + getMixin() { + return this.mixin; + } +} diff --git a/src/exceptions/NoMessageForQName.js b/src/exceptions/NoMessageForQName.js new file mode 100644 index 0000000..46e50c6 --- /dev/null +++ b/src/exceptions/NoMessageForQName.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForQName extends LogicException { + /** + * @param {SchemaQName} qname + */ + constructor(qname) { + super(`MessageResolver is unable to resolve [${qname}] to a SchemaCurie.`); + this.qname = qname; + } + + /** + * @returns {SchemaQName} + */ + getQName() { + return this.qname; + } +} diff --git a/src/exceptions/NoMessageForSchemaId.js b/src/exceptions/NoMessageForSchemaId.js new file mode 100644 index 0000000..9cbdaba --- /dev/null +++ b/src/exceptions/NoMessageForSchemaId.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForSchemaId extends LogicException { + /** + * @param {SchemaId} schemaId + */ + constructor(schemaId) { + super(`MessageResolver is unable to resolve schema id [${schemaId}] to a class.`); + this.schemaId = schemaId; + } + + /** + * @returns {SchemaId} + */ + getSchemaId() { + return this.schemaId; + } +} diff --git a/src/exceptions/RequiredFieldNotSet.js b/src/exceptions/RequiredFieldNotSet.js new file mode 100644 index 0000000..71ae065 --- /dev/null +++ b/src/exceptions/RequiredFieldNotSet.js @@ -0,0 +1,35 @@ +import SchemaException from './SchemaException'; + +export default class RequiredFieldNotSet extends SchemaException { + /** + * @param {Message} pbj + * @param {Field} field + */ + constructor(pbj, field) { + super(`Required field [${field.getName()}] must be set on [${pbj.schema().getCurieMajor()}].`); + this.schema = pbj.schema(); + this.pbj = pbj; + this.field = field; + } + + /** + * @returns {Message} + */ + getPbj() { + return this.pbj; + } + + /** + * @returns {Field} + */ + getField() { + return this.field; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.field.getName(); + } +} diff --git a/src/exceptions/SchemaException.js b/src/exceptions/SchemaException.js new file mode 100644 index 0000000..1ca9397 --- /dev/null +++ b/src/exceptions/SchemaException.js @@ -0,0 +1,10 @@ +import LogicException from './LogicException'; + +export default class SchemaException extends LogicException { + /** + * @returns {Schema} + */ + getSchema() { + return this.schema; + } +} diff --git a/src/exceptions/SchemaNotDefined.js b/src/exceptions/SchemaNotDefined.js new file mode 100644 index 0000000..4bc2923 --- /dev/null +++ b/src/exceptions/SchemaNotDefined.js @@ -0,0 +1,4 @@ +import LogicException from './LogicException'; + +export default class SchemaNotDefined extends LogicException { +} diff --git a/src/field-builder.js b/src/field-builder.js deleted file mode 100644 index 41cd26b..0000000 --- a/src/field-builder.js +++ /dev/null @@ -1,306 +0,0 @@ -'use strict'; - -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Field from 'gdbots/pbj/field'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldBuilder -{ - /** - * @param string name - * @param Type type - */ - constructor(name, type) { - privateProps.set(this, { - /** @var string */ - name: name, - - /** @var Type */ - type: type, - - /** @var FieldRule */ - rule: null, - - /** @var bool */ - required: false, - - /** @var int */ - minLength: null, - - /** @var int */ - maxLength: null, - - /** @var string */ - pattern: null, - - /** @var string */ - format: null, - - /** @var int */ - min: null, - - /** @var int */ - max: null, - - /** @var int */ - precision: 10, - - /** @var int */ - scale: 2, - - /** @var mixed */ - defaultValue: null, - - /** @var bool */ - useTypeDefault: true, - - /** @var string */ - instance: null, - - /** @var array */ - anyOfInstances: null, - - /** @var \Closure */ - assertion: null, - - /** @var bool */ - overridable: false - }); - } - - /** - * @param string name - * @param Type type - * - * @return self - */ - static create(name, type) { - return new this(name, type); - } - - /** - * @return self - */ - required() { - privateProps.get(this).required = true; - return this; - } - - /** - * @return self - */ - optional() { - privateProps.get(this).required = false; - return this; - } - - /** - * @return self - */ - asASingleValue() { - privateProps.get(this).rule = FieldRule.A_SINGLE_VALUE; - return this; - } - - /** - * @return self - */ - asASet() { - privateProps.get(this).rule = FieldRule.A_SET; - return this; - } - - /** - * @return self - */ - asAList() { - privateProps.get(this).rule = FieldRule.A_LIST; - return this; - } - - /** - * @return self - */ - asAMap() { - privateProps.get(this).rule = FieldRule.A_MAP; - return this; - } - - /** - * @param int minLength - * - * @return self - */ - minLength(minLength) { - privateProps.get(this).minLength = parseInt(minLength); - return this; - } - - /** - * @param int maxLength - * - * @return self - */ - maxLength(maxLength) { - privateProps.get(this).maxLength = parseInt(maxLength); - return this; - } - - /** - * @param string pattern - * - * @return self - */ - pattern(pattern) { - privateProps.get(this).pattern = pattern; - return this; - } - - /** - * @param string format - * - * @return self - */ - format(format) { - privateProps.get(this).format = format; - return this; - } - - /** - * @param int min - * - * @return self - */ - min(min) { - privateProps.get(this).min = parseInt(min); - return this; - } - - /** - * @param int max - * - * @return self - */ - max(max) { - privateProps.get(this).max = parseInt(max); - return this; - } - - /** - * @param int precision - * - * @return self - */ - precision(precision) { - privateProps.get(this).precision = parseInt(precision); - return this; - } - - /** - * @param int scale - * - * @return self - */ - scale(scale) { - privateProps.get(this).scale = parseInt(scale); - return this; - } - - /** - * @param mixed defaultValue - * - * @return self - */ - withDefault(defaultValue) { - privateProps.get(this).defaultValue = defaultValue; - return this; - } - - /** - * @param bool useTypeDefault - * - * @return self - */ - useTypeDefault(useTypeDefault) { - privateProps.get(this).useTypeDefault = Boolean(useTypeDefault); - return this; - } - - /** - * @param string instance - * - * @return self - */ - instance(instance) { - privateProps.get(this).instance = instance; - privateProps.get(this).anyOfInstances = null; - return this; - } - - /** - * @param array anyOfInstances - * - * @return self - */ - anyOfInstances(anyOfInstances) { - privateProps.get(this).anyOfInstances = anyOfInstances; - privateProps.get(this).instance = null; - return this; - } - - /** - * @param \Closure assertion - * - * @return self - */ - assertion(assertion) { - privateProps.get(this).assertion = assertion; - return this; - } - - /** - * @param bool overridable - * - * @return self - */ - overridable(overridable) { - privateProps.get(this).overridable = Boolean(overridable); - return this; - } - - /** - * @return Field - */ - build() { - if (null === privateProps.get(this).rule) { - privateProps.get(this).rule = FieldRule.A_SINGLE_VALUE; - } - - return new Field( - privateProps.get(this).name, - privateProps.get(this).type, - privateProps.get(this).rule, - privateProps.get(this).required, - privateProps.get(this).minLength, - privateProps.get(this).maxLength, - privateProps.get(this).pattern, - privateProps.get(this).format, - privateProps.get(this).min, - privateProps.get(this).max, - privateProps.get(this).precision, - privateProps.get(this).scale, - privateProps.get(this).defaultValue, - privateProps.get(this).useTypeDefault, - privateProps.get(this).instance, - privateProps.get(this).anyOfInstances, - privateProps.get(this).assertion, - privateProps.get(this).overridable - ); - } -} diff --git a/src/field.js b/src/field.js deleted file mode 100644 index 2a15eb3..0000000 --- a/src/field.js +++ /dev/null @@ -1,624 +0,0 @@ -'use strict'; - -import ToArray from 'gdbots/common/to-array'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import NumberUtils from 'gdbots/common/util/number-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import TypeName from 'gdbots/pbj/enum/type-name'; -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Format from 'gdbots/pbj/enum/format'; - -/** - * Regular expression pattern for matching a valid field name. The pattern allows - * for camelCase fields name but snake_case is recommend. - * - * @constant string - */ -export const VALID_NAME_PATTERN = /^[a-zA-Z_]{1}[a-zA-Z0-9_]*/; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Field extends SystemUtils.mixinClass(null, ToArray) -{ - /** - * @param string name - * @param Type type - * @param FieldRule rule - * @param bool required - * @param null|int minLength - * @param null|int maxLength - * @param null|string pattern - * @param null|string format - * @param null|int min - * @param null|int max - * @param int precision - * @param int scale - * @param null|mixed defaultValue - * @param bool useTypeDefault - * @param null|string instance - * @param null|array anyOfInstances - * @param bool overridable - */ - constructor( - name, - type, - rule = null, - required = false, - minLength = null, - maxLength = null, - pattern = null, - format = null, - min = null, - max = null, - precision = 10, - scale = 2, - defaultValue = null, - useTypeDefault = true, - instance = null, - anyOfInstances = null, - assertion = null, - overridable = false - ) { - super(); // require before using `this` - - if (1 > name.length || name.length > 127) { - throw new Error('Name length must be between 1 to 127.'); - } - if (!VALID_NAME_PATTERN.test(name)) { - throw new Error('Field [' + name + '] must match pattern [' + VALID_NAME_PATTERN + '].'); - } - if (!type || !type.hasTrait('Type')) { - throw new Error('Class "' + type + '" was expected to be instanceof of "Type" but is not.'); - } - if ('boolean' !== typeof required) { - throw new Error('Required value must be boolean.'); - } - if ('boolean' !== typeof useTypeDefault) { - throw new Error('UseTypeDefault value must be boolean.'); - } - if ('boolean' !== typeof overridable) { - throw new Error('Overridable value must be boolean.'); - } - - /* - * a message type allows for interfaces to be used - * as the "instance". so long as the provided argument - * passes the instanceof check it's okay. - */ - if (type.getTypeName() !== TypeName.MESSAGE) { - - // anyOf is only supported on nested messages - anyOfInstances = null; - } - - privateProps.set(this, { - /** @var string */ - name: name, - - /** @var Type */ - type: type, - - /** @var FieldRule */ - rule: null, - - /** @var bool */ - required: required || false, - - /** @var int */ - minLength: null, - - /** @var int */ - maxLength: null, - - /** - * A regular expression to match against for string types. - * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#pattern - * - * @var string - */ - pattern: null, - - /** - * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#format - * - * @var Format - */ - format: null, - - /** @var int */ - min: null, - - /** @var int */ - max: null, - - /** @var int */ - precision: 10, - - /** @var int */ - scale: 2, - - /** @var mixed */ - defaultValue: null, - - /** @var bool */ - useTypeDefault: useTypeDefault, - - /** @var string */ - instance: instance, - - /** @var array */ - anyOfInstances: anyOfInstances, - - /** @var \Closure */ - assertion: assertion, - - /** @var bool */ - overridable: overridable || false - }); - - applyFieldRule.bind(this)(rule); - applyStringOptions.bind(this)(minLength, maxLength, pattern, format); - applyNumericOptions.bind(this)(min, max, precision, scale); - applyDefault.bind(this)(defaultValue); - } - - /** - * @return string - */ - getName() { - return privateProps.get(this).name; - } - - /** - * @return Type - */ - getType() { - return privateProps.get(this).type; - } - - /** - * @return FieldRule - */ - getRule() { - return privateProps.get(this).rule; - } - - /** - * @return bool - */ - isASingleValue() { - return FieldRule.A_SINGLE_VALUE === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isASet() { - return FieldRule.A_SET === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isAList() { - return FieldRule.A_LIST === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isAMap() { - return FieldRule.A_MAP === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isRequired() { - return privateProps.get(this).required; - } - - /** - * @return int - */ - getMinLength() { - return privateProps.get(this).minLength || 0; - } - - /** - * @return int - */ - getMaxLength() { - if (!privateProps.get(this).maxLength) { - return privateProps.get(this).type.getMaxBytes(); - } - - return privateProps.get(this).maxLength; - } - - /** - * @return string - */ - getPattern() { - return privateProps.get(this).pattern; - } - - /** - * @return Format - */ - getFormat() { - return privateProps.get(this).format; - } - - /** - * @return int - */ - getMin() { - if (!privateProps.get(this).min) { - return privateProps.get(this).type.getMin(); - } - - return privateProps.get(this).min; - } - - /** - * @return int - */ - getMax() { - if (!privateProps.get(this).max) { - return privateProps.get(this).type.getMax(); - } - - return privateProps.get(this).max; - } - - /** - * @return int - */ - getPrecision() { - return privateProps.get(this).precision; - } - - /** - * @return int - */ - getScale() { - return privateProps.get(this).scale; - } - - /** - * @param Message message - * - * @return mixed - */ - getDefault(message = null) { - if (null === privateProps.get(this).defaultValue) { - if (privateProps.get(this).useTypeDefault) { - return this.isASingleValue() ? privateProps.get(this).type.getDefault() : []; - } - - return this.isASingleValue() ? null : []; - } - - if ('function' === typeof privateProps.get(this).defaultValue) { - let defaultValue = privateProps.get(this).defaultValue(message, this); - - guardDefault.bind(this)(defaultValue); - - if (null === defaultValue) { - if (privateProps.get(this).useTypeDefault) { - return this.isASingleValue() ? privateProps.get(this).type.getDefault() : []; - } - - return this.isASingleValue() ? null : []; - } - - return defaultValue; - } - - - return privateProps.get(this).defaultValue; - } - - /** - * @return bool - */ - hasInstance() { - return null !== privateProps.get(this).instance; - } - - /** - * @return string - */ - getInstance() { - return privateProps.get(this).instance; - } - - /** - * @return bool - */ - hasAnyOfInstances() { - return null !== privateProps.get(this).anyOfInstances; - } - - /** - * @return array - */ - getAnyOfInstances() { - return privateProps.get(this).anyOfInstances; - } - - /** - * @return bool - */ - isOverridable() { - return privateProps.get(this).overridable; - } - - /** - * @param mixed value - * - * @throws AssertionFailed - * @throws \Exception - */ - guardValue(value) { - if (privateProps.get(this).required && null === value) { - throw new Error('Field [' + privateProps.get(this).name + '] is required and cannot be null.'); - } - - if (null !== value) { - privateProps.get(this).type.guard(value, this); - } - - if (null !== privateProps.get(this).assertion) { - privateProps.get(this).assertion(value, this); - } - } - - /** - * @return array - */ - toArray() { - return { - 'name': privateProps.get(this).name, - 'type': privateProps.get(this).type.getTypeValue(), - 'rule': privateProps.get(this).rule.getName(), - 'required': privateProps.get(this).required, - 'min_length': privateProps.get(this).minLength, - 'max_length': privateProps.get(this).maxLength, - 'pattern': privateProps.get(this).pattern, - 'format': privateProps.get(this).format.getValue(), - 'min': privateProps.get(this).min, - 'max': privateProps.get(this).max, - 'precision': privateProps.get(this).precision, - 'scale': privateProps.get(this).scale, - 'default': this.getDefault(), - 'use_type_default': privateProps.get(this).useTypeDefault, - 'instance': privateProps.get(this).instance, - 'any_of_instances': privateProps.get(this).anyOfInstances, - 'has_assertion': null !== privateProps.get(this).assertion, - 'overridable': privateProps.get(this).overridable, - }; - } - - /** - * Returns true if this field is likely compatible with the - * provided field during a mergeFrom operation. - * - * @param Field other - * - * @return bool - */ - isCompatibleForMerge(other) { - if (privateProps.get(this).name !== other.name) { - return false; - } - - if (privateProps.get(this).type !== other.type) { - return false; - } - - if (privateProps.get(this).rule !== other.rule) { - return false; - } - - if (privateProps.get(this).instance !== other.instance) { - return false; - } - - if (privateProps.get(this).anyOfInstances.filter(function(k) { - return other.anyOfInstances.indexOf(k) != -1; - }).length === 0) { - return false; - } - - return true; - } - - /** - * Returns true if the provided field can be used as an - * override to this field. - * - * @param Field other - * - * @return bool - */ - isCompatibleForOverride(other) { - if (!privateProps.get(this).overridable) { - return false; - } - - if (privateProps.get(this).name !== other.name) { - return false; - } - - if (privateProps.get(this).type !== other.type) { - return false; - } - - if (privateProps.get(this).rule !== other.rule) { - return false; - } - - if (privateProps.get(this).required !== other.required) { - return false; - } - - return true; - } -} - -/** - * @param FieldRule rule - * - * @throws AssertionFailed - */ -function applyFieldRule(rule = null) { - privateProps.get(this).rule = rule || FieldRule.A_SINGLE_VALUE; - - if (this.isASet() && !privateProps.get(this).type.allowedInSet()) { - throw new Error('Field [' + privateProps.get(this).name + '] with type [' + privateProps.get(this).type.getTypeValue() + '] cannot be used in a set.'); - } -} - -/** - * @param null|int minLength - * @param null|int maxLength - * @param null|string pattern - * @param null|string format - */ -function applyStringOptions(minLength = null, maxLength = null, pattern = null, format = null) { - privateProps.get(this).minLength = parseInt(minLength); - privateProps.get(this).maxLength = parseInt(maxLength); - - if (maxLength > 0) { - privateProps.get(this).maxLength = maxLength; - privateProps.get(this).minLength = NumberUtils.bound(minLength, 0, privateProps.get(this).maxLength); - } else { - // arbitrary string minimum range - privateProps.get(this).minLength = NumberUtils.bound(minLength, 0, privateProps.get(this).type.getMaxBytes()); - } - - if (null !== pattern) { - privateProps.get(this).pattern = pattern.trim().replace('/', ''); - } - - if (null !== format && Format.enumValueOf(format)) { - privateProps.get(this).format = Format.enumValueOf(format); - } else { - privateProps.get(this).format = Format.UNKNOWN; - } -} - -/** - * @param null|int min - * @param null|int max - * @param int precision - * @param int scale - */ -function applyNumericOptions(min = null, max = null, precision = 10, scale = 2) { - if (null !== max) { - privateProps.get(this).max = parseInt(max); - } - - if (null !== min) { - privateProps.get(this).min = parseInt(min); - if (null !== privateProps.get(this).max) { - if (privateProps.get(this).min > privateProps.get(this).max) { - privateProps.get(this).min = privateProps.get(this).max; - } - } - } - - privateProps.get(this).precision = NumberUtils.bound(parseInt(precision), 1, 65); - privateProps.get(this).scale = NumberUtils.bound(parseInt(scale), 0, privateProps.get(this).precision) -} - -/** - * @param mixed defaultValue - * - * @throws AssertionFailed - * @throws \Exception - */ -function applyDefault(defaultValue = null) { - privateProps.get(this).defaultValue = defaultValue; - - if (privateProps.get(this).type.isScalar()) { - if (privateProps.get(this).type.getTypeName() !== TypeName.TIMESTAMP) { - privateProps.get(this).useTypeDefault = true; - } - } else { - let decodeDefault = null !== privateProps.get(this).defaultValue && 'function' !== typeof privateProps.get(this).defaultValue; - - switch (privateProps.get(this).type.getTypeName()) { - case TypeName.IDENTIFIER: - if (null === privateProps.get(this).instance) { - throw new Error('Field [' + privateProps.get(this).name + '] requires an instance.'); - } - - if (decodeDefault && !privateProps.get(this).defaultValue.hasTrait('Identifier')) { - privateProps.get(this).defaultValue = privateProps.get(this).type.decode(privateProps.get(this).defaultValue, this); - } - break; - - case TypeName.INT_ENUM: - case TypeName.STRING_ENUM: - if (null === privateProps.get(this).instance) { - throw new Error('Field [' + privateProps.get(this).name + '] requires an instance.'); - } - - if (decodeDefault && !privateProps.get(this).defaultValue.hasTrait('Enum')) { - privateProps.get(this).defaultValue = privateProps.get(this).type.decode(privateProps.get(this).defaultValue, this); - } - break; - - default: - break; - } - } - - if (null !== privateProps.get(this).defaultValue && 'function' !== typeof privateProps.get(this).defaultValue) { - guardDefault.bind(this)(privateProps.get(this).defaultValue); - } -} - -/** - * @param mixed defaultValue - * - * @throws AssertionFailed - * @throws \Exception - */ -function guardDefault(defaultValue) { - if (this.isASingleValue()) { - this.guardValue(defaultValue); - - return; - } - - if (null !== defaultValue || !Array.isArray(defaultValue)) { - throw new Error('Field [' + privateProps.get(this).name + '] default must be an array.'); - } - - if (null === defaultValue) { - return; - } - - if (this.isAMap()) { - if (!ArrayUtils.isAssoc(defaultValue)) { - throw new Error('Field [' + privateProps.get(this).name + '] default must be an associative array.'); - } - } - - ArrayUtils.each(defaultValue, function(value, key) { - if (null === value) { - throw new Error('Field [' + privateProps.get(this).name + '] default for key [' + value + '] cannot be null.'); - } - - this.guardValue(value); - }.bind(this)); -} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..422a0df --- /dev/null +++ b/src/index.js @@ -0,0 +1,27 @@ +import Field from './Field'; +import FieldBuilder from './FieldBuilder'; +import Message from './Message'; +import MessageRef from './MessageRef'; +import Mixin from './Mixin'; +import Schema from './Schema'; +import SchemaCurie from './SchemaCurie'; +import SchemaId from './SchemaId'; +import SchemaQName from './SchemaQName'; +import SchemaVersion from './SchemaVersion'; +import Types from './types'; +import WellKnown from './well-known'; + +export default { + Field, + FieldBuilder, + Message, + MessageRef, + Mixin, + Schema, + SchemaCurie, + SchemaId, + SchemaQName, + SchemaVersion, + Types, + WellKnown, +}; diff --git a/src/message-ref.js b/src/message-ref.js deleted file mode 100644 index a9faa9c..0000000 --- a/src/message-ref.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import LogicException from 'gdbots/pbj/exception/logic-exception'; -import SchemaCurie from 'gdbots/pbj/schema-curie'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Represents a reference to a message. Typically used to link messages - * together via a correlator or "links". Format for a reference: - * vendor:package:category:message:id#tag (tag is optional) - */ -export default class MessageRef extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * @param SchemaCurie curie - * @param string id - * @param string tag The tag will be automatically fixed to a slug-formatted-string. - * - * @throws \Exception - */ - constructor(curie, id, tag = null) { - super(); // require before using `this` - - privateProps.set(this, { - /** @var SchemaCurie */ - curie: curie, - - /** - * Any string matching pattern /^[\w\/\.:-]+/ - * - * @var string - */ - id: id || 'null', - - /** @var string */ - tag: null - }); - - if (false === /^[\w\/\.:-]+/.test(privateProps.get(this).id)) { - throw new Error('MessageRef.id'); - } - - if (null !== tag) { - privateProps.get(this).tag = tag.toString().toLowerCase() - .replace(/\s+/g, '-') // Replace spaces with - - .replace(/[^\w\-]+/g, '') // Remove all non-word chars - .replace(/\-\-+/g, '-') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, ''); // Trim - from end of text - } - - if (privateProps.get(this).curie.isMixin()) { - throw new LogicException('Mixins cannot be used in a MessageRef.'); - } - } - - /** - * {@inheritdoc} - */ - static fromArray(data = {}) { - if (data.curie || false) { - let id = data.id || 'null'; - let tag = data.tag || null; - - return new this(SchemaCurie.fromString(data.curie), id, tag); - } - - throw new InvalidArgumentException('Payload must be a MessageRef type.'); - } - - /** - * {@inheritdoc} - */ - toArray() { - if (null !== privateProps.get(this).tag) { - return { - 'curie': privateProps.get(this).curie.toString(), - 'id': privateProps.get(this).id, - 'tag': privateProps.get(this).tag - }; - } - - return { - 'curie': privateProps.get(this).curie.toString(), - 'id': privateProps.get(this).id - }; - } - - /** - * @param string string A string with format curie:id#tag - * - * @return self - */ - static fromString(string) { - let parts = string.split('#', 2); - let ref = parts[0]; - let tag = parts[1] || null; - - parts = ref.split(':', 5); - let id = parts.pop(); - let curie = SchemaCurie.fromString(parts.join(':')); - - return new this(curie, id, tag); - } - - /** - * @return string - */ - toString() { - if (null !== privateProps.get(this).tag) { - return privateProps.get(this).curie.toString() + ':' + privateProps.get(this).id + '#' + privateProps.get(this).tag; - } - - return privateProps.get(this).curie.toString() + ':' + privateProps.get(this).id; - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).curie; - } - - /** - * @return bool - */ - hasId() { - return 'null' != privateProps.get(this).id; - } - - /** - * @return string - */ - getId() { - return privateProps.get(this).id; - } - - /** - * @return bool - */ - hasTag() { - return null !== privateProps.get(this).tag; - } - - /** - * @return string - */ - getTag() { - return privateProps.get(this).tag; - } - - /** - * @param MessageRef other - * - * @return bool - */ - equals(other) { - return this.toString() === other.toString(); - } -} diff --git a/src/message-resolver.js b/src/message-resolver.js deleted file mode 100644 index 1c66352..0000000 --- a/src/message-resolver.js +++ /dev/null @@ -1,256 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import NoMessageForCurie from 'gdbots/pbj/exception/no-message-for-curie'; -import NoMessageForSchemaId from 'gdbots/pbj/exception/no-message-for-schema-id'; -import NoMessageForMixin from 'gdbots/pbj/exception/no-message-for-mixin'; -import MoreThanOneMessageForMixin from 'gdbots/pbj/exception/more-than-one-message-for-mixin'; - -let _registerPromise = null; -let _messages = {}; -let _resolved = {}; -let _resolvedMixins = {}; - -export default class MessageResolver -{ - /** - * Used when dynamically loading messages. - * - * @see self::registerMap - * - * @var Promise - */ - static registerPromise() { - return _registerPromise || Promise.resolve(true); - } - - /** - * An array of all the available messages keyed by the schema resolver key - * and curies for resolution that is not version specific. - * - * @var array - */ - static messages() { - return _messages; - } - - /** - * An array of resolved messages in this request. - * - * @var array - */ - static resolved() { - return _resolved; - } - - /** - * An array of resolved lookups by mixin, keyed by the mixin id with major rev - * and optionally a package and category (for faster lookups) - * - * @see SchemaId::getCurieMajor - * - * @var Message[] - */ - static resolvedMixins() { - return _resolvedMixins; - } - - /** - * Returns all of the registed schemas. - * - * @var Message[] - */ - static all() { - return _messages; - } - - /** - * Returns the Message to be used for the provided schema id. - * - * @param SchemaId id - * - * @return Message - * - * @throws NoMessageForSchemaId - */ - static resolveId(id) { - let curieMajor = id.getCurieMajor(); - if (-1 !== Object.keys(_resolved).indexOf(curieMajor)) { - return _resolved[curieMajor]; - } - - let message = null; - - if (-1 !== Object.keys(_messages).indexOf(curieMajor)) { - message = _messages[curieMajor]; - _resolved[curieMajor] = message; - return message; - } - - let curie = id.getCurie().toString(); - if (-1 !== Object.keys(_messages).indexOf(curie)) { - message = _messages[curie]; - _resolved[curieMajor] = message; - _resolved[curie] = message; - return message; - } - - throw new NoMessageForSchemaId(id); - } - - /** - * Returns the Message to be used for the provided curie. - * - * @param SchemaCurie curie - * - * @return Message - * - * @throws NoMessageForCurie - */ - static resolveCurie(curie) { - let key = curie.toString(); - if (-1 !== Object.keys(_resolved).indexOf(key)) { - return _resolved[key]; - } - - if (-1 !== Object.keys(_messages).indexOf(key)) { - let message = _messages[key]; - _resolved[key] = message; - return message; - } - - throw new NoMessageForCurie(curie); - } - - /** - * Adds a single message to the resolver. This is used in tests or dynamic - * message creation (not a typical use case). - * - * @param Message message - * @param Schema schema - */ - static registerSchema(message, schema) { - _messages[schema.getId().getCurieMajor()] = message; - } - - /** - * Adds a single schema id and message. - * - * @see SchemaId::getCurieMajor - * - * @param SchemaId|string id - * @param Message message - */ - static register(id, message) { - if ('SchemaId' === SystemUtils.getClass(id)) { - id = id.getCurieMajor(); - } - - _messages[id] = message; - } - - /** - * Registers an array of id => messagePath values to the resolver. - * - * @param array map - */ - static registerMap(map = {}) { - let promises = []; - - ArrayUtils.each(map, function(value, key) { - if (map.hasOwnProperty(key)) { - if ('object' === typeof value && value.hasTrait('Message')) { - _messages[message.schema().getId().getCurieMajor()] = message; - } else { - promises.push(SystemUtils.import(value)); - } - } - }); - - _registerPromise = Promise.all(promises).then(function(messages) { - ArrayUtils.each(messages, function(message) { - - // @todo: check removing the `default` property - message = message.default; - - _messages[message.schema().getId().getCurieMajor()] = message; - }.bind(this)); - - _registerPromise = null; - }.bind(this)); - } - - /** - * Return the one message expected to be using the provided mixin. - * - * @param Mixin mixin - * @param string inPackage - * @param string inCategory - * - * @return Message - * - * @throws MoreThanOneMessageForMixin - * @throws NoMessageForMixin - */ - static findOneUsingMixin(mixin, inPackage = null, inCategory = null) { - let messages = this.findAllUsingMixin(mixin, inPackage, inCategory); - if (1 !== messages.length) { - throw new MoreThanOneMessageForMixin(mixin, messages); - } - - return messages[0]; - } - - /** - * Returns an array of messages expected to be using the provided mixin. - * - * @param Mixin mixin - * @param string inPackage - * @param string inCategory - * - * @return Message[] - * - * @throws NoMessageForMixin - */ - static findAllUsingMixin(mixin, inPackage = null, inCategory = null) { - let mixinId = mixin.getId().getCurieMajor(); - let key = mixinId + inPackage + inCategory; - - /** @var Message[] */ - let messages = []; - - if (-1 === Object.keys(_resolvedMixins).indexOf(key)) { - let filtered = (inPackage && inPackage.length) || (inCategory && inCategory.length); - - ArrayUtils.each(_messages, function(message, id) { - if (filtered) { - let curie = id.split(':'); - - if (inPackage && inPackage.length && curie[1] != inPackage) { - return; - } - - if (inCategory && inCategory.length && curie[2] != inCategory) { - return; - } - } - - let schema = message.schema(); - if (schema.hasMixin(mixinId)) { - messages.push(message); - } - }); - - _resolvedMixins[key] = messages; - } else { - messages = _resolvedMixins[key]; - } - - if (!messages || messages.length === 0) { - throw new NoMessageForMixin(mixin); - } - - return messages; - } -} diff --git a/src/message.js b/src/message.js deleted file mode 100644 index 866c8bc..0000000 --- a/src/message.js +++ /dev/null @@ -1,911 +0,0 @@ -'use strict'; - -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import SchemaNotDefined from 'gdbots/pbj/exception/schema-not-defined'; -import RequiredFieldNotSet from 'gdbots/pbj/exception/required-field-not-set'; -import FrozenMessageIsImmutable from 'gdbots/pbj/exception/frozen-message-is-immutable'; -import LogicException from 'gdbots/pbj/exception/logic-exception'; -import ArraySerializer from 'gdbots/pbj/serializer/array-serializer'; -import {PBJ_FIELD_NAME} from 'gdbots/pbj/schema'; - -/** - * An array of schemas per message type. - * ['Fully\Qualified\ClassName' => [ array of Schema objects ] - * - * @var array - */ -let _schemas = {}; - -/** @var ArraySerializer */ -let serializer = null; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Message extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * Nothing fancy on new messages... we let the serializers or application code get fancy. - */ - constructor() { - super(); // require before using `this` - - privateProps.set(this, { - /** @var array */ - data: {}, - - /** - * An array of fields that have been cleared or set to null that - * must be included when serialized so it's clear that the - * value has been unset. - * - * @var array - */ - clearedFields: {}, - - /** - * @see Message::freeze - * - * @var bool - */ - isFrozen: false, - - /** - * @see Message::isReplay - * - * @var bool - */ - isReplay: false - }); - } - - /** - * @return Schema - * - * @throws SchemaNotDefined - */ - static schema() { - let type = this.name; - if (undefined !== _schemas[type]) { - return _schemas[type]; - } - - let schema = this.defineSchema(); - if ('Schema' !== SystemUtils.getClass(schema)) { - throw new SchemaNotDefined('Message [' + type + '] must return a Schema from the defineSchema method.'); - } - - if (schema.getClassName() !== type) { - throw new SchemaNotDefined('Schema [' + schema.getId().toString() + '] returned from defineSchema must be for class [' + type + '], not [' + schema.getClassName() + ']'); - } - - _schemas[type] = schema; - return _schemas[type]; - } - - /** - * @return Schema - * - * @throws SchemaNotDefined - */ - static defineSchema() { - throw new SchemaNotDefined('Message [' + this.name + '] must return a Schema from the defineSchema method.'); - } - - /** - * Creates a new message with the defaults populated. - * - * @return static - */ - static create() { - /** @var Message message */ - let message = new this(); - return message.populateDefaults(); - } - - /** - * Returns a new message from the provided array using the Array Serializer. - * @see Gdbots\Pbj\Serializer\ArraySerializer::deserialize - * - * @param array data - * - * @return static - */ - static fromArray(data = {}) { - if (null === serializer) { - serializer = new ArraySerializer(); - } - - if (undefined === data[PBJ_FIELD_NAME]) { - data[PBJ_FIELD_NAME] = this.schema().getId().toString(); - } - - return serializer.deserialize(data); - } - - /** - * Returns the message as an associative array using the Array Serializer. - * @see Gdbots\Pbj\Serializer\ArraySerializer::serialize - * - * @return array - */ - toArray() { - if (null === serializer) { - serializer = new ArraySerializer(); - } - - return serializer.serialize(this); - } - - /** - * Returns a Yaml string version of the message. - * Useful for debugging or logging. - * - * @param array options - * - * @return string - */ - toYaml(options = {}) { - throw new Error('Not yet implemented.'); - } - - /** - * Returns the message as a human readable string. - * - * @return string - */ - toString() { - return this.toArray(); - } - - /** - * Generates an md5 hash of the json representation of the current message. - * - * @param string[] ignoredFields - * - * @return string - */ - generateEtag(ignoredFields = []) { - if (null === serializer) { - serializer = new ArraySerializer(); - } - - let array = serializer.serialize(this, { 'includeAllFields': true }); - - if (ignoredFields.length === 0) { - return StringUtils.md5(JSON.stringify(array)); - } - - ArrayUtils.each(ignoredFields, function(value, key) { - delete array[ignoredFields[key]]; - }); - - return StringUtils.md5(JSON.stringify(array)); - } - - /** - * Generates a reference to this message with an optional tag. - * - * @param string tag - * - * @return MessageRef - */ - generateMessageRef(tag = null) { - throw new Error('Interface function.'); - } - - /** - * Returns an array that can be used in a uri template to generate - * a uri/url for this message. - * @link https://tools.ietf.org/html/rfc6570 - * @link https://github.com/gdbots/uri-template-php - * - * @return array - */ - getUriTemplateVars() { - throw new Error('Interface function.'); - } - - /** - * Verifies all required fields have been populated. - * - * @return static - * - * @throws GdbotsPbjException - * @throws RequiredFieldNotSet - */ - validate() { - ArrayUtils.each(this.constructor.schema().getRequiredFields(), function(field) { - if (!this.has(field.getName())) { - throw new RequiredFieldNotSet(this, field); - } - }.bind(this)); - - return this; - } - - /** - * Freezes the message, making it immutable. The message must be valid - * before it can be frozen so this may throw an exception if some required - * fields have not been populated. - * - * @return static - * - * @throws GdbotsPbjException - * @throws RequiredFieldNotSet - */ - freeze() { - if (privateProps.get(this).isFrozen) { - return this; - } - - this.validate(); - privateProps.get(this).isFrozen = true; - - ArrayUtils.each(this.constructor.schema().getFields(), function(field) { - if (field.getType().isMessage()) { - /** @var self value */ - let value = this.get(field.getName()); - if (!value || value.length === 0) { - return; - } - - if (!Array.isArray(value) && value.hasTrait('Message')) { - value.freeze(); - return; - } - - /** @var self value[v] */ - ArrayUtils.each(value, function(v, k) { - value[k].freeze(); - }); - } - }.bind(this)); - - return this; - } - - /** - * Returns true if the message has been frozen. A frozen message is - * immutable and cannot be modified. - * - * @return bool - */ - isFrozen() { - return privateProps.get(this).isFrozen; - } - - /** - * Returns true if the data of the message matches. - * - * @param Message other - * - * @return bool - */ - equals(other) { - return JSON.stringify(this) === JSON.stringify(other); - } - - /** - * Returns true if this message is being replayed. Providing a value - * will set the flag but this can only be done once. Note that - * setting a message as being "replayed" will also freeze the message. - * - * @param bool|null replay - * - * @return bool - * - * @throws LogicException - */ - isReplay(replay = null) { - if (null === replay) { - if (null === privateProps.get(this).isReplay) { - privateProps.get(this).isReplay = false; - } - return privateProps.get(this).isReplay; - } - - if (null === privateProps.get(this).isReplay) { - privateProps.get(this).isReplay = Boolean(replay); - if (privateProps.get(this).isReplay) { - this.freeze(); - } - return privateProps.get(this).isReplay; - } - - throw new LogicException('You can only set the replay mode on one time.'); - } - - /** - * Populates the defaults on all fields or just the fieldName provided. - * Operation will NOT overwrite any fields already set. - * - * @param string|null fieldName - * - * @return static - */ - populateDefaults(fieldName = null) { - guardFrozenMessage.bind(this)(); - - if (fieldName) { - populateDefault.bind(this)(this.constructor.schema().getField(fieldName)); - - return this; - } - - ArrayUtils.each(this.constructor.schema().getFields(), function(field) { - populateDefault.bind(this)(field); - }.bind(this)); - - return this; - } - - /** - * Returns true if the field has been populated. - * - * @param string fieldName - * - * @return bool - */ - has(fieldName) { - if (undefined === privateProps.get(this).data[fieldName]) { - return false; - } - - if (Array.isArray(privateProps.get(this).data[fieldName])) { - return privateProps.get(this).data[fieldName] && privateProps.get(this).data[fieldName].length; - } - - return true; - } - - /** - * Returns the value for the given field. If the field has not - * been set you will get a null value. - * - * @param string fieldName - * @param mixed defaultValue - * - * @return mixed - */ - get(fieldName, defaultValue = null) { - if (!this.has(fieldName)) { - return defaultValue; - } - - let field = this.constructor.schema().getField(fieldName); - if (field.isASet()) { - return Object.keys(privateProps.get(this).data[fieldName]).map(function(v) { - return privateProps.get(this).data[fieldName][v]; - }.bind(this)); - } - - return privateProps.get(this).data[fieldName]; - } - - /** - * Clears the value of a field. - * - * @param string fieldName - * - * @return static - * - * @throws GdbotsPbjException - * @throws RequiredFieldNotSet - */ - clear(fieldName) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - - delete privateProps.get(this).data[fieldName]; - - privateProps.get(this).clearedFields[fieldName] = true; - - populateDefault.bind(this)(field); - - return this; - } - - /** - * Returns true if the field has been cleared. - * - * @param string fieldName - * - * @return bool - */ - hasClearedField(fieldName) { - return undefined !== privateProps.get(this).clearedFields[fieldName]; - } - - /** - * Returns an array of field names that have been cleared. - * - * @return array - */ - getClearedFields() { - return Object.keys(privateProps.get(this).clearedFields); - } - - /** - * @deprecated Use "set" instead, the method signature is the same. - * - * @param string fieldName - * @param mixed value - * - * @return static - * - * @throws GdbotsPbjException - */ - setSingleValue(fieldName, value) { - return this.set(fieldName, value); - } - - /** - * Sets a single value field. - * - * @param string fieldName - * @param mixed value - * - * @return static - * - * @throws GdbotsPbjException - */ - set(fieldName, value) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isASingleValue()) { - throw new Error('Field [' + fieldName + '] must be a single value.'); - } - - if (null === value) { - return this.clear(fieldName); - } - - field.guardValue(value); - - privateProps.get(this).data[fieldName] = value; - - delete privateProps.get(this).clearedFields[fieldName]; - - return this; - } - - /** - * Returns true if the provided value is in the set of values. - * - * @param string fieldName - * @param mixed value - * - * @return bool - */ - isInSet(fieldName, value) { - if (!privateProps.get(this).data[fieldName] - || privateProps.get(this).data[fieldName].length === 0 - || 'object' !== typeof privateProps.get(this).data[fieldName] - ) { - return false; - } - - if (!(/object|boolean|number|string/).test(typeof value)) { - return false; - } - - let key = null; - try { - key = value.toString(); - } catch (e) { - key = value; - } - key = String(key).trim().toLowerCase(); - - if (0 === key.length) { - return false; - } - - return undefined !== privateProps.get(this).data[fieldName][String(key).trim().toLowerCase()]; - } - - /** - * Adds an array of unique values to an unsorted set of values. - * - * @param string fieldName - * @param array values - * - * @return static - * - * @throws GdbotsPbjException - */ - addToSet(fieldName, values) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isASet()) { - throw new Error('Field [' + fieldName + '] must be a set.'); - } - - if (undefined === privateProps.get(this).data[fieldName]) { - privateProps.get(this).data[fieldName] = {}; - } - - ArrayUtils.each(values, function(value) { - if (0 === value.length) { - return; - } - - field.guardValue(value); - - let key = null; - try { - key = value.toString(); - } catch (e) { - key = value; - } - key = String(key).trim().toLowerCase(); - - privateProps.get(this).data[fieldName][key] = value; - }.bind(this)); - - if (privateProps.get(this).data[fieldName] && privateProps.get(this).data[fieldName].length) { - delete privateProps.get(this).clearedFields[fieldName]; - } - - return this; - } - - /** - * Removes an array of values from a set. - * - * @param string fieldName - * @param array values - * - * @return static - * - * @throws GdbotsPbjException - */ - removeFromSet(fieldName, values) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isASet()) { - throw new Error('Field [' + fieldName + '] must be a set.'); - } - - ArrayUtils.each(values, function(value) { - if (0 === value.length) { - return; - } - - let key = null; - try { - key = value.toString(); - } catch (e) { - key = value; - } - key = String(key).trim().toLowerCase(); - - if (undefined !== privateProps.get(this).data[fieldName][key]) { - delete privateProps.get(this).data[fieldName][key]; - } - }.bind(this)); - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - privateProps.get(this).clearedFields[fieldName] = true - } - - return this; - } - - /** - * Returns true if the provided value is in the list of values. - * This is a NOT a strict comparison, it uses "==". - * @link http://php.net/manual/en/function.in-array.php - * - * @param string fieldName - * @param mixed value - * - * @return bool - */ - isInList(fieldName, value) { - if (!privateProps.get(this).data[fieldName] - || privateProps.get(this).data[fieldName].length === 0 - || !Array.isArray(privateProps.get(this).data[fieldName]) - ) { - return false; - } - - return -1 !== privateProps.get(this).data[fieldName].indexOf(value); - } - - /** - * Returns an item in a list or null if it doesn't exist. - * - * @param string fieldName - * @param int index - * @param mixed defaultValue - * - * @return mixed - */ - getFromListAt(fieldName, index, defaultValue = null) { - index = parseInt(index); - - if (!privateProps.get(this).data[fieldName] - || privateProps.get(this).data[fieldName].length === 0 - || !Array.isArray(privateProps.get(this).data[fieldName]) - || undefined === privateProps.get(this).data[fieldName][index] - ) { - return defaultValue; - } - - return privateProps.get(this).data[fieldName][index]; - } - - /** - * Adds an array of values to an unsorted list/array (not unique). - * - * @param string fieldName - * @param array values - * - * @return static - * - * @throws GdbotsPbjException - */ - addToList(fieldName, values) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAList()) { - throw new Error('Field [' + fieldName + '] must be a list.'); - } - - if (undefined === privateProps.get(this).data[fieldName]) { - privateProps.get(this).data[fieldName] = []; - } - - ArrayUtils.each(values, function(value) { - field.guardValue(value); - - privateProps.get(this).data[fieldName].push(value); - }.bind(this)); - - delete privateProps.get(this).clearedFields[fieldName]; - - return this; - } - - /** - * Removes the element from the array at the index. - * - * @param string fieldName - * @param int index - * - * @return static - * - * @throws GdbotsPbjException - */ - removeFromListAt(fieldName, index) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAList()) { - throw new Error('Field [' + fieldName + '] must be a list.'); - } - - index = parseInt(index); - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - return this; - } - - if (undefined !== privateProps.get(this).data[fieldName][index]) { - delete privateProps.get(this).data[fieldName][index]; - } - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - privateProps.get(this).clearedFields[fieldName] = true; - - return this; - } - - privateProps.get(this).data[fieldName] = Object.keys(privateProps.get(this).data[fieldName]).map(function(v) { - return privateProps.get(this).data[fieldName][v]; - }); - - return this; - } - - /** - * Returns true if the map contains the provided key. - * - * @param string fieldName - * @param string key - * - * @return bool - */ - isInMap(fieldName, key) { - if (!privateProps.get(this).data[fieldName] - || Object.keys(privateProps.get(this).data[fieldName]).length === 0 - || 'object' !== typeof privateProps.get(this).data[fieldName] - || 'string' !== typeof key - ) { - return false; - } - - return undefined !== privateProps.get(this).data[fieldName][key]; - } - - /** - * Returns the value of a key in a map or null if it doesn't exist. - * - * @param string fieldName - * @param string key - * @param mixed defaultValue - * - * @return mixed - */ - getFromMap(fieldName, key, defaultValue = null) { - if (!this.isInMap(fieldName, key)) { - return defaultValue; - } - - return privateProps.get(this).data[fieldName][key]; - } - - /** - * Adds a key/value pair to a map. - * - * @param string fieldName - * @param string key - * @param mixed value - * - * @return static - * - * @throws GdbotsPbjException - */ - addToMap(fieldName, key, value) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAMap()) { - throw new Error('Field [' + fieldName + '] must be a map.'); - } - - if (null === value) { - return this.removeFromMap(fieldName, key); - } - - field.guardValue(value); - - if (undefined === privateProps.get(this).data[fieldName]) { - privateProps.get(this).data[fieldName] = {}; - } - - privateProps.get(this).data[fieldName][key] = value; - - delete privateProps.get(this).clearedFields[fieldName]; - - return this; - } - - /** - * Removes a key/value pair from a map. - * - * @param string fieldName - * @param string key - * - * @return static - * - * @throws GdbotsPbjException - */ - removeFromMap(fieldName, key) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAMap()) { - throw new Error('Field [' + fieldName + '] must be a map.'); - } - - delete privateProps.get(this).data[fieldName][key]; - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - privateProps.get(this).clearedFields[fieldName] = true; - } - - return this; - } -} - -/** - * Recursively unfreezes this object and any of its children. - * Used internally during the clone process. - */ -function unFreeze() { - privateProps.get(this).isFrozen = false; - privateProps.get(this).isReplay = null; - - ArrayUtils.each(this.constructor.schema().getFields(), function(field) { - if (field.getType().isMessage()) { - /** @var self value */ - let value = this.get(field.getName()); - if (!value || value.length === 0) { - return; - } - - if (value.hasTrait('Message')) { - unFreeze.bind(value)(); - return; - } - - /** @var self value[v] */ - ArrayUtils.each(value, function(v, k) { - unFreeze.bind(value[k])(); - }); - } - }.bind(this)); -} - -/** - * Ensures a frozen message can't be modified. - * - * @throws FrozenMessageIsImmutable - */ -function guardFrozenMessage() { - if (privateProps.get(this).isFrozen) { - throw new FrozenMessageIsImmutable(this); - } -} - -/** - * Populates the default on a single field if it's not already set - * and the default generated is not a null value or empty array. - * - * @param Field field - * - * @return bool Returns true if a non null/empty default was applied or already present. - */ -function populateDefault(field) { - if (this.has(field.getName())) { - return true; - } - - let defaultValue = field.getDefault(this); - if (null === defaultValue) { - return false; - } - - if (field.isASingleValue()) { - privateProps.get(this).data[field.getName()] = defaultValue; - - delete privateProps.get(this).clearedFields[field.getName()]; - - return true; - } - - if (!defaultValue || defaultValue.length === 0) { - return false; - } - - /* - * sets have a special handling to deal with unique values - */ - if (field.isASet()) { - this.addToSet(field.getName(), defaultValue); - - return true; - } - - privateProps.get(this).data[field.getName()] = defaultValue; - - delete privateProps.get(this).clearedFields[field.getName()]; - - return true; -} diff --git a/src/mixin.js b/src/mixin.js deleted file mode 100644 index 53553d5..0000000 --- a/src/mixin.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import ToArray from 'gdbots/common/to-array'; - -let _instances = {}; - -export default class Mixin extends SystemUtils.mixinClass(null, ToArray) -{ - /** - * @return Mixin - */ - static create() { - let type = this.name; - if (undefined === _instances[type]) { - _instances[type] = new this(); - } - - return _instances[type]; - } - - /** - * Returns the id for this mixin. - * - * @return SchemaId - */ - getId() { - return null; - } - - /** - * Returns an array of fields that the mixin provides. - * - * @return Field[] - */ - getFields() { - return []; - } - - /** - * @return array - */ - toArray() { - return { - id: this.getId(), - fields: this.getFields() - }; - } - - /** - * @return string - */ - toString() { - if (this.getId()) { - return this.getId().toString(); - } - - return null; - } -} diff --git a/src/schema-curie.js b/src/schema-curie.js deleted file mode 100644 index 0b60e7a..0000000 --- a/src/schema-curie.js +++ /dev/null @@ -1,158 +0,0 @@ -'use strict'; - -import InvalidSchemaCurie from 'gdbots/pbj/exception/invalid-schema-curie'; -import SchemaQName from 'gdbots/pbj/schema-q-name'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Regular expression pattern for matching a valid SchemaCurie string. - * @constant string - */ -export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+)/; - -/** - * Schemas can be fully qualified by the schema id (which includes the version) - * or the short form which is called a CURIE or "compact uri". - * @link http://en.wikipedia.org/wiki/CURIE - * - * Schema Curie Format: - * vendor:package:category:message - * - * @see SchemaId - * - */ -export default class SchemaCurie -{ - /** - * @param string vendor - * @param string packageName - * @param string category - * @param string message - */ - constructor(vendor, packageName, category, message) { - if (!category) { - category = ''; - } - - privateProps.set(this, { - /** @var string */ - vendor: vendor, - - /** @var string */ - package: packageName, - - /** @var string */ - category: category, - - /** @var string */ - message: message, - - /** @var string */ - curie: vendor + ':' + packageName + ':' + category + ':' + message, - - /** @var SchemaQName */ - qname: null - }); - - privateProps.get(this).qname = SchemaQName.fromCurie(this); - } - - /** - * @param SchemaId id - * - * @return SchemaCurie - */ - static fromId(id) { - let curie = id.toString().replace(':' + id.getVersion().toString(), '').substr(4); - - if (undefined !== _instances[curie]) { - return _instances[curie]; - } - - _instances[curie] = new this(id.getVendor(), id.getPackage(), id.getCategory(), id.getMessage()); - return _instances[curie]; - } - - /** - * @param string curie - * - * @return SchemaCurie - * - * @throws InvalidSchemaCurie - */ - static fromString(curie) { - if (undefined !== _instances[curie]) { - return _instances[curie]; - } - - if (curie.length > 145) { - throw new Error('Schema curie cannot be greater than 145 chars.'); - } - - let matches = curie.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaCurie('Schema curie [' + curie + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - _instances[curie] = new this(matches[1], matches[2], matches[3], matches[4]); - return _instances[curie]; - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).curie; - } - - /** - * @return string - */ - getVendor() { - return privateProps.get(this).vendor; - } - - /** - * @return string - */ - getPackage() { - return privateProps.get(this).package; - } - - /** - * @return string - */ - getCategory() { - return privateProps.get(this).category; - } - - /** - * @return string - */ - getMessage() { - return privateProps.get(this).message; - } - - /** - * @return bool - */ - isMixin() { - return 'mixin' === privateProps.get(this).category; - } - - /** - * @return SchemaQName - */ - getQName() { - return privateProps.get(this).qname; - } -} diff --git a/src/schema-id.js b/src/schema-id.js deleted file mode 100644 index 9863568..0000000 --- a/src/schema-id.js +++ /dev/null @@ -1,203 +0,0 @@ -'use strict'; - -import InvalidSchemaId from 'gdbots/pbj/exception/invalid-schema-id'; -import SchemaCurie from 'gdbots/pbj/schema-curie'; -import SchemaVersion from 'gdbots/pbj/schema-version'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Regular expression pattern for matching a valid SchemaId string. - * @constant string - */ -export const VALID_PATTERN = /^pbj:([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+):([0-9]+-[0-9]+-[0-9]+)/; - -/** - * Schemas have fully qualified names, similar to a "urn". This is combination of ideas from: - * - * Amazon Resource Names (ARNs) and AWS Service Namespaces - * @link http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html - * - * SnowPlow Analytics (Iglu) - * @link http://snowplowanalytics.com/blog/2014/07/01/iglu-schema-repository-released/ - * - * @link http://en.wikipedia.org/wiki/CURIE - * - * And of course the various package managers like composer, npm, etc. - * - * Schema Id Format: - * pbj:vendor:package:category:message:version - * - * Schema Curie Format: - * vendor:package:category:message - * - * Schema Curie Major Format: - * vendor:package:category:message:v# - * - * Schema QName Format: - * vendor:message - * - * Formats: - * VENDOR: [a-z0-9-]+ - * PACKAGE: [a-z0-9\.-]+ - * CATEGORY: ([a-z0-9-]+)? (clarifies the intent of the message, e.g. command, request, event, response, etc.) - * MESSAGE: [a-z0-9-]+ - * VERSION: @see SchemaVersion::VALID_PATTERN - * - * Examples of fully qualified schema ids: - * pbj:acme:videos:event:video-uploaded:1-0-0 - * pbj:acme:users:command:register-user:1-1-0 - * pbj:acme:api.videos:request:get-video:1-0-0 - * - * The fully qualified schema identifier corresponds to a json schema implementing the Gdbots PBJ Json Schema. - * - * The schema id must be resolveable to a php class that should be able to read and write - * messages with payloads that validate using the json schema. The target class is ideally - * major revision specific. As in GetVideoV1, GetVideoV2, etc. Only "major" revisions - * should require a unique class since all other schema changes should not break anything. - * - * @see SchemaVersion - * - */ -export default class SchemaId -{ - /** - * @param string vendor - * @param string packageName - * @param string category - * @param string message - * @param string version - */ - constructor(vendor, packageName, category, message, version) { - if (!category) { - category = ''; - } - - privateProps.set(this, { - /** @var string */ - vendor: vendor, - - /** @var string */ - package: packageName, - - /** @var string */ - category: category, - - /** @var string */ - message: message, - - /** @var SchemaVersion */ - version: SchemaVersion.fromString(version), - - /** @var string */ - id: 'pbj:' + vendor + ':' + packageName + ':' + category + ':' + message + ':' + version.toString(), - - /** - * The curie is the short name for the schema (without the version) that can be used - * to reference another message without fully qualifying the version. - * - * @var SchemaCurie - */ - curie: null - }); - - privateProps.get(this).curie = SchemaCurie.fromId(this); - } - - /** - * @param string schemaId - * - * @return SchemaId - * - * @throws InvalidSchemaId - */ - static fromString(schemaId) { - if (undefined !== _instances[schemaId]) { - return _instances[schemaId]; - } - - if (schemaId.length > 145) { - throw new Error('Schema id cannot be greater than 150 chars.'); - } - - let matches = schemaId.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaId('Schema id [' + schemaId + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - _instances[schemaId] = new this(matches[1], matches[2], matches[3], matches[4], matches[5]); - return _instances[schemaId]; - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).id; - } - - /** - * @return string - */ - getVendor() { - return privateProps.get(this).vendor; - } - - /** - * @return string - */ - getPackage() { - return privateProps.get(this).package; - } - - /** - * @return string - */ - getCategory() { - return privateProps.get(this).category; - } - - /** - * @return string - */ - getMessage() { - return privateProps.get(this).message; - } - - /** - * @return SchemaVersion - */ - getVersion() { - return privateProps.get(this).version; - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).curie; - } - - /** - * Returns the major version qualified curie. This should be used by the MessageResolver, - * event dispatchers, etc. where consumers will need to be able to reliably type hint or - * locate classes and provide functionality for a given message, with the expectation - * that a major revision is likely not compatible with another major revision of the - * same message. - * - * e.g. "vendor:package:category:message:v1" - * - * @return string - */ - getCurieMajor() { - return privateProps.get(this).curie.toString() + ':v' + privateProps.get(this).version.getMajor(); - } -} diff --git a/src/schema-q-name.js b/src/schema-q-name.js deleted file mode 100644 index 0f4094b..0000000 --- a/src/schema-q-name.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict'; - -import InvalidSchemaQName from 'gdbots/pbj/exception/invalid-schema-q-name'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Regular expression pattern for matching a valid SchemaCurie string. - * @constant string - */ -export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9-]+)$/; - -/** - * Schemas can be referenced in an extremely compact manner using a QName. - * This is NOT 100% reliably unique as the larger your app is the more likely the - * same message name will be duplicated in another service. - * @link https://en.wikipedia.org/wiki/QName - * - * Schema QName Format: - * vendor:message - * - * @see SchemaId - * @see SchemaCurie - * - */ -export default class SchemaQName -{ - /** - * @param string vendor - * @param string message - */ - constructor(vendor, message) { - privateProps.set(this, { - /** @var string */ - vendor: vendor, - - /** @var string */ - message: message, - - /** @var string */ - qname: vendor + ':' + message - }); - } - - /** - * @param SchemaId id - * - * @return SchemaQName - */ - static fromId(id) { - return this.fromCurie(id.getCurie()); - } - - /** - * @param SchemaCurie curie - * - * @return SchemaQName - */ - static fromCurie(curie) { - let qname = curie.getVendor() + ':' + curie.getMessage(); - - if (undefined !== _instances[qname]) { - return _instances[qname]; - } - - _instances[qname] = new this(curie.getVendor(), curie.getMessage()); - return _instances[qname]; - } - - /** - * @param string qname - * - * @return SchemaQName - * - * @throws InvalidSchemaQName - */ - static fromString(qname) { - if (undefined !== _instances[qname]) { - return _instances[qname]; - } - - let matches = qname.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaCurie('SchemaQName [' + qname + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - _instances[qname] = new this(matches[1], matches[4]); - return _instances[qname]; - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).qname; - } - - /** - * @return string - */ - getVendor() { - return privateProps.get(this).vendor; - } - - /** - * @return string - */ - getMessage() { - return privateProps.get(this).message; - } -} diff --git a/src/schema-version.js b/src/schema-version.js deleted file mode 100644 index f0e0767..0000000 --- a/src/schema-version.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -import InvalidSchemaVersion from 'gdbots/pbj/exception/invalid-schema-version'; - -/** - * Regular expression pattern for matching a valid SchemaVersion string. - * @constant string - */ -export const VALID_PATTERN = /^([0-9]+)-([0-9]+)-([0-9]+)/; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Similar to semantic versioning but with dashes and no "alpha, beta, etc." qualifiers. - * - * E.g. 1-0-0 (major-minor-patch) - * - * MAJOR - * Is incremented when a change is made which breaks the rules of Protobuf/Thrift backward compatibility, - * such as changing the type of a field. - * - * MINOR - * Is a change which is backward compatible but not forward compatible. Records created from - * the old version of the schema can be deserialized using the new schema, but not the other way - * around. Example: adding a new field to a union type. - * - * PATCH - * Is a change which is both backward compatible and forward compatible. The previous version of - * the schema can be used to deserialize records created from the new version of the schema, and - * vice versa. Example: adding a new optional field. - * - * @link http://semver.org/ - * @link http://snowplowanalytics.com/blog/2014/05/13/introducing-schemaver-for-semantic-versioning-of-schemas/ - * - */ -export default class SchemaVersion -{ - /** - * @param int major - * @param int minor - * @param int patch - */ - constructor(major = 1, minor = 0, patch = 0) { - major = parseInt(major); - minor = parseInt(minor); - patch = parseInt(patch); - - privateProps.set(this, { - /** @var int */ - major: major, - - /** @var int */ - minor: minor, - - /** @var int */ - patch: patch, - - /** - * E.g. 1-0-0 (major-minor-patch) - * - * @var string - */ - version: major + '-' + minor + '-' + patch - }); - } - - /** - * @param string version SchemaVersion string, e.g. 1-0-0 - * - * @return SchemaVersion - * - * @throws InvalidSchemaVersion - */ - static fromString(version = '1-0-0') { - let matches = version.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaVersion('Schema version [' + version + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - return new this(matches[1], matches[2], matches[3]); - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).version; - } - - /** - * @return int - */ - getMajor() { - return privateProps.get(this).major; - } - - /** - * @return int - */ - getMinor() { - return privateProps.get(this).minor; - } - - /** - * @return int - */ - getPatch() { - return privateProps.get(this).patch; - } -} diff --git a/src/schema.js b/src/schema.js deleted file mode 100644 index 2c5377d..0000000 --- a/src/schema.js +++ /dev/null @@ -1,294 +0,0 @@ -'use strict'; - -import ToArray from 'gdbots/common/to-array'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import FieldAlreadyDefined from 'gdbots/pbj/exception/field-already-defined'; -import FieldNotDefined from 'gdbots/pbj/exception/field-not-defined'; -import FieldOverrideNotCompatible from 'gdbots/pbj/exception/field-override-not-compatible'; -import MixinAlreadyAdded from 'gdbots/pbj/exception/mixin-already-added'; -import MixinNotDefined from 'gdbots/pbj/exception/mixin-not-defined'; -import SchemaId from 'gdbots/pbj/schema-id'; -import FieldBuilder from 'gdbots/pbj/field-builder'; -import StringType from 'gdbots/pbj/type/string-type'; - -export const PBJ_FIELD_NAME = '_schema'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Schema extends SystemUtils.mixinClass(null, ToArray) -{ - /** - * @param SchemaId|string id - * @param string className - * @param Field[] fields - * @param Mixin[] mixins - */ - constructor(id, className, fields = [], mixins = []) { - super(); // require before using `this` - - privateProps.set(this, { - /** @var string */ - id: 'SchemaId' === SystemUtils.getClass(id) ? id : SchemaId.fromString(id), - - /** @var string */ - className: className, - - /** @var Mixin[] */ - mixins: {}, - mixinsByCurie: {}, - - /** @var Field[] */ - fields: {}, - requiredFields: {}, - - /** @var string[] */ - mixinIds: [], - mixinCuries: [] - }); - - addField.bind(this)( - FieldBuilder.create(PBJ_FIELD_NAME, StringType.create()) - .required() - .pattern(SchemaId.VALID_PATTERN) - .withDefault(privateProps.get(this).id.toString()) - .build() - ); - - ArrayUtils.each(mixins, function(mixin) { - addMixin.bind(this)(mixin); - }.bind(this)); - - ArrayUtils.each(fields, function(field) { - addField.bind(this)(field); - }.bind(this)); - - /** @var string[] */ - privateProps.get(this).mixinIds = Object.keys(privateProps.get(this).mixins); - - /** @var string[] */ - privateProps.get(this).mixinCuries = Object.keys(privateProps.get(this).mixinsByCurie); - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).id.toString(); - } - - /** - * @return array - */ - toArray() { - return { - 'id': privateProps.get(this).id, - 'curie': this.getCurie(), - 'curie_major': this.getCurieMajor(), - 'qname': this.getQName(), - 'class_name': privateProps.get(this).className, - 'mixins': privateProps.get(this).mixins.map( - function(mixin) { - return mixin.getId(); - } - ), - 'fields': privateProps.get(this).fields - }; - } - - /** - * @return SchemaId - */ - getId() { - return privateProps.get(this).id; - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).id.getCurie(); - } - - /** - * @param string|SchemaCurie curie - * - * @return bool - */ - isA(curie) { - if ('SchemaCurie' === SystemUtils.getClass(curie)) { - curie = curie.toString(); - } - - return curie === this.getCurie().toString(); - } - - /** - * @see SchemaId::getCurieMajor - * - * @return string - */ - getCurieMajor() { - return privateProps.get(this).id.getCurieMajor(); - } - - /** - * @return SchemaQName - */ - getQName() { - return privateProps.get(this).id.getCurie().getQName(); - } - - /** - * @return string - */ - getClassName() { - return privateProps.get(this).className; - } - - /** - * @param string fieldName - * - * @return bool - */ - hasField(fieldName) { - return undefined !== privateProps.get(this).fields[fieldName]; - } - - /** - * @param string fieldName - * - * @return Field - * - * @throws FieldNotDefined - */ - getField(fieldName) { - if (undefined === privateProps.get(this).fields[fieldName]) { - throw new FieldNotDefined(this, fieldName); - } - - return privateProps.get(this).fields[fieldName]; - } - - /** - * @return Field[] - */ - getFields() { - return privateProps.get(this).fields; - } - - /** - * @return Field[] - */ - getRequiredFields() { - return privateProps.get(this).requiredFields; - } - - /** - * Returns true if the mixin is on this schema. Id provided can be - * qualified to major rev or just the curie. - * @see SchemaId::getCurieMajor - * - * @param string mixinId - * - * @return bool - */ - hasMixin(mixinId) { - return undefined !== privateProps.get(this).mixins[mixinId] || undefined !== privateProps.get(this).mixinsByCurie[mixinId]; - } - - /** - * @param string mixinId - * - * @return Mixin - * - * @throws MixinNotDefined - */ - getMixin(mixinId) { - if (undefined !== privateProps.get(this).mixins[mixinId]) { - return privateProps.get(this).mixins[mixinId]; - } - - if (undefined !== privateProps.get(this).mixinsByCurie[mixinId]) { - return privateProps.get(this).mixinsByCurie[mixinId]; - } - - throw new MixinNotDefined(this, mixinId); - } - - /** - * @return Mixin[] - */ - getMixins() { - return privateProps.get(this).mixins; - } - - /** - * Returns an array of curies with the major rev. - * @see SchemaId::getCurieMajor - * - * @return array - */ - getMixinIds() { - return privateProps.get(this).mixinIds; - } - - /** - * Returns an array of curies (string version). - * - * @return array - */ - getMixinCuries() { - return privateProps.get(this).mixinCuries; - } -} - -/** - * @param Field field - * - * @throws FieldAlreadyDefined - * @throws FieldOverrideNotCompatible - */ -function addField(field) { - let fieldName = field.getName(); - if (this.hasField(fieldName)) { - let existingField = this.getField(fieldName); - if (!existingField.isOverridable()) { - throw new FieldAlreadyDefined(this, fieldName); - } - if (!existingField.isCompatibleForOverride(field)) { - throw new FieldOverrideNotCompatible(this, fieldName, field); - } - } - - privateProps.get(this).fields[fieldName] = field; - if (field.isRequired()) { - privateProps.get(this).requiredFields[fieldName] = field; - } -} - -/** - * @param Mixin mixin - * - * @throws MixinAlreadyAdded - */ -function addMixin(mixin) { - let id = mixin.getId(); - let curieStr = id.getCurie().toString(); - - if (undefined !== privateProps.get(this).mixinsByCurie[curieStr]) { - throw new MixinAlreadyAdded(this, privateProps.get(this).mixinsByCurie[curieStr], mixin); - } - - privateProps.get(this).mixins[id.getCurieMajor()] = mixin; - privateProps.get(this).mixinsByCurie[curieStr] = mixin; - - ArrayUtils.each(mixin.getFields(), function(field) { - addField.bind(this)(field); - }.bind(this)); -} diff --git a/src/serializer/array-serializer.js b/src/serializer/array-serializer.js deleted file mode 100644 index b566180..0000000 --- a/src/serializer/array-serializer.js +++ /dev/null @@ -1,273 +0,0 @@ -'use strict'; - -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import DynamicField from 'gdbots/pbj/well-known/dynamic-field'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import DeserializeMessageFailed from 'gdbots/pbj/exception/deserialize-message-failed'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import InvalidResolvedSchema from 'gdbots/pbj/exception/invalid-resolved-schema'; -import Serializer from 'gdbots/pbj/serializer/serializer'; -import TypeName from 'gdbots/pbj/enum/type-name'; -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Codec from 'gdbots/pbj/codec'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import SchemaId from 'gdbots/pbj/schema-id'; -import {PBJ_FIELD_NAME} from 'gdbots/pbj/schema'; - -/** - * Options for the serializer to use, e.g. json encoding options, - * 'includeAllFields' which includes fields even if they're not set, etc. - * - * @var array - */ -let _options = {}; - -export default class ArraySerializer extends SystemUtils.mixinClass(Serializer, Codec) -{ - /** - * {@inheritdoc} - */ - serialize(message, options = {}) { - _options = options; - - return doSerialize.bind(this)(message); - } - - /** - * {@inheritdoc} - */ - deserialize(data, options = {}) { - _options = options; - - if (-1 === Object.keys(data).indexOf(PBJ_FIELD_NAME)) { - throw new Error('[' + this.constructor.name + '::deserialize] Array provided must contain the [' + PBJ_FIELD_NAME +'] key.'); - } - - return doDeserialize.bind(this)(data); - } - - /** - * @param Message message - * @param Field field - * - * @return mixed - */ - encodeMessage(message, field) { - return doSerialize.bind(this)(message); - } - - /** - * @param mixed value - * @param Field field - * - * @return Message - */ - decodeMessage(value, field) { - return doDeserialize.bind(this)(value); - } - - /** - * @param MessageRef messageRef - * @param Field field - * - * @return mixed - */ - encodeMessageRef(messageRef, field) { - return messageRef.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return MessageRef - */ - decodeMessageRef(value, field) { - return MessageRef.fromArray(value); - } - - /** - * @param GeoPoint geoPoint - * @param Field field - * - * @return mixed - */ - encodeGeoPoint(geoPoint, field) { - return geoPoint.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return GeoPoint - */ - decodeGeoPoint(value, field) { - return GeoPoint.fromArray(value); - } - - /** - * @param DynamicField dynamicField - * @param Field field - * - * @return mixed - */ - encodeDynamicField(dynamicField, field) { - return dynamicField.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return DynamicField - */ - decodeDynamicField(value, field) { - return DynamicField.fromArray(value); - } -} - -/** - * @param Message message - * - * @return array - */ -function doSerialize(message) { - let schema = message.constructor.schema(); - message.validate(); - - let payload = {}; - let includeAllFields = undefined !== _options.includeAllFields && true === _options.includeAllFields; - - ArrayUtils.each(schema.getFields(), function(field) { - let fieldName = field.getName(); - - if (!message.has(fieldName)) { - if (includeAllFields || message.hasClearedField(fieldName)) { - payload[fieldName] = null; - } - - return; - } - - let value = message.get(fieldName); - let type = field.getType(); - - switch (field.getRule()) { - case FieldRule.A_SINGLE_VALUE: - payload[fieldName] = type.encode(value, field, this); - - break; - - case FieldRule.A_SET: - case FieldRule.A_LIST: - payload[fieldName] = []; - - ArrayUtils.each(value, function(v) { - payload[fieldName].push(type.encode(v, field, this)); - }.bind(this)); - - break; - - case FieldRule.A_MAP: - payload[fieldName] = {}; - - ArrayUtils.each(value, function(v) { - payload[fieldName][k] = type.encode(v, field, this); - }.bind(this)); - - break; - - default: - break; - } - }.bind(this)); - - return payload; -} - -/** - * @param array data - * - * @return Message - * - * @throws \Exception - * @throws GdbotsPbjException - */ -function doDeserialize(data) { - - /** @var SchemaId schemaId */ - let schemaId = SchemaId.fromString(data[PBJ_FIELD_NAME]); - - /** @var Message message */ - let message = MessageResolver.resolveId(schemaId); - if (!message.hasTrait('Message')) { - throw new Error('Invalid message.'); - } - - message = message.create(); - - if (message.constructor.schema().getCurieMajor() !== schemaId.getCurieMajor()) { - throw new InvalidResolvedSchema(message.constructor.schema(), schemaId, message.name); - } - - let schema = message.constructor.schema(); - - ArrayUtils.each(data, function(value, fieldName) { - if (!schema.hasField(fieldName)) { - return; - } - - if (null === value) { - message.clear(fieldName); - return; - } - - let field = schema.getField(fieldName); - let type = field.getType(); - - switch (field.getRule()) { - case FieldRule.A_SINGLE_VALUE: - message.set(fieldName, type.decode(value, field, this)); - break; - - case FieldRule.A_SET: - case FieldRule.A_LIST: - if (!Array.isArray(value)) { - throw new Error('Field [' + fieldName + '] must be an array.'); - } - - let values = []; - - ArrayUtils.each(value, function(v) { - values.push(type.decode(v, field, this)); - }.bind(this)); - - if (field.isASet()) { - message.addToSet(fieldName, values); - } else { - message.addToList(fieldName, values); - } - - break; - - case FieldRule.A_MAP: - if (!ArrayUtils.isAssoc(value)) { - throw new Error('Field [' + fieldName + '] must be an associative array.'); - } - - ArrayUtils.each(value, function(v, k) { - message.addToMap(fieldName, k, type.decode(v, field, this)); - }.bind(this)); - - break; - - default: - break; - } - }.bind(this)); - - return message.set(PBJ_FIELD_NAME, schema.getId().toString()).populateDefaults(); -} diff --git a/src/serializer/json-serializer.js b/src/serializer/json-serializer.js deleted file mode 100644 index 43c76d0..0000000 --- a/src/serializer/json-serializer.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import DeserializeMessageFailed from 'gdbots/pbj/exception/deserialize-message-failed'; -import ArraySerializer from 'gdbots/pbj/serializer/array-serializer'; - -export default class JsonSerializer extends SystemUtils.mixinClass(ArraySerializer) -{ - /** - * {@inheritdoc} - */ - serialize(message, options = {}) { - return JSON.stringify(super.serialize(message, options)); - } - - /** - * {@inheritdoc} - */ - deserialize(data, options = {}) { - if ('string' === typeof data) { - try { - data = JSON.parse(data); - } catch (e) { - if (!(e instanceof SyntaxError)) { - throw new Error('Unexpected error type in JSON.parse()') - } - - throw new DeserializeMessageFailed(getLastErrorMessage(4)); - } - } - - return super.deserialize(data, options); - } -} - -/** - * Resolves json_last_error message. - * - * @param int code - * - * @return string - */ -function getLastErrorMessage(code) { - switch (code) { - case 0: //JSON_ERROR_DEPTH - return 'Maximum stack depth exceeded'; - case 2: //JSON_ERROR_STATE_MISMATCH - return 'Underflow or the modes mismatch'; - case 3: //JSON_ERROR_CTRL_CHAR - return 'Unexpected control character found'; - case 4: //JSON_ERROR_SYNTAX - return 'Syntax error, malformed JSON'; - case 5: //JSON_ERROR_UTF8 - return 'Malformed UTF-8 characters, possibly incorrectly encoded'; - default: - return 'Unknown error'; - } -} diff --git a/src/serializer/serializer.js b/src/serializer/serializer.js deleted file mode 100644 index 5ec7ea5..0000000 --- a/src/serializer/serializer.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -export default class Serializer -{ - /** - * @param Message message - * @param array options - * - * @return mixed - */ - serialize(message, options = {}) { - throw new Error('Interface function.'); - } - - /** - * @param mixed data - * @param array options - * - * @return Message - * - * @throws \Exception - * @throws GdbotsPbjException - */ - deserialize(data, options = {}) { - throw new Error('Interface function.'); - } -} diff --git a/src/serializers/JsonSerializer.js b/src/serializers/JsonSerializer.js new file mode 100644 index 0000000..27a38f3 --- /dev/null +++ b/src/serializers/JsonSerializer.js @@ -0,0 +1,36 @@ +import AssertionFailed from '../exceptions/AssertionFailed'; +import ObjectSerializer from './ObjectSerializer'; + +export default class JsonSerializer { + /** + * @param {Message} message + * @param {Object} options + * + * @returns {string} + * + * @throws {GdbotsPbjException} + */ + static serialize(message, options = {}) { + return JSON.stringify(ObjectSerializer.serialize(message, options)); + } + + /** + * @param {string} json + * @param {Object} options + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + static deserialize(json, options = {}) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return ObjectSerializer.deserialize(obj, options); + } +} diff --git a/src/serializers/ObjectSerializer.js b/src/serializers/ObjectSerializer.js new file mode 100644 index 0000000..59e75e5 --- /dev/null +++ b/src/serializers/ObjectSerializer.js @@ -0,0 +1,213 @@ +/* eslint-disable no-unused-vars */ +import isArray from 'lodash/isArray'; +import isPlainObject from 'lodash/isPlainObject'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import InvalidResolvedSchema from '../exceptions/InvalidResolvedSchema'; +import DynamicField from '../well-known/DynamicField'; +import GeoPoint from '../well-known/GeoPoint'; +import MessageRef from '../MessageRef'; +import MessageResolver from '../MessageResolver'; +import { PBJ_FIELD_NAME } from '../Schema'; +import SchemaId from '../SchemaId'; + +let opt = {}; + +export default class ObjectSerializer { + /** + * @param {Message} message + * @param {Object} options + * + * @returns {Object} + * + * @throws {GdbotsPbjException} + */ + static serialize(message, options = {}) { + opt = options; + const schema = message.schema(); + message.validate(); + + const payload = {}; + const includeAllFields = opt.includeAllFields || false; + + schema.getFields().forEach((field) => { + const fieldName = field.getName(); + if (!message.has(fieldName)) { + if (includeAllFields || message.hasClearedField(fieldName)) { + payload[fieldName] = null; + } + + return; + } + + const value = message.get(fieldName); + const type = field.getType(); + + if (field.isASingleValue()) { + payload[fieldName] = type.encode(value, field, this); + return; + } + + if (field.isAMap()) { + payload[fieldName] = {}; + // eslint-disable-next-line no-return-assign + Object.keys(value).forEach(k => payload[fieldName][k] = type.encode(value[k], field, this)); + return; + } + + payload[fieldName] = []; + value.forEach(v => payload[fieldName].push(type.encode(v, field, this))); + }); + + return payload; + } + + /** + * @param {Object} obj + * @param {Object} options + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + static deserialize(obj, options = {}) { + opt = options; + if (!obj[PBJ_FIELD_NAME]) { + throw new AssertionFailed(`Object provided must contain the [${PBJ_FIELD_NAME}] key.`); + } + + const schemaId = SchemaId.fromString(obj[PBJ_FIELD_NAME]); + const message = new (MessageResolver.resolveId(schemaId))(); + const schema = message.schema(); + + if (schema.getCurieMajor() !== schemaId.getCurieMajor()) { + throw new InvalidResolvedSchema(schema, schemaId); + } + + Object.keys(obj).forEach((fieldName) => { + if (!schema.hasField(fieldName)) { + return; + } + + const value = obj[fieldName]; + if (value === null) { + message.clear(fieldName); + return; + } + + const field = schema.getField(fieldName); + const type = field.getType(); + + if (field.isASingleValue()) { + message.set(fieldName, type.decode(value, field, this)); + return; + } + + if (field.isASet() || field.isAList()) { + if (!isArray(value)) { + throw new AssertionFailed(`Field [${fieldName}] must be an array.`); + } + + const values = []; + value.forEach(v => values.push(type.decode(v, field, this))); + + if (field.isASet()) { + message.addToSet(fieldName, values); + } else { + message.addToList(fieldName, values); + } + + return; + } + + if (!isPlainObject(value)) { + throw new AssertionFailed(`Field [${fieldName}] must be an object.`); + } + + Object.keys(value).forEach((k) => { + message.addToMap(fieldName, k, type.decode(value[k], field, this)); + }); + }); + + return message.set(PBJ_FIELD_NAME, schema.getId().toString()).populateDefaults(); + } + + /** + * @param {Message} message + * @param {Field} field + * + * @returns {Object} + */ + static encodeMessage(message, field) { + return this.serialize(message, opt); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {Message} + */ + static decodeMessage(value, field) { + return this.deserialize(value, opt); + } + + /** + * @param {MessageRef} messageRef + * @param {Field} field + * + * @returns {Object} + */ + static encodeMessageRef(messageRef, field) { + return messageRef.toObject(); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {MessageRef} + */ + static decodeMessageRef(value, field) { + return MessageRef.fromObject(value); + } + + /** + * @param {GeoPoint} geoPoint + * @param {Field} field + * + * @returns {Object} + */ + static encodeGeoPoint(geoPoint, field) { + return geoPoint.toObject(); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {GeoPoint} + */ + static decodeGeoPoint(value, field) { + return GeoPoint.fromObject(value); + } + + /** + * @param {DynamicField} dynamicField + * @param {Field} field + * + * @returns {Object} + */ + static encodeDynamicField(dynamicField, field) { + return dynamicField.toObject(); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {DynamicField} + */ + static decodeDynamicField(value, field) { + return DynamicField.fromObject(value); + } +} diff --git a/src/type/abstract-binary-type.js b/src/type/abstract-binary-type.js deleted file mode 100644 index ae99f1c..0000000 --- a/src/type/abstract-binary-type.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -import NumberUtils from 'gdbots/common/util/number-utils'; -import UrlUtils from 'gdbots/common/util/url-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class AbstractBinaryType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - constructor(typeName) { - super(typeName); - - privateProps.set(this, { - /** @var bool */ - decodeFromBase64: true, - - /** @var bool */ - encodeToBase64: true - }); - } - - /** - * @param bool useBase64 - */ - decodeFromBase64(useBase64) { - privateProps.get(this).decodeFromBase64 = Boolean(useBase64); - } - - /** - * @param bool useBase64 - */ - encodeToBase64(useBase64) { - privateProps.get(this).encodeToBase64 = Boolean(useBase64); - } - - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('string' !== typeof value) { - throw new Error('Value must be a string.'); - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = privateProps.get(this).encodeToBase64 ? this.encode(value, field).length : value.length; - let minLength = field.getMinLength(); - let maxLength = NumberUtils.bound(field.getMaxLength(), minLength, this.getMaxBytes()); - let okay = length >= minLength && length <= maxLength; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [' + minLength + '] and [' + maxLength + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - return privateProps.get(this).encodeToBase64 ? UrlUtils.base64Encode(value) : value; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - if (!privateProps.get(this).decodeFromBase64) { - return value; - } - - value = UrlUtils.base64Decode(value); - if (false === value) { - throw new DecodeValueFailed(value, field, 'Strict base64_decode failed.'); - } - - return value; - } - - /** - * {@inheritdoc} - */ - isBinary() { - return true; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/type/abstract-int-type.js b/src/type/abstract-int-type.js deleted file mode 100644 index f865d66..0000000 --- a/src/type/abstract-int-type.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -import NumberUtils from 'gdbots/common/util/number-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class AbstractIntType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('number' !== typeof value) { - throw new Error('Value must be a number.'); - } - - let min = NumberUtils.bound(field.getMin(), this.getMin(), this.getMax()); - let max = NumberUtils.bound(field.getMax(), this.getMin(), this.getMax()); - let okay = value >= min && value <= max; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] value must be between [' + min + '] and [' + max + '], [' + value + '] given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/abstract-string-type.js b/src/type/abstract-string-type.js deleted file mode 100644 index 5ca1b65..0000000 --- a/src/type/abstract-string-type.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -import NumberUtils from 'gdbots/common/util/number-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class AbstractStringType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('string' !== typeof value) { - throw new Error('Value must be a string.'); - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = value.length; - let minLength = field.getMinLength(); - let maxLength = NumberUtils.bound(field.getMaxLength(), minLength, this.getMaxBytes()); - let okay = length >= minLength && length <= maxLength; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [' + minLength + '] and [' + maxLength + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - return value; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - return value; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/type/big-int-type.js b/src/type/big-int-type.js deleted file mode 100644 index 976f5d0..0000000 --- a/src/type/big-int-type.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import BigNumber from 'gdbots/pbj/well-known/big-number'; -import Type from 'gdbots/pbj/type/type'; - -export default class BigIntType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('BigNumber' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "BigNumber" but is not.'); - } - - if (value.isNegative()) { - throw new Error('Field [' + field.getName() + '] cannot be negative.'); - } - - if (!value.lessThanOrEqualTo('18446744073709551615')) { - throw new Error('Field [' + field.getName() + '] cannot be greater than [18446744073709551615].'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if ('BigNumber' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return '0'; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value || 'BigNumber' === SystemUtils.getClass(value)) { - return value; - } - - return new BigNumber(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return new BigNumber(0); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/binary-type.js b/src/type/binary-type.js deleted file mode 100644 index b571dfd..0000000 --- a/src/type/binary-type.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractBinaryType from 'gdbots/pbj/type/abstract-binary-type'; - -export default class BinaryType extends SystemUtils.mixinClass(AbstractBinaryType) -{ - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 255; - } -} diff --git a/src/type/blob-type.js b/src/type/blob-type.js deleted file mode 100644 index fa465ea..0000000 --- a/src/type/blob-type.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractBinaryType from 'gdbots/pbj/type/abstract-binary-type'; - -export default class BlobType extends SystemUtils.mixinClass(AbstractBinaryType) -{ - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/boolean-type.js b/src/type/boolean-type.js deleted file mode 100644 index e307bbb..0000000 --- a/src/type/boolean-type.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class BooleanType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (value !== true && value !== false) { - throw new Error('Value "' + value + '" is not a boolean.') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return Boolean(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return Boolean(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return false; - } - - /** - * @see Type::isBoolean - */ - isBoolean() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/date-time-type.js b/src/type/date-time-type.js deleted file mode 100644 index 683d3d3..0000000 --- a/src/type/date-time-type.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -import DateUtils from 'gdbots/common/util/date-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DateTimeType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(value instanceof Date)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Date" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value instanceof Date) { - return value.toISOString(); //same format as ISO8601_ZULU - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - if (value instanceof Date) { - return value; - } - - return new Date(); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/date-type.js b/src/type/date-type.js deleted file mode 100644 index 4551508..0000000 --- a/src/type/date-type.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DateType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(value instanceof Date)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Date" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value instanceof Date) { - return value.toISOString().slice(0,10); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - if (value instanceof Date) { - return value; - } - - return new Date(); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/decimal-type.js b/src/type/decimal-type.js deleted file mode 100644 index 97ea684..0000000 --- a/src/type/decimal-type.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DecimalType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(+value === value && (!isFinite(value) || !!(value % 1)))) { - throw new Error('Value "' + value + '" is not a float.') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseFloat(bcadd(parseFloat(value), '0', field.getScale())); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseFloat(bcadd(parseFloat(value), '0', field.getScale())); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0.0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return -1; - } - - /** - * {@inheritdoc} - */ - getMax() { - return Infinity; - } -} - -/** - * Add two arbitrary precision numbers - * - * @param string leftOperand The left operand, as a string. - * @param string rightOperand The right operand, as a string. - * @param int scale This optional parameter is used to set the number of - * digits after the decimal place in the result. If omitted, - * it will default to the scale set globally with the bcscale() - * function, or fallback to 0 if this has not been set. - * - * @return string - */ -function bcadd(leftOperand, rightOperand, scale) { - throw new Error('Not yet implemented.'); -} diff --git a/src/type/dynamic-field-type.js b/src/type/dynamic-field-type.js deleted file mode 100644 index 776ab5a..0000000 --- a/src/type/dynamic-field-type.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DynamicFieldType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('DynamicField' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "DynamicField" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeDynamicField(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeDynamicField(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/float-type.js b/src/type/float-type.js deleted file mode 100644 index 804d5ac..0000000 --- a/src/type/float-type.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class FloatType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(+value === value && (!isFinite(value) || !!(value % 1)))) { - throw new Error('Value "' + value + '" is not a float.') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseFloat(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseFloat(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0.0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return -1; - } - - /** - * {@inheritdoc} - */ - getMax() { - return Infinity; - } -} diff --git a/src/type/geo-point-type.js b/src/type/geo-point-type.js deleted file mode 100644 index 3d85c1a..0000000 --- a/src/type/geo-point-type.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -export default class GeoPointType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('GeoPoint' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "GeoPoint" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeGeoPoint(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeGeoPoint(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/identifier-type.js b/src/type/identifier-type.js deleted file mode 100644 index 375b47e..0000000 --- a/src/type/identifier-type.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; -import Type from 'gdbots/pbj/type/type'; - -export default class IdentifierType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Identifier')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Identifier" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = value.toString().length; - let maxBytes = this.getMaxBytes(); - let okay = length > 0 && length <= maxBytes; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [1] and [' + maxBytes + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('Identifier')) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - /** @var Identifier instance */ - let instance = field.getInstance(); - - try { - return instance.fromString(value); - } catch (e) { - throw new DecodeValueFailed(value, field, e); - } - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 100; - } -} diff --git a/src/type/int-enum-type.js b/src/type/int-enum-type.js deleted file mode 100644 index 8c54915..0000000 --- a/src/type/int-enum-type.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -export default class IntEnumType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Enum')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Enum" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - if (value === +value && isFinite(value) && !(value % 1)) { - throw new Error('Value "' + value + '" is not a integer.') - } - - if (value.getValue() < this.getMin() || value.getValue() > this.getMax()) { - throw new Error('Number "' + value.getValue() + '" was expected to be at least "' + value.getMin() + '" and at most "' + value.getMax() + '".') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('Enum')) { - return parseInt(value.getValue()); - } - - return 0; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value) { - return null; - } - - /** @var Enum instance */ - let instance = field.getInstance(); - let enumValue = null; - - ArrayUtils.each(instance.enumValues, function(item) { - if (value === parseInt(item.getValue())) { - enumValue = item; - } - }); - - if (null === enumValue) { - throw new DecodeValueFailed(value, field, e); - } - - return enumValue; - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 65535; - } -} diff --git a/src/type/int-type.js b/src/type/int-type.js deleted file mode 100644 index 8c4fe59..0000000 --- a/src/type/int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class IntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 4294967295; - } -} diff --git a/src/type/medium-blob-type.js b/src/type/medium-blob-type.js deleted file mode 100644 index 274a14f..0000000 --- a/src/type/medium-blob-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractBinaryType from 'gdbots/pbj/type/abstract-binary-type'; - -export default class MediumBlobType extends SystemUtils.mixinClass(AbstractBinaryType) -{ - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 16777215; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/medium-int-type.js b/src/type/medium-int-type.js deleted file mode 100644 index 386db7c..0000000 --- a/src/type/medium-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class MediumIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 16777215; - } -} diff --git a/src/type/medium-text-type.js b/src/type/medium-text-type.js deleted file mode 100644 index a411b11..0000000 --- a/src/type/medium-text-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractStringType from 'gdbots/pbj/type/abstract-string-type'; - -export default class MediumTextType extends SystemUtils.mixinClass(AbstractStringType) -{ - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 16777215; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/message-ref-type.js b/src/type/message-ref-type.js deleted file mode 100644 index 9fba813..0000000 --- a/src/type/message-ref-type.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; -import Type from 'gdbots/pbj/type/type'; - -export default class MessageRefType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('MessageRef' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "MessageRef" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeMessageRef(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeMessageRef(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } -} diff --git a/src/type/message-type.js b/src/type/message-type.js deleted file mode 100644 index 22452d9..0000000 --- a/src/type/message-type.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; -import Type from 'gdbots/pbj/type/type'; - -export default class MessageType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Message')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Message" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - if (!field.getAnyOfInstances()) { - return; - } - - let instances = field.getAnyOfInstances(); - if (!instances || instances.length === 0) { - // means it can be "any message" - return; - } - - let found = false; - let classNames = []; - ArrayUtils.each(instances, function(instance) { - classNames.push(instance.name); - - if (value.hasTrait(instance.name) || instance.name === SystemUtils.getClass(value)) { - found = true; - } - }); - - if (!found) { - throw new Error('Field [' + field.getName() + '] must be an instance of at least one of: ' + classNames.join(',') + '.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeMessage(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeMessage(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isMessage() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/microtime-type.js b/src/type/microtime-type.js deleted file mode 100644 index 204f364..0000000 --- a/src/type/microtime-type.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Microtime from 'gdbots/pbj/well-known/microtime'; -import Type from 'gdbots/pbj/type/type'; - -export default class MicrotimeType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('Microtime' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Microtime" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if ('Microtime' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - if ('Microtime' === SystemUtils.getClass(value)) { - return value; - } - - return Microtime.fromString(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return Microtime.create(); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/signed-big-int-type.js b/src/type/signed-big-int-type.js deleted file mode 100644 index f48ef1f..0000000 --- a/src/type/signed-big-int-type.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import BigNumber from 'gdbots/pbj/well-known/big-number'; -import Type from 'gdbots/pbj/type/type'; - -export default class SignedBigIntType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('BigNumber' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "BigNumber" but is not.'); - } - - if (!value.greaterThanOrEqualTo('-9223372036854775808')) { - throw new Error('Field [' + field.getName() + '] cannot be less than [-9223372036854775808].'); - } - - if (!value.lessThanOrEqualTo('9223372036854775807')) { - throw new Error('Field [' + field.getName() + '] cannot be greater than [9223372036854775807].'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if ('BigNumber' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return '0'; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value || 'BigNumber' === SystemUtils.getClass(value)) { - return value; - } - - return new BigNumber(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return new BigNumber(0); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/signed-int-type.js b/src/type/signed-int-type.js deleted file mode 100644 index 4b3059b..0000000 --- a/src/type/signed-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -2147483648; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 2147483647; - } -} diff --git a/src/type/signed-medium-int-type.js b/src/type/signed-medium-int-type.js deleted file mode 100644 index 392bd02..0000000 --- a/src/type/signed-medium-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedMediumIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -8388608; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 8388607; - } -} diff --git a/src/type/signed-small-int-type.js b/src/type/signed-small-int-type.js deleted file mode 100644 index cfd96f4..0000000 --- a/src/type/signed-small-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedSmallIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -32768; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 32767; - } -} diff --git a/src/type/signed-tiny-int-type.js b/src/type/signed-tiny-int-type.js deleted file mode 100644 index 4d9c7e1..0000000 --- a/src/type/signed-tiny-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedTinyIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -128; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 127; - } -} diff --git a/src/type/small-int-type.js b/src/type/small-int-type.js deleted file mode 100644 index 37d9167..0000000 --- a/src/type/small-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SmallIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 65535; - } -} diff --git a/src/type/string-enum-type.js b/src/type/string-enum-type.js deleted file mode 100644 index cff94a6..0000000 --- a/src/type/string-enum-type.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -export default class StringEnumType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Enum')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Enum" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - if ('string' !== typeof value.getValue()) { - throw new Error('Value "' + value.getValue() + '" expected to be string.') - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = value.toString().length; - let maxBytes = this.getMaxBytes(); - let okay = length > 0 && length <= maxBytes; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [1] and [' + maxBytes + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('Enum')) { - return String(value.getValue()); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value) { - return null; - } - - /** @var Enum instance */ - let instance = field.getInstance(); - let enumValue = null; - - ArrayUtils.each(instance.enumValues, function(item) { - if (value === String(item.getValue())) { - enumValue = item; - } - }); - - if (null === enumValue) { - throw new DecodeValueFailed(value, field, e); - } - - return enumValue; - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 100; - } -} diff --git a/src/type/string-type.js b/src/type/string-type.js deleted file mode 100644 index ff0b139..0000000 --- a/src/type/string-type.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -import {default as DateUtils, ISO8601_ZULU, ISO8601} from 'gdbots/common/util/date-utils'; -import HashtagUtils from 'gdbots/common/util/hashtag-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractStringType from 'gdbots/pbj/type/abstract-string-type'; -import Format from 'gdbots/pbj/enum/format'; - -export default class StringType extends SystemUtils.mixinClass(AbstractStringType) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - super.guard(value, field); - - let pattern = field.getPattern(); - if (pattern && !new RegExp(pattern).test(value)) { - throw new Error('Value [' + value + '] is invalid. It must match the pattern [' + pattern + '].'); - } - - switch (field.getFormat()) { - case Format.UNKNOWN: - break; - - case Format.DATE: - if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.DATE_TIME: - if (!DateUtils.isValidISO8601Date(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid ISO8601 date-time. Format must match one of [' + ISO8601_ZULU + '], [' + ISO8601 + '] or [' + new Date().toISOString() + '].'); - } - - break; - - case Format.SLUG: - if (!/^([\w\/-]|[\w-][\w\/-]*[\w-])$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.EMAIL: - if (!/^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.HASHTAG: - if (!HashtagUtils.isValid(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid hashtag. @see HashtagUtils.isValid.'); - } - - break; - - case Format.IPV4: - if (!/^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.IPV6: - if (!/^((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.HOSTNAME: - case Format.URI: - case Format.URL: - if (!/(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.UUID: - if (!/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - default: - break; - } - } - - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 255; - } -} diff --git a/src/type/text-type.js b/src/type/text-type.js deleted file mode 100644 index 05dfd87..0000000 --- a/src/type/text-type.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractStringType from 'gdbots/pbj/type/abstract-string-type'; - -export default class TextType extends SystemUtils.mixinClass(AbstractStringType) -{ - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/time-uuid-type.js b/src/type/time-uuid-type.js deleted file mode 100644 index 178005a..0000000 --- a/src/type/time-uuid-type.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import TimeUuidIdentifier from 'gdbots/pbj/well-known/time-uuid-identifier'; -import Type from 'gdbots/pbj/type/type'; - -export default class TimeUuidType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('TimeUuidIdentifier') && 'TimeUuidIdentifier' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "TimeUuidIdentifier" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('TimeUuidIdentifier') || 'TimeUuidIdentifier' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - /** @var TimeUuidIdentifier instance */ - let instance = field.getInstance() || TimeUuidIdentifier; - if ('object' === typeof value - && ( - value.hasTrait(instance.name) - || instance.name === SystemUtils.getClass(value) - ) - ) { - return value; - } - - return instance.fromString(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return TimeUuidIdentifier.generate(); - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/type/timestamp-type.js b/src/type/timestamp-type.js deleted file mode 100644 index 411c9a0..0000000 --- a/src/type/timestamp-type.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -import DateUtils from 'gdbots/common/util/date-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class TimestampType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('number' !== typeof value) { - throw new Error('Value must be a number.'); - } - - if (!DateUtils.isValidTimestamp(value)) { - throw new Error('Field [' + field.getName() + '] value [' + value + '] is not a valid unix timestamp.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return new Date().getTime(); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/tiny-int-type.js b/src/type/tiny-int-type.js deleted file mode 100644 index 22f3ec0..0000000 --- a/src/type/tiny-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class TinyIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 255; - } -} diff --git a/src/type/trinary-type.js b/src/type/trinary-type.js deleted file mode 100644 index 7a9c9fd..0000000 --- a/src/type/trinary-type.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -/** - * @link https://en.wikipedia.org/wiki/Three-valued_logic - * 0 = unknown - * 1 = true - * 2 = false - */ -export default class TrinaryType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ([0, 1, 2].indexOf(value) === -1) { - throw new Error('Field [' + field.getName() + '] value [' + value + '] is not a valid. Must be 0, 1, or 2.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - let tmp = parseInt(value); - return isNaN(tmp) || !isFinite(tmp) ? 0 : tmp; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - let tmp = parseInt(value); - return isNaN(tmp) || !isFinite(tmp) ? 0 : tmp; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 2; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/type.js b/src/type/type.js deleted file mode 100644 index 567921e..0000000 --- a/src/type/type.js +++ /dev/null @@ -1,185 +0,0 @@ -'use strict'; - -import StringUtils from 'gdbots/common/util/string-utils'; -import TypeName from 'gdbots/pbj/enum/type-name'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Type -{ - /** - * Private constructor to ensure flyweight construction. - * - * @param TypeName typeName - */ - constructor(typeName) { - privateProps.set(this, { - /** @var TypeName */ - typeName: typeName - }); - } - - /** - * @return Type - */ - static create() { - let type = this.name; - if (undefined === _instances[type]) { - _instances[type] = new this(TypeName[StringUtils.toSnakeCase(type.replace('Type', '')).toUpperCase()]); - } - - return _instances[type]; - } - - /** - * @return TypeName - */ - getTypeName() { - return privateProps.get(this).typeName; - } - - /** - * Shortcut to returning the value of the TypeName - * - * @return string - */ - getTypeValue() { - return privateProps.get(this).typeName.value; - } - - /** - * @param mixed value - * @param Field field - * - * @throws \Exception - */ - guard(value, field) {} - - /** - * @param mixed value - * @param Field field - * @param Codec codec - * - * @return mixed - * - * @throws GdbotsPbjException - * @throws EncodeValueFailed - */ - encode(value, field, codec = null) {} - - /** - * @param mixed value - * @param Field field - * @param Codec codec - * - * @return mixed - * - * @throws GdbotsPbjException - * @throws DecodeValueFailed - */ - decode(value, field, codec = null) {} - - /** - * Returns true if the value gets decoded and stored during runtime as a scalar value. - * - * @return bool - */ - isScalar() { - return true; - } - - /** - * Returns true if the value gets encoded to a scalar value. This is important to - * know because a big int, date, enum, etc. is stored as an object on the message - * but when the message is encoded to an array, json, etc. it's a scalar value. - * - * @return bool - */ - encodesToScalar() { - return true; - } - - /** - * @return mixed - */ - getDefault() { - return null; - } - - /** - * @return bool - */ - isBoolean() { - return false; - } - - /** - * @return bool - */ - isBinary() { - return false; - } - - /** - * @return bool - */ - isNumeric() { - return false; - } - - /** - * @return bool - */ - isString() { - return false; - } - - /** - * @return bool - */ - isMessage() { - return false; - } - - /** - * Returns the minimum value supported by an integer type. - * - * @return int - */ - getMin() { - return -2147483648; - } - - /** - * Returns the maximum value supported by an integer type. - * - * @return int - */ - getMax() { - return 2147483647; - } - - /** - * Returns the maximum number of bytes supported by the string or binary type. - * - * @return int - */ - getMaxBytes() { - return 65535; - } - - /** - * @return bool - */ - allowedInSet() { - return true; - } -} diff --git a/src/type/uuid-type.js b/src/type/uuid-type.js deleted file mode 100644 index a45014a..0000000 --- a/src/type/uuid-type.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; -import Type from 'gdbots/pbj/type/type'; - -export default class UuidType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('UuidIdentifier') && 'UuidIdentifier' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "UuidIdentifier" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('UuidIdentifier') || 'UuidIdentifier' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - /** @var UuidIdentifier instance */ - let instance = field.getInstance() || UuidIdentifier; - if ('object' === typeof value - && ( - value.hasTrait(instance.name) - || instance.name === SystemUtils.getClass(value) - ) - ) { - return value; - } - - return instance.fromString(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return UuidIdentifier.generate(); - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/types/AbstractBinaryType.js b/src/types/AbstractBinaryType.js new file mode 100644 index 0000000..f42b8e6 --- /dev/null +++ b/src/types/AbstractBinaryType.js @@ -0,0 +1,111 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import base64 from 'base-64'; +import clamp from 'lodash/clamp'; +import isString from 'lodash/isString'; +import trim from 'lodash/trim'; +import utf8 from 'utf8'; +import Type from './Type'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +let useDecodeFromBase64 = true; +let useEncodeToBase64 = true; + +export default class AbstractBinaryType extends Type { + /** + * @param {boolean} [useBase64] + */ + decodeFromBase64(useBase64 = true) { + useDecodeFromBase64 = useBase64; + } + + /** + * @param {boolean} [useBase64] + */ + encodeToBase64(useBase64 = true) { + useEncodeToBase64 = useBase64; + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isString(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a string.`); + } + + // fixme: deal with browsers not having "Buffer" available + // we must get BYTES, not characters ಠ_ಠ + const strLength = Buffer.from(useEncodeToBase64 ? this.encode(value, field) : value).byteLength; + const minLength = field.getMinLength(); + const maxLength = clamp(field.getMaxLength(), minLength, this.getMaxBytes()); + + if (strLength >= minLength && strLength <= maxLength) { + return; + } + + throw new AssertionFailed( + `Field [${field.getName()}] :: Must be between [${minLength}] and [${maxLength}] bytes, [${strLength}] bytes given.`, + ); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + const trimmed = trim(value); + if (trimmed === '') { + return null; + } + + return useEncodeToBase64 ? base64.encode(utf8.encode(trimmed)) : trimmed; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + * + * @throws {DecodeValueFailed} + */ + decode(value, field, codec = null) { + const trimmed = trim(value); + if (trimmed === '') { + return null; + } + + if (!useDecodeFromBase64) { + return trimmed; + } + + try { + return utf8.decode(base64.decode(trimmed)); + } catch (e) { + throw new DecodeValueFailed(value, field, `${e.name}::${e.message}`); + } + } + + /** + * @returns {boolean} + */ + isBinary() { + return true; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/AbstractIntType.js b/src/types/AbstractIntType.js new file mode 100644 index 0000000..31821a4 --- /dev/null +++ b/src/types/AbstractIntType.js @@ -0,0 +1,64 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import clamp from 'lodash/clamp'; +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import Type from './Type'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class AbstractIntType extends Type { + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isSafeInteger(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not an integer.`); + } + + const min = clamp(field.getMin(), this.getMin(), this.getMax()); + const max = clamp(field.getMax(), this.getMin(), this.getMax()); + + if (value < min || value > max) { + throw new AssertionFailed(`Field [${field.getName()}] :: Number "${value}" was expected to be at least "${min}" and at most "${max}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @returns {number} + */ + getDefault() { + return 0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/AbstractStringType.js b/src/types/AbstractStringType.js new file mode 100644 index 0000000..336d08e --- /dev/null +++ b/src/types/AbstractStringType.js @@ -0,0 +1,66 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import clamp from 'lodash/clamp'; +import isString from 'lodash/isString'; +import trim from 'lodash/trim'; +import Type from './Type'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class AbstractStringType extends Type { + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isString(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a string.`); + } + + // fixme: deal with browsers not having "Buffer" available + // we must get BYTES, not characters ಠ_ಠ + const strLength = Buffer.from(value).byteLength; + const minLength = field.getMinLength(); + const maxLength = clamp(field.getMaxLength(), minLength, this.getMaxBytes()); + + if (strLength >= minLength && strLength <= maxLength) { + return; + } + + throw new AssertionFailed( + `Field [${field.getName()}] :: Must be between [${minLength}] and [${maxLength}] bytes, [${strLength}] bytes given.`, + ); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + const trimmed = trim(value); + return trimmed === '' ? null : trimmed; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + decode(value, field, codec = null) { + const trimmed = trim(value); + return trimmed === '' ? null : trimmed; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/BigIntType.js b/src/types/BigIntType.js new file mode 100644 index 0000000..8a3d07d --- /dev/null +++ b/src/types/BigIntType.js @@ -0,0 +1,83 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import BigNumber from '../well-known/BigNumber'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class BigIntType extends Type { + constructor() { + super(TypeName.BIG_INT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof BigNumber)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a BigNumber.`); + } + + if (value.isNegative()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be negative.`); + } + + if (!value.lessThanOrEqualTo('18446744073709551615')) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be greater than [18446744073709551615].`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {string} + */ + encode(value, field, codec = null) { + if (value instanceof BigNumber) { + return `${value.toFixed(0)}`; + } + + return '0'; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?BigNumber} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof BigNumber) { + return value; + } + + return new BigNumber(value); + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {BigNumber} + */ + getDefault() { + return new BigNumber(0); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/BinaryType.js b/src/types/BinaryType.js new file mode 100644 index 0000000..1a1fb04 --- /dev/null +++ b/src/types/BinaryType.js @@ -0,0 +1,17 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractBinaryType from './AbstractBinaryType'; +import TypeName from '../enums/TypeName'; + +export default class BinaryType extends AbstractBinaryType { + constructor() { + super(TypeName.BINARY); + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 255; + } +} diff --git a/src/types/BlobType.js b/src/types/BlobType.js new file mode 100644 index 0000000..0a3c371 --- /dev/null +++ b/src/types/BlobType.js @@ -0,0 +1,17 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractBinaryType from './AbstractBinaryType'; +import TypeName from '../enums/TypeName'; + +export default class BlobType extends AbstractBinaryType { + constructor() { + super(TypeName.BLOB); + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/BooleanType.js b/src/types/BooleanType.js new file mode 100644 index 0000000..eef4ccd --- /dev/null +++ b/src/types/BooleanType.js @@ -0,0 +1,75 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isBoolean from 'lodash/isBoolean'; +import toLower from 'lodash/toLower'; +import trim from 'lodash/trim'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class BooleanType extends Type { + constructor() { + super(TypeName.BOOLEAN); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (isBoolean(value)) { + return; + } + + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a boolean.`); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {boolean} + */ + encode(value, field, codec = null) { + return !!value; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {boolean} + */ + decode(value, field, codec = null) { + if (isBoolean(value)) { + return !!value; + } + + return ['1', 'true', 'yes', 'on', '+'].indexOf(trim(toLower(value))) !== -1; + } + + /** + * @returns {boolean} + */ + getDefault() { + return false; + } + + /** + * @returns {boolean} + */ + isBoolean() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/DateTimeType.js b/src/types/DateTimeType.js new file mode 100644 index 0000000..a35021a --- /dev/null +++ b/src/types/DateTimeType.js @@ -0,0 +1,88 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import moment from 'moment'; +import isValidISO8601Date from '@gdbots/common/isValidISO8601Date'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class DateTimeType extends Type { + constructor() { + super(TypeName.DATE_TIME); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Date)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Date.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Date) { + return value.toISOString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Date} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + if (!(value instanceof Date) && !isValidISO8601Date(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid ISO8601 date/time. E.g. "2017-05-25T02:54:18Z".`, + ); + } + + const m = moment(value); + if (m.isValid()) { + return m.utc().toDate(); + } + + throw new DecodeValueFailed(value, field, `Field [${field.getName()}] :: Value "${value}" is not a valid IS8601 date.`); + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/DateType.js b/src/types/DateType.js new file mode 100644 index 0000000..31dd6eb --- /dev/null +++ b/src/types/DateType.js @@ -0,0 +1,92 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class DateType extends Type { + constructor() { + super(TypeName.DATE); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Date)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Date.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Date) { + return value.toISOString().substr(0, 10); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Date} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + let date; + if (value instanceof Date) { + // ensures no time component and utc + date = value.toISOString().substr(0, 10); + } else { + date = value.substr(0, 10); + } + + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + throw new DecodeValueFailed(value, field, `Field [${field.getName()}] :: Value "${value}" is not a valid date with format "YYYY-MM-DD".`); + } + + try { + const [year, month, day] = date.split('-').map(Number); + return new Date(Date.UTC(year, month - 1, day)); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/DecimalType.js b/src/types/DecimalType.js new file mode 100644 index 0000000..497a973 --- /dev/null +++ b/src/types/DecimalType.js @@ -0,0 +1,76 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isFinite from 'lodash/isFinite'; +import toFinite from 'lodash/toFinite'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +// fixme: handle precision +export default class DecimalType extends Type { + constructor() { + super(TypeName.DECIMAL); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isFinite(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a decimal.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {string} + */ + encode(value, field, codec = null) { + return toFinite(value).toFixed(field.getScale()); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toFinite(toFinite(value).toFixed(field.getScale())); + } + + /** + * @returns {number} + */ + getDefault() { + return 0.0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return Number.MIN_VALUE; + } + + /** + * @returns {number} + */ + getMax() { + return Number.MAX_VALUE; + } +} diff --git a/src/types/DynamicFieldType.js b/src/types/DynamicFieldType.js new file mode 100644 index 0000000..a71c789 --- /dev/null +++ b/src/types/DynamicFieldType.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import DynamicField from '../well-known/DynamicField'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class DynamicFieldType extends Type { + constructor() { + super(TypeName.DYNAMIC_FIELD); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof DynamicField)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a DynamicField.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof DynamicField) { + return codec.encodeDynamicField(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?DynamicField} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof DynamicField) { + return value; + } + + try { + return codec.decodeDynamicField(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/FloatType.js b/src/types/FloatType.js new file mode 100644 index 0000000..0693de3 --- /dev/null +++ b/src/types/FloatType.js @@ -0,0 +1,76 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isFinite from 'lodash/isFinite'; +import toFinite from 'lodash/toFinite'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +// fixme: handle precision and scale +export default class FloatType extends Type { + constructor() { + super(TypeName.FLOAT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isFinite(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a float.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toFinite(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toFinite(value); + } + + /** + * @returns {number} + */ + getDefault() { + return 0.0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return Number.MIN_VALUE; + } + + /** + * @returns {number} + */ + getMax() { + return Number.MAX_VALUE; + } +} diff --git a/src/types/GeoPointType.js b/src/types/GeoPointType.js new file mode 100644 index 0000000..e90e1b7 --- /dev/null +++ b/src/types/GeoPointType.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import GeoPoint from '../well-known/GeoPoint'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class GeoPointType extends Type { + constructor() { + super(TypeName.GEO_POINT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof GeoPoint)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a GeoPoint.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof GeoPoint) { + return codec.encodeGeoPoint(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?GeoPoint} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof GeoPoint) { + return value; + } + + try { + return codec.decodeGeoPoint(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/IdentifierType.js b/src/types/IdentifierType.js new file mode 100644 index 0000000..8d29142 --- /dev/null +++ b/src/types/IdentifierType.js @@ -0,0 +1,90 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import Identifier from '../well-known/Identifier'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class IdentifierType extends Type { + constructor() { + super(TypeName.IDENTIFIER); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Identifier)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be an Identifier.`); + } + + if (!(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" was expected to be a "${field.getClassProto().name}".`); + } + + const str = value.toString(); + if (str.length < 1 || str.length > this.getMaxBytes()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Must be between [1] and [${this.getMaxBytes()}] bytes, [${str.length}] bytes given.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Identifier) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Identifier} + */ + decode(value, field, codec = null) { + const expectedProto = field.getClassProto(); + if (value === null || value instanceof expectedProto) { + return value; + } + + try { + return expectedProto.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 100; + } +} diff --git a/src/types/IntEnumType.js b/src/types/IntEnumType.js new file mode 100644 index 0000000..9fd9371 --- /dev/null +++ b/src/types/IntEnumType.js @@ -0,0 +1,104 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Enum from '@gdbots/common/Enum'; +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class IntEnumType extends Type { + constructor() { + super(TypeName.INT_ENUM); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Enum)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be an Enum.`); + } + + if (!(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value.getEnumId()}" was expected to be "${field.getClassProto().getEnumId()}".`); + } + + const enumValue = value.getValue(); + if (!isSafeInteger(enumValue)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Enum's value "${value}" is not an integer.`); + } + + if (enumValue < this.getMin() || enumValue > this.getMax()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Enum's value "${enumValue}" was expected to be at least "${this.getMin()}" and at most "${this.getMax()}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + if (value instanceof Enum) { + return toSafeInteger(value.getValue()); + } + + return 0; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Enum} + * + * @throws {DecodeValueFailed} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + try { + return field.getClassProto().create(value); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 65535; + } +} diff --git a/src/types/IntType.js b/src/types/IntType.js new file mode 100644 index 0000000..750d701 --- /dev/null +++ b/src/types/IntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class IntType extends AbstractIntType { + constructor() { + super(TypeName.INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 4294967295; + } +} diff --git a/src/types/MediumBlobType.js b/src/types/MediumBlobType.js new file mode 100644 index 0000000..486d12e --- /dev/null +++ b/src/types/MediumBlobType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractBinaryType from './AbstractBinaryType'; +import TypeName from '../enums/TypeName'; + +export default class MediumBlobType extends AbstractBinaryType { + constructor() { + super(TypeName.MEDIUM_BLOB); + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 16777215; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/MediumIntType.js b/src/types/MediumIntType.js new file mode 100644 index 0000000..9815621 --- /dev/null +++ b/src/types/MediumIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class MediumIntType extends AbstractIntType { + constructor() { + super(TypeName.MEDIUM_INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 16777215; + } +} diff --git a/src/types/MediumTextType.js b/src/types/MediumTextType.js new file mode 100644 index 0000000..87e5a3a --- /dev/null +++ b/src/types/MediumTextType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractStringType from './AbstractStringType'; +import TypeName from '../enums/TypeName'; + +export default class MediumTextType extends AbstractStringType { + constructor() { + super(TypeName.MEDIUM_TEXT); + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 16777215; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/MessageRefType.js b/src/types/MessageRefType.js new file mode 100644 index 0000000..eb4a9cf --- /dev/null +++ b/src/types/MessageRefType.js @@ -0,0 +1,73 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import MessageRef from '../MessageRef'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class MessageRefType extends Type { + constructor() { + super(TypeName.MESSAGE_REF); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof MessageRef)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a MessageRef.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof MessageRef) { + return codec.encodeMessageRef(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?MessageRef} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof MessageRef) { + return value; + } + + try { + return codec.decodeMessageRef(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } +} diff --git a/src/types/MessageType.js b/src/types/MessageType.js new file mode 100644 index 0000000..0688eda --- /dev/null +++ b/src/types/MessageType.js @@ -0,0 +1,99 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import Message from '../Message'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class MessageType extends Type { + constructor() { + super(TypeName.MESSAGE); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Message)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Message.`); + } + + const anyOfCuries = field.getAnyOfCuries(); + if (!anyOfCuries.length) { + return; + } + + const schema = value.schema(); + if (anyOfCuries.includes(schema.getCurie().toString())) { + return; + } + + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${schema.getCurie()}" must be one of: ${anyOfCuries.join(',')}.`); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof Message) { + return codec.encodeMessage(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Message} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof Message) { + return value; + } + + try { + return codec.decodeMessage(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isMessage() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/MicrotimeType.js b/src/types/MicrotimeType.js new file mode 100644 index 0000000..1570d09 --- /dev/null +++ b/src/types/MicrotimeType.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import Microtime from '../well-known/Microtime'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class MicrotimeType extends Type { + constructor() { + super(TypeName.MICROTIME); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Microtime)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Microtime.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Microtime) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Microtime} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof Microtime) { + return value; + } + + try { + return Microtime.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {Microtime} + */ + getDefault() { + return Microtime.create(); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/SignedBigIntType.js b/src/types/SignedBigIntType.js new file mode 100644 index 0000000..c2534e2 --- /dev/null +++ b/src/types/SignedBigIntType.js @@ -0,0 +1,83 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import BigNumber from '../well-known/BigNumber'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class SignedBigIntType extends Type { + constructor() { + super(TypeName.SIGNED_BIG_INT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof BigNumber)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a BigNumber.`); + } + + if (!value.greaterThanOrEqualTo('-9223372036854775808')) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be less than [-9223372036854775808].`); + } + + if (!value.lessThanOrEqualTo('9223372036854775807')) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be greater than [9223372036854775807].`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {string} + */ + encode(value, field, codec = null) { + if (value instanceof BigNumber) { + return `${value.toFixed(0)}`; + } + + return '0'; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?BigNumber} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof BigNumber) { + return value; + } + + return new BigNumber(value); + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {BigNumber} + */ + getDefault() { + return new BigNumber(0); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/SignedIntType.js b/src/types/SignedIntType.js new file mode 100644 index 0000000..df47aaa --- /dev/null +++ b/src/types/SignedIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -2147483648; + } + + /** + * @returns {number} + */ + getMax() { + return 2147483647; + } +} diff --git a/src/types/SignedMediumIntType.js b/src/types/SignedMediumIntType.js new file mode 100644 index 0000000..0e56005 --- /dev/null +++ b/src/types/SignedMediumIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedMediumIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_MEDIUM_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -8388608; + } + + /** + * @returns {number} + */ + getMax() { + return 8388607; + } +} diff --git a/src/types/SignedSmallIntType.js b/src/types/SignedSmallIntType.js new file mode 100644 index 0000000..7bc598f --- /dev/null +++ b/src/types/SignedSmallIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedSmallIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_SMALL_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -32768; + } + + /** + * @returns {number} + */ + getMax() { + return 32767; + } +} diff --git a/src/types/SignedTinyIntType.js b/src/types/SignedTinyIntType.js new file mode 100644 index 0000000..85dd965 --- /dev/null +++ b/src/types/SignedTinyIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedTinyIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_TINY_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -128; + } + + /** + * @returns {number} + */ + getMax() { + return 127; + } +} diff --git a/src/types/SmallIntType.js b/src/types/SmallIntType.js new file mode 100644 index 0000000..fa1ef1f --- /dev/null +++ b/src/types/SmallIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SmallIntType extends AbstractIntType { + constructor() { + super(TypeName.SMALL_INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 65535; + } +} diff --git a/src/types/StringEnumType.js b/src/types/StringEnumType.js new file mode 100644 index 0000000..a7623c9 --- /dev/null +++ b/src/types/StringEnumType.js @@ -0,0 +1,96 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Enum from '@gdbots/common/Enum'; +import isString from 'lodash/isString'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class StringEnumType extends Type { + constructor() { + super(TypeName.STRING_ENUM); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Enum)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be an Enum.`); + } + + if (!(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value.getEnumId()}" was expected to be "${field.getClassProto().getEnumId()}".`); + } + + const enumValue = value.getValue(); + if (!isString(enumValue)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Enum's value "${value}" is not a string.`); + } + + if (enumValue.length < 1 || enumValue.length > this.getMaxBytes()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Must be between [1] and [${this.getMaxBytes()}] bytes, [${enumValue.length}] bytes given.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Enum) { + return `${value.getValue()}`; + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Enum} + * + * @throws {DecodeValueFailed} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + try { + return field.getClassProto().create(value); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 100; + } +} diff --git a/src/types/StringType.js b/src/types/StringType.js new file mode 100644 index 0000000..a6d38de --- /dev/null +++ b/src/types/StringType.js @@ -0,0 +1,153 @@ +/* eslint-disable class-methods-use-this, no-unused-vars, max-len, no-useless-escape */ + +import isValidEmail from '@gdbots/common/isValidEmail'; +import isValidHashtag from '@gdbots/common/isValidHashtag'; +import isValidISO8601Date from '@gdbots/common/isValidISO8601Date'; +import isValidHostname from '@gdbots/common/isValidHostname'; +import isValidIpv4 from '@gdbots/common/isValidIpv4'; +import isValidIpv6 from '@gdbots/common/isValidIpv6'; +import isValidUri from '@gdbots/common/isValidUri'; +import isValidUrl from '@gdbots/common/isValidUrl'; +import AbstractStringType from './AbstractStringType'; +import Format from '../enums/Format'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class StringType extends AbstractStringType { + constructor() { + super(TypeName.STRING); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + super.guard(value, field); + + if (field.getPattern() && !field.getPattern().test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" does not match expression "${field.getPattern()}".`, + ); + } + + switch (field.getFormat()) { + case Format.UNKNOWN: + break; + + case Format.DATE: + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid date with format "YYYY-MM-DD".`, + ); + } + + break; + + case Format.DATE_TIME: + if (!isValidISO8601Date(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid ISO8601 date/time. E.g. "2017-05-25T02:54:18Z".`, + ); + } + + break; + + case Format.SLUG: + // note that this format is less restrictive than "isValidSlug" function from @gdbots/common. + // This is intentional as not everyone is as strict with slug formats. for example, youtube + // "slugs" contain both upper and lower case characters and underscores and hyphens. + if (!/^([\w\/-]|[\w-][\w\/-]*[\w-])$/.test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid slug.`, + ); + } + + break; + + case Format.EMAIL: + if (!isValidEmail(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid email address.`, + ); + } + + break; + + case Format.HASHTAG: + if (!isValidHashtag(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid hashtag.`, + ); + } + + break; + + case Format.IPV4: + if (!isValidIpv4(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid IPv4 address.`, + ); + } + + break; + + case Format.IPV6: + if (!isValidIpv6(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid IPv6 address.`, + ); + } + + break; + + case Format.HOSTNAME: + if (!isValidHostname(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid HOSTNAME.`, + ); + } + + break; + + case Format.URI: + if (!isValidUri(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid URI.`, + ); + } + + break; + + case Format.URL: + if (!isValidUrl(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid URL.`, + ); + } + + break; + + case Format.UUID: + if (!/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid UUID.`, + ); + } + + break; + + default: + break; + } + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 255; + } +} diff --git a/src/types/TextType.js b/src/types/TextType.js new file mode 100644 index 0000000..e37ccbf --- /dev/null +++ b/src/types/TextType.js @@ -0,0 +1,17 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractStringType from './AbstractStringType'; +import TypeName from '../enums/TypeName'; + +export default class TextType extends AbstractStringType { + constructor() { + super(TypeName.TEXT); + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/TimeUuidType.js b/src/types/TimeUuidType.js new file mode 100644 index 0000000..272a5c7 --- /dev/null +++ b/src/types/TimeUuidType.js @@ -0,0 +1,85 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import TimeUuidIdentifier from '../well-known/TimeUuidIdentifier'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class TimeUuidType extends Type { + constructor() { + super(TypeName.TIME_UUID); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof TimeUuidIdentifier)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a TimeUuidIdentifier.`); + } + + if (field.hasClassProto() && !(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" was expected to be a "${field.getClassProto().name}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof TimeUuidIdentifier) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?TimeUuidIdentifier} + */ + decode(value, field, codec = null) { + const expectedProto = field.hasClassProto() ? field.getClassProto() : TimeUuidIdentifier; + if (value === null || value instanceof expectedProto) { + return value; + } + + try { + return expectedProto.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {TimeUuidIdentifier} + */ + getDefault() { + return TimeUuidIdentifier.generate(); + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/TimestampType.js b/src/types/TimestampType.js new file mode 100644 index 0000000..d766881 --- /dev/null +++ b/src/types/TimestampType.js @@ -0,0 +1,62 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import isValidTimestamp from '@gdbots/common/isValidTimestamp'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class TimestampType extends Type { + constructor() { + super(TypeName.TIMESTAMP); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isSafeInteger(value) || !isValidTimestamp(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a valid unix timestamp.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @returns {number} + */ + getDefault() { + return Math.floor(Date.now() / 1000); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/TinyIntType.js b/src/types/TinyIntType.js new file mode 100644 index 0000000..d9bdb08 --- /dev/null +++ b/src/types/TinyIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class TinyIntType extends AbstractIntType { + constructor() { + super(TypeName.TINY_INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 255; + } +} diff --git a/src/types/TrinaryType.js b/src/types/TrinaryType.js new file mode 100644 index 0000000..58f7b39 --- /dev/null +++ b/src/types/TrinaryType.js @@ -0,0 +1,92 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +/** + * @link https://en.wikipedia.org/wiki/Three-valued_logic + * 0 = unknown + * 1 = true + * 2 = false + */ +export default class TrinaryType extends Type { + constructor() { + super(TypeName.TRINARY); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isSafeInteger(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not an integer.`); + } + + if ([0, 1, 2].indexOf(value) === -1) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" is not an element of the valid values: [0, 1, 2]`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @returns {number} + */ + getDefault() { + return 0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 2; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/Type.js b/src/types/Type.js new file mode 100644 index 0000000..a86d2d6 --- /dev/null +++ b/src/types/Type.js @@ -0,0 +1,129 @@ +/* eslint-disable class-methods-use-this */ + +/** + * We store all Type instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory create method to create types. + * + * @type {Map} + */ +const instances = new Map(); + +export default class Type { + /** + * @param {TypeName} typeName + */ + constructor(typeName) { + this.typeName = typeName; + Object.freeze(this); + } + + /** + * @returns {Type} + */ + static create() { + if (!instances.has(this)) { + instances.set(this, new this()); + } + + return instances.get(this); + } + + /** + * @returns {TypeName} + */ + getTypeName() { + return this.typeName; + } + + /** + * @returns {string} + */ + getTypeValue() { + return this.typeName.getValue(); + } + + /** + * @returns {boolean} + */ + isScalar() { + return true; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return true; + } + + /** + * @returns {*} + */ + getDefault() { + return null; + } + + /** + * @returns {boolean} + */ + isBoolean() { + return false; + } + + /** + * @returns {boolean} + */ + isBinary() { + return false; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return false; + } + + /** + * @returns {boolean} + */ + isMessage() { + return false; + } + + /** + * @returns {number} + */ + getMin() { + return -2147483648; + } + + /** + * @returns {number} + */ + getMax() { + return 2147483647; + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 65535; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return true; + } +} diff --git a/src/types/UuidType.js b/src/types/UuidType.js new file mode 100644 index 0000000..20393a4 --- /dev/null +++ b/src/types/UuidType.js @@ -0,0 +1,85 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import UuidIdentifier from '../well-known/UuidIdentifier'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class UuidType extends Type { + constructor() { + super(TypeName.UUID); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof UuidIdentifier)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a UuidIdentifier.`); + } + + if (field.hasClassProto() && !(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" was expected to be a "${field.getClassProto().name}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof UuidIdentifier) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?UuidIdentifier} + */ + decode(value, field, codec = null) { + const expectedProto = field.hasClassProto() ? field.getClassProto() : UuidIdentifier; + if (value === null || value instanceof expectedProto) { + return value; + } + + try { + return expectedProto.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {UuidIdentifier} + */ + getDefault() { + return UuidIdentifier.generate(); + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 0000000..378a775 --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,71 @@ +import BigIntType from './BigIntType'; +import BinaryType from './BinaryType'; +import BlobType from './BlobType'; +import BooleanType from './BooleanType'; +import DateType from './DateType'; +import DateTimeType from './DateTimeType'; +import DecimalType from './DecimalType'; +import DynamicFieldType from './DynamicFieldType'; +import FloatType from './FloatType'; +import GeoPointType from './GeoPointType'; +import IdentifierType from './IdentifierType'; +import IntEnumType from './IntEnumType'; +import IntType from './IntType'; +import MediumBlobType from './MediumBlobType'; +import MediumIntType from './MediumIntType'; +import MediumTextType from './MediumTextType'; +import MessageRefType from './MessageRefType'; +import MessageType from './MessageType'; +import MicrotimeType from './MicrotimeType'; +import SignedBigIntType from './SignedBigIntType'; +import SignedIntType from './SignedIntType'; +import SignedMediumIntType from './SignedMediumIntType'; +import SignedSmallIntType from './SignedSmallIntType'; +import SignedTinyIntType from './SignedTinyIntType'; +import SmallIntType from './SmallIntType'; +import StringEnumType from './StringEnumType'; +import StringType from './StringType'; +import TextType from './TextType'; +import TimestampType from './TimestampType'; +import TimeUuidType from './TimeUuidType'; +import TinyIntType from './TinyIntType'; +import TrinaryType from './TrinaryType'; +import Type from './Type'; +import UuidType from './UuidType'; + +export default { + BigIntType, + BinaryType, + BlobType, + BooleanType, + DateTimeType, + DateType, + DecimalType, + DynamicFieldType, + FloatType, + GeoPointType, + IdentifierType, + IntEnumType, + IntType, + MediumBlobType, + MediumIntType, + MediumTextType, + MessageRefType, + MessageType, + MicrotimeType, + SignedBigIntType, + SignedIntType, + SignedMediumIntType, + SignedSmallIntType, + SignedTinyIntType, + SmallIntType, + StringEnumType, + StringType, + TextType, + TimestampType, + TimeUuidType, + TinyIntType, + TrinaryType, + Type, + UuidType, +}; diff --git a/src/well-known/BigNumber.js b/src/well-known/BigNumber.js new file mode 100644 index 0000000..af5dced --- /dev/null +++ b/src/well-known/BigNumber.js @@ -0,0 +1,3 @@ +import BigNumber from 'bignumber.js'; + +export default BigNumber; diff --git a/src/well-known/DatedSlugIdentifier.js b/src/well-known/DatedSlugIdentifier.js new file mode 100644 index 0000000..8ec271d --- /dev/null +++ b/src/well-known/DatedSlugIdentifier.js @@ -0,0 +1,37 @@ +import addDateToSlug from '@gdbots/common/addDateToSlug'; +import createSlug from '@gdbots/common/createSlug'; +import isValidSlug from '@gdbots/common/isValidSlug'; +import slugContainsDate from '@gdbots/common/slugContainsDate'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Identifier from './Identifier'; + +export default class DatedSlugIdentifier extends Identifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!isValidSlug(this.value, true) || !slugContainsDate(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid dated slug.`); + } + + Object.freeze(this); + } + + /** + * @param {string} str + * @param {?Date} [date] + * + * @returns {DatedSlugIdentifier} + */ + static create(str, date = null) { + const slug = createSlug(str, true); + + if (slugContainsDate(slug)) { + return new this(slug); + } + + return new this(addDateToSlug(slug, date)); + } +} diff --git a/src/well-known/DynamicField.js b/src/well-known/DynamicField.js new file mode 100644 index 0000000..0d83ca3 --- /dev/null +++ b/src/well-known/DynamicField.js @@ -0,0 +1,296 @@ +import isString from 'lodash/isString'; +import DynamicFieldKind from '../enums/DynamicFieldKind'; +import FieldRule from '../enums/FieldRule'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Field from '../Field'; +import BooleanType from '../types/BooleanType'; +import DateType from '../types/DateType'; +import FloatType from '../types/FloatType'; +import IntType from '../types/IntType'; +import StringType from '../types/StringType'; +import TextType from '../types/TextType'; + +/** + * Dynamic fields need one field object per "kind". + * Map provides the storage for the flyweight strategy. + * + * @type {Map} + */ +const fields = new Map(); + +/** + * @param {string} kind + * + * @returns {Field} + */ +function createField(kind) { + if (!fields.has(kind)) { + let type; + switch (kind) { + case DynamicFieldKind.STRING_VAL.toString(): + type = StringType.create(); + break; + + case DynamicFieldKind.TEXT_VAL.toString(): + type = TextType.create(); + break; + + case DynamicFieldKind.INT_VAL.toString(): + type = IntType.create(); + break; + + case DynamicFieldKind.BOOL_VAL.toString(): + type = BooleanType.create(); + break; + + case DynamicFieldKind.FLOAT_VAL.toString(): + type = FloatType.create(); + break; + + case DynamicFieldKind.DATE_VAL.toString(): + type = DateType.create(); + break; + + default: + throw new AssertionFailed(`DynamicField "${kind}" is not a valid type.`); + } + + fields.set( + kind, new Field({ name: kind, type, rule: FieldRule.A_SINGLE_VALUE, required: true }), + ); + } + + return fields.get(kind); +} + +/** + * Regular expression pattern for matching a valid dynamic field name. + * @type {RegExp} + */ +export const VALID_NAME_PATTERN = /^[a-zA-Z_]{1}[a-zA-Z0-9_-]{0,126}$/; + +/** + * DynamicField is a wrapper for fields which would not be ideal as a map because + * you don't know what the field name is going to be until runtime or the number + * of fields you'll end up having will be too large. + * + * A common use case is a polling or custom form service. Eventually the number of + * fields you have is in the thousands and systems like SQL, ElasticSearch will not + * do well with that many fields. DynamicField is designed to be a "named union". + * + * For example: + * [ + * // the name of the field + * 'name' => 'your-field-name', + * // only one of the following values can be populated. + * 'bool_val' => true, + * 'date_val' => '2015-12-25', + * 'float_val' => 1.0, + * 'int_val' => 1, + * 'string_val' => 'string', + * 'text_val' => 'some text', + * ] + */ +export default class DynamicField { + /** + * @param {string} name + * @param {*} kind + * @param {*} value + * + * @throws {AssertionFailed} + */ + constructor(name, kind, value) { + if (!isString(name)) { + throw new AssertionFailed('DynamicField name must be a string.'); + } + + if (!(kind instanceof DynamicFieldKind)) { + throw new AssertionFailed('DynamicField kind was expected to be an instance of DynamicFieldKind.'); + } + + if (!VALID_NAME_PATTERN.test(name)) { + throw new AssertionFailed(`DynamicField [${name}] is invalid. It must match the pattern [${VALID_NAME_PATTERN}].`); + } + + Object.defineProperty(this, 'name', { value: name }); + Object.defineProperty(this, 'kind', { value: `${kind}` }); + + const field = createField(this.kind); + const decodedValue = field.getType().decode(value, field); + field.guardValue(decodedValue); + Object.defineProperty(this, 'value', { value: decodedValue }); + + Object.freeze(this); + } + + /** + * @param {string} json + * + * @returns {DynamicField} + * + * @throws {AssertionFailed} + */ + static fromJSON(json) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return DynamicField.fromObject(obj); + } + + /** + * @param {Object} obj + * + * @returns {DynamicField} + * + * @throws {AssertionFailed} + */ + static fromObject(obj = {}) { + if (!obj.name) { + throw new AssertionFailed('DynamicField "name" property must be set.'); + } + + const kind = Object.keys(obj).filter(key => key !== 'name').pop(); + + try { + return new DynamicField(obj.name, DynamicFieldKind.create(kind), obj[kind]); + } catch (e) { + throw new AssertionFailed(`DynamicField "${kind}" is invalid.`); + } + } + + /** + * @param {string} name + * @param {boolean} value + * + * @return {DynamicField} + */ + static createBoolVal(name, value = false) { + return new DynamicField(name, DynamicFieldKind.BOOL_VAL, value); + } + + /** + * @param {string} name + * @param {Date} value + * + * @return {DynamicField} + */ + static createDateVal(name, value) { + return new DynamicField(name, DynamicFieldKind.DATE_VAL, value); + } + + /** + * @param {string} name + * @param {number} value + * + * @return {DynamicField} + */ + static createFloatVal(name, value = 0.0) { + return new DynamicField(name, DynamicFieldKind.FLOAT_VAL, value); + } + + /** + * @param {string} name + * @param {number} value + * + * @return {DynamicField} + */ + static createIntVal(name, value = 0) { + return new DynamicField(name, DynamicFieldKind.INT_VAL, value); + } + + /** + * @param {string} name + * @param {string} value + * + * @return {DynamicField} + */ + static createStringVal(name, value) { + return new DynamicField(name, DynamicFieldKind.STRING_VAL, value); + } + + /** + * @param {string} name + * @param {string} value + * + * @return {DynamicField} + */ + static createTextVal(name, value) { + return new DynamicField(name, DynamicFieldKind.TEXT_VAL, value); + } + + /** + * @returns {string} + */ + getName() { + return this.name; + } + + /** + * @returns {string} + */ + getKind() { + return this.kind; + } + + /** + * @returns {Field} + */ + getField() { + return createField(this.kind); + } + + /** + * @returns {*} + */ + getValue() { + return this.value; + } + + /** + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * @returns {Object} + */ + toObject() { + const field = createField(this.kind); + return { + name: this.name, + [this.kind]: field.getType().encode(this.value, field), + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {DynamicField} other + * + * @returns {boolean} + */ + equals(other) { + return this.name === other.name + && this.kind === other.kind + && this.value === other.value; + } +} diff --git a/src/well-known/GeoPoint.js b/src/well-known/GeoPoint.js new file mode 100644 index 0000000..a3ba8dd --- /dev/null +++ b/src/well-known/GeoPoint.js @@ -0,0 +1,123 @@ +import isArray from 'lodash/isArray'; +import toFinite from 'lodash/toFinite'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +/** + * Represents a GeoJson Point value. + * @link http://geojson.org/geojson-spec.html#point + */ +export default class GeoPoint { + /** + * @param {number} lat + * @param {number} lon + * + * @throws {AssertionFailed} + */ + constructor(lat, lon) { + const flat = toFinite(toFinite(lat).toFixed(8)); + const flon = toFinite(toFinite(lon).toFixed(8)); + + if (flat > 90.0 || flat < -90.0) { + throw new AssertionFailed(`Latitude "${flat}" must be within range [-90.0, 90.0].`); + } + + if (flon > 180.0 || flon < -180.0) { + throw new AssertionFailed(`Longitude "${flon}" must be within range [-180.0, 180.0].`); + } + + Object.defineProperty(this, 'lat', { value: flat }); + Object.defineProperty(this, 'lon', { value: flon }); + Object.freeze(this); + } + + /** + * @param {string} value + * + * @returns {GeoPoint} + */ + static fromString(value) { + const p = value.split(','); + return new GeoPoint(p[0], p[1]); + } + + /** + * @param {string} json + * + * @returns {GeoPoint} + */ + static fromJSON(json) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return GeoPoint.fromObject(obj); + } + + /** + * @param {Object} obj + * + * @returns {GeoPoint} + */ + static fromObject(obj = {}) { + if (obj.coordinates && isArray(obj.coordinates) && obj.coordinates.length === 2) { + return new GeoPoint(obj.coordinates[1], obj.coordinates[0]); + } + + throw new AssertionFailed('Invalid GeoJson "Point" type.'); + } + + /** + * @returns {number} + */ + getLatitude() { + return this.lat; + } + + /** + * @returns {number} + */ + getLongitude() { + return this.lon; + } + + /** + * @returns {string} + */ + toString() { + return `${this.lat},${this.lon}`; + } + + /** + * @returns {Object} + */ + toObject() { + return { type: 'Point', coordinates: [this.lon, this.lat] }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {GeoPoint} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/well-known/Identifier.js b/src/well-known/Identifier.js new file mode 100644 index 0000000..e725cfa --- /dev/null +++ b/src/well-known/Identifier.js @@ -0,0 +1,62 @@ +import isString from 'lodash/isString'; +import trim from 'lodash/trim'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class Identifier { + /** + * @param {string} value + * + * @throws {AssertionFailed} + */ + constructor(value) { + if (!isString(value)) { + throw new AssertionFailed(`${this.constructor.name}'s value must be a string.`); + } + + const trimmed = trim(value); + if (trimmed === '') { + throw new AssertionFailed(`${this.constructor.name}'s value cannot be empty.`); + } + + Object.defineProperty(this, 'value', { value: trimmed }); + } + + /** + * @param {string} value + * + * @returns {Identifier} + */ + static fromString(value) { + return new this(value); + } + + /** + * @returns {string} + */ + toString() { + return this.value; + } + + /** + * @returns {string} + */ + toJSON() { + return this.value; + } + + /** + * @returns {string} + */ + valueOf() { + return this.value; + } + + /** + * @param {Identifier} other + * + * @returns {boolean} + */ + equals(other) { + return this.value === other.value; + } +} diff --git a/src/well-known/Microtime.js b/src/well-known/Microtime.js new file mode 100644 index 0000000..81b930d --- /dev/null +++ b/src/well-known/Microtime.js @@ -0,0 +1,126 @@ +import isString from 'lodash/isString'; +import moment from 'moment'; +import padEnd from 'lodash/padEnd'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +/** + * Value object for microtime with methods to convert to and from integers. + * Note that this is a unix timestamp __WITH__ microseconds but stored + * as an integer NOT a float. + * + * In the PHP lib we have 10 digits (unix timestamp) concatenated with + * 6 microsecond digits (from php's microtime function we get that value). + * In JavaScript we don't get microsecond precision, we get milliseconds. + * fixme: generate microseconds precision to match php. + * + * @link http://php.net/manual/en/function.microtime.php + * @link https://github.com/gdbots/pbj-php/blob/master/src/well-known/Microtime.php + */ +export default class Microtime { + /** + * @param {string} value + * + * @throws {AssertionFailed} + */ + constructor(value) { + if (!isString(value)) { + throw new AssertionFailed('Microtime value must be a string.'); + } + + if (!/^[0-9]{16}$/.test(value)) { + throw new AssertionFailed('Microtime must be 16 digits.'); + } + + Object.defineProperty(this, 'value', { value }); + Object.freeze(this); + } + + /** + * @returns {Microtime} + */ + static create() { + return Microtime.fromMoment(moment()); + } + + /** + * @param {Date} date + * + * @returns {Microtime} + */ + static fromDate(date) { + return Microtime.fromMoment(moment(date)); + } + + /** + * @param {string} value + * + * @returns {Microtime} + */ + static fromString(value) { + return new Microtime(value); + } + + /** + * @private + * + * @param {moment.Moment} m + * + * @returns {Microtime} + */ + static fromMoment(m) { + return new Microtime(`${padEnd(m.valueOf(), 16, '0')}`); + } + + /** + * @returns {number} + */ + toNumber() { + return +`${this.value.substr(0, 10)}.${this.value.substr(10)}`; + } + + /** + * @private + * + * @returns {moment.Moment} + */ + toMoment() { + return moment.unix(this.toNumber()); + } + + /** + * @returns {Date} + */ + toDate() { + return this.toMoment().toDate(); + } + + /** + * @returns {string} + */ + toString() { + return this.value; + } + + /** + * @returns {string} + */ + toJSON() { + return this.value; + } + + /** + * @returns {string} + */ + valueOf() { + return this.value; + } + + /** + * @param {Microtime} other + * + * @returns {boolean} + */ + equals(other) { + return this.value === other.value; + } +} diff --git a/src/well-known/SlugIdentifier.js b/src/well-known/SlugIdentifier.js new file mode 100644 index 0000000..19e35d6 --- /dev/null +++ b/src/well-known/SlugIdentifier.js @@ -0,0 +1,28 @@ +import createSlug from '@gdbots/common/createSlug'; +import isValidSlug from '@gdbots/common/isValidSlug'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Identifier from './Identifier'; + +export default class SlugIdentifier extends Identifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!isValidSlug(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid slug.`); + } + + Object.freeze(this); + } + + /** + * @param {string} str + * + * @returns {SlugIdentifier} + */ + static create(str) { + return new this(createSlug(str)); + } +} diff --git a/src/well-known/TimeUuidIdentifier.js b/src/well-known/TimeUuidIdentifier.js new file mode 100644 index 0000000..ef830c8 --- /dev/null +++ b/src/well-known/TimeUuidIdentifier.js @@ -0,0 +1,25 @@ +import uuid from 'uuid'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import UuidIdentifier from './UuidIdentifier'; + +export default class TimeUuidIdentifier extends UuidIdentifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-1[0-9A-Fa-f]{3}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid version 1 UUID.`); + } + + Object.freeze(this); + } + + /** + * @returns {TimeUuidIdentifier} + */ + static generate() { + return new this(uuid.v1()); + } +} diff --git a/src/well-known/UuidIdentifier.js b/src/well-known/UuidIdentifier.js new file mode 100644 index 0000000..4cf971e --- /dev/null +++ b/src/well-known/UuidIdentifier.js @@ -0,0 +1,25 @@ +import uuid from 'uuid'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Identifier from './Identifier'; + +export default class UuidIdentifier extends Identifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid UUID.`); + } + + Object.freeze(this); + } + + /** + * @returns {UuidIdentifier} + */ + static generate() { + return new this(uuid.v4()); + } +} diff --git a/src/well-known/big-number.js b/src/well-known/big-number.js deleted file mode 100644 index 9d79a04..0000000 --- a/src/well-known/big-number.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -import BaseBigNumber from 'bignumber.js'; - -/** - * @link https://www.npmjs.com/package/bignumber.js - */ -export default class BigNumber extends BaseBigNumber {} diff --git a/src/well-known/dated-slug-identifier.js b/src/well-known/dated-slug-identifier.js deleted file mode 100644 index 1547b90..0000000 --- a/src/well-known/dated-slug-identifier.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -import SlugUtils from 'gdbots/common/util/slug-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import Identifier from 'gdbots/pbj/well-known/identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class DatedSlugIdentifier extends SystemUtils.mixinClass(Identifier) -{ - /** - * @param string slug - * - * @throws \InvalidArgumentException - */ - constructor(slug) { - super(); // require before using `this` - - if ('string' !== typeof slug) { - throw new InvalidArgumentException('String expected but got [' + StringUtils.varToString(slug) + '].'); - } - - if (!SlugUtils.isValid(slug, true) || !SlugUtils.containsDate(slug)) { - throw new InvalidArgumentException('The value [' + slug + '] is not a valid dated slug.'); - } - - privateProps.set(this, { - /** @var string */ - slug: slug - }); - } - - /** - * @param string string - * @param Date date - * - * @return static - */ - static create(string, date) { - let slug = new this(SlugUtils.create(string)); - - if (!SlugUtils.containsDate(slug)) { - date = date ? date : new Date(); - - slug = SlugUtils.addDate(slug, date); - } - - return new this(slug); - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(string); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).slug; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/src/well-known/dynamic-field.js b/src/well-known/dynamic-field.js deleted file mode 100644 index 9904f01..0000000 --- a/src/well-known/dynamic-field.js +++ /dev/null @@ -1,280 +0,0 @@ -'use strict'; - -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import DynamicFieldKind from 'gdbots/pbj/enum/dynamic-field-kind'; -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Field from 'gdbots/pbj/field'; -import BooleanType from 'gdbots/pbj/type/boolean-type'; -import DateType from 'gdbots/pbj/type/date-type'; -import FloatType from 'gdbots/pbj/type/float-type'; -import IntType from 'gdbots/pbj/type/int-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import TextType from 'gdbots/pbj/type/text-type'; - -/** - * Regular expression pattern for matching a valid dynamic field name. - * - * @constant string - */ -export const VALID_NAME_PATTERN = /^[a-zA-Z_]{1}[a-zA-Z0-9_-]*/; - -/** - * Fields are only used to allow for type guarding/encoding/decoding. - * - * @var Field[] - */ -let _fields = []; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * DynamicField is a wrapper for fields which would not be ideal as a map because - * you don't know what the field name is going to be until runtime or the number - * of fields you'll end up having will be too large. - * - * A common use case is a polling or custom form service. Eventually the number of - * fields you have is in the thousands and systems like SQL, ElasticSearch will not - * do well with that many fields. DynamicField is designed to be a "named union". - * - * For example: - * [ - * // the name of the field - * 'name' => 'your-field-name', - * // only one of the following values can be populated. - * 'bool_val' => true, - * 'date_val' => '2015-12-25', - * 'float_val' => 1.0, - * 'int_val' => 1, - * 'string_val' => 'string', - * 'text_val' => 'some text', - * ] - */ -export default class DynamicField extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * @param string name - * @param DynamicFieldKind kind - * @param mixed value - */ - constructor(name, kind, value) { - super(); // require before using `this` - - if (1 > name.length || name.length > 127) { - throw new Error('DynamicField name length must be between 1 to 127.'); - } - if (!VALID_NAME_PATTERN.test(name)) { - throw new Error('DynamicField name [' + name + '] must match pattern [' + VALID_NAME_PATTERN + '].'); - } - - let field = createField(kind.getValue()); - - privateProps.set(this, { - /** @var string */ - name: name, - - /** @var string */ - kind: kind.getValue(), - - /** @var mixed */ - value: field.getType().decode(value, field) - }); - - field.guardValue(privateProps.get(this).value); - } - - /** - * @param string name - * @param bool value - * - * @return self - */ - static createBoolVal(name, value = false) { - return new this(name, DynamicFieldKind.BOOL_VAL, value); - } - - /** - * @param string name - * @param \DateTime value - * - * @return self - */ - static createDateVal(name, value) { - return new this(name, DynamicFieldKind.DATE_VAL, value); - } - - /** - * @param string name - * @param float value - * - * @return self - */ - static createFloatVal(name, value = 0.0) { - return new this(name, DynamicFieldKind.FLOAT_VAL, value); - } - - /** - * @param string name - * @param int value - * - * @return self - */ - static createIntVal(name, value = 0) { - return new this(name, DynamicFieldKind.INT_VAL, value); - } - - /** - * @param string name - * @param string value - * - * @return self - */ - static createStringVal(name, value) { - return new this(name, DynamicFieldKind.STRING_VAL, value); - } - - /** - * @param string name - * @param string value - * - * @return self - */ - static createTextVal(name, value) { - return new this(name, DynamicFieldKind.TEXT_VAL, value); - } - - /** - * {@inheritdoc} - */ - static fromArray(data = {}) { - if (undefined === data.name) { - throw new InvalidArgumentException('DynamicField "name" property must be set.'); - } - - let name = data.name; - - delete data.name; - - let kind = Array.keys(data)[0]; - - try { - kind = DynamicFieldKind[kind.toUpperCase()]; - } catch (e) { - throw new InvalidArgumentException('DynamicField "' + kind + '" is not a valid kind.'); - } - - return new this(name, kind, data[kind.getValue()]); - } - - /** - * {@inheritdoc} - */ - toArray() { - let field = createField(privateProps.get(this).kind); - - let data = { - 'name': privateProps.get(this).name - }; - - data[privateProps.get(this).kind] = field.getType().encode(privateProps.get(this).value, field); - - return data; - } - - /** - * @return string - */ - toString() { - return JSON.stringify(this); - } - - /** - * @return string - */ - getName() { - return privateProps.get(this).name; - } - - /** - * @return string - */ - getKind() { - return privateProps.get(this).kind; - } - - /** - * @return Field - */ - getField() { - return createField(privateProps.get(this).kind); - } - - /** - * @return mixed - */ - getValue() { - return privateProps.get(this).value; - } - - /** - * @param DynamicField other - * - * @return bool - */ - equals(other) { - return privateProps.get(this).name === privateProps.get(other).name - && privateProps.get(this).kind === privateProps.get(other).kind - && privateProps.get(this).value === privateProps.get(other).value; - } -} - -/** - * @param string kind - * - * @return Field - */ -function createField(kind) { - if (undefined === _fields[kind]) { - let type; - - switch (kind) { - case DynamicFieldKind.STRING_VAL.getValue(): - type = StringType.create(); - break; - - case DynamicFieldKind.TEXT_VAL.getValue(): - type = TextType.create(); - break; - - case DynamicFieldKind.INT_VAL.getValue(): - type = IntType.create(); - break; - - case DynamicFieldKind.BOOL_VAL.getValue(): - type = BooleanType.create(); - break; - - case DynamicFieldKind.FLOAT_VAL.getValue(): - type = FloatType.create(); - break; - - case DynamicFieldKind.DATE_VAL.getValue(): - type = DateType.create(); - break; - - default: - throw new InvalidArgumentException('DynamicField "' + kind + '" is not a valid type.'); - } - - _fields[kind] = new Field(kind, type, FieldRule.A_SINGLE_VALUE, true); - } - - return _fields[kind]; -} diff --git a/src/well-known/generates-identifier.js b/src/well-known/generates-identifier.js deleted file mode 100644 index 462f03f..0000000 --- a/src/well-known/generates-identifier.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -export default class GeneratesIdentifier -{ - /** - * @return static - */ - static generate() { - throw new Error('Interface function.'); - } -} diff --git a/src/well-known/geo-point.js b/src/well-known/geo-point.js deleted file mode 100644 index 395a85c..0000000 --- a/src/well-known/geo-point.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Represents a GeoJson Point value. - * - * @link http://geojson.org/geojson-spec.html#point - */ -export default class GeoPoint extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * @param float lat - * @param float lon - * - * @throws \InvalidArgumentException - */ - constructor(lat, lon) { - super(); // require before using `this` - - privateProps.set(this, { - /** @var float */ - latitude: parseFloat(lat), - - /** @var float */ - longitude: parseFloat(lon) - }); - - if (privateProps.get(this).latitude > 90.0 || privateProps.get(this).latitude < -90.0) { - throw new InvalidArgumentException('Latitude must be within range [-90.0, 90.0]'); - } - - if (privateProps.get(this).longitude > 180.0 || privateProps.get(this).longitude < -180.0) { - throw new InvalidArgumentException('Longitude must be within range [-180.0, 180.0]'); - } - } - - /** - * @return float - */ - getLatitude() { - return privateProps.get(this).latitude; - } - - /** - * @return float - */ - getLongitude() { - return privateProps.get(this).longitude; - } - - /** - * {@inheritdoc} - */ - static fromArray(data = {}) { - if (undefined !== data.coordinates) { - return new this(data.coordinates[1], data.coordinates[0]); - } - - throw new InvalidArgumentException('Payload must be a GeoJson "Point" type.'); - } - - /** - * {@inheritdoc} - */ - toArray() { - return { - 'type': 'Point', - 'coordinates': [privateProps.get(this).longitude, privateProps.get(this).latitude] - }; - } - - /** - * @param string string A string with format lat,long - * @return self - */ - static fromString(string) { - string = string.split(','); - - return new this(string[0], string[1]); - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).latitude + ',' + privateProps.get(this).longitude; - } -} diff --git a/src/well-known/identifier.js b/src/well-known/identifier.js deleted file mode 100644 index e30bfa9..0000000 --- a/src/well-known/identifier.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -export default class Identifier -{ - /** - * Creates an identifier object from a string representation - * - * @param string string - * - * @return static - * - * @throws \InvalidArgumentException - */ - static fromString(string) { - throw new Error('Interface function.'); - } - - /** - * Returns a string that can be parsed by fromString() - * - * @return string - */ - toString() { - throw new Error('Interface function.'); - } - - /** - * Compares the object to another Identifier object. Returns true if both have the same type and value. - * - * @param Identifier other - * - * @return boolean - */ - equals(other) { - throw new Error('Interface function.'); - } -} diff --git a/src/well-known/index.js b/src/well-known/index.js new file mode 100644 index 0000000..6c2c63b --- /dev/null +++ b/src/well-known/index.js @@ -0,0 +1,21 @@ +import BigNumber from './BigNumber'; +import DatedSlugIdentifier from './DatedSlugIdentifier'; +import DynamicField from './DynamicField'; +import GeoPoint from './GeoPoint'; +import Identifier from './Identifier'; +import Microtime from './Microtime'; +import SlugIdentifier from './SlugIdentifier'; +import TimeUuidIdentifier from './TimeUuidIdentifier'; +import UuidIdentifier from './UuidIdentifier'; + +export default { + BigNumber, + DatedSlugIdentifier, + DynamicField, + GeoPoint, + Identifier, + Microtime, + SlugIdentifier, + TimeUuidIdentifier, + UuidIdentifier, +}; diff --git a/src/well-known/microtime.js b/src/well-known/microtime.js deleted file mode 100644 index 7723573..0000000 --- a/src/well-known/microtime.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -import DateUtils from 'gdbots/common/util/date-utils.js'; -import StringUtils from 'gdbots/common/util/string-utils.js'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Value object for microtime with methods to convert to and from integers. - * - * @link http://php.net/manual/en/function.microtime.php - */ -export default class Microtime -{ - /** - * Private constructor to ensure static methods are used. - */ - constructor() { - - privateProps.set(this, { - /** - * The microtime is stored as a 16 digit integer. - * - * @var int - */ - int: 0, - - /** @var int */ - sec: 0, - - /** @var int */ - usec: 0 - }); - } - - /** - * Create a new object using the current microtime. - * - * @return self - */ - static create() { - return this.fromTimeOfDay(DateUtils.gettimeofday()); - } - - /** - * Create a new object from a float value, typically one that is returned - * from the microtime(true) call. - * - * @link http://php.net/manual/en/function.microtime.php - * - * @param float float e.g. 1422060753.9581 - * - * @return self - */ - static fromFloat(float) { - let str = StringUtils.strPad(float.replace('.', ''), 16, '0').substring(0, 16); - let m = new this(); - privateProps.get(m).int = parseInt(str); - privateProps.get(m).sec = parseInt(str.substring(0, 10)); - privateProps.get(m).usec = parseInt(str.slice(-6)); - return m; - } - - /** - * Create a new object from the result of a gettimeofday call that - * is NOT returned as a float. - * - * @link http://php.net/manual/en/function.gettimeofday.php - * - * @param array tod - * - * @return self - */ - static fromTimeOfDay(tod) { - let str = tod.sec + StringUtils.strPad(tod.usec, 6, '0', 'STR_PAD_LEFT'); - let m = new this(); - privateProps.get(m).int = parseInt(str); - privateProps.get(m).sec = parseInt(str.substring(0, 10)); - privateProps.get(m).usec = parseInt(str.slice(-6)); - return m; - } - - /** - * Create a new object from the integer (or string) version of the microtime. - * - * Total digits would be unix timestamp (10) + (3-6) microtime digits. - * Lack of precision on digits will be automatically padded with zeroes. - * - * @param string|int stringOrInteger - * - * @return self - * - * @throws \InvalidArgumentException - */ - static fromString(stringOrInteger) { - let int = String(stringOrInteger); - let len = String(int).length; - if (len < 13 || len > 16) { - throw new InvalidArgumentException('Input [' + int + '] must be between 13 and 16 digits, [' + len + '] given.'); - } - - if (len < 16) { - int = StringUtils.strPad(int, 16, '0'); - } - - let m = new this(); - privateProps.get(m).int = parseInt(int); - privateProps.get(m).sec = parseInt(int.substring(0, 10)); - privateProps.get(m).usec = parseInt(int.slice(-6)); - return m; - } - - /** - * @return string - */ - toString() { - return String(privateProps.get(this).int); - } - - /** - * @return int - */ - getSeconds() { - return privateProps.get(this).sec; - } - - /** - * @return int - */ - getMicroSeconds() { - return privateProps.get(this).usec; - } - - /** - * @return Date - */ - toDateTime() { - let d = new Date(); - d.setTime(parseFloat(privateProps.get(this).sec + '.' + StringUtils.strPad(privateProps.get(this).usec, 6, '0', 'STR_PAD_LEFT')) * 1000); - return d; - } - - /** - * @return float - */ - toFloat() { - return parseFloat(privateProps.get(this).sec + '.' + StringUtils.strPad(privateProps.get(this).usec, 6, '0', 'STR_PAD_LEFT')); - } -} diff --git a/src/well-known/slug-identifier.js b/src/well-known/slug-identifier.js deleted file mode 100644 index 45daedf..0000000 --- a/src/well-known/slug-identifier.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -import SlugUtils from 'gdbots/common/util/slug-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import Identifier from 'gdbots/pbj/well-known/identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class SlugIdentifier extends SystemUtils.mixinClass(Identifier) -{ - /** - * @param string slug - * - * @throws \InvalidArgumentException - */ - constructor(slug) { - super(); // require before using `this` - - if ('string' !== typeof slug) { - throw new InvalidArgumentException('String expected but got [' + StringUtils.varToString(slug) + '].'); - } - - if (!SlugUtils.isValid(slug)) { - throw new InvalidArgumentException('The value [' + slug + '] is not a valid slug.'); - } - - privateProps.set(this, { - /** @var string */ - slug: slug - }); - } - - /** - * @param string string - * - * @return static - */ - static create(string) { - return new this(SlugUtils.create(string)); - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(string); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).slug; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/src/well-known/string-identifier.js b/src/well-known/string-identifier.js deleted file mode 100644 index 72f306d..0000000 --- a/src/well-known/string-identifier.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import Identifier from 'gdbots/pbj/well-known/identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class StringIdentifier extends SystemUtils.mixinClass(Identifier) -{ - /** - * @param string string - * - * @throws \InvalidArgumentException - */ - constructor(string) { - super(); // require before using `this` - - if ('string' !== typeof string) { - throw new InvalidArgumentException('String expected but got [' + StringUtils.varToString(string) + '].'); - } - - privateProps.set(this, { - /** @var string */ - string: String(string).trim() - }); - - if (!privateProps.get(this).string || privateProps.get(this).string.length === 0) { - throw new InvalidArgumentException('String cannot be empty.'); - } - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(string); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).string; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/src/well-known/time-uuid-identifier.js b/src/well-known/time-uuid-identifier.js deleted file mode 100644 index 59d9e21..0000000 --- a/src/well-known/time-uuid-identifier.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -import Uuid from 'uuid'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; - -export default class TimeUuidIdentifier extends SystemUtils.mixinClass(UuidIdentifier) -{ - /** - * @param string uuid - * - * @throws \InvalidArgumentException - */ - constructor(uuid) { - super(uuid); - - let version1Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - if (!version1Regex.test(uuid)) { - throw new InvalidArgumentException('A time based (version 1) uuid is required.'); - } - } - - /** - * {@inheritdoc} - */ - static generate() { - return new this(Uuid.v1()); - } -} diff --git a/src/well-known/uuid-identifier.js b/src/well-known/uuid-identifier.js deleted file mode 100644 index 90b5256..0000000 --- a/src/well-known/uuid-identifier.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -import Uuid from 'uuid'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Identifier from 'gdbots/pbj/well-known/identifier'; -import GeneratesIdentifier from 'gdbots/pbj/well-known/generates-identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class UuidIdentifier extends SystemUtils.mixinClass(Identifier, GeneratesIdentifier) -{ - /** - * @param string uuid - */ - constructor(uuid) { - super(); // require before using `this` - - uuid = uuid.toLowerCase(); - - let version1Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - if (!version1Regex.test(uuid)) { - throw new Error('An invalid uuid [' + uuid + '] was provided.'); - } - - privateProps.set(this, { - /** @var string */ - uuid: uuid - }); - } - - /** - * {@inheritdoc} - */ - static generate() { - return new this(Uuid.v4()); - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(Uuid.unparse(Uuid.parse(string))); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).uuid; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/tests/Field.test.js b/tests/Field.test.js new file mode 100644 index 0000000..c3df2ad --- /dev/null +++ b/tests/Field.test.js @@ -0,0 +1,319 @@ +import test from 'tape'; +import Field from '../src/Field'; +import FieldRule from '../src/enums/FieldRule'; +import Format from '../src/enums/Format'; +import T from '../src/types'; +import SampleStringEnum from './fixtures/enums/SampleStringEnum'; + +test('Field tests', (t) => { + let field = new Field({ + name: 'test', + type: T.StringType.create(), + required: true, + format: Format.SLUG, + defaultValue: 'homer-simpson', + }); + + t.true(field instanceof Field, 'field MUST be an instanceOf Field'); + t.same(field.getName(), 'test'); + t.true(field.isRequired()); + t.same(field.getFormat(), Format.SLUG); + t.same(field.getType(), T.StringType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.true(field.isASingleValue()); + t.false(field.isOverridable()); + t.same(field.getDefault(), 'homer-simpson'); + + try { + field.test = 1; + t.fail('field instance is mutable'); + } catch (e) { + t.pass('field instance is immutable'); + } + + field = new Field({ + name: 'test', + type: T.DateType.create(), + rule: FieldRule.A_LIST, + overridable: true, + }); + t.same(field.getType(), T.DateType.create()); + t.true(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.true(field.isOverridable()); + + field = new Field({ + name: 'test', + type: T.DateType.create(), + rule: FieldRule.A_MAP, + }); + t.same(field.getType(), T.DateType.create()); + t.false(field.isAList()); + t.true(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = new Field({ + name: 'test', + type: T.UuidType.create(), + rule: FieldRule.A_SET, + }); + t.same(field.getType(), T.UuidType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.true(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = new Field({ + name: 'test', + type: T.UuidType.create(), + useTypeDefault: false, + }); + t.false(field.useTypeDefault); + t.same(field.getDefault(), null); + + field = new Field({ + name: 'test', + type: T.IntType.create(), + min: 5, + max: 10, + }); + t.same(field.getMin(), 5); + t.same(field.getMax(), 10); + + field = new Field({ + name: 'test', + type: T.DecimalType.create(), + precision: 8, + scale: 4, + }); + t.same(field.getPrecision(), 8); + t.same(field.getScale(), 4); + + field = new Field({ + name: 'test', + type: T.StringType.create(), + minLength: 5, + maxLength: 10, + }); + t.same(field.getMinLength(), 5); + t.same(field.getMaxLength(), 10); + + field = new Field({ + name: 'test', + type: T.StringType.create(), + pattern: '/^a-z$/', + }); + const regex = new RegExp('^a-z$'); + t.true(field.getPattern() instanceof RegExp); + t.same(`${field.getPattern()}`, `${regex}`); + + t.end(); +}); + + +test('Field applyDefault(Enum) tests', (t) => { + let field = new Field({ + name: 'test', + type: T.StringEnumType.create(), + classProto: SampleStringEnum, + required: true, + defaultValue: SampleStringEnum.ENUM1.toString(), + }); + t.true(field.defaultValue === SampleStringEnum.ENUM1); + + field = new Field({ + name: 'test', + type: T.StringEnumType.create(), + classProto: SampleStringEnum, + required: true, + defaultValue: SampleStringEnum.ENUM1, + }); + t.true(field.defaultValue === SampleStringEnum.ENUM1); + + field = new Field({ + name: 'test', + type: T.StringEnumType.create(), + classProto: SampleStringEnum, + required: true, + defaultValue: () => SampleStringEnum.ENUM1, + }); + t.true(field.getDefault() === SampleStringEnum.ENUM1); + + t.end(); +}); + + +test('Field getDefault(dynamic) tests', (t) => { + const field = new Field({ + name: 'test', + type: T.StringType.create(), + required: true, + defaultValue: (message, f) => { + const m = message ? message.test : ''; + return `dynamic:${m}:${f.getName()}`; + }, + }); + + t.same(field.getDefault(null), 'dynamic::test'); + + const message = { test: 1 }; + t.same(field.getDefault(message), 'dynamic:1:test'); + + message.test = 2; + t.same(field.getDefault(message), 'dynamic:2:test'); + + t.end(); +}); + + +test('Field assertion tests', (t) => { + const field = new Field({ + name: 'test', + type: T.StringType.create(), + required: true, + assertion: (value) => { + if (value === 'should_fail') { + throw new Error('should_fail is not accepted.'); + } + }, + }); + + try { + field.guardValue(null); + t.fail('required field accepted null'); + } catch (e) { + t.pass(e.message); + } + + try { + field.guardValue('should_fail'); + t.fail('should_fail was accepted.'); + } catch (e) { + t.pass(e.message); + } + + try { + field.guardValue('should_not_fail'); + t.pass('should_not_fail was accepted.'); + } catch (e) { + t.pass(e.message); + } + + t.end(); +}); + + +test('Field guardDefault(A_SINGLE_VALUE) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_SINGLE_VALUE }); + + try { + field.guardDefault('value'); + t.pass('accepted a valid single value'); + } catch (e) { + t.fail('did not accept a valid single value'); + } + + const invalid = [1, ['not-a-single-value']]; + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid single value'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('Field guardDefault(A_LIST) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_LIST }); + + try { + field.guardDefault(['string', 'test']); + t.pass('accepted a valid list/array'); + } catch (e) { + t.fail('did not accept a valid list/array'); + } + + const invalid = [[1], new Map(), new Set(), 'string']; + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid list/array'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('Field guardDefault(A_MAP) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_MAP }); + + try { + field.guardDefault((new Map()).set('string', 'test')); + t.pass('accepted a valid map'); + } catch (e) { + t.fail('did not accept a valid map'); + } + + const invalid = [ + (new Map()).set('notastring', 1), + new Set(), + ['stringnotinmap'], + 'string', + ]; + + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid map'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('Field guardDefault(A_SET) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_SET }); + + try { + field.guardDefault((new Set()).add('val1').add('val2')); + t.pass('accepted a valid set'); + } catch (e) { + t.fail('did not accept a valid set'); + } + + const invalid = [ + (new Set()).add('val1').add(2), + new Map(), + ['not-a-set'], + 'not-a-set', + ]; + + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid set'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + diff --git a/tests/FieldBuilder.test.js b/tests/FieldBuilder.test.js new file mode 100644 index 0000000..4036ed6 --- /dev/null +++ b/tests/FieldBuilder.test.js @@ -0,0 +1,79 @@ +import test from 'tape'; +import Fb from '../src/FieldBuilder'; +import Field from '../src/Field'; +import Format from '../src/enums/Format'; +import T from '../src/types'; + +test('FieldBuilder tests', (t) => { + let field = Fb.create('test', T.StringType.create()) + .required() + .format(Format.SLUG) + .withDefault('homer-simpson') + .build(); + + t.true(field instanceof Field, 'field MUST be an instanceOf Field'); + t.same(field.getName(), 'test'); + t.true(field.isRequired()); + t.same(field.getFormat(), Format.SLUG); + t.same(field.getType(), T.StringType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.true(field.isASingleValue()); + t.false(field.isOverridable()); + t.same(field.getDefault(), 'homer-simpson'); + + try { + field.test = 1; + t.fail('field instance is mutable'); + } catch (e) { + t.pass('field instance is immutable'); + } + + field = Fb.create('test', T.DateType.create()).asAList().overridable(true).build(); + t.same(field.getType(), T.DateType.create()); + t.true(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.true(field.isOverridable()); + + field = Fb.create('test', T.DateType.create()).asAMap().build(); + t.same(field.getType(), T.DateType.create()); + t.false(field.isAList()); + t.true(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = Fb.create('test', T.UuidType.create()).asASet().build(); + t.same(field.getType(), T.UuidType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.true(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = Fb.create('test', T.UuidType.create()).useTypeDefault(false).build(); + t.false(field.useTypeDefault); + t.same(field.getDefault(), null); + + field = Fb.create('test', T.IntType.create()).min(5).max(10).build(); + t.same(field.getMin(), 5); + t.same(field.getMax(), 10); + + field = Fb.create('test', T.DecimalType.create()).precision(8).scale(4).build(); + t.same(field.getPrecision(), 8); + t.same(field.getScale(), 4); + + field = Fb.create('test', T.StringType.create()).minLength(5).maxLength(10).build(); + t.same(field.getMinLength(), 5); + t.same(field.getMaxLength(), 10); + + field = Fb.create('test', T.StringType.create()).pattern('/^a-z$/').build(); + const regex = new RegExp('^a-z$'); + t.true(field.getPattern() instanceof RegExp); + t.same(`${field.getPattern()}`, `${regex}`); + + t.end(); +}); diff --git a/tests/Message.list.test.js b/tests/Message.list.test.js new file mode 100644 index 0000000..66ff363 --- /dev/null +++ b/tests/Message.list.test.js @@ -0,0 +1,137 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; + +test('Message string_list tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_list')); + t.false(msg.hasClearedField('string_list')); + + msg.addToList('string_list', ['test1', 'test2']); + t.true(msg.has('string_list')); + t.same(msg.get('string_list'), ['test1', 'test2']); + t.same(msg.get('string_list', ['default']), ['test1', 'test2']); + t.same(msg.getFromListAt('string_list', 0), 'test1'); + t.same(msg.getFromListAt('string_list', 1), 'test2'); + t.same(msg.getFromListAt('string_list', 0, 'default'), 'test1'); + t.same(msg.getFromListAt('string_list', 1, 'default'), 'test2'); + t.same(msg.getFromListAt('string_list', 2, 'default'), 'default'); + t.true(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.false(msg.isInList('string_list', 'test3')); + + msg.addToList('string_list', ['test3']); + t.same(msg.get('string_list'), ['test1', 'test2', 'test3']); + t.same(msg.getFromListAt('string_list', 2, 'default'), 'test3'); + t.true(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.true(msg.isInList('string_list', 'test3')); + + msg.removeFromListAt('string_list', 0); + t.same(msg.get('string_list'), ['test2', 'test3']); + t.same(msg.getFromListAt('string_list', 0), 'test2'); + t.same(msg.getFromListAt('string_list', 1), 'test3'); + t.false(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.true(msg.isInList('string_list', 'test3')); + + msg.addToList('string_list', ['test1']); + t.same(msg.get('string_list'), ['test2', 'test3', 'test1']); + t.same(msg.getFromListAt('string_list', 0), 'test2'); + t.same(msg.getFromListAt('string_list', 1), 'test3'); + t.same(msg.getFromListAt('string_list', 2), 'test1'); + t.true(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.true(msg.isInList('string_list', 'test3')); + + t.same(msg.getFromListAt('string_list', 0), 'test2'); + msg.removeFromListAt('string_list', 0); + t.same(msg.getFromListAt('string_list', 0), 'test3'); + msg.removeFromListAt('string_list', 0); + t.same(msg.getFromListAt('string_list', 0), 'test1'); + msg.removeFromListAt('string_list', 0); + t.true(msg.hasClearedField('string_list')); + t.same(msg.getClearedFields(), ['string_list']); + t.same(msg.get('string_list', ['what']), ['what']); + + msg.addToList('string_list', ['test1', 'test2']); + // ensure we can't modify the internal array + const myList = msg.get('string_list'); + t.false(myList === msg.get('string_list')); + t.same(myList, ['test1', 'test2']); + myList.push('test3'); + t.same(myList, ['test1', 'test2', 'test3']); + t.same(msg.get('string_list'), ['test1', 'test2']); + + t.end(); +}); + + +test('Message message_list tests', (t) => { + const msg = SampleMessageV1.create(); + const otherMsg1 = SampleOtherMessageV1.create().set('test', 'test1'); + const otherMsg2 = SampleOtherMessageV1.create().set('test', 'test2'); + const otherMsg3 = SampleOtherMessageV1.create().set('test', 'test3'); + + t.false(msg.has('message_list')); + t.false(msg.hasClearedField('message_list')); + + msg.addToList('message_list', [otherMsg1, otherMsg2]); + t.true(msg.has('message_list')); + t.same(msg.get('message_list'), [otherMsg1, otherMsg2]); + t.same(msg.get('message_list', ['default']), [otherMsg1, otherMsg2]); + t.same(msg.getFromListAt('message_list', 0), otherMsg1); + t.same(msg.getFromListAt('message_list', 1), otherMsg2); + t.same(msg.getFromListAt('message_list', 0, 'default'), otherMsg1); + t.same(msg.getFromListAt('message_list', 1, 'default'), otherMsg2); + t.same(msg.getFromListAt('message_list', 2, 'default'), 'default'); + t.true(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.false(msg.isInList('message_list', otherMsg3)); + + msg.addToList('message_list', [otherMsg3]); + t.same(msg.get('message_list'), [otherMsg1, otherMsg2, otherMsg3]); + t.same(msg.getFromListAt('message_list', 2, 'default'), otherMsg3); + t.true(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.true(msg.isInList('message_list', otherMsg3)); + + msg.removeFromListAt('message_list', 0); + t.same(msg.get('message_list'), [otherMsg2, otherMsg3]); + t.same(msg.getFromListAt('message_list', 0), otherMsg2); + t.same(msg.getFromListAt('message_list', 1), otherMsg3); + t.false(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.true(msg.isInList('message_list', otherMsg3)); + + msg.addToList('message_list', [otherMsg1]); + t.same(msg.get('message_list'), [otherMsg2, otherMsg3, otherMsg1]); + t.same(msg.getFromListAt('message_list', 0), otherMsg2); + t.same(msg.getFromListAt('message_list', 1), otherMsg3); + t.same(msg.getFromListAt('message_list', 2), otherMsg1); + t.true(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.true(msg.isInList('message_list', otherMsg3)); + + t.same(msg.getFromListAt('message_list', 0), otherMsg2); + msg.removeFromListAt('message_list', 0); + t.same(msg.getFromListAt('message_list', 0), otherMsg3); + msg.removeFromListAt('message_list', 0); + t.same(msg.getFromListAt('message_list', 0), otherMsg1); + msg.removeFromListAt('message_list', 0); + t.true(msg.hasClearedField('message_list')); + t.same(msg.getClearedFields(), ['message_list']); + t.same(msg.get('message_list', ['what']), ['what']); + + msg.addToList('message_list', [otherMsg1, otherMsg2]); + // ensure we can't modify the internal array + const myList = msg.get('message_list'); + t.false(myList === msg.get('message_list')); + t.same(myList, [otherMsg1, otherMsg2]); + myList.push(otherMsg3); + t.same(myList, [otherMsg1, otherMsg2, otherMsg3]); + t.same(msg.get('message_list'), [otherMsg1, otherMsg2]); + + t.end(); +}); diff --git a/tests/Message.map.test.js b/tests/Message.map.test.js new file mode 100644 index 0000000..521a87f --- /dev/null +++ b/tests/Message.map.test.js @@ -0,0 +1,160 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; + +test('Message string_map tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_map')); + t.false(msg.hasClearedField('string_map')); + + msg.addToMap('string_map', 'test1', 'val1'); + msg.addToMap('string_map', 'test2', 'val2'); + t.true(msg.has('string_map')); + t.same(msg.get('string_map'), { test1: 'val1', test2: 'val2' }); + t.same(msg.get('string_map', { test1: 'default' }), { test1: 'val1', test2: 'val2' }); + t.same(msg.getFromMap('string_map', 'test1'), 'val1'); + t.same(msg.getFromMap('string_map', 'test2'), 'val2'); + t.true(msg.isInMap('string_map', 'test1')); + t.true(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test3', 'val3'); + t.same(msg.get('string_map'), { test1: 'val1', test2: 'val2', test3: 'val3' }); + t.same(msg.getFromMap('string_map', 'test1'), 'val1'); + t.same(msg.getFromMap('string_map', 'test2'), 'val2'); + t.same(msg.getFromMap('string_map', 'test3'), 'val3'); + t.true(msg.isInMap('string_map', 'test1')); + t.true(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.removeFromMap('string_map', 'test2'); + t.same(msg.get('string_map'), { test1: 'val1', test3: 'val3' }); + t.true(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test1', 'newval1'); + t.same(msg.getFromMap('string_map', 'test1'), 'newval1'); + t.same(msg.get('string_map'), { test1: 'newval1', test3: 'val3' }); + t.true(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test2', 'newval2'); + t.same(msg.getFromMap('string_map', 'test2'), 'newval2'); + t.same(msg.getFromMap('string_map', 'invalid', 'default'), 'default'); + t.same(msg.get('string_map'), { test1: 'newval1', test3: 'val3', test2: 'newval2' }); + t.true(msg.isInMap('string_map', 'test1')); + t.true(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.removeFromMap('string_map', 'test1'); + msg.removeFromMap('string_map', 'test2'); + msg.removeFromMap('string_map', 'test3'); + t.true(msg.hasClearedField('string_map')); + t.same(msg.getClearedFields(), ['string_map']); + t.false(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test1', 'val1'); + t.false(msg.hasClearedField('string_map')); + t.same(msg.getClearedFields(), []); + t.true(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + msg.clear('string_map'); + t.true(msg.hasClearedField('string_map')); + t.same(msg.getClearedFields(), ['string_map']); + t.false(msg.has('string_map')); + t.same(msg.get('string_map'), null); + t.same(msg.get('string_map', { test: 'what' }), { test: 'what' }); + t.false(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + t.end(); +}); + + +test('Message message_map tests', (t) => { + const msg = SampleMessageV1.create(); + const otherMsg1 = SampleOtherMessageV1.create().set('test', 'test1'); + const otherMsg2 = SampleOtherMessageV1.create().set('test', 'test2'); + const otherMsg3 = SampleOtherMessageV1.create().set('test', 'test3'); + const otherMsg4 = SampleOtherMessageV1.create().set('test', 'test4'); + + t.false(msg.has('message_map')); + t.false(msg.hasClearedField('message_map')); + + msg.addToMap('message_map', 'test1', otherMsg1); + msg.addToMap('message_map', 'test2', otherMsg2); + t.true(msg.has('message_map')); + t.same(msg.get('message_map'), { test1: otherMsg1, test2: otherMsg2 }); + t.same(msg.get('message_map', { test1: 'default' }), { test1: otherMsg1, test2: otherMsg2 }); + t.same(msg.getFromMap('message_map', 'test1'), otherMsg1); + t.same(msg.getFromMap('message_map', 'test2'), otherMsg2); + t.true(msg.isInMap('message_map', 'test1')); + t.true(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test3', otherMsg3); + t.same(msg.get('message_map'), { test1: otherMsg1, test2: otherMsg2, test3: otherMsg3 }); + t.same(msg.getFromMap('message_map', 'test1'), otherMsg1); + t.same(msg.getFromMap('message_map', 'test2'), otherMsg2); + t.same(msg.getFromMap('message_map', 'test3'), otherMsg3); + t.true(msg.isInMap('message_map', 'test1')); + t.true(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.removeFromMap('message_map', 'test2'); + t.same(msg.get('message_map'), { test1: otherMsg1, test3: otherMsg3 }); + t.true(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test1', otherMsg4); + t.same(msg.getFromMap('message_map', 'test1'), otherMsg4); + t.same(msg.get('message_map'), { test1: otherMsg4, test3: otherMsg3 }); + t.true(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test2', otherMsg4); + t.same(msg.getFromMap('message_map', 'test2'), otherMsg2); + t.same(msg.getFromMap('message_map', 'invalid', 'default'), 'default'); + t.same(msg.get('message_map'), { test1: otherMsg4, test3: otherMsg3, test2: otherMsg4 }); + t.true(msg.isInMap('message_map', 'test1')); + t.true(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.removeFromMap('message_map', 'test1'); + msg.removeFromMap('message_map', 'test2'); + msg.removeFromMap('message_map', 'test3'); + t.true(msg.hasClearedField('message_map')); + t.same(msg.getClearedFields(), ['message_map']); + t.false(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test1', otherMsg1); + t.false(msg.hasClearedField('message_map')); + t.same(msg.getClearedFields(), []); + t.true(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + msg.clear('message_map'); + t.true(msg.hasClearedField('message_map')); + t.same(msg.getClearedFields(), ['message_map']); + t.false(msg.has('message_map')); + t.same(msg.get('message_map'), null); + t.same(msg.get('message_map', { test: 'what' }), { test: 'what' }); + t.false(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + t.end(); +}); diff --git a/tests/Message.set.test.js b/tests/Message.set.test.js new file mode 100644 index 0000000..c3a975f --- /dev/null +++ b/tests/Message.set.test.js @@ -0,0 +1,61 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; + +test('Message string_set tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_set')); + t.false(msg.hasClearedField('string_set')); + + msg.addToSet('string_set', ['test1', 'test2']); + t.true(msg.has('string_set')); + t.same(msg.get('string_set'), ['test1', 'test2']); + t.same(msg.get('string_set', ['default']), ['test1', 'test2']); + t.true(msg.isInSet('string_set', 'test1')); + t.true(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + msg.addToSet('string_set', ['test3']); + t.same(msg.get('string_set'), ['test1', 'test2', 'test3']); + t.true(msg.isInSet('string_set', 'test1')); + t.true(msg.isInSet('string_set', 'test2')); + t.true(msg.isInSet('string_set', 'test3')); + + msg.removeFromSet('string_set', ['test2']); + t.same(msg.get('string_set'), ['test1', 'test3']); + t.true(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.true(msg.isInSet('string_set', 'test3')); + + msg.addToSet('string_set', ['test1']); + t.same(msg.get('string_set'), ['test1', 'test3']); + t.true(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.true(msg.isInSet('string_set', 'test3')); + + msg.removeFromSet('string_set', ['test1', 'test2', 'test3']); + t.true(msg.hasClearedField('string_set')); + t.same(msg.getClearedFields(), ['string_set']); + t.false(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + msg.addToSet('string_set', ['test1', 'test2']); + t.false(msg.hasClearedField('string_set')); + t.same(msg.getClearedFields(), []); + t.true(msg.isInSet('string_set', 'test1')); + t.true(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + msg.clear('string_set'); + t.true(msg.hasClearedField('string_set')); + t.same(msg.getClearedFields(), ['string_set']); + t.false(msg.has('string_set')); + t.same(msg.get('string_set'), null); + t.same(msg.get('string_set', ['default']), ['default']); + t.false(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + t.end(); +}); diff --git a/tests/Message.single.test.js b/tests/Message.single.test.js new file mode 100644 index 0000000..53540cb --- /dev/null +++ b/tests/Message.single.test.js @@ -0,0 +1,23 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; + +test('Message string_single tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_single')); + t.false(msg.hasClearedField('string_single')); + + msg.set('string_single', 'test'); + t.true(msg.has('string_single')); + t.same(msg.get('string_single'), 'test'); + t.same(msg.get('string_single', 'default'), 'test'); + + msg.clear('string_single'); + t.true(msg.hasClearedField('string_single')); + t.same(msg.getClearedFields(), ['string_single']); + t.false(msg.has('string_single')); + t.same(msg.get('string_single'), null); + t.same(msg.get('string_single', 'default'), 'default'); + + t.end(); +}); diff --git a/tests/Message.test.js b/tests/Message.test.js new file mode 100644 index 0000000..979b15f --- /dev/null +++ b/tests/Message.test.js @@ -0,0 +1,154 @@ +import test from 'tape'; +import FrozenMessageIsImmutable from '../src/exceptions/FrozenMessageIsImmutable'; +import Message from '../src/Message'; +import MessageRef from '../src/MessageRef'; +import SchemaId from '../src/SchemaId'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleMessageV2 from './fixtures/SampleMessageV2'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; + +test('Message tests', (t) => { + const msg1 = SampleMessageV1.create(); + const msg2 = SampleMessageV2.create(); + + t.true(msg1 instanceof Message, 'msg1 MUST be an instanceOf Message'); + t.true(msg2 instanceof Message, 'msg2 MUST be an instanceOf Message'); + t.true(msg1 instanceof SampleMessageV1, 'msg1 MUST be an instanceOf SampleMessageV1'); + t.true(msg2 instanceof SampleMessageV2, 'msg2 MUST be an instanceOf SampleMessageV2'); + + t.true(SampleMessageV1.schema() === msg1.schema()); + t.true(SampleMessageV2.schema() === msg2.schema()); + t.true(SampleMessageV1.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.true(SampleMessageV2.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + t.true(msg1.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.true(msg2.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + + msg1.set('string_single', '123'); + t.true(msg1.generateMessageRef().equals(MessageRef.fromString('gdbots:pbj.tests::sample-message:123'))); + t.true(msg1.generateMessageRef('tag').equals(MessageRef.fromString('gdbots:pbj.tests::sample-message:123#tag'))); + t.same(msg1.getUriTemplateVars(), { string_single: '123' }); + + t.end(); +}); + + +test('Message generateEtag tests', (t) => { + const msg = SampleMessageV1.create(); + + msg.set('string_single', '123'); + t.same(msg.generateEtag(), '691d8ff26b59e53823ab9190624e34ed'); + t.same(msg.generateEtag(['string_single']), '573915f0765194ff95833311ca5c15c1'); + + msg.set('string_single', ' ice 🍦 poop 💩 doh 😳 '); + t.same(msg.generateEtag(), '2114f330e1ce728c47aad9013f84b07c'); + + msg.set('string_single', '✓ à la mode'); + t.same(msg.generateEtag(), 'ee38a55ab894dbaa551f8870dd9cc4c4'); + + msg.set('string_single', 'foo © bar 𝌆 baz'); + t.same(msg.generateEtag(), 'd6c22aca50a8c30f57e8e40e4d7c400d'); + + msg.clear('string_single'); + t.same(msg.generateEtag(), '3362e2cd5e114f9c9bac62666fc05587'); + t.same(msg.generateEtag(['string_single']), '573915f0765194ff95833311ca5c15c1'); + t.same(msg.generateEtag(['string_single', 'message_map']), '796d3e61906902c815e1bf67685a45d8'); + + t.end(); +}); + + +test('Message freeze tests', (t) => { + let msg = SampleMessageV1.create(); + msg.set('string_single', '123'); + msg.freeze(); + + t.true(msg.isFrozen()); + + try { + msg.set('string_single', 'test'); + t.fail('frozen message is mutable'); + } catch (e) { + t.true(e instanceof FrozenMessageIsImmutable, 'Exception MUST be an instanceOf FrozenMessageIsImmutable'); + t.pass(e.message); + } + + msg = SampleMessageV1.create(); + msg.set('message_single', SampleOtherMessageV1.create().set('test', 'freeze')); + msg.freeze(); + + t.true(msg.isFrozen()); + t.true(msg.get('message_single').isFrozen()); + + try { + msg.get('message_single').set('test', 'test'); + t.fail('nested frozen message is mutable'); + } catch (e) { + t.true(e instanceof FrozenMessageIsImmutable, 'Exception MUST be an instanceOf FrozenMessageIsImmutable'); + t.pass(e.message); + } + + t.end(); +}); + + +test('Message isReplay tests', (t) => { + let msg = SampleMessageV1.create(); + msg.isReplay(true); + t.true(msg.isReplay()); + t.true(msg.isReplay()); + + try { + msg.isReplay(true); + t.fail('isReplay(true) was allowed to be set more than once.'); + } catch (e) { + t.pass(e.message); + } + + msg = SampleMessageV1.create(); + msg.isReplay(false); + t.false(msg.isReplay()); + t.false(msg.isReplay()); + + try { + msg.isReplay(false); + t.fail('isReplay(false) was allowed to be set more than once.'); + } catch (e) { + t.pass(e.message); + } + + msg = SampleMessageV1.create(); + t.false(msg.isReplay()); + t.false(msg.isReplay()); + + try { + msg.isReplay(true); + t.fail('isReplay was allowed to be reset.'); + } catch (e) { + t.pass(e.message); + } + + t.end(); +}); + + +test('Message clone tests', (t) => { + const msg = SampleMessageV1.create(); + msg.set('string_single', '123'); + msg.set('message_single', SampleOtherMessageV1.create().set('test', 'clone')); + msg.freeze(); + + t.true(msg.isFrozen()); + t.true(msg.get('message_single').isFrozen()); + + const msgClone = msg.clone(); + t.false(msg === msgClone); + t.false(msgClone.isFrozen()); + t.false(msgClone.get('message_single').isFrozen()); + t.true(msg.equals(msgClone)); + + msgClone.set('string_single', '456'); + msgClone.get('message_single').set('test', 'clone2'); + t.false(msg.equals(msgClone)); + + t.end(); +}); diff --git a/tests/MessageRef.test.js b/tests/MessageRef.test.js new file mode 100644 index 0000000..2e55a46 --- /dev/null +++ b/tests/MessageRef.test.js @@ -0,0 +1,319 @@ +import test from 'tape'; +import MessageRef from '../src/MessageRef'; +import SchemaCurie from '../src/SchemaCurie'; + +test('MessageRef tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog:node:article'); + const id = '123'; + const tag = null; + const refStr = `${curie}:${id}`; + + const ref = MessageRef.fromString(refStr); + t.true(ref instanceof MessageRef, 'ref MUST be an instanceOf MessageRef'); + t.same(`${ref}`, refStr); + t.same(ref.toString(), refStr); + t.same(ref.valueOf(), refStr); + t.same(ref.toJSON(), { curie: curie.toString(), id }); + t.same(JSON.stringify(ref), `{"curie":"${curie}","id":"${id}"}`); + t.same(ref.getId(), id); + t.same(ref.getTag(), tag); + t.true(ref.hasId()); + t.false(ref.hasTag()); + t.true(ref.equals(new MessageRef(curie, id, tag))); + t.true(ref.equals(MessageRef.fromString(refStr))); + t.true(ref.getCurie() === curie); + + try { + ref.test = 1; + t.fail('ref instance is mutable'); + } catch (e) { + t.pass('ref instance is immutable'); + } + + t.end(); +}); + + +test('MessageRef with empty tag tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog::article'); + const id = '123'; + const tag = ''; + const refStr = `${curie}:${id}`; + + const ref = new MessageRef(curie, id, tag); + t.true(ref instanceof MessageRef, 'ref MUST be an instanceOf MessageRef'); + t.same(`${ref}`, refStr); + t.same(ref.toString(), refStr); + t.same(ref.valueOf(), refStr); + t.same(ref.toJSON(), { curie: curie.toString(), id }); + t.same(JSON.stringify(ref), `{"curie":"${curie}","id":"${id}"}`); + t.same(ref.getId(), id); + t.same(ref.getTag(), null); + t.true(ref.hasId()); + t.false(ref.hasTag()); + t.true(ref.equals(new MessageRef(curie, id, tag))); + t.true(ref.equals(MessageRef.fromString(refStr))); + t.true(ref.getCurie() === curie); + + try { + ref.test = 1; + t.fail('ref instance is mutable'); + } catch (e) { + t.pass('ref instance is immutable'); + } + + t.end(); +}); + + +test('MessageRef with tag tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog:node:article'); + const id = '123'; + const tag = 'tag'; + const refStr = `${curie}:${id}#${tag}`; + + const ref = MessageRef.fromString(refStr); + t.true(ref instanceof MessageRef, 'ref MUST be an instanceOf MessageRef'); + t.same(`${ref}`, refStr); + t.same(ref.toString(), refStr); + t.same(ref.valueOf(), refStr); + t.same(ref.toJSON(), { curie: curie.toString(), id, tag }); + t.same(JSON.stringify(ref), `{"curie":"${curie}","id":"${id}","tag":"${tag}"}`); + t.same(ref.getId(), id); + t.same(ref.getTag(), tag); + t.true(ref.hasId()); + t.true(ref.hasTag()); + t.true(ref.equals(new MessageRef(curie, id, tag))); + t.true(ref.equals(MessageRef.fromString(refStr))); + t.true(ref.getCurie() === curie); + + try { + ref.test = 1; + t.fail('ref instance is mutable'); + } catch (e) { + t.pass('ref instance is immutable'); + } + + t.end(); +}); + + +test('MessageRef fromJSON tests', (t) => { + const valid = [ + { + input: '{"curie":"acme:blog:node:article","id":"123","tag":"tag"}', + output: { + curie: 'acme:blog:node:article', + id: '123', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"123","tag":"tag"}', + output: { + curie: 'acme:blog::article', + id: '123', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"123"}', + output: { + curie: 'acme:blog::article', + id: '123', + }, + }, + { + input: '{"curie":"acme:blog:node:article","id":"2015/12/25/test","tag":"tag"}', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog:node:article","id":"2015/12/25/test"}', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test","tag":"tag"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test:still:the:id"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test:still:the:id', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test:Still_The:id","tag":"2015.Q4"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test:Still_The:id', + tag: '2015.q4', + }, + }, + ]; + + valid.forEach((sample) => { + try { + const ref1 = MessageRef.fromJSON(sample.input); + const ref2 = MessageRef.fromJSON(sample.input); + t.same(`${ref1}`, `${ref2}`); + t.same(ref1.toJSON(), sample.output); + t.true(ref1.getCurie() === SchemaCurie.fromString(sample.output.curie)); + t.true(ref1.getCurie().toString() === sample.output.curie); + t.same(ref1.getId(), sample.output.id); + t.same(ref1.getTag(), sample.output.tag || null); + t.same(ref1.hasTag(), !!sample.output.tag); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('MessageRef fromString tests', (t) => { + const valid = [ + { + input: 'acme:blog:node:article:123#tag', + output: { + curie: 'acme:blog:node:article', + id: '123', + tag: 'tag', + }, + }, + { + input: 'acme:blog::article:123#tag', + output: { + curie: 'acme:blog::article', + id: '123', + tag: 'tag', + }, + }, + { + input: 'acme:blog::article:123', + output: { + curie: 'acme:blog::article', + id: '123', + }, + }, + { + input: 'acme:blog:node:article:2015/12/25/test#tag', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: 'acme:blog:node:article:2015/12/25/test', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + }, + }, + { + input: 'acme:blog::article:2015/12/25/test#tag', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: 'acme:blog::article:2015/12/25/test', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + }, + }, + { + input: 'acme:blog::article:2015/12/25/test:still:the:id', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test:still:the:id', + }, + }, + ]; + + valid.forEach((sample) => { + try { + const ref1 = MessageRef.fromString(sample.input); + const ref2 = MessageRef.fromString(sample.input); + t.same(`${ref1}`, `${ref2}`); + t.same(ref1.toJSON(), sample.output); + t.true(ref1.getCurie() === SchemaCurie.fromString(sample.output.curie)); + t.true(ref1.getCurie().toString() === sample.output.curie); + t.same(ref1.getId(), sample.output.id); + t.same(ref1.getTag(), sample.output.tag || null); + t.same(ref1.hasTag(), !!sample.output.tag); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('MessageRef fromString(invalid) tests', (t) => { + const invalid = [ + 'test::what', + 'test::', + 'test:::', + ':test', + 'john@doe.com', + '#hashtag', + 'http://www.what.com/', + 'test.value:2015/01/01/test:what', + 'cool~topic', + 'some:thin!@##$%$%&^^&**()-=+', + 'some:test%20', + 'ACME:blog:node:article:1:2:3:4#tag', + 'ACME:blog:node:article#tag', + 'ACME:blog:node:', + 'ACME:blog::', + 'ACME:::', + 'acme:blog:node:', + 'acme:blog::', + 'acme:::', + 'acme:::#tag', + ' : ', + ' : : : #tag', + null, + false, + true, + {}, + [], + NaN, + ]; + + invalid.forEach((str) => { + try { + const ref = MessageRef.fromString(str); + t.fail(`MessageRef [${ref}] created with invalid format [${JSON.stringify(str)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/MessageResolver.test.js b/tests/MessageResolver.test.js new file mode 100644 index 0000000..89ea8a6 --- /dev/null +++ b/tests/MessageResolver.test.js @@ -0,0 +1,137 @@ +import test from 'tape'; +import MoreThanOneMessageForMixin from '../src/exceptions/MoreThanOneMessageForMixin'; +import NoMessageForCurie from '../src/exceptions/NoMessageForCurie'; +import NoMessageForMixin from '../src/exceptions/NoMessageForMixin'; +import NoMessageForQName from '../src/exceptions/NoMessageForQName'; +import NoMessageForSchemaId from '../src/exceptions/NoMessageForSchemaId'; +import MessageResolver from '../src/MessageResolver'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaId from '../src/SchemaId'; +import SchemaQName from '../src/SchemaQName'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleMessageV2 from './fixtures/SampleMessageV2'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; +import SampleMixinV1 from './fixtures/SampleMixinV1'; +import SampleMixinV2 from './fixtures/SampleMixinV2'; +import SampleUnusedMixinV1 from './fixtures/SampleUnusedMixinV1'; + +test('MessageResolver all tests', (t) => { + const all = MessageResolver.all(); + + t.same(all.length, 3); + t.true(all.includes(SampleMessageV1)); + t.true(all.includes(SampleMessageV2)); + t.true(all.includes(SampleOtherMessageV1)); + + t.end(); +}); + + +test('MessageResolver resolveId tests', (t) => { + const message = MessageResolver.resolveId(SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.true(message === SampleMessageV1); + + try { + MessageResolver.resolveId(SchemaId.fromString('pbj:gdbots:pbj.tests::invalid-message:1-0-0')); + t.fail('resolved invalid SchemaId'); + } catch (e) { + t.true(e instanceof NoMessageForSchemaId, 'Exception MUST be an instanceOf NoMessageForSchemaId'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver resolveCurie tests', (t) => { + let message = MessageResolver.resolveCurie(SchemaCurie.fromString('gdbots:pbj.tests::sample-message')); + t.true(message === SampleMessageV2); + + message = MessageResolver.resolveCurie(SchemaCurie.fromString('gdbots:pbj.tests::sample-other-message')); + t.true(message === SampleOtherMessageV1); + + try { + MessageResolver.resolveCurie(SchemaCurie.fromString('gdbots:pbj.tests::invalid-message')); + t.fail('resolved invalid SchemaCurie'); + } catch (e) { + t.true(e instanceof NoMessageForCurie, 'Exception MUST be an instanceOf NoMessageForCurie'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver resolveQName tests', (t) => { + let curie = MessageResolver.resolveQName(SchemaQName.fromString('gdbots:sample-message')); + t.true(curie === SchemaCurie.fromString('gdbots:pbj.tests::sample-message')); + + curie = MessageResolver.resolveQName(SchemaQName.fromString('gdbots:sample-other-message')); + t.true(curie === SchemaCurie.fromString('gdbots:pbj.tests::sample-other-message')); + + try { + MessageResolver.resolveQName(SchemaQName.fromString('gdbots:invalid-message')); + t.fail('resolved invalid SchemaQName'); + } catch (e) { + t.true(e instanceof NoMessageForQName, 'Exception MUST be an instanceOf NoMessageForQName'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver findOneUsingMixin tests', (t) => { + const mixin = SampleMixinV2.create(); + const schema = MessageResolver.findOneUsingMixin(mixin); + + t.same(schema.getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + t.true(schema.createMessage() instanceof SampleMessageV2); + + try { + MessageResolver.findOneUsingMixin(SampleUnusedMixinV1.create()); + t.fail('findOneUsingMixin found schema for invalid mixin'); + } catch (e) { + t.true(e instanceof NoMessageForMixin, 'Exception MUST be an instanceOf NoMessageForMixin'); + t.pass(e.message); + } + + try { + MessageResolver.findOneUsingMixin(SampleMixinV1.create()); + t.fail('findOneUsingMixin found one schema for mixin used more than once'); + } catch (e) { + t.true(e instanceof MoreThanOneMessageForMixin, 'Exception MUST be an instanceOf MoreThanOneMessageForMixin'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver findAllUsingMixin tests', (t) => { + let mixin = SampleMixinV1.create(); + let schemas = MessageResolver.findAllUsingMixin(mixin); + + t.same(2, schemas.length); + t.same(schemas[0].getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.same(schemas[1].getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-other-message:1-0-0')); + t.true(schemas[0].createMessage() instanceof SampleMessageV1); + t.true(schemas[1].createMessage() instanceof SampleOtherMessageV1); + + mixin = SampleMixinV2.create(); + schemas = MessageResolver.findAllUsingMixin(mixin); + + t.same(1, schemas.length); + t.same(schemas[0].getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + t.true(schemas[0].createMessage() instanceof SampleMessageV2); + + try { + MessageResolver.findAllUsingMixin(SampleUnusedMixinV1.create()); + t.fail('findOneUsingMixin found schema for invalid mixin'); + } catch (e) { + t.true(e instanceof NoMessageForMixin, 'Exception MUST be an instanceOf NoMessageForMixin'); + t.pass(e.message); + } + + t.end(); +}); diff --git a/tests/Mixin.test.js b/tests/Mixin.test.js new file mode 100644 index 0000000..adb211f --- /dev/null +++ b/tests/Mixin.test.js @@ -0,0 +1,23 @@ +import test from 'tape'; +import Mixin from '../src/Mixin'; +import SchemaId from '../src/SchemaId'; +import SampleMixinV1 from './fixtures/SampleMixinV1'; + +test('Mixin tests', (t) => { + const mixin = SampleMixinV1.create(); + + t.true(mixin instanceof Mixin, 'mixin MUST be an instanceOf Mixin'); + t.true(mixin instanceof SampleMixinV1, 'mixin MUST be an instanceOf SampleMixinV1'); + t.true(mixin === SampleMixinV1.create(), 'SampleMixinV1.create() must return the same instance'); + t.true(mixin.getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-mixin:1-0-0')); + t.same(2, mixin.getFields().length); + + try { + mixin.test = 1; + t.fail('mixin instance is mutable'); + } catch (e) { + t.pass('mixin instance is immutable'); + } + + t.end(); +}); diff --git a/tests/Schema.test.js b/tests/Schema.test.js new file mode 100644 index 0000000..2dcd9cd --- /dev/null +++ b/tests/Schema.test.js @@ -0,0 +1,173 @@ +import test from 'tape'; +import FieldAlreadyDefined from '../src/exceptions/FieldAlreadyDefined'; +import FieldNotDefined from '../src/exceptions/FieldNotDefined'; +import FieldOverrideNotCompatible from '../src/exceptions/FieldOverrideNotCompatible'; +import MixinAlreadyAdded from '../src/exceptions/MixinAlreadyAdded'; +import MixinNotDefined from '../src/exceptions/MixinNotDefined'; +import Fb from '../src/FieldBuilder'; +import Schema from '../src/Schema'; +import SchemaId from '../src/SchemaId'; +import T from '../src/types'; +// import TypeName from '../src/enums/TypeName'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleMixinV1 from './fixtures/SampleMixinV1'; +import SampleMixinV2 from './fixtures/SampleMixinV2'; + +test('Schema tests', (t) => { + const schema = SampleMessageV1.schema(); + const mixinId = SampleMixinV1.create().getId(); + + t.true(schema instanceof Schema, 'schema MUST be an instanceOf Schema'); + t.true(schema.getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.same(`${schema}`, schema.getId().toString()); + t.same(`${schema.getCurie()}`, 'gdbots:pbj.tests::sample-message'); + t.same(`${schema.getCurieMajor()}`, 'gdbots:pbj.tests::sample-message:v1'); + t.same(`${schema.getQName()}`, 'gdbots:sample-message'); + t.same(schema.getFields().length, 10, 'schema should have 10 fields'); + t.same(schema.getClassName(), 'SampleMessageV1'); + t.same(schema.getHandlerMethodName(), 'sampleMessage'); + t.same(schema.getHandlerMethodName(true), 'sampleMessageV1'); + t.true(schema.hasMixin(mixinId.getCurieMajor())); + t.true(schema.getMixin(mixinId.getCurieMajor()), SampleMixinV1.create()); + t.true(schema.hasMixin(mixinId.getCurie().toString())); + t.true(schema.getMixin(mixinId.getCurie().toString()), SampleMixinV1.create()); + t.same(schema.getMixins(), [SampleMixinV1.create()]); + t.same(schema.getMixinIds(), [mixinId.getCurieMajor()]); + t.same(schema.getMixinCuries(), [mixinId.getCurie().toString()]); + t.same(schema.getRequiredFields()[0].getName(), '_schema'); + + // TypeName.getKeys() + ['string'].forEach((typeName) => { + ['single', 'set', 'list', 'map'].forEach((rule) => { + const fieldName = `${typeName.toLowerCase()}_${rule}`; + + if (rule === 'set' && !schema.hasField(fieldName)) { + return; + } + + t.true(schema.hasField(fieldName), `schema MUST have field [${fieldName}]`); + t.same(schema.getField(fieldName).getType().getTypeName().getName(), typeName.toUpperCase()); + }); + }); + + try { + schema.getField('invalid_field'); + t.fail('schema.getField("invalid_field") should have thrown FieldNotDefined'); + } catch (e) { + t.true(e instanceof FieldNotDefined, 'Exception MUST be an instanceOf FieldNotDefined'); + t.pass(e.message); + } + + try { + schema.test = 1; + t.fail('schema instance is mutable'); + } catch (e) { + t.pass('schema instance is immutable'); + } + + t.end(); +}); + + +test('Schema overridable tests', (t) => { + let schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.StringType.create()).withDefault('homer').build()], + [SampleMixinV1.create()], + ); + + const fields = schema.getFields(); + t.same(fields[0].getName(), '_schema'); + t.same(fields[1].getName(), 'mixin_string'); + t.same(fields[2].getName(), 'mixin_int'); + t.same(schema.getField('mixin_string').getDefault(), 'homer'); + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.IntType.create()).build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (type mismatch)'); + } catch (e) { + t.true(e instanceof FieldOverrideNotCompatible, 'Exception MUST be an instanceOf FieldOverrideNotCompatible'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.StringType.create()).required().build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (original optional, override required)'); + } catch (e) { + t.true(e instanceof FieldOverrideNotCompatible, 'Exception MUST be an instanceOf FieldOverrideNotCompatible'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.StringType.create()).asAMap().build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (original single, override map)'); + } catch (e) { + t.true(e instanceof FieldOverrideNotCompatible, 'Exception MUST be an instanceOf FieldOverrideNotCompatible'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_int', T.IntType.create()).build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (not overridable)'); + } catch (e) { + t.true(e instanceof FieldAlreadyDefined, 'Exception MUST be an instanceOf FieldAlreadyDefined'); + t.pass(e.message); + } + + t.end(); +}); + + +test('Schema mixin tests', (t) => { + let schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, [], + [SampleMixinV1.create()], + ); + + try { + schema.getMixin('invalid_mixin'); + t.fail('schema.getMixin("invalid_mixin") should have thrown MixinNotDefined'); + } catch (e) { + t.true(e instanceof MixinNotDefined, 'Exception MUST be an instanceOf MixinNotDefined'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, [], + [ + SampleMixinV1.create(), + SampleMixinV1.create(), + ], + ); + t.fail('schema allowed same mixin twice'); + } catch (e) { + t.true(e instanceof MixinAlreadyAdded, 'Exception MUST be an instanceOf MixinAlreadyAdded'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, [], + [ + SampleMixinV1.create(), + SampleMixinV2.create(), + ], + ); + t.fail('schema allowed same mixin (by curie) twice'); + } catch (e) { + t.true(e instanceof MixinAlreadyAdded, 'Exception MUST be an instanceOf MixinAlreadyAdded'); + t.pass(e.message); + } + + t.end(); +}); + diff --git a/tests/SchemaCurie.test.js b/tests/SchemaCurie.test.js new file mode 100644 index 0000000..f54b488 --- /dev/null +++ b/tests/SchemaCurie.test.js @@ -0,0 +1,91 @@ +import test from 'tape'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaQName from '../src/SchemaQName'; + +test('SchemaCurie tests', (t) => { + const valid = [ + 'acme:blog:node:article', + 'acme:blog::article', + 'acme:blog.v1::article', + 'acme:blog.v1:node:article', + 'acme-widgets:web.v1::article', + 'acme-widgets:web.v1:node:article', + ]; + valid.forEach((str) => { + try { + const [vendor, pkg, category, message] = str.split(':'); + const curie1 = new SchemaCurie(vendor, pkg, category, message); + const curie2 = SchemaCurie.fromString(str); + const qname = SchemaQName.fromString(`${vendor}:${message}`); + t.same(`${curie1}`, `${curie2}`); + t.true(curie1 instanceof SchemaCurie, 'curie1 MUST be an instanceOf SchemaCurie'); + t.true(curie2 instanceof SchemaCurie, 'curie2 MUST be an instanceOf SchemaCurie'); + t.same(curie1.toString(), str); + t.same(curie1.valueOf(), str); + t.same(curie1.toJSON(), str); + t.same(`${curie1}`, str); + t.same(JSON.stringify(curie1), `"${str}"`); + t.same(curie1.getVendor(), vendor); + t.same(curie1.getPackage(), pkg); + t.same(curie1.getCategory(), category || null); + t.same(curie1.getMessage(), message); + t.true(curie1.equals(curie2)); + t.true(curie2.equals(curie1)); + t.true(curie1 === curie2); + t.true(curie1.getQName() === qname); + t.true(curie2.getQName() === qname); + + try { + curie1.test = 1; + t.fail('curie1 instance is mutable'); + } catch (e) { + t.pass('curie1 instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + `acme:blog:node:article${'x'.repeat(124)}`, + 'test::what', + 'test::', + 'test:::', + ':test', + 'john@doe.com', + '#hashtag', + 'http://www.what.com/', + 'test.value:2015/01/01/test:what', + 'cool~topic', + 'some:thin!@##$%$%&^^&**()-=+', + 'some:test%20', + 'ACME:blog:node:article:1:2:3:4#tag', + 'ACME:blog:node:article#tag', + 'ACME:blog:node:', + 'ACME:blog::', + 'ACME:::', + 'acme:blog:node:', + 'acme:blog::', + 'acme:::', + 'acme:::', + ' : ', + ' : : : ', + ':', + null, + false, + true, + {}, + [], + NaN, + ]; + invalid.forEach((str) => { + try { + const curie = SchemaCurie.fromString(str); + t.fail(`SchemaCurie [${curie}] created with invalid format [${JSON.stringify(str)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/SchemaId.test.js b/tests/SchemaId.test.js new file mode 100644 index 0000000..fde0237 --- /dev/null +++ b/tests/SchemaId.test.js @@ -0,0 +1,98 @@ +import test from 'tape'; +import SchemaId from '../src/SchemaId'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaQName from '../src/SchemaQName'; + +test('SchemaId tests', (t) => { + const valid = [ + 'pbj:acme:blog:node:article:1-2-3', + 'pbj:acme:blog::article:1-2-3', + 'pbj:acme:blog.v1::article:1-2-3', + 'pbj:acme:blog.v1:node:article:1-2-3', + 'pbj:acme-widgets:web.v1::article:1-2-3', + 'pbj:acme-widgets:web.v1:node:article:1-2-3', + ]; + valid.forEach((str) => { + try { + const [vendor, pkg, category, message, version] = str.substr(4).split(':'); + const id1 = new SchemaId(vendor, pkg, category, message, version); + const id2 = SchemaId.fromString(str); + const curie = SchemaCurie.fromString(`${vendor}:${pkg}:${category}:${message}`); + const qname = SchemaQName.fromString(`${vendor}:${message}`); + + t.same(`${id1}`, `${id2}`); + t.true(id1 instanceof SchemaId, 'id1 MUST be an instanceOf SchemaId'); + t.true(id2 instanceof SchemaId, 'id2 MUST be an instanceOf SchemaId'); + t.same(id1.toString(), str); + t.same(id1.valueOf(), str); + t.same(id1.toJSON(), str); + t.same(`${id1}`, str); + t.same(JSON.stringify(id1), `"${str}"`); + + t.same(id1.getVendor(), vendor); + t.same(id1.getPackage(), pkg); + t.same(id1.getCategory(), category || null); + t.same(id1.getMessage(), message); + t.true(id1.getCurie() === curie); + t.true(id1.getQName() === qname); + t.same(id1.getVersion().toString(), version); + t.same(id1.getCurieMajor(), `${curie}:v${id1.getVersion().getMajor()}`); + + t.true(id1.equals(id2)); + t.true(id2.equals(id1)); + t.true(id1 === id2); + + try { + id1.test = 1; + t.fail('id1 instance is mutable'); + } catch (e) { + t.pass('id1 instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + `pbj:acme:blog:node:article${'x'.repeat(124)}:1-2-3`, + 'test::what', + 'test::', + 'test:::', + ':test', + 'john@doe.com', + '#hashtag', + 'http://www.what.com/', + 'test.value:2015/01/01/test:what', + 'cool~topic', + 'some:thin!@##$%$%&^^&**()-=+', + 'some:test%20', + 'ACME:blog:node:article:1:2:3:4#tag', + 'ACME:blog:node:article#tag', + 'ACME:blog:node:', + 'ACME:blog::', + 'ACME:::', + 'pbj:acme:blog:node:', + 'pbj:acme:blog::', + 'pbj:acme:::', + 'pbj:acme:::', + ' : ', + ' : : : ', + ':', + null, + false, + true, + {}, + [], + NaN, + ]; + invalid.forEach((str) => { + try { + const id = SchemaId.fromString(str); + t.fail(`SchemaId [${id}] created with invalid format [${JSON.stringify(str)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/SchemaQName.test.js b/tests/SchemaQName.test.js new file mode 100644 index 0000000..143e9d6 --- /dev/null +++ b/tests/SchemaQName.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaId from '../src/SchemaId'; +import SchemaQName from '../src/SchemaQName'; +import InvalidSchemaQName from '../src/exceptions/InvalidSchemaQName'; + +test('SchemaQName tests', (t) => { + const valid = ['acme:article', 'youtube:video', 'acme-widgets:widget-thing']; + valid.forEach((qname) => { + try { + const [vendor, message] = qname.split(':'); + const schemaQName = SchemaQName.fromString(qname); + const schemaQName2 = new SchemaQName(vendor, message); + t.same(`${schemaQName}`, `${schemaQName2}`); + t.true(schemaQName instanceof SchemaQName, 'schemaQName MUST be an instanceOf SchemaQName'); + t.same(schemaQName.toString(), qname); + t.same(schemaQName.valueOf(), qname); + t.same(schemaQName.toJSON(), qname); + t.same(`${schemaQName}`, qname); + t.same(JSON.stringify(schemaQName), `"${qname}"`); + t.same(schemaQName.getVendor(), vendor); + t.same(schemaQName.getMessage(), message); + + try { + schemaQName.test = 1; + t.fail('schemaQName instance is mutable'); + } catch (e) { + t.pass('schemaQName instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + 'Not A qname', + 'acme.widgets:widget', + ' acme:widget ', + ':', + ' : ', + ' ', + 1, + 0, + '', + null, + false, + true, + {}, + [], + NaN, + ]; + invalid.forEach((qname) => { + try { + const schemaQName = SchemaQName.fromString(qname); + t.fail(`SchemaQName [${schemaQName}] created with invalid value [${JSON.stringify(qname)}].`); + } catch (e) { + t.true(e instanceof InvalidSchemaQName, 'Exception MUST be an instanceOf InvalidSchemaQName'); + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('SchemaQName instance tests', (t) => { + const instance1 = SchemaQName.fromString('acme:article'); + const instance2 = SchemaQName.fromString('acme:article'); + t.same(instance1, instance2); + + try { + instance1.test = 1; + t.fail('SchemaQName instance1 is mutable'); + } catch (e) { + t.pass('SchemaQName instance1 is immutable'); + } + + try { + instance2.test = 1; + t.fail('SchemaQName instance2 is mutable'); + } catch (e) { + t.pass('SchemaQName instance2 is immutable'); + } + + t.end(); +}); + + +test('SchemaQName fromCurie tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog:node:article'); + const qname = SchemaQName.fromCurie(curie); + t.same(qname.toString(), 'acme:article'); + t.end(); +}); + + +test('SchemaQName fromId tests', (t) => { + const qname = SchemaQName.fromId(SchemaId.fromString('pbj:acme:blog:node:article:1-2-3')); + t.same(qname.toString(), 'acme:article'); + t.end(); +}); diff --git a/tests/SchemaVersion.test.js b/tests/SchemaVersion.test.js new file mode 100644 index 0000000..f6e0ebd --- /dev/null +++ b/tests/SchemaVersion.test.js @@ -0,0 +1,48 @@ +import test from 'tape'; +import SchemaVersion from '../src/SchemaVersion'; +import InvalidSchemaVersion from '../src/exceptions/InvalidSchemaVersion'; + +test('SchemaVersion tests', (t) => { + const valid = ['1-0-0', '1-1-1', '2-20-0', '300-4000-5000']; + valid.forEach((version) => { + try { + const [major, minor, patch] = version.split('-').map(Number); + const schemaVersion = SchemaVersion.fromString(version); + const schemaVersion2 = new SchemaVersion(major, minor, patch); + t.same(`${schemaVersion}`, `${schemaVersion2}`); + t.true(schemaVersion instanceof SchemaVersion, 'schemaVersion MUST be an instanceOf SchemaVersion'); + t.same(schemaVersion.toString(), version); + t.same(schemaVersion.valueOf(), version); + t.same(schemaVersion.toJSON(), version); + t.same(`${schemaVersion}`, version); + t.same(JSON.stringify(schemaVersion), `"${version}"`); + t.same(schemaVersion.getMajor(), major); + t.same(schemaVersion.getMinor(), minor); + t.same(schemaVersion.getPatch(), patch); + + try { + schemaVersion.test = 1; + t.fail('schemaVersion instance is mutable'); + } catch (e) { + t.pass('schemaVersion instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + 'Not A version', '1-0-0.1', '1.0.0', '1-1-1-dev', ' 1-0-0 ', 1, 0, '', null, false, true, {}, [], NaN, + ]; + invalid.forEach((version) => { + try { + const schemaVersion = SchemaVersion.fromString(version); + t.fail(`SchemaVersion [${schemaVersion}] created with invalid version [${JSON.stringify(version)}].`); + } catch (e) { + t.true(e instanceof InvalidSchemaVersion, 'Exception MUST be an instanceOf InvalidSchemaVersion'); + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/add-types-test.js b/tests/add-types-test.js deleted file mode 100644 index 4a89ea4..0000000 --- a/tests/add-types-test.js +++ /dev/null @@ -1,239 +0,0 @@ -'use strict'; - -import IntEnum from './fixtures/enum/int-enum'; -import Priority from './fixtures/enum/priority'; -import Provider from './fixtures/enum/provider'; -import StringEnum from './fixtures/enum/string-enum'; -import MapsMessage from './fixtures/maps-message'; -import EmailMessage from './fixtures/email-message'; -import NestedMessage from './fixtures/nested-message'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import BigNumber from 'gdbots/pbj/well-known/big-number'; -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import DynamicField from 'gdbots/pbj/well-known/dynamic-field'; -import Microtime from 'gdbots/pbj/well-known/microtime'; -import TimeUuidIdentifier from 'gdbots/pbj/well-known/time-uuid-identifier'; -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; -import MessageRef from 'gdbots/pbj/message-ref'; - -describe('add-types-test', function() { - it('should add invalid types', function(done) { - let message = MapsMessage.create(); - - ArrayUtils.each(getInvalidTypeValues(), function(v, k) { - let thrown = false; - - try { - if (Array.isArray(v)) { - message.addToMap(k, 'test1', v[0]); - message.addToMap(k, 'test2', v[1]); - } else { - message.addToMap(k, 'test1', v); - } - } catch (e) { - thrown = true; - } - - if (!thrown) { - if (Array.isArray(v)) { - console.log('[' + k + '] accepted an invalid [' + StringUtils.varToString(v[0]) + '] value.'); - console.log('[' + k + '] accepted an invalid [' + StringUtils.varToString(v[1]) + '] value.'); - } else { - console.log('[' + k + '] accepted an invalid [' + StringUtils.varToString(v) + '] value.'); - } - } - }); - - done(); - }); - - it('should add invalid type to map', function(done) { - let shouldWork = MapsMessage.create(); - let shouldFail = Object.assign({}, shouldWork); - - /* - * some int types won't fail because they're all ints of course, just different ranges. - * e.g. an Int is also all other unsigned ints (except BigInt but that's a class so we're fine) - */ - let allInts = [ - 'TinyInt', - 'SmallInt', - 'MediumInt', - 'Int', - 'SignedTinyInt', - 'SignedSmallInt', - 'SignedMediumInt', - 'SignedInt', - 'Timestamp' - ]; - - let allStrings = ['Binary', 'Blob', 'MediumBlob', 'MediumText', 'String', 'Text']; - - ArrayUtils.each(shouldWork.constructor.getAllTypes(), function(type) { - ArrayUtils.each(getTypeValues(), function(v, k) { - let thrown = false; - - if (type == k) { - if (Array.isArray(v)) { - shouldWork.addToMap(type, 'test1', v[0]); - shouldWork.addToMap(type, 'test2', v[1]); - } else { - shouldWork.addToMap(type, 'test1', v); - } - - return; - } - - try { - if (Array.isArray(v)) { - shouldFail.addToMap(type, 'test1', v[0]); - shouldFail.addToMap(type, 'test2', v[1]); - } else { - shouldFail.addToMap(type, 'test1', v); - } - - switch (type) { - case 'Binary': - case 'Blob': - case 'MediumBlob': - case 'MediumText': - case 'String': - case 'Text': - case 'Timestamp': - if (allStrings.indexOf(k) >= 0) { - return; - } - break; - - case 'Decimal': - if (k === 'Float') { - return; - } - break; - - case 'Date': - if (k === 'DateTime') { - return; - } - break; - - case 'DateTime': - if (k === 'Date') { - return; - } - break; - - case 'Float': - if (k === 'Decimal') { - return; - } - break; - - case 'Identifier': - if (['TimeUuid', 'Uuid'].indexOf(k) >= 0) { - return; - } - break; - - case 'Uuid': - if (['Identifier', 'TimeUuid'].indexOf(k) >= 0) { - return; - } - break; - - default: - // do nothing - } - - if (type.name === 'IntType' && allInts.indexOf(k) >= 0) { - return; - } - } catch (e) { - thrown = true; - } - - if (!thrown) { - if (Array.isArray(v)) { - console.log('[' + type + '] accepted an invalid/mismatched [' + StringUtils.varToString(v[0]) + '] value.'); - console.log('[' + type + '] accepted an invalid/mismatched [' + StringUtils.varToString(v[1]) + '] value.'); - } else { - console.log('[' + type + '] accepted an invalid/mismatched [' + StringUtils.varToString(v) + '] value.'); - } - } - }); - }); - - done(); - }); -}); - -function getTypeValues() { - return { - 'BigInt': [new BigNumber(0), new BigNumber('18446744073709551615')], - 'Binary': 'aG9tZXIgc2ltcHNvbg==', - 'Blob': 'aG9tZXIgc2ltcHNvbg==', - 'Boolean': [false, true], - 'Date': new Date(), - 'DateTime': new Date(), - 'Decimal': 3.14, - 'DynamicField': DynamicField.createIntVal('int_val', 1), - 'Float': 13213.032468, - 'GeoPoint': new GeoPoint(0.5, 102.0), - 'IntEnum': IntEnum.UNKNOWN.getValue(), - 'Int': [0, 4294967295], - 'MediumInt': [0, 16777215], - 'MediumBlob': 'aG9tZXIgc2ltcHNvbg==', - 'MediumText': 'medium text', - 'Message': NestedMessage.create(), - 'MessageRef': new MessageRef(NestedMessage.schema().getCurie(), UuidIdentifier.generate()), - 'Microtime': Microtime.create(), - 'SignedBigInt': [new BigNumber('-9223372036854775808'), new BigNumber('9223372036854775807')], - 'SignedMediumInt': [-8388608, 8388607], - 'SignedSmallInt': [-32768, 32767], - 'SignedTinyInt': [-128, 127], - 'SmallInt': [0, 65535], - 'StringEnum': StringEnum.UNKNOWN.getValue(), - 'String': 'string', - 'Text': 'text', - 'TimeUuid': TimeUuidIdentifier.generate(), - 'Timestamp': Math.floor(new Date().getTime() / 1000), - 'TinyInt': [0, 255], - 'Uuid': UuidIdentifier.generate(), - }; -} - -function getInvalidTypeValues() { - return { - 'BigInt': [new BigNumber(-1), new BigNumber('18446744073709551616')], - 'Binary': false, - 'Blob': false, - 'Boolean': 'not_a_bool', - 'Date': 'not_a_date', - 'DateTime': 'not_a_date', - 'Decimal': 1, - 'DynamicField': 'not_a_dynamic_field', - 'Float': 1, - 'GeoPoint': 'not_a_geo_point', - 'IntEnum': Priority.NORMAL.getValue(), // not the correct enum - 'Int': [-1, 4294967296], - 'MediumInt': [-1, 16777216], - 'MediumBlob': false, - 'MediumText': false, - 'Message': EmailMessage.create(), // not the correct message - 'MessageRef': 'not_a_message_ref', - 'Microtime': new Date().getTime(), - 'SignedBigInt': [new BigNumber('-9223372036854775809'), new BigNumber('9223372036854775808')], - 'SignedMediumInt': [-8388609, 8388608], - 'SignedSmallInt': [-32769, 32768], - 'SignedTinyInt': [-129, 128], - 'SmallInt': [-1, 65536], - 'StringEnum': Provider.AOL.getValue(), // not the correct enum - 'String': false, - 'Text': false, - 'TimeUuid': 'not_a_time_uuid', - 'Timestamp': 'not_a_timestamp', - 'TinyInt': [-1, 256], - 'Uuid': 'not_a_uuid', - }; -} diff --git a/tests/babel-register.js b/tests/babel-register.js new file mode 100644 index 0000000..06cc9b0 --- /dev/null +++ b/tests/babel-register.js @@ -0,0 +1,3 @@ +require('babel-register')({ + ignore: /node_modules\/(?!@gdbots|lodash-es)/, +}); diff --git a/tests/bootstrap.js b/tests/bootstrap.js deleted file mode 100644 index 025dfeb..0000000 --- a/tests/bootstrap.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -// disable babel cache. -process.env.BABEL_DISABLE_CACHE = 1; - -require('babel-register')({ - ignore: /node_modules(?![\/]@gdbots)/, - - plugins: [ - ['module-alias', [ - { src: './src', expose: 'gdbots/pbj' }, - { src: 'npm:@gdbots/common/src', expose: 'gdbots/common' } - ]] - ] -}); - -require('chai').should(); -global.expect = require('chai'); diff --git a/tests/fixtures/SampleMessageV1.js b/tests/fixtures/SampleMessageV1.js new file mode 100644 index 0000000..235a466 --- /dev/null +++ b/tests/fixtures/SampleMessageV1.js @@ -0,0 +1,53 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Message from '../../src/Message'; +import MessageResolver from '../../src/MessageResolver'; +import Schema from '../../src/Schema'; +import T from '../../src/types'; +import SampleMixinV1 from './SampleMixinV1'; +import SampleTraitV1 from './SampleTraitV1'; + +export default class SampleMessageV1 extends Message { + /** + * @private + * + * @returns {Schema} + */ + static defineSchema() { + return new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [ + Fb.create('string_single', T.StringType.create()) + .build(), + Fb.create('string_set', T.StringType.create()) + .asASet() + .build(), + Fb.create('string_list', T.StringType.create()) + .asAList() + .build(), + Fb.create('string_map', T.StringType.create()) + .asAMap() + .build(), + + Fb.create('message_single', T.MessageType.create()) + .anyOfCuries(['gdbots:pbj.tests::sample-other-message']) + .build(), + Fb.create('message_list', T.MessageType.create()) + .asAList() + .anyOfCuries(['gdbots:pbj.tests::sample-other-message']) + .build(), + Fb.create('message_map', T.MessageType.create()) + .asAMap() + .anyOfCuries(['gdbots:pbj.tests::sample-other-message']) + .build(), + ], + [ + SampleMixinV1.create(), + ], + ); + } +} + +SampleTraitV1(SampleMessageV1); +MessageResolver.register('gdbots:pbj.tests::sample-message:v1', SampleMessageV1); +Object.freeze(SampleMessageV1); +Object.freeze(SampleMessageV1.prototype); diff --git a/tests/fixtures/SampleMessageV2.js b/tests/fixtures/SampleMessageV2.js new file mode 100644 index 0000000..eb97516 --- /dev/null +++ b/tests/fixtures/SampleMessageV2.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Message from '../../src/Message'; +import MessageResolver from '../../src/MessageResolver'; +import Schema from '../../src/Schema'; +import T from '../../src/types'; +import SampleMixinV2 from './SampleMixinV2'; +import SampleTraitV2 from './SampleTraitV2'; + +export default class SampleMessageV2 extends Message { + /** + * @private + * + * @returns {Schema} + */ + static defineSchema() { + return new Schema('pbj:gdbots:pbj.tests::sample-message:2-0-0', SampleMessageV2, + [ + Fb.create('string_single', T.StringType.create()) + .build(), + Fb.create('string_set', T.StringType.create()) + .asASet() + .build(), + Fb.create('string_list', T.StringType.create()) + .asAList() + .build(), + Fb.create('string_map', T.StringType.create()) + .asAMap() + .build(), + ], + [ + SampleMixinV2.create(), + ], + ); + } +} + +SampleTraitV2(SampleMessageV2); +MessageResolver.register('gdbots:pbj.tests::sample-message', SampleMessageV2); +Object.freeze(SampleMessageV2); +Object.freeze(SampleMessageV2.prototype); diff --git a/tests/fixtures/SampleMixinV1.js b/tests/fixtures/SampleMixinV1.js new file mode 100644 index 0000000..0a75641 --- /dev/null +++ b/tests/fixtures/SampleMixinV1.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Mixin from '../../src/Mixin'; +import SchemaId from '../../src/SchemaId'; +import T from '../../src/types'; + +export default class SampleMixinV1 extends Mixin { + /** + * @returns {SchemaId} + */ + getId() { + return SchemaId.fromString('pbj:gdbots:pbj.tests::sample-mixin:1-0-0'); + } + + /** + * @returns {Field[]} + */ + getFields() { + return [ + Fb.create('mixin_string', T.StringType.create()) + .overridable(true) + .build(), + Fb.create('mixin_int', T.IntType.create()) + .required() + .build(), + ]; + } +} diff --git a/tests/fixtures/SampleMixinV2.js b/tests/fixtures/SampleMixinV2.js new file mode 100644 index 0000000..22af59d --- /dev/null +++ b/tests/fixtures/SampleMixinV2.js @@ -0,0 +1,30 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Mixin from '../../src/Mixin'; +import SchemaId from '../../src/SchemaId'; +import T from '../../src/types'; + +export default class SampleMixinV2 extends Mixin { + /** + * @returns {SchemaId} + */ + getId() { + return SchemaId.fromString('pbj:gdbots:pbj.tests::sample-mixin:2-0-0'); + } + + /** + * @returns {Field[]} + */ + getFields() { + return [ + Fb.create('mixin_string', T.StringType.create()) + .overridable(true) + .build(), + Fb.create('mixin_int', T.IntType.create()) + .required() + .build(), + Fb.create('mixin_date', T.DateType.create()) + .build(), + ]; + } +} diff --git a/tests/fixtures/SampleOtherMessageV1.js b/tests/fixtures/SampleOtherMessageV1.js new file mode 100644 index 0000000..3411fbb --- /dev/null +++ b/tests/fixtures/SampleOtherMessageV1.js @@ -0,0 +1,32 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Message from '../../src/Message'; +import MessageResolver from '../../src/MessageResolver'; +import Schema from '../../src/Schema'; +import T from '../../src/types'; +import SampleMixinV1 from './SampleMixinV1'; +import SampleTraitV1 from './SampleTraitV1'; + +export default class SampleOtherMessageV1 extends Message { + /** + * @private + * + * @returns {Schema} + */ + static defineSchema() { + return new Schema('pbj:gdbots:pbj.tests::sample-other-message:1-0-0', SampleOtherMessageV1, + [ + Fb.create('test', T.StringType.create()) + .build(), + ], + [ + SampleMixinV1.create(), + ], + ); + } +} + +SampleTraitV1(SampleOtherMessageV1); +MessageResolver.register('gdbots:pbj.tests::sample-other-message', SampleOtherMessageV1); +Object.freeze(SampleOtherMessageV1); +Object.freeze(SampleOtherMessageV1.prototype); diff --git a/tests/fixtures/SampleTraitV1.js b/tests/fixtures/SampleTraitV1.js new file mode 100644 index 0000000..65fe8ba --- /dev/null +++ b/tests/fixtures/SampleTraitV1.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this, no-param-reassign */ +import MessageRef from '../../src/MessageRef'; + +export default function SampleTraitV1(m) { + return Object.assign(m.prototype, { + /** + * @param {?string} tag + * + * @returns {MessageRef} + */ + generateMessageRef(tag = null) { + return new MessageRef(this.schema().getCurie(), this.get('string_single'), tag); + }, + + /** + * @returns {Object} + */ + getUriTemplateVars() { + return { + string_single: this.get('string_single'), + }; + }, + }); +} diff --git a/tests/fixtures/SampleTraitV2.js b/tests/fixtures/SampleTraitV2.js new file mode 100644 index 0000000..13aa7d3 --- /dev/null +++ b/tests/fixtures/SampleTraitV2.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this, no-param-reassign */ +import MessageRef from '../../src/MessageRef'; + +export default function SampleTraitV2(m) { + return Object.assign(m.prototype, { + /** + * @param {?string} tag + * + * @returns {MessageRef} + */ + generateMessageRef(tag = null) { + return new MessageRef(this.schema().getCurie(), this.get('string_single'), tag); + }, + + /** + * @returns {Object} + */ + getUriTemplateVars() { + return { + string_single: this.get('string_single'), + }; + }, + }); +} diff --git a/tests/fixtures/SampleUnusedMixinV1.js b/tests/fixtures/SampleUnusedMixinV1.js new file mode 100644 index 0000000..e1b722d --- /dev/null +++ b/tests/fixtures/SampleUnusedMixinV1.js @@ -0,0 +1,12 @@ +/* eslint-disable class-methods-use-this */ +import Mixin from '../../src/Mixin'; +import SchemaId from '../../src/SchemaId'; + +export default class SampleUnusedMixinV1 extends Mixin { + /** + * @returns {SchemaId} + */ + getId() { + return SchemaId.fromString('pbj:gdbots:pbj.tests::sample-unused-mixin:1-0-0'); + } +} diff --git a/tests/fixtures/email-message.js b/tests/fixtures/email-message.js deleted file mode 100644 index db44f8c..0000000 --- a/tests/fixtures/email-message.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict'; - -import Priority from './enum/priority'; -import Provider from './enum/provider'; -import NestedMessage from './nested-message'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Format from 'gdbots/pbj/enum/format'; -import BooleanType from 'gdbots/pbj/type/boolean-type'; -import DateTimeType from 'gdbots/pbj/type/date-time-type'; -import DynamicFieldType from 'gdbots/pbj/type/dynamic-field-type'; -import IntEnumType from 'gdbots/pbj/type/int-enum-type'; -import MessageType from 'gdbots/pbj/type/message-type'; -import MicrotimeType from 'gdbots/pbj/type/microtime-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import StringEnumType from 'gdbots/pbj/type/string-enum-type'; -import TimeUuidType from 'gdbots/pbj/type/time-uuid-type'; -import Fb from 'gdbots/pbj/field-builder'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import Message from 'gdbots/pbj/message'; -import Schema from 'gdbots/pbj/schema'; - -export default class EmailMessage extends SystemUtils.mixinClass(Message) -{ - /** - * @return Schema - */ - static defineSchema() { - let schema = new Schema('pbj:gdbots:tests.pbj:fixtures:email-message:1-0-0', this.name, - [ - Fb.create('id', TimeUuidType.create()) - .required() - .build(), - Fb.create('from_name', StringType.create()) - .build(), - Fb.create('from_email', StringType.create()) - .required() - .format('email') - .build(), - Fb.create('subject', StringType.create()) - .withDefault(function (message = null) { - if (!message) { - return null; - } - return message.get('labels', []).join(',') + ' test'; - }) - .build(), - Fb.create('body', StringType.create()).build(), - Fb.create('priority', IntEnumType.create()) - .required() - .instance(Priority) - .withDefault(Priority.NORMAL) - .build(), - Fb.create('sent', BooleanType.create()).build(), - Fb.create('date_sent', DateTimeType.create()).build(), - Fb.create('microtime_sent', MicrotimeType.create()).build(), - Fb.create('provider', StringEnumType.create()) - .instance(Provider) - .withDefault(Provider.GMAIL) - .build(), - Fb.create('labels', StringType.create()) - .format(Format.HASHTAG.getValue()) - .asASet() - .build(), - Fb.create('nested', MessageType.create()) - .instance(NestedMessage) - .build(), - Fb.create('enum_in_set', StringEnumType.create()) - .instance(Provider) - .asASet() - .build(), - Fb.create('enum_in_list', StringEnumType.create()) - .instance(Provider) - .asAList() - .build(), - Fb.create('any_of_message', MessageType.create()) - .instance(Message) - .asAList() - .build(), - Fb.create('dynamic_fields', DynamicFieldType.create()) - .asAList() - .build(), - ] - ); - - MessageResolver.registerSchema(this, schema); - - return schema; - } - - /** - * {@inheritdoc} - */ - generateMessageRef(tag = null) { - return new MessageRef(this.constructor.schema().getCurie(), this.get('id'), tag); - } - - /** - * {@inheritdoc} - */ - getUriTemplateVars() { - return { - 'id': this.get('id').toString() - }; - } -} diff --git a/tests/fixtures/enum/int-enum.js b/tests/fixtures/enum/int-enum.js deleted file mode 100644 index d8dcbd7..0000000 --- a/tests/fixtures/enum/int-enum.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static IntEnum UNKNOWN() - * @method static IntEnum A_INT() - */ -export default class IntEnum extends SystemUtils.mixinClass(Enum) {} - -IntEnum.initEnum({ - UNKNOWN: 0, - A_INT: 1 -}); diff --git a/tests/fixtures/enum/priority.js b/tests/fixtures/enum/priority.js deleted file mode 100644 index d1d63e0..0000000 --- a/tests/fixtures/enum/priority.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static Priority NORMAL() - * @method static Priority HIGH() - * @method static Priority LOW() - */ -export default class Priority extends SystemUtils.mixinClass(Enum) {} - -Priority.initEnum({ - NORMAL: 1, - HIGH: 2, - LOW: 3 -}); diff --git a/tests/fixtures/enum/provider.js b/tests/fixtures/enum/provider.js deleted file mode 100644 index 40904c0..0000000 --- a/tests/fixtures/enum/provider.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static Provider AOL() - * @method static Provider GMAIL() - * @method static Provider HOTMAIL() - */ -export default class Provider extends SystemUtils.mixinClass(Enum) {} - -Provider.initEnum({ - AOL: 'aol', - GMAIL: 'gmail', - HOTMAIL: 'hotmail' -}); diff --git a/tests/fixtures/enum/string-enum.js b/tests/fixtures/enum/string-enum.js deleted file mode 100644 index 30a6dfd..0000000 --- a/tests/fixtures/enum/string-enum.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static StringEnum UNKNOWN() - * @method static StringEnum A_STRING() - */ -export default class StringEnum extends SystemUtils.mixinClass(Enum) {} - -StringEnum.initEnum({ - UNKNOWN: 'unknown', - A_STRING: 'string' -}); diff --git a/tests/fixtures/enums/SampleIntEnum.js b/tests/fixtures/enums/SampleIntEnum.js new file mode 100644 index 0000000..e63a885 --- /dev/null +++ b/tests/fixtures/enums/SampleIntEnum.js @@ -0,0 +1,10 @@ +import Enum from '@gdbots/common/Enum'; + +export default class SampleIntEnum extends Enum { +} + +SampleIntEnum.configure({ + UNKNOWN: 0, + ENUM1: 1, + ENUM2: 2, +}, 'gdbots:pbj.tests:sample-int-enum'); diff --git a/tests/fixtures/enums/SampleStringEnum.js b/tests/fixtures/enums/SampleStringEnum.js new file mode 100644 index 0000000..7321e98 --- /dev/null +++ b/tests/fixtures/enums/SampleStringEnum.js @@ -0,0 +1,10 @@ +import Enum from '@gdbots/common/Enum'; + +export default class SampleStringEnum extends Enum { +} + +SampleStringEnum.configure({ + UNKNOWN: 'unknown', + ENUM1: 'val1', + ENUM2: 'val2', +}, 'gdbots:pbj.tests:sample-string-enum'); diff --git a/tests/fixtures/maps-message.js b/tests/fixtures/maps-message.js deleted file mode 100644 index f348a62..0000000 --- a/tests/fixtures/maps-message.js +++ /dev/null @@ -1,136 +0,0 @@ -'use strict'; - -import NestedMessage from './nested-message'; -import StringEnum from './enum/string-enum'; -import IntEnum from './enum/int-enum'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import TimeUuidIdentifier from 'gdbots/pbj/well-known/time-uuid-identifier'; -import Fb from 'gdbots/pbj/field-builder'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import Message from 'gdbots/pbj/message'; -import Schema from 'gdbots/pbj/schema'; -import BigIntType from 'gdbots/pbj/type/big-int-type'; -import BinaryType from 'gdbots/pbj/type/binary-type'; -import BlobType from 'gdbots/pbj/type/blob-type'; -import BooleanType from 'gdbots/pbj/type/boolean-type'; -import DateTimeType from 'gdbots/pbj/type/date-time-type'; -import DateType from 'gdbots/pbj/type/date-type'; -import DecimalType from 'gdbots/pbj/type/decimal-type'; -import FloatType from 'gdbots/pbj/type/float-type'; -import GeoPointType from 'gdbots/pbj/type/geo-point-type'; -import IdentifierType from 'gdbots/pbj/type/identifier-type'; -import IntEnumType from 'gdbots/pbj/type/int-enum-type'; -import IntType from 'gdbots/pbj/type/int-type'; -import MediumBlobType from 'gdbots/pbj/type/medium-blob-type'; -import MediumIntType from 'gdbots/pbj/type/medium-int-type'; -import MediumTextType from 'gdbots/pbj/type/medium-text-type'; -import MessageRefType from 'gdbots/pbj/type/message-ref-type'; -import MessageType from 'gdbots/pbj/type/message-type'; -import MicrotimeType from 'gdbots/pbj/type/microtime-type'; -import SignedBigIntType from 'gdbots/pbj/type/signed-big-int-type'; -import SignedIntType from 'gdbots/pbj/type/signed-int-type'; -import SignedMediumIntType from 'gdbots/pbj/type/signed-medium-int-type'; -import SignedSmallIntType from 'gdbots/pbj/type/signed-small-int-type'; -import SignedTinyIntType from 'gdbots/pbj/type/signed-tiny-int-type'; -import SmallIntType from 'gdbots/pbj/type/small-int-type'; -import StringEnumType from 'gdbots/pbj/type/string-enum-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import TextType from 'gdbots/pbj/type/text-type'; -import TimeUuidType from 'gdbots/pbj/type/time-uuid-type'; -import TimestampType from 'gdbots/pbj/type/timestamp-type'; -import TinyIntType from 'gdbots/pbj/type/tiny-int-type'; -import UuidType from 'gdbots/pbj/type/uuid-type'; - -export default class MapsMessage extends SystemUtils.mixinClass(Message) -{ - /** - * @return array - */ - static getAllTypes() { - return [ - BigIntType, BinaryType, BlobType, BooleanType, DateTimeType, DateType, DecimalType, - FloatType, GeoPointType, IdentifierType, IntEnumType, IntType, MediumBlobType, MediumIntType, - MediumTextType, MessageRefType, MessageType, MicrotimeType, SignedBigIntType, SignedIntType, - SignedMediumIntType, SignedSmallIntType, SignedTinyIntType, SmallIntType, StringEnumType, - StringType, TextType, TimeUuidType, TimestampType, TinyIntType, UuidType - ]; - } - - /** - * @return Schema - */ - static defineSchema() { - let fields = []; - - /** @var Type type */ - for (let type of this.getAllTypes()) { - let typeName = StringUtils.toSnakeCase(type.name.substring(0, type.name.length-4)).toLowerCase(); - let field = null; - - switch (typeName) { - case 'identifier': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(TimeUuidIdentifier) - .build(); - - break; - - case 'int_enum': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(IntEnum) - .build(); - - break; - - case 'string_enum': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(StringEnum) - .build(); - - break; - - case 'message': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(NestedMessage) - .build(); - - break; - - default: - field = Fb.create(typeName, type.create()) - .asAMap() - .build(); - } - - if (field) { - fields.push(field); - } - } - - let schema = new Schema('pbj:gdbots:tests.pbj:fixtures:maps-message:1-0-0', this.name, fields); - - MessageResolver.registerSchema(this, schema); - - return schema; - } - - /** - * {@inheritdoc} - */ - generateMessageRef(tag = null) { - return new MessageRef(this.constructor.schema().getCurie(), null, tag); - } - - /** - * {@inheritdoc} - */ - getUriTemplateVars() { - return {}; - } -} diff --git a/tests/fixtures/nested-message.js b/tests/fixtures/nested-message.js deleted file mode 100644 index 852c441..0000000 --- a/tests/fixtures/nested-message.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GeoPointType from 'gdbots/pbj/type/geo-point-type'; -import IntType from 'gdbots/pbj/type/int-type'; -import MessageRefType from 'gdbots/pbj/type/message-ref-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import Fb from 'gdbots/pbj/field-builder'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import Message from 'gdbots/pbj/message'; -import Schema from 'gdbots/pbj/schema'; - -export default class NestedMessage extends SystemUtils.mixinClass(Message) -{ - /** - * @return Schema - */ - static defineSchema() { - let schema = new Schema('pbj:gdbots:tests.pbj:fixtures:nested-message:1-0-0', this.name, - [ - Fb.create('test1', StringType.create()).build(), - Fb.create('test2', IntType.create()).asASet().build(), - Fb.create('location', GeoPointType.create()).build(), - Fb.create('refs', MessageRefType.create()).asASet().build() - ] - ); - - MessageResolver.registerSchema(this, schema); - - return schema; - } - - /** - * {@inheritdoc} - */ - generateMessageRef(tag = null) { - return new MessageRef(this.constructor.schema().getCurie(), null, tag); - } - - /** - * {@inheritdoc} - */ - getUriTemplateVars() { - return {}; - } -} diff --git a/tests/fixtures/well-known/SampleDatedSlugIdentifier.js b/tests/fixtures/well-known/SampleDatedSlugIdentifier.js new file mode 100644 index 0000000..6516af3 --- /dev/null +++ b/tests/fixtures/well-known/SampleDatedSlugIdentifier.js @@ -0,0 +1,4 @@ +import DatedSlugIdentifier from '../../../src/well-known/DatedSlugIdentifier'; + +export default class SampleDatedSlugIdentifier extends DatedSlugIdentifier { +} diff --git a/tests/fixtures/well-known/SampleSlugIdentifier.js b/tests/fixtures/well-known/SampleSlugIdentifier.js new file mode 100644 index 0000000..d6bb0d5 --- /dev/null +++ b/tests/fixtures/well-known/SampleSlugIdentifier.js @@ -0,0 +1,4 @@ +import SlugIdentifier from '../../../src/well-known/SlugIdentifier'; + +export default class SampleSlugIdentifier extends SlugIdentifier { +} diff --git a/tests/fixtures/well-known/SampleTimeUuidIdentifier.js b/tests/fixtures/well-known/SampleTimeUuidIdentifier.js new file mode 100644 index 0000000..8619a38 --- /dev/null +++ b/tests/fixtures/well-known/SampleTimeUuidIdentifier.js @@ -0,0 +1,4 @@ +import TimeUuidIdentifier from '../../../src/well-known/TimeUuidIdentifier'; + +export default class SampleTimeUuidIdentifier extends TimeUuidIdentifier { +} diff --git a/tests/fixtures/well-known/SampleUuidIdentifier.js b/tests/fixtures/well-known/SampleUuidIdentifier.js new file mode 100644 index 0000000..bb46132 --- /dev/null +++ b/tests/fixtures/well-known/SampleUuidIdentifier.js @@ -0,0 +1,4 @@ +import UuidIdentifier from '../../../src/well-known/UuidIdentifier'; + +export default class SampleUuidIdentifier extends UuidIdentifier { +} diff --git a/tests/index.test.js b/tests/index.test.js new file mode 100644 index 0000000..de361b2 --- /dev/null +++ b/tests/index.test.js @@ -0,0 +1,5 @@ +import test from 'tape'; + +test('index tests', (t) => { + t.end(); +}); diff --git a/tests/maps-test.js b/tests/maps-test.js deleted file mode 100644 index f16445e..0000000 --- a/tests/maps-test.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -import MapsMessage from './fixtures/maps-message'; - -describe('maps-test', function() { - it('should validate string map', function(done) { - let message = MapsMessage.create() - .addToMap('string', 'test1', '123') - .addToMap('string', 'test2', '456'); - - message.get('string').should.eql({ - test1: '123', - test2: '456' - }); - - message.removeFromMap('string', 'test2'); - - message.get('string').should.eql({ - test1: '123' - }); - - message - .addToMap('string', 'test2', '456') - .addToMap('string', 'test3', '789'); - - message.get('string').should.eql({ - test1: '123', - test2: '456', - test3: '789' - }); - - done(); - }); -}); diff --git a/tests/message-test.js b/tests/message-test.js deleted file mode 100644 index d6a9fd0..0000000 --- a/tests/message-test.js +++ /dev/null @@ -1,339 +0,0 @@ -'use strict'; - -import Priority from './fixtures/enum/priority'; -import Provider from './fixtures/enum/provider'; -import MapsMessage from './fixtures/maps-message'; -import EmailMessage from './fixtures/email-message'; -import NestedMessage from './fixtures/nested-message'; -import Enum from 'gdbots/common/enum'; -import FrozenMessageIsImmutable from 'gdbots/pbj/exception/frozen-message-is-immutable'; -import JsonSerializer from 'gdbots/pbj/serializer/json-serializer'; - -/** @var Serializer */ -let serializer = null; - -/** @var EmailMessage */ -let emailMessageFixture = null; - -describe('maps-test', function() { - it('create message from array', function(done) { - let message = createEmailMessage(); - message.set('priority', Priority.HIGH); - - message.get('priority').should.eql(Priority.HIGH); - Priority.HIGH.should.eql(message.get('priority')); - - let json = getSerializer().serialize(message); - message = getSerializer().deserialize(json); - - message.get('priority').should.eql(Priority.HIGH); - Priority.HIGH.should.eql(message.get('priority')); - - message.get('nested').get('location').getLatitude().should.eql(0.5); - - done(); - }); - - it('unique items in set', function(done) { - let message = EmailMessage.create() - .addToSet('labels', ['CHICKEN', 'Chicken', 'chicken', 'DONUTS', 'Donuts', 'donuts']); - - message.get('labels').length.should.eql(2); - message.get('labels').should.eql(['chicken', 'donuts']); - - done(); - }); - - it('is in set', function(done) { - let message = EmailMessage.create() - .addToSet('labels', ['abc']) - .addToSet( - 'enum_in_set', - [ - Provider.AOL, - Provider.GMAIL, - ] - ); - - message.isInSet('labels', 'abc').should.true; - message.isInSet('labels', 'idontexist').should.false; - message.isInSet('enum_in_set', Provider.AOL).should.true; - message.isInSet('enum_in_set', Provider.HOTMAIL).should.false; - - done(); - }); - - it('enum in set', function(done) { - let message = EmailMessage.create() - .addToSet( - 'enum_in_set', - [ - Provider.AOL, - Provider.AOL, - Provider.GMAIL, - Provider.GMAIL, - ] - ); - - message.get('enum_in_set').length.should.eql(2); - message.get('enum_in_set').should.eql([Provider.AOL, Provider.GMAIL]); - - done(); - }); - - it('is in list', function(done) { - let message = createEmailMessage(); - - let messageInList = message.get('any_of_message')[0]; - let messageNotInList = cloneMessage(messageInList); - messageNotInList.addToMap('string', 'key', 'val'); - - message.isInList('any_of_message', messageInList).should.true; - message.isInList('any_of_message', messageNotInList).should.false; - message.isInList('any_of_message', 'notinlist').should.false; - message.isInList('any_of_message', NestedMessage.create()).should.false; - message.isInList('enum_in_list', 'aol').should.false; - message.isInList('enum_in_list', Provider.AOL).should.true; - message.isInList('enum_in_list', 'notinlist').should.false; - message.isInList('enum_in_list', Provider.HOTMAIL).should.false; - - done(); - }); - - it('enum in list', function(done) { - let message = EmailMessage.create() - .addToList( - 'enum_in_list', - [ - Provider.AOL, - Provider.AOL, - Provider.GMAIL, - Provider.GMAIL, - ] - ); - - message.get('enum_in_list').length.should.eql(4); - message.get('enum_in_list').should.eql([Provider.AOL, Provider.AOL, Provider.GMAIL, Provider.GMAIL]); - - done(); - }); - - it('is in map', function(done) { - let message = MapsMessage.create(); - message.addToMap('string', 'string1', 'val1'); - - message.isInMap('string', 'string1').should.true; - message.isInMap('string', 'notinmap').should.false; - message.isInMap('microtime', 'notinmap').should.false; - - message.clear('string'); - - message.isInMap('string', 'string1').should.false; - - done(); - }); - - it('nested message', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create() - .set('test1', 'val1') - .addToSet('test2', [1, 2]); - - message.set('nested', nestedMessage); - - nestedMessage.get('test2').should.eql([1, 2]); - message.get('nested').should.eql(nestedMessage); - - done(); - }); - - it('any of message in list', function(done) { - let message = EmailMessage.create() - .addToList( - 'any_of_message', - [ - MapsMessage.create().addToMap('string', 'test:field:name', 'value1'), - NestedMessage.create().set('test1', 'value1') - ] - ); - - message.get('any_of_message').length.should.eql(2); - - done(); - }); - - it('freeze', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - message.freeze(); - - message.isFrozen().should.true; - nestedMessage.isFrozen().should.true; - - done(); - }); - - it('frozen message is immutable', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - try { - - message.freeze(); - - message.set('from_name', 'homer'); - nestedMessage.set('test1', 'test1'); - } catch (e) { - e.should.eql(new FrozenMessageIsImmutable()); - } - - done(); - }); - - it('clone', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - nestedMessage.set('test1', 'original'); - - let message2 = cloneMessage(message); - message2.set('from_name', 'marge').get('nested').set('test1', 'clone'); - - (message == message2).should.false; - (message.get('date_sent') == message2.get('date_sent')).should.false; - (message.get('microtime_sent') == message2.get('microtime_sent')).should.false; - (message.get('nested') == message2.get('nested')).should.false; - (message.get('nested').get('test1') == message2.get('nested').get('test1')).should.false; - - done(); - }); - - it('clone is mutable after original is frozen', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - nestedMessage.set('test1', 'original'); - - message.freeze(); - - let message2 = cloneMessage(message); - message2.set('from_name', 'marge').get('nested').set('test1', 'clone'); - - try { - message.set('from_name', 'homer').get('nested').set('test1', 'original'); - - console.error('Original message should still be immutable.'); - } catch (e) { - e.should.eql(new FrozenMessageIsImmutable()); - } - - done(); - }); -}); - -/** - * @return Message message - */ -function cloneMessage(message) { - return getSerializer().deserialize( - getSerializer().serialize(message) - ); -} - -/** - * @return Serializer - */ -function getSerializer() { - if (null === serializer) { - serializer = new JsonSerializer(); - } - return serializer; -} - -/** - * @return EmailMessage - */ -function createEmailMessage() { - if (null === emailMessageFixture) { - emailMessageFixture = getSerializer().deserialize(jsonEmailMessage()); - } - - let message = cloneMessage(emailMessageFixture); - - message.set('date_sent', new Date('2014-12-25T12:12:00.123456Z')); - - return message; -} - -/** - * @return string - */ -function jsonEmailMessage() { - return JSON.stringify({ - "_schema": "pbj:gdbots:tests.pbj:fixtures:email-message:1-0-0", - "id": "0dcee564-aa71-11e4-a811-3c15c2c60168", - "from_name": "homer ", - "from_email": "homer@thesimpsons.com", - "priority": 2, - "sent": false, - "date_sent": "2014-12-25T12:12:00.123456+00:00", - "microtime_sent": "1422122017734617", - "provider": "gmail", - "labels": [ - "donuts", - "mmmm", - "chicken" - ], - "nested": { - "_schema": "pbj:gdbots:tests.pbj:fixtures:nested-message:1-0-0", - "test1": "val1", - "test2": [ - 1, - 2 - ], - "location": { - "type": "Point", - "coordinates": [102.0,0.5] - }, - "refs": [ - { - "curie": "gdbots:tests.pbj:fixtures:email-message", - "id": "0dcee564-aa71-11e4-a811-3c15c2c60168", - "tag": "parent" - }, - { - "curie": "gdbots:tests.pbj:fixtures:email-message", - "id": "0dcee564-aa71-11e4-a811-3c15c2c60168", - "tag": "parent" - } - ] - }, - "enum_in_set": [ - "aol", - "gmail" - ], - "enum_in_list": [ - "aol", - "aol", - "gmail", - "gmail" - ], - "any_of_message": [ - { - "_schema": "pbj:gdbots:tests.pbj:fixtures:maps-message:1-0-0", - "String": { - "test:field:name": "value1" - } - }, - { - "_schema": "pbj:gdbots:tests.pbj:fixtures:nested-message:1-0-0", - "test1": "value1" - } - ] - }); -} diff --git a/tests/serializers/JsonSerializer.test.js b/tests/serializers/JsonSerializer.test.js new file mode 100644 index 0000000..deb3e8c --- /dev/null +++ b/tests/serializers/JsonSerializer.test.js @@ -0,0 +1,16 @@ +import test from 'tape'; +import JsonSerializer from '../../src/serializers/JsonSerializer'; +import SampleMessageV1 from './../fixtures/SampleMessageV1'; + +test('JsonSerializer tests', (t) => { + const message = SampleMessageV1.create() + .set('string_single', 'test') + .addToSet('string_set', ['set1', 'set2']) + .addToList('string_list', ['list1', 'list2']) + .addToMap('string_map', 'key1', 'val1') + .addToMap('string_map', 'key2', 'val2'); + + t.same(JsonSerializer.serialize(message), '{"_schema":"pbj:gdbots:pbj.tests::sample-message:1-0-0","mixin_int":0,"string_single":"test","string_set":["set1","set2"],"string_list":["list1","list2"],"string_map":{"key1":"val1","key2":"val2"}}'); + + t.end(); +}); diff --git a/tests/serializers/ObjectSerializer.test.js b/tests/serializers/ObjectSerializer.test.js new file mode 100644 index 0000000..4348fb3 --- /dev/null +++ b/tests/serializers/ObjectSerializer.test.js @@ -0,0 +1,121 @@ +import test from 'tape'; +import Fb from '../../src/FieldBuilder'; +import DynamicField from '../../src/well-known/DynamicField'; +import GeoPoint from '../../src/well-known/GeoPoint'; +import MessageRef from '../../src/MessageRef'; +import T from '../../src/types'; +import ObjectSerializer from '../../src/serializers/ObjectSerializer'; +import SampleMessageV1 from '../fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from '../fixtures/SampleOtherMessageV1'; + +test('ObjectSerializer serialize tests', (t) => { + const message = SampleMessageV1.create() + .set('string_single', 'test') + .addToSet('string_set', ['set1', 'set2']) + .addToList('string_list', ['list1', 'list2']) + .addToMap('string_map', 'key1', 'val1') + .addToMap('string_map', 'key2', 'val2') + .set('message_single', SampleOtherMessageV1.create().set('test', 'single')) + .addToList('message_list', [SampleOtherMessageV1.create().set('test', 'list')]) + .addToMap('message_map', 'test', SampleOtherMessageV1.create().set('test', 'map')); + + const obj = { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + string_single: 'test', + string_set: ['set1', 'set2'], + string_list: ['list1', 'list2'], + string_map: { key1: 'val1', key2: 'val2' }, + message_single: { + _schema: 'pbj:gdbots:pbj.tests::sample-other-message:1-0-0', + mixin_int: 0, + test: 'single', + }, + message_list: [ + { + _schema: 'pbj:gdbots:pbj.tests::sample-other-message:1-0-0', + mixin_int: 0, + test: 'list', + }, + ], + message_map: { + test: { + _schema: 'pbj:gdbots:pbj.tests::sample-other-message:1-0-0', + mixin_int: 0, + test: 'map', + }, + }, + }; + + t.same(ObjectSerializer.serialize(message), obj); + + t.end(); +}); + + +test('ObjectSerializer deserialize tests', (t) => { + const message = SampleMessageV1.create() + .addToList('string_list', ['list1', 'list2']) + .set('string_single', 'test') + .addToSet('string_set', ['set1', 'set2']) + .addToMap('string_map', 'key1', 'val1') + .addToMap('string_map', 'key2', 'val2') + .set('message_single', SampleOtherMessageV1.create().set('test', 'single')) + .addToList('message_list', [SampleOtherMessageV1.create().set('test', 'list')]) + .addToMap('message_map', 'test', SampleOtherMessageV1.create().set('test', 'map')); + + t.true(message.equals(ObjectSerializer.deserialize(ObjectSerializer.serialize(message)))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode Message tests', (t) => { + const message = SampleMessageV1.create(); + const field = message.schema().getField('string_single'); + const obj = { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + }; + + t.same(ObjectSerializer.encodeMessage(message, field), obj); + t.true(message.equals(ObjectSerializer.decodeMessage(obj, field))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode MessageRef tests', (t) => { + const obj = { curie: 'acme:blog:node:article', id: '123', tag: 'tag' }; + const ref = MessageRef.fromObject(obj); + const field = Fb.create('test', T.MessageRefType.create()).build(); + + t.same(ObjectSerializer.encodeMessageRef(ref, field), obj); + t.true(ref.equals(ObjectSerializer.decodeMessageRef(obj, field))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode GeoPoint tests', (t) => { + const obj = { type: 'Point', coordinates: [102, 0.5] }; + const geoPoint = GeoPoint.fromObject(obj); + const field = Fb.create('test', T.GeoPointType.create()).build(); + + t.same(ObjectSerializer.encodeGeoPoint(geoPoint, field), obj); + t.true(geoPoint.equals(ObjectSerializer.decodeGeoPoint(obj, field))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode DynamicField tests', (t) => { + const obj = { name: 'test', bool_val: true }; + const df = DynamicField.fromObject(obj); + const field = Fb.create('test', T.DynamicFieldType.create()).build(); + + t.same(ObjectSerializer.encodeDynamicField(df, field), obj); + t.true(df.equals(ObjectSerializer.decodeDynamicField(obj, field))); + + t.end(); +}); diff --git a/tests/type/trinary-type-test.js b/tests/type/trinary-type-test.js deleted file mode 100644 index cc661f4..0000000 --- a/tests/type/trinary-type-test.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -import TrinaryType from 'gdbots/pbj/type/trinary-type'; -import FieldBuilder from 'gdbots/pbj/field-builder'; - -describe('trinary-type-test', function() { - it('validate encoding', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - - type.encode(0, field).should.eql(0); - type.encode(1, field).should.eql(1); - type.encode(2, field).should.eql(2); - - done(); - }); - - it('validate decoding', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - - type.decode(null, field).should.eql(0); - type.decode(0, field).should.eql(0); - type.decode(1, field).should.eql(1); - type.decode(2, field).should.eql(2); - - type.decode('0', field).should.eql(0); - type.decode('1', field).should.eql(1); - type.decode('2', field).should.eql(2); - - done(); - }); - - it('validate values', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - - type.guard(0, field); - type.guard(1, field); - type.guard(2, field); - - done(); - }); - - it('invalid values validation', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - let thrown = false; - - let invalid = [ - 'a', - [], - 3, - -1, - false, - true, - ]; - - invalid.forEach(function(val) { - try { - type.guard(val, field); - } catch (e) { - thrown = true; - } - - if (false === thrown) { - console.log('TrinaryType field accepted invalid value [' + val + '].'); - } - }); - - done(); - }); -}); diff --git a/tests/type/type-test.js b/tests/type/type-test.js deleted file mode 100644 index 4b249d1..0000000 --- a/tests/type/type-test.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -import NestedMessage from '../fixtures/nested-message'; -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import ArraySerializer from 'gdbots/pbj/serializer/array-serializer'; -import BinaryType from 'gdbots/pbj/type/binary-type'; -import BlobType from 'gdbots/pbj/type/blob-type'; -import MediumBlobType from 'gdbots/pbj/type/medium-blob-type'; -import MediumTextType from 'gdbots/pbj/type/medium-text-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import TextType from 'gdbots/pbj/type/text-type'; -import Fb from 'gdbots/pbj/field-builder'; - -describe('type-test', function() { - it('should validate geo-point type', function(done) { - let point = GeoPoint.fromArray({ - type: 'Point', - coordinates: [102.0, 0.5] - }); - - let message = NestedMessage.create(); - message.set('location', point); - - message.get('location').getLatitude().should.eql(0.5); - message.get('location').getLongitude().should.eql(102.0); - message.toArray().location.should.eql(point.toArray()); - - let json = new ArraySerializer().serialize(message); - let newMessage = message.constructor.fromArray(json); - - newMessage.toArray().should.eql(message.toArray()); - - done(); - }); - - it('should thrown an exception when guard max-bytes invalid', function(done) { - let types = [ - BinaryType, BlobType, MediumBlobType, - MediumTextType, StringType, TextType - ]; - - types.forEach(function(TypeName) { - let field = Fb.create(TypeName.name, TypeName.create()).build(); - let text = 'a'.repeat(field.getType().getMaxBytes() + 1); - let thrown = false; - - try { - field.getType().guard(text, field); - } catch (e) { - thrown = true; - } - - if (false === thrown) { - console.log('[' + TypeName.name + '] accepted more than [' + field.getType().getMaxBytes() + '] bytes.'); - } - }); - - done(); - }); -}); diff --git a/tests/types/BigIntType.test.js b/tests/types/BigIntType.test.js new file mode 100644 index 0000000..bd92f1d --- /dev/null +++ b/tests/types/BigIntType.test.js @@ -0,0 +1,99 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BigIntType from '../../src/types/BigIntType'; +import BigNumber from '../../src/well-known/BigNumber'; +import helpers from './helpers'; + +test('BigIntType property tests', (t) => { + const bigIntType = BigIntType.create(); + t.true(bigIntType instanceof Type); + t.true(bigIntType instanceof BigIntType); + t.same(bigIntType, BigIntType.create()); + t.true(bigIntType === BigIntType.create()); + t.same(bigIntType.getTypeName(), TypeName.BIG_INT); + t.same(bigIntType.getTypeValue(), TypeName.BIG_INT.valueOf()); + t.same(bigIntType.isScalar(), false); + t.same(bigIntType.encodesToScalar(), true); + t.same(bigIntType.getDefault(), new BigNumber(0)); + t.same(bigIntType.isBoolean(), false); + t.same(bigIntType.isBinary(), false); + t.same(bigIntType.isNumeric(), true); + t.same(bigIntType.isString(), false); + t.same(bigIntType.isMessage(), false); + t.same(bigIntType.allowedInSet(), true); + + try { + bigIntType.test = 1; + t.fail('BigIntType instance is mutable'); + } catch (e) { + t.pass('BigIntType instance is immutable'); + } + + t.end(); +}); + + +test('BigIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: BigIntType.create() }); + const valid = [ + new BigNumber(0), + new BigNumber('18446744073709551615'), + ]; + const invalid = [ + -1, + new BigNumber('-1'), + new BigNumber('18446744073709551616'), + '0', + '1', + '2', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BigIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: BigIntType.create() }); + const samples = [ + { input: new BigNumber('18446744073709551615'), output: '18446744073709551615' }, + { input: new BigNumber('18446744073709551610.555'), output: '18446744073709551611' }, + { input: new BigNumber(1), output: '1' }, + { input: new BigNumber(1.44444), output: '1' }, + { input: 0, output: '0' }, + { input: 1, output: '0' }, + { input: 2, output: '0' }, + { input: false, output: '0' }, + { input: '', output: '0' }, + { input: null, output: '0' }, + { input: undefined, output: '0' }, + { input: NaN, output: '0' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BigIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: BigIntType.create() }); + const samples = [ + { input: '18446744073709551615', output: new BigNumber('18446744073709551615') }, + { input: '1', output: new BigNumber('1') }, + { input: '0', output: new BigNumber('0') }, + { input: '0', output: new BigNumber(0) }, + { input: new BigNumber(1), output: new BigNumber(1) }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/BinaryType.test.js b/tests/types/BinaryType.test.js new file mode 100644 index 0000000..09a1786 --- /dev/null +++ b/tests/types/BinaryType.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BinaryType from '../../src/types/BinaryType'; +import helpers from './helpers'; + +test('BinaryType property tests', (t) => { + const binaryType = BinaryType.create(); + t.true(binaryType instanceof Type); + t.true(binaryType instanceof BinaryType); + t.same(binaryType, BinaryType.create()); + t.true(binaryType === BinaryType.create()); + t.same(binaryType.getTypeName(), TypeName.BINARY); + t.same(binaryType.getTypeValue(), TypeName.BINARY.valueOf()); + t.same(binaryType.isScalar(), true); + t.same(binaryType.encodesToScalar(), true); + t.same(binaryType.getDefault(), null); + t.same(binaryType.isBoolean(), false); + t.same(binaryType.isBinary(), true); + t.same(binaryType.isNumeric(), false); + t.same(binaryType.isString(), true); + t.same(binaryType.isMessage(), false); + t.same(binaryType.allowedInSet(), true); + t.same(binaryType.getMaxBytes(), 255); + + try { + binaryType.test = 1; + t.fail('binaryType instance is mutable'); + } catch (e) { + t.pass('binaryType instance is immutable'); + } + + t.end(); +}); + + +test('BinaryType guard tests', (t) => { + const field = new Field({ name: 'test', type: BinaryType.create() }); + const valid = [ + 'test', + 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', + 'IGljZSDwn42mIHBvb3Ag8J+SqSBkb2gg8J+YsyA=', + '4LKgX+CyoA==', + ]; + const invalid = [-1, 1, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BinaryType guard (min/max length) tests', (t) => { + const binaryType = BinaryType.create(); + binaryType.decodeFromBase64(false); + binaryType.encodeToBase64(false); + + const field = new Field({ name: 'test', type: binaryType, minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + + binaryType.decodeFromBase64(true); + binaryType.encodeToBase64(true); + t.end(); +}); + + +test('BinaryType encode tests', (t) => { + const field = new Field({ name: 'test', type: BinaryType.create() }); + const samples = [ + { input: 'test', output: 'dGVzdA==' }, + { input: 'homer simpson', output: 'aG9tZXIgc2ltcHNvbg==' }, + { input: '✓ à la mode', output: '4pyTIMOgIGxhIG1vZGU=' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz' }, + { input: '(╯°□°)╯︵ ┻━┻', output: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==' }, + { input: 'ಠ_ಠ', output: '4LKgX+CyoA==' }, + { input: 'foo © bar 𝌆 baz', output: 'Zm9vIMKpIGJhciDwnYyGIGJheg==' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BinaryType decode tests', (t) => { + const field = new Field({ name: 'test', type: BinaryType.create() }); + const samples = [ + { input: 'dGVzdA==', output: 'test' }, + { input: 'aG9tZXIgc2ltcHNvbg==', output: 'homer simpson' }, + { input: '4pyTIMOgIGxhIG1vZGU=', output: '✓ à la mode' }, + { input: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', output: '(╯°□°)╯︵ ┻━┻' }, + { input: '4LKgX+CyoA==', output: 'ಠ_ಠ' }, + { input: 'Zm9vIMKpIGJhciDwnYyGIGJheg==', output: 'foo © bar 𝌆 baz' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/BlobType.test.js b/tests/types/BlobType.test.js new file mode 100644 index 0000000..c13a409 --- /dev/null +++ b/tests/types/BlobType.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BlobType from '../../src/types/BlobType'; +import helpers from './helpers'; + +test('BlobType property tests', (t) => { + const blobType = BlobType.create(); + t.true(blobType instanceof Type); + t.true(blobType instanceof BlobType); + t.same(blobType, BlobType.create()); + t.true(blobType === BlobType.create()); + t.same(blobType.getTypeName(), TypeName.BLOB); + t.same(blobType.getTypeValue(), TypeName.BLOB.valueOf()); + t.same(blobType.isScalar(), true); + t.same(blobType.encodesToScalar(), true); + t.same(blobType.getDefault(), null); + t.same(blobType.isBoolean(), false); + t.same(blobType.isBinary(), true); + t.same(blobType.isNumeric(), false); + t.same(blobType.isString(), true); + t.same(blobType.isMessage(), false); + t.same(blobType.allowedInSet(), false); + t.same(blobType.getMaxBytes(), 65535); + + try { + blobType.test = 1; + t.fail('blobType instance is mutable'); + } catch (e) { + t.pass('blobType instance is immutable'); + } + + t.end(); +}); + + +test('BlobType guard tests', (t) => { + const field = new Field({ name: 'test', type: BlobType.create() }); + const valid = [ + 'test', + 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', + 'IGljZSDwn42mIHBvb3Ag8J+SqSBkb2gg8J+YsyA=', + '4LKgX+CyoA==', + ]; + const invalid = [-1, 1, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BlobType guard (min/max length) tests', (t) => { + const blobType = BlobType.create(); + blobType.decodeFromBase64(false); + blobType.encodeToBase64(false); + + const field = new Field({ name: 'test', type: blobType, minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + + blobType.decodeFromBase64(true); + blobType.encodeToBase64(true); + t.end(); +}); + + +test('BlobType encode tests', (t) => { + const field = new Field({ name: 'test', type: BlobType.create() }); + const samples = [ + { input: 'test', output: 'dGVzdA==' }, + { input: 'homer simpson', output: 'aG9tZXIgc2ltcHNvbg==' }, + { input: '✓ à la mode', output: '4pyTIMOgIGxhIG1vZGU=' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz' }, + { input: '(╯°□°)╯︵ ┻━┻', output: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==' }, + { input: 'ಠ_ಠ', output: '4LKgX+CyoA==' }, + { input: 'foo © bar 𝌆 baz', output: 'Zm9vIMKpIGJhciDwnYyGIGJheg==' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BlobType decode tests', (t) => { + const field = new Field({ name: 'test', type: BlobType.create() }); + const samples = [ + { input: 'dGVzdA==', output: 'test' }, + { input: 'aG9tZXIgc2ltcHNvbg==', output: 'homer simpson' }, + { input: '4pyTIMOgIGxhIG1vZGU=', output: '✓ à la mode' }, + { input: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', output: '(╯°□°)╯︵ ┻━┻' }, + { input: '4LKgX+CyoA==', output: 'ಠ_ಠ' }, + { input: 'Zm9vIMKpIGJhciDwnYyGIGJheg==', output: 'foo © bar 𝌆 baz' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/BooleanType.test.js b/tests/types/BooleanType.test.js new file mode 100644 index 0000000..1974f19 --- /dev/null +++ b/tests/types/BooleanType.test.js @@ -0,0 +1,104 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BooleanType from '../../src/types/BooleanType'; +import helpers from './helpers'; + +test('BooleanType property tests', (t) => { + const booleanType = BooleanType.create(); + t.true(booleanType instanceof Type); + t.true(booleanType instanceof BooleanType); + t.same(booleanType, BooleanType.create()); + t.true(booleanType === BooleanType.create()); + t.same(booleanType.getTypeName(), TypeName.BOOLEAN); + t.same(booleanType.getTypeValue(), TypeName.BOOLEAN.valueOf()); + t.same(booleanType.isScalar(), true); + t.same(booleanType.encodesToScalar(), true); + t.same(booleanType.getDefault(), false); + t.same(booleanType.isBoolean(), true); + t.same(booleanType.isBinary(), false); + t.same(booleanType.isNumeric(), false); + t.same(booleanType.isString(), false); + t.same(booleanType.isMessage(), false); + t.same(booleanType.allowedInSet(), false); + + try { + booleanType.test = 1; + t.fail('booleanType instance is mutable'); + } catch (e) { + t.pass('booleanType instance is immutable'); + } + + t.end(); +}); + + +test('BooleanType guard tests', (t) => { + const field = new Field({ name: 'test', type: BooleanType.create() }); + const valid = [true, false]; + const invalid = ['true', 'false', 1, 0, 'on', 'off', 'yes', 'no', '+', '-', null, [], {}, -1, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BooleanType encode tests', (t) => { + const field = new Field({ name: 'test', type: BooleanType.create() }); + const samples = [ + { input: false, output: false }, + { input: '', output: false }, + { input: null, output: false }, + { input: undefined, output: false }, + { input: 0, output: false }, + { input: NaN, output: false }, + { input: true, output: true }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BooleanType decode tests', (t) => { + const field = new Field({ name: 'test', type: BooleanType.create() }); + const samples = [ + { input: false, output: false }, + { input: 'false', output: false }, + { input: 'FALSE', output: false }, + { input: 'False', output: false }, + { input: 'FaLSe', output: false }, + { input: '0', output: false }, + { input: '-1', output: false }, + { input: 'no', output: false }, + { input: 'null', output: false }, + { input: '', output: false }, + { input: 0, output: false }, + { input: -1, output: false }, + { input: null, output: false }, + { input: undefined, output: false }, + { input: {}, output: false }, + { input: [], output: false }, + { input: NaN, output: false }, + + { input: true, output: true }, + { input: 'true', output: true }, + { input: 'TRUE', output: true }, + { input: 'True', output: true }, + { input: 'tRuE', output: true }, + { input: '1', output: true }, + { input: 'yes', output: true }, + { input: 'YES', output: true }, + { input: 'Yes', output: true }, + { input: 'yEs', output: true }, + { input: '+', output: true }, + { input: 'on', output: true }, + { input: 'ON', output: true }, + { input: 'On', output: true }, + { input: 1, output: true }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/DateTimeType.test.js b/tests/types/DateTimeType.test.js new file mode 100644 index 0000000..17ea49f --- /dev/null +++ b/tests/types/DateTimeType.test.js @@ -0,0 +1,126 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DateTimeType from '../../src/types/DateTimeType'; +import helpers from './helpers'; + +test('DateTimeType property tests', (t) => { + const dateTimeType = DateTimeType.create(); + t.true(dateTimeType instanceof Type); + t.true(dateTimeType instanceof DateTimeType); + t.same(dateTimeType, DateTimeType.create()); + t.true(dateTimeType === DateTimeType.create()); + t.same(dateTimeType.getTypeName(), TypeName.DATE_TIME); + t.same(dateTimeType.getTypeValue(), TypeName.DATE_TIME.valueOf()); + t.same(dateTimeType.isScalar(), false); + t.same(dateTimeType.encodesToScalar(), true); + t.same(dateTimeType.getDefault(), null); + t.same(dateTimeType.isBoolean(), false); + t.same(dateTimeType.isBinary(), false); + t.same(dateTimeType.isNumeric(), false); + t.same(dateTimeType.isString(), true); + t.same(dateTimeType.isMessage(), false); + t.same(dateTimeType.allowedInSet(), false); + + try { + dateTimeType.test = 1; + t.fail('DateTimeType instance is mutable'); + } catch (e) { + t.pass('DateTimeType instance is immutable'); + } + + t.end(); +}); + + +test('DateTimeType guard tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const valid = [ + new Date('2015-12-25T07:30:45.123Z'), + new Date('2015-12-25T07:30:45.123+08:00'), + ]; + const invalid = [ + '2015-12-25', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DateTimeType encode tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const samples = [ + { + input: new Date('2015-12-25T07:30:45.123Z'), + output: '2015-12-25T07:30:45.123Z', + }, + { + input: new Date('2015-12-25T07:30:45.123+08:00'), + output: '2015-12-24T23:30:45.123Z', + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('DateTimeType decode tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const date = new Date(Date.UTC(2015, 11, 25, 12, 30, 45, 123)); + const samples = [ + { + input: '2015-12-25T07:30:45.123Z', + output: new Date('2015-12-25T07:30:45.123Z'), + }, + { + input: '2015-12-25T07:30:45.123+08:00', + output: new Date('2015-12-24T23:30:45.123Z'), + }, + { + input: '2015-12-25T07:30:45.123+0800', + output: new Date('2015-12-24T23:30:45.123Z'), + }, + { input: date, output: date }, + { input: null, output: null }, + ]; + + function format(d) { + return d instanceof Date ? d.toISOString() : d; + } + + samples.forEach((obj) => { + try { + const actual = field.getType().decode(obj.input, field); + t.same(format(actual), format(obj.output)); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('DateTimeType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const samples = ['nope', '12/25/2015', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/DateType.test.js b/tests/types/DateType.test.js new file mode 100644 index 0000000..a3fbcc2 --- /dev/null +++ b/tests/types/DateType.test.js @@ -0,0 +1,111 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DateType from '../../src/types/DateType'; +import helpers from './helpers'; + +test('DateType property tests', (t) => { + const dateType = DateType.create(); + t.true(dateType instanceof Type); + t.true(dateType instanceof DateType); + t.same(dateType, DateType.create()); + t.true(dateType === DateType.create()); + t.same(dateType.getTypeName(), TypeName.DATE); + t.same(dateType.getTypeValue(), TypeName.DATE.valueOf()); + t.same(dateType.isScalar(), false); + t.same(dateType.encodesToScalar(), true); + t.same(dateType.getDefault(), null); + t.same(dateType.isBoolean(), false); + t.same(dateType.isBinary(), false); + t.same(dateType.isNumeric(), false); + t.same(dateType.isString(), true); + t.same(dateType.isMessage(), false); + t.same(dateType.allowedInSet(), false); + + try { + dateType.test = 1; + t.fail('DateType instance is mutable'); + } catch (e) { + t.pass('DateType instance is immutable'); + } + + t.end(); +}); + + +test('DateType guard tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const valid = [ + new Date(2015, 11, 25), + new Date('2015-12-25'), + ]; + const invalid = [ + '2015-12-25', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DateType encode tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const samples = [ + { input: new Date(2015, 11, 25), output: '2015-12-25' }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('DateType decode tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const date = new Date(Date.UTC(2015, 11, 25)); + const samples = [ + { input: '2015-12-25T07:30:45.123Z', output: date }, + { input: '2015-12-25T07:30:45.123+08:00', output: date }, + { input: '2015-12-25T07:30:45.123+0800', output: date }, + { input: '2015-12-25', output: date }, + { input: date, output: date }, + { input: null, output: null }, + ]; + + function format(d) { + return d instanceof Date ? d.toISOString().substr(0, 10) : d; + } + + samples.forEach((obj) => { + try { + const actual = field.getType().decode(obj.input, field); + t.same(format(actual), format(obj.output)); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('DateType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const samples = ['nope', '12/25/2015', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/DecimalType.test.js b/tests/types/DecimalType.test.js new file mode 100644 index 0000000..e6fe6bb --- /dev/null +++ b/tests/types/DecimalType.test.js @@ -0,0 +1,118 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DecimalType from '../../src/types/DecimalType'; +import helpers from './helpers'; + +test('DecimalType property tests', (t) => { + const decimalType = DecimalType.create(); + t.true(decimalType instanceof Type); + t.true(decimalType instanceof DecimalType); + t.same(decimalType, DecimalType.create()); + t.true(decimalType === DecimalType.create()); + t.same(decimalType.getTypeName(), TypeName.DECIMAL); + t.same(decimalType.getTypeValue(), TypeName.DECIMAL.valueOf()); + t.same(decimalType.isScalar(), true); + t.same(decimalType.encodesToScalar(), true); + t.same(decimalType.getDefault(), 0.0); + t.same(decimalType.isBoolean(), false); + t.same(decimalType.isBinary(), false); + t.same(decimalType.isNumeric(), true); + t.same(decimalType.isString(), false); + t.same(decimalType.isMessage(), false); + t.same(decimalType.allowedInSet(), true); + t.same(decimalType.getMin(), Number.MIN_VALUE); + t.same(decimalType.getMax(), Number.MAX_VALUE); + + try { + decimalType.test = 1; + t.fail('decimalType instance is mutable'); + } catch (e) { + t.pass('decimalType instance is immutable'); + } + + t.end(); +}); + + +test('DecimalType guard tests', (t) => { + const field = new Field({ name: 'test', type: DecimalType.create() }); + const valid = [0.0, 3.14, -3.14, Number.MIN_VALUE, Number.MAX_VALUE]; + const invalid = ['0', '0.0', '3.14', '-3.14', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DecimalType encode tests', (t) => { + const field = new Field({ name: 'test', type: DecimalType.create() }); + const samples = [ + { input: 0.1, output: '0.10' }, + { input: 3.14159265358979, output: '3.14' }, + { input: -3.14159265358979, output: '-3.14' }, + { input: false, output: '0.00' }, + { input: '', output: '0.00' }, + { input: null, output: '0.00' }, + { input: undefined, output: '0.00' }, + { input: NaN, output: '0.00' }, + { input: '3.14159265358979', output: '3.14' }, + { input: '-3.14159265358979', output: '-3.14' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.comment('test samples with scale of 6'); + + const fieldWith6Scale = new Field({ name: 'test_6_scale', type: DecimalType.create(), precision: 10, scale: 6 }); + const samplesWith6Scale = [ + { input: 0.1, output: '0.100000' }, + { input: 1.1, output: '1.100000' }, + { input: 3.14159265358979, output: '3.141593' }, + { input: -3.14159265358979, output: '-3.141593' }, + { input: '3.14159265358979', output: '3.141593' }, + { input: '-3.14159265358979', output: '-3.141593' }, + ]; + + helpers.encodeSamples(fieldWith6Scale, samplesWith6Scale, t); + + t.end(); +}); + + +test('DecimalType decode tests', (t) => { + const field = new Field({ name: 'test', type: DecimalType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 3.14159265358979, output: 3.14 }, + { input: -3.14159265358979, output: -3.14 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.14 }, + { input: '-3.14159265358979', output: -3.14 }, + ]; + + helpers.decodeSamples(field, samples, t); + + const fieldWith6Scale = new Field({ name: 'test_6_scale', type: DecimalType.create(), precision: 10, scale: 6 }); + const samplesWith6Scale = [ + { input: '0.100000', output: 0.1 }, + { input: '1.100000', output: 1.1 }, + { input: 3.14159265358979, output: 3.141593 }, + { input: -3.14159265358979, output: -3.141593 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.141593 }, + { input: '-3.14159265358979', output: -3.141593 }, + ]; + + helpers.decodeSamples(fieldWith6Scale, samplesWith6Scale, t); + + t.end(); +}); diff --git a/tests/types/DynamicFieldType.test.js b/tests/types/DynamicFieldType.test.js new file mode 100644 index 0000000..f186a5e --- /dev/null +++ b/tests/types/DynamicFieldType.test.js @@ -0,0 +1,124 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DynamicFieldType from '../../src/types/DynamicFieldType'; +import DynamicField from '../../src/well-known/DynamicField'; +import helpers from './helpers'; + +test('DynamicFieldType property tests', (t) => { + const dynamicFieldType = DynamicFieldType.create(); + t.true(dynamicFieldType instanceof Type); + t.true(dynamicFieldType instanceof DynamicFieldType); + t.same(dynamicFieldType, DynamicFieldType.create()); + t.true(dynamicFieldType === DynamicFieldType.create()); + t.same(dynamicFieldType.getTypeName(), TypeName.DYNAMIC_FIELD); + t.same(dynamicFieldType.getTypeValue(), TypeName.DYNAMIC_FIELD.valueOf()); + t.same(dynamicFieldType.isScalar(), false); + t.same(dynamicFieldType.encodesToScalar(), false); + t.same(dynamicFieldType.getDefault(), null); + t.same(dynamicFieldType.isBoolean(), false); + t.same(dynamicFieldType.isBinary(), false); + t.same(dynamicFieldType.isNumeric(), false); + t.same(dynamicFieldType.isString(), false); + t.same(dynamicFieldType.isMessage(), false); + t.same(dynamicFieldType.allowedInSet(), false); + + try { + dynamicFieldType.test = 1; + t.fail('DynamicFieldType instance is mutable'); + } catch (e) { + t.pass('DynamicFieldType instance is immutable'); + } + + t.end(); +}); + + +test('DynamicFieldType guard tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const valid = [ + DynamicField.createStringVal('test', 'taco'), + DynamicField.createIntVal('test', 9000), + ]; + const invalid = [ + 'test', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DynamicFieldType encode tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const codec = { encodeDynamicField: value => value.toJSON() }; + const samples = [ + { + input: DynamicField.createStringVal('test', 'taco'), + output: { name: 'test', string_val: 'taco' }, + }, + { + input: DynamicField.createBoolVal('test', true), + output: { name: 'test', bool_val: true }, + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('DynamicFieldType decode tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const codec = { decodeDynamicField: value => DynamicField.fromObject(value) }; + const df = DynamicField.createStringVal('test1', 'taco'); + const samples = [ + { + input: { name: 'test', string_val: 'taco' }, + output: DynamicField.createStringVal('test', 'taco'), + }, + { + input: { name: 'test', bool_val: true }, + output: DynamicField.createBoolVal('test', true), + }, + { input: df, output: df }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('DynamicFieldType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const codec = { decodeDynamicField: value => DynamicField.fromObject(value) }; + const samples = [ + 'nope', + { name: 'test', nothing: true }, + { name: 'test' }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/FloatType.test.js b/tests/types/FloatType.test.js new file mode 100644 index 0000000..f457244 --- /dev/null +++ b/tests/types/FloatType.test.js @@ -0,0 +1,90 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import FloatType from '../../src/types/FloatType'; +import helpers from './helpers'; + +test('FloatType property tests', (t) => { + const floatType = FloatType.create(); + t.true(floatType instanceof Type); + t.true(floatType instanceof FloatType); + t.same(floatType, FloatType.create()); + t.true(floatType === FloatType.create()); + t.same(floatType.getTypeName(), TypeName.FLOAT); + t.same(floatType.getTypeValue(), TypeName.FLOAT.valueOf()); + t.same(floatType.isScalar(), true); + t.same(floatType.encodesToScalar(), true); + t.same(floatType.getDefault(), 0.0); + t.same(floatType.isBoolean(), false); + t.same(floatType.isBinary(), false); + t.same(floatType.isNumeric(), true); + t.same(floatType.isString(), false); + t.same(floatType.isMessage(), false); + t.same(floatType.allowedInSet(), true); + t.same(floatType.getMin(), Number.MIN_VALUE); + t.same(floatType.getMax(), Number.MAX_VALUE); + + try { + floatType.test = 1; + t.fail('floatType instance is mutable'); + } catch (e) { + t.pass('floatType instance is immutable'); + } + + t.end(); +}); + + +test('FloatType guard tests', (t) => { + const field = new Field({ name: 'test', type: FloatType.create() }); + const valid = [ + 0.0, 3.14159265358979323846, -3.14159265358979323846, Number.MIN_VALUE, Number.MAX_VALUE, + ]; + const invalid = [ + '0', '0.0', '3.14159265358979323846', '-3.14159265358979323846', null, [], {}, '', NaN, undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('FloatType encode tests', (t) => { + const field = new Field({ name: 'test', type: FloatType.create() }); + const samples = [ + { input: 0.0, output: 0.0 }, + { input: 3.14159265358979, output: 3.14159265358979 }, + { input: -3.14159265358979, output: -3.14159265358979 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.14159265358979 }, + { input: '-3.14159265358979', output: -3.14159265358979 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('FloatType decode tests', (t) => { + const field = new Field({ name: 'test', type: FloatType.create() }); + const samples = [ + { input: 0.0, output: 0.0 }, + { input: 3.14159265358979, output: 3.14159265358979 }, + { input: -3.14159265358979, output: -3.14159265358979 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.14159265358979 }, + { input: '-3.14159265358979', output: -3.14159265358979 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/GeoPointType.test.js b/tests/types/GeoPointType.test.js new file mode 100644 index 0000000..fe0a8d9 --- /dev/null +++ b/tests/types/GeoPointType.test.js @@ -0,0 +1,115 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import GeoPointType from '../../src/types/GeoPointType'; +import GeoPoint from '../../src/well-known/GeoPoint'; +import helpers from './helpers'; + +test('GeoPointType property tests', (t) => { + const geoPointType = GeoPointType.create(); + t.true(geoPointType instanceof Type); + t.true(geoPointType instanceof GeoPointType); + t.same(geoPointType, GeoPointType.create()); + t.true(geoPointType === GeoPointType.create()); + t.same(geoPointType.getTypeName(), TypeName.GEO_POINT); + t.same(geoPointType.getTypeValue(), TypeName.GEO_POINT.valueOf()); + t.same(geoPointType.isScalar(), false); + t.same(geoPointType.encodesToScalar(), false); + t.same(geoPointType.getDefault(), null); + t.same(geoPointType.isBoolean(), false); + t.same(geoPointType.isBinary(), false); + t.same(geoPointType.isNumeric(), false); + t.same(geoPointType.isString(), false); + t.same(geoPointType.isMessage(), false); + t.same(geoPointType.allowedInSet(), false); + + try { + geoPointType.test = 1; + t.fail('GeoPointType instance is mutable'); + } catch (e) { + t.pass('GeoPointType instance is immutable'); + } + + t.end(); +}); + + +test('GeoPointType guard tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const valid = [ + GeoPoint.fromString('34.1789335,-118.347594'), + new GeoPoint(34.1789335, -118.347594), + ]; + const invalid = [ + '34.1789335,-118.347594', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('GeoPointType encode tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const codec = { encodeGeoPoint: value => value.toJSON() }; + const samples = [ + { + input: GeoPoint.fromString('34.1789335,-118.347594'), + output: { type: 'Point', coordinates: [-118.347594, 34.1789335] }, + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('GeoPointType decode tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const codec = { decodeGeoPoint: value => GeoPoint.fromObject(value) }; + const gp = GeoPoint.fromString('34.1789335,-118.347594'); + const samples = [ + { + input: { type: 'Point', coordinates: [-118.347594, 34.1789335] }, + output: gp, + }, + { input: gp, output: gp }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('GeoPointType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const codec = { decodeGeoPoint: value => GeoPoint.fromObject(value) }; + const samples = [ + 'nope', + { type: 'Point', coordinates: [-181, 91] }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/IdentifierType.test.js b/tests/types/IdentifierType.test.js new file mode 100644 index 0000000..b7cbf84 --- /dev/null +++ b/tests/types/IdentifierType.test.js @@ -0,0 +1,109 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import IdentifierType from '../../src/types/IdentifierType'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import SampleUuidIdentifier from '../fixtures/well-known/SampleUuidIdentifier'; +import helpers from './helpers'; + +test('IdentifierType property tests', (t) => { + const identifierType = IdentifierType.create(); + t.true(identifierType instanceof Type); + t.true(identifierType instanceof IdentifierType); + t.same(identifierType, IdentifierType.create()); + t.true(identifierType === IdentifierType.create()); + t.same(identifierType.getTypeName(), TypeName.IDENTIFIER); + t.same(identifierType.getTypeValue(), TypeName.IDENTIFIER.valueOf()); + t.same(identifierType.isScalar(), false); + t.same(identifierType.encodesToScalar(), true); + t.same(identifierType.getDefault(), null); + t.same(identifierType.isBoolean(), false); + t.same(identifierType.isBinary(), false); + t.same(identifierType.isNumeric(), false); + t.same(identifierType.isString(), true); + t.same(identifierType.isMessage(), false); + t.same(identifierType.allowedInSet(), true); + + try { + identifierType.test = 1; + t.fail('IdentifierType instance is mutable'); + } catch (e) { + t.pass('IdentifierType instance is immutable'); + } + + t.end(); +}); + + +test('IdentifierType guard tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const valid = [ + SampleUuidIdentifier.generate(), + SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + new SampleUuidIdentifier('4b268351-2445-4d98-a777-b461330d5c7a'), + ]; + const invalid = [ + UuidIdentifier.generate(), + '4b268351-2445-4d98-a777-b461330d5c7f', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IdentifierType encode tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + output: '4b268351-2445-4d98-a777-b461330d5c7f', + }, + { input: id, output: id.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('IdentifierType decode tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: '4b268351-2445-4d98-a777-b461330d5c7f', + output: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + }, + { input: id.toString(), output: id }, + { input: id, output: id }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('IdentifierType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const samples = ['nope', '4b268351-2445-4d98-a777-b461330d5c7fX', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/IntEnumType.test.js b/tests/types/IntEnumType.test.js new file mode 100644 index 0000000..a797687 --- /dev/null +++ b/tests/types/IntEnumType.test.js @@ -0,0 +1,92 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import IntEnumType from '../../src/types/IntEnumType'; +import SampleIntEnum from '../fixtures/enums/SampleIntEnum'; +import SampleStringEnum from '../fixtures/enums/SampleStringEnum'; +import helpers from './helpers'; + +test('IntEnumType property tests', (t) => { + const intEnumType = IntEnumType.create(); + t.true(intEnumType instanceof Type); + t.true(intEnumType instanceof IntEnumType); + t.same(intEnumType, IntEnumType.create()); + t.true(intEnumType === IntEnumType.create()); + t.same(intEnumType.getTypeName(), TypeName.INT_ENUM); + t.same(intEnumType.getTypeValue(), TypeName.INT_ENUM.valueOf()); + t.same(intEnumType.isScalar(), false); + t.same(intEnumType.encodesToScalar(), true); + t.same(intEnumType.getDefault(), null); + t.same(intEnumType.isBoolean(), false); + t.same(intEnumType.isBinary(), false); + t.same(intEnumType.isNumeric(), true); + t.same(intEnumType.isString(), false); + t.same(intEnumType.isMessage(), false); + t.same(intEnumType.allowedInSet(), true); + t.same(intEnumType.getMin(), 0); + t.same(intEnumType.getMax(), 65535); + + try { + intEnumType.test = 1; + t.fail('IntEnumType instance is mutable'); + } catch (e) { + t.pass('IntEnumType instance is immutable'); + } + + t.end(); +}); + + +test('IntEnumType guard tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const valid = [SampleIntEnum.UNKNOWN, SampleIntEnum.ENUM1, SampleIntEnum.ENUM2]; + const invalid = [0, 1, 2, '0', '1', '2', null, [], {}, '', NaN, undefined, SampleStringEnum.UNKNOWN]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IntEnumType encode tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const samples = [ + { input: SampleIntEnum.UNKNOWN, output: 0 }, + { input: SampleIntEnum.ENUM1, output: 1 }, + { input: SampleIntEnum.ENUM2, output: 2 }, + { input: 0, output: 0 }, + { input: 1, output: 0 }, + { input: 2, output: 0 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: SampleStringEnum.UNKNOWN, output: 0 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('IntEnumType decode tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const samples = [ + { input: 0, output: SampleIntEnum.UNKNOWN }, + { input: 1, output: SampleIntEnum.ENUM1 }, + { input: 2, output: SampleIntEnum.ENUM2 }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('IntEnumType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const samples = [3, false, [], {}, '', NaN, undefined, SampleStringEnum.UNKNOWN]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/IntType.test.js b/tests/types/IntType.test.js new file mode 100644 index 0000000..8173f7e --- /dev/null +++ b/tests/types/IntType.test.js @@ -0,0 +1,98 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import IntType from '../../src/types/IntType'; +import helpers from './helpers'; + +test('IntType property tests', (t) => { + const intType = IntType.create(); + t.true(intType instanceof Type); + t.true(intType instanceof IntType); + t.same(intType, IntType.create()); + t.true(intType === IntType.create()); + t.same(intType.getTypeName(), TypeName.INT); + t.same(intType.getTypeValue(), TypeName.INT.valueOf()); + t.same(intType.isScalar(), true); + t.same(intType.encodesToScalar(), true); + t.same(intType.getDefault(), 0); + t.same(intType.isBoolean(), false); + t.same(intType.isBinary(), false); + t.same(intType.isNumeric(), true); + t.same(intType.isString(), false); + t.same(intType.isMessage(), false); + t.same(intType.allowedInSet(), true); + t.same(intType.getMin(), 0); + t.same(intType.getMax(), 4294967295); + + try { + intType.test = 1; + t.fail('intType instance is mutable'); + } catch (e) { + t.pass('intType instance is immutable'); + } + + t.end(); +}); + + +test('IntType guard tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create() }); + const valid = [0, 4294967295, 1, 4294967294]; + const invalid = [-1, 4294967296, '0', '4294967295', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IntType guard (min/max) tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create(), min: 5, max: 10 }); + const valid = [5, 6, 10, 9]; + const invalid = [4, 11]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IntType encode tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 4294967295, output: 4294967295 }, + { input: 1, output: 1 }, + { input: 4294967294, output: 4294967294 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('IntType decode tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 4294967295, output: 4294967295 }, + { input: 1, output: 1 }, + { input: 4294967294, output: 4294967294 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MediumBlobType.test.js b/tests/types/MediumBlobType.test.js new file mode 100644 index 0000000..2bb7612 --- /dev/null +++ b/tests/types/MediumBlobType.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MediumBlobType from '../../src/types/MediumBlobType'; +import helpers from './helpers'; + +test('MediumBlobType property tests', (t) => { + const mediumBlobType = MediumBlobType.create(); + t.true(mediumBlobType instanceof Type); + t.true(mediumBlobType instanceof MediumBlobType); + t.same(mediumBlobType, MediumBlobType.create()); + t.true(mediumBlobType === MediumBlobType.create()); + t.same(mediumBlobType.getTypeName(), TypeName.MEDIUM_BLOB); + t.same(mediumBlobType.getTypeValue(), TypeName.MEDIUM_BLOB.valueOf()); + t.same(mediumBlobType.isScalar(), true); + t.same(mediumBlobType.encodesToScalar(), true); + t.same(mediumBlobType.getDefault(), null); + t.same(mediumBlobType.isBoolean(), false); + t.same(mediumBlobType.isBinary(), true); + t.same(mediumBlobType.isNumeric(), false); + t.same(mediumBlobType.isString(), true); + t.same(mediumBlobType.isMessage(), false); + t.same(mediumBlobType.allowedInSet(), false); + t.same(mediumBlobType.getMaxBytes(), 16777215); + + try { + mediumBlobType.test = 1; + t.fail('mediumBlobType instance is mutable'); + } catch (e) { + t.pass('mediumBlobType instance is immutable'); + } + + t.end(); +}); + + +test('MediumBlobType guard tests', (t) => { + const field = new Field({ name: 'test', type: MediumBlobType.create() }); + const valid = [ + 'test', + 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', + 'IGljZSDwn42mIHBvb3Ag8J+SqSBkb2gg8J+YsyA=', + '4LKgX+CyoA==', + ]; + const invalid = [-1, 1, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MediumBlobType guard (min/max length) tests', (t) => { + const mediumBlobType = MediumBlobType.create(); + mediumBlobType.decodeFromBase64(false); + mediumBlobType.encodeToBase64(false); + + const field = new Field({ name: 'test', type: mediumBlobType, minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + + mediumBlobType.decodeFromBase64(true); + mediumBlobType.encodeToBase64(true); + t.end(); +}); + + +test('MediumBlobType encode tests', (t) => { + const field = new Field({ name: 'test', type: MediumBlobType.create() }); + const samples = [ + { input: 'test', output: 'dGVzdA==' }, + { input: 'homer simpson', output: 'aG9tZXIgc2ltcHNvbg==' }, + { input: '✓ à la mode', output: '4pyTIMOgIGxhIG1vZGU=' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz' }, + { input: '(╯°□°)╯︵ ┻━┻', output: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==' }, + { input: 'ಠ_ಠ', output: '4LKgX+CyoA==' }, + { input: 'foo © bar 𝌆 baz', output: 'Zm9vIMKpIGJhciDwnYyGIGJheg==' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MediumBlobType decode tests', (t) => { + const field = new Field({ name: 'test', type: MediumBlobType.create() }); + const samples = [ + { input: 'dGVzdA==', output: 'test' }, + { input: 'aG9tZXIgc2ltcHNvbg==', output: 'homer simpson' }, + { input: '4pyTIMOgIGxhIG1vZGU=', output: '✓ à la mode' }, + { input: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', output: '(╯°□°)╯︵ ┻━┻' }, + { input: '4LKgX+CyoA==', output: 'ಠ_ಠ' }, + { input: 'Zm9vIMKpIGJhciDwnYyGIGJheg==', output: 'foo © bar 𝌆 baz' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MediumIntType.test.js b/tests/types/MediumIntType.test.js new file mode 100644 index 0000000..645126c --- /dev/null +++ b/tests/types/MediumIntType.test.js @@ -0,0 +1,88 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MediumIntType from '../../src/types/MediumIntType'; +import helpers from './helpers'; + +test('MediumIntType property tests', (t) => { + const mediumIntType = MediumIntType.create(); + t.true(mediumIntType instanceof Type); + t.true(mediumIntType instanceof MediumIntType); + t.same(mediumIntType, MediumIntType.create()); + t.true(mediumIntType === MediumIntType.create()); + t.same(mediumIntType.getTypeName(), TypeName.MEDIUM_INT); + t.same(mediumIntType.getTypeValue(), TypeName.MEDIUM_INT.valueOf()); + t.same(mediumIntType.isScalar(), true); + t.same(mediumIntType.encodesToScalar(), true); + t.same(mediumIntType.getDefault(), 0); + t.same(mediumIntType.isBoolean(), false); + t.same(mediumIntType.isBinary(), false); + t.same(mediumIntType.isNumeric(), true); + t.same(mediumIntType.isString(), false); + t.same(mediumIntType.isMessage(), false); + t.same(mediumIntType.allowedInSet(), true); + t.same(mediumIntType.getMin(), 0); + t.same(mediumIntType.getMax(), 16777215); + + try { + mediumIntType.test = 1; + t.fail('mediumIntType instance is mutable'); + } catch (e) { + t.pass('mediumIntType instance is immutable'); + } + + t.end(); +}); + + +test('MediumIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: MediumIntType.create() }); + const valid = [0, 16777215, 1, 16777214]; + const invalid = [-1, 16777216, '0', '16777215', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MediumIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: MediumIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 16777215, output: 16777215 }, + { input: 1, output: 1 }, + { input: 16777214, output: 16777214 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MediumIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: MediumIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 16777215, output: 16777215 }, + { input: 1, output: 1 }, + { input: 16777214, output: 16777214 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MediumTextType.test.js b/tests/types/MediumTextType.test.js new file mode 100644 index 0000000..26d36ec --- /dev/null +++ b/tests/types/MediumTextType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MediumTextType from '../../src/types/MediumTextType'; +import helpers from './helpers'; + +test('MediumTextType property tests', (t) => { + const mediumTextType = MediumTextType.create(); + t.true(mediumTextType instanceof Type); + t.true(mediumTextType instanceof MediumTextType); + t.same(mediumTextType, MediumTextType.create()); + t.true(mediumTextType === MediumTextType.create()); + t.same(mediumTextType.getTypeName(), TypeName.MEDIUM_TEXT); + t.same(mediumTextType.getTypeValue(), TypeName.MEDIUM_TEXT.valueOf()); + t.same(mediumTextType.isScalar(), true); + t.same(mediumTextType.encodesToScalar(), true); + t.same(mediumTextType.getDefault(), null); + t.same(mediumTextType.isBoolean(), false); + t.same(mediumTextType.isBinary(), false); + t.same(mediumTextType.isNumeric(), false); + t.same(mediumTextType.isString(), true); + t.same(mediumTextType.isMessage(), false); + t.same(mediumTextType.allowedInSet(), false); + t.same(mediumTextType.getMaxBytes(), 16777215); + + try { + mediumTextType.test = 1; + t.fail('mediumTextType instance is mutable'); + } catch (e) { + t.pass('mediumTextType instance is immutable'); + } + + t.end(); +}); + + +test('MediumTextType guard tests', (t) => { + const field = new Field({ name: 'test', type: MediumTextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const valid = ['test', largeText, '(╯°□°)╯︵ ┻━┻', ' ice 🍦 poop 💩 doh 😳 ', 'ಠ_ಠ']; + const invalid = [-1, 1, `${largeText}b`, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MediumTextType encode tests', (t) => { + const field = new Field({ name: 'test', type: MediumTextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MediumTextType decode tests', (t) => { + const field = new Field({ name: 'test', type: MediumTextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: false, output: 'false' }, + { input: true, output: 'true' }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: 'NaN' }, + { input: 3.14, output: '3.14' }, + { input: '3.14', output: '3.14' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MessageRefType.test.js b/tests/types/MessageRefType.test.js new file mode 100644 index 0000000..9f2209b --- /dev/null +++ b/tests/types/MessageRefType.test.js @@ -0,0 +1,115 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MessageRefType from '../../src/types/MessageRefType'; +import MessageRef from '../../src/MessageRef'; +import helpers from './helpers'; + +test('MessageRefType property tests', (t) => { + const messageRefType = MessageRefType.create(); + t.true(messageRefType instanceof Type); + t.true(messageRefType instanceof MessageRefType); + t.same(messageRefType, MessageRefType.create()); + t.true(messageRefType === MessageRefType.create()); + t.same(messageRefType.getTypeName(), TypeName.MESSAGE_REF); + t.same(messageRefType.getTypeValue(), TypeName.MESSAGE_REF.valueOf()); + t.same(messageRefType.isScalar(), false); + t.same(messageRefType.encodesToScalar(), false); + t.same(messageRefType.getDefault(), null); + t.same(messageRefType.isBoolean(), false); + t.same(messageRefType.isBinary(), false); + t.same(messageRefType.isNumeric(), false); + t.same(messageRefType.isString(), false); + t.same(messageRefType.isMessage(), false); + t.same(messageRefType.allowedInSet(), true); + + try { + messageRefType.test = 1; + t.fail('MessageRefType instance is mutable'); + } catch (e) { + t.pass('MessageRefType instance is immutable'); + } + + t.end(); +}); + + +test('MessageRefType guard tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const valid = [ + MessageRef.fromString('acme:blog:node:article:123#tag'), + MessageRef.fromString('acme:blog::article:2015/12/25/test:Still_The:id#2015.q4'), + ]; + const invalid = [ + 'acme:blog:node:article:123#tag', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MessageRefType encode tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const codec = { encodeMessageRef: value => value.toString() }; + const samples = [ + { + input: MessageRef.fromString('acme:blog:node:article:123#tag'), + output: 'acme:blog:node:article:123#tag', + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageRefType decode tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const codec = { decodeMessageRef: value => MessageRef.fromObject(value) }; + const ref = MessageRef.fromString('acme:blog::article:2015/12/25/test:Still_The:id#2015.Q4'); + const samples = [ + { + input: ref.toObject(), + output: ref, + }, + { input: ref, output: ref }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageRefType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const codec = { decodeMessageRef: value => MessageRef.fromObject(value) }; + const samples = [ + 'nope', + { curie: 'invalid', id: 'what' }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/MessageType.test.js b/tests/types/MessageType.test.js new file mode 100644 index 0000000..9988dc2 --- /dev/null +++ b/tests/types/MessageType.test.js @@ -0,0 +1,162 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MessageType from '../../src/types/MessageType'; +import Message from '../../src/Message'; +import helpers from './helpers'; +import SampleMessageV1 from '../fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from '../fixtures/SampleOtherMessageV1'; + +test('MessageType property tests', (t) => { + const messageType = MessageType.create(); + t.true(messageType instanceof Type); + t.true(messageType instanceof MessageType); + t.same(messageType, MessageType.create()); + t.true(messageType === MessageType.create()); + t.same(messageType.getTypeName(), TypeName.MESSAGE); + t.same(messageType.getTypeValue(), TypeName.MESSAGE.valueOf()); + t.same(messageType.isScalar(), false); + t.same(messageType.encodesToScalar(), false); + t.same(messageType.getDefault(), null); + t.same(messageType.isBoolean(), false); + t.same(messageType.isBinary(), false); + t.same(messageType.isNumeric(), false); + t.same(messageType.isString(), false); + t.same(messageType.isMessage(), true); + t.same(messageType.allowedInSet(), false); + + try { + messageType.test = 1; + t.fail('MessageType instance is mutable'); + } catch (e) { + t.pass('MessageType instance is immutable'); + } + + t.end(); +}); + + +test('MessageType guard tests', (t) => { + const field = new Field({ name: 'test', type: MessageType.create(), classProto: SampleMessageV1 }); + const valid = [ + SampleMessageV1.create().set('string_single', 'test'), + SampleMessageV1.create().set('mixin_int', 5), + SampleOtherMessageV1.create(), + ]; + const invalid = [ + 'test', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MessageType guard (anyOfCuries) tests', (t) => { + const field = new Field({ + name: 'test', + type: MessageType.create(), + classProto: SampleMessageV1, + anyOfCuries: ['gdbots:pbj.tests::sample-message'], + }); + const valid = [SampleMessageV1.create()]; + const invalid = [SampleOtherMessageV1.create()]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MessageType encode tests', (t) => { + const field = new Field({ name: 'test', type: MessageType.create(), classProto: SampleMessageV1 }); + const codec = { encodeMessage: value => value.toJSON() }; + const samples = [ + { + input: SampleMessageV1.create().set('string_single', 'test'), + output: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + string_single: 'test', + }, + }, + { + input: SampleMessageV1.create().set('mixin_int', 5), + output: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 5, + }, + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageType decode tests', (t) => { + const field = new Field({ name: 'test', type: MessageType.create(), classProto: SampleMessageV1 }); + const codec = { decodeMessage: value => Message.fromObject(value) }; + const message = SampleMessageV1.create().set('string_single', 'test'); + const samples = [ + { + input: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + string_single: 'test', + }, + output: SampleMessageV1.create().set('string_single', 'test'), + }, + { + input: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 5, + }, + output: SampleMessageV1.create().set('mixin_int', 5), + }, + { input: message, output: message }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageType decode(invalid) tests', (t) => { + const field = new Field({ + name: 'test', + type: MessageType.create(), + classProto: SampleMessageV1, + anyOfCuries: ['gdbots:pbj.tests::sample-message'], + }); + const codec = { decodeMessage: value => Message.fromObject(value) }; + const samples = [ + SampleMessageV1, + 'nope', + { name: 'test', nothing: true }, + { name: 'test' }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/MicrotimeType.test.js b/tests/types/MicrotimeType.test.js new file mode 100644 index 0000000..6000ac9 --- /dev/null +++ b/tests/types/MicrotimeType.test.js @@ -0,0 +1,107 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MicrotimeType from '../../src/types/MicrotimeType'; +import Microtime from '../../src/well-known/Microtime'; +import helpers from './helpers'; + +test('MicrotimeType property tests', (t) => { + const microtimeType = MicrotimeType.create(); + t.true(microtimeType instanceof Type); + t.true(microtimeType instanceof MicrotimeType); + t.same(microtimeType, MicrotimeType.create()); + t.true(microtimeType === MicrotimeType.create()); + t.same(microtimeType.getTypeName(), TypeName.MICROTIME); + t.same(microtimeType.getTypeValue(), TypeName.MICROTIME.valueOf()); + t.same(microtimeType.isScalar(), false); + t.same(microtimeType.encodesToScalar(), true); + t.true(microtimeType.getDefault() instanceof Microtime); + t.same(microtimeType.isBoolean(), false); + t.same(microtimeType.isBinary(), false); + t.same(microtimeType.isNumeric(), true); + t.same(microtimeType.isString(), false); + t.same(microtimeType.isMessage(), false); + t.same(microtimeType.allowedInSet(), true); + + try { + microtimeType.test = 1; + t.fail('MicrotimeType instance is mutable'); + } catch (e) { + t.pass('MicrotimeType instance is immutable'); + } + + t.end(); +}); + + +test('MicrotimeType guard tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const valid = [ + Microtime.create(), + Microtime.fromString('1495766080123456'), + new Microtime('1495766080123456'), + ]; + const invalid = [ + '1495766080123456', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MicrotimeType encode tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const mtime = Microtime.create(); + const samples = [ + { + input: Microtime.fromString('1495766080123456'), + output: '1495766080123456', + }, + { input: mtime, output: mtime.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MicrotimeType decode tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const mtime = Microtime.create(); + const samples = [ + { + input: '1495766080123456', + output: Microtime.fromString('1495766080123456'), + }, + { input: mtime.toString(), output: mtime }, + { input: mtime, output: mtime }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('MicrotimeType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const samples = ['nope', '1495766080', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedBigIntType.test.js b/tests/types/SignedBigIntType.test.js new file mode 100644 index 0000000..58aee81 --- /dev/null +++ b/tests/types/SignedBigIntType.test.js @@ -0,0 +1,106 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedBigIntType from '../../src/types/SignedBigIntType'; +import BigNumber from '../../src/well-known/BigNumber'; +import helpers from './helpers'; + +test('SignedBigIntType property tests', (t) => { + const signedBigIntType = SignedBigIntType.create(); + t.true(signedBigIntType instanceof Type); + t.true(signedBigIntType instanceof SignedBigIntType); + t.same(signedBigIntType, SignedBigIntType.create()); + t.true(signedBigIntType === SignedBigIntType.create()); + t.same(signedBigIntType.getTypeName(), TypeName.SIGNED_BIG_INT); + t.same(signedBigIntType.getTypeValue(), TypeName.SIGNED_BIG_INT.valueOf()); + t.same(signedBigIntType.isScalar(), false); + t.same(signedBigIntType.encodesToScalar(), true); + t.same(signedBigIntType.getDefault(), new BigNumber(0)); + t.same(signedBigIntType.isBoolean(), false); + t.same(signedBigIntType.isBinary(), false); + t.same(signedBigIntType.isNumeric(), true); + t.same(signedBigIntType.isString(), false); + t.same(signedBigIntType.isMessage(), false); + t.same(signedBigIntType.allowedInSet(), true); + + try { + signedBigIntType.test = 1; + t.fail('SignedBigIntType instance is mutable'); + } catch (e) { + t.pass('SignedBigIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedBigIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedBigIntType.create() }); + const valid = [ + new BigNumber(0), + new BigNumber('-9223372036854775808'), + new BigNumber('-9223372036854775807'), + new BigNumber('9223372036854775807'), + new BigNumber('9223372036854775806'), + ]; + const invalid = [ + -1, + new BigNumber('-9223372036854775809'), + new BigNumber('9223372036854775808'), + '0', + '1', + '2', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedBigIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedBigIntType.create() }); + const samples = [ + { input: new BigNumber('-9223372036854775808'), output: '-9223372036854775808' }, + { input: new BigNumber('-9223372036854775807.111'), output: '-9223372036854775807' }, + { input: new BigNumber('9223372036854775807'), output: '9223372036854775807' }, + { input: new BigNumber('9223372036854775806.111'), output: '9223372036854775806' }, + { input: new BigNumber(1), output: '1' }, + { input: new BigNumber(1.44444), output: '1' }, + { input: 0, output: '0' }, + { input: 1, output: '0' }, + { input: 2, output: '0' }, + { input: false, output: '0' }, + { input: '', output: '0' }, + { input: null, output: '0' }, + { input: undefined, output: '0' }, + { input: NaN, output: '0' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedBigIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedBigIntType.create() }); + const samples = [ + { input: '-9223372036854775808', output: new BigNumber('-9223372036854775808') }, + { input: '-9223372036854775807', output: new BigNumber('-9223372036854775807') }, + { input: '9223372036854775807', output: new BigNumber('9223372036854775807') }, + { input: '9223372036854775806', output: new BigNumber('9223372036854775806') }, + { input: '0', output: new BigNumber('0') }, + { input: '0', output: new BigNumber(0) }, + { input: new BigNumber(1), output: new BigNumber(1) }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedIntType.test.js b/tests/types/SignedIntType.test.js new file mode 100644 index 0000000..19d55d8 --- /dev/null +++ b/tests/types/SignedIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedIntType from '../../src/types/SignedIntType'; +import helpers from './helpers'; + +test('SignedIntType property tests', (t) => { + const signedIntType = SignedIntType.create(); + t.true(signedIntType instanceof Type); + t.true(signedIntType instanceof SignedIntType); + t.same(signedIntType, SignedIntType.create()); + t.true(signedIntType === SignedIntType.create()); + t.same(signedIntType.getTypeName(), TypeName.SIGNED_INT); + t.same(signedIntType.getTypeValue(), TypeName.SIGNED_INT.valueOf()); + t.same(signedIntType.isScalar(), true); + t.same(signedIntType.encodesToScalar(), true); + t.same(signedIntType.getDefault(), 0); + t.same(signedIntType.isBoolean(), false); + t.same(signedIntType.isBinary(), false); + t.same(signedIntType.isNumeric(), true); + t.same(signedIntType.isString(), false); + t.same(signedIntType.isMessage(), false); + t.same(signedIntType.allowedInSet(), true); + t.same(signedIntType.getMin(), -2147483648); + t.same(signedIntType.getMax(), 2147483647); + + try { + signedIntType.test = 1; + t.fail('signedIntType instance is mutable'); + } catch (e) { + t.pass('signedIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedIntType.create() }); + const valid = [0, -2147483648, 2147483647, -2147483647, 2147483646]; + const invalid = [-2147483649, 2147483648, '-2147483648', '2147483647', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedIntType.create() }); + const samples = [ + { input: -2147483648, output: -2147483648 }, + { input: 2147483647, output: 2147483647 }, + { input: -2147483647, output: -2147483647 }, + { input: 2147483646, output: 2147483646 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedIntType.create() }); + const samples = [ + { input: -2147483648, output: -2147483648 }, + { input: 2147483647, output: 2147483647 }, + { input: -2147483647, output: -2147483647 }, + { input: 2147483646, output: 2147483646 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedMediumIntType.test.js b/tests/types/SignedMediumIntType.test.js new file mode 100644 index 0000000..c05c0b1 --- /dev/null +++ b/tests/types/SignedMediumIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedMediumIntType from '../../src/types/SignedMediumIntType'; +import helpers from './helpers'; + +test('SignedMediumIntType property tests', (t) => { + const signedMediumIntType = SignedMediumIntType.create(); + t.true(signedMediumIntType instanceof Type); + t.true(signedMediumIntType instanceof SignedMediumIntType); + t.same(signedMediumIntType, SignedMediumIntType.create()); + t.true(signedMediumIntType === SignedMediumIntType.create()); + t.same(signedMediumIntType.getTypeName(), TypeName.SIGNED_MEDIUM_INT); + t.same(signedMediumIntType.getTypeValue(), TypeName.SIGNED_MEDIUM_INT.valueOf()); + t.same(signedMediumIntType.isScalar(), true); + t.same(signedMediumIntType.encodesToScalar(), true); + t.same(signedMediumIntType.getDefault(), 0); + t.same(signedMediumIntType.isBoolean(), false); + t.same(signedMediumIntType.isBinary(), false); + t.same(signedMediumIntType.isNumeric(), true); + t.same(signedMediumIntType.isString(), false); + t.same(signedMediumIntType.isMessage(), false); + t.same(signedMediumIntType.allowedInSet(), true); + t.same(signedMediumIntType.getMin(), -8388608); + t.same(signedMediumIntType.getMax(), 8388607); + + try { + signedMediumIntType.test = 1; + t.fail('signedMediumIntType instance is mutable'); + } catch (e) { + t.pass('signedMediumIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedMediumIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedMediumIntType.create() }); + const valid = [0, -8388608, 8388607, -8388607, 8388606]; + const invalid = [-8388609, 8388608, '-8388608', '8388607', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedMediumIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedMediumIntType.create() }); + const samples = [ + { input: -8388608, output: -8388608 }, + { input: 8388607, output: 8388607 }, + { input: -8388607, output: -8388607 }, + { input: 8388606, output: 8388606 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedMediumIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedMediumIntType.create() }); + const samples = [ + { input: -8388608, output: -8388608 }, + { input: 8388607, output: 8388607 }, + { input: -8388607, output: -8388607 }, + { input: 8388606, output: 8388606 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedSmallIntType.test.js b/tests/types/SignedSmallIntType.test.js new file mode 100644 index 0000000..0314e60 --- /dev/null +++ b/tests/types/SignedSmallIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedSmallIntType from '../../src/types/SignedSmallIntType'; +import helpers from './helpers'; + +test('SignedSmallIntType property tests', (t) => { + const signedSmallIntType = SignedSmallIntType.create(); + t.true(signedSmallIntType instanceof Type); + t.true(signedSmallIntType instanceof SignedSmallIntType); + t.same(signedSmallIntType, SignedSmallIntType.create()); + t.true(signedSmallIntType === SignedSmallIntType.create()); + t.same(signedSmallIntType.getTypeName(), TypeName.SIGNED_SMALL_INT); + t.same(signedSmallIntType.getTypeValue(), TypeName.SIGNED_SMALL_INT.valueOf()); + t.same(signedSmallIntType.isScalar(), true); + t.same(signedSmallIntType.encodesToScalar(), true); + t.same(signedSmallIntType.getDefault(), 0); + t.same(signedSmallIntType.isBoolean(), false); + t.same(signedSmallIntType.isBinary(), false); + t.same(signedSmallIntType.isNumeric(), true); + t.same(signedSmallIntType.isString(), false); + t.same(signedSmallIntType.isMessage(), false); + t.same(signedSmallIntType.allowedInSet(), true); + t.same(signedSmallIntType.getMin(), -32768); + t.same(signedSmallIntType.getMax(), 32767); + + try { + signedSmallIntType.test = 1; + t.fail('SignedSmallIntType instance is mutable'); + } catch (e) { + t.pass('SignedSmallIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedSmallIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedSmallIntType.create() }); + const valid = [0, -32768, 32767, -32767, 32766]; + const invalid = [-32769, 32768, '-32768', '32767', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedSmallIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedSmallIntType.create() }); + const samples = [ + { input: -32768, output: -32768 }, + { input: 32767, output: 32767 }, + { input: -32767, output: -32767 }, + { input: 32766, output: 32766 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedSmallIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedSmallIntType.create() }); + const samples = [ + { input: -32768, output: -32768 }, + { input: 32767, output: 32767 }, + { input: -32767, output: -32767 }, + { input: 32766, output: 32766 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedTinyIntType.test.js b/tests/types/SignedTinyIntType.test.js new file mode 100644 index 0000000..450b696 --- /dev/null +++ b/tests/types/SignedTinyIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedTinyIntType from '../../src/types/SignedTinyIntType'; +import helpers from './helpers'; + +test('SignedTinyIntType property tests', (t) => { + const signedTinyIntType = SignedTinyIntType.create(); + t.true(signedTinyIntType instanceof Type); + t.true(signedTinyIntType instanceof SignedTinyIntType); + t.same(signedTinyIntType, SignedTinyIntType.create()); + t.true(signedTinyIntType === SignedTinyIntType.create()); + t.same(signedTinyIntType.getTypeName(), TypeName.SIGNED_TINY_INT); + t.same(signedTinyIntType.getTypeValue(), TypeName.SIGNED_TINY_INT.valueOf()); + t.same(signedTinyIntType.isScalar(), true); + t.same(signedTinyIntType.encodesToScalar(), true); + t.same(signedTinyIntType.getDefault(), 0); + t.same(signedTinyIntType.isBoolean(), false); + t.same(signedTinyIntType.isBinary(), false); + t.same(signedTinyIntType.isNumeric(), true); + t.same(signedTinyIntType.isString(), false); + t.same(signedTinyIntType.isMessage(), false); + t.same(signedTinyIntType.allowedInSet(), true); + t.same(signedTinyIntType.getMin(), -128); + t.same(signedTinyIntType.getMax(), 127); + + try { + signedTinyIntType.test = 1; + t.fail('signedTinyIntType instance is mutable'); + } catch (e) { + t.pass('signedTinyIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedTinyIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedTinyIntType.create() }); + const valid = [0, -128, 127, -127, 126]; + const invalid = [-129, 128, '-128', '127', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedTinyIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedTinyIntType.create() }); + const samples = [ + { input: -128, output: -128 }, + { input: 127, output: 127 }, + { input: -127, output: -127 }, + { input: 126, output: 126 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedTinyIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedTinyIntType.create() }); + const samples = [ + { input: -128, output: -128 }, + { input: 127, output: 127 }, + { input: -127, output: -127 }, + { input: 126, output: 126 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SmallIntType.test.js b/tests/types/SmallIntType.test.js new file mode 100644 index 0000000..73ceaa6 --- /dev/null +++ b/tests/types/SmallIntType.test.js @@ -0,0 +1,88 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SmallIntType from '../../src/types/SmallIntType'; +import helpers from './helpers'; + +test('SmallIntType property tests', (t) => { + const smallIntType = SmallIntType.create(); + t.true(smallIntType instanceof Type); + t.true(smallIntType instanceof SmallIntType); + t.same(smallIntType, SmallIntType.create()); + t.true(smallIntType === SmallIntType.create()); + t.same(smallIntType.getTypeName(), TypeName.SMALL_INT); + t.same(smallIntType.getTypeValue(), TypeName.SMALL_INT.valueOf()); + t.same(smallIntType.isScalar(), true); + t.same(smallIntType.encodesToScalar(), true); + t.same(smallIntType.getDefault(), 0); + t.same(smallIntType.isBoolean(), false); + t.same(smallIntType.isBinary(), false); + t.same(smallIntType.isNumeric(), true); + t.same(smallIntType.isString(), false); + t.same(smallIntType.isMessage(), false); + t.same(smallIntType.allowedInSet(), true); + t.same(smallIntType.getMin(), 0); + t.same(smallIntType.getMax(), 65535); + + try { + smallIntType.test = 1; + t.fail('SmallIntType instance is mutable'); + } catch (e) { + t.pass('SmallIntType instance is immutable'); + } + + t.end(); +}); + + +test('SmallIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SmallIntType.create() }); + const valid = [0, 65535, 1, 65534]; + const invalid = [-1, 65536, '0', '65535', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SmallIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SmallIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 65535, output: 65535 }, + { input: 1, output: 1 }, + { input: 65534, output: 65534 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SmallIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SmallIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 65535, output: 65535 }, + { input: 1, output: 1 }, + { input: 65534, output: 65534 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/StringEnumType.test.js b/tests/types/StringEnumType.test.js new file mode 100644 index 0000000..7e9f653 --- /dev/null +++ b/tests/types/StringEnumType.test.js @@ -0,0 +1,90 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import StringEnumType from '../../src/types/StringEnumType'; +import SampleIntEnum from '../fixtures/enums/SampleIntEnum'; +import SampleStringEnum from '../fixtures/enums/SampleStringEnum'; +import helpers from './helpers'; + +test('StringEnumType property tests', (t) => { + const stringEnumType = StringEnumType.create(); + t.true(stringEnumType instanceof Type); + t.true(stringEnumType instanceof StringEnumType); + t.same(stringEnumType, StringEnumType.create()); + t.true(stringEnumType === StringEnumType.create()); + t.same(stringEnumType.getTypeName(), TypeName.STRING_ENUM); + t.same(stringEnumType.getTypeValue(), TypeName.STRING_ENUM.valueOf()); + t.same(stringEnumType.isScalar(), false); + t.same(stringEnumType.encodesToScalar(), true); + t.same(stringEnumType.getDefault(), null); + t.same(stringEnumType.isBoolean(), false); + t.same(stringEnumType.isBinary(), false); + t.same(stringEnumType.isNumeric(), false); + t.same(stringEnumType.isString(), true); + t.same(stringEnumType.isMessage(), false); + t.same(stringEnumType.allowedInSet(), true); + t.same(stringEnumType.getMaxBytes(), 100); + + try { + stringEnumType.test = 1; + t.fail('StringEnumType instance is mutable'); + } catch (e) { + t.pass('StringEnumType instance is immutable'); + } + + t.end(); +}); + + +test('StringEnumType guard tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const valid = [SampleStringEnum.UNKNOWN, SampleStringEnum.ENUM1, SampleStringEnum.ENUM2]; + const invalid = [0, 1, 2, '0', '1', '2', null, [], {}, '', NaN, undefined, SampleIntEnum.UNKNOWN]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringEnumType encode tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const samples = [ + { input: SampleStringEnum.UNKNOWN, output: 'unknown' }, + { input: SampleStringEnum.ENUM1, output: 'val1' }, + { input: SampleStringEnum.ENUM2, output: 'val2' }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('StringEnumType decode tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const samples = [ + { input: 'unknown', output: SampleStringEnum.UNKNOWN }, + { input: 'val1', output: SampleStringEnum.ENUM1 }, + { input: 'val2', output: SampleStringEnum.ENUM2 }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('StringEnumType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const samples = ['nope', false, [], {}, '', NaN, undefined, SampleIntEnum.UNKNOWN]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/StringType.test.js b/tests/types/StringType.test.js new file mode 100644 index 0000000..127ddd1 --- /dev/null +++ b/tests/types/StringType.test.js @@ -0,0 +1,333 @@ +import test from 'tape'; +import Format from '../../src/enums/Format'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import StringType from '../../src/types/StringType'; +import helpers from './helpers'; + +test('StringType property tests', (t) => { + const stringType = StringType.create(); + t.true(stringType instanceof Type); + t.true(stringType instanceof StringType); + t.same(stringType, StringType.create()); + t.true(stringType === StringType.create()); + t.same(stringType.getTypeName(), TypeName.STRING); + t.same(stringType.getTypeValue(), TypeName.STRING.valueOf()); + t.same(stringType.isScalar(), true); + t.same(stringType.encodesToScalar(), true); + t.same(stringType.getDefault(), null); + t.same(stringType.isBoolean(), false); + t.same(stringType.isBinary(), false); + t.same(stringType.isNumeric(), false); + t.same(stringType.isString(), true); + t.same(stringType.isMessage(), false); + t.same(stringType.allowedInSet(), true); + t.same(stringType.getMaxBytes(), 255); + + try { + stringType.test = 1; + t.fail('StringType instance is mutable'); + } catch (e) { + t.pass('StringType instance is immutable'); + } + + t.end(); +}); + + +test('StringType guard tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const valid = ['test', largeText, '(╯°□°)╯︵ ┻━┻', ' ice 🍦 poop 💩 doh 😳 ', 'ಠ_ಠ']; + const invalid = [-1, 1, `${largeText}b`, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (min/max length) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (custom pattern) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), pattern: '^\\w+$' }); + const valid = ['AValidValue', 'a_zA_Z0_9', 'all_lower', 'ALL_UPPER']; + const invalid = ['No spaces, commas, etc.', 'nope!', 'http://www.', '--', '', '#test', 'ಠ_ಠ']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=date) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.DATE }); + const valid = ['2015-12-25', '1999-12-31']; + const invalid = [ + '01-01-2000', + 'nope!', + '20151225', + '2000-1-1', + '2015/12/25', + '12/25/2015', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=date-time) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.DATE_TIME }); + const valid = ['2017-05-25T02:54:18Z', '2017-05-25T02:54:18+00:00']; + const invalid = [ + '01-01-2000', + 'nope!', + '2000-1-1', + '2015/12/25', + '12/25/2015', + '2017-05-25 23:31:53.197954 Z', + '2017-05-25T02:54:18a', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=slug) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.SLUG }); + const valid = [ + 'slug-case', + 'gCcx85zbxz4', + 'b8ib4r_UqFM', + '2015/12/25/slug-test', + '2015/12/25/slug_test', + '2015-12-25-Slug_Test', + ]; + const invalid = [ + 'Not A Slug', + 'nope!', + 'http://nope.', + '(╯°□°)╯︵ ┻━┻', + 'ice 🍦 poop 💩 doh 😳', + + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=email) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.EMAIL }); + const valid = ['homer@simpsons.com', 'TEST@WHAT.co.uk']; + const invalid = ['Not An Email', 'nope!', 'http://www.', 'test@what', '@']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=hashtag) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.HASHTAG }); + const valid = ['#Hashtag', 'NotherHashtag']; + const invalid = ['Not A Hashtag', 'nope!', 'http://www.', '111', '_111']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=ipv4) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.IPV4 }); + const valid = ['192.168.0.10', '4.2.2.2', '10.0.0.0']; + const invalid = ['Not An IPv4', 'nope!', 'http://www.', '10.1.2.', '10.1.2', '10.1', '.10.1.']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=ipv6) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.IPV6 }); + const valid = ['fe80::6ae3:b5ff:fe92:330e', '2001:0db8:0a0b:12f0:0000:0000:0000:0001']; + const invalid = [ + 'Not An IPv6', + 'nope!', + 'http://www.', + '192.168.0.10', + '03:1d:f2:64:6a:01', + 'fe80::35:92ff:fe24:24a3/64', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=hostname) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.HOSTNAME }); + const valid = ['test.com', 'localhost', 'local-dev', 'test.whatever.com']; + const invalid = [ + 'Not A Hostname', + 'nope!', + '1234', + '192.168.0.2000', + 'http://www.mydomain.com', + 'www.mydomain.com/page', + 'mydomain.com#page', + '_domain', + '*hi*', + '-hi-', + ':54:sda54', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=uri) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.URI }); + const valid = [ + 'tel:+1-816-555-1212', + 'telnet://192.0.2.16:80/', + 'gdbots:iam:command:create-user', + 'urn:isbn:0451450523', + ]; + const invalid = [ + 'Not A Uri', + 'nope!', + '1234', + 'http://➡.ws/䨹', + 'http://⌘.ws', + 'http://foo.com/unicode_(✪)_in_parens', + 'http://☺.damowmow.com/', + 'foo', + 'mailto:user@[255:192:168:1]', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=url) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.URL }); + const valid = [ + 'http://www.foo.bar./', + 'http://userid:password@example.com:8080', + 'http://userid:password@example.com:8080/', + 'http://userid@example.com', + 'http://userid@example.com/', + 'http://userid@example.com:8080', + 'http://userid@example.com:80', + 'http://userid:password@example.com', + 'http://userid:password@example.com/', + 'http://142.42.1.1/', + 'http://127.0.0.1:8080/', + 'http://foo.com/blah_(wikipedia)#cite-1', + 'http://foo.com/blah_(wikipedia)_blah#cite-1', + 'http://foo.com/(something)?after=parens', + 'http://code.google.com/events/sub/#&product=browser', + 'http://j.mp', + ]; + const invalid = [ + 'Not A Url', + 'nope!', + '1234', + 'http://⌘.ws', + 'http://foo.com/unicode_(✪)_in_parens', + 'http://☺.damowmow.com/', + 'htt://shouldfailed.com', + 'scheme://shouldfailed.com', + 'emailto:info@example.com', + 'http://##', + 'http://##/', + 'http://foo.bar?q=Spaces should be encoded', + '//', + '//a', + '///a', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=uuid) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.UUID }); + const valid = [ + 'e452dd74-41b5-11e7-a919-92ebcb67fe33', + 'd0410f23-75b0-4524-9ce7-2fcc008a7afd', + '093dc7f7-5915-56a5-87de-033e20310b14', + ]; + const invalid = [ + 'Not A UUID', + 'nope!', + '1234', + '11111111-2222-3333-4444-555555556', + '_xxxxxxxx-yyyy-zzzz-0000-11111111', + 'xxxxxxxx-yyyy-zzzz-0000-11111111_', + '1111111122223333444455555555', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType encode tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('StringType decode tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: false, output: 'false' }, + { input: true, output: 'true' }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: 'NaN' }, + { input: 3.14, output: '3.14' }, + { input: '3.14', output: '3.14' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TextType.test.js b/tests/types/TextType.test.js new file mode 100644 index 0000000..e605bdb --- /dev/null +++ b/tests/types/TextType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TextType from '../../src/types/TextType'; +import helpers from './helpers'; + +test('TextType property tests', (t) => { + const textType = TextType.create(); + t.true(textType instanceof Type); + t.true(textType instanceof TextType); + t.same(textType, TextType.create()); + t.true(textType === TextType.create()); + t.same(textType.getTypeName(), TypeName.TEXT); + t.same(textType.getTypeValue(), TypeName.TEXT.valueOf()); + t.same(textType.isScalar(), true); + t.same(textType.encodesToScalar(), true); + t.same(textType.getDefault(), null); + t.same(textType.isBoolean(), false); + t.same(textType.isBinary(), false); + t.same(textType.isNumeric(), false); + t.same(textType.isString(), true); + t.same(textType.isMessage(), false); + t.same(textType.allowedInSet(), false); + t.same(textType.getMaxBytes(), 65535); + + try { + textType.test = 1; + t.fail('TextType instance is mutable'); + } catch (e) { + t.pass('TextType instance is immutable'); + } + + t.end(); +}); + + +test('TextType guard tests', (t) => { + const field = new Field({ name: 'test', type: TextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const valid = ['test', largeText, '(╯°□°)╯︵ ┻━┻', ' ice 🍦 poop 💩 doh 😳 ', 'ಠ_ಠ']; + const invalid = [-1, 1, `${largeText}b`, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TextType encode tests', (t) => { + const field = new Field({ name: 'test', type: TextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TextType decode tests', (t) => { + const field = new Field({ name: 'test', type: TextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: false, output: 'false' }, + { input: true, output: 'true' }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: 'NaN' }, + { input: 3.14, output: '3.14' }, + { input: '3.14', output: '3.14' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TimeUuidType.test.js b/tests/types/TimeUuidType.test.js new file mode 100644 index 0000000..2f49047 --- /dev/null +++ b/tests/types/TimeUuidType.test.js @@ -0,0 +1,119 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TimeUuidType from '../../src/types/TimeUuidType'; +import TimeUuidIdentifier from '../../src/well-known/TimeUuidIdentifier'; +import SampleTimeUuidIdentifier from '../fixtures/well-known/SampleTimeUuidIdentifier'; +import helpers from './helpers'; + +test('TimeUuidType property tests', (t) => { + const timeUuidType = TimeUuidType.create(); + t.true(timeUuidType instanceof Type); + t.true(timeUuidType instanceof TimeUuidType); + t.same(timeUuidType, TimeUuidType.create()); + t.true(timeUuidType === TimeUuidType.create()); + t.same(timeUuidType.getTypeName(), TypeName.TIME_UUID); + t.same(timeUuidType.getTypeValue(), TypeName.TIME_UUID.valueOf()); + t.same(timeUuidType.isScalar(), false); + t.same(timeUuidType.encodesToScalar(), true); + t.true(timeUuidType.getDefault() instanceof TimeUuidIdentifier); + t.same(timeUuidType.isBoolean(), false); + t.same(timeUuidType.isBinary(), false); + t.same(timeUuidType.isNumeric(), false); + t.same(timeUuidType.isString(), true); + t.same(timeUuidType.isMessage(), false); + t.same(timeUuidType.allowedInSet(), true); + + try { + timeUuidType.test = 1; + t.fail('TimeUuidType instance is mutable'); + } catch (e) { + t.pass('TimeUuidType instance is immutable'); + } + + t.end(); +}); + + +test('TimeUuidType guard tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const valid = [ + SampleTimeUuidIdentifier.generate(), + SampleTimeUuidIdentifier.fromString('b385af9a-4413-11e7-a919-92ebcb67fe33'), + new SampleTimeUuidIdentifier('b385af9a-4413-11e7-a919-92ebcb67fe33'), + ]; + const invalid = [ + TimeUuidIdentifier.generate(), + 'b385af9a-4413-11e7-a919-92ebcb67fe33', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TimeUuidType encode tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const id = SampleTimeUuidIdentifier.generate(); + const samples = [ + { + input: SampleTimeUuidIdentifier.fromString('b385af9a-4413-11e7-a919-92ebcb67fe33'), + output: 'b385af9a-4413-11e7-a919-92ebcb67fe33', + }, + { input: id, output: id.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TimeUuidType decode tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const id = SampleTimeUuidIdentifier.generate(); + const samples = [ + { + input: 'b385af9a-4413-11e7-a919-92ebcb67fe33', + output: SampleTimeUuidIdentifier.fromString('b385af9a-4413-11e7-a919-92ebcb67fe33'), + }, + { input: id.toString(), output: id }, + { input: id, output: id }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('TimeUuidType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const samples = [ + 'nope', + '4b268351-2445-4d98-a777-b461330d5c7f', + 'b385af9a-4413-11e7-a919-92ebcb67fe33X', + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TimestampType.test.js b/tests/types/TimestampType.test.js new file mode 100644 index 0000000..ccd651d --- /dev/null +++ b/tests/types/TimestampType.test.js @@ -0,0 +1,84 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TimestampType from '../../src/types/TimestampType'; +import helpers from './helpers'; + +test('TimestampType property tests', (t) => { + const timestampType = TimestampType.create(); + t.true(timestampType instanceof Type); + t.true(timestampType instanceof TimestampType); + t.same(timestampType, TimestampType.create()); + t.true(timestampType === TimestampType.create()); + t.same(timestampType.getTypeName(), TypeName.TIMESTAMP); + t.same(timestampType.getTypeValue(), TypeName.TIMESTAMP.valueOf()); + t.same(timestampType.isScalar(), true); + t.same(timestampType.encodesToScalar(), true); + t.same(timestampType.getDefault(), Math.floor(Date.now() / 1000)); + t.same(timestampType.isBoolean(), false); + t.same(timestampType.isBinary(), false); + t.same(timestampType.isNumeric(), true); + t.same(timestampType.isString(), false); + t.same(timestampType.isMessage(), false); + t.same(timestampType.allowedInSet(), true); + + try { + timestampType.test = 1; + t.fail('TimestampType instance is mutable'); + } catch (e) { + t.pass('TimestampType instance is immutable'); + } + + t.end(); +}); + + +test('TimestampType guard tests', (t) => { + const field = new Field({ name: 'test', type: TimestampType.create() }); + const valid = [1451001600, 1495053313]; + const invalid = [-1, '1451001600', '1495053313', true, false, null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TimestampType encode tests', (t) => { + const field = new Field({ name: 'test', type: TimestampType.create() }); + const samples = [ + { input: 1451001600, output: 1451001600 }, + { input: 1495053313, output: 1495053313 }, + { input: '1451001600', output: 1451001600 }, + { input: '1495053313', output: 1495053313 }, + { input: false, output: 0 }, + { input: true, output: 1 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TimestampType decode tests', (t) => { + const field = new Field({ name: 'test', type: TimestampType.create() }); + const samples = [ + { input: 1451001600, output: 1451001600 }, + { input: 1495053313, output: 1495053313 }, + { input: '1451001600', output: 1451001600 }, + { input: '1495053313', output: 1495053313 }, + { input: false, output: 0 }, + { input: true, output: 1 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TinyIntType.test.js b/tests/types/TinyIntType.test.js new file mode 100644 index 0000000..fd58ebe --- /dev/null +++ b/tests/types/TinyIntType.test.js @@ -0,0 +1,88 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TinyIntType from '../../src/types/TinyIntType'; +import helpers from './helpers'; + +test('TinyIntType property tests', (t) => { + const tinyIntType = TinyIntType.create(); + t.true(tinyIntType instanceof Type); + t.true(tinyIntType instanceof TinyIntType); + t.same(tinyIntType, TinyIntType.create()); + t.true(tinyIntType === TinyIntType.create()); + t.same(tinyIntType.getTypeName(), TypeName.TINY_INT); + t.same(tinyIntType.getTypeValue(), TypeName.TINY_INT.valueOf()); + t.same(tinyIntType.isScalar(), true); + t.same(tinyIntType.encodesToScalar(), true); + t.same(tinyIntType.getDefault(), 0); + t.same(tinyIntType.isBoolean(), false); + t.same(tinyIntType.isBinary(), false); + t.same(tinyIntType.isNumeric(), true); + t.same(tinyIntType.isString(), false); + t.same(tinyIntType.isMessage(), false); + t.same(tinyIntType.allowedInSet(), true); + t.same(tinyIntType.getMin(), 0); + t.same(tinyIntType.getMax(), 255); + + try { + tinyIntType.test = 1; + t.fail('TinyIntType instance is mutable'); + } catch (e) { + t.pass('TinyIntType instance is immutable'); + } + + t.end(); +}); + + +test('TinyIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: TinyIntType.create() }); + const valid = [0, 255, 1, 254]; + const invalid = [-1, 256, '0', '255', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TinyIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: TinyIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 255, output: 255 }, + { input: 1, output: 1 }, + { input: 254, output: 254 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TinyIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: TinyIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 255, output: 255 }, + { input: 1, output: 1 }, + { input: 254, output: 254 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TrinaryType.test.js b/tests/types/TrinaryType.test.js new file mode 100644 index 0000000..ef25b2d --- /dev/null +++ b/tests/types/TrinaryType.test.js @@ -0,0 +1,90 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TrinaryType from '../../src/types/TrinaryType'; +import helpers from './helpers'; + +test('TrinaryType property tests', (t) => { + const trinaryType = TrinaryType.create(); + t.true(trinaryType instanceof Type); + t.true(trinaryType instanceof TrinaryType); + t.same(trinaryType, TrinaryType.create()); + t.true(trinaryType === TrinaryType.create()); + t.same(trinaryType.getTypeName(), TypeName.TRINARY); + t.same(trinaryType.getTypeValue(), TypeName.TRINARY.valueOf()); + t.same(trinaryType.isScalar(), true); + t.same(trinaryType.encodesToScalar(), true); + t.same(trinaryType.getDefault(), 0); + t.same(trinaryType.isBoolean(), false); + t.same(trinaryType.isBinary(), false); + t.same(trinaryType.isNumeric(), true); + t.same(trinaryType.isString(), false); + t.same(trinaryType.isMessage(), false); + t.same(trinaryType.allowedInSet(), false); + t.same(trinaryType.getMin(), 0); + t.same(trinaryType.getMax(), 2); + + try { + trinaryType.test = 1; + t.fail('trinaryType instance is mutable'); + } catch (e) { + t.pass('trinaryType instance is immutable'); + } + + t.end(); +}); + + +test('TrinaryType guard tests', (t) => { + const field = new Field({ name: 'test', type: TrinaryType.create() }); + const valid = [0, 1, 2]; + const invalid = [-1, 3, '0', '1', '2', true, false, null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TrinaryType encode tests', (t) => { + const field = new Field({ name: 'test', type: TrinaryType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 1, output: 1 }, + { input: 2, output: 2 }, + { input: '0', output: 0 }, + { input: '1', output: 1 }, + { input: '2', output: 2 }, + { input: false, output: 0 }, + { input: true, output: 1 }, // this is weird, true becomes 1 in _.toSafeInteger + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TrinaryType decode tests', (t) => { + const field = new Field({ name: 'test', type: TrinaryType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 1, output: 1 }, + { input: 2, output: 2 }, + { input: '0', output: 0 }, + { input: '1', output: 1 }, + { input: '2', output: 2 }, + { input: false, output: 0 }, + { input: true, output: 1 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/UuidType.test.js b/tests/types/UuidType.test.js new file mode 100644 index 0000000..4ce8e86 --- /dev/null +++ b/tests/types/UuidType.test.js @@ -0,0 +1,109 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import UuidType from '../../src/types/UuidType'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import SampleUuidIdentifier from '../fixtures/well-known/SampleUuidIdentifier'; +import helpers from './helpers'; + +test('UuidType property tests', (t) => { + const uuidType = UuidType.create(); + t.true(uuidType instanceof Type); + t.true(uuidType instanceof UuidType); + t.same(uuidType, UuidType.create()); + t.true(uuidType === UuidType.create()); + t.same(uuidType.getTypeName(), TypeName.UUID); + t.same(uuidType.getTypeValue(), TypeName.UUID.valueOf()); + t.same(uuidType.isScalar(), false); + t.same(uuidType.encodesToScalar(), true); + t.true(uuidType.getDefault() instanceof UuidIdentifier); + t.same(uuidType.isBoolean(), false); + t.same(uuidType.isBinary(), false); + t.same(uuidType.isNumeric(), false); + t.same(uuidType.isString(), true); + t.same(uuidType.isMessage(), false); + t.same(uuidType.allowedInSet(), true); + + try { + uuidType.test = 1; + t.fail('UuidType instance is mutable'); + } catch (e) { + t.pass('UuidType instance is immutable'); + } + + t.end(); +}); + + +test('UuidType guard tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const valid = [ + SampleUuidIdentifier.generate(), + SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + new SampleUuidIdentifier('4b268351-2445-4d98-a777-b461330d5c7a'), + ]; + const invalid = [ + UuidIdentifier.generate(), + '4b268351-2445-4d98-a777-b461330d5c7f', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('UuidType encode tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + output: '4b268351-2445-4d98-a777-b461330d5c7f', + }, + { input: id, output: id.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('UuidType decode tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: '4b268351-2445-4d98-a777-b461330d5c7f', + output: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + }, + { input: id.toString(), output: id }, + { input: id, output: id }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('UuidType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const samples = ['nope', '4b268351-2445-4d98-a777-b461330d5c7fX', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/helpers.js b/tests/types/helpers.js new file mode 100644 index 0000000..ea59531 --- /dev/null +++ b/tests/types/helpers.js @@ -0,0 +1,114 @@ +import toString from 'lodash/toString'; +import truncate from 'lodash/truncate'; + +/** + * Runs guard against an array of valid samples for the provided + * type and asserts that it *MUST* pass. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of valid samples to test. + * @param {Object} test - The test provider (with pass/fail methods). + */ +function guardValidSamples(field, samples, test) { + const type = field.getType(); + samples.forEach((value) => { + try { + type.guard(value, field); + const truncated = truncate(JSON.stringify(value)); + test.pass(`${type.getTypeName().getName()}.guard accepted valid value [${truncated}].`); + } catch (e) { + test.fail(e.message); + } + }); +} + +/** + * Runs guard against an array of invalid samples for the provided + * type and asserts that it *MUST* fail. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of invalid samples to test. + * @param {Object} test - The test provider (with pass/fail methods). + */ +function guardInvalidSamples(field, samples, test) { + const type = field.getType(); + samples.forEach((value) => { + try { + type.guard(value, field); + test.fail(`${type.getTypeName().getName()}.guard accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + test.pass(e.message); + } + }); +} + +/** + * Runs encode against the field's type with an array of samples containing + * the input to run and the expected output. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of objects with input/output properties. + * @param {Object} test - The test provider (with pass/fail methods). + * @param {Object} codec - Codec to use when type requires it. + */ +function encodeSamples(field, samples, test, codec = null) { + samples.forEach((obj) => { + try { + const actual = field.getType().encode(obj.input, field, codec); + test.same(actual, obj.output); + test.same(toString(actual), toString(obj.output)); + } catch (e) { + test.fail(e.message); + } + }); +} + +/** + * Runs decode against the field's type with an array of samples containing + * the input to run and the expected output. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of objects with input/output properties. + * @param {Object} test - The test provider (with pass/fail methods). + * @param {Object} codec - Codec to use when type requires it. + */ +function decodeSamples(field, samples, test, codec = null) { + samples.forEach((obj) => { + try { + const actual = field.getType().decode(obj.input, field, codec); + test.same(actual, obj.output); + test.same(toString(actual), toString(obj.output)); + } catch (e) { + test.fail(e.message); + } + }); +} + +/** + * Runs decode against an array of invalid samples for the provided + * type and asserts that it *MUST* pass. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of invalid samples to test. + * @param {Object} test - The test provider (with pass/fail methods). + * @param {Object} codec - Codec to use when type requires it. + */ +function decodeInvalidSamples(field, samples, test, codec = null) { + const type = field.getType(); + samples.forEach((value) => { + try { + const decoded = type.decode(value, field, codec); + test.fail(`${type.getTypeName().getName()}.decode accepted invalid value [${JSON.stringify(value)}] and returned [${JSON.stringify(decoded)}].`); + } catch (e) { + test.pass(e.message); + } + }); +} + +export default { + guardValidSamples, + guardInvalidSamples, + encodeSamples, + decodeSamples, + decodeInvalidSamples, +}; diff --git a/tests/well-known/DatedSlugIdentifier.test.js b/tests/well-known/DatedSlugIdentifier.test.js new file mode 100644 index 0000000..a7fdeb3 --- /dev/null +++ b/tests/well-known/DatedSlugIdentifier.test.js @@ -0,0 +1,83 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import DatedSlugIdentifier from '../../src/well-known/DatedSlugIdentifier'; +import SampleDatedSlugIdentifier from '../fixtures/well-known/SampleDatedSlugIdentifier'; + +test('DatedSlugIdentifier tests', (t) => { + const id = new SampleDatedSlugIdentifier('2015/12/25/homer-simpson'); + t.true(id instanceof Identifier); + t.true(id instanceof DatedSlugIdentifier); + t.true(id instanceof SampleDatedSlugIdentifier); + t.true(id.equals(SampleDatedSlugIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('DatedSlugIdentifier fromString tests', (t) => { + const slug1 = '2015/12/25/homer-simpson'; + const slug2 = '2016/12/25/bart-simpson'; + const id = SampleDatedSlugIdentifier.fromString(slug1); + t.true(id instanceof Identifier); + t.true(id instanceof DatedSlugIdentifier); + t.true(id instanceof SampleDatedSlugIdentifier); + + t.same(slug1, id.toString()); + t.same(slug1, id.valueOf()); + t.same(slug1, `${id}`); + t.same(JSON.stringify(slug1), JSON.stringify(id)); + t.true(id.equals(SampleDatedSlugIdentifier.fromString(slug1))); + t.false(id.equals(SampleDatedSlugIdentifier.fromString(slug2))); + + t.end(); +}); + + +test('DatedSlugIdentifier create tests', (t) => { + const slug = '2015/12/25/homer-simpson'; + const date = new Date(2015, 11, 25); + const id = SampleDatedSlugIdentifier.create('Homer Simpson', date); + t.true(id instanceof Identifier); + t.true(id instanceof DatedSlugIdentifier); + t.true(id instanceof SampleDatedSlugIdentifier); + + t.same(slug, id.toString()); + t.same(slug, id.valueOf()); + t.same(slug, `${id}`); + t.same(JSON.stringify(slug), JSON.stringify(id)); + t.true(id.equals(SampleDatedSlugIdentifier.fromString(slug))); + + t.end(); +}); + + +test('DatedSlugIdentifier (invalid) tests', (t) => { + const invalid = [ + 'Not a dated Slug', + 'not-a-dated-slug', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleDatedSlugIdentifier.fromString(value); + t.fail(`SampleDatedSlugIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/DynamicField.test.js b/tests/well-known/DynamicField.test.js new file mode 100644 index 0000000..9520e2e --- /dev/null +++ b/tests/well-known/DynamicField.test.js @@ -0,0 +1,118 @@ +import test from 'tape'; +import DynamicField from '../../src/well-known/DynamicField'; +import Field from '../../src/Field'; + +test('DynamicField property tests', (t) => { + const df1 = DynamicField.createStringVal('test1', 'taco'); + const df2 = DynamicField.createStringVal('test2', 'pizza'); + t.true(df1 instanceof DynamicField); + t.true(df1.getField() instanceof Field); + t.true(df2.getField() instanceof Field); + t.true(df1.getField() === df2.getField()); // ensures flyweight "field" instances + t.same(df1.getName(), 'test1'); + t.same(df1.getKind(), 'string_val'); + t.same(df1.getValue(), 'taco'); + t.same(df1.toJSON(), { name: 'test1', string_val: 'taco' }); + t.same(df1.toString(), '{"name":"test1","string_val":"taco"}'); + + t.same(df2.getName(), 'test2'); + t.same(df2.getKind(), 'string_val'); + t.same(df2.getValue(), 'pizza'); + t.same(df2.toJSON(), { name: 'test2', string_val: 'pizza' }); + t.same(df2.toString(), '{"name":"test2","string_val":"pizza"}'); + + t.false(df1.equals(df2)); + t.false(df2.equals(df1)); + + try { + df1.test = 1; + t.fail('df1 instance is mutable'); + } catch (e) { + t.pass('df1 instance is immutable'); + } + + try { + df2.test = 1; + t.fail('df2 instance is mutable'); + } catch (e) { + t.pass('df2 instance is immutable'); + } + + t.end(); +}); + + +test('DynamicField createBoolVal tests', (t) => { + const df = DynamicField.createBoolVal('test', true); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'bool_val'); + t.same(df.getValue(), true); + t.same(df.toJSON(), { name: 'test', bool_val: true }); + t.same(df.toString(), '{"name":"test","bool_val":true}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createDateVal tests', (t) => { + const date = new Date(Date.UTC(2015, 11, 25)); + const df = DynamicField.createDateVal('test', date); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'date_val'); + t.same(df.getValue(), date); + t.same(df.toJSON(), { name: 'test', date_val: '2015-12-25' }); + t.same(df.toString(), '{"name":"test","date_val":"2015-12-25"}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createFloatVal tests', (t) => { + const df = DynamicField.createFloatVal('test', 3.14); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'float_val'); + t.same(df.getValue(), 3.14); + t.same(df.toJSON(), { name: 'test', float_val: 3.14 }); + t.same(df.toString(), '{"name":"test","float_val":3.14}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createIntVal tests', (t) => { + const df = DynamicField.createIntVal('test', 9000); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'int_val'); + t.same(df.getValue(), 9000); + t.same(df.toJSON(), { name: 'test', int_val: 9000 }); + t.same(df.toString(), '{"name":"test","int_val":9000}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createTextVal tests', (t) => { + const df = DynamicField.createTextVal('test', 'ice 🍦 poop 💩 doh 😳'); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'text_val'); + t.same(df.getValue(), 'ice 🍦 poop 💩 doh 😳'); + t.same(df.toJSON(), { name: 'test', text_val: 'ice 🍦 poop 💩 doh 😳' }); + t.same(df.toString(), '{"name":"test","text_val":"ice 🍦 poop 💩 doh 😳"}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); diff --git a/tests/well-known/GeoPoint.test.js b/tests/well-known/GeoPoint.test.js new file mode 100644 index 0000000..4a83732 --- /dev/null +++ b/tests/well-known/GeoPoint.test.js @@ -0,0 +1,91 @@ +import test from 'tape'; +import GeoPoint from '../../src/well-known/GeoPoint'; + +test('GeoPoint new tests', (t) => { + const gp = new GeoPoint(0.5, 102.0); + t.true(gp instanceof GeoPoint); + t.true(gp.equals(new GeoPoint(gp.getLatitude(), gp.getLongitude()))); + t.true(gp.equals(GeoPoint.fromString(`${gp}`))); + t.true(gp.equals(GeoPoint.fromJSON(JSON.stringify(gp)))); + + t.same(gp.getLatitude(), 0.5); + t.same(gp.getLongitude(), 102); + t.same(`${gp.getLatitude()}`, '0.5'); + t.same(`${gp.getLongitude()}`, '102'); + t.same(gp.toString(), '0.5,102'); + t.same(gp.toJSON(), { type: 'Point', coordinates: [102, 0.5] }); + t.same(JSON.stringify(gp), '{"type":"Point","coordinates":[102,0.5]}'); + + try { + gp.test = 1; + t.fail('gp instance is mutable'); + } catch (e) { + t.pass('gp instance is immutable'); + } + + t.end(); +}); + + +test('GeoPoint fromString tests', (t) => { + const gp = GeoPoint.fromString('34.1789335,-118.347594'); + t.true(gp instanceof GeoPoint); + t.true(gp.equals(new GeoPoint(34.1789335, -118.347594))); + t.true(gp.equals(GeoPoint.fromString(`${gp}`))); + t.true(gp.equals(GeoPoint.fromJSON(JSON.stringify(gp)))); + + t.same(gp.getLatitude(), 34.1789335); + t.same(gp.getLongitude(), -118.347594); + t.same(`${gp.getLatitude()}`, '34.1789335'); + t.same(`${gp.getLongitude()}`, '-118.347594'); + t.same(gp.toString(), '34.1789335,-118.347594'); + t.same(gp.toJSON(), { type: 'Point', coordinates: [-118.347594, 34.1789335] }); + t.same(JSON.stringify(gp), '{"type":"Point","coordinates":[-118.347594,34.1789335]}'); + t.end(); +}); + + +test('GeoPoint fromJSON tests', (t) => { + const gp = GeoPoint.fromJSON('{"type":"Point","coordinates":[-118.347594,34.1789335]}'); + t.true(gp instanceof GeoPoint); + t.true(gp.equals(new GeoPoint(34.1789335, -118.347594))); + t.true(gp.equals(GeoPoint.fromString(`${gp}`))); + t.true(gp.equals(GeoPoint.fromJSON(JSON.stringify(gp)))); + + t.same(gp.getLatitude(), 34.1789335); + t.same(gp.getLongitude(), -118.347594); + t.same(`${gp.getLatitude()}`, '34.1789335'); + t.same(`${gp.getLongitude()}`, '-118.347594'); + t.same(gp.toString(), '34.1789335,-118.347594'); + t.same(gp.toJSON(), { type: 'Point', coordinates: [-118.347594, 34.1789335] }); + t.same(JSON.stringify(gp), '{"type":"Point","coordinates":[-118.347594,34.1789335]}'); + t.end(); +}); + + +test('GeoPoint fromJSON(invalid) tests', (t) => { + const invalid = [ + '[-118.347594,34.1789335]', // not even GeoJson + '{"type":"Point","coordinates":[34.1789335,-118.347594]}', // wrong order lat/long + '{"type":"Point","coordinates":[190,-100]}', + 'a,b', + 'nope', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + GeoPoint.fromJSON(value); + t.fail(`GeoPoint.fromJSON accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/Microtime.test.js b/tests/well-known/Microtime.test.js new file mode 100644 index 0000000..9b55aaf --- /dev/null +++ b/tests/well-known/Microtime.test.js @@ -0,0 +1,64 @@ +import test from 'tape'; +import moment from 'moment'; +import Microtime from '../../src/well-known/Microtime'; + +test('Microtime create tests', (t) => { + const m = Microtime.create(); + t.true(m instanceof Microtime); + t.true(m.toDate() instanceof Date); + t.true(m.toMoment() instanceof moment); + t.true(moment.isMoment(m.toMoment())); + t.true(/^[0-9]{16}$/.test(m.toString())); + t.same(m.toString().length, 16); + t.same(`${m}`.length, 16); + + try { + m.test = 1; + t.fail('m instance is mutable'); + } catch (e) { + t.pass('m instance is immutable'); + } + + t.end(); +}); + + +test('Microtime fromString tests', (t) => { + const mString = '1495766080123456'; + const mMoment = moment.unix(1495766080.123456); + const mDate = mMoment.toDate(); + const m = Microtime.fromString(mString); + + t.true(m instanceof Microtime); + t.true(m.equals(Microtime.fromString(mString))); + t.same(m.toString(), mString); + t.same(m.valueOf(), mString); + t.same(m.toJSON(), mString); + t.same(JSON.stringify(m), JSON.stringify(mString)); + t.same(`${m.toNumber()}`, '1495766080.123456'); + t.same(m.toMoment(), mMoment); + t.same(m.toDate(), mDate); + + t.end(); +}); + + +test('Microtime fromDate tests', (t) => { + const mString = '1495766080123000'; + const mMoment = moment.unix(1495766080.123456); + const mDate = mMoment.toDate(); + const m = Microtime.fromDate(mDate); + + t.true(m instanceof Microtime); + t.true(m.equals(Microtime.fromString(mString))); + t.same(m.toString(), mString); + t.same(m.valueOf(), mString); + t.same(m.toJSON(), mString); + t.same(JSON.stringify(m), JSON.stringify(mString)); + t.same(`${m.toNumber()}`, '1495766080.123'); + t.true(m.toMoment().isSame(mMoment)); + t.same(m.toDate().toISOString(), mDate.toISOString()); + t.same(m.toDate().toISOString(), '2017-05-26T02:34:40.123Z'); + + t.end(); +}); diff --git a/tests/well-known/SlugIdentifier.test.js b/tests/well-known/SlugIdentifier.test.js new file mode 100644 index 0000000..7251f8e --- /dev/null +++ b/tests/well-known/SlugIdentifier.test.js @@ -0,0 +1,82 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import SlugIdentifier from '../../src/well-known/SlugIdentifier'; +import SampleSlugIdentifier from '../fixtures/well-known/SampleSlugIdentifier'; + +test('SlugIdentifier tests', (t) => { + const id = new SampleSlugIdentifier('homer-simpson'); + t.true(id instanceof Identifier); + t.true(id instanceof SlugIdentifier); + t.true(id instanceof SampleSlugIdentifier); + t.true(id.equals(SampleSlugIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('SlugIdentifier fromString tests', (t) => { + const slug1 = 'homer-simpson'; + const slug2 = 'bart-simpson'; + const id = SampleSlugIdentifier.fromString(slug1); + t.true(id instanceof Identifier); + t.true(id instanceof SlugIdentifier); + t.true(id instanceof SampleSlugIdentifier); + + t.same(slug1, id.toString()); + t.same(slug1, id.valueOf()); + t.same(slug1, `${id}`); + t.same(JSON.stringify(slug1), JSON.stringify(id)); + t.true(id.equals(SampleSlugIdentifier.fromString(slug1))); + t.false(id.equals(SampleSlugIdentifier.fromString(slug2))); + + t.end(); +}); + + +test('SlugIdentifier create tests', (t) => { + const slug = 'homer-simpson'; + const id = SampleSlugIdentifier.create('Homer Simpson'); + t.true(id instanceof Identifier); + t.true(id instanceof SlugIdentifier); + t.true(id instanceof SampleSlugIdentifier); + + t.same(slug, id.toString()); + t.same(slug, id.valueOf()); + t.same(slug, `${id}`); + t.same(JSON.stringify(slug), JSON.stringify(id)); + t.true(id.equals(SampleSlugIdentifier.fromString(slug))); + + t.end(); +}); + + +test('SlugIdentifier (invalid) tests', (t) => { + const invalid = [ + 'Not a Slug', + '2015/12/25/not-a-simple-slug', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleSlugIdentifier.fromString(value); + t.fail(`SampleSlugIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/TimeUuidIdentifier.test.js b/tests/well-known/TimeUuidIdentifier.test.js new file mode 100644 index 0000000..e5fe326 --- /dev/null +++ b/tests/well-known/TimeUuidIdentifier.test.js @@ -0,0 +1,71 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import TimeUuidIdentifier from '../../src/well-known/TimeUuidIdentifier'; +import SampleTimeUuidIdentifier from '../fixtures/well-known/SampleTimeUuidIdentifier'; + +test('TimeUuidIdentifier generate tests', (t) => { + const id = SampleTimeUuidIdentifier.generate(); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof TimeUuidIdentifier); + t.true(id instanceof SampleTimeUuidIdentifier); + t.true(/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-1[0-9A-Fa-f]{3}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(id)); + t.true(id.equals(SampleTimeUuidIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('TimeUuidIdentifier fromString tests', (t) => { + const idString = 'b385af9a-4413-11e7-a919-92ebcb67fe33'; + const idString2 = 'b385af9a-4413-11e7-a919-92ebcb67fe34'; + const id = SampleTimeUuidIdentifier.fromString(idString); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof TimeUuidIdentifier); + t.true(id instanceof SampleTimeUuidIdentifier); + + t.same(idString, id.toString()); + t.same(idString, id.valueOf()); + t.same(idString, `${id}`); + t.same(JSON.stringify(idString), JSON.stringify(id)); + t.true(id.equals(SampleTimeUuidIdentifier.fromString(idString))); + t.false(id.equals(SampleTimeUuidIdentifier.fromString(idString2))); + + t.end(); +}); + + +test('TimeUuidIdentifier (invalid) tests', (t) => { + const invalid = [ + 'b385af9a-4413-11e7-a919-92ebcb67fe33X', + 'b385af9a-4413-11e7-a919-92ebcb67fe3', + 'b385af9a441311e7a91992ebcb67fe33', + 'nope', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleTimeUuidIdentifier.fromString(value); + t.fail(`SampleTimeUuidIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/UuidIdentifier.test.js b/tests/well-known/UuidIdentifier.test.js new file mode 100644 index 0000000..258fbc7 --- /dev/null +++ b/tests/well-known/UuidIdentifier.test.js @@ -0,0 +1,68 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import SampleUuidIdentifier from '../fixtures/well-known/SampleUuidIdentifier'; + +test('UuidIdentifier generate tests', (t) => { + const id = SampleUuidIdentifier.generate(); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof SampleUuidIdentifier); + t.true(/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(id)); + t.true(id.equals(SampleUuidIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('UuidIdentifier fromString tests', (t) => { + const idString = '4b268351-2445-4d98-a777-b461330d5c7f'; + const idString2 = '4b268351-2445-4d98-a777-b461330d5c7a'; + const id = SampleUuidIdentifier.fromString(idString); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof SampleUuidIdentifier); + + t.same(idString, id.toString()); + t.same(idString, id.valueOf()); + t.same(idString, `${id}`); + t.same(JSON.stringify(idString), JSON.stringify(id)); + t.true(id.equals(SampleUuidIdentifier.fromString(idString))); + t.false(id.equals(SampleUuidIdentifier.fromString(idString2))); + + t.end(); +}); + + +test('UuidIdentifier (invalid) tests', (t) => { + const invalid = [ + '4b268351-2445-4d98-a777-b461330d5c7fX', + '4b268351-2445-4d98-a777-b461330d5c7', + '4b26835124454d98a777b461330d5c7f', + 'nope', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleUuidIdentifier.fromString(value); + t.fail(`SampleUuidIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/dynamic-field-test.js b/tests/well-known/dynamic-field-test.js deleted file mode 100644 index b061381..0000000 --- a/tests/well-known/dynamic-field-test.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -import {expect} from 'chai' -import EmailMessage from '../fixtures/email-message'; -import DynamicField from 'gdbots/pbj/well-known/dynamic-field.js'; - -const TEST_COUNT = 2500; - -describe('dynamic-field-test', function() { - it('add to message', function(done) { - let message = EmailMessage.create(); - let field = DynamicField.createFloatVal('float_val', 3.14); - - message.addToList('dynamic_fields', [field]); - - message.getFromListAt('dynamic_fields', 0).should.eql(field); - - done(); - }); -}); diff --git a/tests/well-known/microtime-test.js b/tests/well-known/microtime-test.js deleted file mode 100644 index 0b53231..0000000 --- a/tests/well-known/microtime-test.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -import {expect} from 'chai' -import DateUtils from 'gdbots/common/util/date-utils.js'; -import StringUtils from 'gdbots/common/util/string-utils.js'; -import Microtime from 'gdbots/pbj/well-known/microtime.js'; - -const TEST_COUNT = 2500; - -describe('microtime-test', function() { - it('check from time-of-day', function(done) { - let i = TEST_COUNT; - do { - let tod = DateUtils.gettimeofday(); - let sec = tod.sec; - let usec = tod.usec; - let str = sec + StringUtils.strPad(tod.usec, 6, '0', 'STR_PAD_LEFT'); - let m = Microtime.fromTimeOfDay(tod); - - expect(sec).to.be.eq(m.getSeconds()); - expect(sec).to.be.eq(Math.floor(m.toDateTime().getTime() / 1000)); - expect(usec).to.be.eq(m.getMicroSeconds()); - expect(str).to.be.eq(m.toString()); - - --i; - } while (i > 0); - - done(); - }); -}); diff --git a/tests/well-known/uuid-identifier-test.js b/tests/well-known/uuid-identifier-test.js deleted file mode 100644 index 6964299..0000000 --- a/tests/well-known/uuid-identifier-test.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -import {expect} from 'chai' -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; - -describe('uuid-identifier-test', function() { - it('using generate()', function(done) { - let id = UuidIdentifier.generate(); - - var v4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - v4Regex.test(id.toString()).should.be.true; - - done(); - }); - - it('generate from string', function(done) { - const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; - - let id = UuidIdentifier.fromString(NAMESPACE_DNS); - - (id.toString() == NAMESPACE_DNS).should.be.true; - - done(); - }); - - it('check equal uuids', function(done) { - const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; - const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; - - let id = UuidIdentifier.fromString(NAMESPACE_DNS); - let id2 = UuidIdentifier.fromString(NAMESPACE_DNS); - let id3 = UuidIdentifier.fromString(NAMESPACE_OID); - - (id.equals(id2)).should.be.true; - (id.equals(id3)).should.be.false; - - done(); - }); -});