├── .gitignore ├── .npmignore ├── AUTHORS ├── LICENSE ├── README.md ├── TUTORIAL.md ├── bin └── yang ├── browser.js ├── example ├── jukebox.coffee └── jukebox.yang ├── index.d.ts ├── index.js ├── package.json ├── schema ├── complex-types.yang ├── experimental │ └── complex-types.yaml ├── iana-crypt-hash.yang ├── ietf-inet-types.yang ├── ietf-yang-library.js ├── ietf-yang-library@2016-06-21.yang ├── ietf-yang-types.yang ├── yang-graph-query.yang └── yang-meta-types.yang ├── src ├── container.litcoffee ├── context.coffee ├── element.litcoffee ├── expression.coffee ├── extension.coffee ├── index.coffee ├── lang │ ├── arguments.coffee │ ├── extensions.coffee │ └── typedefs.coffee ├── list.coffee ├── method.litcoffee ├── model.litcoffee ├── node.coffee ├── notification.coffee ├── property.litcoffee ├── store.coffee ├── typedef.coffee ├── xpath.coffee └── yang.litcoffee ├── test ├── ChangeLog ├── extension │ ├── choice.coffee │ ├── container.coffee │ ├── extension.coffee │ ├── grouping.coffee │ ├── leaf-list.coffee │ ├── leaf.coffee │ ├── list.coffee │ ├── module.coffee │ ├── rpc.coffee │ └── type.coffee ├── package.json ├── yang-1.1 │ └── type.coffee ├── yang-compliance-coverage.coffee ├── yang-compliance-coverage.md ├── yang-example-jukebox.coffee ├── yang-import-resolve.coffee ├── yang-property.coffee └── yang-transaction.coffee └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | *~ 40 | 41 | # ignore generated *.js files inside lib and dist 42 | lib/* 43 | dist/* 44 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | src* 3 | test* 4 | 5 | 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Special thanks to the following contributors providing PRs to this project! 2 | 3 | Henrik Hugo 4 | Jiang Huajun 5 | Quan Tang 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yang-js 2 | 3 | YANG parser and evaluator 4 | 5 | Super light-weight and fast. Produces adaptive JS objects bound by 6 | YANG schema expressions according to 7 | [RFC 6020](http://tools.ietf.org/html/rfc6020) 8 | specifications. Composes dynamic YANG schema expressions by analyzing 9 | arbitrary JS objects. 10 | 11 | [![NPM Version][npm-image]][npm-url] 12 | [![NPM Downloads][downloads-image]][downloads-url] 13 | 14 | ```javascript 15 | const Yang = require('yang-js'); 16 | const schema = ` 17 | container foo { 18 | leaf a { type string; } 19 | leaf b { type uint8; } 20 | list bar { 21 | key "b1"; 22 | leaf b1 { type uint16; } 23 | container blob; 24 | } 25 | } 26 | `; 27 | const model = Yang(schema)({ 28 | foo: { 29 | a: 'apple', 30 | b: 10, 31 | bar: [ 32 | { b1: 100 } 33 | ], 34 | } 35 | }); 36 | ``` 37 | 38 | ## Installation 39 | 40 | ```bash 41 | $ npm install yang-js 42 | ``` 43 | 44 | For development/testing, clone from repo and initialize: 45 | 46 | ```bash 47 | $ git clone https://github.com/corenova/yang-js 48 | $ cd yang-js 49 | $ npm install 50 | ``` 51 | 52 | When using with the web browser, you can generate a *browserified* version 53 | from the repo (or find it in `dist/yang.js`): 54 | 55 | ```bash 56 | $ npm run prepublishOnly 57 | ``` 58 | 59 | It will produce `dist/yang.js` that can then be used directly 60 | inside the web browser. 61 | 62 | ## Features 63 | 64 | * Robust parsing 65 | * Focus on high performance 66 | * Extensive test coverage 67 | * Flexible control logic binding 68 | * Powerful XPATH expressions 69 | * Isomorphic runtime 70 | * Adaptive validations 71 | * Dynamic schema generation 72 | * Granular event subscriptions 73 | 74 | Please note that `yang-js` is not a code-stub generator based on YANG 75 | schema input. It directly embeds YANG schema compliance into ordinary 76 | JS objects as well as generates YANG schema(s) from ordinary JS 77 | objects. 78 | 79 | ## Quick Start 80 | 81 | Here's a quick example for using this module: 82 | 83 | ```javascript 84 | const Yang = require('yang-js'); 85 | const schema = ` 86 | container foo { 87 | leaf a { type string; } 88 | leaf b { type uint8; } 89 | } 90 | `; 91 | const model = Yang.parse(schema).eval({ 92 | foo: { 93 | a: 'apple', 94 | b: 10, 95 | } 96 | }); 97 | ``` 98 | 99 | The example above uses the *explict* long-hand version of using this 100 | module, which uses the [parse](./src/yang.litcoffee#parse-schema) 101 | method to generate the [Yang expression](./src/yang.litcoffee) and 102 | immediately perform an [eval](./src/yang.litcoffee#eval-data-opts) 103 | using the [Yang expression](./src/yang.litcoffee) for the passed-in JS 104 | data object. 105 | 106 | Since the above is a common usage pattern sequence, this module also 107 | provides a *cast-style* short-hand version as follows: 108 | 109 | ```javascript 110 | const model = Yang(schema)({ 111 | foo: { 112 | a: 'apple', 113 | b: 10, 114 | } 115 | }); 116 | ``` 117 | 118 | It is functionally equivalent to the *explicit* version but provides 119 | cleaner syntactic expression regarding how the data object is being 120 | *cast* with the `Yang` expression to get back a new schema-driven 121 | object. 122 | 123 | Once you have the `model` instance, you can directly interact with its 124 | properties and see the schema enforcement and validations in action. 125 | 126 | As the above example illustrates, the `yang-js` module takes a 127 | free-form approach when dealing with YANG schema statements. You can 128 | use **any** YANG statement as the top of the expression and 129 | [parse](./src/yang.litcoffee#parse-schema) it to return a 130 | corresponding YANG expression instance. However, only YANG expressions 131 | that represent a data node element will 132 | [eval](./src/yang.litcoffee#eval-data-opts) to generate a new 133 | [Property](./src/property.litcoffee) instance. Also, only `module` 134 | schemas will [eval](./src/yang.litcoffee#eval-data-opts) to generate a 135 | new [Model](./src/model.litcoffee) instance. 136 | 137 | ## Reference Guides 138 | 139 | - [Getting Started Guide](./TUTORIAL.md) 140 | - [Storing Data](http://github.com/corenova/yang-store) 141 | - [Expressing Interfaces](http://github.com/corenova/yang-express) 142 | - [Automating Documentation](http://github.com/corenova/yang-swagger) 143 | - [Coverage Report](./test/yang-compliance-coverage.md) 144 | 145 | ## Bundled YANG Modules 146 | 147 | - [iana-crypt-hash.yang](./schema/iana-crypt-hash.yang) 148 | - [ietf-yang-types.yang](./schema/ietf-yang-types.yang) 149 | - [ietf-inet-types.yang](./schema/ietf-inet-types.yang) 150 | - [ietf-yang-library.yang](./schema/ietf-yang-library.yang) ([bindings](./src/module/ietf-yang-library.coffee)) 151 | - [yang-meta-types.yang](./schema/yang-meta-types.yang) 152 | 153 | Please refer to 154 | [Working with Multiple Schemas](./TUTORIAL.md#working-with-multiple-schemas) 155 | section of the [Getting Started Guide](./TUTORIAL.md) for usage 156 | examples. 157 | 158 | ## API 159 | 160 | Below are the list of methods provided by the `yang-js` module. You 161 | can click on each method entry for detailed info on usage. 162 | 163 | ### Main module 164 | 165 | The following operations are available from `require('yang-js')`. 166 | 167 | - [parse (schema)](./src/yang.litcoffee#parse-schema) 168 | - [compose (data)](./src/yang.litcoffee#compose-data-opts) 169 | - [resolve (name)](./src/yang.litcoffee#resolve-from-name) 170 | - [import (name)](./src/yang.litcoffee#import-name-opts) 171 | 172 | Please note that when you load the main module, it will attempt to 173 | automatically register `.yang` extension into `require.extensions`. 174 | 175 | ### Yang instance 176 | 177 | The [Yang](./src/yang.litcoffee) instance is created from 178 | `parse/compose` operations from the main module. 179 | 180 | - [compile ()](./src/yang.litcoffee#compile) 181 | - [bind (obj)](./src/yang.litcoffee#bind-obj) 182 | - [eval (data)](./src/yang.litcoffee#eval-data-opts) 183 | - [extends (schema)](./src/yang.litcoffee#extends-schema) 184 | - [locate (ypath)](./src/yang.litcoffee#locate-ypath) 185 | - [toString ()](./src/yang.litcoffee#tostring-opts) 186 | - [toJSON ()](./src/yang.litcoffee#tojson) 187 | 188 | ### Property instance 189 | 190 | The [Property](./src/property.litcoffee) instances are created during 191 | [Yang.eval](./src/yang.litcoffee#eval-data-opts) operation and are 192 | bound to every *node element* defined by the underlying 193 | [Yang](./src/yang.litcoffee) schema expression. 194 | 195 | - [join (obj)](./src/property.litcoffee#join-obj) 196 | - [get (pattern)](./src/property.litcoffee#get-pattern) 197 | - [set (value)](./src/property.litcoffee#set-value) 198 | - [merge (value)](./src/property.litcoffee#merge-value) 199 | - [create (value)](./src/property.litcoffee#create-value) 200 | - [remove ()](./src/property.litcoffee#remove-value) 201 | - [find (pattern)](./src/property.litcoffee#find-pattern) 202 | - [in (pattern)](./src/model.litcoffee#in-pattern) 203 | - [do (args...)](./src/property.litcoffee#do-args) 204 | - [toJSON ()](./src/property.litcoffee#tojson) 205 | 206 | Please refer to [Property](./src/property.litcoffee) for a list of all 207 | available properties on this instance. 208 | 209 | ### Model instance 210 | 211 | The [Model](./src/model.litcoffee) instance is created from 212 | [Yang.eval](./src/yang.litcoffee#eval-data-opts) operation for 213 | YANG `module` schema and aggregates 214 | [Property](./src/property.litcoffee) instances. 215 | 216 | This instance also *inherits* all [Property](./src/property.litcoffee) 217 | methods and properties. 218 | 219 | - [access (model)](./src/model.litcoffee#access-model) 220 | - [enable (feature)](./src/model.litcoffee#enable-feature) 221 | - [save ()](./src/model.litcoffee#save) 222 | - [rollback ()](./src/model.litcoffee#rollback) 223 | - [on (event)](./src/model.litcoffee#on-event) 224 | - [do (path, args...)](./src/model.litcoffee#do-path-args) 225 | 226 | Please refer to [Model](./src/model.litcoffee) for a list of all 227 | available properties on this instance. 228 | 229 | ## Examples 230 | 231 | **Jukebox** is a simple example YANG module extracted from 232 | [RFC 6020](http://tools.ietf.org/html/rfc6020). This example 233 | implementation is included in this repository's [example](./example) 234 | folder and exercised as part of the test suite. It demonstrates use of 235 | the [register](./src/yang.litcoffee#register) and 236 | [import](./src/yang.litcoffee#import-name-opts) facilities for 237 | loading the YANG schema file and binding various control logic 238 | behavior. 239 | 240 | - [YANG Schema](./example/jukebox.yang) 241 | - [Schema Bindings](./example/jukebox.coffee) 242 | 243 | **Promise** is a resource reservation module implemented for 244 | [OPNFV](http://opnfv.org). This example implementation is hosted in a 245 | separate GitHub repository 246 | [opnfv/promise](http://github.com/opnfv/promise) and utilizes 247 | `yang-js` for the complete implementation. It demonstrates use of 248 | multiple YANG data models in modeling complex systems. Please be sure 249 | to [check it out](http://github.com/opnfv/promise) to learn more about 250 | advanced usage of `yang-js`. 251 | 252 | ## Tests 253 | 254 | To run the test suite, first install the dependencies, then run `npm 255 | test`: 256 | ``` 257 | $ npm install 258 | $ npm test 259 | ``` 260 | 261 | Also refer to [Compliance Report](./test/yang-compliance-coverage.md) 262 | for the latest [RFC 6020](http://tools.ietf.org/html/rfc6020) YANG 263 | specification compliance. There's also **active** effort to support 264 | the latest **YANG 1.1** draft specifications. You can take a look at 265 | the *mocha* test suite in the [test](./test) directory for compliance 266 | coverage unit-tests and other examples. 267 | 268 | ## License 269 | [Apache 2.0](LICENSE) 270 | 271 | This software is brought to you by 272 | [Corenova Technologies](http://www.corenova.com). We'd love to hear 273 | your feedback. Please feel free to reach me at 274 | anytime with questions, suggestions, etc. 275 | 276 | [npm-image]: https://img.shields.io/npm/v/yang-js.svg 277 | [npm-url]: https://npmjs.org/package/yang-js 278 | [downloads-image]: https://img.shields.io/npm/dt/yang-js.svg 279 | [downloads-url]: https://npmjs.org/package/yang-js 280 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # Getting Started with yang-js 2 | 3 | This guide/tutorial will provide various examples around usage of 4 | `yang-js` library for working with YANG schemas and generated 5 | Models. It will get better soon... :-) 6 | 7 | ## Table of Contents 8 | 9 | - [Terminology](#terminology) 10 | - [Working with Yang Schema](#working-with-yang-schema) 11 | - [Composite Type](#composite-type) 12 | - [Schema Extension](#schema-extension) 13 | - [Schema Conversion](#schema-conversion) 14 | - [Schema Composition](#schema-composition) 15 | - [Schema Binding](#schema-binding) 16 | - [Working with Multiple Schemas](#working-with-multiple-schemas) 17 | - [Preload Dependency](#preload-dependency) 18 | - [Automatic Resolution](#automatic-resolution) 19 | - [External Package Bundle](#external-package-bundle) 20 | - [Internal Package Bunlde](#internal-package-bundle) 21 | - [Working with Models](#working-with-models) 22 | - [Model Events](#model-events) 23 | 24 | ## Terminology 25 | 26 | Here's a collection of commonly used terms and their definitions 27 | within this module and other related modules: 28 | 29 | - **Schema**: A descriptive resource that expresses the data 30 | hierarchy, constraints, and behavior of a data model 31 | - **Model**: An instance of Schema evaluated with data 32 | - **Component**: An implementation resource that associates control 33 | logic bindings to a Model 34 | 35 | ## Working with Yang Schema 36 | 37 | ### Composite Type 38 | 39 | A handy convention is to define/save the generated 40 | [Yang.eval](./src/yang.litcoffee#main-constructor) function as a 41 | **Composite Type** definition and re-use for creating multiple 42 | adaptive schema objects: 43 | 44 | ```coffeescript 45 | # coffeescript 46 | FooType = (Yang schema) 47 | foo1 = (FooType) { 48 | foo: 49 | a: 'apple' 50 | b: 10 51 | } 52 | foo2 = (FooType) { 53 | foo: 54 | a: 'banana' 55 | b: 20 56 | } 57 | ``` 58 | 59 | ### Schema Extension 60 | 61 | ```javascript 62 | // javascript 63 | var schema = Yang.parse('container foo { leaf a; }'); 64 | var model = schema.eval({ foo: { a: 'bar' } }); 65 | // try assigning a new arbitrary property 66 | model.foo.b = 'hello'; 67 | console.log(model.foo.b); 68 | // returns: 'hello' (without validation since not part of schema) 69 | ``` 70 | 71 | Here comes the magic: 72 | 73 | ```javascript 74 | // extend the previous container foo expression with an additional leaf 75 | schema.extends('leaf b { type uint8; }') 76 | model.foo.b = 'hello'; // throws error since not uint8 type 77 | ``` 78 | 79 | The [extends](./src/yang.litcoffee#extends-schema) mechanism provides 80 | interesting programmatic approach to *dynamically* modify a given 81 | `Yang` expression over time on a running system. 82 | 83 | ### Schema Conversion 84 | 85 | ```yang 86 | module foo { 87 | description "A Foo Example"; 88 | container bar { 89 | leaf a { 90 | type string; 91 | } 92 | leaf b { 93 | type uint8; 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | Result of [parse](./src/yang.litcoffee#parse-schema) looks like: 100 | 101 | ```js 102 | { kind: 'module', 103 | tag: 'foo', 104 | description: 105 | { kind: 'description', 106 | tag: 'A Foo Example' }, 107 | container: 108 | [ { kind: 'container', 109 | tag: 'bar', 110 | leaf: [Object] } ] } 111 | ``` 112 | 113 | When the above `Yang` expression is converted 114 | [toJSON](./src/yang.litcoffee#tojson): 115 | 116 | ```js 117 | { module: 118 | { foo: 119 | { description: 'A Foo Example', 120 | container: 121 | { bar: { leaf: { a: [Object], b: [Object] } } } } } } 122 | ``` 123 | 124 | ### Schema Composition 125 | 126 | Below example demonstrates typical use: 127 | 128 | ```coffeescript 129 | # coffeescript 130 | Yang = require 'yang-js' 131 | schema = Yang.compose { 132 | bar: 133 | a: 'hello' 134 | b: 123 135 | }, tag: 'foo' 136 | console.log schema.toString() 137 | ``` 138 | ```javascript 139 | // javascript 140 | const Yang = require('yang-js'); 141 | const schema = Yang.compose({ 142 | bar: { 143 | a: 'hello', 144 | b: 123, 145 | } 146 | }, { tag: 'foo' }) 147 | 148 | The output of [schema.toString()](./src/yang.litcoffee#tostring) looks 149 | as follows: 150 | 151 | ``` 152 | container foo { 153 | container bar { 154 | leaf a { type string; } 155 | leaf b { type number; } 156 | } 157 | } 158 | ``` 159 | 160 | Please note that [compose](../src/yang.litcoffee#compose-data) 161 | detected the top-level YANG construct to be a simple `container` 162 | instead of a `module`. It will only auto-detect as a `module` if any 163 | of the properties of the top-level object contains a `function` or the 164 | passed-in object itself is a `named function` with additional 165 | properties. 166 | 167 | Below example will auto-detect as `module` since a simple `container` 168 | cannot contain a *function* as one of its properties. 169 | 170 | ```coffeescript 171 | # coffeescript 172 | Yang = require 'yang-js' 173 | obj = 174 | bar: 175 | a: 'hello' 176 | b: 123 177 | test: -> 178 | schema = Yang.compose obj, { tag: 'foo' } 179 | console.log schema.toString() 180 | ``` 181 | 182 | This is a very handy facility to dynamically discover YANG schema 183 | mapping for any arbitrary asset being used (even NPM modules) so that 184 | you can qualify/validate the target resource for schema compliance. 185 | 186 | You can also **override** the detected YANG construct as follows: 187 | 188 | ```coffeescript 189 | Yang = require 'yang-js' 190 | obj = 191 | bar: 192 | a: 'hello' 193 | b: 123 194 | schema = Yang.compose obj, { tag: 'foo', kind: 'module' } 195 | console.log schema.toString() 196 | ``` 197 | 198 | When you *manually* alter the `Yang` expression instance, it will 199 | internally trigger a check for scope validation and reject if the the 200 | change will render the current schema invalid. Basically, you can't 201 | simply change a `container` that contains other elements into a `leaf` 202 | or any other arbitrary kind. 203 | 204 | ### Schema Binding 205 | 206 | ```coffeescript 207 | # coffeescript 208 | Yang = require 'yang-js' 209 | schema = """ 210 | module foo { 211 | feature hello; 212 | container bar { 213 | leaf readonly { 214 | config false; 215 | type boolean; 216 | } 217 | } 218 | rpc test; 219 | } 220 | """ 221 | schema = Yang.parse(schema).bind { 222 | 'feature(hello)': -> true # provide some capability 223 | '/foo:bar/readonly': -> true 224 | '/test': -> 'success' 225 | } 226 | ``` 227 | ```javascript 228 | // javascript 229 | const Yang = require('yang-js'); 230 | let schema = ` 231 | module foo { 232 | feature hello; 233 | container bar { 234 | leaf readonly { 235 | config false; 236 | type boolean; 237 | } 238 | } 239 | rpc test; 240 | } 241 | ` 242 | schema = Yang.parse(schema).bind({ 243 | 'feature(hello)': () => true, 244 | '/foo:bar/readonly': () => true, 245 | '/test': () => 'success' 246 | 247 | }); 248 | ``` 249 | 250 | In the above example, a `key/value` object was passed-in to the 251 | [bind](./src/yang.litcoffee#bind-obj) method where the `key` is a 252 | string that will be mapped to a Yang Expression contained within the 253 | expression being bound. It accepts XPATH-like expression which will be 254 | used to locate the target expression within the schema. The `value` of 255 | the binding must be a JS function, otherwise it will be *silently* 256 | ignored. 257 | 258 | You can also [bind](./src/yang.litcoffee#bind-obj) a function directly 259 | to a given Yang Expression instance as follows: 260 | 261 | ```coffeescript 262 | Yang = require 'yang-js' 263 | schema = Yang.parse('rpc test;').bind -> @output = "ok" 264 | ``` 265 | 266 | Please note that calling [bind](./src/yang.litcoffee#bind-obj) 267 | more than once on a given [Yang](./src/yang.litcoffee) expression 268 | will *replace* any prior binding. 269 | 270 | ## Working with Multiple Schemas 271 | 272 | ### Preload Dependency 273 | 274 | You can utilize 275 | [Yang.import](./src/yang.litcoffee#import-name-opts) to load 276 | the dependency module into the [Yang](./src/yang.litcoffee) 277 | compiler: 278 | 279 | ```js 280 | const Yang = require('yang-js'); 281 | Yang.import('/some/path/to/dependency.yang'); 282 | ``` 283 | 284 | You can also use built-in `require()` directly: 285 | 286 | ```js 287 | require('yang-js'); 288 | require('/some/path/to/dependency.yang'); 289 | ``` 290 | 291 | The pre-load approach is *iterative* in that you would need to ensure 292 | dependency YANG modules are loaded in the proper order of the nested 293 | dependency chain. 294 | 295 | 296 | ### Automatic Resolution 297 | 298 | When utilizing `Yang.import` or `register/require`, the 299 | [Yang](./src/yang.litcoffee) compiler internally utilizes 300 | [Yang.resolve](./src/yang.litcoffee#resolve-from-name) to attempt 301 | to locate dependency modules automatically. 302 | 303 | It first checks local `package.json` to resolve the dependency via 304 | [External](#external-package-bundle) or 305 | [Internal](#internal-package-bundle) definitions. If not found, it 306 | will try to locate the `some-dependency.yang` in the same directory 307 | that the *dependent* schema is being required. 308 | 309 | ### External Package Bundle 310 | 311 | To utilize YANG modules bundled in an external package inside your own 312 | app, you can add a section inside your local `package.json` as 313 | follows: 314 | 315 | ```json 316 | { 317 | "models": { 318 | "ietf-yang-types": "yang-js", 319 | "ietf-inet-types": "yang-js", 320 | "yang-store": "yang-js" 321 | } 322 | } 323 | ``` 324 | 325 | This will enable `Yang.resolve` and `Yang.import` to locate these 326 | YANG modules from the `yang-js` package. 327 | 328 | ### Interal Package Bundle 329 | 330 | To allow external packages to perform 331 | [Automatic Resolution](#automatic-resolution) of modules being 332 | provided by your app, as well as for your own app to resolve local 333 | dependencies, you can add a section inside your `package.json` as 334 | follows: 335 | 336 | ```json 337 | { 338 | "models": { 339 | "my-module": "./some/path/to/my-module.yang", 340 | "my-bound-module": "./lib/my-bound-module.js" 341 | } 342 | } 343 | ``` 344 | 345 | Please note that you can reference a YANG schema file directly as well 346 | as a **JavaScript file** which exports a 347 | [Yang](./src/yang.litcoffee) schema instance. The second approach 348 | is useful for exporting **bound** schemas (see 349 | [Schema Binding](#schema-binding)) which contains function bindings on 350 | the YANG schema. 351 | 352 | ## Working with Models 353 | 354 | ### Model Events 355 | 356 | ```coffeescript 357 | # coffeescript 358 | Yang = require 'yang-js' 359 | schema = """ 360 | module foo { 361 | list bar { 362 | container obj { 363 | leaf a { type string; } 364 | leaf b { type uint8; } 365 | } 366 | } 367 | } 368 | """ 369 | model = (Yang schema) { 370 | 'foo:bar': [ 371 | { obj: { a: 'apple', b: 10 } } 372 | { obj: { a: 'orange, b: 20 } } 373 | ] 374 | } 375 | model.on 'update', (prop, prev) -> 376 | # do something with 'prop' 377 | ``` 378 | 379 | The example above will register an event listener using 380 | [Model.on](./src/model.litcoffee#on-event) to trigger whenever the 381 | data state of the `model` is updated. 382 | 383 | You can also utilize XPATH expressions to only listen for specific 384 | events occurring inside the data tree: 385 | 386 | ```coffeescript 387 | model.on 'update', '/foo:bar/obj/a', (prop, prev) -> 388 | console.log "the property 'a' changed on one of the elements in the 'list bar'" 389 | model.in('/foo:bar[0]/obj/a').set 'pineapple' # trigger event 390 | model.in('/foo:bar[1]/obj/a').set 'grape' # trigger event 391 | ``` 392 | 393 | -------------------------------------------------------------------------------- /bin/yang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // -*- mode: js-mode; -*- 3 | 4 | var argv = require('minimist')(process.argv.slice(2), { 5 | boolean: [ 'help' ], 6 | alias: { 7 | help: 'h', 8 | output: 'o' 9 | } 10 | }); 11 | 12 | if (argv.h === true) { 13 | var help; 14 | help = " Usage: yang [options] file...\n\n"; 15 | help += " Options:\n"; 16 | help += " -o, --output Place output into \n"; 17 | console.info(help); 18 | process.exit(); 19 | } 20 | 21 | console.log('coming soon...'); 22 | 23 | // TBD 24 | // require('..') 25 | // .load(argv._) // returns new YIN instance 26 | // .dump( function (str) { 27 | // if (argv.o != null) { 28 | // require('fs').writeFile(argv.o, str, 'utf8'); 29 | // } else { 30 | // console.info(str); 31 | // } 32 | // }) 33 | 34 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Yang, Store, Model, Container, Property } = require('./lib'); 4 | 5 | // initialize with YANG 1.1 extensions and typedefs 6 | Yang.use(require('./lib/lang/extensions')); 7 | Yang.use(require('./lib/lang/typedefs')); 8 | 9 | const parseYangSchema = (...args) => { 10 | const [ schema, spec ] = args.flat(); 11 | return Yang.parse(schema).bind(spec); 12 | } 13 | 14 | exports = Yang; 15 | exports.yang = parseYangSchema; 16 | exports.Yang = Yang; 17 | exports.Store = Store; 18 | exports.Model = Model; 19 | exports.Container = Container; 20 | exports.Property = Property; 21 | 22 | module.exports = global.Yang = exports; 23 | 24 | -------------------------------------------------------------------------------- /example/jukebox.coffee: -------------------------------------------------------------------------------- 1 | # Example: jukebox module implementation 2 | require('..') 3 | 4 | module.exports = require('./jukebox.yang').bind { 5 | 6 | # bind behavior to config: false read-only elements 7 | '/jukebox/library/artist-count': get: (ctx) -> ctx.get('../artist')?.length ? 0 8 | '/jukebox/library/album-count': get: (ctx) -> ctx.get('../artist/album')?.length ? 0 9 | '/jukebox/library/song-count': get: (ctx) -> ctx.get('../artist/album/song')?.length ? 0 10 | 11 | '/play': (ctx, input) -> 12 | song = ctx.get ( 13 | "/jukebox/playlist['#{input.playlist}']/" + 14 | "song['#{input['song-number']}']" 15 | ) 16 | unless song? 17 | throw ctx.error "selected song #{input['song-number']} not found in library" 18 | else if song.id instanceof Error 19 | throw ctx.error song.id 20 | else 21 | return "ok" 22 | } 23 | -------------------------------------------------------------------------------- /example/jukebox.yang: -------------------------------------------------------------------------------- 1 | module example-jukebox { 2 | 3 | namespace "http://example.com/ns/example-jukebox"; 4 | prefix "jbox"; 5 | organization "Example, Inc."; 6 | contact "support at example.com"; 7 | description "Example Jukebox Data Model Module"; 8 | revision "2015-04-04" { 9 | description "Initial version."; 10 | reference "example.com document 1-4673"; 11 | } 12 | 13 | identity genre { 14 | description "Base for all genre types"; 15 | } 16 | 17 | // abbreviated list of genre classifications 18 | identity alternative { 19 | base genre; 20 | description "Alternative music"; 21 | } 22 | identity blues { 23 | base genre; 24 | description "Blues music"; 25 | } 26 | identity country { 27 | base genre; 28 | description "Country music"; 29 | } 30 | identity jazz { 31 | base genre; 32 | description "Jazz music"; 33 | } 34 | identity pop { 35 | base genre; 36 | description "Pop music"; 37 | } 38 | identity rock { 39 | base genre; 40 | description "Rock music"; 41 | } 42 | 43 | container jukebox { 44 | presence 45 | "An empty container indicates that the jukebox 46 | service is available"; 47 | 48 | description 49 | "Represents a jukebox resource, with a library, playlists, 50 | and a play operation."; 51 | 52 | container library { 53 | description "Represents the jukebox library resource."; 54 | 55 | list artist { 56 | key name; 57 | 58 | description 59 | "Represents one artist resource within the 60 | jukebox library resource."; 61 | 62 | leaf name { 63 | type string { 64 | length "1 .. max"; 65 | } 66 | description "The name of the artist."; 67 | } 68 | 69 | list album { 70 | key name; 71 | 72 | description 73 | "Represents one album resource within one 74 | artist resource, within the jukebox library."; 75 | 76 | leaf name { 77 | type string { 78 | length "1 .. max"; 79 | } 80 | description "The name of the album."; 81 | } 82 | 83 | leaf genre { 84 | type identityref { base genre; } 85 | description 86 | "The genre identifying the type of music on 87 | the album."; 88 | } 89 | 90 | leaf year { 91 | type uint16 { 92 | range "1900 .. max"; 93 | } 94 | description "The year the album was released"; 95 | } 96 | 97 | container admin { 98 | description 99 | "Administrative information for the album."; 100 | leaf label { 101 | type string; 102 | description "The label that released the album."; 103 | } 104 | leaf catalogue-number { 105 | type string; 106 | description "The album's catalogue number."; 107 | } 108 | } 109 | 110 | list song { 111 | key name; 112 | 113 | description 114 | "Represents one song resource within one 115 | album resource, within the jukebox library."; 116 | 117 | leaf name { 118 | type string { 119 | length "1 .. max"; 120 | } 121 | description "The name of the song"; 122 | } 123 | leaf location { 124 | type string; 125 | mandatory true; 126 | description 127 | "The file location string of the 128 | media file for the song"; 129 | } 130 | leaf format { 131 | type string; 132 | description 133 | "An identifier string for the media type 134 | for the file associated with the 135 | 'location' leaf for this entry."; 136 | } 137 | leaf length { 138 | type uint32; 139 | units "seconds"; 140 | description 141 | "The duration of this song in seconds."; 142 | } 143 | } // end list 'song' 144 | } // end list 'album' 145 | } // end list 'artist' 146 | 147 | leaf artist-count { 148 | type uint32; 149 | units "artists"; 150 | config false; 151 | description "Number of artists in the library"; 152 | } 153 | leaf album-count { 154 | type uint32; 155 | units "albums"; 156 | config false; 157 | description "Number of albums in the library"; 158 | } 159 | leaf song-count { 160 | type uint32; 161 | units "songs"; 162 | config false; 163 | description "Number of songs in the library"; 164 | } 165 | } // end library 166 | 167 | list playlist { 168 | key name; 169 | 170 | description 171 | "Example configuration data resource"; 172 | 173 | leaf name { 174 | type string; 175 | description 176 | "The name of the playlist."; 177 | } 178 | leaf description { 179 | type string; 180 | description 181 | "A comment describing the playlist."; 182 | } 183 | list song { 184 | key index; 185 | ordered-by user; 186 | 187 | description 188 | "Example nested configuration data resource"; 189 | 190 | leaf index { // not really needed 191 | type uint32; 192 | description 193 | "An arbitrary integer index for this playlist song."; 194 | } 195 | leaf id { 196 | type leafref { 197 | path "/jbox:jukebox/jbox:library/jbox:artist/" + 198 | "jbox:album/jbox:song/jbox:name"; 199 | } 200 | mandatory true; 201 | description 202 | "Song identifier. Must identify an instance of 203 | /jukebox/library/artist/album/song/name."; 204 | } 205 | } 206 | } 207 | 208 | container player { 209 | description 210 | "Represents the jukebox player resource."; 211 | 212 | leaf gap { 213 | type decimal64 { 214 | fraction-digits 1; 215 | range "0.0 .. 2.0"; 216 | } 217 | units "tenths of seconds"; 218 | description "Time gap between each song"; 219 | } 220 | } 221 | } 222 | 223 | rpc play { 224 | description "Control function for the jukebox player"; 225 | input { 226 | leaf playlist { 227 | type string; 228 | mandatory true; 229 | description "playlist name"; 230 | } 231 | leaf song-number { 232 | type uint32; 233 | mandatory true; 234 | description "Song number in playlist to play"; 235 | } 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for yang-js 2 | // Project: yang-js 3 | // Definitions by: quan.tang 4 | import EventEmitter = NodeJS.EventEmitter; 5 | 6 | export function parse(schema: string): YangInstance; 7 | export function compose(data: any): YangInstance; 8 | export function resolve(name: any): YangInstance; 9 | export function clear(): void; 10 | 11 | 12 | export interface YangInstance { 13 | bind(obj: any): this; 14 | eval(data?: any): YangModel; 15 | validate(data: any): any; 16 | extends(schema: string): this; 17 | } 18 | 19 | export interface YangModel extends YangProperty { 20 | access(model: string): YangModel; 21 | on(event: string, path: string, callback: any): EventEmitter; 22 | } 23 | 24 | export interface YangProperty { 25 | name: string; 26 | parent: YangProperty; 27 | path: YangXPath; 28 | children: YangProperty[]; 29 | schema: any; 30 | change: any[]; 31 | 32 | get(key?: string): any; 33 | set(value: any): this; 34 | merge(value: any): this; 35 | create(value: any): this; 36 | detach(): this; 37 | find(pattern?: string): YangProperty; 38 | in(pattern?: string): YangProperty; 39 | do(): Promise; 40 | toJSON(tag?: boolean): any; 41 | } 42 | 43 | export interface YangXPath { 44 | tail: YangXPath; 45 | contains(path: string): boolean; 46 | locate(path: string): YangXPath; 47 | } 48 | 49 | export function _import(name: string): YangInstance; 50 | export {_import as import}; 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Yang, Store, Model, Container, Property } = require('./lib'); 2 | 3 | // initialize with YANG 1.1 extensions and typedefs 4 | Yang.use(require('./lib/lang/extensions')); 5 | Yang.use(require('./lib/lang/typedefs')); 6 | 7 | // extend with NodeJS filesystem related capabilities 8 | Yang.resolve = require('./lib/node').resolve; 9 | Yang.import = require('./lib/node').import; 10 | 11 | // automatically register if require.extensions available 12 | // may be deprecated in the future but hasn't happened in a while... 13 | if (require.extensions && !require.extensions['.yang']) { 14 | require.extensions['.yang'] = (m, filename) => { 15 | m.exports = Yang.import(filename); 16 | }; 17 | } 18 | 19 | const parseYangSchema = (...args) => { 20 | const [ schema, spec ] = args.flat(); 21 | return Yang.parse(schema).bind(spec); 22 | } 23 | 24 | exports = Yang; 25 | exports.yang = parseYangSchema; 26 | exports.Yang = Yang; 27 | exports.Store = Store; 28 | exports.Model = Model; 29 | exports.Container = Container; 30 | exports.Property = Property; 31 | 32 | module.exports = exports; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yang-js", 3 | "version": "0.24.69", 4 | "description": "YANG parser and evaluator", 5 | "keywords": [ 6 | "yang", 7 | "compile", 8 | "compose", 9 | "parse", 10 | "expression", 11 | "require", 12 | "model", 13 | "schema", 14 | "adaptive", 15 | "validate", 16 | "object", 17 | "rfc6020" 18 | ], 19 | "author": "Peter Lee (http://corenova.com)", 20 | "homepage": "https://github.com/corenova/yang-js", 21 | "license": "Apache-2.0", 22 | "repository": "corenova/yang-js", 23 | "main": "index.js", 24 | "types": "index.d.ts", 25 | "browser": "browser.js", 26 | "yang": { 27 | "search": [ 28 | "./schema" 29 | ], 30 | "order": [ 31 | ".js", 32 | ".yang" 33 | ] 34 | }, 35 | "dependencies": { 36 | "debug": "^4.1.1", 37 | "delegates": "^1.0.0", 38 | "indent-string": "^2.1.0", 39 | "lodash.clonedeep": "^4.5.0", 40 | "stacktrace-parser": "^0.1.4", 41 | "xparse": "^1.0.0", 42 | "yang-parser": "^0.2.1" 43 | }, 44 | "devDependencies": { 45 | "babel-preset-es2015": "^6.24.0", 46 | "babelify": "^7.3.0", 47 | "brfs": "^1.4.3", 48 | "browserify": "^13.1.0", 49 | "chai": "^4.2.0", 50 | "chai-as-promised": "^7.1.1", 51 | "coffeescript": "2", 52 | "minifyify": "^7.3.5", 53 | "mocha": "^7.1.2", 54 | "rimraf": "^2.5.2", 55 | "should": "~3.1.3" 56 | }, 57 | "engines": { 58 | "node": ">=4.0.0" 59 | }, 60 | "scripts": { 61 | "clean": "rimraf dist/* lib/*", 62 | "prepare:clean": "yarn clean -s && mkdir -p dist", 63 | "prepare:src": "coffee -o lib -c src", 64 | "prepare": "yarn prepare:clean && yarn prepare:src", 65 | "prepublishOnly": "yarn test && browserify -t babelify -t brfs -i crypto -i buffer . > dist/yang.js", 66 | "pretest": "yarn prepare:src", 67 | "test": "mocha" 68 | }, 69 | "babel": { 70 | "presets": [ 71 | "es2015" 72 | ] 73 | }, 74 | "mocha": { 75 | "require": [ 76 | "chai", 77 | "should", 78 | "coffeescript/register" 79 | ], 80 | "sort": true, 81 | "spec": "test/*.coffee" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /schema/complex-types.yang: -------------------------------------------------------------------------------- 1 | module complex-types { 2 | 3 | namespace "urn:ietf:params:xml:ns:yang:complex-types"; 4 | prefix "ct"; 5 | 6 | organization 7 | "NETMOD WG"; 8 | 9 | contact 10 | "bernd.linowski@ext.nsn.com"; 11 | 12 | description 13 | "This module defines extensions to model complex types and typed 14 | instance identifiers."; 15 | 16 | revision 2009-09-29 { 17 | description "Initial revision."; 18 | } 19 | 20 | extension complex-type { 21 | description "Defines a complex-type."; 22 | reference "chapter 2.2., complex-type extension statement"; 23 | argument type-identifier { 24 | yin-element true; 25 | } 26 | } 27 | 28 | extension extends { 29 | description "Defines the base type of a complex-type."; 30 | reference "chapter 2.5., extends extension statement"; 31 | argument base-type-identifier { 32 | yin-element true; 33 | } 34 | } 35 | 36 | extension abstract { 37 | description "Makes the complex-type abstract."; 38 | reference "chapter 2.6., complex-type extension statement"; 39 | argument status; 40 | } 41 | 42 | extension instance { 43 | description "Declares an instance of the given complex type."; 44 | reference "chapter 2.3., instance extension statement"; 45 | argument ct-instance-identifier { 46 | yin-element true; 47 | } 48 | } 49 | 50 | extension instance-list { 51 | description "Declares a list of instances of the given complex type"; 52 | reference "chapter 2.4., instance-list extension 53 | statement"; 54 | argument ct-instance-identifier { 55 | yin-element true; 56 | } 57 | } 58 | 59 | extension instance-type { 60 | description "Tells to which type instance the instance identifier refers to."; 61 | reference "chapter 3.2., instance-type extension statement"; 62 | argument target-type-identifier { 63 | yin-element true; 64 | } 65 | } 66 | 67 | feature complex-types { 68 | description "This feature indicates that the agent supports complex types and 69 | instance identifiers."; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /schema/experimental/complex-types.yaml: -------------------------------------------------------------------------------- 1 | # YANG complex-types Specifications 2 | 3 | # The YANG complex-types module provides additional language 4 | # extensions according to [RFC 5 | # 6095](http://tools.ietf.org/html/rfc6095). These extensions provide 6 | # mechanisms to manage the `complex-type` schema definitions which 7 | # essentially allows a given YANG data schema module to describe more 8 | # than one data models and to build relationships between the data 9 | # models. 10 | 11 | specification: 12 | complex-types: 13 | extension: 14 | complex-type: 15 | scope: 16 | abstract: 0..1 17 | container: 0..n 18 | description: 0..1 19 | extends: 0..1 20 | grouping: 0..n 21 | if-feature: 0..n 22 | key: 0..1 23 | instance: 0..n 24 | instance-list: 0..n 25 | leaf: 0..n 26 | leaf-list: 0..n 27 | reference: 0..1 28 | refine: 0..n 29 | uses: 0..n 30 | parent-scope: 31 | module: 0..n 32 | submodule: 0..n 33 | preprocess: !coffee/function | 34 | (arg, params, ctx) -> 35 | synth = @resolve 'synthesizer' 36 | @define 'complex-type', arg, switch 37 | when params.extends? 38 | ctype = params.extends 39 | delete params.extends 40 | delete params.key if (ctype.get 'key')? 41 | (synth ctype) params, -> @set name: arg 42 | else 43 | synth.Model params, -> @set name: arg 44 | construct: !coffee/function | 45 | (arg, params, children) -> 46 | ct = (@resolve 'complex-type', arg) 47 | if children.type? 48 | ct.bind (children.type.get 'bindings') 49 | delete children.type 50 | ct.bind children 51 | for target, changes of params.refine 52 | target = target.replace '/','.' 53 | ct.rebind target, (prev) -> prev.merge changes 54 | null 55 | 56 | abstract: 57 | 58 | extends: 59 | scope: 60 | description: 0..1 61 | reference: 0..1 62 | status: 0..1 63 | preprocess: !coffee/function | 64 | (arg, params, ctx) -> 65 | ctx.extends = @resolve 'complex-type', arg 66 | unless ctx.extends? 67 | throw @error "unable to resolve dependent complex-type '#{arg}'", 'extends' 68 | construct: !coffee/function | 69 | (arg, params, children, ctx) -> 70 | ctx.type = @resolve 'complex-type', arg 71 | null 72 | 73 | instance: 74 | scope: 75 | augment: 0..n 76 | choice: 0..n 77 | config: 0..1 78 | container: 0..n 79 | description: 0..1 80 | if-feature: 0..n 81 | instance: 0..n 82 | instance-list: 0..n 83 | instance-type: 1 84 | leaf: 0..n 85 | leaf-list: 0..n 86 | list: 0..n 87 | mandatory: 0..1 88 | must: 0..n 89 | reference: 0..1 90 | status: 0..1 91 | when: 0..1 92 | parent-scope: 93 | module: 0..n 94 | submodule: 0..n 95 | container: 0..n 96 | grouping: 0..n 97 | input: 0..n 98 | output: 0..n 99 | construct: !coffee/function | 100 | (arg, params, children) -> 101 | synth = @resolve 'synthesizer' 102 | Model = children.type 103 | delete children.type 104 | ((synth Model) params).bind children 105 | 106 | instance-list: 107 | scope: 108 | augment: 0..n 109 | choice: 0..n 110 | config: 0..1 111 | container: 0..n 112 | description: 0..1 113 | if-feature: 0..n 114 | instance: 0..n 115 | instance-list: 0..n 116 | instance-type: 1 117 | leaf: 0..n 118 | leaf-list: 0..n 119 | list: 0..n 120 | min-elements: 0..n 121 | max-elements: 0..n 122 | must: 0..n 123 | reference: 0..1 124 | status: 0..1 125 | when: 0..1 126 | parent-scope: 127 | module: 0..n 128 | submodule: 0..n 129 | container: 0..n 130 | grouping: 0..n 131 | input: 0..n 132 | output: 0..n 133 | construct: !coffee/function | 134 | (arg, params, children) -> 135 | synth = @resolve 'synthesizer' 136 | Model = children.type 137 | delete children.type 138 | unless (Model.get 'key')? 139 | throw @error "missing 'key' for #{Model.get 'name'} used in instance-list" 140 | item = ((synth Model) null).bind children 141 | #item = (class extends Model).bind children 142 | (synth.List params).set type: item, key: (item.get 'key') 143 | 144 | instance-type: 145 | parent-scope: 146 | type: 0..1 147 | construct: !coffee/function | 148 | (arg, params, children, ctx) -> 149 | ctx.type = (@resolve 'complex-type', arg) 150 | unless ctx.type? 151 | throw @error "unable to resolve '#{arg}' complex-type instance" 152 | null 153 | 154 | # module: 155 | # construct: !coffee/function | 156 | # (arg, params, children, ctx, self) -> 157 | # (self.origin.construct.apply this, arguments) 158 | # .merge models: (@resolve 'complex-type') 159 | -------------------------------------------------------------------------------- /schema/iana-crypt-hash.yang: -------------------------------------------------------------------------------- 1 | module iana-crypt-hash { 2 | 3 | yang-version 1; 4 | 5 | namespace 6 | "urn:ietf:params:xml:ns:yang:iana-crypt-hash"; 7 | 8 | prefix ianach; 9 | 10 | organization "IANA"; 11 | 12 | contact 13 | " Internet Assigned Numbers Authority 14 | 15 | Postal: ICANN 16 | 4676 Admiralty Way, Suite 330 17 | Marina del Rey, CA 90292 18 | 19 | Tel: +1 310 823 9358 20 | E-Mail: iana&iana.org"; 21 | 22 | description 23 | "This YANG module defines a typedef for storing passwords 24 | using a hash function, and features to indicate which hash 25 | functions are supported by an implementation. 26 | 27 | The latest revision of this YANG module can be obtained from 28 | the IANA web site. 29 | 30 | Requests for new values should be made to IANA via 31 | email (iana&iana.org). 32 | 33 | Copyright (c) 2014 IETF Trust and the persons identified as 34 | authors of the code. All rights reserved. 35 | 36 | Redistribution and use in source and binary forms, with or 37 | without modification, is permitted pursuant to, and subject 38 | to the license terms contained in, the Simplified BSD License 39 | set forth in Section 4.c of the IETF Trust's Legal Provisions 40 | Relating to IETF Documents 41 | (http://trustee.ietf.org/license-info). 42 | 43 | The initial version of this YANG module is part of RFC XXXX; 44 | see the RFC itself for full legal notices."; 45 | 46 | revision "2014-04-04" { 47 | description "Initial revision."; 48 | reference 49 | "RFC XXXX: A YANG Data Model for System Management"; 50 | } 51 | 52 | typedef crypt-hash { 53 | type string { 54 | pattern 55 | '$0$.*|$1$[a-zA-Z0-9./]{1,8}$[a-zA-Z0-9./]{22}|$5$(rounds=\d+$)?[a-zA-Z0-9./]{1,16}$[a-zA-Z0-9./]{43}|$6$(rounds=\d+$)?[a-zA-Z0-9./]{1,16}$[a-zA-Z0-9./]{86}'; 56 | } 57 | description 58 | "The crypt-hash type is used to store passwords using 59 | a hash function. The algorithms for applying the hash 60 | function and encoding the result are implemented in 61 | various UNIX systems as the function crypt(3). 62 | 63 | A value of this type matches one of the forms: 64 | 65 | $0$ 66 | $$$ 67 | $$$$ 68 | 69 | The '$0$' prefix signals that the value is clear text. When 70 | such a value is received by the server, a hash value is 71 | calculated, and the string '$$$' or 72 | $$$$ is prepended to the result. This 73 | value is stored in the configuration data store. 74 | 75 | If a value starting with '$$', where is not '0', is 76 | received, the server knows that the value already represents a 77 | hashed value, and stores it as is in the data store. 78 | 79 | When a server needs to verify a password given by a user, it 80 | finds the stored password hash string for that user, extracts 81 | the salt, and calculates the hash with the salt and given 82 | password as input. If the calculated hash value is the same 83 | as the stored value, the password given by the client is 84 | accepted. 85 | 86 | This type defines the following hash functions: 87 | 88 | id | hash function | feature 89 | ---+---------------+------------------- 90 | 1 | MD5 | crypt-hash-md5 91 | 5 | SHA-256 | crypt-hash-sha-256 92 | 6 | SHA-512 | crypt-hash-sha-512 93 | 94 | The server indicates support for the different hash functions 95 | by advertising the corresponding feature."; 96 | reference 97 | "IEEE Std 1003.1-2008 - crypt() function 98 | RFC 1321: The MD5 Message-Digest Algorithm 99 | FIPS.180-3.2008: Secure Hash Standard"; 100 | } 101 | 102 | feature crypt-hash-md5 { 103 | description 104 | "Indicates that the device supports the MD5 105 | hash function in 'crypt-hash' values"; 106 | reference 107 | "RFC 1321: The MD5 Message-Digest Algorithm"; 108 | } 109 | 110 | feature crypt-hash-sha-256 { 111 | description 112 | "Indicates that the device supports the SHA-256 113 | hash function in 'crypt-hash' values"; 114 | reference 115 | "FIPS.180-3.2008: Secure Hash Standard"; 116 | } 117 | 118 | feature crypt-hash-sha-512 { 119 | description 120 | "Indicates that the device supports the SHA-512 121 | hash function in 'crypt-hash' values"; 122 | reference 123 | "FIPS.180-3.2008: Secure Hash Standard"; 124 | } 125 | } // module iana-crypt-hash 126 | -------------------------------------------------------------------------------- /schema/ietf-yang-library.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Yang = require('..'); 4 | const Schema = require('./ietf-yang-library@2016-06-21.yang'); 5 | const crypto = require('crypto'); 6 | 7 | module.exports = Schema.bind({ 8 | 9 | '/yanglib:modules-state': { 10 | get: async (ctx) => { 11 | if (!Yang.module) return {} 12 | const modules = Yang.module.map(module => { 13 | const { namespace, revision=[], feature=[], include=[] } = module; 14 | return { 15 | name: module.tag, 16 | namespace: (namespace ? namespace.tag : ''), 17 | revision: (revision[0] ? revision[0].tag : ''), 18 | feature: feature.map(x => x.tag), 19 | 'conformance-type': 'implement', 20 | submodule: include.map(x => ({ 21 | name: x.tag, 22 | revision: x['revision-date'] ? x['revision-date'].tag : '' 23 | })), 24 | }; 25 | }); 26 | const keys = modules.map(x => x.name); 27 | const hash = crypto.createHash('md5').update(keys.join(',')).digest('hex'); 28 | if (!hash) return ctx.value; 29 | 30 | const prev = ctx.get('module-set-id'); 31 | if (hash !== prev) { 32 | // TODO: notification yang-library-change 33 | ctx.logDebug("trigger yang-library-change notification", keys); 34 | } 35 | ctx.data = { 36 | 'module-set-id': hash, 37 | module: modules 38 | } 39 | return ctx.value; 40 | } 41 | }, 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /schema/ietf-yang-library@2016-06-21.yang: -------------------------------------------------------------------------------- 1 | module ietf-yang-library { 2 | namespace "urn:ietf:params:xml:ns:yang:ietf-yang-library"; 3 | prefix "yanglib"; 4 | import ietf-yang-types { 5 | prefix yang; 6 | } 7 | import ietf-inet-types { 8 | prefix inet; 9 | } 10 | organization 11 | "IETF NETCONF (Network Configuration) Working Group"; 12 | contact 13 | "WG Web: 14 | WG List: 15 | WG Chair: Mehmet Ersue 16 | 17 | WG Chair: Mahesh Jethanandani 18 | 19 | Editor: Andy Bierman 20 | 21 | Editor: Martin Bjorklund 22 | 23 | Editor: Kent Watsen 24 | "; 25 | description 26 | "This module contains monitoring information about the YANG 27 | modules and submodules that are used within a YANG-based 28 | server. 29 | Copyright (c) 2016 IETF Trust and the persons identified as 30 | authors of the code. All rights reserved. 31 | Redistribution and use in source and binary forms, with or 32 | without modification, is permitted pursuant to, and subject 33 | to the license terms contained in, the Simplified BSD License 34 | set forth in Section 4.c of the IETF Trust's Legal Provisions 35 | Relating to IETF Documents 36 | (http://trustee.ietf.org/license-info). 37 | This version of this YANG module is part of RFC 7895; see 38 | the RFC itself for full legal notices."; 39 | revision 2016-06-21 { 40 | description 41 | "Initial revision."; 42 | reference 43 | "RFC 7895: YANG Module Library."; 44 | } 45 | /* 46 | * Typedefs 47 | */ 48 | typedef revision-identifier { 49 | type string { 50 | pattern '\d{4}-\d{2}-\d{2}'; 51 | } 52 | description 53 | "Represents a specific date in YYYY-MM-DD format."; 54 | } 55 | /* 56 | * Groupings 57 | */ 58 | grouping module-list { 59 | description 60 | "The module data structure is represented as a grouping 61 | so it can be reused in configuration or another monitoring 62 | data structure."; 63 | grouping common-leafs { 64 | description 65 | "Common parameters for YANG modules and submodules."; 66 | leaf name { 67 | type yang:yang-identifier; 68 | description 69 | "The YANG module or submodule name."; 70 | } 71 | leaf revision { 72 | type union { 73 | type revision-identifier; 74 | type string { length 0; } 75 | } 76 | description 77 | "The YANG module or submodule revision date. 78 | A zero-length string is used if no revision statement 79 | is present in the YANG module or submodule."; 80 | } 81 | } 82 | grouping schema-leaf { 83 | description 84 | "Common schema leaf parameter for modules and submodules."; 85 | leaf schema { 86 | type inet:uri; 87 | description 88 | "Contains a URL that represents the YANG schema 89 | resource for this module or submodule. 90 | This leaf will only be present if there is a URL 91 | available for retrieval of the schema for this entry."; 92 | } 93 | } 94 | list module { 95 | key "name revision"; 96 | description 97 | "Each entry represents one revision of one module 98 | currently supported by the server."; 99 | uses common-leafs; 100 | uses schema-leaf; 101 | leaf namespace { 102 | type inet:uri; 103 | mandatory true; 104 | description 105 | "The XML namespace identifier for this module."; 106 | } 107 | leaf-list feature { 108 | type yang:yang-identifier; 109 | description 110 | "List of YANG feature names from this module that are 111 | supported by the server, regardless of whether they are 112 | defined in the module or any included submodule."; 113 | } 114 | list deviation { 115 | key "name revision"; 116 | description 117 | "List of YANG deviation module names and revisions 118 | used by this server to modify the conformance of 119 | the module associated with this entry. Note that 120 | the same module can be used for deviations for 121 | multiple modules, so the same entry MAY appear 122 | within multiple 'module' entries. 123 | The deviation module MUST be present in the 'module' 124 | list, with the same name and revision values. 125 | The 'conformance-type' value will be 'implement' for 126 | the deviation module."; 127 | uses common-leafs; 128 | } 129 | leaf conformance-type { 130 | type enumeration { 131 | enum implement { 132 | description 133 | "Indicates that the server implements one or more 134 | protocol-accessible objects defined in the YANG module 135 | identified in this entry. This includes deviation 136 | statements defined in the module. 137 | For YANG version 1.1 modules, there is at most one 138 | module entry with conformance type 'implement' for a 139 | particular module name, since YANG 1.1 requires that, 140 | at most, one revision of a module is implemented. 141 | For YANG version 1 modules, there SHOULD NOT be more 142 | than one module entry for a particular module name."; 143 | } 144 | enum import { 145 | description 146 | "Indicates that the server imports reusable definitions 147 | from the specified revision of the module but does 148 | not implement any protocol-accessible objects from 149 | this revision. 150 | Multiple module entries for the same module name MAY 151 | exist. This can occur if multiple modules import the 152 | same module but specify different revision dates in 153 | the import statements."; 154 | } 155 | } 156 | mandatory true; 157 | description 158 | "Indicates the type of conformance the server is claiming 159 | for the YANG module identified by this entry."; 160 | } 161 | list submodule { 162 | key "name revision"; 163 | description 164 | "Each entry represents one submodule within the 165 | parent module."; 166 | uses common-leafs; 167 | uses schema-leaf; 168 | } 169 | } 170 | } 171 | /* 172 | * Operational state data nodes 173 | */ 174 | container modules-state { 175 | config false; 176 | description 177 | "Contains YANG module monitoring information."; 178 | leaf module-set-id { 179 | type string; 180 | mandatory true; 181 | description 182 | "Contains a server-specific identifier representing 183 | the current set of modules and submodules. The 184 | server MUST change the value of this leaf if the 185 | information represented by the 'module' list instances 186 | has changed."; 187 | } 188 | uses module-list; 189 | } 190 | /* 191 | * Notifications 192 | */ 193 | notification yang-library-change { 194 | description 195 | "Generated when the set of modules and submodules supported 196 | by the server has changed."; 197 | leaf module-set-id { 198 | type leafref { 199 | path "/yanglib:modules-state/yanglib:module-set-id"; 200 | } 201 | mandatory true; 202 | description 203 | "Contains the module-set-id value representing the 204 | set of modules and submodules supported at the server at 205 | the time the notification is generated."; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /schema/yang-graph-query.yang: -------------------------------------------------------------------------------- 1 | module yang-graph-query { 2 | extension querydef { 3 | 4 | } 5 | extension query { 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /schema/yang-meta-types.yang: -------------------------------------------------------------------------------- 1 | module yang-meta-types { 2 | namespace "urn:corenova:yang:yang-meta-types"; 3 | prefix meta; 4 | yang-version 1.1; 5 | 6 | import ietf-yang-types { prefix yang; } 7 | import ietf-inet-types { prefix inet; } 8 | import iana-crypt-hash { prefix ianach; } 9 | 10 | organization 11 | "Corenova Technologies, Inc."; 12 | contact 13 | "Peter K. Lee "; 14 | 15 | description 16 | "This module provides common metadata type definitions"; 17 | 18 | revision 2016-09-14 { 19 | description 20 | "Initial revision."; 21 | } 22 | /* 23 | * Type Definitions 24 | */ 25 | typedef meta-identifier { 26 | type yang:yang-identifier; 27 | } 28 | typedef title { 29 | type string { 30 | length 1..255; 31 | } 32 | } 33 | typedef description { 34 | type string; 35 | } 36 | typedef person-name { 37 | type string { 38 | length 1..255; 39 | } 40 | } 41 | typedef email-address { 42 | type string { 43 | pattern "[\\-_.a-zA-Z0-9]+@[\\-_.a-zA-Z0-9]+(\\.[\\-_.a-zA-Z0-9]+)*"; 44 | } 45 | description 46 | "Valid format of an email address."; 47 | } 48 | typedef person-contact { 49 | type string { 50 | pattern '["-\w,. ]+\s*(<.+?@.+?>)?\s*(\(.+?\))?'; 51 | } 52 | } 53 | typedef phone-number { 54 | type string { 55 | pattern "\\+?[0-9]+(-[0-9]+)*"; 56 | } 57 | description 58 | "Valid format of a phone number."; 59 | } 60 | typedef timezone { 61 | type string; 62 | } 63 | typedef password { 64 | type ianach:crypt-hash; 65 | } 66 | typedef empty-string { 67 | type string { 68 | length 0; 69 | } 70 | } 71 | typedef wildcard { 72 | type string { 73 | pattern '\*'; 74 | } 75 | } 76 | typedef semantic-version { 77 | type string { 78 | pattern '\d+\.\d+\.\d+(-.+)?'; 79 | } 80 | } 81 | typedef semantic-version-match { 82 | type union { 83 | type semantic-version; 84 | type string { 85 | pattern '([><~^]|<=|>=)\s*\d+\.[\dx]+(\.[\dx]+)?'; 86 | } 87 | type string { 88 | pattern '\d+\.[\dx]+(\.[\dx]+)?'; 89 | } 90 | type string { 91 | pattern '([><]|<=|>=)\s*\d+\.[\dx]+\.[\dx]+ ([><]|<=|>=)\s*\d+\.?[\dx]*\.?[\dx]*'; 92 | } 93 | // TODO: add support composite ranges with || 94 | } 95 | } 96 | typedef api-version { 97 | type string { 98 | pattern '\d+\.\d+'; 99 | } 100 | } 101 | typedef license { 102 | type yang:yang-identifier; 103 | description "TODO: should reference https://spdx.org/licenses for complete enumeration."; 104 | } 105 | typedef file-name { 106 | type string { 107 | length 0..255; 108 | //pattern '[\\\/\-\w\.]+'; // might be too restrictive... 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/container.litcoffee: -------------------------------------------------------------------------------- 1 | # Container - controller of object properties 2 | 3 | ## Class Container 4 | 5 | delegate = require 'delegates' 6 | Emitter = require('events').EventEmitter 7 | cloneDeep = require('lodash.clonedeep') 8 | Property = require('./property') 9 | kProp = Symbol.for('property') 10 | 11 | class Container extends Property 12 | logger: require('debug')('yang:container') 13 | 14 | constructor: -> 15 | super arguments... 16 | @state.children = new Map # committed props 17 | @state.pending = new Map # uncommitted changed props 18 | @state.delta = undefined 19 | @state.locked = false 20 | @state.proxy = 21 | has: (obj, key) => @children.has(key) or key of obj 22 | get: (obj, key) => switch 23 | when key is kProp then this 24 | when key is '_' then this 25 | when key is 'toJSON' then @toJSON.bind(this) 26 | when @has(key) then @get(key) 27 | when key of obj then obj[key] 28 | when key is 'inspect' then @toJSON.bind(this) 29 | when key of this and typeof @[key] is 'function' then @[key].bind(this) 30 | when typeof key is 'string' and key[0] is '_' then @[key.substring(1)] 31 | set: (obj, key, value) => switch 32 | when @has(key) then @_get(key).set(value) 33 | else obj[key] = value 34 | deleteProperty: (obj, key) => switch 35 | when @has(key) then @_get(key).delete() 36 | when key of obj then delete obj[key] 37 | Object.setPrototypeOf @state, Emitter.prototype 38 | 39 | delegate @prototype, 'state' 40 | .getter 'children' 41 | .getter 'pending' 42 | .getter 'locked' 43 | .getter 'delta' 44 | .method 'once' 45 | .method 'on' 46 | .method 'off' 47 | .method 'emit' 48 | 49 | @property 'props', 50 | get: -> Array.from(@children.values()) 51 | 52 | @property 'changed', 53 | get: -> @pending.size > 0 or @state.changed 54 | 55 | @property 'changes', 56 | get: -> Array.from(@pending.values()) 57 | 58 | @property 'change', 59 | get: -> switch 60 | when @changed and not @active then null 61 | when @changed and @pending.size 62 | obj = {} 63 | obj[prop.key] = prop.change for prop from @changes 64 | obj 65 | when @changed then @data 66 | 67 | @property 'data', 68 | set: (value) -> @set value, { force: true } 69 | get: -> 70 | value = switch 71 | when @binding?.get? then @binding.get @context 72 | else @value 73 | return value unless value instanceof Object 74 | new Proxy value, @state.proxy 75 | 76 | clone: -> 77 | copy = super children: new Map, pending: new Map 78 | copy.add prop.clone(parent: copy) for prop in @props 79 | return copy 80 | 81 | ### add (child) 82 | 83 | This call is used to add a child property to map of children. 84 | 85 | add: (child) -> 86 | @children.set child.key, child 87 | if @value? 88 | Object.defineProperty @value, child.key, 89 | configurable: true 90 | enumerable: child.active 91 | 92 | ### remove (child) 93 | 94 | This call is used to remove a child property from map of children. 95 | 96 | remove: (child) -> 97 | @children.delete child.key 98 | if @value? 99 | delete @value[child.key] 100 | 101 | ### has (key) 102 | 103 | has: (key) -> @children.has(key) 104 | 105 | ### get (key) 106 | 107 | _get: (key) -> @children.get(key) 108 | 109 | get: (key) -> switch 110 | when key? and @has(key) then @_get(key).data 111 | else super arguments... 112 | 113 | ### set (obj, opts) 114 | 115 | set: (obj, opts={}) -> 116 | # TODO: should we preserve prior changes and restore if super fails? 117 | @pending.clear() 118 | # TODO: should we also clear Object.defineProperties? 119 | #try obj = Object.assign {}, obj if kProp of obj 120 | try obj = cloneDeep(obj) if kProp of obj 121 | super obj, opts 122 | # remove all props not part of pending changes 123 | subopts = Object.assign {}, opts 124 | #prop.delete(subopts) for prop in @props when not @pending.has(prop.key) 125 | @props.forEach (prop) => prop.delete(subopts) unless @pending.has(prop.key) 126 | return this 127 | 128 | ### merge (obj, opts) 129 | 130 | Enumerate key/value of the passed in `obj` and merge into known child 131 | properties. 132 | 133 | merge: (obj, opts={}) -> 134 | opts.origin ?= this 135 | return @delete opts if obj is null 136 | return @set obj, opts if opts.replace or not @value? 137 | 138 | # TODO: protect this as a transaction? 139 | { deep = true } = opts 140 | 141 | subopts = Object.assign {}, opts, inner: true, replace: not deep 142 | for own k, v of obj 143 | @debug => "[merge] looking for #{k} inside #{@children.size} children" 144 | prop = @children.get(k) ? @in(k) 145 | continue unless prop? and not Array.isArray(prop) 146 | @debug => "[merge] applying value to child prop #{prop.key}" 147 | prop.merge(v, subopts) 148 | 149 | # TODO: we should consider evaluating schema.attrs here before calling update 150 | # 151 | @update this, opts 152 | 153 | ### update 154 | 155 | Updates the value to the data model. Called *once* for each node that 156 | is part of the change branch. 157 | 158 | update: (value, opts={}) -> 159 | opts.origin ?= this 160 | 161 | if value instanceof Property 162 | # handle updating pending map if value is a child prop 163 | if value.parent is this 164 | @pending.set value.key, value 165 | @debug => "[update] pending.set '#{value.key}' now have #{@pending.size} pending changes" 166 | 167 | unless value is this 168 | # return right away if part of update on a higher node 169 | return this if opts.inner or opts.origin is this 170 | # higher up the tree from change origin and continue 171 | value = @value 172 | 173 | @debug => "[update] handle #{@pending.size} changed props" 174 | for prop from @changes 175 | @debug => "[update] child #{prop.uri} changed? #{prop.changed}" 176 | @add prop, opts 177 | @pending.delete prop.key unless prop.changed 178 | 179 | # dynamically compute value to be used for storing into @state 180 | value = @value if value is this 181 | 182 | # we must clear children here if being deleted before calling super (which calls parent.update) 183 | @children.clear() if value is null 184 | super value, opts 185 | 186 | @emit 'update', this, opts 187 | return this 188 | 189 | ### commit (opts) 190 | 191 | Commits the changes to the data model. Async transaction. 192 | Events: commit, change 193 | 194 | lock: (opts={}) -> 195 | randomize = (min, max) -> Math.floor(Math.random() * (max - min)) + min 196 | opts.seq ?= randomize 1000, 9999 197 | @debug "[lock:#{opts.seq}] acquiring lock... already have it?", (opts.lock is this) 198 | unless opts.lock is this 199 | while @locked 200 | await (new Promise (resolve) => @once 'ready', -> resolve true) 201 | @debug "[lock:#{opts.seq}] acquired and has #{@pending.size} changes, changed? #{@changed}" 202 | @debug "[lock:#{opts.seq}] acquired by #{opts.caller.uri} and locked? #{opts.caller.locked}" if opts.caller? 203 | super opts 204 | 205 | unlock: (opts={}) -> 206 | @debug "[unlock:#{opts.seq}] freeing lock..." 207 | return this unless @locked 208 | super opts 209 | @emit 'ready' 210 | return this 211 | 212 | commit: (opts={}) -> 213 | try 214 | await @lock opts 215 | id = opts.seq ? 0 216 | if @changed 217 | subopts = Object.assign {}, opts, caller: this, inner: true 218 | delete subopts.lock 219 | # 1. commit all the changed children 220 | @debug "[commit:#{id}] wait to commit all the children..." 221 | await Promise.all @changes.filter((p) -> not p.locked).map (prop) -> prop.commit subopts 222 | # 2. run the commit binding 223 | if not opts.sync and @binding?.commit? 224 | @debug "[commit:#{id}] executing commit binding..." 225 | await @binding.commit @context.with(opts) 226 | 227 | # wait for the parent to commit unless called by parent 228 | opts.origin ?= this 229 | unless opts.inner 230 | subopts = Object.assign {}, opts, caller: this 231 | @debug "[commit:#{id}] wait for parent to commit..." 232 | await @parent?.commit? subopts 233 | @emit 'change', opts.origin, opts.actor if not opts.suppress and not opts.inner 234 | @clean opts unless opts.inner 235 | 236 | catch err 237 | @debug "[commit:#{id}] error: #{err.message}" 238 | failed = true 239 | throw @error err, 'commit' 240 | 241 | finally 242 | @debug "[commit:#{id}] finalizing... successful? #{!failed}" 243 | await @revert opts if failed 244 | @debug "[commit:#{id}] #{@pending.size} changes, now have #{@children.size} props, releasing lock!" 245 | @unlock opts 246 | 247 | return this 248 | 249 | revert: (opts={}) -> 250 | return unless @changed 251 | 252 | id = opts.seq ? 0 253 | @debug => "[revert:#{id}] changing back #{@pending.size} pending changes..." 254 | 255 | # NOTE: save a copy of current data here since reverting changed children may alter @state.value 256 | copy = @toJSON() 257 | 258 | # XXX: may want to consider Promise.all here 259 | for prop from @changes when (not prop.locked) or (prop is opts.caller) 260 | @debug "[revert:#{id}] changing back: #{prop.key}" 261 | await prop.revert opts 262 | @add prop 263 | 264 | # XXX: need to deal with scenario where child nodes reverting is sufficient? 265 | 266 | # below is hackish but works to make a copy of current value 267 | # to be used as ctx.prior during revert commit binding call 268 | @state.value = copy 269 | 270 | await super opts 271 | @debug "[revert:#{id}] #{@children.size} remaining props" 272 | 273 | clean: (opts={}) -> 274 | return unless @changed 275 | # traverse down committed nodes and clean their state 276 | # console.warn(opts.caller) if opts.caller 277 | id = opts.seq 278 | @debug "[clean:#{id}] #{@pending.size} changes with #{@children.size} props" 279 | unless @state.value? 280 | @children.clear() 281 | @pending.clear() 282 | else 283 | for prop from @changes when (not prop.locked) or (prop is opts.caller) 284 | prop.clean opts 285 | @pending.delete(prop.key) 286 | @debug "[clean:#{id}] #{@pending.size} remaining changes" 287 | super opts 288 | 289 | 290 | 291 | ### toJSON 292 | 293 | This call creates a new copy of the current `Property.data` 294 | completely detached/unbound to the underlying data schema. It's main 295 | utility is to represent the current data state for subsequent 296 | serialization/transmission. It accepts optional argument `tag` which 297 | when called with `true` will tag the produced object with the current 298 | property's `@name`. 299 | 300 | toJSON: (key, state = true) -> 301 | props = @props 302 | value = switch 303 | when props.length 304 | obj = {} 305 | for prop in props when prop.enumerable and (state or prop.mutable) 306 | value = prop.toJSON false, state 307 | obj[prop.key] = value if value? 308 | obj 309 | else @value 310 | value = "#{@name}": value if key is true 311 | return value 312 | 313 | ### inspect 314 | 315 | inspect: -> 316 | output = super arguments... 317 | return Object.assign output, children: @children.size 318 | 319 | module.exports = Container 320 | -------------------------------------------------------------------------------- /src/context.coffee: -------------------------------------------------------------------------------- 1 | # Context - control logic binding context 2 | 3 | ## Context Object 4 | debug = require('debug')('yang:context') 5 | delegate = require 'delegates' 6 | 7 | proto = module.exports = { 8 | use: (name) -> 9 | # TODO: below is a bit of a hack... 10 | return @lookup('feature', name)?.binding 11 | 12 | with: (options...) -> 13 | ctx = Object.create(this) 14 | ctx.opts = Object.assign {}, @opts, options... 15 | Object.preventExtensions ctx 16 | return ctx 17 | 18 | at: (key) -> 19 | node = @node.in key 20 | unless node? then throw @error "unable to access #{key}" 21 | return node.context.with @opts 22 | 23 | push: (data) -> 24 | return @node.do(data, @opts) if @kind in [ 'rpc', 'action' ] 25 | 26 | opts = Object.assign {}, @opts # make a copy 27 | try (await @node.lock opts).merge(data, opts) 28 | catch err 29 | @node.unlock opts 30 | throw @error err 31 | try await @node.commit opts 32 | catch err then throw @error err 33 | 34 | return @node 35 | 36 | # convenience function for replace (set operation) 37 | replace: (data) -> @with( replace: true ).push(data) 38 | 39 | set: (data) -> @node.set(data, Object.assign {}, @opts) 40 | merge: (data) -> @node.merge(data, Object.assign {}, @opts) 41 | 42 | commit: -> @node.commit(Object.assign {}, @opts) 43 | revert: -> @node.revert(Object.assign {}, @opts) 44 | 45 | after: (timeout, max) -> 46 | timeout = parseInt(timeout) || 100 47 | max = parseInt(max) || 5000 48 | new Promise (resolve) -> 49 | setTimeout (-> resolve(Math.round(Math.min(max, timeout * 1.5)))), timeout 50 | 51 | logDebug: -> @log 'debug', arguments... 52 | logInfo: -> @log 'info', arguments... 53 | logWarn: -> @log 'warn', arguments... 54 | logError: -> @log 'error', @error arguments... 55 | 56 | log: (topic, args...) -> 57 | @root.emit('log', topic, args, this) 58 | } 59 | 60 | ## Property node delegation 61 | delegate proto, 'node' 62 | .access 'data' # read/write with validations 63 | .getter 'prior' 64 | .getter 'delta' 65 | .getter 'value' 66 | 67 | .getter 'root' 68 | .getter 'parent' 69 | .getter 'schema' 70 | 71 | .getter 'uri' 72 | .getter 'name' 73 | .getter 'kind' 74 | .getter 'path' 75 | .getter 'active' 76 | .getter 'attached' # used for instance-identifier and leafref validations 77 | .getter 'changed' # boolean 78 | .getter 'changes' # Set of changed properties 79 | .getter 'change' # Object of uncommitted changes 80 | .getter 'delta' # Object of latest change snapshot 81 | 82 | .method 'get' 83 | .method 'error' 84 | .method 'locate' 85 | .method 'lookup' 86 | .method 'find' 87 | .method 'inspect' 88 | .method 'toJSON' 89 | 90 | ## Module delegation 91 | delegate proto, 'root' 92 | .method 'access' 93 | -------------------------------------------------------------------------------- /src/element.litcoffee: -------------------------------------------------------------------------------- 1 | # Element - cascading element tree 2 | 3 | ## Class Element 4 | 5 | debug = require('debug') 6 | logger = debug('yang:element') 7 | delegate = require('delegates') 8 | Emitter = require('events').EventEmitter 9 | Emitter.defaultMaxListeners = 100 10 | 11 | kIndex = Symbol.for('element:index'); 12 | kCache = Symbol.for('element:cache'); 13 | 14 | class Element 15 | 16 | @property: (prop, desc) -> 17 | Object.defineProperty @prototype, prop, desc 18 | 19 | @use: -> 20 | res = [].concat(arguments...) 21 | .filter (x) -> x? 22 | .map (elem) => 23 | exists = Element::match.call this, elem.kind, elem.tag 24 | if exists? 25 | @debug => "use: using previously loaded '#{elem.kind}:#{elem.tag}'" 26 | return exists 27 | try Element::merge.call this, elem 28 | catch e 29 | throw @error "use: unable to merge '#{elem.kind}:#{elem.tag}'", e 30 | return switch 31 | when res.length > 1 then res 32 | when res.length is 1 then res[0] 33 | else undefined 34 | 35 | @logger: logger 36 | @debug: (f) -> switch 37 | when debug.enabled @logger.namespace then switch 38 | when typeof f is 'function' then @logger @uri, [].concat(f())... 39 | else @logger @uri, arguments... 40 | 41 | @error: (err, ctx) -> 42 | err = new Error err unless err instanceof Error 43 | err.uri = @uri 44 | err.src = this 45 | err.ctx = ctx 46 | return err 47 | 48 | logger: @logger 49 | debug: @debug 50 | error: @error 51 | 52 | constructor: (@kind, @tag, scope) -> 53 | unless @kind? 54 | throw @error "must supply 'kind' to create a new Element" 55 | 56 | @scope = scope if scope? 57 | 58 | Object.defineProperties this, 59 | parent: value: null, writable: true 60 | origin: value: null, writable: true 61 | state: value: {}, writable: true 62 | [kIndex]: value: 0, writable: true 63 | emitter: value: new Emitter 64 | 65 | delegate @prototype, 'emitter' 66 | .method 'emit' 67 | .method 'once' 68 | .method 'on' 69 | .method 'off' 70 | 71 | ### Computed Properties 72 | 73 | @property 'datakey', 74 | get: -> @tag ? @kind 75 | 76 | @property 'uri', 77 | get: -> switch 78 | when @parent instanceof Element 79 | mark = @kind 80 | mark += "(#{@tag})" if @tag? and @parent.scope?[@kind] in [ '0..n', '1..n', '*' ] 81 | "#{@parent.uri}/#{mark}" 82 | when @tag? 83 | "#{@kind}(#{@tag})" 84 | else 85 | @kind 86 | 87 | @property 'root', 88 | get: -> switch 89 | when @parent instanceof Element then @parent.root 90 | when @origin instanceof Element then @origin.root 91 | else this 92 | 93 | @property 'children', 94 | get: -> 95 | unless this[kCache]? 96 | elements = (v for own k, v of this when k not in [ 'parent', 'origin', 'tag', kCache, 'source' ]) 97 | .reduce ((a,b) -> switch 98 | when b instanceof Element then a.concat b 99 | when b instanceof Array 100 | a.concat b.filter (x) -> x instanceof Element 101 | else a 102 | ), [] 103 | this[kCache] = elements.sort (a,b) -> a[kIndex] - b[kIndex] 104 | return this[kCache]; 105 | 106 | @property '*', get: -> @children 107 | @property '..', get: -> @parent 108 | 109 | ## Instance-level methods 110 | 111 | ### clone 112 | 113 | clone: (opts={}) -> 114 | { origin = @origin, relative = true } = opts 115 | @debug => "cloning #{@kind}:#{@tag} with #{@children.length} elements" 116 | copy = (new @constructor @kind, @tag, @source).extends @children.map (x) -> x.clone opts 117 | copy.state = Object.create(@state) 118 | copy.state.relative = relative 119 | copy.origin = origin ? this 120 | return copy 121 | 122 | ### extends (elements...) 123 | 124 | This is the primary mechanism for defining sub-elements to become part 125 | of the element tree 126 | 127 | extends: -> 128 | elems = ([].concat arguments...).filter (x) -> x? and !!x 129 | return this unless elems.length > 0 130 | elems.forEach (expr) => @merge expr 131 | @emit 'change', elems... 132 | return this 133 | 134 | ### merge (element) 135 | 136 | This helper method merges a specific Element into current Element 137 | while performing `@scope` validations. 138 | 139 | merge: (elem, opts={}) -> 140 | unless elem instanceof Element 141 | throw @error "cannot merge invalid element into Element", elem 142 | 143 | elem.parent = this 144 | elem[kIndex] = @children.length if @children? 145 | this[kCache] = null 146 | 147 | _merge = (item) -> 148 | if not item.node or opts.append or item.datakey not in (@keys ? []) 149 | @push item 150 | true 151 | else if opts.replace is true 152 | for x, i in this when x.datakey is item.datakey 153 | @splice i, 1, item 154 | break 155 | true 156 | else false 157 | 158 | unless @scope? 159 | unless @hasOwnProperty elem.kind 160 | @[elem.kind] = elem 161 | return elem 162 | 163 | unless Array.isArray @[elem.kind] 164 | exists = @[elem.kind] 165 | @[elem.kind] = [ exists ] 166 | Object.defineProperty @[elem.kind], 'keys', 167 | get: (-> @map (x) -> x.datakey ).bind @[elem.kind] 168 | unless _merge.call @[elem.kind], elem 169 | throw @error "constraint violation for '#{elem.kind} #{elem.datakey}' - cannot define more than once" 170 | 171 | return elem 172 | 173 | unless elem.kind of @scope 174 | if elem.scope? and (not elem.source.state.unbound and not @source.state.unbound) 175 | @debug => @scope 176 | throw @error "scope violation - invalid '#{elem.kind}' extension found" 177 | else 178 | @scope[elem.kind] = '*' # this is hackish... 179 | 180 | switch @scope[elem.kind] 181 | when '0..n', '1..n', '*' 182 | unless @hasOwnProperty elem.kind 183 | @[elem.kind] = [] 184 | Object.defineProperty @[elem.kind], 'keys', 185 | get: (-> @map (x) -> x.datakey ).bind @[elem.kind] 186 | unless Array.isArray @[elem.kind] 187 | exists = @[elem.kind] 188 | @[elem.kind] = [ exists ] 189 | Object.defineProperty @[elem.kind], 'keys', 190 | get: (-> @map (x) -> x.datakey ).bind @[elem.kind] 191 | unless _merge.call @[elem.kind], elem 192 | throw @error "constraint violation for '#{elem.kind} #{elem.datakey}' - already defined" 193 | when '0..1', '1' 194 | unless @hasOwnProperty elem.kind 195 | @[elem.kind] = elem 196 | else if opts.replace is true 197 | @debug => "replacing pre-existing #{elem.kind}" 198 | @[elem.kind] = elem 199 | else 200 | throw @error "constraint violation for '#{elem.kind}' - cannot define more than once" 201 | else 202 | throw @error "unrecognized scope constraint defined for '#{elem.kind}' with #{@scope[elem.kind]}" 203 | 204 | return elem 205 | 206 | removes: -> 207 | elems = ([].concat arguments...).filter (x) -> x? and !!x 208 | return this unless elems.length > 0 209 | elems.forEach (expr) => @remove expr 210 | @emit 'change', elems... 211 | return this 212 | 213 | remove: (elem) -> 214 | unless elem instanceof Element 215 | throw @error "cannot remove a non-Element from an Element", elem 216 | 217 | exists = Element::match.call this, elem.kind, elem.datakey 218 | return this unless exists? 219 | 220 | if Array.isArray @[elem.kind] 221 | @[elem.kind] = @[elem.kind].filter (x) -> x.datakey isnt elem.datakey 222 | delete @[elem.kind] unless @[elem.kind].length 223 | else 224 | delete @[elem.kind] 225 | 226 | this[kCache] = null 227 | return this 228 | 229 | ### update (element) 230 | 231 | This alternative form of [merge](#merge-element) performs conditional 232 | merge based on existence check. It is considered *safer* alternative 233 | to direct [merge](#merge-element) call. 234 | 235 | # performs conditional merge based on existence 236 | update: (elem) -> 237 | unless elem instanceof Element 238 | throw @error "cannot update a non-Element into an Element", elem 239 | 240 | #@debug => "update with #{elem.kind}/#{elem.tag}" 241 | exists = switch 242 | when elem.tag? then Element::match.call this, elem.kind, elem.datakey 243 | else Element::match.call this, elem.kind 244 | return @merge elem unless exists? 245 | 246 | #@debug => "update #{exists.kind} in-place for #{elem.children.length} elements" 247 | exists.update target for target in elem.children 248 | return exists 249 | 250 | # Looks for matching Elements using kind and tag 251 | # Direction: up the hierarchy (towards root) 252 | lookup: (kind, tag) -> 253 | #@debug => "lookup: #{kind}(#{tag})..." 254 | res = switch 255 | when this not instanceof Object then undefined 256 | when this instanceof Element then @match kind, tag 257 | else Element::match.call this, kind, tag 258 | res ?= switch 259 | when @origin? then Element::lookup.apply @origin, arguments 260 | when @parent? then Element::lookup.apply @parent, arguments 261 | else Element::match.call @constructor, kind, tag 262 | #@debug => "lookup: #{kind}(#{tag}) got result: #{res?}" 263 | return res 264 | 265 | # Looks for matching Elements using YPATH notation 266 | # Direction: down the hierarchy (away from root) 267 | at: -> @locate arguments... 268 | locate: (ypath) -> 269 | return unless ypath? 270 | if typeof ypath is 'string' 271 | @debug => "locate: #{ypath}" 272 | ypath = ypath.replace /\s/g, '' 273 | if (/^\//.test ypath) and this isnt @root 274 | return @root.locate ypath 275 | [ key, rest... ] = ypath.split('/').filter (e) -> !!e 276 | else 277 | @debug => "locate: #{ypath.join('/')}" 278 | [ key, rest... ] = ypath 279 | return this unless key? 280 | 281 | match = switch 282 | when key is '..' then @match key 283 | else @match '*', key 284 | 285 | match ?= @match key if @scope[key] in ['0..1', '1'] 286 | 287 | return switch 288 | when rest.length > 0 then match?.locate rest 289 | else match 290 | 291 | # Looks for a matching Element(s) in immediate sub-elements 292 | match: (kind, tag) -> 293 | return unless kind? and @[kind]? 294 | return @[kind] unless tag? 295 | 296 | match = @[kind] 297 | match = [ match ] unless match instanceof Array 298 | return match if tag is '*' 299 | 300 | for elem in match when elem instanceof Element 301 | return elem if tag is elem.datakey or tag is elem.tag 302 | return undefined 303 | 304 | ### toJSON 305 | 306 | Converts the Element into a JS object 307 | 308 | toJSON: (opts={ tag: true, extended: false }) -> 309 | #@debug => "converting #{@kind} toJSON with #{@children.length}" 310 | sub = 311 | @children 312 | .filter (x) => opts.extended or x.parent is this 313 | .reduce ((a,b) -> 314 | for k, v of b.toJSON() 315 | if a[k] instanceof Object 316 | a[k][kk] = vv for kk, vv of v if v instanceof Object 317 | else 318 | a[k] = v 319 | return a 320 | ), {} 321 | if opts.tag 322 | "#{@kind}": switch 323 | when Object.keys(sub).length > 0 324 | if @tag? then "#{@tag}": sub else sub 325 | when @tag instanceof Object then "#{@tag}" 326 | else @tag 327 | else sub 328 | 329 | ## Export Element Class 330 | 331 | module.exports = Element 332 | -------------------------------------------------------------------------------- /src/expression.coffee: -------------------------------------------------------------------------------- 1 | # expression - evaluable Element 2 | 3 | delegate = require 'delegates' 4 | Element = require './element' 5 | 6 | class Expression extends Element 7 | logger: require('debug')('yang:expression') 8 | # 9 | # Source delegation 10 | # 11 | delegate @prototype, 'source' 12 | .getter 'scope' 13 | .getter 'resolve' 14 | .getter 'transform' 15 | .getter 'construct' 16 | .getter 'predicate' 17 | .getter 'compose' 18 | 19 | delegate @prototype, 'state' 20 | .access 'binding' 21 | .access 'resolved' 22 | 23 | @property 'argument', 24 | get: -> @state.argument ? @source.argument 25 | set: (value) -> @state.argument = value 26 | 27 | @property 'exprs', 28 | get: -> @children.filter (x) -> x instanceof Expression 29 | 30 | @property 'nodes', 31 | get: -> @exprs.filter (x) -> x.node is true 32 | 33 | @property 'attrs', 34 | get: -> @exprs.filter (x) -> x.node is false 35 | 36 | @property 'node', 37 | get: -> @construct instanceof Function 38 | 39 | @property 'data', 40 | get: -> Boolean @source.data 41 | 42 | @property '*', get: -> @nodes 43 | 44 | #debug: (f) -> if debug.enabled logger.namespace then logger @uri, [].concat(f())... 45 | 46 | constructor: (kind, tag, source) -> 47 | super kind, tag 48 | @source = source 49 | evaluate = (-> self.eval arguments...) 50 | self = Object.setPrototypeOf evaluate, this 51 | Object.defineProperties self, 52 | inspect: value: -> @toJSON() 53 | delete self.length # TODO: this may not work for Edge browser... 54 | return self 55 | 56 | clone: -> 57 | copy = super arguments... 58 | copy.convert = @convert if @convert? 59 | return copy 60 | 61 | compile: -> 62 | @debug => "[compile] enter... (#{@resolved})" 63 | @emit 'compiling', arguments 64 | @resolve?.apply this, arguments unless @resolved 65 | if @tag? and not @argument 66 | throw @error "cannot contain argument '#{@tag}' for expression '#{@kind}'" 67 | if @argument and not @tag? 68 | throw @error "must contain argument '#{@argument}' for expression '#{@kind}'" 69 | (@debug => "has sub-expressions: #{@exprs.map (x) -> x.kind}") if @exprs.length 70 | @exprs.forEach (x) -> x.compile() 71 | @resolved = true 72 | @emit 'compiled' 73 | @debug => "[compile] done" 74 | return this 75 | 76 | bind: (data) -> 77 | if data? 78 | @debug => "[bind] registering #{typeof data} binding" 79 | @binding = data 80 | @emit 'bind', data 81 | else # allows unbinding... 82 | @debug => "[bind] removing prior #{typeof @binding} binding" 83 | @binding = undefined 84 | return this 85 | 86 | # internally used to apply the expression to the passed in data 87 | apply: (data, ctx, opts) -> 88 | @compile() unless @resolved 89 | @emit 'transforming', data 90 | if @transform instanceof Function 91 | data = @transform.call this, data, ctx, opts 92 | else 93 | data = expr.eval data, ctx, opts for expr in @exprs when data? 94 | 95 | try @predicate?.call this, data, opts 96 | catch e 97 | @debug => data 98 | throw @error "predicate validation error: #{e}", data 99 | @emit 'transformed', data 100 | return data 101 | 102 | # evalute the provided data 103 | eval: (data, ctx, opts) -> 104 | @compile() unless @resolved 105 | if @construct instanceof Function 106 | data ?= {} 107 | @construct.call this, data, ctx, opts 108 | else 109 | @apply data, ctx, opts 110 | 111 | update: (elem) -> 112 | res = super arguments... 113 | res.binding = elem.binding 114 | return res 115 | 116 | error: -> 117 | res = super arguments... 118 | res.message = "[#{@uri}] #{res.message}" 119 | res.name = 'ExpressionError' 120 | return res 121 | 122 | module.exports = Expression 123 | -------------------------------------------------------------------------------- /src/extension.coffee: -------------------------------------------------------------------------------- 1 | Expression = require './expression' 2 | 3 | class Extension extends Expression 4 | logger: require('debug')('yang:extension') 5 | @scope = 6 | argument: '0..1' 7 | description: '0..1' 8 | reference: '0..1' 9 | status: '0..1' 10 | constructor: (name, spec={}) -> 11 | spec.scope ?= {} 12 | super 'extension', name, spec 13 | 14 | module.exports = Extension 15 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | Yang = require './yang' 2 | XPath = require './xpath' 3 | Extension = require './extension' 4 | Typedef = require './typedef' 5 | Context = require './context' 6 | 7 | Store = require './store' 8 | Model = require './model' 9 | Container = require './container' 10 | List = require './list' 11 | Method = require './method' 12 | Notification = require './notification' 13 | Property = require './property' 14 | 15 | module.exports = { 16 | Yang, Extension, Typedef, XPath, Context, 17 | Store, Model, Container, List, Method, Notification, Property 18 | } 19 | -------------------------------------------------------------------------------- /src/lang/arguments.coffee: -------------------------------------------------------------------------------- 1 | P = require('xparse').Parser 2 | 3 | optSep = P.space.skipMany() 4 | 5 | identifier = 6 | P.variable.bind (v) -> 7 | (P.alphanum.orElse P.oneOf ':.-').concat().bind (rest) -> 8 | P.unit v + rest 9 | 10 | module.exports = 11 | 'node-identifier': P.create -> identifier 12 | 'if-feature-expr': P.create -> 13 | expr = -> P.union.step(term, expr) 14 | term = -> P.combine.step(factor, term) 15 | factor = -> P.choice( 16 | P.negate.bind (op) -> factor().bind (f) -> P.wrap op, f 17 | P.char('(').bind -> 18 | optSep.bind -> expr().bind (e) -> optSep.bind -> 19 | P.char(')').bind -> P.unit e 20 | identifier.bind (kw) -> P.wrap null, kw 21 | ) 22 | return expr().within(optSep) 23 | -------------------------------------------------------------------------------- /src/lang/typedefs.coffee: -------------------------------------------------------------------------------- 1 | { Typedef } = require('..') 2 | 3 | generateRangeTest = (expr) -> 4 | [ min, max ] = expr.split /\s*\.\.\s*/ 5 | min = (Number) min 6 | max = switch 7 | when max is 'max' then null 8 | else (Number) max 9 | (v) -> (not min? or v >= min) and (not max? or v <= max) 10 | 11 | class Integer extends Typedef 12 | constructor: (name, range) -> 13 | source = 14 | construct: (value, ctx, opts={}) -> 15 | if (Number.isNaN (Number value)) or ((Number value) % 1) isnt 0 16 | throw ctx.error "[#{@tag}] unable to convert '#{value}'" 17 | # treat '' string as undefined 18 | return if typeof value is 'string' and value is '' 19 | 20 | if opts.strict and typeof value isnt number 21 | throw ctx.error "[#{@tag}] must be a number but got #{typeof value}" 22 | 23 | value = Number value 24 | ranges = @range?.tag.split '|' 25 | tests = ranges.map generateRangeTest if ranges? and ranges.length 26 | unless (not tests? or tests.some (test) -> test? value) 27 | throw ctx.error "[#{@tag}] custom range violation for '#{value}' on #{ranges}" 28 | unless generateRangeTest(range)(value) 29 | throw ctx.error "[#{@tag}] range violation for '#{value}' on #{range}" 30 | value 31 | super name, source 32 | 33 | module.exports = [ 34 | 35 | new Typedef 'bits', 36 | construct: (value, ctx) -> 37 | unless @bit?.length > 0 38 | throw ctx.error "[#{@tag}] must have one or more 'bit' definitions" 39 | return unless value? and typeof value is 'string' 40 | # TODO: handle value a number in the future 41 | value = value.split ' ' 42 | unless (value.every (v) -> @bit.some (b) -> b.tag is v) 43 | throw ctx.error "[#{@tag}] invalid bit name(s) for '#{value}' on #{@bit.map (x) -> x.tag}" 44 | value 45 | 46 | new Typedef 'boolean', 47 | construct: (value, ctx) -> 48 | switch 49 | when typeof value is 'string' 50 | unless value in [ 'true', 'false' ] 51 | throw ctx.error "[#{@tag}] #{value} must be 'true' or 'false'" 52 | value is 'true' 53 | when typeof value is 'boolean' then value 54 | else throw ctx.error "[#{@tag}] unable to convert '#{value}'" 55 | 56 | new Typedef 'empty', 57 | construct: (value, ctx) -> 58 | @debug => "convert" 59 | @debug => value 60 | unless value is null 61 | throw ctx.error "[#{@tag}] cannot contain value other than null" 62 | null 63 | 64 | new Typedef 'binary', 65 | construct: (value, ctx) -> 66 | unless value instanceof Buffer 67 | throw ctx.error "[#{@tag}] unable to convert '#{value}'" 68 | value 69 | 70 | new Integer 'int8', '-128..127' 71 | new Integer 'int16', '-32768..32767' 72 | new Integer 'int32', '-2147483648..2147483647' 73 | new Integer 'int64', '-9223372036854775808..9223372036854775807' 74 | new Integer 'uint8', '0..255' 75 | new Integer 'uint16', '0..65535' 76 | new Integer 'uint32', '0..4294967295' 77 | new Integer 'uint64', '0..18446744073709551615' 78 | 79 | new Typedef 'decimal64', 80 | construct: (value, ctx) -> 81 | if Number.isNaN (Number value) 82 | throw ctx.error "[#{@tag}] unable to convert '#{value}'" 83 | # treat '' string as undefined 84 | return if typeof value is 'string' and value is '' 85 | 86 | fixed = @['fraction-digits']?.tag or 1 87 | [ a, b ] = value.toString().split('.') 88 | if b?.length > fixed 89 | throw ctx.error "[#{@tag}] fraction-digits violation for '#{value}' with more than #{fixed} decimal precision" 90 | value = Number Number(value).toFixed(fixed) 91 | ranges = @range?.tag.split '|' 92 | tests = ranges.map generateRangeTest if ranges? and ranges.length 93 | unless (not tests? or tests.some (test) -> test? value) 94 | throw ctx.error "[#{@tag}] custom range violation for '#{value}' on #{ranges}" 95 | value 96 | 97 | new Typedef 'string', 98 | construct: (value, ctx, opts={}) -> 99 | patterns = @pattern?.map (x) -> x.tag 100 | lengths = @length?.tag.split '|' 101 | tests = lengths?.map (e) -> 102 | [ min, max ] = e.split /\s*\.\.\s*/ 103 | min = (Number) min 104 | max = switch 105 | when not max? then min 106 | when max is 'max' then null 107 | else (Number) max 108 | (v) -> (not min? or v.length >= min) and (not max? or v.length <= max) 109 | 110 | return if value is null 111 | 112 | type = typeof value 113 | if opts.strict and type isnt 'string' 114 | throw ctx.error "[#{@tag}] must be a string but got #{type}" 115 | value = String value 116 | if type is 'object' and /^\[object/.test value 117 | throw ctx.error "[#{@tag}] unable to convert '#{value}' into string" 118 | unless (not tests? or tests.some (test) -> test? value) 119 | throw ctx.error "[#{@tag}] length violation for '#{value}' on #{lengths}" 120 | unless (not patterns? or patterns.every (regex) -> regex.test value) 121 | throw ctx.error "[#{@tag}] pattern violation for '#{value}'" 122 | value 123 | 124 | new Typedef 'union', 125 | construct: (value, ctx) -> 126 | unless @type? 127 | throw ctx.error "[#{@tag}] must contain one or more type definitions" 128 | for type in @type 129 | try return type.convert value 130 | catch then continue 131 | throw ctx.error "[#{@tag}] unable to find matching type for '#{value}' within: #{@type}" 132 | 133 | new Typedef 'enumeration', 134 | construct: (value, ctx) -> 135 | unless @enum?.length > 0 136 | throw ctx.error "[#{@tag}] must have one or more 'enum' definitions" 137 | for i in @enum 138 | return i.tag if value is i.tag 139 | return i.tag if value is i.value.tag 140 | return i.tag if "#{value}" is i.value.tag 141 | throw ctx.error "[#{@tag}] type violation for '#{value}' on #{@enum.map (x) -> x.tag}" 142 | 143 | # TODO 144 | new Typedef 'identityref', 145 | construct: (value, ctx) -> 146 | unless @base? and typeof @base.tag is 'string' 147 | throw ctx.error "[#{@tag}] must reference 'base' identity" 148 | 149 | return value # BYPASS FOR NOW 150 | 151 | match = @lookup 'identity', value 152 | unless match? 153 | imports = (@lookup 'import') ? [] 154 | for dep in imports 155 | match = dep.module.lookup 'identity', value 156 | break if match? 157 | unless match? 158 | modules = @lookup 'module' 159 | @debug => "fallback searching all modules #{modules.map (x) -> x.tag}" 160 | for m in modules 161 | match = m.lookup 'identity', value 162 | break if match? 163 | match = match.base.state.identity if match?.base? 164 | @debug => "base: #{@base} match: #{match} value: #{value}" 165 | # TODO - need to figure out how to return namespace value... 166 | unless (match? and @base.state.identity is match) 167 | throw ctx.error "[#{@tag}] identityref is invalid for '#{value}'" 168 | value 169 | 170 | new Typedef 'instance-identifier', 171 | construct: (value, ctx) -> 172 | @debug => "processing instance-identifier with #{value}" 173 | try 174 | prop = ctx.in value 175 | throw ctx.error "missing schema element, identifier is invalid" unless prop? 176 | if @['require-instance']?.tag and not prop.active 177 | throw ctx.error "missing instance data" 178 | catch e 179 | err = new Error "[#{@tag}] #{ctx.name} is invalid for '#{value}' (not found in #{value})" 180 | err['error-tag'] = 'data-missing' 181 | err['error-app-tag'] = 'instance-required' 182 | err['err-path'] = value 183 | err.toString = -> value 184 | throw ctx.error err if ctx.attached 185 | return err 186 | value 187 | 188 | new Typedef 'leafref', 189 | construct: (value, ctx) -> 190 | unless @path? 191 | throw new Error "[#{@tag}] must contain 'path' statement" 192 | 193 | return value if @['require-instance']?.tag is false 194 | 195 | @debug => "processing leafref with #{@path.tag}" 196 | res = ctx.get @path.tag 197 | @debug => "got back #{res}" 198 | valid = switch 199 | when res instanceof Array then res.some (x) -> "#{x}" is "#{value}" 200 | else "#{res}" is "#{value}" 201 | unless valid is true 202 | @debug => "invalid leafref '#{value}' detected for #{@path.tag}" 203 | @debug => ctx.state 204 | err = new Error "[#{@tag}] #{ctx.name} is invalid for '#{value}' (not found in #{@path.tag})" 205 | err['error-tag'] = 'data-missing' 206 | err['error-app-tag'] = 'instance-required' 207 | err['err-path'] = @path.tag 208 | err.toString = -> value 209 | throw ctx.error err if ctx.attached 210 | return err 211 | value 212 | ] 213 | -------------------------------------------------------------------------------- /src/list.coffee: -------------------------------------------------------------------------------- 1 | delegate = require 'delegates' 2 | 3 | Container = require './container' 4 | Property = require './property' 5 | XPath = require './xpath' 6 | 7 | class ListItem extends Container 8 | logger: require('debug')('yang:list:item') 9 | 10 | delegate @prototype, 'state' 11 | .getter 'key' 12 | 13 | @property 'uri', 14 | get: -> switch 15 | when @parent? then "#{@parent.uri}['#{@key}']" 16 | else @schema.datapath ? @schema.uri 17 | 18 | # @property 'uri', 19 | # get: -> (@schema.datapath ? @schema.uri) + "['#{@key}']" 20 | 21 | @property 'keys', 22 | get: -> if @schema.key then @schema.key.tag else [] 23 | 24 | @property 'pos', 25 | get: -> (@parent.props.findIndex (x) => x is this) + 1 if @parent? 26 | 27 | @property 'path', 28 | get: -> 29 | entity = switch 30 | when @keys.length then ".['#{@key}']" 31 | else ".[#{@pos}]" 32 | unless @parent? 33 | return XPath.parse entity, @schema 34 | # XXX - do not cache into @state.path since keys may change... 35 | @parent.path.clone().append entity 36 | 37 | merge: -> 38 | prevkey = @key 39 | super arguments... 40 | unless prevkey is @key 41 | @parent?.remove? key: prevkey 42 | return this 43 | 44 | update: (value, opts) -> 45 | # @debug => "[update] prior key is: #{@key}" 46 | if @keys.length and value? and not (value instanceof Property) 47 | @state.key = value['@key'] if ('@key' of value) 48 | # @debug => "[update] current key is: #{@key}" 49 | super arguments... 50 | 51 | attach: (obj, parent, opts) -> 52 | unless obj instanceof Object 53 | throw @error "list item must be an object", 'attach' 54 | opts ?= { replace: false, force: false } 55 | @parent = parent 56 | @state.key = this unless @keys.length 57 | # list item directly applies the passed in object 58 | @set obj, opts 59 | @state.attached = true 60 | return obj 61 | 62 | revert: (opts) -> 63 | return unless @changed 64 | await super opts 65 | @parent?.update this 66 | 67 | find: (pattern) -> switch 68 | # here we skip a level of hierarchy 69 | when /^\.\./.test(pattern) and @parent? 70 | @parent.find arguments... 71 | else super arguments... 72 | 73 | inspect: -> 74 | res = super arguments... 75 | res.key = @key 76 | res.keys = @keys 77 | return res 78 | 79 | class List extends Container 80 | logger: require('debug')('yang:list') 81 | 82 | @Item = ListItem 83 | @property 'value', 84 | get: -> switch 85 | when @state.value? then @props.map((item) -> item.data).filter(Boolean) 86 | else [] 87 | 88 | @property 'props', 89 | get: -> switch 90 | when @schema.key? then Array.from(@children.values()) 91 | else Array.from(@children.keys()) 92 | 93 | @property 'changed', 94 | get: -> @pending.size > 0 or (@state.changed and not @active) 95 | 96 | @property 'active', 97 | get: -> @enumerable and @children.size > 0 98 | 99 | @property 'change', 100 | get: -> switch 101 | when @changed and not @active then null 102 | when @changed and @pending.size 103 | Array.from(@changes) 104 | .filter (i) -> i.active 105 | .map (i) -> 106 | obj = i.change 107 | obj[k] = i.get(k) for k in i.keys if obj? 108 | obj 109 | 110 | # private methods 111 | 112 | _key: (s) -> "key(#{s})" 113 | 114 | add: (child, opts={}) -> 115 | return unless child.active 116 | if @schema.key? 117 | key = @_key(child.key) 118 | if @has(key) and @_get(key) isnt child 119 | @pending.delete child.key 120 | throw @error "cannot update due to key conflict: #{child.key}", 'add' 121 | @children.set(key, child) 122 | else 123 | @children.set(child) 124 | 125 | remove: (child, opts={}) -> 126 | if @schema.key? 127 | key = @_key(child.key) 128 | @children.delete(key) if @_get(key) is child 129 | else @children.delete(child) 130 | 131 | equals: (a, b) -> 132 | return false unless Array.isArray(a) and Array.isArray(b) and a.length is b.length 133 | # figure out how to deal with empty array later... 134 | # return true if a.length is 0 135 | return false 136 | # a.every (x) => b.some (y) => x is y 137 | 138 | # public methods 139 | 140 | has: (key) -> typeof key is 'string' and @schema.key? and super(key) 141 | 142 | set: (data, opts={}) -> 143 | if data? and not Array.isArray(data) 144 | throw @error "list must be an array", 'set' 145 | data = [].concat(data).filter(Boolean) if data? 146 | prev = @props 147 | @children.clear() 148 | try super data, opts 149 | catch err 150 | @children.clear() 151 | prev.forEach (prop) => @add prop 152 | throw err 153 | return this 154 | 155 | merge: (data, opts={}) -> 156 | opts.origin ?= this 157 | data = [].concat(data).filter(Boolean) if data? 158 | return @delete opts if data is null 159 | return @set data, opts if not @children.size or opts.replace 160 | return this unless data? # do nothing if data is undefined 161 | 162 | creates = [] 163 | subopts = Object.assign {}, opts, inner: true 164 | for item in data 165 | if @schema.key? and not opts.createOnly 166 | #pre = process.memoryUsage() 167 | try 168 | item = @schema.key.apply item 169 | key = @_key(item['@key']) 170 | if @has(key) 171 | if not opts.appendOnly 172 | @debug => "[merge] merge into list item for #{key}" 173 | @debug => item 174 | @_get(key).merge(item, subopts) 175 | @debug => "[merge] merge done for list item #{key}" 176 | #post = process.memoryUsage() 177 | #console.log("item growth: %d KB", (post.heapUsed - pre.heapUsed) / 1024); 178 | continue 179 | creates.push(item) 180 | try @schema.apply creates, this, subopts if creates.length 181 | catch e then throw @error e, 'create' 182 | 183 | @update this, opts # pass itself if merging 184 | 185 | # here, we validate the other min/max type schema attributes 186 | # use synchronous opts so that we do not do any await 187 | vopts = Object.assign {}, opts, sync: true 188 | newlist = @props.map (p) -> p.value 189 | try attr.apply newlist, this, vopts for attr in @schema.attrs 190 | catch e 191 | @revert vopts 192 | throw @error e, 'create' 193 | 194 | update: (value, opts) -> 195 | @remove value if value instanceof ListItem and not value.active 196 | super value, opts 197 | 198 | revert: (opts={}) -> 199 | return unless @changed 200 | return super opts unless @replaced # revisit if we need to do this 201 | 202 | # TODO: find a more optimal way to revert entire list? 203 | @debug "[revert] complete list... #{@replaced}" 204 | @set @state.prior, force: true # this will trigger 'update' events! 205 | @debug "[revert] execute binding..." unless opts.sync 206 | try await @binding?.commit? @context.with(opts) unless opts.sync 207 | catch err 208 | @debug "[revert] failed due to #{err.message}" 209 | throw @error err, 'revert' 210 | @clean opts 211 | 212 | toJSON: (key, state = true) -> 213 | value = switch 214 | when @children.size then @props.map (x) -> x.toJSON false, state 215 | else @value 216 | value = "#{@name}": value if key is true 217 | return value 218 | 219 | module.exports = List 220 | -------------------------------------------------------------------------------- /src/method.litcoffee: -------------------------------------------------------------------------------- 1 | # Method - controller of functions 2 | 3 | ## Class Method 4 | 5 | Property = require('./property') 6 | 7 | class Method extends Property 8 | logger: require('debug')('yang:method') 9 | 10 | @property 'data', 11 | set: (value) -> @set value, { force: true } 12 | get: -> switch 13 | when @binding? then @do.bind this 14 | else @value 15 | 16 | ### do () 17 | 18 | A convenience wrap to a Property instance that holds a function to 19 | perform a Promise-based execution. 20 | 21 | Always returns a Promise. 22 | 23 | do: (input={}, opts={}) -> 24 | unless (@binding instanceof Function) or (@data instanceof Function) 25 | return Promise.reject @error "cannot perform action on a property without function" 26 | @debug "[do] executing method: #{@name}" 27 | @debug input 28 | ctx = @parent?.context ? @context 29 | ctx = ctx.with opts, path: @path 30 | try 31 | # calling context is the parent node of the method 32 | input = @schema.input.apply input, this, force: true 33 | 34 | # first apply schema bound function (if availble), otherwise 35 | # execute assigned function (if available and not 'missing') 36 | if @binding? 37 | @debug "[do] calling bound function with: #{Object.keys(input)}" if typeof input is 'object' 38 | @debug @binding.toString() 39 | output = @binding? ctx, input 40 | else 41 | @debug "[do] calling assigned function: #{@data.name}" 42 | @debug => @value 43 | @debug => @data 44 | output = @data.call @parent.data, input, ctx 45 | 46 | output = await output 47 | { output } = @schema.output.eval { output }, this, force: true 48 | return output 49 | catch e 50 | @debug e 51 | return Promise.reject(e) 52 | 53 | update: (value, opts) -> 54 | super value, opts unless value instanceof Property 55 | 56 | ### toJSON 57 | 58 | This call always returns undefined for the Method node. 59 | 60 | toJSON: (key) -> 61 | value = undefined 62 | value = "#{@name}": value if key is true 63 | return value 64 | 65 | module.exports = Method 66 | -------------------------------------------------------------------------------- /src/model.litcoffee: -------------------------------------------------------------------------------- 1 | # Model - instance of schema-driven data 2 | 3 | The `Model` class aggregates [Property](./property.litcoffee) 4 | attachments to provide the *adaptive* and *event-driven* data 5 | interactions. 6 | 7 | It is typically not instantiated directly, but is generated as a 8 | result of [Yang::eval](../yang.litcoffee#eval-data-opts) for a YANG 9 | `module` schema. 10 | 11 | ```javascript 12 | var schema = Yang.parse('module foo { container bar { leaf a { type uint8; } } }'); 13 | var model = schema.eval({ 'foo:bar': { a: 7 } }); 14 | // model is { 'foo:bar': [Getter/Setter] } 15 | ``` 16 | 17 | The generated `Model` is a hierarchical composition of 18 | [Property](./property.litcoffee) instances. The instance itself uses 19 | `Object.preventExtensions` to ensure no additional properties that are 20 | not known to itself can be added. 21 | 22 | It is designed to provide *stand-alone* interactions on a per-module 23 | basis. For flexible management of multiple modules (such as hotplug 24 | modules) and data persistence, please take a look at the 25 | [yang-store](http://github.com/corenova/yang-store) project. 26 | 27 | Below are list of properties available to every instance of `Model` 28 | (it also inherits properties from [Property](./property.litcoffee)): 29 | 30 | property | type | mapping | description 31 | --- | --- | --- | --- 32 | transactable | boolean | computed | getter/setter for `state.transactable` 33 | instance | Emitter | access(state) | holds runtime features 34 | 35 | ## Dependencies 36 | 37 | Stack = require('stacktrace-parser') 38 | Emitter = require('events').EventEmitter 39 | Store = require('./store') 40 | Container = require('./container') 41 | XPath = require('./xpath') 42 | 43 | ## Class Model 44 | 45 | class Model extends Container 46 | logger: require('debug')('yang:model') 47 | constructor: -> 48 | # CS2 does not support below 49 | # unless this instanceof Model then return new Model arguments... 50 | super arguments... 51 | 52 | @state.transactable = false 53 | @state.maxTransactions = 100 54 | @state.queue = [] 55 | @state.imports = new Map 56 | @state.store = undefined 57 | 58 | # listen for schema changes and adapt! 59 | @schema.on? 'change', (elem) => 60 | @debug "[adaptive] detected schema change at #{elem.datapath}" 61 | try props = @find(elem.datapath) 62 | catch then props = [] 63 | props.forEach (prop) -> prop.set prop.content, force: true 64 | 65 | @debug "created a new YANG Model: #{@name}" 66 | 67 | ### Computed Properties 68 | 69 | enqueue = (prop) -> 70 | if @queue.length > @maxTransactions 71 | throw prop.error "exceeded max transaction queue of #{@maxTransactions}, forgot to save()?" 72 | @queue.push { target: prop, value: prop.state.prev } 73 | 74 | @property 'transactable', 75 | enumerable: true 76 | get: -> @state.transactable 77 | set: (toggle) -> 78 | return if toggle is @state.transactable 79 | if toggle is true 80 | @state.on 'update', enqueue 81 | else 82 | @state.removeListener 'update', enqueue 83 | @state.queue.splice(0, @state.queue.length) 84 | @state.transactable = toggle 85 | 86 | @property 'store', 87 | get: -> @state.store 88 | set: (store) -> @state.store = store 89 | 90 | ### attach 91 | 92 | attach: (obj, parent, opts) -> 93 | return this unless obj instanceof Object 94 | @store = parent?.store ? new Store 95 | 96 | @set obj, opts unless @attached 97 | @store.add this 98 | @state.attached = true 99 | return this 100 | 101 | ### access (model) 102 | 103 | This is a unique capability for a Model to be able to access any 104 | other arbitrary model present inside the `Model.store`. 105 | 106 | access: (model) -> @store?.access(model) 107 | 108 | ### save 109 | 110 | This routine triggers a 'commit' event for listeners to handle any 111 | persistence operations. It also clears the `@state.queue` transaction 112 | queue so that future [rollback](#rollback) will reset back to this 113 | state. 114 | 115 | save: -> 116 | @debug "[save] trigger commit and clear queue" 117 | @emit 'commit', @state.queue.slice(); 118 | @state.queue.splice(0, @state.queue.length) 119 | return this 120 | 121 | ### rollback 122 | 123 | This routine will replay tracked `@state.queue` in reverse chronological 124 | order (most recent -> oldest) when `@transactable` is set to 125 | `true`. It will restore the Property instance back to the last known 126 | [save](#save-opts) state. 127 | 128 | rollback: -> 129 | while change = @state.queue.pop() 130 | change.target.set change.value, suppress: true 131 | return this 132 | 133 | ## Prototype Overrides 134 | 135 | ### on (event) 136 | 137 | The `Model` instance registers `@state` as an `EventEmitter` and you 138 | can attach various event listeners to handle events generated by the 139 | `Model`: 140 | 141 | event | arguments | description 142 | --- | --- | --- 143 | update | (prop, prev) | fired when an update takes place within the data tree 144 | change | (elems...) | fired when the schema is modified 145 | create | (items...) | fired when one or more `list` element is added 146 | delete | (items...) | fired when one or more `list` element is deleted 147 | 148 | It also accepts optional XPATH/YPATH expressions which will *filter* 149 | for granular event subscription to specified events from only the 150 | elements of interest. 151 | 152 | The event listeners to the `Model` can handle any customized behavior 153 | such as saving to database, updating read-only state, scheduling 154 | background tasks, etc. 155 | 156 | This operation is protected from recursion, where operations by the 157 | `callback` may result in the same `callback` being executed multiple 158 | times due to subsequent events triggered due to changes to the 159 | `Model`. Currently, it will allow the same `callback` to be executed 160 | at most two times within the same execution stack. 161 | 162 | emit: (event) -> 163 | super arguments... 164 | @store?.emit arguments... unless event is 'error' 165 | 166 | on: (event, filters..., callback) -> 167 | unless callback instanceof Function 168 | throw new Error "must supply callback function to listen for events" 169 | 170 | recursive = (name) -> 171 | seen = {} 172 | frames = Stack.parse(new Error().stack) 173 | for frame, i in frames when ~frame.methodName.indexOf(name) 174 | { file, lineNumber, column } = frames[i-1] 175 | callee = "#{file}:#{lineNumber}:#{column}" 176 | seen[callee] ?= 0 177 | if ++seen[callee] > 1 178 | console.warn "detected recursion for '#{callee}'" 179 | return true 180 | return false 181 | 182 | ctx = @context 183 | $$$ = (prop, args...) -> 184 | debug? "$$$: check if '#{prop.path}' in '#{filters}'" 185 | if not filters.length or prop.path.contains filters... 186 | unless recursive('$$$') 187 | callback.apply ctx, [prop].concat args 188 | 189 | @state.on event, $$$ 190 | 191 | Please refer to [Model Events](../TUTORIAL.md#model-events) section of 192 | the [Getting Started Guide](../TUTORIAL.md) for usage examples. 193 | 194 | ### find (pattern) 195 | 196 | This routine enables *cross-model* property search when the `Model` is 197 | joined to another object (such as a datastore). The schema-bound model 198 | restricts *cross-model* property access to only those modules that are 199 | `import` dependencies of the current model instance. 200 | 201 | find: (pattern='.', opts={}) -> 202 | return super arguments... unless @attached 203 | 204 | @debug "[find] match #{pattern} (root: #{opts.root})" 205 | try match = super pattern, root: true 206 | catch e then match = [] 207 | return match if match.length or opts.root 208 | 209 | xpath = switch 210 | when pattern instanceof XPath then pattern 211 | else XPath.parse pattern, @schema 212 | return [] unless xpath.xpath? 213 | 214 | [ target ] = xpath.xpath.tag.split(':') 215 | return [] if target is @name 216 | 217 | # enforce cross-model access only to import dependencies 218 | return [] unless @schema.import?.some (x) -> x.tag is target 219 | 220 | @debug "[find] locate #{target} and apply #{xpath}" 221 | opts.root = true 222 | try return @access(target).find xpath, opts 223 | # TODO: below is kind of heavy-handed... 224 | try return @schema.lookup('module', target).eval(@content).find xpath, opts 225 | return [] 226 | 227 | ## Export Model Class 228 | 229 | module.exports = Model 230 | -------------------------------------------------------------------------------- /src/node.coffee: -------------------------------------------------------------------------------- 1 | debug = require('debug')('yang:node') 2 | path = require 'path' 3 | fs = require 'fs' 4 | 5 | Yang = require './yang' # needed for Yang::match 6 | 7 | resolvePackagePath = (base, target, name, pkginfo = {}) -> 8 | { dependencies = {}, peerDependencies = {} } = pkginfo 9 | if (target of dependencies) or (target of peerDependencies) 10 | debug "[resolve:#{name}] find #{target} package location" 11 | # due to npm changes, the dependency may be at higher in the 12 | # directory tree instead of being found inside subdirectory 13 | while base != path.dirname base # not at the root of filesystem 14 | pkgdir = path.resolve base, 'node_modules', target 15 | debug "[resolve:#{name}] look for #{target} package in #{pkgdir}" 16 | return pkgdir if fs.existsSync pkgdir 17 | base = path.dirname base 18 | 19 | DEFAULT_SEARCH_ORDER = ['.js', '.yang'] 20 | 21 | scanDirectory = (dir, name, checked={}) -> 22 | dir = path.resolve dir 23 | return null if dir of checked 24 | checked[dir] = true 25 | debug "[resolve:#{name}] scanning #{dir} folder..." 26 | try 27 | source = path.resolve dir, "package.json" 28 | debug "[resolve:#{name}] try opening #{source} inside #{dir}" 29 | pkginfo = JSON.parse(fs.readFileSync(source)) 30 | 31 | if pkginfo? 32 | { search = [], order, resolve } = pkginfo.yang if pkginfo.yang? 33 | target = resolve?[name] ? pkginfo.models?[name] 34 | if target? 35 | debug "[resolve:#{name}] check '#{target}' defined in #{pkginfo.name} package.json" 36 | unless !!path.extname target 37 | # target is not a filename, check if it's a package 38 | where = resolvePackagePath dir, target, name, pkginfo 39 | where ?= path.resolve dir, target # probably folder otherwise 40 | file = scanDirectory where, name, checked 41 | else 42 | # target is an explicit filename 43 | file = path.resolve dir, target 44 | return file if fs.existsSync file 45 | throw @error "unable to resolve (#{name}) using explicit target (#{target}) definition inside #{pkginfo.name} package.json" 46 | else if pkginfo.name is name 47 | # target is the package itself 48 | return path.dirname source 49 | 50 | # try using search target array 51 | for target in [].concat(search...) 52 | where = resolvePackagePath dir, target, name, pkginfo 53 | where ?= path.resolve dir, target 54 | file = scanDirectory where, name, checked 55 | return file if fs.existsSync file 56 | 57 | # we didn't find explicit match inside package.json or there wasn't one in the dir 58 | order ?= DEFAULT_SEARCH_ORDER; 59 | debug "[resolve:#{name}] scanning #{dir} folder using [#{order}] extension order..." 60 | for filename in ([].concat(order...).map (ext) -> "#{name}#{ext}") 61 | file = path.resolve dir, filename 62 | debug "[resolve:#{name}] checking for file #{file}..." 63 | return file if fs.existsSync file 64 | return null 65 | 66 | YangNodeUtils = { 67 | 68 | ### resolve (from..., name) 69 | 70 | This call is used to perform a search within the local filesystem to 71 | locate a given YANG schema module by `name`. 72 | 73 | 1. It will first check the calling code's local 74 | [package.json](../package.json) to look for a `yang: { resolve: {} }` 75 | configuration section to identify where the target module can be 76 | found. 77 | 78 | 1a. If entry defined, it will then follow that 79 | reference - which may be a JS file, YANG schema text file, another 80 | NPM module or a directory. 81 | 82 | 1b. If no entry defined, it will then check for `yang: { search: [] }` 83 | configuration section to perform directory search. 84 | 85 | If it is not found within the `yang: { resolve: {} }` configuration 86 | block or it fails to load the referenced dependency, it will then 87 | fallback to attempt to locate a YANG schema text file in the same 88 | folder that the `resolve` request was made: `#{name}.yang`. 89 | 90 | ### 91 | resolve: (search..., name) -> 92 | return null unless typeof name is 'string' 93 | # use current directory if called without search dirs 94 | search.push path.resolve() unless search.length 95 | checked = {} # keep track of already checked directories 96 | for dir in search 97 | dir = path.resolve dir 98 | while dir != path.dirname dir 99 | match = scanDirectory dir, name, checked 100 | if fs.existsSync match 101 | debug "[resolve:#{name}] found #{match}" 102 | return match 103 | dir = path.dirname dir # go up a directory 104 | return null 105 | 106 | ### import (name [, opts={}]) 107 | 108 | This call provides a convenience mechanism for dealing with YANG 109 | schema module dependencies. It performs parsing of the YANG schema 110 | content from the specified `name` and saves the generated `Yang` 111 | expression inside the internal registry. The `name` can be a YANG 112 | module name or a *filename* to the actual schema content (JS or YANG). 113 | 114 | Once a given YANG module has been saved inside the registry, 115 | subsequent [parse](#parse-schema) of YANG schema that *import* the 116 | saved module will successfully resolve. 117 | 118 | Typical usage scenario for this pattern is to internally define common 119 | modules such as `ietf-yang-types` which can then be *imported* by 120 | other schemas. 121 | 122 | It will also return the new `Yang` expression instance (to do with as 123 | you please). 124 | 125 | Please note that this method will look for the `name` in current 126 | working directory of the script execution if the `name` is a relative 127 | path. It utilizes the [resolve](#resolve-from-name) method and will 128 | attempt to **recursively** resolve any failed `import` dependencies. 129 | 130 | While this is a convenient abstraction, it is **recommended** to 131 | directly use the Node.js built-in `require` mechanism (if 132 | available). Using native `require` instead of `Yang.import` will 133 | allow package bundlers such as `browserify` to capture the 134 | dependencies as part of the produced bundle. It also allows you to 135 | directly load YANG schema files from other NPM modules. 136 | 137 | By default, loading the [yang-js](./main.coffee) module will attempt 138 | to associate `.yang` extension inside `require` facility. If 139 | available, it will allow you to `require('./some-dependency.yang')` 140 | and get back a parsed `Yang expression` instance. 141 | 142 | ### 143 | import: (name, opts={}) -> 144 | return unless name? 145 | opts.basedir ?= '' 146 | extname = path.extname name 147 | filename = path.resolve opts.basedir, name 148 | basedir = path.dirname filename 149 | 150 | debug "[import] trying #{name}..." 151 | unless !!extname 152 | return (Yang::match.call this, 'module', name) ? @import (@resolve name), opts 153 | 154 | unless extname is '.yang' 155 | res = require filename 156 | unless res instanceof Yang 157 | throw @error "unable to import '#{name}' from '#{filename}' (not Yang expression)", res 158 | return res 159 | 160 | try return @use (@parse (fs.readFileSync filename, 'utf-8'), opts) 161 | catch e 162 | debug? e 163 | unless opts.compile and e.name is 'ExpressionError' and e.src.kind in [ 'include', 'import' ] 164 | console.error "unable to parse '#{name}' YANG module from '#{filename}'" 165 | throw e 166 | switch e.src.kind 167 | when 'import' 168 | throw e if e.src.module? 169 | when 'include' 170 | opts = Object.assign {}, opts 171 | opts.compile = false 172 | 173 | # try to find the dependency module for import 174 | dependency = @import (@resolve basedir, e.src.tag), opts 175 | unless dependency? 176 | e.message = "unable to auto-resolve '#{e.src.tag}' dependency module from '#{filename}'" 177 | throw e 178 | unless dependency.tag is e.src.tag 179 | e.message = "found mismatching module '#{dependency.tag}' while resolving '#{e.src.tag}'" 180 | throw e 181 | 182 | # retry the original request 183 | debug "[import] retrying for #{name}..." 184 | return @import arguments... 185 | } 186 | 187 | module.exports = YangNodeUtils 188 | -------------------------------------------------------------------------------- /src/notification.coffee: -------------------------------------------------------------------------------- 1 | Container = require './container' 2 | 3 | class Notification extends Container 4 | logger: require('debug')('yang:notification') 5 | merge: (value, opts) -> @set value, opts 6 | 7 | module.exports = Notification 8 | -------------------------------------------------------------------------------- /src/store.coffee: -------------------------------------------------------------------------------- 1 | delegate = require('delegates') 2 | Container = require('./container') 3 | 4 | class Store extends Container 5 | logger: require('debug')('yang:store') 6 | 7 | constructor: -> 8 | # CS2 does not support below 9 | # unless this instanceof Store then return new Store arguments... 10 | super arguments... 11 | @state.schemas = new Set 12 | @state.models = new Map 13 | 14 | delegate @prototype, 'state' 15 | .getter 'schemas' 16 | .getter 'models' 17 | 18 | delegate @prototype, 'models' 19 | .method 'has' 20 | 21 | @property 'store', 22 | get: -> this 23 | 24 | use: (schemas...) -> 25 | schemas 26 | .filter (s) -> s.kind is 'module' 27 | .forEach (s) => @schemas.add(s) 28 | return this 29 | 30 | add: (models...) -> 31 | models 32 | .filter (m) -> m.kind is 'module' 33 | .forEach (m) => 34 | m.on 'error', @emit.bind(this,'error') 35 | @models.set(m.name, m) 36 | return this 37 | 38 | access: (model) -> 39 | unless @models.has(model) 40 | throw @error "unable to locate '#{model}' instance in the Store" 41 | return @models.get(model) 42 | 43 | set: (data) -> 44 | @models.clear() 45 | @schemas.forEach (s) => s.eval(data, this) 46 | return this 47 | 48 | find: (pattern, opts) -> 49 | i = @models.entries() 50 | while( v = i.next(); !v.done) 51 | [key, value] = v.value 52 | match = value.find(pattern, opts) 53 | return match if match.length 54 | return [] 55 | 56 | toJSON: (key, state = true) -> 57 | obj = {} 58 | i = @models.entries() 59 | while( v = i.next(); !v.done) 60 | [name, model] = v.value 61 | continue unless model? 62 | obj[k] = v for k, v of model.toJSON false, state 63 | return obj 64 | 65 | module.exports = Store 66 | -------------------------------------------------------------------------------- /src/typedef.coffee: -------------------------------------------------------------------------------- 1 | Expression = require './expression' 2 | 3 | class Typedef extends Expression 4 | logger: require('debug')('yang:typedef') 5 | constructor: -> 6 | super 'typedef', arguments... 7 | 8 | @property 'basetype', get: -> @tag 9 | @property 'convert', get: -> @construct ? (x) -> x 10 | 11 | module.exports = Typedef 12 | -------------------------------------------------------------------------------- /src/xpath.coffee: -------------------------------------------------------------------------------- 1 | Expression = require('./expression') 2 | xparse = require('xparse') 3 | 4 | class Filter extends Expression 5 | logger: require('debug')('yang:xpath:filter') 6 | 7 | constructor: (pattern='') -> 8 | source = 9 | argument: 'predicate' 10 | scope: {} 11 | transform: (prop) -> 12 | expr = @tag 13 | switch typeof expr 14 | when 'number' then prop.props[expr-1] 15 | when 'string' then prop.children.get("key(#{expr})") 16 | else 17 | props = switch 18 | when prop.kind is 'list' then prop.props 19 | else [ prop ] 20 | props.filter (prop) -> expr (name, arg) -> 21 | elem = prop.data 22 | return elem[name] unless arg? 23 | switch name 24 | when 'current' then elem 25 | when 'false' then false 26 | when 'true' then true 27 | when 'key' then arg 28 | when 'name' then elem[arg] 29 | 30 | super 'filter', xparse(pattern), source 31 | @pattern = pattern 32 | 33 | clone: -> new @constructor @pattern 34 | toString: -> @pattern 35 | 36 | class XPath extends Expression 37 | logger: require('debug')('yang:xpath') 38 | 39 | @split: (pattern) -> 40 | elements = pattern.match /([^\/^\[]*(?:\[.+?\])*)/g 41 | elements ?= [] 42 | elements = elements.filter (x) -> !!x 43 | return elements 44 | 45 | constructor: (pattern, schema) -> 46 | return pattern if pattern instanceof XPath 47 | 48 | unless typeof pattern is 'string' 49 | throw new Error "must pass in 'pattern' as valid string" 50 | 51 | elements = XPath.split(pattern) 52 | 53 | if /^\//.test pattern 54 | target = '/' 55 | schema = schema.root if schema instanceof Expression 56 | predicates = [] 57 | else 58 | unless elements.length > 0 59 | throw new Error "unable to process '#{pattern}' (please check your input)" 60 | [ target, predicates... ] = elements.shift().split /\[\s*(.+?)\s*\]/ 61 | unless target? 62 | throw new Error "unable to process '#{pattern}' (missing axis)" 63 | predicates = predicates.filter (x) -> !!x 64 | if schema instanceof Expression 65 | try match = schema.locate target 66 | catch e then console.warn e 67 | unless match? then switch schema.kind 68 | when 'list' 69 | predicates.unshift switch 70 | when schema.key? then "'#{target}'" 71 | else target 72 | target = '.' 73 | when 'anydata' then schema = undefined 74 | else 75 | throw new Error "unable to locate '#{target}' inside schema: #{schema.uri}" 76 | else 77 | schema = match 78 | target = schema.datakey unless /^\./.test target 79 | 80 | source = 81 | argument: 'node' 82 | scope: 83 | filter: '0..n' 84 | xpath: '0..1' 85 | transform: (data) -> @process data 86 | 87 | super 'xpath', target, source 88 | 89 | if schema instanceof Expression 90 | Object.defineProperty this, 'schema', value: schema 91 | 92 | @extends (predicates.map (x) -> new Filter x)... if predicates.length > 0 93 | @extends elements.join('/') if elements.length > 0 94 | 95 | @property 'tail', 96 | get: -> 97 | end = this 98 | end = end.xpath while end.xpath? 99 | return end 100 | 101 | process: (data) -> 102 | @debug "[#{@tag}] process using schema from #{@schema?.kind}:#{@schema?.tag}" 103 | return [] unless data instanceof Object 104 | 105 | # 1. find all matching props 106 | data = [].concat(data) 107 | data = data.reduce ((a, prop) => a.concat(@match(prop))), [] 108 | return @xpath.eval data if @xpath? and data.length 109 | 110 | @debug "[#{@tag}] returning #{data.length} properties" 111 | # @debug data 112 | return data 113 | 114 | match: (prop) -> 115 | # console.warn('MATCH', @tag, prop.children); 116 | result = switch 117 | when @tag is '/' then prop.root 118 | when @tag is '.' then prop 119 | when @tag is '..' then prop.parent 120 | when @tag is '*' then prop.props 121 | when prop.children.has(@tag) then prop.children.get(@tag) 122 | when prop.kind is 'list' then prop.props.map (li) => li.children.get(@tag) 123 | when @schema? then prop.children.get(@schema.datakey) 124 | result = [].concat(result).filter(Boolean); 125 | # console.warn('MATCH RESULT', result); 126 | 127 | # 2. filter by predicate(s) and sub-expressions 128 | if @filter? 129 | for expr in @filter 130 | break unless result.length 131 | result = result.reduce ((a, b) -> 132 | a.concat(expr.eval(b)).filter(Boolean) 133 | ), [] 134 | 135 | return result 136 | 137 | clone: -> 138 | schema = if @tag is '/' then @schema else @parent?.schema 139 | (new @constructor @tag, schema).extends @exprs.map (x) -> x.clone() 140 | 141 | merge: (elem) -> 142 | elem = switch 143 | when elem instanceof Expression then elem 144 | else new XPath elem, @schema 145 | if elem.tag is '.' 146 | @extends elem.filter, elem.xpath 147 | return this 148 | else super elem 149 | 150 | # returns the XPATH instance found matching the `pattern` 151 | locate: (pattern) -> 152 | try 153 | pattern = new XPath pattern, @schema unless pattern instanceof XPath 154 | return unless @tag is pattern.tag 155 | return unless not pattern.filter? or "#{@filter}" is "#{pattern.filter}" 156 | switch 157 | when @xpath? and pattern.xpath? then @xpath.locate pattern.xpath 158 | when pattern.xpath? then undefined 159 | else this 160 | 161 | # trims the current XPATH expressions after matching `pattern` 162 | trim: (pattern) -> 163 | match = @locate pattern 164 | delete match.xpath if match? 165 | return this 166 | 167 | # append a new pattern at the tail of the current XPATH expression 168 | append: (pattern) -> 169 | @tail.merge pattern 170 | return this 171 | 172 | # returns the XPATH `pattern` that matches part or all of this XPATH instance 173 | contains: (patterns...) -> 174 | for pattern in patterns 175 | return pattern if @locate(pattern)? 176 | 177 | toString: -> 178 | s = if @tag is '/' then '' else @tag 179 | if @filter? 180 | s += "[#{filter}]" for filter in @filter 181 | if @xpath? 182 | s += "/#{@xpath}" 183 | s = @tag if !s 184 | return s 185 | 186 | exports = module.exports = XPath 187 | exports.Filter = Filter 188 | exports.parse = (pattern, schema) -> new XPath pattern, schema 189 | -------------------------------------------------------------------------------- /test/ChangeLog: -------------------------------------------------------------------------------- 1 | 2016-08-12 Peter K. Lee 2 | 3 | * yang-example-jukebox.coffee (should): 4 | 5 | -------------------------------------------------------------------------------- /test/extension/choice.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe 'simple schema', -> 4 | schema = 'choice foo;' 5 | 6 | it "should parse simple choice statement", -> 7 | y = Yang.parse schema 8 | y.should.have.property('tag').and.equal('foo') 9 | 10 | describe 'extended schema', -> 11 | schema = """ 12 | choice foo { 13 | case a { 14 | leaf bar1; 15 | } 16 | case b { 17 | leaf bar2; 18 | } 19 | default a; 20 | } 21 | """ 22 | it "should parse extended choice statement", -> 23 | y = Yang.parse schema 24 | y.case.should.be.instanceOf(Array).and.have.length(2) 25 | 26 | it "should create extended choice element (default a)", -> 27 | o = (Yang.parse schema)() 28 | o.should.have.property('bar1') 29 | 30 | it "should select case b choice element", -> 31 | o = (Yang.parse schema) bar2: 'hi' 32 | o.should.have.property('bar2') 33 | o.should.not.have.property('bar1') 34 | 35 | describe 'without case', -> 36 | schema = """ 37 | choice foo { 38 | container bar { 39 | leaf a; 40 | leaf b; 41 | } 42 | } 43 | """ 44 | it "should parse choice statement without case", -> 45 | y = Yang.parse schema 46 | y.case.should.have.length(1) 47 | 48 | it "should parse more than one non-case data node", -> 49 | (-> Yang.parse 'choice foo { leaf a; leaf b; }' ).should.not.throw() 50 | 51 | describe 'invalid schema', -> 52 | 53 | it "should reject default and mandatory set at same time", -> 54 | (-> Yang.parse 'choice foo { mandatory true; default a; }' ).should.throw() 55 | 56 | it "should reject default without matching case", -> 57 | (-> Yang.parse 'choice foo { default a; }' ).should.throw() 58 | -------------------------------------------------------------------------------- /test/extension/container.coffee: -------------------------------------------------------------------------------- 1 | describe 'simple schema', -> 2 | schema = 'container foo;' 3 | 4 | it "should parse simple container statement", -> 5 | y = Yang.parse schema 6 | y.should.have.property('tag').and.equal('foo') 7 | 8 | it "should create simple container element", -> 9 | o = (Yang.parse schema)() 10 | o.should.have.property('foo') 11 | 12 | it "should allow setting an arbitrary object", -> 13 | o = (Yang.parse schema)() 14 | o.foo = bar: [ 'hello', 'world' ] 15 | o.foo.should.have.property('bar') 16 | 17 | it "should validate object assignment", -> 18 | o = (Yang.parse schema)() 19 | (-> o.foo = 'hello').should.throw() 20 | (-> o.foo = bar: 'hello').should.not.throw() 21 | 22 | describe 'extended schema', -> 23 | schema = """ 24 | container foo { 25 | description "extended container test"; 26 | leaf-list vegetables; 27 | leaf favorite; 28 | } 29 | """ 30 | it "should parse extended container statement", -> 31 | y = Yang.parse schema 32 | y.leaf.should.be.instanceOf(Array).and.have.length(1) 33 | 34 | it "should create extended container element", -> 35 | o = (Yang.parse schema) foo: favorite: 'bar' 36 | o.foo.should.have.property('favorite') 37 | 38 | describe 'nested schema', -> 39 | schema = """ 40 | container foo { 41 | description "nested container test"; 42 | container bar1 { 43 | leaf hello; 44 | } 45 | container bar2 { 46 | leaf world; 47 | } 48 | } 49 | """ 50 | it "should parse nested container statement", -> 51 | y = Yang.parse schema 52 | y.container.should.be.instanceOf(Array).and.have.length(2) 53 | 54 | it "should create nested container element", -> 55 | o = (Yang.parse schema) foo: { bar1: {}, bar2: {} } 56 | o.foo.should.have.properties('bar1','bar2') 57 | 58 | -------------------------------------------------------------------------------- /test/extension/extension.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe 'simple extension', -> 4 | schema = """ 5 | extension foo-ext { 6 | description "A simple extension"; 7 | argument "name"; 8 | } 9 | """ 10 | 11 | it "should parse simple extension statement", -> 12 | y = Yang.parse schema 13 | y.should.have.property('tag').and.equal('foo-ext') 14 | y.should.have.property('kind').and.equal('extension') 15 | 16 | describe 'extended extension', -> 17 | schema = """ 18 | module foo { 19 | extension c-define { 20 | description 21 | "Takes as argument a name string. 22 | Makes the code generator use the given name in the 23 | #define."; 24 | argument "name"; 25 | } 26 | container interfaces { 27 | c-define "MY_INTERFACES"; 28 | } 29 | } 30 | """ 31 | 32 | it "should parse use of extension statement", -> 33 | y = Yang.parse schema 34 | y.should.have.property('tag').and.equal('foo') 35 | 36 | it "should handle binding extension", -> 37 | y = Yang.parse(schema).bind 'extension(c-define)': construct: (a) -> a 38 | y.locate('foo:interfaces').nodes.should.have.length(1); 39 | 40 | describe 'unknown extension', -> 41 | schema = """ 42 | module foo { 43 | something; 44 | unknown-define "HELLO"; 45 | } 46 | """ 47 | it "should fail parsing unknown extension statement", -> 48 | (-> Yang.parse schema).should.throw() 49 | 50 | describe 'imported extension', -> 51 | imported_schema = """ 52 | module foo2 { 53 | extension c-define { 54 | description 55 | "Takes as argument a name string. 56 | Makes the code generator use the given name in the 57 | #define."; 58 | argument "name"; 59 | } 60 | } 61 | """ 62 | schema= """ 63 | module bar { 64 | import foo2 { 65 | prefix foo; 66 | } 67 | container interfaces { 68 | foo:c-define "MY_INTERFACES"; 69 | } 70 | } 71 | """ 72 | it "should parse imported extension", -> 73 | y1 = Yang.use (Yang.parse imported_schema) 74 | y2 = Yang.parse schema 75 | y2.should.have.property('tag').and.equal('bar') 76 | 77 | describe 'extension without argument', -> 78 | schema = """ 79 | module foo { 80 | extension config_locked { 81 | description "no argument extension"; 82 | } 83 | container test { 84 | config_locked; 85 | } 86 | } 87 | """ 88 | it "should parse extension without argument", -> 89 | (-> Yang.parse schema).should.not.throw() 90 | -------------------------------------------------------------------------------- /test/extension/grouping.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe 'simple schema', -> 4 | schema = 'grouping foo;' 5 | 6 | it "should parse simple grouping statement", -> 7 | y = Yang.parse schema 8 | y.should.have.property('tag').and.equal('foo') 9 | 10 | it "should not create simple grouping element", -> 11 | o = (Yang.parse schema)() 12 | should.not.exist(o) 13 | 14 | describe 'extended schema', -> 15 | schema = """ 16 | grouping foo { 17 | description "extended grouping test"; 18 | leaf-list vegetables; 19 | leaf favorite; 20 | } 21 | """ 22 | it "should parse extended grouping statement", -> 23 | y = Yang.parse schema 24 | y.leaf.should.be.instanceOf(Array).and.have.length(1) 25 | 26 | it "should not create extended grouping element", -> 27 | o = (Yang.parse schema)() 28 | should.not.exist(o) 29 | 30 | describe 'nested schema', -> 31 | schema = """ 32 | grouping foo { 33 | description "extended grouping test"; 34 | grouping attr { 35 | leaf color; 36 | } 37 | container fruit { 38 | uses attr; 39 | leaf favorite { type boolean; } 40 | } 41 | } 42 | """ 43 | it "should parse nested grouping statement", -> 44 | y = Yang.parse schema 45 | y.grouping.should.be.instanceOf(Array).and.have.length(1) 46 | 47 | it "should not create nested grouping element", -> 48 | o = (Yang.parse schema)() 49 | should.not.exist(o) 50 | 51 | describe 'uses schema', -> 52 | schema = """ 53 | container top { 54 | description "grouping usage test"; 55 | grouping foo { 56 | leaf bar; 57 | } 58 | container user { 59 | uses foo; 60 | leaf name { type string; } 61 | leaf active { type boolean; } 62 | } 63 | } 64 | """ 65 | it "should parse grouping uses container statement", -> 66 | y = Yang.parse schema 67 | y.grouping.should.be.instanceOf(Array).and.have.length(1) 68 | 69 | it "should create grouping uses container element", -> 70 | o = (Yang.parse schema) top: user: bar: 'foo' 71 | o.top.should.have.property('user').and.have.property('bar') 72 | 73 | it "should check valid grouping reference during parse", -> 74 | invalid = """ 75 | container top { 76 | grouping foo { 77 | leaf bar; 78 | } 79 | container user { 80 | uses nofoo; 81 | } 82 | } 83 | """ 84 | (-> Yang.parse invalid).should.throw() 85 | 86 | describe 'refine schema', -> 87 | schema = """ 88 | container top { 89 | description "grouping refine test"; 90 | grouping foo { 91 | leaf bar; 92 | } 93 | container user { 94 | uses foo { 95 | refine 'bar' { 96 | config false; 97 | description "refined bar description"; 98 | } 99 | } 100 | leaf name { type string; } 101 | leaf active { type boolean; } 102 | } 103 | } 104 | """ 105 | it "should parse grouping refine container statement", -> 106 | y = Yang.parse schema 107 | y.grouping.should.be.instanceOf(Array).and.have.length(1) 108 | bar = y.locate('user/bar') 109 | bar.should.have.property('config').property('tag').equal(false) 110 | 111 | 112 | describe 'augment schema', -> 113 | schema = """ 114 | container top { 115 | description "grouping augment test"; 116 | grouping foo { 117 | container bar; 118 | } 119 | container user { 120 | uses foo { 121 | augment 'bar' { 122 | description "refined bar description"; 123 | leaf extra; 124 | } 125 | } 126 | leaf name { type string; } 127 | leaf active { type boolean; } 128 | } 129 | } 130 | """ 131 | it "should parse grouping augment container statement", -> 132 | y = Yang.parse schema 133 | y.grouping.should.be.instanceOf(Array).and.have.length(1) 134 | bar = y.locate('user/bar') 135 | bar.should.have.property('leaf') 136 | 137 | 138 | -------------------------------------------------------------------------------- /test/extension/leaf-list.coffee: -------------------------------------------------------------------------------- 1 | describe 'simple schema', -> 2 | schema = 'leaf-list foo;' 3 | 4 | it "should parse simple leaf-list statement", -> 5 | y = Yang.parse schema 6 | y.should.have.property('tag').and.equal('foo') 7 | 8 | it "should create simple leaf-list element", -> 9 | o = (Yang.parse schema) foo: [ 'hello' ] 10 | o.should.have.property('foo').and.be.instanceOf(Array) 11 | o.foo.should.have.length(1) 12 | o.foo[0].should.equal('hello') 13 | 14 | it "should allow setting a new leaf-list", -> 15 | o = (Yang.parse schema)() 16 | o.foo = [ 'hello', 'world' ] 17 | o.foo.should.be.instanceOf(Array).and.have.length(2) 18 | 19 | describe 'extended schema', -> 20 | schema = """ 21 | leaf-list foo { 22 | description "extended leaf-list foo"; 23 | min-elements 1; 24 | max-elements 5; 25 | } 26 | """ 27 | it "should parse extended leaf-list statement", -> 28 | y = Yang.parse schema 29 | y['min-elements'].should.have.property('tag').and.equal(1) 30 | y['max-elements'].should.have.property('tag').and.equal(5) 31 | 32 | it "should create extended leaf-list element", -> 33 | o = (Yang.parse schema) foo: [ 'hello' ] 34 | o.foo.should.be.instanceOf(Array).and.have.length(1) 35 | 36 | it "should validate min/max elements constraint", -> 37 | o = (Yang.parse schema) foo: [ 'hello' ] 38 | (-> o.foo = []).should.throw() 39 | (-> o.foo = [ 1, 2, 3, 4, 5, 6 ]).should.throw() 40 | (-> o.foo = [ 1, 2, 3, 4, 5 ]).should.not.throw() 41 | 42 | it.skip "should support order-by condition", -> 43 | 44 | describe 'typed schema', -> 45 | schema = """ 46 | leaf-list foo { 47 | type string; 48 | } 49 | """ 50 | it "should parse type extended leaf-list statement", -> 51 | y = Yang.parse schema 52 | y.type.should.have.property('tag').and.equal('string') 53 | 54 | it "should create type extended leaf-list element", -> 55 | o = (Yang.parse schema) foo: [] 56 | o.should.have.property('foo') 57 | 58 | -------------------------------------------------------------------------------- /test/extension/leaf.coffee: -------------------------------------------------------------------------------- 1 | describe 'simple schema', -> 2 | schema = 'leaf foo;' 3 | 4 | it "should parse simple leaf statement", -> 5 | y = Yang.parse schema 6 | y.should.have.property('tag').and.equal('foo') 7 | 8 | it "should create simple leaf element", -> 9 | o = (Yang.parse schema) foo: 'hello' 10 | o.should.have.property('foo').and.equal('hello') 11 | o.foo = 'bye' 12 | o.foo.should.equal('bye') 13 | 14 | describe 'extended schema', -> 15 | schema = """ 16 | leaf foo { 17 | description "extended leaf foo"; 18 | default "bar"; 19 | } 20 | """ 21 | it "should parse extended leaf statement", -> 22 | y = Yang.parse schema 23 | y.default.should.have.property('tag').and.equal('bar') 24 | 25 | it "should create extended leaf element", -> 26 | o = (Yang.parse schema)() 27 | o.should.have.property('foo') 28 | 29 | it "should contain default leaf value", -> 30 | o = (Yang.parse schema)() 31 | o.foo.should.equal('bar') 32 | 33 | it "should not allow mandatory and default at the same time", -> 34 | schema = """ 35 | leaf foo { 36 | mandatory true; 37 | default "bar"; 38 | } 39 | """ 40 | (-> Yang.parse schema ).should.throw() 41 | 42 | it "should enforce mandatory leaf", -> 43 | schema = """ 44 | leaf foo { 45 | mandatory true; 46 | } 47 | """ 48 | (-> (Yang.parse schema)()).should.throw() 49 | (-> (Yang.parse schema) foo: undefined).should.throw() 50 | (-> (Yang.parse schema) foo: 'bar').should.not.throw() 51 | 52 | it "should enforce config false (readonly)", -> 53 | schema = """ 54 | leaf foo { 55 | config false; 56 | } 57 | """ 58 | (-> (Yang.parse schema) foo: 'hi').should.throw() 59 | 60 | it "should allow binding computed function", -> 61 | schema = """ 62 | leaf foo { 63 | config false; 64 | } 65 | """ 66 | o = Yang.parse(schema).bind( get: (ctx) -> ctx.data = 'bar').eval() 67 | o.foo.should.equal('bar') 68 | 69 | describe 'typed schema', -> 70 | schema = 'leaf foo { type string; }' 71 | it "should parse type extended leaf statement", -> 72 | y = Yang.parse schema 73 | y.type.should.have.property('tag').and.equal('string') 74 | 75 | it "should create type extended leaf element", -> 76 | o = (Yang.parse schema) foo: 'hello' 77 | o.should.have.property('foo') 78 | 79 | it "should validate type on computed function result", -> 80 | schema = """ 81 | leaf foo { 82 | type int8; 83 | config false; 84 | } 85 | """ 86 | (-> 87 | o = Yang.parse(schema).bind( get: (ctx) -> ctx.data = 123).eval() 88 | o.foo 89 | ).should.not.throw() 90 | (-> 91 | o = Yang.parse(schema).bind( get: (ctx) -> ctx.data = 'bar').eval() 92 | o.foo 93 | ).should.throw() 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /test/extension/list.coffee: -------------------------------------------------------------------------------- 1 | describe 'simple schema', -> 2 | schema = 'list foo;' 3 | 4 | it "should parse simple list statement", -> 5 | y = Yang.parse schema 6 | y.should.have.property('tag').and.equal('foo') 7 | 8 | it "should create simple list element", -> 9 | o = (Yang.parse schema) foo: [ bar: 'hello' ] 10 | o.should.have.property('foo').and.be.instanceOf(Array) 11 | o.foo.should.have.length(1) 12 | 13 | it "should allow setting a new list", -> 14 | o = (Yang.parse schema)() 15 | o.foo = [ { bar1: 'hello' }, { bar2: 'world' } ] 16 | o.foo.should.be.instanceOf(Array).and.have.length(2) 17 | 18 | it "should allow adding additional items to the list", -> 19 | o = (Yang.parse schema) foo: [] 20 | o.foo.merge a: 'hi' 21 | o.foo.should.be.instanceOf(Array).and.have.length(1) 22 | o.foo.merge a: 'bye' 23 | o.foo.should.be.instanceOf(Array).and.have.length(2) 24 | 25 | describe 'extended schema', -> 26 | schema = """ 27 | list foo { 28 | description "extended list foo"; 29 | min-elements 1; 30 | max-elements 3; 31 | } 32 | """ 33 | it "should parse extended list statement", -> 34 | y = Yang.parse schema 35 | y['min-elements'].should.have.property('tag').and.equal(1) 36 | y['max-elements'].should.have.property('tag').and.equal(3) 37 | 38 | it "should create extended list element", -> 39 | o = (Yang.parse schema) foo: [ bar: 'hello' ] 40 | o.foo.should.be.instanceOf(Array).and.have.length(1) 41 | 42 | it "should reject non-object list element", -> 43 | (-> (Yang.parse schema) foo: [ 'not an object' ]).should.throw() 44 | 45 | it "should validate min/max elements constraint", -> 46 | o = (Yang.parse schema) foo: [ bar: 'hello' ] 47 | (-> o.foo = []).should.throw() 48 | (-> o.foo = [ {}, {}, {}, {} ]).should.throw() 49 | (-> o.foo = [ {}, {}, {} ]).should.not.throw() 50 | (-> 51 | o.foo = [ {}, {}, {} ] 52 | o.foo.merge {} 53 | ).should.throw() 54 | 55 | it.skip "should support order-by condition", -> 56 | 57 | it "should enforce key/leaf mapping during resolve", -> 58 | schema = """ 59 | list foo { 60 | key 'bar'; 61 | } 62 | """ 63 | (-> Yang.parse schema ).should.throw() 64 | 65 | it "should enforce unique/leaf mapping during resolve", -> 66 | schema = """ 67 | list foo { 68 | unique 'bar'; 69 | } 70 | """ 71 | (-> Yang.parse schema ).should.throw() 72 | 73 | describe 'complex schema', -> 74 | schema = """ 75 | list foo { 76 | key 'bar1 bar2'; 77 | unique 'bar2 name/first'; 78 | leaf bar1 { type string; } 79 | leaf bar2 { type int8; } 80 | leaf bar3 { type string; } 81 | container name { 82 | leaf first; 83 | leaf last; 84 | } 85 | leaf-list friends { type string; } 86 | } 87 | """ 88 | it "should parse complex list statement", -> 89 | y = Yang.parse schema 90 | y.key.should.have.property('tag').and.be.instanceof(Array) 91 | 92 | it "should create complex list element", -> 93 | o = (Yang.parse schema)() 94 | o.should.have.property('foo') 95 | 96 | it "should support key based list access", -> 97 | o = (Yang.parse schema) foo: [ 98 | bar1: 'apple' 99 | bar2: 10 100 | , 101 | bar1: 'apple' 102 | bar2: 20 103 | ] 104 | o.foo.get('key(apple,10)').should.have.property('bar1') 105 | 106 | it "should not allow conflicting key", -> 107 | (-> 108 | (Yang.parse schema) foo: [ 109 | bar1: 'apple' 110 | bar2: 10 111 | , 112 | bar1: 'apple' 113 | bar2: 10 114 | ] 115 | ).should.throw() 116 | 117 | o = (Yang.parse schema) foo: [ 118 | bar1: 'apple' 119 | bar2: 10 120 | ] 121 | (-> 122 | o.foo.create 123 | bar1: 'apple' 124 | bar2: 10 125 | ).should.throw() 126 | 127 | it.skip "should validate nested unique constraint", -> 128 | (-> 129 | (Yang.parse schema) foo: [ 130 | bar1: 'apple' 131 | bar2: 10 132 | name: first: 'conflict' 133 | , 134 | bar1: 'orange' 135 | bar2: 10 136 | name: first: 'conflict' 137 | ] 138 | ).should.throw() 139 | 140 | it "should support merge operation", -> 141 | o = (Yang.parse schema) foo: [ 142 | bar1: 'apple' 143 | bar2: 10 144 | ] 145 | o.foo.merge 146 | bar1: 'apple' 147 | bar2: 10 148 | bar3: 'test' 149 | o.foo.get('key(apple,10)').should.have.property('bar3').and.equal('test') 150 | 151 | it "should support delete operation", -> 152 | o = (Yang.parse schema) foo: [ 153 | bar1: 'apple' 154 | bar2: 10 155 | , 156 | bar1: 'apple' 157 | bar2: 20 158 | , 159 | bar1: 'orange' 160 | bar2: 3 161 | ] 162 | o.foo.should.have.length(3); 163 | delete o.foo['key(apple,10)']; 164 | o.foo.should.have.length(2); 165 | o.foo['key(apple,20)'] = null 166 | o.foo.should.have.length(1); 167 | o.foo['key(orange,3)'].merge(null) 168 | o.foo.should.have.length(0); 169 | 170 | describe 'edge cases', -> 171 | schema = """ 172 | module m1 { 173 | grouping g1 { leaf id; } 174 | // key declared before uses where the leaf is 175 | list foo { 176 | key "id"; 177 | uses g1; 178 | leaf ref { 179 | type leafref { 180 | path "../../bar"; 181 | } 182 | } 183 | } 184 | leaf bar; 185 | } 186 | """ 187 | it "should resolve 'key' reference to used grouping", -> 188 | (-> Yang.parse schema ).should.not.throw() 189 | 190 | it "should properly traverse relative leafref path", -> 191 | (-> 192 | (Yang.parse schema) 'm1:bar': 'hello', 'm1:foo': [ 193 | id: 1 194 | ref: 'hello' 195 | ] 196 | ).should.not.throw() 197 | 198 | describe 'performance', -> 199 | schema = """ 200 | list foo { 201 | key id; 202 | leaf id { 203 | type uint16; 204 | } 205 | container bar { 206 | leaf v1 { 207 | type uint32; 208 | } 209 | leaf v2 { 210 | type uint32; 211 | } 212 | } 213 | } 214 | """ 215 | model = undefined 216 | filler = (_,i) -> { id: i, bar: { v1: i*123, v2: i*321 } } 217 | d100 = Array(100).fill(null).map filler 218 | d500 = Array(500).fill(null).map filler 219 | 220 | before -> 221 | model = (Yang.parse schema).eval() 222 | 223 | it "time setting 100 entries", -> 224 | model.foo = d100 225 | 226 | it "time setting 500 entries", -> 227 | model.foo = d500 228 | 229 | it "time merging one existing item into 500 entries", -> 230 | pre = process.memoryUsage() 231 | model.foo.merge({ id: 1 }) 232 | post = process.memoryUsage() 233 | # console.log("growth: %d KB", (post.heapUsed - pre.heapUsed) / 1024); 234 | model.foo.should.be.instanceof(Array).and.have.length(500) 235 | 236 | it "time setting 1000 entries to large list", -> 237 | model.foo = Array(1000).fill(null).map(filler) 238 | 239 | it "time merging an existing entry to large list", -> 240 | one = Array(1).fill(null).map filler 241 | pre = process.memoryUsage() 242 | model.foo.merge(one); 243 | #model.foo.merge([{ id: 1, bar: { v1: 30, v2: 50 } }]); 244 | post = process.memoryUsage() 245 | console.log("mem growth: %d KB", (post.heapUsed - pre.heapUsed) / 1024); 246 | 247 | it "time merging existing 1000 entries to large list", -> 248 | many = Array(1000).fill(null).map(filler) 249 | pre = process.memoryUsage() 250 | for i in [0...10] by 1 251 | model.foo.merge(many, { appendOnly: true }); 252 | #model.foo.merge(many); 253 | post = process.memoryUsage() 254 | growth = (post.heapUsed - pre.heapUsed) / 1024 255 | console.error("mem growth: %d KB", growth); 256 | 257 | 258 | -------------------------------------------------------------------------------- /test/extension/module.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe 'simple schema', -> 4 | schema = undefined 5 | 6 | it "should parse simple module statement", -> 7 | schema = Yang.parse "module foo;" 8 | schema.should.have.property('tag').and.equal('foo') 9 | 10 | it "should create simple module element", -> 11 | o = schema() 12 | o.should.be.instanceof(Object) 13 | 14 | describe 'extended schema', -> 15 | schema = """ 16 | module foo { 17 | prefix foo; 18 | namespace "http://corenova.com"; 19 | 20 | description "extended module test"; 21 | contact "Peter K. Lee "; 22 | organization "Corenova Technologies, Inc."; 23 | reference "http://github.com/corenova/yang-js"; 24 | 25 | identity core { 26 | description "the core identity"; 27 | } 28 | 29 | grouping some-shared-info { 30 | leaf a { type string; } 31 | leaf b { type uint8; } 32 | } 33 | 34 | container bar { 35 | uses some-shared-info; 36 | } 37 | 38 | rpc some-method-1 { 39 | description "update config for 'bar'"; 40 | input { 41 | uses some-shared-info; 42 | } 43 | } 44 | rpc some-method-2; 45 | } 46 | """ 47 | it "should parse extended module statement", -> 48 | y = Yang.parse schema 49 | y.prefix.should.have.property('tag').and.equal('foo') 50 | 51 | it "should convert toJSON", -> 52 | y = Yang.parse schema 53 | obj = y.toJSON() 54 | obj.should.have.property('module').and.have.property('foo') 55 | 56 | it "should create extended module element", -> 57 | o = (Yang.parse schema)() 58 | o.get('/').should.have.property('foo:bar') 59 | 60 | it "should evaluate configuration data", -> 61 | o = (Yang.parse schema) 62 | 'foo:bar': 63 | 'foo:a': 'hello' # fully qualifed property name 64 | b: 10 # contextual property name 65 | o.get('foo:bar').should.have.property('a').and.equal('hello') 66 | o.get('foo:bar').should.have.property('b').and.equal(10) 67 | 68 | it "should implement functional module", -> 69 | o = (Yang.parse schema) 70 | 'foo:bar': 71 | a: 'hello' 72 | b: 10 73 | 'foo:some-method-1': (input) -> 74 | bar = @['foo:bar'] 75 | bar.a = input.a 76 | bar.b = input.b 77 | return message: 'success' 78 | o.in('some-method-1').do 79 | a: 'bye' 80 | b: 0 81 | .then (res) -> 82 | res.should.have.property('message').and.equal('success') 83 | o.get('foo:bar').should.have.property('a').and.equal('bye') 84 | o.get('foo:bar').should.have.property('b').and.equal(0) 85 | 86 | describe 'augment schema (local)', -> 87 | schema = """ 88 | module foo { 89 | prefix foo; 90 | namespace "http://corenova.com"; 91 | 92 | description "augment module test"; 93 | 94 | container bar { 95 | leaf a1; 96 | } 97 | augment "/foo:bar" { 98 | leaf a2; 99 | } 100 | augment "/foo:bar" { 101 | leaf a3; 102 | } 103 | } 104 | """ 105 | it "should parse augment module statement", -> 106 | y = Yang.parse schema 107 | y.prefix.should.have.property('tag').and.equal('foo') 108 | y.locate('/bar/a2').should.have.property('tag').and.equal('a2') 109 | y.locate('/bar/a3').should.have.property('tag').and.equal('a3') 110 | 111 | 112 | describe 'augment schema (external)', -> 113 | before -> Yang.clear() 114 | 115 | schema1 = """ 116 | module foo { 117 | prefix foo; 118 | namespace "http://corenova.com"; 119 | 120 | description "augment module test"; 121 | 122 | grouping test { 123 | container c3 { 124 | leaf a3 { type string; } 125 | } 126 | } 127 | container c1 { 128 | container c2 { 129 | leaf a1; 130 | } 131 | } 132 | } 133 | """ 134 | schema2 = """ 135 | module bar { 136 | prefix bar; 137 | 138 | import foo { prefix foo; } 139 | 140 | augment "/foo:c1/foo:c2" { 141 | leaf a2; 142 | } 143 | augment "/foo:c1" { 144 | uses foo:test; 145 | } 146 | } 147 | """ 148 | it "should parse augment module statement", -> 149 | y1 = Yang.use (Yang.parse schema1) 150 | y2 = Yang.parse schema2 151 | y2.locate('/foo:c1/c2/bar:a2').should.have.property('tag').and.equal('a2') 152 | y2.locate('/foo:c1/bar:c3').should.have.property('tag').and.equal('c3') 153 | o = y2.eval 154 | 'foo:c1': 155 | 'bar:c3': 156 | a3: 'hello' 157 | o.get('/foo:c1/bar:c3').should.have.property('bar:a3').and.equal('hello') 158 | 159 | describe "import schema", -> 160 | before -> Yang.clear() 161 | 162 | schema1 = """ 163 | module foo { 164 | prefix foo; 165 | namespace "http://corenova.com/yang/bar"; 166 | 167 | description "extended module test"; 168 | contact "Peter K. Lee "; 169 | organization "Corenova Technologies, Inc."; 170 | reference "http://github.com/corenova/yang-js"; 171 | 172 | grouping some-shared-info { 173 | leaf a { type string; } 174 | leaf b { type uint8; } 175 | } 176 | } 177 | """ 178 | schema2 = """ 179 | module bar { 180 | prefix bar; 181 | namespace "http://corenova.com/yang/foo"; 182 | 183 | import foo { prefix f; } 184 | 185 | description "extended module test"; 186 | contact "Peter K. Lee "; 187 | organization "Corenova Technologies, Inc."; 188 | reference "http://github.com/corenova/yang-js"; 189 | 190 | container xyz { 191 | description "empty container"; 192 | uses f:some-shared-info; 193 | } 194 | } 195 | """ 196 | 197 | it "should parse import statement", -> 198 | y1 = Yang.use (Yang.parse schema1) 199 | y2 = Yang.parse schema2 200 | y2.prefix.should.have.property('tag').and.equal('bar') 201 | 202 | describe 'include schema', -> 203 | before -> Yang.clear() 204 | 205 | schema = """ 206 | module foo { 207 | prefix foo; 208 | namespace "http://corenova.com/yang/foo"; 209 | 210 | include A; 211 | include B; 212 | 213 | description "extended module test"; 214 | contact "Peter K. Lee "; 215 | organization "Corenova Technologies, Inc."; 216 | reference "http://github.com/corenova/yang-js"; 217 | 218 | grouping some-shared-info { 219 | leaf a { type string; } 220 | leaf b { type uint8; } 221 | } 222 | } 223 | """ 224 | 225 | it "should parse submodule schema", -> 226 | sub = """ 227 | submodule A { 228 | belongs-to foo { 229 | prefix foo; 230 | } 231 | 232 | description "extended module test"; 233 | contact "Peter K. Lee "; 234 | organization "Corenova Technologies, Inc."; 235 | reference "http://github.com/corenova/yang-js"; 236 | 237 | revision 2016-06-28 { 238 | description 239 | "Test revision"; 240 | } 241 | 242 | typedef T1 { 243 | type uint8; 244 | } 245 | } 246 | """ 247 | y = Yang.use (Yang.parse sub, compile: false) # compile=false necessary! 248 | y['belongs-to'].should.have.property('tag').and.equal('foo') 249 | 250 | it "should parse submodule schema (include another submodule)", -> 251 | sub = """ 252 | submodule B { 253 | belongs-to foo { 254 | prefix foo; 255 | } 256 | include A; 257 | 258 | description "extended module test"; 259 | contact "Peter K. Lee "; 260 | organization "Corenova Technologies, Inc."; 261 | reference "http://github.com/corenova/yang-js"; 262 | 263 | revision 2016-06-28 { 264 | description 265 | "Test revision"; 266 | } 267 | container xyz { 268 | uses foo:some-shared-info; 269 | leaf c { type T1; } 270 | } 271 | } 272 | """ 273 | y = Yang.use (Yang.parse sub, compile: false) # compile=false necessary! 274 | y['belongs-to'].should.have.property('tag').and.equal('foo') 275 | 276 | it "should parse include statement", -> 277 | y = Yang.parse schema 278 | xyz = y.match('container','xyz') 279 | xyz.should.have.property('leaf') 280 | 281 | -------------------------------------------------------------------------------- /test/extension/rpc.coffee: -------------------------------------------------------------------------------- 1 | describe 'simple schema', -> 2 | schema = undefined 3 | 4 | it "should parse simple rpc statement", -> 5 | schema = Yang.parse 'rpc foo;' 6 | schema.should.have.property('kind').and.equal('rpc') 7 | 8 | it "should create simple rpc element", -> 9 | o = schema.bind(-> 'bye').eval() 10 | o.should.have.property('foo').and.be.instanceof(Function) 11 | o.foo() 12 | .then (res) -> res.should.equal('bye') 13 | 14 | describe 'extended schema', -> 15 | schema = undefined 16 | 17 | it "should parse extended rpc statement", -> 18 | schema = Yang.parse """ 19 | rpc foo { 20 | description "input extended rpc foo"; 21 | input { 22 | leaf bar { type string; } 23 | } 24 | output { 25 | leaf message { type string; } 26 | } 27 | } 28 | """ 29 | schema.input.should.have.property('leaf') 30 | 31 | it "should create extended rpc element", -> 32 | o = schema() 33 | o.should.have.property('foo') 34 | 35 | it "should allow assigning handler function", -> 36 | (-> schema foo: 'error').should.throw() 37 | (-> schema foo: ->).should.not.throw() 38 | (-> schema foo: -> message: 'ok').should.not.throw() 39 | 40 | it "should validate input parameters", -> 41 | o = (schema.bind -> message: 'ok')() 42 | o.foo 'hello' 43 | .catch (err) -> err.should.be.instanceof(Error) 44 | o.foo bar: 'good' 45 | .then (res) -> res.should.have.property('message').and.is.equal('ok') 46 | 47 | it "should validate output parameters", -> 48 | o = (schema.bind -> dummy: 'bad')() 49 | o.foo bar: 'good' 50 | .then (res) -> res.should.not.have.property('dummy') 51 | .catch (err) -> err.should.be.instanceof(Error) 52 | -------------------------------------------------------------------------------- /test/extension/type.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe 'bits', -> 4 | schema = """ 5 | type bits { 6 | bit one { 7 | position 0; 8 | } 9 | bit two; 10 | bit three; 11 | } 12 | """ 13 | it "should parse type bits statement", -> 14 | y = Yang.parse schema 15 | y.should.have.property('tag').and.equal('bits') 16 | y.bit.should.be.instanceOf(Array).and.have.length(3) 17 | 18 | describe 'boolean', -> 19 | schema = undefined 20 | 21 | before -> 22 | schema = Yang.parse 'leaf foo { type boolean; }'; 23 | 24 | it "should validate boolean value", -> 25 | o = schema() 26 | (-> o.foo = 'yes').should.throw() 27 | (-> o.foo = 'True').should.throw() 28 | (-> o.foo = 1).should.throw() 29 | (-> o.foo = 0).should.throw() 30 | (-> o.foo = 'true').should.not.throw() 31 | (-> o.foo = true).should.not.throw() 32 | (-> o.foo = false).should.not.throw() 33 | 34 | it "should convert input to boolean value", -> 35 | o = schema() 36 | o.foo = 'true'; o.foo.should.equal(true) 37 | o.foo = true; o.foo.should.equal(true) 38 | o.foo = 'false'; o.foo.should.equal(false) 39 | o.foo = false; o.foo.should.equal(false) 40 | 41 | describe 'enumeration', -> 42 | schema = """ 43 | type enumeration { 44 | enum apple; 45 | enum orange { value 20; } 46 | enum banana; 47 | } 48 | """ 49 | it "should parse type enumeration statement", -> 50 | y = Yang.parse schema 51 | y.should.have.property('tag').and.equal('enumeration') 52 | y.enum.should.be.instanceOf(Array).and.have.length(3) 53 | for i in y.enum 54 | switch i.tag 55 | when 'apple' 56 | i.should.have.property('value') 57 | i.value.tag.should.equal('0') 58 | when 'orange' 59 | i.value.tag.should.equal('20') 60 | when 'banana' 61 | i.should.have.property('value') 62 | i.value.tag.should.equal('21') 63 | 64 | it "should validate enum constraint", -> 65 | o = (Yang.parse "leaf foo { #{schema} }")() 66 | (-> o.foo = 'lemon').should.throw() 67 | (-> o.foo = 3).should.throw() 68 | (-> o.foo = '1').should.throw() 69 | (-> o.foo = 'apple').should.not.throw() 70 | (-> o.foo = '0').should.not.throw() 71 | (-> o.foo = 21).should.not.throw() 72 | 73 | describe 'string', -> 74 | schema = """ 75 | type string { 76 | length 2..5; 77 | pattern '[a-z]+'; 78 | } 79 | """ 80 | it "should parse type string statement", -> 81 | y = Yang.parse schema 82 | y.should.have.property('tag').and.equal('string') 83 | y.length.should.have.property('tag').and.equal('2..5') 84 | y.pattern.should.be.instanceOf(Array).and.have.length(1) 85 | 86 | it "should parse multi-line regexp pattern", -> 87 | y = Yang.parse """ 88 | type string { 89 | pattern 90 | '[a-z]+' 91 | + '[0-9]+'; 92 | } 93 | """ 94 | y.resolve() 95 | y.pattern.should.be.instanceof(Array) 96 | y.pattern[0].should.have.property('tag').and.be.instanceof(RegExp) 97 | should(y.pattern[0].tag.toString()).equal('/^(?:[a-z]+[0-9]+)$/') 98 | 99 | it "should parse special escape regexp pattern", -> 100 | y = Yang.parse 'type string { pattern "\\d+"; }' 101 | y.pattern[0].should.have.property('tag').and.be.instanceof(RegExp) 102 | y.pattern[0].tag.test(123).should.equal(true) 103 | y.pattern[0].tag.test('hi').should.equal(false) 104 | 105 | it "should validate length constraint", -> 106 | o = (Yang.parse "leaf foo { #{schema} }")() 107 | (-> o.foo = 'x').should.throw() 108 | (-> o.foo = 'xxxxxxxxxx').should.throw() 109 | 110 | it "should validate pattern constraint", -> 111 | o = (Yang.parse "leaf foo { #{schema} }")() 112 | (-> o.foo = 'app1').should.throw() 113 | (-> o.foo = 'Apple').should.throw() 114 | (-> o.foo = 'abc').should.not.throw() 115 | 116 | it "should validate multi-pattern constraint", -> 117 | schema = """ 118 | type string { 119 | length 1..5; 120 | pattern '[a-z]+'; 121 | pattern 'x.+'; 122 | } 123 | """ 124 | o = (Yang.parse "leaf foo { #{schema} }")() 125 | (-> o.foo = 'abc').should.throw() 126 | (-> o.foo = 'xyz').should.not.throw() 127 | 128 | describe 'integer', -> 129 | schema = """ 130 | type uint16 { 131 | range '1..10|100..1000'; 132 | } 133 | """ 134 | it "should parse type integer statement", -> 135 | y = Yang.parse schema 136 | y.should.have.property('tag').and.equal('uint16') 137 | y.range.should.have.property('tag').and.equal('1..10|100..1000') 138 | 139 | it "should validate input as integer", -> 140 | o = (Yang.parse "leaf foo { #{schema} }")() 141 | (-> o.foo = 'abc').should.throw() 142 | (-> o.foo = '123abc').should.throw() 143 | (-> o.foo = 7).should.not.throw() 144 | (-> o.foo = '777').should.not.throw() 145 | 146 | it "should validate range constraint", -> 147 | o = (Yang.parse "leaf foo { #{schema} }")() 148 | (-> o.foo = 0).should.throw() 149 | (-> o.foo = 11).should.throw() 150 | (-> o.foo = 99).should.throw() 151 | (-> o.foo = 1001).should.throw() 152 | 153 | it "should validate unsigned integers", -> 154 | o = (Yang.parse "leaf foo { type uint8; }")() 155 | (-> o.foo = 0).should.not.throw() 156 | (-> o.foo = 1).should.not.throw() 157 | (-> o.foo = 255).should.not.throw() 158 | (-> o.foo = -1).should.throw() 159 | (-> o.foo = 256).should.throw() 160 | 161 | it "should validate signed integers", -> 162 | o = (Yang.parse "leaf foo { type int8; }")() 163 | (-> o.foo = 0).should.not.throw() 164 | (-> o.foo = 1).should.not.throw() 165 | (-> o.foo = -1).should.not.throw() 166 | (-> o.foo = 127).should.not.throw() 167 | (-> o.foo = 128).should.throw() 168 | (-> o.foo = -129).should.throw() 169 | 170 | describe 'decimal64', -> 171 | it "should convert/validate input as decimal64", -> 172 | o = (Yang.parse 'leaf foo { type decimal64; }')() 173 | (-> o.foo = 'abc').should.throw() 174 | (-> o.foo = '').should.not.throw() 175 | (-> o.foo = '0.').should.not.throw() 176 | (-> o.foo = '0.0').should.not.throw() 177 | (-> o.foo = 0).should.not.throw() 178 | (-> o.foo = '1.3').should.not.throw() 179 | (-> o.foo = 1.2).should.not.throw() 180 | (-> o.foo = '1.3134').should.throw() 181 | (-> o.foo = 1.2345).should.throw() 182 | 183 | it "should validate range constraint", -> 184 | o = (Yang.parse "leaf foo { type decimal64 { range '-10..5.34'; } }")() 185 | (-> o.foo = 0).should.not.throw() 186 | (-> o.foo = -9).should.not.throw() 187 | (-> o.foo = 5).should.not.throw() 188 | (-> o.foo = 6).should.throw() 189 | (-> o.foo = -10.1).should.throw() 190 | 191 | it "should convert to fraction-digits constraint", -> 192 | o = (Yang.parse "leaf foo { type decimal64 { fraction-digits 3; } }")() 193 | o.foo = "1.234" 194 | o.foo.should.equal(1.234) 195 | o.foo = 125.2 196 | o.foo.should.equal(125.200) 197 | 198 | # TODO 199 | describe "binary", -> 200 | 201 | describe "empty", -> 202 | it "should convert/validate input as empty", -> 203 | o = (Yang.parse 'leaf foo { type empty; }')() 204 | (-> o.foo = null).should.not.throw() 205 | (-> o.foo = [null]).should.not.throw() 206 | (-> o.foo = 'bar').should.throw() 207 | 208 | describe "identityref", -> 209 | schema = undefined 210 | it "should parse identityref statement", -> 211 | schema = Yang.parse """ 212 | module foo { 213 | identity my-id; 214 | identity my-sub-id { base my-id; } 215 | identity my-sub-sub-id { base my-sub-id; } 216 | leaf a { type identityref { base my-id; } } 217 | leaf b { type identityref { base my-sub-id; } } 218 | } 219 | """ 220 | schema.should.have.property('identity').and.be.instanceof(Array) 221 | 222 | it "should create identityref element", -> 223 | o = schema() 224 | o.get('/').should.have.property('foo:a') 225 | 226 | it.skip "should validate identityref element", -> 227 | (-> schema 'foo:a': 'my-id').should.not.throw() 228 | (-> schema 'foo:a': 'my-sub-id').should.not.throw() 229 | (-> schema 'foo:b': 'my-sub-sub-id').should.not.throw() 230 | (-> schema 'foo:a': 'invalid').should.throw() 231 | 232 | describe "instance-identifier", -> 233 | schema = undefined 234 | it "should parse instance-identifier statement", -> 235 | schema = Yang.parse """ 236 | module foo { 237 | leaf a; 238 | leaf b { type instance-identifier; } 239 | } 240 | """ 241 | schema.should.have.property('leaf').and.be.instanceof(Array) 242 | 243 | it "should create instance-identifier element", -> 244 | o = schema() 245 | o.get('/').should.have.property('foo:b') 246 | 247 | it "should validate instance-identifier element", -> 248 | (-> schema 'foo:b': '/foo:a' ).should.not.throw() 249 | (-> schema 'foo:b': '/foo:c' ).should.throw() 250 | 251 | describe "require-instance", -> 252 | schema = undefined 253 | it "should parse require-instance statement", -> 254 | schema = Yang.parse """ 255 | module foo { 256 | leaf a; 257 | leaf b { 258 | type instance-identifier { 259 | require-instance true; 260 | } 261 | } 262 | } 263 | """ 264 | schema.should.have.property('leaf').and.be.instanceof(Array) 265 | 266 | it "should validate require-instance parameter", -> 267 | (-> schema 'foo:a': 1, 'foo:b': '/foo:a' ).should.not.throw() 268 | 269 | describe "leafref", -> 270 | schema = undefined 271 | it "should parse leafref statement", -> 272 | schema = Yang.parse """ 273 | container foo { 274 | leaf bar1 { type string; } 275 | leaf bar2 { 276 | type leafref { path '../bar1'; } 277 | } 278 | } 279 | """ 280 | schema.should.have.property('leaf').and.be.instanceof(Array) 281 | schema.lookup('leaf','bar2').should.have.property('type') 282 | 283 | it "should create leafref element", -> 284 | o = schema() 285 | o.should.have.property('foo') 286 | 287 | it "should validate leafref element", -> 288 | o = schema foo: bar1: 'exists' 289 | (-> o.foo.bar2 = 'dummy').should.throw() 290 | (-> o.foo.bar2 = 'exists').should.not.throw() 291 | 292 | describe "union", -> 293 | schema = """ 294 | type union { 295 | type string { length 1..5; } 296 | type uint8; 297 | } 298 | """ 299 | schema2 = """ 300 | type union { 301 | type string { 302 | length 1..3; 303 | pattern '[a-fA-F0-9]*'; 304 | } 305 | type string { 306 | length 6; 307 | pattern '[a-fA-F0-9]*'; 308 | } 309 | } 310 | """ 311 | it "should parse union statement", -> 312 | y = Yang.parse schema 313 | y.should.have.property('tag').and.be.equal('union') 314 | y.type.should.be.instanceof(Array) 315 | 316 | it "should convert/validate union type element", -> 317 | o = (Yang.parse "leaf foo { #{schema} }")() 318 | (-> o.foo = 'abcdefg').should.throw() 319 | (-> o.foo = 'a').should.not.throw() 320 | (-> o.foo = 123).should.not.throw() 321 | (-> o.foo = 12345).should.not.throw() 322 | 323 | it "should parse multiple primitives", -> 324 | y = Yang.parse schema2 325 | o = Yang.parse("leaf foo { #{schema2} }")() 326 | (-> o.foo = '').should.throw() 327 | (-> o.foo = 'abc').should.not.throw() 328 | (-> o.foo = 'Abcde1').should.not.throw() 329 | 330 | describe 'typedef', -> 331 | schema = """ 332 | module A { 333 | typedef t_A { 334 | type union { 335 | type string { 336 | length 4; 337 | } 338 | type string { 339 | length 6; 340 | } 341 | } 342 | } 343 | leaf foo { 344 | type t_A { 345 | length 3; 346 | } 347 | } 348 | } 349 | """ 350 | 351 | it "should parse complex typedef statement", -> 352 | y = Yang.parse schema 353 | y.locate('leaf(foo)').should.have.property('type') 354 | 355 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "import-resolve-test", 3 | "private": true, 4 | "yang": { 5 | "search": [ "../example", "test1", "test2" ], 6 | "resolve": { 7 | "example-jukebox": "../example/jukebox.yang", 8 | "target-in-package": "test1", 9 | "missing-schema": "test2" 10 | } 11 | }, 12 | "peerDependencies": { 13 | "test1": false, 14 | "test2": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/yang-1.1/type.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe "leafref", -> 4 | describe "require-instance", -> 5 | schema = """ 6 | container foo { 7 | leaf bar1 { type string; } 8 | leaf bar2 { 9 | type leafref { 10 | path '../bar1'; 11 | require-instance false; 12 | } 13 | } 14 | leaf bar3 { 15 | type leafref { 16 | path '../bar1'; 17 | require-instance true; 18 | } 19 | } 20 | } 21 | """ 22 | it "should parse require-instance statement", -> 23 | y = Yang.parse schema 24 | y.should.have.property('leaf').and.be.instanceof(Array) 25 | 26 | it "should validate require-instance parameter", -> 27 | o = (Yang.parse schema) 28 | foo: bar1: 'exists' 29 | (-> o.foo.bar2 = 'dummy').should.not.throw() 30 | (-> o.foo.bar3 = 'dummy').should.throw() 31 | -------------------------------------------------------------------------------- /test/yang-compliance-coverage.coffee: -------------------------------------------------------------------------------- 1 | global.Yang = require '..' 2 | 3 | describe "YANG 1.0 (RFC-6020) Compliance:", -> 4 | 5 | describe 'leaf', -> require './extension/leaf' 6 | describe 'leaf-list', -> require './extension/leaf-list' 7 | describe 'container', -> require './extension/container' 8 | describe 'list', -> require './extension/list' 9 | describe 'type', -> require './extension/type' 10 | describe 'rpc', -> require './extension/rpc' 11 | describe 'grouping', -> require './extension/grouping' 12 | describe 'choice', -> require './extension/choice' 13 | describe 'extension', -> require './extension/extension' 14 | describe 'module', -> require './extension/module' 15 | 16 | describe "YANG 1.1 (DRAFT) Compliance:", -> 17 | 18 | describe 'type', -> require './yang-1.1/type' 19 | describe 'action', -> it.skip "todo" 20 | describe 'anydata', -> it.skip "todo" 21 | describe 'modifier', -> it.skip "todo" 22 | -------------------------------------------------------------------------------- /test/yang-compliance-coverage.md: -------------------------------------------------------------------------------- 1 | ## Current RFC 6020 Implementation Coverage 2 | 3 | The below table provides up-to-date information about various YANG 4 | schema language extensions and associated support within this module. 5 | All extensions are syntactically and lexically processed already, but 6 | the below table provides details on the status of extensions as it 7 | pertains to how it is **processed** by the compiler for implementing 8 | the intended behavior of each extension. 9 | 10 | Basically, note that the *unsupported* status below indicates it is 11 | not compliant with expected behavior although it is properly parsed 12 | and processed by the compiler. 13 | 14 | Most of the *supported* extensions and typedefs have a corresponding 15 | *test case* reference link which will contain the associated mocha 16 | test suite which contains the validation tests. 17 | 18 | ### Language Extensions 19 | 20 | extension | status | notes 21 | --- | --- | --- 22 | action | supported | v1.1 draft 23 | anydata | unsupported | semantic parsing only 24 | anyxml | unsupported | no plans to support this extension 25 | augment | supported | [test case 1](./extensions/module.coffee) [test case 2](./extensions/grouping.coffee) 26 | base | supported | identity reference verification 27 | belongs-to | supported | resolve prefix 28 | bit | unsupported | TBD (0.17) 29 | case | supported | [test case](./extensions/choice.coffee) 30 | choice | supported | [test case](./extensions/choice.coffee) 31 | config | supported | [test case](./extensions/leaf.coffee) 32 | contact | supported | meta data only 33 | container | supported | [test case](./extensions/container.coffee) 34 | default | supported | [test case](./extensions/leaf.coffee) 35 | description | supported | meta data only 36 | deviate | unsupported | TBD merge/alter 37 | deviation | unsupported | TBD merge/alter 38 | enum | suported | [test case](./extensions/type.coffee) 39 | error-app-tag | unsupported | TBD (0.16) 40 | error-message | unsupported | TBD (0.16) 41 | feature | supported | enable feature binding (test case TBD) 42 | fraction-digits | supported | [test case](./extensions/type.coffee) 43 | grouping | supported | [test case](./extensions/grouping.coffee) 44 | identity | supported | [test case](./extensions/module.coffee) 45 | if-feature | supported | conditional verification on feature binding 46 | import | supported | [test case](./extensions/import.coffee) 47 | include | supported | [test case](./extensions/import.coffee) 48 | input | supported | [test case](./extensions/rpc.coffee) 49 | key | supported | [test case](./extensions/list.coffee) 50 | leaf | supported | [test case](./extensions/leaf.coffee) 51 | leaf-list | supported | [test case](./extensions/leaf-list.coffee) 52 | length | supported | [test case](./extensions/type.coffee) 53 | list | supported | [test case](./extensions/list.coffee) 54 | mandatory | supported | [test case](./extensions/leaf.coffee) 55 | max-elements | supported | [test case](./extensions/leaf-list.coffee) 56 | min-elements | supported | [test case](./extensions/leaf-list.coffee) 57 | modifier | unsupported | v1.1 draft - TBD 58 | module | supported | [test case](./extensions/module.coffee) 59 | must | unsupported | TBD 60 | namespace | supported | meta data only 61 | notification | unsupported | TBD 62 | ordered-by | unsupported | TBD 63 | organization | supported | meta data only 64 | output | supported | [test case](./extensions/rpc.coffee) 65 | path | supported | [test case](./extensions/type.coffee) 66 | pattern | supported | [test case](./extensions/type.coffee) 67 | position | unsupported | TBD 68 | prefix | supported | [test case](./extensions/module.coffee) 69 | presence | unsupported | TBD 70 | range | supported | [test case](./extensions/type.coffee) 71 | reference | supported | meta data only 72 | refine | supported | [test case](./extensions/grouping.coffee) 73 | require-instance | supported | [test case](./extensions/type.coffee) 74 | revision | supported | meta data only 75 | revision-date | supported | meta data only 76 | rpc | supported | [test case](./extensions/rpc.coffee) 77 | status | supported | meta data only 78 | submodule | supported | [test case](./extensions/module.coffee) 79 | type | supported | [test case](./extensions/type.coffee) 80 | typedef | supported | [test case](./extensions/type.coffee) 81 | unique | supported | [test case](./extensions/list.coffee) 82 | units | supported | [test case](./extensions/leaf.coffee) 83 | uses | supported | [test case](./extensions/grouping.coffee) 84 | value | supported | [test case](./extensions/type.coffee) 85 | when | unsupported | TBD 86 | yang-version | supported | differentiates 1.0 and 1.1 87 | yin-element | supported | internal extension implementation 88 | 89 | ### Built-in Types 90 | 91 | type | status | notes 92 | --- | --- | --- 93 | binary | unsupported | TBD 94 | bits | unsupported | TBD 95 | boolean | supported | [test case](./extensions/type.coffee) 96 | decimal64 | supported | [test case](./extensions/type.coffee) 97 | empty | supported | [test case](./extensions/type.coffee) 98 | enumeration | supported | [test case](./extensions/type.coffee) 99 | identityref | unsupported | does not enforce 100 | leafref | supported | [test case](./extensions/type.coffee) 101 | int8 | supported | [test case](./extensions/type.coffee) 102 | int16 | supported | [test case](./extensions/type.coffee) 103 | int32 | supported | [test case](./extensions/type.coffee) 104 | int64 | supported | [test case](./extensions/type.coffee) 105 | uint8 | supported | [test case](./extensions/type.coffee) 106 | uint16 | supported | [test case](./extensions/type.coffee) 107 | uint32 | supported | [test case](./extensions/type.coffee) 108 | number | supported | [test case](./extensions/type.coffee) 109 | string | supported | [test case](./extensions/type.coffee) 110 | union | supported | [test case](./extensions/type.coffee) 111 | instance-identifier | supported | [test case](./extensions/type.coffee) 112 | -------------------------------------------------------------------------------- /test/yang-example-jukebox.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe "YANG Jukebox Example", -> 4 | jbox = undefined 5 | before -> 6 | jbox = require('../example/jukebox').eval { 7 | 'example-jukebox:jukebox': 8 | library: {} 9 | playlist: [ 10 | { 11 | name: 'ellie playtime', 12 | description: 'tunes for toddler play' 13 | } 14 | ] 15 | } 16 | 17 | it 'should contain initial playlist', -> 18 | jbox.get('/jukebox/playlist').should.be.instanceof(Array).and.have.length(1) 19 | 20 | it 'should setup jukebox library', -> 21 | jbox.get('/jukebox').library = 22 | artist: [ 23 | name: 'Super Simple Songs' 24 | album: [ 25 | name: 'Animals Vol. 1' 26 | year: '2015' 27 | song: [ 28 | name: 'old mcdonald had a farm' 29 | location: '/hard/wired/in/my/head.mpg' 30 | ] 31 | ] 32 | ] 33 | jbox.get('/jukebox/library/artist').should.be.instanceof(Array).and.have.length(1) 34 | 35 | it 'should enable adding a song to the playlist', -> 36 | jbox.get('/jukebox/playlist/ellie playtime').song = [ 37 | index: 1 38 | id: 'old mcdonald had a farm' 39 | ] 40 | 41 | it 'should play the song', -> 42 | jbox.in('play').do 43 | playlist: 'ellie playtime', 44 | 'song-number': 1 45 | .then (res) -> should(res).equal('ok') 46 | -------------------------------------------------------------------------------- /test/yang-import-resolve.coffee: -------------------------------------------------------------------------------- 1 | Yang = require '..' 2 | should = require 'should' 3 | path = require 'path' 4 | 5 | describe "YANG Import/Resolve Implementation:", -> 6 | testdir = __dirname; 7 | 8 | describe "resolve", -> 9 | it "pre-bundled modules", -> 10 | match = Yang.resolve testdir, "ietf-yang-types" 11 | should.exist(match) 12 | 13 | it "explicit target module", -> 14 | match = Yang.resolve testdir, "example-jukebox" 15 | should.exist(match) 16 | 17 | it "explict target in dependency package", -> 18 | match = Yang.resolve testdir, "target-in-package" 19 | should.exist(match) 20 | 21 | it "fail if explicit target not found", -> 22 | (-> Yang.resolve testdir, "missing-schema").should.throw() 23 | 24 | it "via search in directory", -> 25 | match = Yang.resolve testdir, "jukebox" 26 | should.exist(match) 27 | 28 | it "via search in dependency package", -> 29 | match = Yang.resolve testdir, "schema-in-test1" 30 | should.exist(match) 31 | 32 | it "using specified order (.yang first)", -> 33 | match = Yang.resolve testdir, "schema-in-test2" 34 | should.exist(match) 35 | should(path.extname match).be.equal(".yang") 36 | 37 | -------------------------------------------------------------------------------- /test/yang-property.coffee: -------------------------------------------------------------------------------- 1 | Yang = require '..' 2 | 3 | describe "YANG Property Implementation:", -> 4 | 5 | describe "property without schema", -> 6 | property = undefined 7 | it "should create basic property", -> 8 | property = new Yang.Property name: 'test' 9 | property.should.have.property('name').and.equal('test') 10 | 11 | it "should initialize array property", -> 12 | property.set [] 13 | property.get().should.be.instanceof(Array) 14 | 15 | it "should join arbitrary object", -> 16 | o = property.attach {} 17 | o.should.have.property('test') 18 | 19 | describe "property with schema", -> 20 | 21 | it "should create basic property", -> 22 | property = new Yang.Property name: 'test', schema: kind: 'leaf' 23 | 24 | describe "property memory profile", -> 25 | 26 | it "should have minimal memory footprint", -> 27 | pre = process.memoryUsage() 28 | a = Array(10000).fill(null).map( () -> new Yang.Container ) 29 | post = process.memoryUsage() 30 | growth = (post.heapUsed - pre.heapUsed) / 1024 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/yang-transaction.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | 3 | describe "YANG commit/revert transactions", -> 4 | 5 | describe "list transaction", -> 6 | schema = """ 7 | container foo { 8 | list x { 9 | key a; 10 | leaf a { 11 | type uint8; 12 | } 13 | leaf b { 14 | type string; 15 | } 16 | } 17 | } 18 | """ 19 | randomize = (min, max) -> Math.floor(Math.random() * (max - min)) + min 20 | it "should revert back to uninitialized state", -> 21 | o = (Yang.parse schema) foo: x: [ { a: 1 }, { a: 2 } ] 22 | await o.foo.revert() 23 | should.not.exist(o.foo) 24 | 25 | it "should revert back to prior state after merge", -> 26 | o = (Yang.parse schema) foo: x: [ { a: 1, b: 'hi' }, { a: 2 } ] 27 | await o.foo.commit() 28 | o.foo.merge x: [ { a: 1, b: 'bye' }, { a: 2, b: 'bogus' } ] 29 | await o.foo.revert() 30 | o.foo.x.should.be.instanceOf(Array).and.have.length(2) 31 | o.foo.x.get('key(1)').should.have.property('b').and.equal('hi') 32 | #console.warn(o.foo.x.toJSON()) 33 | 34 | it "should revert back to prior state after merge and creates", -> 35 | o = (Yang.parse schema) foo: x: [ { a: 1 }, { a: 2 } ] 36 | await o.foo.commit() 37 | o.foo.merge x: [ { a: 1, b: 'hi' }, { a: 2, b: 'bye' }, { a: 3 } ] 38 | await o.foo.revert() 39 | o.foo.x.should.be.instanceOf(Array).and.have.length(2) 40 | 41 | it "should revert back to prior state after set/replace", -> 42 | o = (Yang.parse schema) foo: x: [ { a: 1 }, { a: 2 } ] 43 | await o.foo.commit() 44 | o.foo.x = [ { a: 1, b: 'hi' }, { a: 2, b: 'bye' }, { a: 3 } ] 45 | await o.foo.revert() 46 | o.foo.x.should.be.instanceOf(Array).and.have.length(2) 47 | 48 | it "should revert back to prior state after delete", -> 49 | o = (Yang.parse schema) foo: x: [ { a: 1 }, { a: 2 } ] 50 | await o.foo.commit() 51 | o.foo.x = null 52 | await o.foo.revert() 53 | o.foo.x.should.be.instanceOf(Array).and.have.length(2) 54 | 55 | it "should revert back to immediate prior state after multiple pushes", -> 56 | o = (Yang.parse schema) foo: x: [ { a: 1 }, { a: 2 } ] 57 | await o.foo.commit() 58 | o.foo.merge x: [ { a: 1, b: 'hi' }, { a: 2, b: 'bye' }, { a: 3 } ] 59 | await o.foo.commit() 60 | o.foo.x = [ { a: 3 }, { a: 4 } ] 61 | await o.foo.revert() 62 | o.foo.x.should.be.instanceOf(Array).and.have.length(3) 63 | 64 | it "should revert back to prior state after delete commit failure", -> 65 | o = (Yang.parse schema) 66 | .bind { commit: (ctx) -> 67 | await ctx.after randomize(10, 20) 68 | if (ctx.get('x').length < 1) 69 | throw new Error "the list cannot be empty" 70 | } 71 | .eval foo: x: [ { a: 1 }, { a: 2 } ] 72 | await o.foo.commit() 73 | #o.foo.x = null 74 | #o.foo.merge x: null 75 | o.foo.x.merge null 76 | try await o.foo.commit() 77 | o.foo.x.should.be.instanceOf(Array).and.have.length(2) 78 | 79 | it "should cleanly handle concurrent commit failures", -> 80 | this.timeout(5000) 81 | o = (Yang.parse schema) 82 | .bind { commit: (ctx) -> 83 | # console.warn('commit fired...', ctx.get('x').length) 84 | await ctx.after randomize(10, 20) 85 | if (ctx.get('x').length > 5) 86 | throw new Error "cannot add more list items" 87 | } 88 | .eval foo: x: [ { a: 1 }, { a: 2 } ] 89 | await o.foo.commit() 90 | promises = [3...20].map (i) -> o.foo.x._context.push a: i 91 | try await Promise.all(promises.map (p) -> (p.catch (err) -> null)) 92 | #console.warn(o.foo.x._.inspect()) 93 | o.foo.x.length.should.equal(5) 94 | 95 | describe "concurrent transaction", -> 96 | schema = """ 97 | container foo { 98 | container a { 99 | leaf a1; 100 | leaf a2; 101 | } 102 | container b { 103 | leaf b1; 104 | leaf b2; 105 | } 106 | } 107 | """ 108 | it "should complete parallel transactions on same node", -> 109 | o = (Yang.parse schema) 110 | .bind { commit: (ctx) -> 111 | # console.warn('commit fired...'); 112 | await ctx.after 50 113 | # console.warn('commit done...'); 114 | } 115 | .eval foo: { a: a1: 'hi' } 116 | # console.warn('initial commit started'); 117 | promise = o.foo.commit() # asynchronous commit 118 | await o.foo._context.after 10 119 | o.foo._changes.length.should.equal(1) # one pending change 120 | # console.warn('pushing b'); 121 | await o.foo._context.push b: b1: 'there' 122 | # console.warn('pushed b'); 123 | # console.warn(o.foo._changes.length) 124 | await promise 125 | o.foo._changes.length.should.equal(0) 126 | 127 | it "should complete parallel transactions on peer nodes", -> 128 | o = (Yang.parse schema) 129 | .bind { commit: (ctx) -> 130 | # console.warn('commit fired...'); 131 | await ctx.after 100 132 | # console.warn('commit done...'); 133 | } 134 | .eval foo: { 135 | a: a1: 'hi' 136 | b: b1: 'there' 137 | } 138 | # console.warn('initial commit started'); 139 | await o.foo.commit() # asynchronous commit 140 | 141 | # console.warn('parallel push to a and b'); 142 | p1 = o.foo.a._context.push a1: 'merry' 143 | p2 = o.foo.b._context.push b1: 'christmas' 144 | 145 | # at first there should be no changes detected on o.foo 146 | o.foo._changes.length.should.equal(0); 147 | await o.foo._context.after 10 148 | o.foo._changes.length.should.equal(2); 149 | await p1 150 | o.foo._changes.length.should.equal(1); 151 | await p2 152 | o.foo._changes.length.should.equal(0); 153 | --------------------------------------------------------------------------------