├── .babelrc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist └── .git-keep ├── karma.conf.js ├── lib ├── ArraySlice.js ├── KeyValuePair.js ├── Namespace.js ├── ObjectSlice.js ├── elements.js ├── elements │ ├── LinkElement.js │ └── RefElement.js ├── minim.js ├── primitives │ ├── ArrayElement.js │ ├── BooleanElement.js │ ├── Element.js │ ├── MemberElement.js │ ├── NullElement.js │ ├── NumberElement.js │ ├── ObjectElement.js │ └── StringElement.js └── serialisers │ ├── JSON06Serialiser.js │ └── JSONSerialiser.js ├── package.json └── test ├── ArraySlice-test.js ├── Namespace-test.js ├── ObjectSlice-test.js ├── converters-test.js ├── elements ├── LinkElement-test.js ├── RefElement-test.js ├── attribute-with-attribute-test.js └── meta-with-meta-test.js ├── mocha.opts ├── primitives ├── ArrayElement-fantasy-land-test.js ├── ArrayElement-test.js ├── BooleanElement-test.js ├── Element-test.js ├── MemberElement-test.js ├── NullElement-test.js ├── NumberElement-test.js ├── ObjectElement-test.js └── StringElement-test.js ├── refract-test.js ├── serialisers ├── JSON06Serialiser-test.js └── JSONSerialiser-test.js ├── spec-helper.js └── subclass-test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": [ ">0.25%", "not op_mini all", "ie 11"], 6 | "node": "4.0" 7 | } 8 | }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:10 7 | steps: 8 | - checkout 9 | - run: npm install 10 | - run: npm run test 11 | 12 | jobs: 13 | test-node6: 14 | <<: *defaults 15 | docker: 16 | - image: circleci/node:6 17 | 18 | test-node8: 19 | <<: *defaults 20 | docker: 21 | - image: circleci/node:8 22 | 23 | test-node10: 24 | <<: *defaults 25 | 26 | test-browser: 27 | <<: *defaults 28 | docker: 29 | - image: circleci/node:10-browsers 30 | steps: 31 | - checkout 32 | - run: npm install 33 | - run: npm run test:browser 34 | 35 | lint: 36 | <<: *defaults 37 | steps: 38 | - checkout 39 | - run: npm install 40 | - run: npm run lint 41 | 42 | workflows: 43 | version: 2 44 | test: 45 | jobs: 46 | - lint 47 | - test-node6 48 | - test-node8 49 | - test-node10 50 | - test-browser 51 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "array-callback-return": "off", 8 | "class-methods-use-this": "off", 9 | "comma-dangle": [ 10 | "error", 11 | { 12 | "arrays": "always-multiline", 13 | "objects": "always-multiline", 14 | "functions": "never" 15 | } 16 | ], 17 | "eqeqeq": "off", 18 | "guard-for-in": "off", 19 | "max-len": "off", 20 | "no-loop-func": "off", 21 | "no-param-reassign": "off", 22 | "no-underscore-dangle": "off", 23 | }, 24 | "overrides": [ 25 | { 26 | "files": "*-test.js", 27 | "rules": { 28 | "no-unused-expressions": "off" 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # next.js build output 58 | .next 59 | 60 | dist/ 61 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .eslintrc 3 | .travis.yml 4 | karma.conf.js 5 | dist/.git-keep 6 | dist/lib 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Minim Changelog 2 | 3 | ## 0.23.8 (2020-06-12) 4 | 5 | ### Enhancements 6 | 7 | - `ArrayElement`'s `contains` method has been renamed to `includes` to be 8 | consistent with `Array.includes`. `ArrayElement.contains` has been 9 | deprecated, and remains for compatibility. 10 | 11 | ### Bug Fixes 12 | 13 | - Prevent throwing an error when calling `toValue()` on an element with a key 14 | value pair which does not have a value. 15 | 16 | ## 0.23.7 (2020-04-27) 17 | 18 | ### Bug Fixes 19 | 20 | - Prevents the JSON serializer from serializing an empty object (`{}`) under 21 | meta and attributes under the case where none of the meta or attribute 22 | member's have a value. This prevents `{}` from being present under meta or 23 | attributes when setting a member with an undefined key. 24 | 25 | ## 0.23.6 (2019-09-10) 26 | 27 | ### Bug Fixes 28 | 29 | - Fixes a JSON 0.6 serialisation bug where httpRequest and similar array-based 30 | elements with undefined content would be serialised with undefined content 31 | instead of an empty array as content. 32 | 33 | ## 0.23.5 (2019-07-02) 34 | 35 | This release brings some performance improvements, namely to serialising with 36 | the JSON serialisers. 37 | 38 | ## 0.23.4 (2019-06-11) 39 | 40 | ### Bug Fixes 41 | 42 | - Fixes serialisation of default values in enumerations in 43 | Refract JSON 0.6 serialisation. 44 | 45 | ## 0.23.3 (2019-04-06) 46 | 47 | ### Enhancements 48 | 49 | - Added support for IE11 in the included web distribution of minim 50 | (`dist/minim.js`). 51 | 52 | ## 0.23.2 (2019-03-15) 53 | 54 | ### Bug Fixes 55 | 56 | - Fixes serialisation of array and object sample values in enumerations in 57 | Refract JSON 0.6 serialisation. 58 | 59 | ## 0.23.1 (2019-02-25) 60 | 61 | ### Bug Fixes 62 | 63 | - Restores documentation coverage for all elements, some was unintentionally 64 | removed in 0.23.0. 65 | 66 | ## 0.23.0 (2019-02-22) 67 | 68 | ### Breaking 69 | 70 | - Support for Node 4 has been removed. Minim now supports Node >= 6. 71 | - Minim no longer uses [uptown](http://github.com/smizell/uptown) and thus the 72 | `extend` API has been removed. 73 | 74 | ### Enhancements 75 | 76 | - Calling `.freeze()` on a frozen element is now supported. Previously you may 77 | see an error thrown while freeze was trying to attach parents to any child 78 | elements. 79 | 80 | ## 0.22.1 (2018-12-10) 81 | 82 | ### Bug Fixes 83 | 84 | - Fixes serialising an element with an undefined meta or attributes value. For 85 | example if a meta value (`id`) was set to `undefined`, then it should not be 86 | serialised. Previously the serialiser would throw an exception that 87 | undefined was not an element. 88 | 89 | ## 0.22.0 90 | 91 | ### Enhancements 92 | 93 | - `ArrayElement` now conforms to parts of the [Fantasy 94 | Land](https://github.com/fantasyland/fantasy-land) 3.5 specification. 95 | `Functor`, `Semigroup`, `Monoid`, `Filterable`, `Chain`, and `Foldable` are 96 | now supported. 97 | 98 | ## 0.21.1 99 | 100 | ### Bug Fixes 101 | 102 | - Empty parseResult and link arrays are serialised in JSON 06 Serialiser, a 103 | regression of 0.21.0 caused these to not be serialised. 104 | 105 | ## 0.21.0 106 | 107 | ### Breaking 108 | 109 | - Minim no longer supports importing files directly from the minim package. 110 | Importing the JSON 0.6 serialiser via 111 | `require('minim/lib/serialisers/json-0.6')` is not supported, it is now 112 | recommended to import `JSON06Serialiser` and other APIs from minim directly. 113 | 114 | ```js 115 | const { JSON06Serialiser } = require('minim'); 116 | ``` 117 | 118 | - `flatMap` in `ArraySlice` no longer removes empty items. Instead `flatMap` is 119 | aligned with 120 | [`Array.flatMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) 121 | which first maps each element using a mapping function, then flattens the 122 | result into a new array. 123 | 124 | Existing `flatMap` behaviour is now available under the method `compactMap`. 125 | 126 | ### Enhancements 127 | 128 | - Object Element can now be created with an array of member elements. 129 | 130 | - You can now create an element from an ArraySlice or ObjectSlice, for example, 131 | passing the result of a `filter` operation into a new element. 132 | 133 | ``` 134 | const numbers = new ArrayElement([1, 2, 3, 4]) 135 | new ArrayElement(numbers.filter((e) => e.toValue() % 2)) 136 | ``` 137 | 138 | - Adds `compactMap` functionality to Array and Object elements allowing you to 139 | returns an array containing the truthy results of calling the given 140 | transformation with each element of this sequence. 141 | 142 | - Added `flatMap` to `ArrayElement`. 143 | 144 | ### Bug Fixes 145 | 146 | - The default content value of an element is undefined. Whereas before the 147 | default value was `null`. 148 | 149 | - Setting the `content` property on an Element now behaves the same as passing 150 | content in to the constructor. For example, the following two elements are 151 | identical: 152 | 153 | ```js 154 | new ArrayElement([1]) 155 | 156 | const element = new ArrayElement() 157 | element.content = [1] 158 | ``` 159 | 160 | Passing `[1]` to an `ArrayElement` constructor would produce an array of 161 | number elements, whereas setting the content to `[1]` resulted in setting the 162 | content to be an array of non-elements which is invalid. 163 | 164 | - The serialisation of the `variable` attribute in the JSON 0.6 serialisation 165 | is updated to reflect API Elements 1.0. The `variable` attribute is now 166 | present on a member element instead of the key of a member element. 167 | 168 | - Empty arrays are no longer serialised in JSON 06 Serialiser. 169 | 170 | ## 0.20.7 171 | 172 | ### Bug Fixes 173 | 174 | - Fixes a regression from 0.20.6 where `metadata` became an `ObjectElement` 175 | instead of `ArrayElement` as it was in the past. 176 | 177 | ## 0.20.6 178 | 179 | ### Bug Fixes 180 | 181 | - JSON 0.6 deserialiser will now correct deserialise an API Categories `meta` 182 | attribute into `metadata`. 183 | 184 | - JSON Serialisers will now use elements from the given namespace during 185 | serialisation checks and deserialisation. 186 | 187 | ## 0.20.5 188 | 189 | ### Bug Fixes 190 | 191 | - JSON 0.6 enum serialisation will now remove `fixed` typeAttributes which are 192 | now present in API Elements 1.0 enumerations. These are removed for 193 | consistent serialisation of the 0.6 serialiser. 194 | 195 | ## 0.20.4 196 | 197 | - Further performance improvements have been made to JSON Serialisation. The 198 | serialiser can now deserialise deep structures substantially faster. 199 | 200 | ## 0.20.3 201 | 202 | ### Enhancements 203 | 204 | - Minim NPM package now contains a browser distribution in `dist/minim.js`. 205 | - Performance improvements have been made to JSON Serialisation. The serialiser 206 | can now serialise deep structures a little faster. 207 | 208 | ## 0.20.2 209 | 210 | ### Bug Fixes 211 | 212 | - The JSON 0.6 serialiser will now serialise empty content arrays. A regression 213 | caused in 0.20.1 because of the logic was applied to both Refract JSON 1.0 214 | and 0.6 serialisers. 215 | 216 | ## 0.20.1 217 | 218 | ### Bug Fixes 219 | 220 | - Prevent de-serialising `undefined` if the default element's content is not 221 | null. 222 | - No longer serialise an empty array in the JSON serialisers, instead the 223 | content can be removed for consistency with other tools. 224 | 225 | ## 0.20.0 226 | 227 | ### Enhancements 228 | 229 | - Adds a `reject` method to `ArrayElement`, `ObjectElement`, `ArraySlice`, 230 | and `ObjectSlice` which complements the `filter` method providing the ability 231 | to exclude vs filter matched elements. 232 | 233 | ### Breaking 234 | 235 | - The Refract JSON 0.6 serialiser will de-serialise enum elements into the form 236 | in the API Elements 1.0 specification. This is a breaking change on the 237 | layout of the enum. Default and sample values will now be an `enum` element 238 | themselves. 239 | 240 | ### Bug Fixes 241 | 242 | - JSON deserialisers will now prevent overriding default element content 243 | values with undefined. This could cause problems where internal state of 244 | array or object element would have undefined as content and thus cause other 245 | Element methods to later fail such as `toValue` or `get`. 246 | 247 | ## 0.19.2 248 | 249 | ### Enhancements 250 | 251 | - ArraySlice now provides a `find` method allowing you to find the first 252 | element satisfying the given value. 253 | - ArraySlice.filter now accepts element names or element classes to filter. 254 | - ArraySlice now provides `flatMap` allowing you to map and then flatten the 255 | results. 256 | 257 | ### Bug Fixes 258 | 259 | - Accessing lazy meta accessors on frozen elements such as `title` will now 260 | return a frozen default value. Previously this would raise an exception 261 | trying to mutate the element. 262 | 263 | ## 0.19.1 264 | 265 | ### Enhancements 266 | 267 | - Serialisers will now throw TypeError with straight forward messages when you 268 | try to serialise a non-element type. 269 | 270 | ### Bug Fixes 271 | 272 | - While accessing meta or attributes of a frozen element that does not contain 273 | meta or attributes, an exception was raised because these accessors would 274 | lazy load and attempt to mutate the element. 275 | 276 | These accessors will now return an empty frozen `ObjectElement` in these 277 | cases now to prevent mutation. 278 | - Fixes JSON 0.6 Deserialiser to correct deserialise enum elements. 279 | - When multiple sample values were present additional values were being discarded. 280 | - Deserialised enum content contained duplicate enumeration values. 281 | 282 | ## 0.19.0 283 | 284 | ### Breaking 285 | 286 | - Updated enum serialization/deserialization in the JSON 0.6 serializer to match 287 | https://github.com/apiaryio/api-elements/pull/28 288 | - `Element.children` and `Element.recursiveChildren` now return `ArraySlice` 289 | instead of an `ArrayElement`. 290 | - `ArrayElement.filter` and `ArrayElement.find*` now return `ArraySlice` 291 | instead of an `ArrayElement`. 292 | - The `first`, `second` and `last` methods on `ArrayElement` are now properties 293 | instead of methods. 294 | - `ObjectElement.filter` now returns an `ObjectSlice` instead of an 295 | `ObjectElement`. 296 | - When providing multiple element names to `Element.findRecursive` you must 297 | call `freeze` on the element beforehand so that the element has access to the 298 | parent of the element. 299 | 300 | ### Enhancements 301 | 302 | - Introduced JSDoc documentation to public interfaces 303 | - `Element` now contains a `freeze` method to freeze and prevent an element 304 | from being mutated, this also adds a parent property on all child elements. 305 | 306 | ### Bug Fixes 307 | 308 | - Handle serializing key-value pair without value 309 | - Deserialize `dataStructure` containing an array correctly 310 | 311 | ## 0.18.1 312 | 313 | ### Bug Fixes 314 | 315 | - Prevent JSON Serialisers from throwing exception when serialising a key value 316 | pair without any value. 317 | 318 | ## 0.18.0 319 | 320 | ### Breaking 321 | 322 | - JSON Serialisation now follows the JSON Refract serialisation rules defined at 323 | https://github.com/refractproject/refract-spec/blob/master/formats/json-refract.md. 324 | 325 | Existing serialiser is available during a transition period to aid migration 326 | to the new format. 327 | 328 | ```js 329 | const JSONSerialiser = require('minim/serialisers/json-0.6'); 330 | const serialiser = new JSONSerialiser(); 331 | const element = serialiser.deserialise('Hello'); 332 | serialiser.serialise(element); 333 | ``` 334 | 335 | ### Enhancements 336 | 337 | - ArrayElement high-order functions, `map`, `filter` and `forEach` now accept 338 | `thisArg` like the equivalent functionality in `Array`. 339 | 340 | ## 0.17.1 (2016-07-29) 341 | 342 | ### Bug Fixes 343 | 344 | - Initialising an Element with given meta or attributes as ObjectElement is now 345 | supported. 346 | - When converting JavaScript values to Refract, objects are now supported. 347 | - Adds a special case to serialise sourceMap elements as values. 348 | 349 | ## 0.17.0 (2017-06-16) 350 | 351 | ### Breaking 352 | 353 | - `Element.toRefract()` and `Element.fromRefract()` have been removed. JSON 354 | Serialisation is now decoupled from the Element model. A minim namespace 355 | provides a convenience `toRefract(element)` and `fromRefract(object)` 356 | methods. 357 | 358 | - `ArrayElement` `children` method has been replaced by a `children` property 359 | on all elements. You may now chain children in conjunction with `filter` to 360 | get the existing behaviour. 361 | 362 | Before: 363 | 364 | ```js 365 | const numbers = doc.children((element) => element.element == 'number'); 366 | ``` 367 | 368 | After: 369 | 370 | ```js 371 | const numbers = doc.children.filter((element) => element.element == 'number'); 372 | ``` 373 | 374 | *OR* 375 | 376 | ```js 377 | const numbers = doc.children.findByElement('number'); 378 | ``` 379 | 380 | - `BaseElement` has been renamed to `Element`. 381 | 382 | - Embedded Refract support has been removed. 383 | 384 | ### Enhancements 385 | 386 | - All elements now contain a `children` and `recursiveChildren` properties that 387 | return an ArrayElement of the respective children elements. 388 | - JSON Serialiser will no longer serialise empty `meta` and `attributes` into 389 | JSON objects. 390 | - Minim now contains a `RefElement`. 391 | - Element now contains a `toRef()` function to create a ref element referencing 392 | the element. 393 | 394 | ## 0.16.0 (2017-05-04) 395 | 396 | ### Breaking 397 | 398 | - Node 0.10 and 0.12 are no longer supported. 399 | - Elements `name` property was removed. There is no longer a name property in 400 | Refract specification. 401 | 402 | ### Enhancements 403 | 404 | - Elements now provide a `findRecursive` method allowing you to recursively 405 | find matching elements. 406 | - Added function for remove key in an Object element and Array element 407 | 408 | #### Array Element 409 | 410 | - New `isEmpty` convenience property for determining if an array is empty. 411 | 412 | ## 0.15.0 (2017-04-03) 413 | 414 | - Getters of link element will now return an element 415 | - Meta convenience methods will now return an element 416 | 417 | ## 0.14.2 (2016-08-19) 418 | 419 | - Update Lodash version 420 | 421 | ## 0.14.1 (2016-08-17) 422 | 423 | - Update Uptown to 0.4.1 424 | 425 | ## 0.14.0 (2016-04-28) 426 | 427 | - **BREAKING** The public interface of the `minim` module has changed significantly. List of changes: 428 | 429 | - Removed `toCompactRefract` and `fromCompactRefract` 430 | - Improved the default refract serialization such that when an element in `attributes` has its own metadata or attributes defined then it will now be refracted when calling `toRefract` 431 | 432 | ## 0.13.0 (2015-12-03) 433 | 434 | - Added support for hyperlinks per [RFC 0008](https://github.com/refractproject/rfcs/blob/b6e390f7bbc960808ba053e172cccd9e4a81a04a/text/0008-add-hyperlinks.md) 435 | - Upgraded Lodash to 3.10.1 436 | - Refract elements will be automatically parsed when found in arrays in `meta` 437 | 438 | ## 0.12.3 (2015-11-30) 439 | 440 | - When an element in `meta` has its own metadata or attributes defined then it will now be refracted when calling `toRefract` or `toCompactRefract`. 441 | - When loading from refract or compact refract, if an item in `meta` looks like an element it will be loaded as such. This may cause false positives. 442 | 443 | ## 0.12.2 (2015-11-24) 444 | 445 | - Fix a bug related to setting the default key names that should be treated as refracted elements in element attributes. This is now accomplished via the namespace: `namespace._elementAttributeKeys.push('my-value');`. This fixes bugs related to overwriting the `namespace.BaseElement`. 446 | 447 | ## 0.12.1 (2015-11-24) 448 | 449 | - Fix a bug when loading refracted attributes from compact refract. 450 | 451 | ## 0.12.0 (2015-11-23) 452 | 453 | - Provide a way for elements to mark attributes as unrefracted arrays of 454 | refracted elements. Subclassed elements can push onto the 455 | `_attributeElementArrayKeys` property to use this feature. **Note**: in the 456 | future this feature may go away. 457 | - Allow `load` to be used for plugins where a namespace is not being used 458 | - Add an `elements` property to the `Namespace` class which returns an object of PascalCased element name keys to registered element class values. This allows for ES6 use cases like: 459 | 460 | ```js 461 | const {StringElement, ArrayElement, ObjectElement} = namespace.elements; 462 | ``` 463 | 464 | - Add functionality for Embedded Refract 465 | 466 | ## 0.11.0 (2015-09-07) 467 | 468 | ### Breaking 469 | 470 | The public interface of the `minim` module has changed significantly. List of changes: 471 | 472 | - `ElementRegistry` has been renamed to `Namespace`. 473 | - `minim` has only one public method, called `namespace`, which creates a new `Namespace` instance. 474 | - `minim.convertToElement` is now `namespace.toElement` 475 | - `minim.convertFromRefract` is now `namespace.fromRefract` 476 | - `minim.convertFromCompactRefract` is now `namespace.fromCompactRefract` 477 | - `minim.*Element` are removed (except for `namespace.BaseElement`). These should be accessed via `namespace.getElementClass('name')` now. 478 | - The `Namespace` has a new method `use` which loads a plugin namespace and is chainable, e.g. `namespace.use(plugin1).use(plugin2)`. 479 | - A `Namespace` can be initialized without any default elements by passing an options object with `noDefault` set to `false` to the constructor. They can be initialized later via the `useDefault` method. 480 | 481 | Before: 482 | 483 | ```js 484 | var minim = require('minim'); 485 | minim.convertToElement([1, 2, 3]); 486 | ``` 487 | 488 | After: 489 | 490 | ```js 491 | var minim = require('minim'); 492 | var namespace = minim.namespace(); 493 | namespace.toElement([1, 2, 3]); 494 | ``` 495 | 496 | - Add a `.toValue()` method to member elements which returns a hash with the key 497 | and value and their respective values. 498 | 499 | ## 0.10.0 (2015-08-18) 500 | 501 | - Rename the `class` metadata property to `classes`. The convenience property 502 | is also now called `classes`, e.g. `element.classes.contains('abc')`. 503 | 504 | ## 0.9.0 (2015-07-28) 505 | 506 | - Allow the iterator protocol to be used with arrays and objects if the runtime 507 | supports it. This enables using `for ... of` loops on elements as well as 508 | rest operators, destructuring, `yield*`, etc. 509 | - Convenience properties for simple types now return the value result. Instead 510 | of `element.title.toValue()` you now use `element.title`. 511 | - Add array indexes to `#forEach`. 512 | - Add a `#clone` method. 513 | - Add a `#reduce` method. 514 | - Fix a serialization bug when initializing using falsey values 515 | (`null`, `0`, `false`). 516 | 517 | ## 0.8.0 (2015-07-09) 518 | 519 | - Allow `#set` to take an object for Object Elements 520 | - Convert `meta` to be Minim Object Elements 521 | - Convert `attributes` to be Minim Object Elements 522 | - Sync class and method names with Refract 0.2.0 spec 523 | - Add convenience methods for `meta` attributes, such as `id` or `class` 524 | - Add finder functions, such as `findByElement` and `findByClass` 525 | - Upgrade to use Uptown 0.4.0 526 | - Organize code 527 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Stephen Mizell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minim 2 | 3 | 4 | A library for interacting with [Refract elements](https://github.com/refractproject/refract-spec). 5 | 6 | ## Install 7 | 8 | ```shell 9 | $ npm install minim 10 | ``` 11 | 12 | ## About 13 | 14 | In working with the XML-based DOM, there is a limitation on what types are available in the document. Element attributes may only be strings, and element values can only be strings, mixed types, and nested elements. 15 | 16 | JSON provides additional types, which include objects, arrays, booleans, and nulls. A plain JSON document, though, provides no structure and no attributes for each property and value in the document. 17 | 18 | Refract is a JSON structure for JSON documents to make a more flexible document object model. In Refract, each element has three components: 19 | 20 | 1. Name of the element 21 | 1. Metadata 22 | 1. Attributes 23 | 1. Content (which can be of different elements depending on the element) 24 | 25 | An element ends up looking like this: 26 | 27 | ```javascript 28 | const element = { 29 | element: 'string', 30 | content: 'bar' 31 | }; 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Converting JavaScript Values into Elements 37 | 38 | ```javascript 39 | var minim = require('minim').namespace(); 40 | var arrayElement = minim.toElement([1, 2, 3]); 41 | var refract = minim.toRefract(arrayElement); 42 | ``` 43 | 44 | The `refract` variable above has the following JSON value. 45 | 46 | ```json 47 | { 48 | "element": "array", 49 | "content": [ 50 | { 51 | "element": "number", 52 | "content": 1 53 | }, 54 | { 55 | "element": "number", 56 | "content": 2 57 | }, 58 | { 59 | "element": "number", 60 | "content": 3 61 | } 62 | ] 63 | } 64 | ``` 65 | 66 | ### Converting Serialized Refract into Elements 67 | 68 | Serialized Refract can be converted back to Minim elements to make a roundtrip. 69 | 70 | ```javascript 71 | var arrayElement1 = minim.toElement([1, 2, 3]); 72 | var refracted = minim.toRefract(arrayElement1); 73 | var arrayElement2 = minim.fromRefract(refracted); 74 | ``` 75 | 76 | Note that due to optional refracting in `meta`, anything that looks like an element in the given serialization will be loaded as such. 77 | 78 | ### Extending elements 79 | 80 | You can extend elements using the `extend` static method. 81 | 82 | ```javascript 83 | var StringElement = minim.getElementClass('string'); 84 | var NewElement = StringElement.extend({ 85 | constructor: function() { 86 | this.__super(); 87 | }, 88 | 89 | customMethod: function() { 90 | // custom code here 91 | } 92 | }) 93 | ``` 94 | 95 | ### Element Attributes 96 | 97 | Each Minim element provides the following attributes: 98 | 99 | - element (string) - The name of the element 100 | - meta (object) - The element's metadata 101 | - attributes (object) - The element's attributes 102 | - content - The element's content, e.g. a list of other elements. 103 | 104 | Additionally, convenience attributes are exposed on the element: 105 | 106 | - id (StringElement) - Shortcut for `.meta.get('id')`. 107 | - name (StringElement) - Shortcut for `.meta.get('name')`. 108 | - classes (ArrayElement) - Shortcut for `.meta.get('classes')`. 109 | - title (StringElement) - Shortcut for `.meta.get('title')`. 110 | - description (StringElement) - Shortcut for `.meta.get('description')`. 111 | 112 | ### Element Methods 113 | 114 | Each Minim element provides the following methods. 115 | 116 | #### toValue 117 | 118 | The `toValue` method returns the JSON value of the Minim element. 119 | 120 | ```javascript 121 | var arrayElement = minim.toElement([1, 2, 3]); 122 | var arrayValue = arrayElement.toValue(); // [1, 2, 3] 123 | ``` 124 | 125 | #### toRef 126 | 127 | The `toRef` method returns a RefElement referencing the element. 128 | 129 | ```javascript 130 | var ref = element.toRef(); 131 | ``` 132 | 133 | `toRef` accepts an optional path. 134 | 135 | ```javascript 136 | var ref = element.toRef('attributes'); 137 | ``` 138 | 139 | #### equals 140 | 141 | Allows for testing equality with the content of the element. 142 | 143 | ```javascript 144 | var stringElement = minim.toElement("foobar"); 145 | stringElement.equals('abcd'); // returns false 146 | ``` 147 | 148 | #### clone 149 | 150 | Creates a clone of the given instance. 151 | 152 | ```javascript 153 | var stringElement = minim.toElement("foobar"); 154 | var stringElementClone = stringElement.clone(); 155 | ``` 156 | 157 | #### findRecursive 158 | 159 | Recursively find an element. Returns an ArrayElement containing all elements 160 | that match the given element name. 161 | 162 | ```javascript 163 | const strings = element.findRecursive('string'); 164 | ``` 165 | 166 | You may pass multiple element names to `findRecursive`. When multiple element 167 | names are passed down, minim will only find an element that is found within 168 | the other given elements. For example, we can pass in `member` and `string` so 169 | that we are recursively looking for all `string` elements that are found within a 170 | `member` element: 171 | 172 | ```javascript 173 | const stringInsideMembers = element.findRecursive('member', 'string'); 174 | ``` 175 | 176 | #### children 177 | 178 | The `children` property returns an `ArrayElement` containing all of the direct children elements. 179 | 180 | ```javascript 181 | var arrayElement = minim.toElement(['a', [1, 2], 'b', 3]); 182 | var numbers = arrayElement.children(function(el) { 183 | return el.element === 'number'; 184 | }).toValue(); // [3] 185 | ``` 186 | 187 | #### recursiveChildren 188 | 189 | The `recursiveChildren` property returns an `ArrayElement` containing all of the children elements recursively. 190 | 191 | ```javascript 192 | var arrayElement = minim.toElement(['a', [1, 2], 'b', 3]); 193 | var children = arrayElement.recursiveChildren; 194 | children.toValue(); // ['a', 1, 2, 'b', 3] 195 | ``` 196 | 197 | ##### Chaining 198 | 199 | ```javascript 200 | var evenNumbers = array 201 | .recursiveChildren 202 | .findByElement('number') 203 | .filter((element) => element.toValue() % 2) 204 | ``` 205 | 206 | ### Minim Elements 207 | 208 | Minim supports the following primitive elements 209 | 210 | #### NullElement 211 | 212 | This is an element for representing the `null` value. 213 | 214 | #### StringElement 215 | 216 | This is an element for representing string values. 217 | 218 | ##### set 219 | 220 | The `set` method sets the value of the `StringElement` instance. 221 | 222 | ```javascript 223 | var stringElement = minim.toElement(''); 224 | stringElement.set('foobar'); 225 | var value = stringElement.toValue() // toValue() returns 'foobar' 226 | ``` 227 | 228 | #### NumberElement 229 | 230 | This is an element for representing number values. 231 | 232 | ##### set 233 | 234 | The `set` method sets the value of the `NumberElement` instance. 235 | 236 | ```javascript 237 | var numberElement = minim.toElement(0); 238 | numberElement.set(4); 239 | var value = numberElement.toValue() // toValue() returns 4 240 | ``` 241 | 242 | #### BooleanElement 243 | 244 | This is an element for representing boolean values. 245 | 246 | ##### set 247 | 248 | The `set` method sets the value of the `BooleanElement` instance. 249 | 250 | ```javascript 251 | var booleanElement = minim.toElement(false); 252 | booleanElement.set(true); 253 | var value = booleanElement.toValue() // toValue() returns true 254 | ``` 255 | 256 | #### ArrayElement 257 | 258 | This is an element for representing arrays. 259 | 260 | ##### Iteration 261 | 262 | The array element is iterable if the environment supports the [iteration protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#iterable). You can then use the element in `for ... of` loops, use the spread operator, `yield*`, and destructuring assignment. 263 | 264 | ```js 265 | const arrayElement = minim.toElement(['a', 'b', 'c']); 266 | 267 | for (let item of arrayElement) { 268 | console.log(item); 269 | } 270 | ``` 271 | 272 | ##### get 273 | 274 | The `get` method returns the item of the `ArrayElement` instance at the given index. 275 | 276 | ```javascript 277 | var arrayElement = minim.toElement(['a', 'b', 'c']); 278 | var value = arrayElement.get(0) // get(0) returns item for 'a' 279 | ``` 280 | 281 | ##### getValue 282 | 283 | The `getValue` method returns the value of the item of the `ArrayElement` instance at the given index. 284 | 285 | ```javascript 286 | var arrayElement = minim.toElement(['a', 'b', 'c']); 287 | var value = arrayElement.getValue(0) // get(0) returns 'a' 288 | ``` 289 | 290 | ##### getIndex 291 | 292 | The `getIndex` method returns the item of the array at a given index. 293 | 294 | ```javascript 295 | var arrayElement = minim.toElement(['a', 'b', 'c']); 296 | var value = arrayElement.getIndex(0) // returns the item for 'a' 297 | ``` 298 | 299 | ##### set 300 | 301 | The `set` method sets the value of the `ArrayElement` instance. 302 | 303 | ```javascript 304 | var arrayElement = minim.toElement([]); 305 | arrayElement.set(0, 'z'); 306 | var value = arrayElement.get(0) // get(0) returns 'z' 307 | ``` 308 | 309 | ##### remove 310 | 311 | The `remove` method removes an item (specified by index) from the `ArrayElement` instance. 312 | 313 | ```javascript 314 | var arrayElement = minim.toElement(['a', 'b', 'c']); 315 | arrayElement.remove(0); 316 | var value = arrayElement.get(0) // returns 'b' 317 | ``` 318 | 319 | ##### map 320 | 321 | The `map` method may be used to map over an array. Each item given is a Minim instance. 322 | 323 | ```javascript 324 | var arrayElement =minim.toElement(['a', 'b', 'c']); 325 | var newArray = arrayElement.map(function(item) { 326 | return item.element; 327 | }); // newArray is now ['string', 'string', 'string'] 328 | ``` 329 | 330 | ##### filter 331 | 332 | The `filter` method may be used to filter a Minim array. This method returns a Minim array itself rather than a JavaScript array instance. 333 | 334 | ```javascript 335 | var arrayElement = minim.toElement(['a', 'b', 'c']); 336 | var newArray = arrayElement.filter(function(item) { 337 | return item.toValue() === 'a' 338 | }); // newArray.toValue() is now ['a'] 339 | ``` 340 | 341 | ##### reduce 342 | 343 | The `reduce` method may be used to reduce over a Minim array or object. The method takes a function and an optional beginning value. 344 | 345 | ```javascript 346 | var numbers = minim.toElement([1, 2, 3, 4]); 347 | var total = numbers.reduce(function(result, item) { 348 | return result.toValue() + item.toValue(); 349 | }); // total.toValue() === 10 350 | ``` 351 | 352 | The `reduce` method also takes an initial value, which can either be a value or Minim element. 353 | 354 | ```javascript 355 | var numbers = minim.toElement([1, 2, 3, 4]); 356 | var total = numbers.reduce(function(result, item) { 357 | return result.toValue() + item.toValue(); 358 | }, 10); // total.toValue() === 20 359 | ``` 360 | 361 | The `reduce` method also works with objects: 362 | 363 | ```javascript 364 | var objNumbers = minim.toElement({a: 1, b:2, c:3, d:4}); 365 | var total = objNumbers.reduce(function(result, item) { 366 | return result.toValue() + item.toValue(); 367 | }, 10); // total.toValue() === 20 368 | ``` 369 | 370 | The function passed to `reduce` can accept up to five optional parameters and depends on whether you are using an array element or object element: 371 | 372 | **Array** 373 | 1. `result`: the reduced value thus far 374 | 2. `item`: the current item in the array 375 | 3. `index`: the zero-based index of the current item in the array 376 | 4. `arrayElement`: the array element which contains `item` (e.g. `numbers` above) 377 | 378 | **Object** 379 | 1. `result`: the reduced value thus far 380 | 2. `item`: the value element of the current item in the object 381 | 3. `key`: the key element of the current item in the object 382 | 4. `memberElement`: the member element which contains `key` and `value` 383 | 5. `objectElement`: the object element which contains `memberElement` (e.g. `objNumbers` above) 384 | 385 | ##### forEach 386 | 387 | The `forEach` method may be used to iterate over a Minim array. 388 | 389 | ```javascript 390 | var arrayElement = minim.toElement(['a', 'b', 'c']); 391 | arrayElement.forEach(function(item) { 392 | console.log(item.toValue()) 393 | }); // logs each value to console 394 | ``` 395 | 396 | ##### shift 397 | 398 | The `shift` method may be used to remove an item from the start of a Minim array. 399 | 400 | ```javascript 401 | var arrayElement = minim.toElement(['a', 'b', 'c']); 402 | var element = arrayElement.shift(); 403 | console.log(element.toValue()); // a 404 | ``` 405 | 406 | ##### unshift 407 | 408 | The `unshift` method may be used to inserts items to the start of a Minim array. 409 | 410 | ```javascript 411 | var arrayElement = minim.toElement(['a', 'b', 'c']); 412 | arrayElement.unshift('d'); 413 | console.log(arrayElement.toValue()); // ['d', 'a', 'b', 'c'] 414 | ``` 415 | 416 | ##### push 417 | 418 | The `push` method may be used to add items to a Minim array. 419 | 420 | ```javascript 421 | var arrayElement = minim.toElement(['a', 'b', 'c']); 422 | arrayElement.push('d'); 423 | console.log(arrayElement.toValue()); // ['a', 'b', 'c', 'd'] 424 | ``` 425 | 426 | ##### find 427 | 428 | The `find` method traverses the entire descendent element tree and returns an `ArrayElement` of all elements that match the conditional function given. 429 | 430 | ```javascript 431 | var arrayElement = minim.toElement(['a', [1, 2], 'b', 3]); 432 | var numbers = arrayElement.find(function(el) { 433 | return el.element === 'number' 434 | }).toValue(); // [1, 2, 3] 435 | ``` 436 | 437 | ##### findByClass 438 | 439 | The `findByClass` method traverses the entire descendent element tree and returns an `ArrayElement` of all elements that match the given class. 440 | 441 | ##### findByElement 442 | 443 | The `findByElement` method traverses the entire descendent element tree and returns an `ArrayElement` of all elements that match the given element name. 444 | 445 | ##### getById 446 | 447 | Search the entire tree to find a matching ID. 448 | 449 | ```javascript 450 | elTree.getById('some-id'); 451 | ``` 452 | 453 | ##### includes 454 | 455 | Test to see if a collection includes the value given. Does a deep equality check. 456 | 457 | ```javascript 458 | var arrayElement = minim.toElement(['a', [1, 2], 'b', 3]); 459 | arrayElement.includes('a'); // returns true 460 | ``` 461 | 462 | ##### length 463 | 464 | Returns the amount of items in the array element. 465 | 466 | ```javascript 467 | arrayElement.length; 468 | ``` 469 | 470 | ##### isEmpty 471 | 472 | Returns whether the array element is empty. 473 | 474 | ```javascript 475 | if (arrayElement.isEmpty) { 476 | console.log("We have an empty array"); 477 | } 478 | ``` 479 | 480 | ##### first 481 | 482 | Returns the first element in the collection. 483 | 484 | ```javascript 485 | var arrayElement = minim.toElement(['a', [1, 2], 'b', 3]); 486 | arrayElement.first; // returns the element for "a" 487 | ``` 488 | 489 | ##### second 490 | 491 | Returns the second element in the collection. 492 | 493 | ```javascript 494 | var arrayElement = minim.toElement(['a', [1, 2], 'b', 3]); 495 | arrayElement.second; // returns the element for "[1, 2]" 496 | ``` 497 | 498 | ##### last 499 | 500 | Returns the last element in the collection. 501 | 502 | ```javascript 503 | var arrayElement = minim.toElement(['a', [1, 2], 'b', 3]); 504 | arrayElement.last; // returns the element for "3" 505 | ``` 506 | 507 | #### ObjectElement 508 | 509 | This is an element for representing objects. Objects store their items as an ordered array, so they inherit most of the methods above from the `ArrayElement`. 510 | 511 | ##### get 512 | 513 | The `get` method returns the `ObjectElement` instance at the given name. 514 | See `getKey` and `getMember` for ways to get more instances around a key-value pair. 515 | 516 | ```javascript 517 | var objectElement = minim.toElement({ foo: 'bar' }); 518 | var value = objectElement.get('foo') // returns string instance for 'bar' 519 | ``` 520 | 521 | ##### getValue 522 | 523 | The `getValue` method returns the value of the `ObjectElement` instance at the given name. 524 | 525 | ```javascript 526 | var objectElement = minim.toElement({ foo: 'bar' }); 527 | var value = objectElement.getValue('foo') // returns 'bar' 528 | ``` 529 | 530 | ##### getKey 531 | 532 | The `getKey` method returns the key element of a key-value pair. 533 | 534 | ```javascript 535 | var objectElement = minim.toElement({ foo: 'bar' }); 536 | var key = objectElement.getKey('foo') // returns the key element instance 537 | ``` 538 | 539 | ##### getMember 540 | 541 | The `getMember` method returns the entire member for a key-value pair. 542 | 543 | ```javascript 544 | var objectElement = minim.toElement({ foo: 'bar' }); 545 | var member = objectElement.getMember('foo') // returns the member element 546 | var key = member.key; // returns what getKey('foo') returns 547 | var value = member.value; // returns what get('foo') returns 548 | ``` 549 | 550 | ##### set 551 | 552 | The `set` method sets the value of the `ObjectElement` instance. 553 | 554 | ```javascript 555 | var objectElement = minim.toElement({}); 556 | objectElement.set('foo', 'hello world'); 557 | var value = objectElement.get('foo') // get('foo') returns 'hello world' 558 | ``` 559 | 560 | ##### keys 561 | 562 | The `keys` method returns an array of keys. 563 | 564 | ```javascript 565 | var objectElement = minim.toElement({ foo: 'bar' }); 566 | var keys = objectElement.keys() // ['foo'] 567 | ``` 568 | 569 | ##### remove 570 | 571 | The `remove` method removes a key from the `ObjectElement` instance. 572 | 573 | ```javascript 574 | var objectElement = minim.toElement({ foo: 'bar' }); 575 | objectElement.remove('foo'); 576 | var keys = objectElement.keys() // [] 577 | ``` 578 | 579 | > You can use elementa.meta.remove() or element.attributes.remove() because of this. 580 | 581 | ##### values 582 | 583 | The `values` method returns an array of keys. 584 | 585 | ```javascript 586 | var objectElement = minim.toElement({ foo: 'bar' }); 587 | var values = objectElement.values() // ['bar'] 588 | ``` 589 | 590 | ##### items 591 | 592 | The `items` method returns an array of key value pairs which can make iteration simpler. 593 | 594 | ```js 595 | const objectElement = minim.toElement({ foo: 'bar' }); 596 | 597 | for (let [key, value] of objectElement.items()) { 598 | console.log(key, value); // foo, bar 599 | } 600 | ``` 601 | 602 | ##### map, filter, reduce, and forEach 603 | 604 | The `map`, `filter`, and `forEach` methods work similar to the `ArrayElement` map function, but the callback receives the value, key, and member element instances. The `reduce` method receives the reduced value, value, key, member, and object element instances. 605 | 606 | See `getMember` to see more on how to interact with member elements. 607 | 608 | ```js 609 | const objectElement = minim.toElement({ foo: 'bar' }); 610 | const values = objectElement.map((value, key, member) => { 611 | // key is an instance for foo 612 | // value is an instance for bar 613 | // member is an instance for the member element 614 | return [key.toValue(), value.toValue()]; // ['foo', 'bar'] 615 | }); 616 | ``` 617 | 618 | ### Namespace 619 | 620 | #### Namespace Methods 621 | 622 | ##### `toRefract` 623 | 624 | The `toRefract` method returns the Refract value of the Minim element. 625 | 626 | Note that if any element in `meta` has metadata or attributes defined that would be lost by calling `toValue()` then that element is also converted to refract. 627 | 628 | ```javascript 629 | var arrayElement = namespace.toElement([1, 2, 3]); 630 | var refract = namespace.toRefract(); 631 | ``` 632 | 633 | #### Customizing Namespaces 634 | 635 | Minim allows you to register custom elements. For example, if the element name you wish to handle is called `category` and it should be handled like an array: 636 | 637 | ```javascript 638 | var minim = require('minim').namespace(); 639 | var ArrayElement = minim.getElementClass('array'); 640 | 641 | // Register your custom element 642 | minim.register('category', ArrayElement); 643 | 644 | // Load serialized refract elements that includes the new element 645 | var elements = minim.fromRefract({ 646 | element: 'category', 647 | meta: {}, 648 | attributes: {}, 649 | content: [ 650 | { 651 | element: 'string', 652 | meta: {}, 653 | attributes: {}, 654 | content: 'hello, world' 655 | } 656 | ] 657 | }); 658 | 659 | console.log(elements.get(0).content); // hello, world 660 | 661 | // Unregister your custom element 662 | minim.unregister('category'); 663 | ``` 664 | 665 | #### Creating Namespace Plugins 666 | 667 | It is also possible to create plugin modules that define elements for custom namespaces. Plugin modules should export a single `namespace` function that takes an `options` object which contains an existing namespace to which you can add your elements: 668 | 669 | ```javascript 670 | var minim = require('minim').namespace(); 671 | 672 | // Define your plugin module (normally done in a separate file) 673 | var plugin = { 674 | namespace: function(options) { 675 | var base = options.base; 676 | var ArrayElement = base.getElementClass('array'); 677 | 678 | base.register('category', ArrayElement); 679 | 680 | return base; 681 | } 682 | } 683 | 684 | // Load the plugin 685 | minim.use(plugin); 686 | ``` 687 | 688 | The `load` property may be used in addition to the `namespace` property when a plugin is not implementing a namespace. 689 | 690 | ```javascript 691 | var minim = require('minim').namespace(); 692 | 693 | // Define your plugin module (normally done in a separate file) 694 | var plugin = { 695 | load: function(options) { 696 | // Plugin code here 697 | return base; 698 | } 699 | } 700 | 701 | // Load the plugin 702 | minim.use(plugin); 703 | ``` 704 | 705 | ### Chaining 706 | 707 | Methods may also be chained when using getters and setters. 708 | 709 | ```javascript 710 | var objectElement = minim.toElement({}) 711 | .set('name', 'John Doe') 712 | .set('email', 'john@example.com') 713 | .set('id', 4) 714 | ``` 715 | -------------------------------------------------------------------------------- /dist/.git-keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/refractproject/minim/2a7996d3e66da6a1cc1601cbdbcb4290ebf664be/dist/.git-keep -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Jul 29 2015 21:36:44 GMT+0200 (CEST) 3 | 4 | module.exports = (config) => { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['browserify', 'sinon', 'chai', 'mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/**/*.js', 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | ], 25 | 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: { 30 | 'test/**/*.js': ['browserify'], 31 | }, 32 | 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress' 36 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 37 | reporters: ['mocha'], 38 | 39 | 40 | // web server port 41 | port: 9876, 42 | 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | 48 | // level of logging 49 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch: false, 55 | 56 | 57 | // start these browsers 58 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 59 | browsers: ['Firefox'], 60 | 61 | 62 | // Continuous Integration mode 63 | // if true, Karma captures browsers, runs the tests and exits 64 | singleRun: true, 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /lib/ArraySlice.js: -------------------------------------------------------------------------------- 1 | const negate = require('lodash/negate'); 2 | 3 | // Coerces an a parameter into a callback for matching elements. 4 | // This accepts an element name, an element type and returns a 5 | // callback to match for those elements. 6 | function coerceElementMatchingCallback(value) { 7 | // Element Name 8 | if (typeof value === 'string') { 9 | return element => element.element === value; 10 | } 11 | 12 | // Element Type 13 | if (value.constructor && value.extend) { 14 | return element => element instanceof value; 15 | } 16 | 17 | return value; 18 | } 19 | 20 | /** 21 | * @class 22 | * 23 | * @param {Element[]} elements 24 | * 25 | * @property {Element[]} elements 26 | */ 27 | class ArraySlice { 28 | constructor(elements) { 29 | this.elements = elements || []; 30 | } 31 | 32 | /** 33 | * @returns {Array} 34 | */ 35 | toValue() { 36 | return this.elements.map(element => element.toValue()); 37 | } 38 | 39 | // High Order Functions 40 | 41 | /** 42 | * @param callback - Function to execute for each element 43 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 44 | * @returns {array} A new array with each element being the result of the callback function 45 | */ 46 | map(callback, thisArg) { 47 | return this.elements.map(callback, thisArg); 48 | } 49 | 50 | /** 51 | * Maps and then flattens the results. 52 | * @param callback - Function to execute for each element. 53 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 54 | * @returns {array} 55 | */ 56 | flatMap(callback, thisArg) { 57 | return this 58 | .map(callback, thisArg) 59 | .reduce((a, b) => a.concat(b), []); 60 | } 61 | 62 | /** 63 | * Returns an array containing the truthy results of calling the given transformation with each element of this sequence 64 | * @param transform - A closure that accepts an element of this array as its argument and returns an optional value. 65 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 66 | * @memberof ArraySlice.prototype 67 | * @returns An array of the non-undefined results of calling transform with each element of the array 68 | */ 69 | compactMap(transform, thisArg) { 70 | const results = []; 71 | 72 | this.forEach((element) => { 73 | const result = transform.bind(thisArg)(element); 74 | 75 | if (result) { 76 | results.push(result); 77 | } 78 | }); 79 | 80 | return results; 81 | } 82 | 83 | /** 84 | * @param callback - Function to execute for each element. This may be a callback, an element name or an element class. 85 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 86 | * @returns {ArraySlice} 87 | * @memberof ArraySlice.prototype 88 | */ 89 | filter(callback, thisArg) { 90 | callback = coerceElementMatchingCallback(callback); 91 | return new ArraySlice(this.elements.filter(callback, thisArg)); 92 | } 93 | 94 | /** 95 | * @param callback - Function to execute for each element. This may be a callback, an element name or an element class. 96 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 97 | * @returns {ArraySlice} 98 | * @memberof ArraySlice.prototype 99 | */ 100 | reject(callback, thisArg) { 101 | callback = coerceElementMatchingCallback(callback); 102 | return new ArraySlice(this.elements.filter(negate(callback), thisArg)); 103 | } 104 | 105 | /** 106 | * Returns the first element in the array that satisfies the given value 107 | * @param callback - Function to execute for each element. This may be a callback, an element name or an element class. 108 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 109 | * @returns {Element} 110 | * @memberof ArraySlice.prototype 111 | */ 112 | find(callback, thisArg) { 113 | callback = coerceElementMatchingCallback(callback); 114 | return this.elements.find(callback, thisArg); 115 | } 116 | 117 | /** 118 | * @param callback - Function to execute for each element 119 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 120 | * @memberof ArraySlice.prototype 121 | */ 122 | forEach(callback, thisArg) { 123 | this.elements.forEach(callback, thisArg); 124 | } 125 | 126 | /** 127 | * @param callback - Function to execute for each element 128 | * @param initialValue 129 | * @memberof ArraySlice.prototype 130 | */ 131 | reduce(callback, initialValue) { 132 | return this.elements.reduce(callback, initialValue); 133 | } 134 | 135 | /** 136 | * @param value 137 | * @returns {boolean} 138 | * @memberof ArraySlice.prototype 139 | */ 140 | includes(value) { 141 | return this.elements.some(element => element.equals(value)); 142 | } 143 | 144 | // Mutation 145 | 146 | /** 147 | * Removes the first element from the slice 148 | * @returns {Element} The removed element or undefined if the slice is empty 149 | * @memberof ArraySlice.prototype 150 | */ 151 | shift() { 152 | return this.elements.shift(); 153 | } 154 | 155 | /** 156 | * Adds the given element to the begining of the slice 157 | * @parameter {Element} value 158 | * @memberof ArraySlice.prototype 159 | */ 160 | unshift(value) { 161 | this.elements.unshift(this.refract(value)); 162 | } 163 | 164 | /** 165 | * Adds the given element to the end of the slice 166 | * @parameter {Element} value 167 | * @memberof ArraySlice.prototype 168 | */ 169 | push(value) { 170 | this.elements.push(this.refract(value)); 171 | return this; 172 | } 173 | 174 | /** 175 | * @parameter {Element} value 176 | * @memberof ArraySlice.prototype 177 | */ 178 | add(value) { 179 | this.push(value); 180 | } 181 | 182 | // Accessors 183 | 184 | /** 185 | * @parameter {number} index 186 | * @returns {Element} 187 | * @memberof ArraySlice.prototype 188 | */ 189 | get(index) { 190 | return this.elements[index]; 191 | } 192 | 193 | /** 194 | * @parameter {number} index 195 | * @memberof ArraySlice.prototype 196 | */ 197 | getValue(index) { 198 | const element = this.elements[index]; 199 | 200 | if (element) { 201 | return element.toValue(); 202 | } 203 | 204 | return undefined; 205 | } 206 | 207 | /** 208 | * Returns the number of elements in the slice 209 | * @type number 210 | */ 211 | get length() { 212 | return this.elements.length; 213 | } 214 | 215 | /** 216 | * Returns whether the slice is empty 217 | * @type boolean 218 | */ 219 | get isEmpty() { 220 | return this.elements.length === 0; 221 | } 222 | 223 | /** 224 | * Returns the first element in the slice or undefined if the slice is empty 225 | * @type Element 226 | */ 227 | get first() { 228 | return this.elements[0]; 229 | } 230 | } 231 | 232 | if (typeof Symbol !== 'undefined') { 233 | ArraySlice.prototype[Symbol.iterator] = function symbol() { 234 | return this.elements[Symbol.iterator](); 235 | }; 236 | } 237 | 238 | module.exports = ArraySlice; 239 | -------------------------------------------------------------------------------- /lib/KeyValuePair.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * 4 | * @property {Element} key 5 | * @property {Element} value 6 | */ 7 | class KeyValuePair { 8 | constructor(key, value) { 9 | this.key = key; 10 | this.value = value; 11 | } 12 | 13 | /** 14 | * @returns {KeyValuePair} 15 | */ 16 | clone() { 17 | const clone = new KeyValuePair(); 18 | 19 | if (this.key) { 20 | clone.key = this.key.clone(); 21 | } 22 | 23 | if (this.value) { 24 | clone.value = this.value.clone(); 25 | } 26 | 27 | return clone; 28 | } 29 | } 30 | 31 | module.exports = KeyValuePair; 32 | -------------------------------------------------------------------------------- /lib/Namespace.js: -------------------------------------------------------------------------------- 1 | const isNull = require('lodash/isNull'); 2 | const isString = require('lodash/isString'); 3 | const isNumber = require('lodash/isNumber'); 4 | const isBoolean = require('lodash/isBoolean'); 5 | const isObject = require('lodash/isObject'); 6 | 7 | const JSONSerialiser = require('./serialisers/JSONSerialiser'); 8 | const elements = require('./elements'); 9 | 10 | /** 11 | * @class 12 | * 13 | * A refract element implementation with an extensible namespace, able to 14 | * load other namespaces into it. 15 | * 16 | * The namespace allows you to register your own classes to be instantiated 17 | * when a particular refract element is encountered, and allows you to specify 18 | * which elements get instantiated for existing Javascript objects. 19 | */ 20 | class Namespace { 21 | constructor(options) { 22 | this.elementMap = {}; 23 | this.elementDetection = []; 24 | this.Element = elements.Element; 25 | this.KeyValuePair = elements.KeyValuePair; 26 | 27 | if (!options || !options.noDefault) { 28 | this.useDefault(); 29 | } 30 | 31 | // These provide the defaults for new elements. 32 | this._attributeElementKeys = []; 33 | this._attributeElementArrayKeys = []; 34 | } 35 | 36 | /** 37 | * Use a namespace plugin or load a generic plugin. 38 | * 39 | * @param plugin 40 | */ 41 | use(plugin) { 42 | if (plugin.namespace) { 43 | plugin.namespace({ base: this }); 44 | } 45 | if (plugin.load) { 46 | plugin.load({ base: this }); 47 | } 48 | return this; 49 | } 50 | 51 | /* 52 | * Use the default namespace. This preloads all the default elements 53 | * into this registry instance. 54 | */ 55 | useDefault() { 56 | // Set up classes for default elements 57 | this 58 | .register('null', elements.NullElement) 59 | .register('string', elements.StringElement) 60 | .register('number', elements.NumberElement) 61 | .register('boolean', elements.BooleanElement) 62 | .register('array', elements.ArrayElement) 63 | .register('object', elements.ObjectElement) 64 | .register('member', elements.MemberElement) 65 | .register('ref', elements.RefElement) 66 | .register('link', elements.LinkElement); 67 | 68 | // Add instance detection functions to convert existing objects into 69 | // the corresponding refract elements. 70 | this 71 | .detect(isNull, elements.NullElement, false) 72 | .detect(isString, elements.StringElement, false) 73 | .detect(isNumber, elements.NumberElement, false) 74 | .detect(isBoolean, elements.BooleanElement, false) 75 | .detect(Array.isArray, elements.ArrayElement, false) 76 | .detect(isObject, elements.ObjectElement, false); 77 | 78 | return this; 79 | } 80 | 81 | /** 82 | * Register a new element class for an element. 83 | * 84 | * @param {string} name 85 | * @param elementClass 86 | */ 87 | register(name, ElementClass) { 88 | this._elements = undefined; 89 | this.elementMap[name] = ElementClass; 90 | return this; 91 | } 92 | 93 | /** 94 | * Unregister a previously registered class for an element. 95 | * 96 | * @param {string} name 97 | */ 98 | unregister(name) { 99 | this._elements = undefined; 100 | delete this.elementMap[name]; 101 | return this; 102 | } 103 | 104 | /* 105 | * Add a new detection function to determine which element 106 | * class to use when converting existing js instances into 107 | * refract element. 108 | */ 109 | detect(test, ElementClass, givenPrepend) { 110 | const prepend = givenPrepend === undefined ? true : givenPrepend; 111 | 112 | if (prepend) { 113 | this.elementDetection.unshift([test, ElementClass]); 114 | } else { 115 | this.elementDetection.push([test, ElementClass]); 116 | } 117 | 118 | return this; 119 | } 120 | 121 | /* 122 | * Convert an existing Javascript object into refract element instances, which 123 | * can be further processed or serialized into refract. 124 | * If the item passed in is already refracted, then it is returned 125 | * unmodified. 126 | */ 127 | toElement(value) { 128 | if (value instanceof this.Element) { return value; } 129 | 130 | let element; 131 | 132 | for (let i = 0; i < this.elementDetection.length; i += 1) { 133 | const test = this.elementDetection[i][0]; 134 | const ElementClass = this.elementDetection[i][1]; 135 | 136 | if (test(value)) { 137 | element = new ElementClass(value); 138 | break; 139 | } 140 | } 141 | 142 | return element; 143 | } 144 | 145 | /* 146 | * Get an element class given an element name. 147 | */ 148 | getElementClass(element) { 149 | const ElementClass = this.elementMap[element]; 150 | 151 | if (ElementClass === undefined) { 152 | // Fall back to the base element. We may not know what 153 | // to do with the `content`, but downstream software 154 | // may know. 155 | return this.Element; 156 | } 157 | 158 | return ElementClass; 159 | } 160 | 161 | /* 162 | * Convert a refract document into refract element instances. 163 | */ 164 | fromRefract(doc) { 165 | return this.serialiser.deserialise(doc); 166 | } 167 | 168 | /* 169 | * Convert an element to a Refracted JSON object. 170 | */ 171 | toRefract(element) { 172 | return this.serialiser.serialise(element); 173 | } 174 | 175 | /* 176 | * Get an object that contains all registered element classes, where 177 | * the key is the PascalCased element name and the value is the class. 178 | */ 179 | get elements() { 180 | if (this._elements === undefined) { 181 | this._elements = { 182 | Element: this.Element, 183 | }; 184 | 185 | Object.keys(this.elementMap).forEach((name) => { 186 | // Currently, all registered element types use a camelCaseName. 187 | // Converting to PascalCase is as simple as upper-casing the first 188 | // letter. 189 | const pascal = name[0].toUpperCase() + name.substr(1); 190 | this._elements[pascal] = this.elementMap[name]; 191 | }); 192 | } 193 | 194 | return this._elements; 195 | } 196 | 197 | /** 198 | * Convinience method for getting a JSON Serialiser configured with the 199 | * current namespace 200 | * 201 | * @type JSONSerialiser 202 | * @readonly 203 | * 204 | * @memberof Namespace.prototype 205 | */ 206 | get serialiser() { 207 | return new JSONSerialiser(this); 208 | } 209 | } 210 | 211 | JSONSerialiser.prototype.Namespace = Namespace; 212 | 213 | module.exports = Namespace; 214 | -------------------------------------------------------------------------------- /lib/ObjectSlice.js: -------------------------------------------------------------------------------- 1 | const negate = require('lodash/negate'); 2 | const ArraySlice = require('./ArraySlice'); 3 | 4 | /** 5 | */ 6 | class ObjectSlice extends ArraySlice { 7 | map(callback, thisArg) { 8 | return this.elements.map(member => callback.bind(thisArg)(member.value, member.key, member)); 9 | } 10 | 11 | filter(callback, thisArg) { 12 | return new ObjectSlice(this.elements.filter(member => callback.bind(thisArg)(member.value, member.key, member))); 13 | } 14 | 15 | reject(callback, thisArg) { 16 | return this.filter(negate(callback.bind(thisArg))); 17 | } 18 | 19 | forEach(callback, thisArg) { 20 | return this.elements.forEach((member, index) => { callback.bind(thisArg)(member.value, member.key, member, index); }); 21 | } 22 | 23 | /** 24 | * @returns {array} 25 | */ 26 | keys() { 27 | return this.map((value, key) => key.toValue()); 28 | } 29 | 30 | /** 31 | * @returns {array} 32 | */ 33 | values() { 34 | return this.map(value => value.toValue()); 35 | } 36 | } 37 | 38 | module.exports = ObjectSlice; 39 | -------------------------------------------------------------------------------- /lib/elements.js: -------------------------------------------------------------------------------- 1 | const Element = require('./primitives/Element'); 2 | const NullElement = require('./primitives/NullElement'); 3 | const StringElement = require('./primitives/StringElement'); 4 | const NumberElement = require('./primitives/NumberElement'); 5 | const BooleanElement = require('./primitives/BooleanElement'); 6 | const ArrayElement = require('./primitives/ArrayElement'); 7 | const MemberElement = require('./primitives/MemberElement'); 8 | const ObjectElement = require('./primitives/ObjectElement'); 9 | const LinkElement = require('./elements/LinkElement'); 10 | const RefElement = require('./elements/RefElement'); 11 | 12 | const ArraySlice = require('./ArraySlice'); 13 | const ObjectSlice = require('./ObjectSlice'); 14 | 15 | const KeyValuePair = require('./KeyValuePair'); 16 | 17 | /** 18 | * Refracts a JSON type to minim elements 19 | * @param value 20 | * @returns {Element} 21 | */ 22 | function refract(value) { 23 | if (value instanceof Element) { 24 | return value; 25 | } 26 | 27 | if (typeof value === 'string') { 28 | return new StringElement(value); 29 | } 30 | 31 | if (typeof value === 'number') { 32 | return new NumberElement(value); 33 | } 34 | 35 | if (typeof value === 'boolean') { 36 | return new BooleanElement(value); 37 | } 38 | 39 | if (value === null) { 40 | return new NullElement(); 41 | } 42 | 43 | if (Array.isArray(value)) { 44 | return new ArrayElement(value.map(refract)); 45 | } 46 | 47 | if (typeof value === 'object') { 48 | const element = new ObjectElement(value); 49 | return element; 50 | } 51 | 52 | return value; 53 | } 54 | 55 | Element.prototype.ObjectElement = ObjectElement; 56 | Element.prototype.RefElement = RefElement; 57 | Element.prototype.MemberElement = MemberElement; 58 | 59 | Element.prototype.refract = refract; 60 | ArraySlice.prototype.refract = refract; 61 | 62 | /** 63 | * Contains all of the element classes, and related structures and methods 64 | * for handling with element instances. 65 | */ 66 | module.exports = { 67 | Element, 68 | NullElement, 69 | StringElement, 70 | NumberElement, 71 | BooleanElement, 72 | ArrayElement, 73 | MemberElement, 74 | ObjectElement, 75 | LinkElement, 76 | RefElement, 77 | 78 | refract, 79 | 80 | ArraySlice, 81 | ObjectSlice, 82 | KeyValuePair, 83 | }; 84 | -------------------------------------------------------------------------------- /lib/elements/LinkElement.js: -------------------------------------------------------------------------------- 1 | const Element = require('../primitives/Element'); 2 | 3 | /** Hyperlinking MAY be used to link to other resources, provide links to 4 | * instructions on how to process a given element (by way of a profile or 5 | * other means), and may be used to provide meta data about the element in 6 | * which it's found. The meaning and purpose of the hyperlink is defined by 7 | * the link relation according to RFC 5988. 8 | * 9 | * @class LinkElement 10 | * 11 | * @param content 12 | * @param meta 13 | * @param attributes 14 | */ 15 | module.exports = class LinkElement extends Element { 16 | constructor(content, meta, attributes) { 17 | super(content || [], meta, attributes); 18 | this.element = 'link'; 19 | } 20 | 21 | /** 22 | * The relation identifier for the link, as defined in RFC 5988. 23 | * @type StringElement 24 | */ 25 | get relation() { 26 | return this.attributes.get('relation'); 27 | } 28 | 29 | set relation(relation) { 30 | this.attributes.set('relation', relation); 31 | } 32 | 33 | /** 34 | * The URI for the given link. 35 | * @type StringElement 36 | */ 37 | get href() { 38 | return this.attributes.get('href'); 39 | } 40 | 41 | set href(href) { 42 | this.attributes.set('href', href); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /lib/elements/RefElement.js: -------------------------------------------------------------------------------- 1 | const Element = require('../primitives/Element'); 2 | 3 | /** 4 | * @class RefElement 5 | * 6 | * @param content 7 | * @param meta 8 | * @param attributes 9 | * 10 | * @extends Element 11 | */ 12 | module.exports = class RefElement extends Element { 13 | constructor(content, meta, attributes) { 14 | super(content || [], meta, attributes); 15 | this.element = 'ref'; 16 | 17 | if (!this.path) { 18 | this.path = 'element'; 19 | } 20 | } 21 | 22 | /** 23 | * Path of referenced element to transclude instead of element itself. 24 | * @type StringElement 25 | * @default element 26 | */ 27 | get path() { 28 | return this.attributes.get('path'); 29 | } 30 | 31 | set path(newValue) { 32 | this.attributes.set('path', newValue); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /lib/minim.js: -------------------------------------------------------------------------------- 1 | const Namespace = require('./Namespace'); 2 | const elements = require('./elements'); 3 | 4 | // Direct access to the Namespace class 5 | exports.Namespace = Namespace; 6 | 7 | // Special constructor for the Namespace class 8 | exports.namespace = function namespace(options) { 9 | return new Namespace(options); 10 | }; 11 | 12 | exports.KeyValuePair = require('./KeyValuePair'); 13 | 14 | exports.ArraySlice = elements.ArraySlice; 15 | exports.ObjectSlice = elements.ObjectSlice; 16 | 17 | exports.Element = elements.Element; 18 | exports.StringElement = elements.StringElement; 19 | exports.NumberElement = elements.NumberElement; 20 | exports.BooleanElement = elements.BooleanElement; 21 | exports.NullElement = elements.NullElement; 22 | exports.ArrayElement = elements.ArrayElement; 23 | exports.ObjectElement = elements.ObjectElement; 24 | exports.MemberElement = elements.MemberElement; 25 | exports.RefElement = elements.RefElement; 26 | exports.LinkElement = elements.LinkElement; 27 | 28 | exports.refract = elements.refract; 29 | 30 | exports.JSONSerialiser = require('./serialisers/JSONSerialiser'); 31 | exports.JSON06Serialiser = require('./serialisers/JSON06Serialiser'); 32 | -------------------------------------------------------------------------------- /lib/primitives/ArrayElement.js: -------------------------------------------------------------------------------- 1 | const negate = require('lodash/negate'); 2 | const Element = require('./Element'); 3 | const ArraySlice = require('../ArraySlice'); 4 | 5 | /** 6 | * @class 7 | * 8 | * @param {Element[]} content 9 | * @param meta 10 | * @param attributes 11 | */ 12 | class ArrayElement extends Element { 13 | constructor(content, meta, attributes) { 14 | super(content || [], meta, attributes); 15 | this.element = 'array'; 16 | } 17 | 18 | primitive() { 19 | return 'array'; 20 | } 21 | 22 | /** 23 | * @returns {Element} 24 | */ 25 | get(index) { 26 | return this.content[index]; 27 | } 28 | 29 | /** 30 | * Helper for returning the value of an item 31 | * This works for both ArrayElement and ObjectElement instances 32 | */ 33 | getValue(indexOrKey) { 34 | const item = this.get(indexOrKey); 35 | 36 | if (item) { 37 | return item.toValue(); 38 | } 39 | 40 | return undefined; 41 | } 42 | 43 | /** 44 | * @returns {Element} 45 | */ 46 | getIndex(index) { 47 | return this.content[index]; 48 | } 49 | 50 | set(index, value) { 51 | this.content[index] = this.refract(value); 52 | return this; 53 | } 54 | 55 | remove(index) { 56 | const removed = this.content.splice(index, 1); 57 | 58 | if (removed.length) { 59 | return removed[0]; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | /** 66 | * @param callback - Function to execute for each element 67 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 68 | */ 69 | map(callback, thisArg) { 70 | return this.content.map(callback, thisArg); 71 | } 72 | 73 | /** 74 | * Maps and then flattens the results. 75 | * @param callback - Function to execute for each element. 76 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 77 | * @returns {array} 78 | */ 79 | flatMap(callback, thisArg) { 80 | return this 81 | .map(callback, thisArg) 82 | .reduce((a, b) => a.concat(b), []); 83 | } 84 | 85 | /** 86 | * Returns an array containing the truthy results of calling the given transformation with each element of this sequence 87 | * @param transform - A closure that accepts an element of this array as its argument and returns an optional value. 88 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 89 | * @memberof ArrayElement.prototype 90 | * @returns An array of the non-undefined results of calling transform with each element of the array 91 | */ 92 | compactMap(transform, thisArg) { 93 | const results = []; 94 | 95 | this.forEach((element) => { 96 | const result = transform.bind(thisArg)(element); 97 | 98 | if (result) { 99 | results.push(result); 100 | } 101 | }); 102 | 103 | return results; 104 | } 105 | 106 | /** 107 | * @param callback - Function to execute for each element 108 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 109 | * @returns {ArraySlice} 110 | */ 111 | filter(callback, thisArg) { 112 | return new ArraySlice(this.content.filter(callback, thisArg)); 113 | } 114 | 115 | /** 116 | * @param callback - Function to execute for each element 117 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 118 | * @returns {ArraySlice} 119 | */ 120 | reject(callback, thisArg) { 121 | return this.filter(negate(callback), thisArg); 122 | } 123 | 124 | /** 125 | * This is a reduce function specifically for Minim arrays and objects. It 126 | * allows for returning normal values or Minim instances, so it converts any 127 | * primitives on each step. 128 | */ 129 | reduce(callback, initialValue) { 130 | let startIndex; 131 | let memo; 132 | 133 | // Allows for defining a starting value of the reduce 134 | if (initialValue !== undefined) { 135 | startIndex = 0; 136 | memo = this.refract(initialValue); 137 | } else { 138 | startIndex = 1; 139 | // Object Element content items are member elements. Because of this, 140 | // the memo should start out as the member value rather than the 141 | // actual member itself. 142 | memo = this.primitive() === 'object' ? this.first.value : this.first; 143 | } 144 | 145 | // Sending each function call to the registry allows for passing Minim 146 | // instances through the function return. This means you can return 147 | // primitive values or return Minim instances and reduce will still work. 148 | for (let i = startIndex; i < this.length; i += 1) { 149 | const item = this.content[i]; 150 | 151 | if (this.primitive() === 'object') { 152 | memo = this.refract(callback(memo, item.value, item.key, item, this)); 153 | } else { 154 | memo = this.refract(callback(memo, item, i, this)); 155 | } 156 | } 157 | 158 | return memo; 159 | } 160 | 161 | /** 162 | * @callback forEachCallback 163 | * @param {Element} currentValue 164 | * @param {NumberElement} index 165 | */ 166 | 167 | /** 168 | * @param {forEachCallback} callback - Function to execute for each element 169 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 170 | * @memberof ArrayElement.prototype 171 | */ 172 | forEach(callback, thisArg) { 173 | this.content.forEach((item, index) => { 174 | callback.bind(thisArg)(item, this.refract(index)); 175 | }); 176 | } 177 | 178 | /** 179 | * @returns {Element} 180 | */ 181 | shift() { 182 | return this.content.shift(); 183 | } 184 | 185 | /** 186 | * @param value 187 | */ 188 | unshift(value) { 189 | this.content.unshift(this.refract(value)); 190 | } 191 | 192 | /** 193 | * @param value 194 | */ 195 | push(value) { 196 | this.content.push(this.refract(value)); 197 | return this; 198 | } 199 | 200 | /** 201 | * @param value 202 | */ 203 | add(value) { 204 | this.push(value); 205 | } 206 | 207 | /** 208 | * Recusively search all descendents using a condition function. 209 | * @returns {Element[]} 210 | */ 211 | findElements(condition, givenOptions) { 212 | const options = givenOptions || {}; 213 | const recursive = !!options.recursive; 214 | const results = options.results === undefined ? [] : options.results; 215 | 216 | // The forEach method for Object Elements returns value, key, and member. 217 | // This passes those along to the condition function below. 218 | this.forEach((item, keyOrIndex, member) => { 219 | // We use duck-typing here to support any registered class that 220 | // may contain other elements. 221 | if (recursive && (item.findElements !== undefined)) { 222 | item.findElements(condition, { 223 | results, 224 | recursive, 225 | }); 226 | } 227 | 228 | if (condition(item, keyOrIndex, member)) { 229 | results.push(item); 230 | } 231 | }); 232 | 233 | return results; 234 | } 235 | 236 | /** 237 | * Recusively search all descendents using a condition function. 238 | * @param condition 239 | * @returns {ArraySlice} 240 | */ 241 | find(condition) { 242 | return new ArraySlice(this.findElements(condition, { recursive: true })); 243 | } 244 | 245 | /** 246 | * @param {string} element 247 | * @returns {ArraySlice} 248 | */ 249 | findByElement(element) { 250 | return this.find(item => item.element === element); 251 | } 252 | 253 | /** 254 | * @param {string} className 255 | * @returns {ArraySlice} 256 | * @memberof ArrayElement.prototype 257 | */ 258 | findByClass(className) { 259 | return this.find(item => item.classes.includes(className)); 260 | } 261 | 262 | /** 263 | * Search the tree recursively and find the element with the matching ID 264 | * @param {string} id 265 | * @returns {Element} 266 | * @memberof ArrayElement.prototype 267 | */ 268 | getById(id) { 269 | return this.find(item => item.id.toValue() === id).first; 270 | } 271 | 272 | /** 273 | * Looks for matching children using deep equality 274 | * @param value 275 | * @returns {boolean} 276 | */ 277 | includes(value) { 278 | return this.content.some(element => element.equals(value)); 279 | } 280 | 281 | /** 282 | * Looks for matching children using deep equality 283 | * @param value 284 | * @returns {boolean} 285 | * @see includes 286 | * @deprecated method was replaced by includes 287 | */ 288 | contains(value) { 289 | return this.includes(value); 290 | } 291 | 292 | // Fantasy Land 293 | 294 | /** 295 | * @returns {ArrayElement} An empty array element 296 | */ 297 | empty() { 298 | return new this.constructor([]); 299 | } 300 | 301 | ['fantasy-land/empty']() { 302 | return this.empty(); 303 | } 304 | 305 | /** 306 | * @param {ArrayElement} other 307 | * @returns {ArrayElement} 308 | */ 309 | concat(other) { 310 | return new this.constructor(this.content.concat(other.content)); 311 | } 312 | 313 | ['fantasy-land/concat'](other) { 314 | return this.concat(other); 315 | } 316 | 317 | ['fantasy-land/map'](transform) { 318 | return new this.constructor(this.map(transform)); 319 | } 320 | 321 | ['fantasy-land/chain'](transform) { 322 | return this 323 | .map(element => transform(element), this) 324 | .reduce((a, b) => a.concat(b), this.empty()); 325 | } 326 | 327 | ['fantasy-land/filter'](callback) { 328 | return new this.constructor(this.content.filter(callback)); 329 | } 330 | 331 | ['fantasy-land/reduce'](transform, initialValue) { 332 | return this.content.reduce(transform, initialValue); 333 | } 334 | 335 | /** 336 | * Returns the length of the collection 337 | * @type number 338 | */ 339 | get length() { 340 | return this.content.length; 341 | } 342 | 343 | /** 344 | * Returns whether the collection is empty 345 | * @type boolean 346 | */ 347 | get isEmpty() { 348 | return this.content.length === 0; 349 | } 350 | 351 | /** 352 | * Return the first item in the collection 353 | * @type Element 354 | */ 355 | get first() { 356 | return this.getIndex(0); 357 | } 358 | 359 | /** 360 | * Return the second item in the collection 361 | * @type Element 362 | */ 363 | get second() { 364 | return this.getIndex(1); 365 | } 366 | 367 | /** 368 | * Return the last item in the collection 369 | * @type Element 370 | */ 371 | get last() { 372 | return this.getIndex(this.length - 1); 373 | } 374 | } 375 | 376 | /** 377 | * @returns {ArrayElement} An empty array element 378 | */ 379 | ArrayElement.empty = function empty() { 380 | return new this(); 381 | }; 382 | 383 | ArrayElement['fantasy-land/empty'] = ArrayElement.empty; 384 | 385 | if (typeof Symbol !== 'undefined') { 386 | ArrayElement.prototype[Symbol.iterator] = function symbol() { 387 | return this.content[Symbol.iterator](); 388 | }; 389 | } 390 | 391 | module.exports = ArrayElement; 392 | -------------------------------------------------------------------------------- /lib/primitives/BooleanElement.js: -------------------------------------------------------------------------------- 1 | const Element = require('./Element'); 2 | 3 | /** 4 | * @class BooleanElement 5 | * 6 | * @param {boolean} content 7 | * @param meta 8 | * @param attributes 9 | */ 10 | module.exports = class BooleanElement extends Element { 11 | constructor(content, meta, attributes) { 12 | super(content, meta, attributes); 13 | this.element = 'boolean'; 14 | } 15 | 16 | primitive() { 17 | return 'boolean'; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/primitives/Element.js: -------------------------------------------------------------------------------- 1 | const isEqual = require('lodash/isEqual'); 2 | const KeyValuePair = require('../KeyValuePair'); 3 | const ArraySlice = require('../ArraySlice.js'); 4 | 5 | /** 6 | * @class 7 | * 8 | * @param content 9 | * @param meta 10 | * @param attributes 11 | * 12 | * @property {string} element 13 | */ 14 | class Element { 15 | constructor(content, meta, attributes) { 16 | // Lazy load this.meta and this.attributes because it's a Minim element 17 | // Otherwise, we get into circuluar calls 18 | if (meta) { 19 | this.meta = meta; 20 | } 21 | 22 | if (attributes) { 23 | this.attributes = attributes; 24 | } 25 | 26 | this.content = content; 27 | } 28 | 29 | /** 30 | * Freezes the element to prevent any mutation. 31 | * A frozen element will add `parent` property to every child element 32 | * to allow traversing up the element tree. 33 | */ 34 | freeze() { 35 | if (Object.isFrozen(this)) { 36 | return; 37 | } 38 | 39 | if (this._meta) { 40 | this.meta.parent = this; 41 | this.meta.freeze(); 42 | } 43 | 44 | if (this._attributes) { 45 | this.attributes.parent = this; 46 | this.attributes.freeze(); 47 | } 48 | 49 | this.children.forEach((element) => { 50 | element.parent = this; 51 | element.freeze(); 52 | }, this); 53 | 54 | if (this.content && Array.isArray(this.content)) { 55 | Object.freeze(this.content); 56 | } 57 | 58 | Object.freeze(this); 59 | } 60 | 61 | primitive() { 62 | 63 | } 64 | 65 | /** 66 | * Creates a deep clone of the instance 67 | */ 68 | clone() { 69 | const copy = new this.constructor(); 70 | 71 | copy.element = this.element; 72 | 73 | if (this.meta.length) { 74 | copy._meta = this.meta.clone(); 75 | } 76 | 77 | if (this.attributes.length) { 78 | copy._attributes = this.attributes.clone(); 79 | } 80 | 81 | if (this.content) { 82 | if (this.content.clone) { 83 | copy.content = this.content.clone(); 84 | } else if (Array.isArray(this.content)) { 85 | copy.content = this.content.map(element => element.clone()); 86 | } else { 87 | copy.content = this.content; 88 | } 89 | } else { 90 | copy.content = this.content; 91 | } 92 | 93 | return copy; 94 | } 95 | 96 | /** 97 | */ 98 | toValue() { 99 | if (this.content instanceof Element) { 100 | return this.content.toValue(); 101 | } 102 | 103 | if (this.content instanceof KeyValuePair) { 104 | return { 105 | key: this.content.key.toValue(), 106 | value: this.content.value ? this.content.value.toValue() : undefined, 107 | }; 108 | } 109 | 110 | if (this.content && this.content.map) { 111 | return this.content.map(element => element.toValue(), this); 112 | } 113 | 114 | return this.content; 115 | } 116 | 117 | /** 118 | * Creates a reference pointing at the Element 119 | * @returns {RefElement} 120 | * @memberof Element.prototype 121 | */ 122 | toRef(path) { 123 | if (this.id.toValue() === '') { 124 | throw Error('Cannot create reference to an element that does not contain an ID'); 125 | } 126 | 127 | const ref = new this.RefElement(this.id.toValue()); 128 | 129 | if (path) { 130 | ref.path = path; 131 | } 132 | 133 | return ref; 134 | } 135 | 136 | /** 137 | * Finds the given elements in the element tree. 138 | * When providing multiple element names, you must first freeze the element. 139 | * 140 | * @param names {...elementNames} 141 | * @returns {ArraySlice} 142 | */ 143 | findRecursive(...elementNames) { 144 | if (arguments.length > 1 && !this.isFrozen) { 145 | throw new Error('Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`'); 146 | } 147 | 148 | const elementName = elementNames.pop(); 149 | let elements = new ArraySlice(); 150 | 151 | const append = (array, element) => { 152 | array.push(element); 153 | return array; 154 | }; 155 | 156 | // Checks the given element and appends element/sub-elements 157 | // that match element name to given array 158 | const checkElement = (array, element) => { 159 | if (element.element === elementName) { 160 | array.push(element); 161 | } 162 | 163 | const items = element.findRecursive(elementName); 164 | if (items) { 165 | items.reduce(append, array); 166 | } 167 | 168 | if (element.content instanceof KeyValuePair) { 169 | if (element.content.key) { 170 | checkElement(array, element.content.key); 171 | } 172 | 173 | if (element.content.value) { 174 | checkElement(array, element.content.value); 175 | } 176 | } 177 | 178 | return array; 179 | }; 180 | 181 | if (this.content) { 182 | // Direct Element 183 | if (this.content.element) { 184 | checkElement(elements, this.content); 185 | } 186 | 187 | // Element Array 188 | if (Array.isArray(this.content)) { 189 | this.content.reduce(checkElement, elements); 190 | } 191 | } 192 | 193 | if (!elementNames.isEmpty) { 194 | elements = elements.filter((element) => { 195 | let parentElements = element.parents.map(e => e.element); 196 | 197 | // eslint-disable-next-line no-restricted-syntax 198 | for (const namesIndex in elementNames) { 199 | const name = elementNames[namesIndex]; 200 | const index = parentElements.indexOf(name); 201 | 202 | if (index !== -1) { 203 | parentElements = parentElements.splice(0, index); 204 | } else { 205 | return false; 206 | } 207 | } 208 | 209 | return true; 210 | }); 211 | } 212 | 213 | return elements; 214 | } 215 | 216 | set(content) { 217 | this.content = content; 218 | return this; 219 | } 220 | 221 | equals(value) { 222 | return isEqual(this.toValue(), value); 223 | } 224 | 225 | getMetaProperty(name, value) { 226 | if (!this.meta.hasKey(name)) { 227 | if (this.isFrozen) { 228 | const element = this.refract(value); 229 | element.freeze(); 230 | return element; 231 | } 232 | 233 | this.meta.set(name, value); 234 | } 235 | 236 | return this.meta.get(name); 237 | } 238 | 239 | setMetaProperty(name, value) { 240 | this.meta.set(name, value); 241 | } 242 | 243 | /** 244 | * @type String 245 | */ 246 | get element() { 247 | // Returns 'element' so we don't have undefined as element 248 | return this._storedElement || 'element'; 249 | } 250 | 251 | set element(element) { 252 | this._storedElement = element; 253 | } 254 | 255 | get content() { 256 | return this._content; 257 | } 258 | 259 | set content(value) { 260 | if (value instanceof Element) { 261 | this._content = value; 262 | } else if (value instanceof ArraySlice) { 263 | this.content = value.elements; 264 | } else if ( 265 | typeof value == 'string' 266 | || typeof value == 'number' 267 | || typeof value == 'boolean' 268 | || value === 'null' 269 | || value == undefined 270 | ) { 271 | // Primitive Values 272 | this._content = value; 273 | } else if (value instanceof KeyValuePair) { 274 | this._content = value; 275 | } else if (Array.isArray(value)) { 276 | this._content = value.map(this.refract); 277 | } else if (typeof value === 'object') { 278 | this._content = Object.keys(value).map(key => new this.MemberElement(key, value[key])); 279 | } else { 280 | throw new Error('Cannot set content to given value'); 281 | } 282 | } 283 | 284 | /** 285 | * @type ObjectElement 286 | */ 287 | get meta() { 288 | if (!this._meta) { 289 | if (this.isFrozen) { 290 | const meta = new this.ObjectElement(); 291 | meta.freeze(); 292 | return meta; 293 | } 294 | 295 | this._meta = new this.ObjectElement(); 296 | } 297 | 298 | return this._meta; 299 | } 300 | 301 | set meta(value) { 302 | if (value instanceof this.ObjectElement) { 303 | this._meta = value; 304 | } else { 305 | this.meta.set(value || {}); 306 | } 307 | } 308 | 309 | /** 310 | * The attributes property defines attributes about the given instance 311 | * of the element, as specified by the element property. 312 | * 313 | * @type ObjectElement 314 | */ 315 | get attributes() { 316 | if (!this._attributes) { 317 | if (this.isFrozen) { 318 | const meta = new this.ObjectElement(); 319 | meta.freeze(); 320 | return meta; 321 | } 322 | 323 | this._attributes = new this.ObjectElement(); 324 | } 325 | 326 | return this._attributes; 327 | } 328 | 329 | set attributes(value) { 330 | if (value instanceof this.ObjectElement) { 331 | this._attributes = value; 332 | } else { 333 | this.attributes.set(value || {}); 334 | } 335 | } 336 | 337 | /** 338 | * Unique Identifier, MUST be unique throughout an entire element tree. 339 | * @type StringElement 340 | */ 341 | get id() { 342 | return this.getMetaProperty('id', ''); 343 | } 344 | 345 | set id(element) { 346 | this.setMetaProperty('id', element); 347 | } 348 | 349 | /** 350 | * @type ArrayElement 351 | */ 352 | get classes() { 353 | return this.getMetaProperty('classes', []); 354 | } 355 | 356 | set classes(element) { 357 | this.setMetaProperty('classes', element); 358 | } 359 | 360 | /** 361 | * Human-readable title of element 362 | * @type StringElement 363 | */ 364 | get title() { 365 | return this.getMetaProperty('title', ''); 366 | } 367 | 368 | set title(element) { 369 | this.setMetaProperty('title', element); 370 | } 371 | 372 | /** 373 | * Human-readable description of element 374 | * @type StringElement 375 | */ 376 | get description() { 377 | return this.getMetaProperty('description', ''); 378 | } 379 | 380 | set description(element) { 381 | this.setMetaProperty('description', element); 382 | } 383 | 384 | /** 385 | * @type ArrayElement 386 | */ 387 | get links() { 388 | return this.getMetaProperty('links', []); 389 | } 390 | 391 | set links(element) { 392 | this.setMetaProperty('links', element); 393 | } 394 | 395 | /** 396 | * Returns whether the element is frozen. 397 | * @type boolean 398 | * @see freeze 399 | */ 400 | get isFrozen() { 401 | return Object.isFrozen(this); 402 | } 403 | 404 | /** 405 | * Returns all of the parent elements. 406 | * @type ArraySlice 407 | */ 408 | get parents() { 409 | let { parent } = this; 410 | const parents = new ArraySlice(); 411 | 412 | while (parent) { 413 | parents.push(parent); 414 | 415 | // eslint-disable-next-line prefer-destructuring 416 | parent = parent.parent; 417 | } 418 | 419 | return parents; 420 | } 421 | 422 | /** 423 | * Returns all of the children elements found within the element. 424 | * @type ArraySlice 425 | * @see recursiveChildren 426 | */ 427 | get children() { 428 | if (Array.isArray(this.content)) { 429 | return new ArraySlice(this.content); 430 | } 431 | 432 | if (this.content instanceof KeyValuePair) { 433 | const children = new ArraySlice([this.content.key]); 434 | 435 | if (this.content.value) { 436 | children.push(this.content.value); 437 | } 438 | 439 | return children; 440 | } 441 | 442 | if (this.content instanceof Element) { 443 | return new ArraySlice([this.content]); 444 | } 445 | 446 | return new ArraySlice(); 447 | } 448 | 449 | /** 450 | * Returns all of the children elements found within the element recursively. 451 | * @type ArraySlice 452 | * @see children 453 | */ 454 | get recursiveChildren() { 455 | const children = new ArraySlice(); 456 | 457 | this.children.forEach((element) => { 458 | children.push(element); 459 | 460 | element.recursiveChildren.forEach((child) => { 461 | children.push(child); 462 | }); 463 | }); 464 | 465 | return children; 466 | } 467 | } 468 | 469 | module.exports = Element; 470 | -------------------------------------------------------------------------------- /lib/primitives/MemberElement.js: -------------------------------------------------------------------------------- 1 | const KeyValuePair = require('../KeyValuePair'); 2 | const Element = require('./Element'); 3 | 4 | /** 5 | * @class MemberElement 6 | * 7 | * @param {Element} key 8 | * @param {Element} value 9 | * @param meta 10 | * @param attributes 11 | */ 12 | module.exports = class MemberElement extends Element { 13 | constructor(key, value, meta, attributes) { 14 | super(new KeyValuePair(), meta, attributes); 15 | 16 | this.element = 'member'; 17 | this.key = key; 18 | this.value = value; 19 | } 20 | 21 | /** 22 | * @type Element 23 | */ 24 | get key() { 25 | return this.content.key; 26 | } 27 | 28 | set key(key) { 29 | this.content.key = this.refract(key); 30 | } 31 | 32 | /** 33 | * @type Element 34 | */ 35 | get value() { 36 | return this.content.value; 37 | } 38 | 39 | set value(value) { 40 | this.content.value = this.refract(value); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/primitives/NullElement.js: -------------------------------------------------------------------------------- 1 | const Element = require('./Element'); 2 | 3 | /** 4 | */ 5 | class NullElement extends Element { 6 | constructor(content, meta, attributes) { 7 | super(content || null, meta, attributes); 8 | this.element = 'null'; 9 | } 10 | 11 | primitive() { 12 | return 'null'; 13 | } 14 | 15 | set() { 16 | return new Error('Cannot set the value of null'); 17 | } 18 | } 19 | 20 | module.exports = NullElement; 21 | -------------------------------------------------------------------------------- /lib/primitives/NumberElement.js: -------------------------------------------------------------------------------- 1 | const Element = require('./Element'); 2 | 3 | /** 4 | * @class NumberElement 5 | * 6 | * @param {number} content 7 | * @param meta 8 | * @param attributes 9 | */ 10 | module.exports = class NumberElement extends Element { 11 | constructor(content, meta, attributes) { 12 | super(content, meta, attributes); 13 | this.element = 'number'; 14 | } 15 | 16 | primitive() { 17 | return 'number'; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/primitives/ObjectElement.js: -------------------------------------------------------------------------------- 1 | const negate = require('lodash/negate'); 2 | const isObject = require('lodash/isObject'); 3 | 4 | const ArrayElement = require('./ArrayElement'); 5 | const MemberElement = require('./MemberElement'); 6 | const ObjectSlice = require('../ObjectSlice'); 7 | 8 | /** 9 | * @class 10 | * 11 | * @param content 12 | * @param meta 13 | * @param attributes 14 | */ 15 | class ObjectElement extends ArrayElement { 16 | constructor(content, meta, attributes) { 17 | super(content || [], meta, attributes); 18 | this.element = 'object'; 19 | } 20 | 21 | primitive() { 22 | return 'object'; 23 | } 24 | 25 | toValue() { 26 | return this.content.reduce((results, el) => { 27 | results[el.key.toValue()] = el.value ? el.value.toValue() : undefined; 28 | return results; 29 | }, {}); 30 | } 31 | 32 | /** 33 | * @param key 34 | * @returns {Element} 35 | */ 36 | get(name) { 37 | const member = this.getMember(name); 38 | 39 | if (member) { 40 | return member.value; 41 | } 42 | 43 | return undefined; 44 | } 45 | 46 | /** 47 | * @param key 48 | * @returns {MemberElement} 49 | */ 50 | getMember(name) { 51 | if (name === undefined) { return undefined; } 52 | 53 | return this.content.find(element => element.key.toValue() === name); 54 | } 55 | 56 | /** 57 | * @param key 58 | */ 59 | remove(name) { 60 | let removed = null; 61 | 62 | this.content = this.content.filter((item) => { 63 | if (item.key.toValue() === name) { 64 | removed = item; 65 | return false; 66 | } 67 | 68 | return true; 69 | }); 70 | 71 | return removed; 72 | } 73 | 74 | /** 75 | * @param key 76 | * @returns {Element} 77 | */ 78 | getKey(name) { 79 | const member = this.getMember(name); 80 | 81 | if (member) { 82 | return member.key; 83 | } 84 | 85 | return undefined; 86 | } 87 | 88 | /** 89 | * Set allows either a key/value pair to be given or an object 90 | * If an object is given, each key is set to its respective value 91 | */ 92 | set(keyOrObject, value) { 93 | if (isObject(keyOrObject)) { 94 | Object.keys(keyOrObject).forEach((objectKey) => { 95 | this.set(objectKey, keyOrObject[objectKey]); 96 | }); 97 | 98 | return this; 99 | } 100 | 101 | // Store as key for clarity 102 | const key = keyOrObject; 103 | const member = this.getMember(key); 104 | 105 | if (member) { 106 | member.value = value; 107 | } else { 108 | this.content.push(new MemberElement(key, value)); 109 | } 110 | 111 | return this; 112 | } 113 | 114 | /** 115 | */ 116 | keys() { 117 | return this.content.map(item => item.key.toValue()); 118 | } 119 | 120 | /** 121 | */ 122 | values() { 123 | return this.content.map(item => item.value.toValue()); 124 | } 125 | 126 | /** 127 | * @returns {boolean} 128 | */ 129 | hasKey(value) { 130 | return this.content.some(member => member.key.equals(value)); 131 | } 132 | 133 | /** 134 | * @returns {array} 135 | */ 136 | items() { 137 | return this.content.map(item => [item.key.toValue(), item.value.toValue()]); 138 | } 139 | 140 | /** 141 | * @param callback 142 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 143 | */ 144 | map(callback, thisArg) { 145 | return this.content.map(item => callback.bind(thisArg)(item.value, item.key, item)); 146 | } 147 | 148 | /** 149 | * Returns an array containing the truthy results of calling the given transformation with each element of this sequence 150 | * @param transform - A closure that accepts the value, key and member element of this object as its argument and returns an optional value. 151 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 152 | * @returns An array of the non-undefined results of calling transform with each element of the array 153 | */ 154 | compactMap(callback, thisArg) { 155 | const results = []; 156 | 157 | this.forEach((value, key, member) => { 158 | const result = callback.bind(thisArg)(value, key, member); 159 | 160 | if (result) { 161 | results.push(result); 162 | } 163 | }); 164 | 165 | return results; 166 | } 167 | 168 | /** 169 | * @param callback 170 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 171 | * 172 | * @returns {ObjectSlice} 173 | */ 174 | filter(callback, thisArg) { 175 | return new ObjectSlice(this.content).filter(callback, thisArg); 176 | } 177 | 178 | /** 179 | * @param callback 180 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 181 | * 182 | * @returns {ObjectSlice} 183 | * 184 | * @memberof ObjectElement.prototype 185 | */ 186 | reject(callback, thisArg) { 187 | return this.filter(negate(callback), thisArg); 188 | } 189 | 190 | /** 191 | * @param callback 192 | * @param thisArg - Value to use as this (i.e the reference Object) when executing callback 193 | * 194 | * @memberof ObjectElement.prototype 195 | */ 196 | forEach(callback, thisArg) { 197 | return this.content.forEach(item => callback.bind(thisArg)(item.value, item.key, item)); 198 | } 199 | } 200 | 201 | module.exports = ObjectElement; 202 | -------------------------------------------------------------------------------- /lib/primitives/StringElement.js: -------------------------------------------------------------------------------- 1 | const Element = require('./Element'); 2 | 3 | /** 4 | * @class StringElement 5 | * 6 | * @param {string} content 7 | * @param meta 8 | * @param attributes 9 | */ 10 | module.exports = class StringElement extends Element { 11 | constructor(content, meta, attributes) { 12 | super(content, meta, attributes); 13 | this.element = 'string'; 14 | } 15 | 16 | primitive() { 17 | return 'string'; 18 | } 19 | 20 | /** 21 | * The length of the string. 22 | * @type number 23 | */ 24 | get length() { 25 | return this.content.length; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /lib/serialisers/JSON06Serialiser.js: -------------------------------------------------------------------------------- 1 | const JSONSerialiser = require('./JSONSerialiser'); 2 | 3 | module.exports = class JSON06Serialiser extends JSONSerialiser { 4 | serialise(element) { 5 | if (!(element instanceof this.namespace.elements.Element)) { 6 | throw new TypeError(`Given element \`${element}\` is not an Element instance`); 7 | } 8 | 9 | let variable; 10 | if (element._attributes && element.attributes.get('variable')) { 11 | variable = element.attributes.get('variable'); 12 | } 13 | 14 | const payload = { 15 | element: element.element, 16 | }; 17 | 18 | if (element._meta && element._meta.length > 0) { 19 | payload.meta = this.serialiseObject(element.meta); 20 | } 21 | 22 | const isEnum = (element.element === 'enum' || element.attributes.keys().indexOf('enumerations') !== -1); 23 | 24 | if (isEnum) { 25 | const attributes = this.enumSerialiseAttributes(element); 26 | 27 | if (attributes) { 28 | payload.attributes = attributes; 29 | } 30 | } else if (element._attributes && element._attributes.length > 0) { 31 | let { attributes } = element; 32 | 33 | // Meta attribute was renamed to metadata 34 | if (attributes.get('metadata')) { 35 | attributes = attributes.clone(); 36 | attributes.set('meta', attributes.get('metadata')); 37 | attributes.remove('metadata'); 38 | } 39 | 40 | if (element.element === 'member' && variable) { 41 | attributes = attributes.clone(); 42 | attributes.remove('variable'); 43 | } 44 | 45 | if (attributes.length > 0) { 46 | payload.attributes = this.serialiseObject(attributes); 47 | } 48 | } 49 | 50 | if (isEnum) { 51 | payload.content = this.enumSerialiseContent(element, payload); 52 | } else if (this[`${element.element}SerialiseContent`]) { 53 | payload.content = this[`${element.element}SerialiseContent`](element, payload); 54 | } else if (element.content !== undefined) { 55 | let content; 56 | 57 | if (variable && element.content.key) { 58 | content = element.content.clone(); 59 | content.key.attributes.set('variable', variable); 60 | content = this.serialiseContent(content); 61 | } else { 62 | content = this.serialiseContent(element.content); 63 | } 64 | 65 | if (this.shouldSerialiseContent(element, content)) { 66 | payload.content = content; 67 | } 68 | } else if (this.shouldSerialiseContent(element, element.content) && element instanceof this.namespace.elements.Array) { 69 | payload.content = []; 70 | } 71 | 72 | return payload; 73 | } 74 | 75 | shouldSerialiseContent(element, content) { 76 | if (element.element === 'parseResult' || element.element === 'httpRequest' 77 | || element.element === 'httpResponse' || element.element === 'category' 78 | || element.element === 'link') { 79 | return true; 80 | } 81 | 82 | if (content === undefined) { 83 | return false; 84 | } 85 | 86 | if (Array.isArray(content) && content.length === 0) { 87 | return false; 88 | } 89 | 90 | return true; 91 | } 92 | 93 | refSerialiseContent(element, payload) { 94 | delete payload.attributes; 95 | 96 | return { 97 | href: element.toValue(), 98 | path: element.path.toValue(), 99 | }; 100 | } 101 | 102 | sourceMapSerialiseContent(element) { 103 | return element.toValue(); 104 | } 105 | 106 | dataStructureSerialiseContent(element) { 107 | return [this.serialiseContent(element.content)]; 108 | } 109 | 110 | enumSerialiseAttributes(element) { 111 | const attributes = element.attributes.clone(); 112 | 113 | // Enumerations attribute was is placed inside content (see `enumSerialiseContent` below) 114 | const enumerations = attributes.remove('enumerations') || new this.namespace.elements.Array([]); 115 | 116 | // Remove fixed type attribute from samples and default 117 | const defaultValue = attributes.get('default'); 118 | let samples = attributes.get('samples') || new this.namespace.elements.Array([]); 119 | 120 | if (defaultValue && defaultValue.content) { 121 | if (defaultValue.content.attributes) { 122 | defaultValue.content.attributes.remove('typeAttributes'); 123 | } 124 | // Wrap default in array (not sure it is really needed because tests pass without this line) 125 | attributes.set('default', new this.namespace.elements.Array([defaultValue.content])); 126 | } 127 | 128 | // Strip typeAttributes from samples, 0.6 doesn't usually contain them in samples 129 | samples.forEach((sample) => { 130 | if (sample.content && sample.content.element) { 131 | sample.content.attributes.remove('typeAttributes'); 132 | } 133 | }); 134 | 135 | // Content -> Samples 136 | if (element.content && enumerations.length !== 0) { 137 | // If we don't have enumerations, content should stay in 138 | // content (enumerations) as per Drafter 3 behaviour. 139 | samples.unshift(element.content); 140 | } 141 | 142 | samples = samples.map((sample) => { 143 | if (sample instanceof this.namespace.elements.Array) { 144 | return [sample]; 145 | } 146 | 147 | return new this.namespace.elements.Array([sample.content]); 148 | }); 149 | 150 | if (samples.length) { 151 | attributes.set('samples', samples); 152 | } 153 | 154 | if (attributes.length > 0) { 155 | return this.serialiseObject(attributes); 156 | } 157 | 158 | return undefined; 159 | } 160 | 161 | enumSerialiseContent(element) { 162 | // In API Elements < 1.0, the content is the enumerations 163 | // If we don't have an enumerations, use the value (Drafter 3 behaviour) 164 | 165 | if (element._attributes) { 166 | const enumerations = element.attributes.get('enumerations'); 167 | 168 | if (enumerations && enumerations.length > 0) { 169 | return enumerations.content.map((enumeration) => { 170 | const e = enumeration.clone(); 171 | e.attributes.remove('typeAttributes'); 172 | return this.serialise(e); 173 | }); 174 | } 175 | } 176 | 177 | if (element.content) { 178 | const value = element.content.clone(); 179 | value.attributes.remove('typeAttributes'); 180 | return [this.serialise(value)]; 181 | } 182 | 183 | return []; 184 | } 185 | 186 | deserialise(value) { 187 | if (typeof value === 'string') { 188 | return new this.namespace.elements.String(value); 189 | } 190 | 191 | if (typeof value === 'number') { 192 | return new this.namespace.elements.Number(value); 193 | } 194 | 195 | if (typeof value === 'boolean') { 196 | return new this.namespace.elements.Boolean(value); 197 | } 198 | 199 | if (value === null) { 200 | return new this.namespace.elements.Null(); 201 | } 202 | 203 | if (Array.isArray(value)) { 204 | return new this.namespace.elements.Array(value.map(this.deserialise, this)); 205 | } 206 | 207 | const ElementClass = this.namespace.getElementClass(value.element); 208 | const element = new ElementClass(); 209 | 210 | if (element.element !== value.element) { 211 | element.element = value.element; 212 | } 213 | 214 | if (value.meta) { 215 | this.deserialiseObject(value.meta, element.meta); 216 | } 217 | 218 | if (value.attributes) { 219 | this.deserialiseObject(value.attributes, element.attributes); 220 | } 221 | 222 | const content = this.deserialiseContent(value.content); 223 | if (content !== undefined || element.content === null) { 224 | element.content = content; 225 | } 226 | 227 | if (element.element === 'enum') { 228 | // Grab enumerations from content 229 | if (element.content) { 230 | element.attributes.set('enumerations', element.content); 231 | } 232 | 233 | // Unwrap the sample value (inside double array) 234 | let samples = element.attributes.get('samples'); 235 | element.attributes.remove('samples'); 236 | 237 | if (samples) { 238 | // Re-wrap samples from array of array to array of enum's 239 | 240 | const existingSamples = samples; 241 | 242 | samples = new this.namespace.elements.Array(); 243 | existingSamples.forEach((existingSample) => { 244 | existingSample.forEach((sample) => { 245 | const enumElement = new ElementClass(sample); 246 | enumElement.element = element.element; 247 | samples.push(enumElement); 248 | }); 249 | }); 250 | 251 | const sample = samples.shift(); 252 | 253 | if (sample) { 254 | element.content = sample.content; 255 | } else { 256 | element.content = undefined; 257 | } 258 | 259 | element.attributes.set('samples', samples); 260 | } else { 261 | element.content = undefined; 262 | } 263 | 264 | // Unwrap the default value 265 | let defaultValue = element.attributes.get('default'); 266 | if (defaultValue && defaultValue.length > 0) { 267 | defaultValue = defaultValue.get(0); 268 | const defaultElement = new ElementClass(defaultValue); 269 | defaultElement.element = element.element; 270 | element.attributes.set('default', defaultElement); 271 | } 272 | } else if (element.element === 'dataStructure' && Array.isArray(element.content)) { 273 | [element.content] = element.content; 274 | } else if (element.element === 'category') { 275 | // "meta" attribute has been renamed to metadata 276 | const metadata = element.attributes.get('meta'); 277 | 278 | if (metadata) { 279 | element.attributes.set('metadata', metadata); 280 | element.attributes.remove('meta'); 281 | } 282 | } else if (element.element === 'member' && element.key && element.key._attributes && element.key._attributes.getValue('variable')) { 283 | element.attributes.set('variable', element.key.attributes.get('variable')); 284 | element.key.attributes.remove('variable'); 285 | } 286 | 287 | return element; 288 | } 289 | 290 | // Private API 291 | 292 | serialiseContent(content) { 293 | if (content instanceof this.namespace.elements.Element) { 294 | return this.serialise(content); 295 | } 296 | 297 | if (content instanceof this.namespace.KeyValuePair) { 298 | const pair = { 299 | key: this.serialise(content.key), 300 | }; 301 | 302 | if (content.value) { 303 | pair.value = this.serialise(content.value); 304 | } 305 | 306 | return pair; 307 | } 308 | 309 | if (content && content.map) { 310 | return content.map(this.serialise, this); 311 | } 312 | 313 | return content; 314 | } 315 | 316 | deserialiseContent(content) { 317 | if (content) { 318 | if (content.element) { 319 | return this.deserialise(content); 320 | } 321 | 322 | if (content.key) { 323 | const pair = new this.namespace.KeyValuePair(this.deserialise(content.key)); 324 | 325 | if (content.value) { 326 | pair.value = this.deserialise(content.value); 327 | } 328 | 329 | return pair; 330 | } 331 | 332 | if (content.map) { 333 | return content.map(this.deserialise, this); 334 | } 335 | } 336 | 337 | return content; 338 | } 339 | 340 | shouldRefract(element) { 341 | if ((element._attributes && element.attributes.keys().length) || (element._meta && element.meta.keys().length)) { 342 | return true; 343 | } 344 | 345 | if (element.element === 'enum') { 346 | // enum elements are treated like primitives (array) 347 | return false; 348 | } 349 | 350 | if (element.element !== element.primitive() || element.element === 'member') { 351 | return true; 352 | } 353 | 354 | return false; 355 | } 356 | 357 | convertKeyToRefract(key, item) { 358 | if (this.shouldRefract(item)) { 359 | return this.serialise(item); 360 | } 361 | 362 | if (item.element === 'enum') { 363 | return this.serialiseEnum(item); 364 | } 365 | 366 | if (item.element === 'array') { 367 | return item.map((subItem) => { 368 | if (this.shouldRefract(subItem) || key === 'default') { 369 | return this.serialise(subItem); 370 | } 371 | 372 | if (subItem.element === 'array' || subItem.element === 'object' || subItem.element === 'enum') { 373 | // items for array or enum inside array are always serialised 374 | return subItem.children.map(subSubItem => this.serialise(subSubItem)); 375 | } 376 | 377 | return subItem.toValue(); 378 | }); 379 | } 380 | 381 | if (item.element === 'object') { 382 | return (item.content || []).map(this.serialise, this); 383 | } 384 | 385 | return item.toValue(); 386 | } 387 | 388 | serialiseEnum(element) { 389 | return element.children.map(item => this.serialise(item)); 390 | } 391 | 392 | serialiseObject(obj) { 393 | const result = {}; 394 | 395 | obj.forEach((value, key) => { 396 | if (value) { 397 | const keyValue = key.toValue(); 398 | result[keyValue] = this.convertKeyToRefract(keyValue, value); 399 | } 400 | }); 401 | 402 | return result; 403 | } 404 | 405 | deserialiseObject(from, to) { 406 | Object.keys(from).forEach((key) => { 407 | to.set(key, this.deserialise(from[key])); 408 | }); 409 | } 410 | }; 411 | -------------------------------------------------------------------------------- /lib/serialisers/JSONSerialiser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class JSONSerialiser 3 | * 4 | * @param {Namespace} namespace 5 | * 6 | * @property {Namespace} namespace 7 | */ 8 | class JSONSerialiser { 9 | constructor(namespace) { 10 | this.namespace = namespace || new this.Namespace(); 11 | } 12 | 13 | /** 14 | * @param {Element} element 15 | * @returns {object} 16 | */ 17 | serialise(element) { 18 | if (!(element instanceof this.namespace.elements.Element)) { 19 | throw new TypeError(`Given element \`${element}\` is not an Element instance`); 20 | } 21 | 22 | const payload = { 23 | element: element.element, 24 | }; 25 | 26 | if (element._meta && element._meta.length > 0) { 27 | payload.meta = this.serialiseObject(element.meta); 28 | } 29 | 30 | if (element._attributes && element._attributes.length > 0) { 31 | payload.attributes = this.serialiseObject(element.attributes); 32 | } 33 | 34 | const content = this.serialiseContent(element.content); 35 | 36 | if (content !== undefined) { 37 | payload.content = content; 38 | } 39 | 40 | return payload; 41 | } 42 | 43 | /** 44 | * @param {object} value 45 | * @returns {Element} 46 | */ 47 | deserialise(value) { 48 | if (!value.element) { 49 | throw new Error('Given value is not an object containing an element name'); 50 | } 51 | 52 | const ElementClass = this.namespace.getElementClass(value.element); 53 | const element = new ElementClass(); 54 | 55 | if (element.element !== value.element) { 56 | element.element = value.element; 57 | } 58 | 59 | if (value.meta) { 60 | this.deserialiseObject(value.meta, element.meta); 61 | } 62 | 63 | if (value.attributes) { 64 | this.deserialiseObject(value.attributes, element.attributes); 65 | } 66 | 67 | const content = this.deserialiseContent(value.content); 68 | if (content !== undefined || element.content === null) { 69 | element.content = content; 70 | } 71 | 72 | return element; 73 | } 74 | 75 | // Private API 76 | 77 | serialiseContent(content) { 78 | if (content instanceof this.namespace.elements.Element) { 79 | return this.serialise(content); 80 | } 81 | 82 | if (content instanceof this.namespace.KeyValuePair) { 83 | const pair = { 84 | key: this.serialise(content.key), 85 | }; 86 | 87 | if (content.value) { 88 | pair.value = this.serialise(content.value); 89 | } 90 | 91 | return pair; 92 | } 93 | 94 | if (content && content.map) { 95 | if (content.length === 0) { 96 | return undefined; 97 | } 98 | 99 | return content.map(this.serialise, this); 100 | } 101 | 102 | return content; 103 | } 104 | 105 | deserialiseContent(content) { 106 | if (content) { 107 | if (content.element) { 108 | return this.deserialise(content); 109 | } 110 | 111 | if (content.key) { 112 | const pair = new this.namespace.KeyValuePair(this.deserialise(content.key)); 113 | 114 | if (content.value) { 115 | pair.value = this.deserialise(content.value); 116 | } 117 | 118 | return pair; 119 | } 120 | 121 | if (content.map) { 122 | return content.map(this.deserialise, this); 123 | } 124 | } 125 | 126 | return content; 127 | } 128 | 129 | serialiseObject(obj) { 130 | const result = {}; 131 | 132 | obj.forEach((value, key) => { 133 | if (value) { 134 | result[key.toValue()] = this.serialise(value); 135 | } 136 | }); 137 | 138 | if (Object.keys(result).length === 0) { 139 | return undefined; 140 | } 141 | 142 | return result; 143 | } 144 | 145 | deserialiseObject(from, to) { 146 | Object.keys(from).forEach((key) => { 147 | to.set(key, this.deserialise(from[key])); 148 | }); 149 | } 150 | } 151 | 152 | 153 | module.exports = JSONSerialiser; 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minim", 3 | "version": "0.23.8", 4 | "description": "A library for interacting with JSON through Refract elements", 5 | "main": "lib/minim.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "build": "npm run build:cjs && npm run build:browser", 9 | "build:cjs": "babel lib --out-dir dist/lib", 10 | "build:browser": "browserify -d -s minim -t [ babelify ] -o dist/minim.js dist/lib/minim.js", 11 | "coverage": "istanbul cover _mocha -- -R spec --recursive --require babel-register", 12 | "coveralls": "coveralls =6" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/ArraySlice-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('./spec-helper'); 2 | const minim = require('../lib/minim'); 3 | 4 | const { Element } = minim; 5 | const { StringElement } = minim; 6 | const { ArraySlice } = minim; 7 | 8 | describe('ArraySlice', () => { 9 | const thisArg = { message: 42 }; 10 | 11 | it('can be created from an array of elements', () => { 12 | const element = new Element(); 13 | const slice = new ArraySlice([element]); 14 | 15 | expect(slice.elements).to.deep.equal([element]); 16 | }); 17 | 18 | it('returns the length of the slice', () => { 19 | const slice = new ArraySlice([new Element()]); 20 | 21 | expect(slice.length).to.equal(1); 22 | }); 23 | 24 | it('returns when the slice is empty', () => { 25 | const slice = new ArraySlice(); 26 | expect(slice.isEmpty).to.be.true; 27 | }); 28 | 29 | it('returns when the slice is not empty', () => { 30 | const slice = new ArraySlice([new ArraySlice()]); 31 | expect(slice.isEmpty).to.be.false; 32 | }); 33 | 34 | it('allows converting to value', () => { 35 | const element = new Element('hello'); 36 | const slice = new ArraySlice([element]); 37 | 38 | expect(slice.toValue()).to.deep.equal(['hello']); 39 | }); 40 | 41 | it('provides map', () => { 42 | const element = new Element('hello'); 43 | const slice = new ArraySlice([element]); 44 | 45 | const mapped = slice.map(function map(e) { 46 | expect(this).to.deep.equal(thisArg); 47 | return e.toValue(); 48 | }, thisArg); 49 | 50 | expect(mapped).to.deep.equal(['hello']); 51 | }); 52 | 53 | context('#filter', () => { 54 | it('filters elements satisfied from callback', () => { 55 | const one = new Element('one'); 56 | const two = new Element('two'); 57 | const slice = new ArraySlice([one, two]); 58 | 59 | const filtered = slice.filter(function filter(element) { 60 | expect(this).to.deep.equal(thisArg); 61 | return element.toValue() === 'one'; 62 | }, thisArg); 63 | 64 | expect(filtered).to.be.instanceof(ArraySlice); 65 | expect(filtered.elements).to.deep.equal([one]); 66 | }); 67 | 68 | it('filters elements satisfied from element class', () => { 69 | const one = new StringElement('one'); 70 | const two = new Element('two'); 71 | const slice = new ArraySlice([one, two]); 72 | 73 | const filtered = slice.filter(elem => elem instanceof StringElement); 74 | 75 | expect(filtered).to.be.instanceof(ArraySlice); 76 | expect(filtered.elements).to.deep.equal([one]); 77 | }); 78 | 79 | it('filters elements satisfied from element name', () => { 80 | const one = new StringElement('one'); 81 | const two = new Element('two'); 82 | const slice = new ArraySlice([one, two]); 83 | 84 | const filtered = slice.filter('string'); 85 | 86 | expect(filtered).to.be.instanceof(ArraySlice); 87 | expect(filtered.elements).to.deep.equal([one]); 88 | }); 89 | }); 90 | 91 | context('#reject', () => { 92 | it('rejects elements satisfied from callback', () => { 93 | const one = new Element('one'); 94 | const two = new Element('two'); 95 | const slice = new ArraySlice([one, two]); 96 | 97 | const filtered = slice.reject(function filter(element) { 98 | expect(this).to.deep.equal(thisArg); 99 | return element.toValue() === 'one'; 100 | }, thisArg); 101 | 102 | expect(filtered).to.be.instanceof(ArraySlice); 103 | expect(filtered.elements).to.deep.equal([two]); 104 | }); 105 | 106 | it('rejects elements satisfied from element class', () => { 107 | const one = new StringElement('one'); 108 | const two = new Element('two'); 109 | const slice = new ArraySlice([one, two]); 110 | 111 | const filtered = slice.reject(elem => elem instanceof StringElement); 112 | 113 | expect(filtered).to.be.instanceof(ArraySlice); 114 | expect(filtered.elements).to.deep.equal([two]); 115 | }); 116 | 117 | it('rejects elements satisfied from element name', () => { 118 | const one = new StringElement('one'); 119 | const two = new Element('two'); 120 | const slice = new ArraySlice([one, two]); 121 | 122 | const filtered = slice.reject('string'); 123 | 124 | expect(filtered).to.be.instanceof(ArraySlice); 125 | expect(filtered.elements).to.deep.equal([two]); 126 | }); 127 | }); 128 | 129 | describe('#find', () => { 130 | it('finds first element satisfied from callback', () => { 131 | const one = new Element('one'); 132 | const two = new Element('two'); 133 | const slice = new ArraySlice([one, two]); 134 | 135 | const element = slice.find(e => e.toValue() === 'two'); 136 | 137 | expect(element).to.be.equal(two); 138 | }); 139 | 140 | it('finds first element satisfied from element class', () => { 141 | const one = new Element('one'); 142 | const two = new StringElement('two'); 143 | const slice = new ArraySlice([one, two]); 144 | 145 | const element = slice.find(elem => elem instanceof StringElement); 146 | 147 | expect(element).to.be.equal(two); 148 | }); 149 | 150 | it('finds first element satisfied from element name', () => { 151 | const one = new Element('one'); 152 | const two = new StringElement('two'); 153 | const slice = new ArraySlice([one, two]); 154 | 155 | const element = slice.find('string'); 156 | 157 | expect(element).to.be.equal(two); 158 | }); 159 | }); 160 | 161 | it('provides flatMap', () => { 162 | const element = new Element('flat mapping for this element'); 163 | const one = new Element('one'); 164 | one.attributes.set('default', element); 165 | const two = new Element('two'); 166 | const slice = new ArraySlice([one, two]); 167 | 168 | const titles = slice.flatMap(function flatMap(e) { 169 | expect(this).to.deep.equal(thisArg); 170 | const defaultAttribute = e.attributes.get('default'); 171 | 172 | if (defaultAttribute) { 173 | return [defaultAttribute]; 174 | } 175 | 176 | return []; 177 | }, thisArg); 178 | 179 | expect(titles).to.deep.equal([element]); 180 | }); 181 | 182 | it('provides compactMap', () => { 183 | const element = new Element('compact mapping for this element'); 184 | const one = new Element('one'); 185 | one.attributes.set('default', element); 186 | const two = new Element('two'); 187 | const slice = new ArraySlice([one, two]); 188 | 189 | const titles = slice.compactMap(function compactMap(e) { 190 | expect(this).to.deep.equal(thisArg); 191 | return e.attributes.get('default'); 192 | }, thisArg); 193 | 194 | expect(titles).to.deep.equal([element]); 195 | }); 196 | 197 | it('provides forEach', () => { 198 | const one = new Element('one'); 199 | const two = new Element('two'); 200 | const slice = new ArraySlice([one, two]); 201 | 202 | const elements = []; 203 | const indexes = []; 204 | 205 | slice.forEach(function forEach(element, index) { 206 | elements.push(element); 207 | indexes.push(index); 208 | expect(this).to.deep.equal(thisArg); 209 | }, thisArg); 210 | 211 | expect(elements).to.deep.equal([one, two]); 212 | expect(indexes).to.deep.equal([0, 1]); 213 | }); 214 | 215 | /** 216 | * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#Examples 217 | */ 218 | it('provides reduce to sum all the values of an array', () => { 219 | const slice = new ArraySlice([0, 1, 2, 3]); 220 | 221 | const sum = slice.reduce((accumulator, currentValue) => accumulator + currentValue, 0); 222 | 223 | expect(sum).to.equal(6); 224 | }); 225 | 226 | /** 227 | * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#Examples 228 | */ 229 | it('provides reduce to flatten an array of arrays', () => { 230 | const slice = new ArraySlice([[0, 1], [2, 3], [4, 5]]); 231 | 232 | const flattened = slice.reduce( 233 | (accumulator, currentValue) => accumulator.concat(currentValue), 234 | [] 235 | ); 236 | 237 | expect(flattened).to.deep.equal([0, 1, 2, 3, 4, 5]); 238 | }); 239 | 240 | /** 241 | * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap#Alternative 242 | */ 243 | it('provides flatMap as an alternative to reduce', () => { 244 | const arr1 = new ArraySlice([1, 2, 3, 4]); 245 | 246 | const reduced = arr1.reduce( 247 | (acc, x) => acc.concat([x * 2]), 248 | [] 249 | ); 250 | 251 | expect(reduced).to.deep.equal([2, 4, 6, 8]); 252 | 253 | const flattened = arr1.flatMap(x => [x * 2]); 254 | 255 | expect(flattened).to.deep.equal(reduced); 256 | }); 257 | 258 | /** 259 | * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap#Examples 260 | */ 261 | it('provides flatMap to flatten one level', () => { 262 | const arr1 = new ArraySlice([1, 2, 3, 4]); 263 | 264 | const mapped = arr1.map(x => [x * 2]); 265 | 266 | expect(mapped).to.deep.equal([[2], [4], [6], [8]]); 267 | 268 | const flattened = arr1.flatMap(x => [x * 2]); 269 | 270 | expect(flattened).to.deep.equal([2, 4, 6, 8]); 271 | 272 | const flattenOnce = arr1.flatMap(x => [[x * 2]]); 273 | 274 | expect(flattenOnce).to.deep.equal([[2], [4], [6], [8]]); 275 | }); 276 | 277 | describe('#includes', () => { 278 | const slice = new ArraySlice([ 279 | new Element('one'), 280 | new Element('two'), 281 | ]); 282 | 283 | it('returns true when the slice contains an matching value', () => { 284 | expect(slice.includes('one')).to.be.true; 285 | }); 286 | 287 | it('returns false when there are no matches', () => { 288 | expect(slice.includes('three')).to.be.false; 289 | }); 290 | }); 291 | 292 | it('allows shifting an element', () => { 293 | const one = new Element('one'); 294 | const two = new Element('two'); 295 | const slice = new ArraySlice([one, two]); 296 | 297 | const shifted = slice.shift(); 298 | 299 | expect(slice.length).to.equal(1); 300 | expect(shifted).to.equal(one); 301 | }); 302 | 303 | it('allows unshifting an element', () => { 304 | const two = new Element('two'); 305 | const slice = new ArraySlice([two]); 306 | 307 | slice.unshift('one'); 308 | 309 | expect(slice.length).to.equal(2); 310 | expect(slice.get(0).toValue()).to.equal('one'); 311 | }); 312 | 313 | it('allows pushing new items to end', () => { 314 | const one = new Element('one'); 315 | const slice = new ArraySlice([one]); 316 | 317 | slice.push('two'); 318 | 319 | expect(slice.length).to.equal(2); 320 | expect(slice.get(1).toValue()).to.equal('two'); 321 | }); 322 | 323 | it('allows adding new items to end', () => { 324 | const one = new Element('one'); 325 | const slice = new ArraySlice([one]); 326 | 327 | slice.add('two'); 328 | 329 | expect(slice.length).to.equal(2); 330 | expect(slice.get(1).toValue()).to.equal('two'); 331 | }); 332 | 333 | it('allows getting an element via index', () => { 334 | const one = new Element('one'); 335 | const slice = new ArraySlice([one]); 336 | expect(slice.get(0)).to.deep.equal(one); 337 | }); 338 | 339 | it('allows getting a value via index', () => { 340 | const one = new Element('one'); 341 | const slice = new ArraySlice([one]); 342 | expect(slice.getValue(0)).to.equal('one'); 343 | }); 344 | 345 | describe('#first', () => { 346 | it('returns the first item', () => { 347 | const element = new Element(); 348 | const slice = new ArraySlice([element]); 349 | 350 | expect(slice.first).to.equal(element); 351 | }); 352 | 353 | it('returns undefined when there isnt any items', () => { 354 | const slice = new ArraySlice(); 355 | 356 | expect(slice.first).to.be.undefined; 357 | }); 358 | }); 359 | }); 360 | -------------------------------------------------------------------------------- /test/Namespace-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('./spec-helper'); 2 | const minim = require('../lib/minim'); 3 | const Namespace = require('../lib/Namespace'); 4 | const JSONSerialiser = require('../lib/serialisers/JSONSerialiser'); 5 | 6 | describe('Minim namespace', () => { 7 | let namespace; 8 | let NullElement; let ObjectElement; let StringElement; 9 | 10 | beforeEach(() => { 11 | namespace = new Namespace(); 12 | namespace.elementMap = {}; 13 | namespace.elementDetection = []; 14 | namespace.useDefault(); 15 | 16 | NullElement = namespace.getElementClass('null'); 17 | ObjectElement = namespace.getElementClass('object'); 18 | StringElement = namespace.getElementClass('string'); 19 | }); 20 | 21 | it('is exposed on the module', () => { 22 | expect(minim.Namespace).to.equal(Namespace); 23 | }); 24 | 25 | it('gets returned from minim.namespace()', () => { 26 | expect(minim.namespace()).to.be.an.instanceof(Namespace); 27 | }); 28 | 29 | describe('default elements', () => { 30 | it('are present by default', () => { 31 | expect(namespace.elementMap).not.to.be.empty; 32 | }); 33 | 34 | it('can be created empty', () => { 35 | expect((new Namespace({ noDefault: true })).elementMap).to.deep.equal({}); 36 | }); 37 | 38 | it('can be added after instantiation', () => { 39 | const testnamespace = new Namespace({ noDefault: true }); 40 | testnamespace.useDefault(); 41 | expect(testnamespace.elementMap).not.to.be.empty; 42 | }); 43 | }); 44 | 45 | describe('#use', () => { 46 | it('can load a plugin module using the namespace property', () => { 47 | const plugin = { 48 | namespace(options) { 49 | const { base } = options; 50 | 51 | // Register a new element 52 | base.register('null2', NullElement); 53 | }, 54 | }; 55 | 56 | namespace.use(plugin); 57 | 58 | expect(namespace.elementMap).to.have.property('null2', NullElement); 59 | }); 60 | 61 | it('can load a plugin module using the load property', () => { 62 | const plugin = { 63 | load(options) { 64 | const { base } = options; 65 | 66 | // Register a new element 67 | base.register('null3', NullElement); 68 | }, 69 | }; 70 | 71 | namespace.use(plugin); 72 | 73 | expect(namespace.elementMap).to.have.property('null3', NullElement); 74 | }); 75 | }); 76 | 77 | describe('#register', () => { 78 | it('should add to the element map', () => { 79 | namespace.register('test', ObjectElement); 80 | expect(namespace.elementMap.test).to.equal(ObjectElement); 81 | }); 82 | }); 83 | 84 | describe('#unregister', () => { 85 | it('should remove from the element map', () => { 86 | namespace.unregister('test'); 87 | expect(namespace.elementMap).to.not.have.key('test'); 88 | }); 89 | }); 90 | 91 | describe('#detect', () => { 92 | const test = () => true; 93 | 94 | it('should prepend by default', () => { 95 | namespace.elementDetection = [[test, NullElement]]; 96 | namespace.detect(test, StringElement); 97 | expect(namespace.elementDetection[0][1]).to.equal(StringElement); 98 | }); 99 | 100 | it('should be able to append', () => { 101 | namespace.elementDetection = [[test, NullElement]]; 102 | namespace.detect(test, ObjectElement, false); 103 | expect(namespace.elementDetection[1][1]).to.equal(ObjectElement); 104 | }); 105 | }); 106 | 107 | describe('#getElementClass', () => { 108 | it('should return ElementClass for unknown elements', () => { 109 | expect(namespace.getElementClass('unknown')).to.equal(namespace.Element); 110 | }); 111 | }); 112 | 113 | describe('#elements', () => { 114 | it('should contain registered element classes', () => { 115 | const { elements } = namespace; 116 | 117 | const elementValues = Object.keys(elements).map(name => elements[name]); 118 | elementValues.shift(); 119 | 120 | const mapValues = Object.keys(namespace.elementMap).map(name => namespace.elementMap[name]); 121 | 122 | expect(elementValues).to.deep.equal(mapValues); 123 | }); 124 | 125 | it('should use pascal casing', () => { 126 | Object.keys(namespace.elements).forEach((name) => { 127 | expect(name[0]).to.equal(name[0].toUpperCase()); 128 | }); 129 | }); 130 | 131 | it('should contain the base element', () => { 132 | expect(namespace.elements.Element).to.equal(namespace.Element); 133 | }); 134 | }); 135 | 136 | describe('#toElement', () => { 137 | it('returns element when given element', () => { 138 | const element = new StringElement('hello'); 139 | const toElement = namespace.toElement(element); 140 | 141 | expect(toElement).to.equal(element); 142 | }); 143 | 144 | it('returns string element when given string', () => { 145 | const element = namespace.toElement('hello'); 146 | 147 | expect(element).to.be.instanceof(StringElement); 148 | expect(element.toValue()).to.equal('hello'); 149 | }); 150 | }); 151 | 152 | describe('serialisation', () => { 153 | it('provides a convenience serialiser', () => { 154 | expect(namespace.serialiser).to.be.instanceof(JSONSerialiser); 155 | expect(namespace.serialiser.namespace).to.equal(namespace); 156 | }); 157 | 158 | it('provides a convenience fromRefract', () => { 159 | const element = namespace.fromRefract({ 160 | element: 'string', 161 | content: 'hello', 162 | }); 163 | 164 | expect(element).to.be.instanceof(StringElement); 165 | expect(element.toValue()).to.equal('hello'); 166 | }); 167 | 168 | it('provides a convenience toRefract', () => { 169 | const element = new StringElement('hello'); 170 | const object = namespace.toRefract(element); 171 | 172 | expect(object).to.deep.equal({ 173 | element: 'string', 174 | content: 'hello', 175 | }); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/ObjectSlice-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('./spec-helper'); 2 | const { MemberElement, ObjectSlice } = require('../lib/minim'); 3 | 4 | describe('ObjectSlice', () => { 5 | const thisArg = { message: 42 }; 6 | 7 | it('provides map', () => { 8 | const slice = new ObjectSlice([ 9 | new MemberElement('name', 'Doe'), 10 | ]); 11 | 12 | const result = slice.map(function map(value) { 13 | expect(this).to.deep.equal(thisArg); 14 | return value.toValue(); 15 | }, thisArg); 16 | 17 | expect(result).to.deep.equal(['Doe']); 18 | }); 19 | 20 | it('provides forEach', () => { 21 | const element = new MemberElement('name', 'Doe'); 22 | const slice = new ObjectSlice([element]); 23 | 24 | const keys = []; 25 | const values = []; 26 | const members = []; 27 | const indexes = []; 28 | 29 | slice.forEach(function forEach(value, key, member, index) { 30 | keys.push(key.toValue()); 31 | values.push(value.toValue()); 32 | members.push(member); 33 | indexes.push(index); 34 | 35 | expect(this).to.deep.equal(thisArg); 36 | }, thisArg); 37 | 38 | expect(keys).to.deep.equal(['name']); 39 | expect(values).to.deep.equal(['Doe']); 40 | expect(members).to.deep.equal([element]); 41 | expect(indexes).to.deep.equal([0]); 42 | }); 43 | 44 | it('provides filter', () => { 45 | const slice = new ObjectSlice([ 46 | new MemberElement('name', 'Doe'), 47 | new MemberElement('name', 'Bill'), 48 | ]); 49 | 50 | const filtered = slice.filter(function filter(value) { 51 | expect(this).to.deep.equal(thisArg); 52 | return value.toValue() === 'Doe'; 53 | }, thisArg); 54 | 55 | expect(filtered).to.be.instanceof(ObjectSlice); 56 | expect(filtered.toValue()).to.deep.equal([{ key: 'name', value: 'Doe' }]); 57 | }); 58 | 59 | it('provides reject', () => { 60 | const slice = new ObjectSlice([ 61 | new MemberElement('name', 'Doe'), 62 | new MemberElement('name', 'Bill'), 63 | ]); 64 | 65 | const filtered = slice.reject(function filter(value) { 66 | expect(this).to.deep.equal(thisArg); 67 | return value.toValue() === 'Doe'; 68 | }, thisArg); 69 | 70 | expect(filtered).to.be.instanceof(ObjectSlice); 71 | expect(filtered.toValue()).to.deep.equal([{ key: 'name', value: 'Bill' }]); 72 | }); 73 | 74 | it('provides keys', () => { 75 | const element = new MemberElement('name', 'Doe'); 76 | const slice = new ObjectSlice([element]); 77 | 78 | expect(slice.keys()).to.deep.equal(['name']); 79 | }); 80 | 81 | it('provides values', () => { 82 | const element = new MemberElement('name', 'Doe'); 83 | const slice = new ObjectSlice([element]); 84 | 85 | expect(slice.values()).to.deep.equal(['Doe']); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/converters-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('./spec-helper'); 2 | const minim = require('../lib/minim').namespace(); 3 | 4 | describe('Minim Converters', () => { 5 | describe('convertToElement', () => { 6 | function elementCheck(name, val) { 7 | let returnedElement; 8 | 9 | context(`when given ${name}`, () => { 10 | before(() => { 11 | returnedElement = minim.toElement(val); 12 | }); 13 | 14 | it(`returns ${name}`, () => { 15 | expect(returnedElement.element).to.equal(name); 16 | }); 17 | }); 18 | } 19 | 20 | elementCheck('null', null); 21 | elementCheck('string', 'foobar'); 22 | elementCheck('number', 1); 23 | elementCheck('boolean', true); 24 | elementCheck('array', [1, 2, 3]); 25 | elementCheck('object', { 26 | foo: 'bar', 27 | }); 28 | }); 29 | 30 | describe('convertFromElement', () => { 31 | function elementCheck(name, el) { 32 | context(`when given ${name}`, () => { 33 | let returnedElement; 34 | 35 | before(() => { 36 | returnedElement = minim.fromRefract(el); 37 | }); 38 | 39 | it(`returns ${name} element`, () => { 40 | expect(returnedElement.element).to.equal(name); 41 | }); 42 | 43 | it('has the correct value', () => { 44 | expect(returnedElement.toValue()).to.equal(el.content); 45 | }); 46 | }); 47 | } 48 | 49 | elementCheck('null', { 50 | element: 'null', 51 | content: null, 52 | }); 53 | 54 | elementCheck('string', { 55 | element: 'string', 56 | content: 'foo', 57 | }); 58 | 59 | elementCheck('number', { 60 | element: 'number', 61 | content: 4, 62 | }); 63 | 64 | elementCheck('boolean', { 65 | element: 'boolean', 66 | content: true, 67 | }); 68 | 69 | context('when given array', () => { 70 | const el = { 71 | element: 'array', 72 | content: [ 73 | { 74 | element: 'number', 75 | content: 1, 76 | }, { 77 | element: 'number', 78 | content: 2, 79 | }, 80 | ], 81 | }; 82 | let returnedElement; 83 | 84 | before(() => { 85 | returnedElement = minim.fromRefract(el); 86 | }); 87 | 88 | it('returns array element', () => { 89 | expect(returnedElement.element).to.equal('array'); 90 | }); 91 | 92 | it('has the correct values', () => { 93 | expect(returnedElement.toValue()).to.deep.equal([1, 2]); 94 | }); 95 | }); 96 | 97 | context('when given object', () => { 98 | const el = { 99 | element: 'object', 100 | meta: {}, 101 | attributes: {}, 102 | content: [ 103 | { 104 | element: 'member', 105 | content: { 106 | key: { 107 | element: 'string', 108 | meta: {}, 109 | attributes: {}, 110 | content: 'foo', 111 | }, 112 | value: { 113 | element: 'string', 114 | meta: {}, 115 | attributes: {}, 116 | content: 'bar', 117 | }, 118 | }, 119 | }, 120 | { 121 | element: 'member', 122 | meta: {}, 123 | attributes: {}, 124 | content: { 125 | key: { 126 | element: 'string', 127 | meta: {}, 128 | attributes: {}, 129 | content: 'z', 130 | }, 131 | value: { 132 | element: 'number', 133 | meta: {}, 134 | attributes: {}, 135 | content: 2, 136 | }, 137 | }, 138 | }, 139 | ], 140 | }; 141 | let returnedElement; 142 | 143 | before(() => { 144 | returnedElement = minim.fromRefract(el); 145 | }); 146 | 147 | it('returns object element', () => { 148 | expect(returnedElement.element).to.equal('object'); 149 | }); 150 | 151 | it('has the correct values', () => { 152 | expect(returnedElement.toValue()).to.deep.equal({ 153 | foo: 'bar', 154 | z: 2, 155 | }); 156 | }); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/elements/LinkElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const LinkElement = minim.getElementClass('link'); 5 | 6 | describe('Link Element', () => { 7 | context('when creating an instance of LinkElement', () => { 8 | let link; 9 | 10 | before(() => { 11 | link = new LinkElement(); 12 | link.relation = 'foo'; 13 | link.href = '/bar'; 14 | }); 15 | 16 | it('sets the correct attributes', () => { 17 | expect(link.attributes.get('relation').toValue()).to.equal('foo'); 18 | expect(link.attributes.get('href').toValue()).to.equal('/bar'); 19 | }); 20 | 21 | it('provides convenience methods', () => { 22 | expect(link.relation.toValue()).to.equal('foo'); 23 | expect(link.href.toValue()).to.equal('/bar'); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/elements/RefElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim'); 3 | 4 | const { RefElement } = minim; 5 | const { StringElement } = minim; 6 | 7 | describe('Ref Element', () => { 8 | it('has ref element name', () => { 9 | const element = new RefElement(); 10 | 11 | expect(element.element).to.equal('ref'); 12 | }); 13 | 14 | it('has a default path of element', () => { 15 | const element = new RefElement(); 16 | 17 | expect(element.path.toValue()).to.equal('element'); 18 | }); 19 | 20 | it('can set the ref element path', () => { 21 | const element = new RefElement(); 22 | element.path = 'attributes'; 23 | 24 | const path = element.attributes.get('path'); 25 | 26 | expect(path).to.be.instanceof(StringElement); 27 | expect(path.toValue()).to.be.equal('attributes'); 28 | }); 29 | 30 | it('can get the ref element path', () => { 31 | const element = new RefElement(); 32 | element.attributes.set('path', 'attributes'); 33 | 34 | expect(element.path).to.be.instanceof(StringElement); 35 | expect(element.path.toValue()).to.be.equal('attributes'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/elements/attribute-with-attribute-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const ObjectElement = minim.getElementClass('object'); 5 | const StringElement = minim.getElementClass('string'); 6 | 7 | describe('Element whose attribute has attribute', () => { 8 | let object; let string; 9 | 10 | before(() => { 11 | object = new ObjectElement({ 12 | foo: 'bar', 13 | }); 14 | 15 | string = new StringElement('xyz'); 16 | string.attributes.set('pqr', 1); 17 | 18 | object.attributes.set('baz', string); 19 | }); 20 | 21 | it('returns the correct Refract value', () => { 22 | const value = object.attributes.get('baz').attributes.get('pqr').toValue(); 23 | expect(value).to.equal(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/elements/meta-with-meta-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const ObjectElement = minim.getElementClass('object'); 5 | const StringElement = minim.getElementClass('string'); 6 | 7 | describe('Element whose meta has meta', () => { 8 | let object; let string; 9 | 10 | before(() => { 11 | object = new ObjectElement({ 12 | foo: 'bar', 13 | }); 14 | 15 | string = new StringElement('xyz'); 16 | string.meta.set('pqr', 1); 17 | 18 | object.meta.set('baz', string); 19 | }); 20 | 21 | it('returns the correct Refract value', () => { 22 | const pqr = object.meta.get('baz').meta.getValue('pqr'); 23 | expect(pqr).to.equal(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | -------------------------------------------------------------------------------- /test/primitives/ArrayElement-fantasy-land-test.js: -------------------------------------------------------------------------------- 1 | const fl = require('fantasy-land'); 2 | const { expect } = require('../spec-helper'); 3 | const namespace = require('../../lib/minim').namespace(); 4 | 5 | const ArrayElement = namespace.getElementClass('array'); 6 | 7 | describe('ArrayElement', () => { 8 | const array = new ArrayElement([1, 2, 3, 4]); 9 | 10 | describe('Functor', () => { 11 | it('can transform elements into new ArrayElement', () => { 12 | const result = array[fl.map](n => new namespace.elements.Number(n.toValue() * 2)); 13 | 14 | expect(result).to.be.instanceof(ArrayElement); 15 | expect(result.toValue()).to.deep.equal([2, 4, 6, 8]); 16 | }); 17 | }); 18 | 19 | describe('Semigroup', () => { 20 | it('can concatinate two array elements', () => { 21 | const result = array[fl.concat](new ArrayElement([5, 6])); 22 | 23 | expect(result).to.be.instanceof(ArrayElement); 24 | expect(result.toValue()).to.deep.equal([1, 2, 3, 4, 5, 6]); 25 | }); 26 | }); 27 | 28 | describe('Monoid', () => { 29 | it('can create an empty ArrayElement', () => { 30 | const result = ArrayElement[fl.empty](); 31 | 32 | expect(result).to.be.instanceof(ArrayElement); 33 | expect(result.toValue()).to.deep.equal([]); 34 | }); 35 | 36 | it('can create an empty ArrayElement from another ArrayElement', () => { 37 | const result = array[fl.empty](); 38 | 39 | expect(result).to.be.instanceof(ArrayElement); 40 | expect(result.toValue()).to.deep.equal([]); 41 | }); 42 | }); 43 | 44 | describe('Filterable', () => { 45 | it('can filter all elements into equivilent ArrayElement', () => { 46 | const result = array[fl.filter](() => true); 47 | 48 | expect(result).to.deep.equal(array); 49 | }); 50 | 51 | it('can filter into empty ArrayElement', () => { 52 | const result = array[fl.filter](() => false); 53 | 54 | expect(result).to.be.instanceof(ArrayElement); 55 | expect(result.isEmpty).to.be.true; 56 | }); 57 | }); 58 | 59 | describe('Chain', () => { 60 | it('can transform and chain results into new ArrayElement', () => { 61 | const duplicate = n => new ArrayElement([n, n]); 62 | const result = array[fl.chain](duplicate); 63 | 64 | expect(result).to.be.instanceof(ArrayElement); 65 | expect(result.toValue()).to.deep.equal([1, 1, 2, 2, 3, 3, 4, 4]); 66 | }); 67 | }); 68 | 69 | describe('Foldable', () => { 70 | it('can reduce results into new ArrayElement', () => { 71 | const result = array[fl.reduce]((accumulator, element) => accumulator.concat(new ArrayElement([element.toValue(), element.toValue()])), new ArrayElement()); 72 | 73 | expect(result).to.be.instanceof(ArrayElement); 74 | expect(result.toValue()).to.deep.equal([1, 1, 2, 2, 3, 3, 4, 4]); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/primitives/ArrayElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const ArrayElement = minim.getElementClass('array'); 5 | 6 | describe('ArrayElement', () => { 7 | const thisArg = { message: 42 }; 8 | 9 | context('value methods', () => { 10 | let arrayElement; 11 | 12 | function setArray() { 13 | arrayElement = new ArrayElement(['a', true, null, 1]); 14 | } 15 | 16 | before(() => { 17 | setArray(); 18 | }); 19 | 20 | beforeEach(() => { 21 | setArray(); 22 | }); 23 | 24 | describe('.content', () => { 25 | let correctElementNames; 26 | let storedElementNames; 27 | 28 | before(() => { 29 | correctElementNames = ['string', 'boolean', 'null', 'number']; 30 | storedElementNames = arrayElement.content.map(el => el.element); 31 | }); 32 | 33 | it('stores the correct elements', () => { 34 | expect(storedElementNames).to.deep.equal(correctElementNames); 35 | }); 36 | }); 37 | 38 | describe('#element', () => { 39 | it('is an array', () => { 40 | expect(arrayElement.element).to.equal('array'); 41 | }); 42 | }); 43 | 44 | describe('#primitive', () => { 45 | it('returns array as the Refract primitive', () => { 46 | expect(arrayElement.primitive()).to.equal('array'); 47 | }); 48 | }); 49 | 50 | describe('#get', () => { 51 | context('when an index is given', () => { 52 | it('returns the item from the array', () => { 53 | expect(arrayElement.get(0).toValue()).to.equal('a'); 54 | }); 55 | }); 56 | 57 | context('when no index is given', () => { 58 | it('is undefined', () => { 59 | expect(arrayElement.get()).to.be.undefined; 60 | }); 61 | }); 62 | }); 63 | 64 | describe('#getValue', () => { 65 | context('when an index is given', () => { 66 | it('returns the item from the array', () => { 67 | expect(arrayElement.getValue(0)).to.equal('a'); 68 | }); 69 | }); 70 | 71 | context('when no index is given', () => { 72 | it('is undefined', () => { 73 | expect(arrayElement.getValue()).to.be.undefined; 74 | }); 75 | }); 76 | }); 77 | 78 | describe('#getIndex', () => { 79 | const numbers = new ArrayElement([1, 2, 3, 4]); 80 | 81 | it('returns the correct item', () => { 82 | expect(numbers.getIndex(1).toValue()).to.equal(2); 83 | }); 84 | }); 85 | 86 | describe('#set', () => { 87 | it('sets the value of the array', () => { 88 | arrayElement.set(0, 'hello world'); 89 | expect(arrayElement.get(0).toValue()).to.equal('hello world'); 90 | }); 91 | }); 92 | 93 | describe('#map', () => { 94 | it('allows for mapping the content of the array', () => { 95 | const newArray = arrayElement.map(function map(item) { 96 | expect(this).to.deep.equal(thisArg); 97 | return item.toValue(); 98 | }, thisArg); 99 | expect(newArray).to.deep.equal(['a', true, null, 1]); 100 | }); 101 | }); 102 | 103 | describe('#flatMap', () => { 104 | /** 105 | * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap#Examples 106 | */ 107 | it('provides flatMap to flatten one level', () => { 108 | const arr1 = new ArrayElement([1, 2, 3, 4]); 109 | 110 | const mapped = arr1.map(x => [x.toValue() * 2]); 111 | 112 | expect(mapped).to.deep.equal([[2], [4], [6], [8]]); 113 | 114 | const flattened = arr1.flatMap(x => [x.toValue() * 2]); 115 | 116 | expect(flattened).to.deep.equal([2, 4, 6, 8]); 117 | 118 | const flattenOnce = arr1.flatMap(x => [[x.toValue() * 2]]); 119 | 120 | expect(flattenOnce).to.deep.equal([[2], [4], [6], [8]]); 121 | }); 122 | }); 123 | 124 | describe('#compactMap', () => { 125 | it('allows compact mapping the content of the array', () => { 126 | const newArray = arrayElement.compactMap(function compactMap(item) { 127 | expect(this).to.deep.equal(thisArg); 128 | 129 | if (item.element === 'string' || item.element === 'number') { 130 | return item.toValue(); 131 | } 132 | 133 | return undefined; 134 | }, thisArg); 135 | expect(newArray).to.deep.equal(['a', 1]); 136 | }); 137 | }); 138 | 139 | describe('#filter', () => { 140 | it('allows for filtering the content', () => { 141 | const newArray = arrayElement.filter(function filter(item) { 142 | expect(this).to.deep.equal(thisArg); 143 | const ref = item.toValue(); 144 | return ref === 'a' || ref === 1; 145 | }, thisArg); 146 | expect(newArray.toValue()).to.deep.equal(['a', 1]); 147 | }); 148 | }); 149 | 150 | describe('#reject', () => { 151 | it('allows for rejecting the content', () => { 152 | const newArray = arrayElement.reject(function (item) { 153 | expect(this).to.deep.equal(thisArg); 154 | const ref = item.toValue(); 155 | return ref === 'a' || ref === 1; 156 | }, thisArg); 157 | expect(newArray.toValue()).to.deep.equal([true, null]); 158 | }); 159 | }); 160 | 161 | describe('#reduce', () => { 162 | const numbers = new ArrayElement([1, 2, 3, 4]); 163 | 164 | it('sends index and array elements', () => { 165 | const sum = numbers.reduce((result, item, index, array) => { 166 | expect(index).to.be.below(numbers.length); 167 | expect(array).to.equal(numbers); 168 | 169 | return result.toValue() + index; 170 | }, 0); 171 | 172 | // Sum of indexes should be 0 + 1 + 2 + 3 = 6 173 | expect(sum.toValue()).to.equal(6); 174 | }); 175 | 176 | context('when no beginning value is given', () => { 177 | it('correctly reduces the array', () => { 178 | const total = numbers.reduce((result, item) => result.toValue() + item.toValue()); 179 | expect(total.toValue()).to.equal(10); 180 | }); 181 | }); 182 | 183 | context('when a beginning value is given', () => { 184 | it('correctly reduces the array', () => { 185 | const total = numbers.reduce((result, item) => result.toValue() + item.toValue(), 20); 186 | expect(total.toValue()).to.equal(30); 187 | }); 188 | }); 189 | }); 190 | 191 | describe('#forEach', () => { 192 | it('iterates over each item', () => { 193 | const indexes = []; 194 | const results = []; 195 | 196 | arrayElement.forEach(function forEach(item, index) { 197 | indexes.push(index.toValue()); 198 | results.push(item); 199 | expect(this).to.deep.equal(thisArg); 200 | }, thisArg); 201 | 202 | expect(results.length).to.equal(4); 203 | expect(indexes).to.deep.equal([0, 1, 2, 3]); 204 | }); 205 | }); 206 | 207 | describe('#length', () => { 208 | it('returns the length of the content', () => { 209 | expect(arrayElement.length).to.equal(4); 210 | }); 211 | }); 212 | 213 | describe('#isEmpty', () => { 214 | it('returns empty when there are no elements', () => { 215 | expect(new ArrayElement().isEmpty).to.be.true; 216 | }); 217 | 218 | it('returns non empty when there are elements', () => { 219 | expect(arrayElement.isEmpty).to.be.false; 220 | }); 221 | }); 222 | 223 | describe('#remove', () => { 224 | it('removes the specified item', () => { 225 | const removed = arrayElement.remove(0); 226 | 227 | expect(removed.toValue()).to.equal('a'); 228 | expect(arrayElement.length).to.equal(3); 229 | }); 230 | 231 | it('removing unknown item', () => { 232 | const removed = arrayElement.remove(10); 233 | 234 | expect(removed).to.be.null; 235 | }); 236 | }); 237 | 238 | function itAddsToArray(instance) { 239 | expect(instance.length).to.equal(5); 240 | expect(instance.get(4).toValue()).to.equal('foobar'); 241 | } 242 | 243 | describe('#shift', () => { 244 | it('removes an item from the start of an array', () => { 245 | const shifted = arrayElement.shift(); 246 | expect(arrayElement.length).to.equal(3); 247 | expect(shifted.toValue()).to.equal('a'); 248 | }); 249 | }); 250 | 251 | describe('#unshift', () => { 252 | it('adds a new item to the start of the array', () => { 253 | arrayElement.unshift('foobar'); 254 | expect(arrayElement.length).to.equal(5); 255 | expect(arrayElement.get(0).toValue()).to.equal('foobar'); 256 | }); 257 | }); 258 | 259 | describe('#push', () => { 260 | it('adds a new item to the end of the array', () => { 261 | arrayElement.push('foobar'); 262 | itAddsToArray(arrayElement); 263 | }); 264 | }); 265 | 266 | describe('#add', () => { 267 | it('adds a new item to the array', () => { 268 | arrayElement.add('foobar'); 269 | itAddsToArray(arrayElement); 270 | }); 271 | }); 272 | 273 | if (typeof Symbol !== 'undefined') { 274 | describe('#[Symbol.iterator]', () => { 275 | it('can be used in a for ... of loop', () => { 276 | // We know the runtime supports Symbol but don't know if it supports 277 | // the actual `for ... of` loop syntax. Here we simulate it by 278 | // directly manipulating the iterator the same way that `for ... of` 279 | // does. 280 | const items = []; 281 | const iterator = arrayElement[Symbol.iterator](); 282 | let result; 283 | 284 | do { 285 | result = iterator.next(); 286 | 287 | if (!result.done) { 288 | items.push(result.value); 289 | } 290 | } while (!result.done); 291 | 292 | expect(items).to.have.length(4); 293 | }); 294 | }); 295 | } 296 | }); 297 | 298 | describe('searching', () => { 299 | const refract = { 300 | element: 'array', 301 | content: [ 302 | { 303 | element: 'string', 304 | content: 'foobar', 305 | }, { 306 | element: 'string', 307 | content: 'hello world', 308 | }, { 309 | element: 'array', 310 | content: [ 311 | { 312 | element: 'string', 313 | meta: { 314 | classes: { 315 | element: 'array', 316 | content: [ 317 | { 318 | element: 'string', 319 | content: 'test-class', 320 | }, 321 | ], 322 | }, 323 | }, 324 | content: 'baz', 325 | }, { 326 | element: 'boolean', 327 | content: true, 328 | }, { 329 | element: 'array', 330 | content: [ 331 | { 332 | element: 'string', 333 | meta: { 334 | id: { 335 | element: 'string', 336 | content: 'nested-id', 337 | }, 338 | }, 339 | content: 'bar', 340 | }, { 341 | element: 'number', 342 | content: 4, 343 | }, 344 | ], 345 | }, 346 | ], 347 | }, 348 | ], 349 | }; 350 | 351 | let doc; 352 | let strings; 353 | let recursiveStrings; 354 | 355 | before(() => { 356 | doc = minim.fromRefract(refract); 357 | strings = doc.children.filter(el => el.element === 'string'); 358 | recursiveStrings = doc.find(el => el.element === 'string'); 359 | }); 360 | 361 | describe('#children', () => { 362 | it('returns the correct number of items', () => { 363 | expect(strings.length).to.equal(2); 364 | }); 365 | 366 | it('returns the correct values', () => { 367 | expect(strings.toValue()).to.deep.equal(['foobar', 'hello world']); 368 | }); 369 | }); 370 | 371 | describe('#find', () => { 372 | it('returns the correct number of items', () => { 373 | expect(recursiveStrings).to.have.lengthOf(4); 374 | }); 375 | 376 | it('returns the correct values', () => { 377 | expect(recursiveStrings.toValue()).to.deep.equal(['foobar', 'hello world', 'baz', 'bar']); 378 | }); 379 | }); 380 | 381 | describe('#findByElement', () => { 382 | let items; 383 | 384 | before(() => { 385 | items = doc.findByElement('number'); 386 | }); 387 | 388 | it('returns the correct number of items', () => { 389 | expect(items).to.have.lengthOf(1); 390 | }); 391 | 392 | it('returns the correct values', () => { 393 | expect(items.toValue()).to.deep.equal([4]); 394 | }); 395 | }); 396 | 397 | describe('#findByClass', () => { 398 | let items; 399 | 400 | before(() => { 401 | items = doc.findByClass('test-class'); 402 | }); 403 | 404 | it('returns the correct number of items', () => { 405 | expect(items).to.have.lengthOf(1); 406 | }); 407 | 408 | it('returns the correct values', () => { 409 | expect(items.toValue()).to.deep.equal(['baz']); 410 | }); 411 | }); 412 | 413 | describe('#first', () => { 414 | it('returns the first item', () => { 415 | expect(doc.first).to.deep.equal(doc.content[0]); 416 | }); 417 | }); 418 | 419 | describe('#second', () => { 420 | it('returns the second item', () => { 421 | expect(doc.second).to.deep.equal(doc.content[1]); 422 | }); 423 | }); 424 | 425 | describe('#last', () => { 426 | it('returns the last item', () => { 427 | expect(doc.last).to.deep.equal(doc.content[2]); 428 | }); 429 | }); 430 | 431 | describe('#getById', () => { 432 | it('returns the item for the ID given', () => { 433 | expect(doc.getById('nested-id').toValue()).to.equal('bar'); 434 | }); 435 | }); 436 | 437 | describe('#includes', () => { 438 | it('uses deep equality', () => { 439 | expect(doc.get(2).includes(['not', 'there'])).to.be.false; 440 | expect(doc.get(2).includes(['bar', 4])).to.be.true; 441 | }); 442 | 443 | context('when given a value that is in the array', () => { 444 | it('returns true', () => { 445 | expect(doc.includes('foobar')).to.be.true; 446 | }); 447 | }); 448 | 449 | context('when given a value that is not in the array', () => { 450 | it('returns false', () => { 451 | expect(doc.includes('not-there')).to.be.false; 452 | }); 453 | }); 454 | }); 455 | 456 | // Deprecated functionality 457 | describe('#contains', () => { 458 | it('uses deep equality', () => { 459 | expect(doc.get(2).contains(['not', 'there'])).to.be.false; 460 | expect(doc.get(2).contains(['bar', 4])).to.be.true; 461 | }); 462 | 463 | context('when given a value that is in the array', () => { 464 | it('returns true', () => { 465 | expect(doc.contains('foobar')).to.be.true; 466 | }); 467 | }); 468 | 469 | context('when given a value that is not in the array', () => { 470 | it('returns false', () => { 471 | expect(doc.contains('not-there')).to.be.false; 472 | }); 473 | }); 474 | }); 475 | }); 476 | }); 477 | -------------------------------------------------------------------------------- /test/primitives/BooleanElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const BooleanElement = minim.getElementClass('boolean'); 5 | 6 | describe('BooleanElement', () => { 7 | let booleanElement; 8 | 9 | beforeEach(() => { 10 | booleanElement = new BooleanElement(true); 11 | }); 12 | 13 | describe('#element', () => { 14 | it('is a boolean', () => { 15 | expect(booleanElement.element).to.equal('boolean'); 16 | }); 17 | }); 18 | 19 | describe('#primitive', () => { 20 | it('returns boolean as the Refract primitive', () => { 21 | expect(booleanElement.primitive()).to.equal('boolean'); 22 | }); 23 | }); 24 | 25 | describe('#get', () => { 26 | it('returns the boolean value', () => { 27 | expect(booleanElement.toValue()).to.equal(true); 28 | }); 29 | }); 30 | 31 | describe('#set', () => { 32 | it('sets the value of the boolean', () => { 33 | booleanElement.set(false); 34 | expect(booleanElement.toValue()).to.equal(false); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/primitives/MemberElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const MemberElement = minim.getElementClass('member'); 5 | 6 | describe('MemberElement', () => { 7 | const member = new MemberElement('foo', 'bar', {}, { foo: 'bar' }); 8 | 9 | context('key', () => { 10 | it('provides the set key', () => { 11 | expect(member.key.toValue()).to.equal('foo'); 12 | }); 13 | 14 | it('sets the key', () => { 15 | member.key = 'updated'; 16 | expect(member.key.toValue()).to.equal('updated'); 17 | }); 18 | }); 19 | 20 | context('value', () => { 21 | it('provides the set value', () => { 22 | expect(member.value.toValue()).to.equal('bar'); 23 | }); 24 | 25 | it('sets the key', () => { 26 | member.value = 'updated'; 27 | expect(member.value.toValue()).to.equal('updated'); 28 | }); 29 | }); 30 | 31 | it('correctly sets the attributes', () => { 32 | expect(member.attributes.get('foo').toValue()).to.equal('bar'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/primitives/NullElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const NullElement = minim.getElementClass('null'); 5 | 6 | describe('NullElement', () => { 7 | let nullElement; 8 | 9 | before(() => { 10 | nullElement = new NullElement(); 11 | }); 12 | 13 | describe('#element', () => { 14 | it('is null', () => { 15 | expect(nullElement.element).to.equal('null'); 16 | }); 17 | }); 18 | 19 | describe('#primitive', () => { 20 | it('returns null as the Refract primitive', () => { 21 | expect(nullElement.primitive()).to.equal('null'); 22 | }); 23 | }); 24 | 25 | describe('#get', () => { 26 | it('returns the null value', () => { 27 | expect(nullElement.toValue()).to.equal(null); 28 | }); 29 | }); 30 | 31 | describe('#set', () => { 32 | it('cannot set the value', () => { 33 | expect(nullElement.set('foobar')).to.be.an.instanceof(Error); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/primitives/NumberElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const NumberElement = minim.getElementClass('number'); 5 | 6 | describe('NumberElement', () => { 7 | let numberElement; 8 | 9 | before(() => { 10 | numberElement = new NumberElement(4); 11 | }); 12 | 13 | describe('#element', () => { 14 | it('is a number', () => { 15 | expect(numberElement.element).to.equal('number'); 16 | }); 17 | }); 18 | 19 | describe('#primitive', () => { 20 | it('returns number as the Refract primitive', () => { 21 | expect(numberElement.primitive()).to.equal('number'); 22 | }); 23 | }); 24 | 25 | describe('#get', () => { 26 | it('returns the number value', () => { 27 | expect(numberElement.toValue()).to.equal(4); 28 | }); 29 | }); 30 | 31 | describe('#set', () => { 32 | it('sets the value of the number', () => { 33 | numberElement.set(10); 34 | expect(numberElement.toValue()).to.equal(10); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/primitives/ObjectElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const ObjectElement = minim.getElementClass('object'); 5 | const StringElement = minim.getElementClass('string'); 6 | 7 | describe('ObjectElement', () => { 8 | const thisArg = { message: 42 }; 9 | 10 | let objectElement; 11 | 12 | function setObject() { 13 | objectElement = new ObjectElement({ 14 | foo: 'bar', 15 | z: 1, 16 | }); 17 | } 18 | 19 | before(() => { 20 | setObject(); 21 | }); 22 | 23 | beforeEach(() => { 24 | setObject(); 25 | }); 26 | 27 | describe('.content', () => { 28 | let correctElementNames; 29 | let storedElementNames; 30 | 31 | before(() => { 32 | correctElementNames = ['string', 'number']; 33 | storedElementNames = objectElement.content.map(el => el.value.element); 34 | }); 35 | 36 | it('has the correct element names', () => { 37 | expect(storedElementNames).to.deep.equal(correctElementNames); 38 | }); 39 | }); 40 | 41 | describe('#element', () => { 42 | it('is a string element', () => { 43 | expect(objectElement.element).to.equal('object'); 44 | }); 45 | }); 46 | 47 | describe('#primitive', () => { 48 | it('returns object as the Refract primitive', () => { 49 | expect(objectElement.primitive()).to.equal('object'); 50 | }); 51 | }); 52 | 53 | describe('#toValue', () => { 54 | it('returns the object', () => { 55 | expect(objectElement.toValue()).to.deep.equal({ 56 | foo: 'bar', 57 | z: 1, 58 | }); 59 | }); 60 | 61 | it('returns undefined with member does not have a value', () => { 62 | const element = new ObjectElement(); 63 | element.set('name', undefined); 64 | 65 | const value = element.toValue(); 66 | expect(value.name).to.be.undefined; 67 | }); 68 | }); 69 | 70 | describe('#get', () => { 71 | context('when a property name is given', () => { 72 | it('returns the value of the name given', () => { 73 | expect(objectElement.get('foo').toValue()).to.equal('bar'); 74 | }); 75 | }); 76 | 77 | context('when a property name is not given', () => { 78 | it('is undefined', () => { 79 | expect(objectElement.get()).to.be.undefined; 80 | }); 81 | }); 82 | }); 83 | 84 | describe('#getValue', () => { 85 | context('when a property name is given', () => { 86 | it('returns the value of the name given', () => { 87 | expect(objectElement.getValue('foo')).to.equal('bar'); 88 | }); 89 | }); 90 | 91 | context('when a property name is not given', () => { 92 | it('is undefined', () => { 93 | expect(objectElement.getValue()).to.be.undefined; 94 | }); 95 | }); 96 | }); 97 | 98 | describe('#getMember', () => { 99 | context('when a property name is given', () => { 100 | it('returns the correct member object', () => { 101 | expect(objectElement.getMember('foo').value.toValue()).to.equal('bar'); 102 | }); 103 | }); 104 | 105 | context('when a property name is not given', () => { 106 | it('is undefined', () => { 107 | expect(objectElement.getMember()).to.be.undefined; 108 | }); 109 | }); 110 | }); 111 | 112 | describe('#getKey', () => { 113 | context('when a property name is given', () => { 114 | it('returns the correct key object', () => { 115 | expect(objectElement.getKey('foo').toValue()).to.equal('foo'); 116 | }); 117 | }); 118 | 119 | context('when a property name given that does not exist', () => { 120 | it('returns undefined', () => { 121 | expect(objectElement.getKey('not-defined')).to.be.undefined; 122 | }); 123 | }); 124 | 125 | context('when a property name is not given', () => { 126 | it('returns undefined', () => { 127 | expect(objectElement.getKey()).to.be.undefined; 128 | }); 129 | }); 130 | }); 131 | 132 | describe('#set', () => { 133 | it('sets the value of the name given', () => { 134 | expect(objectElement.get('foo').toValue()).to.equal('bar'); 135 | objectElement.set('foo', 'hello world'); 136 | expect(objectElement.get('foo').toValue()).to.equal('hello world'); 137 | }); 138 | 139 | it('sets a value that has not been defined yet', () => { 140 | objectElement.set('bar', 'hello world'); 141 | expect(objectElement.get('bar').toValue()).to.equal('hello world'); 142 | }); 143 | 144 | it('accepts an object', () => { 145 | const obj = new ObjectElement(); 146 | obj.set({ foo: 'bar' }); 147 | expect(obj.get('foo').toValue()).to.equal('bar'); 148 | }); 149 | 150 | it('should refract key and value from object', () => { 151 | const obj = new ObjectElement(); 152 | obj.set('key', 'value'); 153 | const member = obj.getMember('key'); 154 | 155 | expect(member.key).to.be.instanceof(StringElement); 156 | expect(member.value).to.be.instanceof(StringElement); 157 | }); 158 | }); 159 | 160 | describe('#keys', () => { 161 | it('gets the keys of all properties', () => { 162 | expect(objectElement.keys()).to.deep.equal(['foo', 'z']); 163 | }); 164 | }); 165 | 166 | describe('#remove', () => { 167 | it('removes the given key', () => { 168 | const removed = objectElement.remove('z'); 169 | 170 | expect(removed.toValue()).to.deep.equal({ key: 'z', value: 1 }); 171 | expect(objectElement.keys()).to.deep.equal(['foo']); 172 | }); 173 | }); 174 | 175 | describe('#remove non-existing item', () => { 176 | it('should not change the object element', () => { 177 | const removed = objectElement.remove('k'); 178 | 179 | expect(removed).to.deep.equal(null); 180 | expect(objectElement.keys()).to.deep.equal(['foo', 'z']); 181 | }); 182 | }); 183 | 184 | describe('#values', () => { 185 | it('gets the values of all properties', () => { 186 | expect(objectElement.values()).to.deep.equal(['bar', 1]); 187 | }); 188 | }); 189 | 190 | describe('#hasKey', () => { 191 | it('checks to see if a key exists', () => { 192 | expect(objectElement.hasKey('foo')).to.be.true; 193 | expect(objectElement.hasKey('does-not-exist')).to.be.false; 194 | }); 195 | }); 196 | 197 | describe('#items', () => { 198 | it('provides a list of name/value pairs to iterate', () => { 199 | const keys = []; 200 | const values = []; 201 | 202 | objectElement.items().forEach((item) => { 203 | const key = item[0]; 204 | const value = item[1]; 205 | 206 | keys.push(key); 207 | values.push(value); 208 | }); 209 | 210 | expect(keys).to.have.members(['foo', 'z']); 211 | expect(values).to.have.length(2); 212 | }); 213 | }); 214 | 215 | function itHascollectionMethod(method) { 216 | describe(`#${method}`, () => { 217 | it(`responds to #${method}`, () => { 218 | expect(objectElement).to.respondTo(method); 219 | }); 220 | }); 221 | } 222 | 223 | itHascollectionMethod('map'); 224 | itHascollectionMethod('filter'); 225 | itHascollectionMethod('forEach'); 226 | itHascollectionMethod('push'); 227 | itHascollectionMethod('add'); 228 | 229 | describe('#map', () => { 230 | it('provides the keys', () => { 231 | const keys = objectElement.map((value, key) => key.toValue()); 232 | expect(keys).to.deep.equal(['foo', 'z']); 233 | }); 234 | 235 | it('provides the values', () => { 236 | const values = objectElement.map(value => value.toValue()); 237 | expect(values).to.deep.equal(['bar', 1]); 238 | }); 239 | 240 | it('provides the members', () => { 241 | const keys = objectElement.map((value, key, member) => member.key.toValue()); 242 | expect(keys).to.deep.equal(['foo', 'z']); 243 | }); 244 | 245 | it('provides thisArg as this to callback', () => { 246 | const keys = objectElement.map(function map(value, key) { 247 | expect(this).to.deep.equal(thisArg); 248 | return key.toValue(); 249 | }, thisArg); 250 | expect(keys).to.deep.equal(['foo', 'z']); 251 | }); 252 | }); 253 | 254 | describe('#compactMap', () => { 255 | it('provides the keys', () => { 256 | const keys = objectElement.compactMap((value, key) => { 257 | if (key.toValue() === 'foo') { 258 | return key.toValue(); 259 | } 260 | 261 | return undefined; 262 | }); 263 | expect(keys).to.deep.equal(['foo']); 264 | }); 265 | 266 | it('provides the values', () => { 267 | const values = objectElement.compactMap((value, key) => { 268 | if (key.toValue() === 'foo') { 269 | return value.toValue(); 270 | } 271 | 272 | return undefined; 273 | }); 274 | expect(values).to.deep.equal(['bar']); 275 | }); 276 | 277 | it('provides the members', () => { 278 | const keys = objectElement.compactMap((value, key, member) => { 279 | if (key.toValue() === 'foo') { 280 | return member.key.toValue(); 281 | } 282 | 283 | return undefined; 284 | }); 285 | expect(keys).to.deep.equal(['foo']); 286 | }); 287 | 288 | it('provides thisArg as this to callback', () => { 289 | const keys = objectElement.compactMap(function compactMap(value, key) { 290 | expect(this).to.deep.equal(thisArg); 291 | if (key.toValue() === 'foo') { 292 | return key.toValue(); 293 | } 294 | 295 | return undefined; 296 | }, thisArg); 297 | expect(keys).to.deep.equal(['foo']); 298 | }); 299 | }); 300 | 301 | describe('#filter', () => { 302 | it('allows for filtering on keys', () => { 303 | const foo = objectElement.filter((value, key) => key.equals('foo')); 304 | expect(foo.keys()).to.deep.equal(['foo']); 305 | }); 306 | 307 | it('allows for filtering on values', () => { 308 | const foo = objectElement.filter(value => value.equals('bar')); 309 | expect(foo.keys()).to.deep.equal(['foo']); 310 | }); 311 | 312 | it('allows for filtering on members', () => { 313 | const foo = objectElement.filter((value, key, member) => member.value.equals('bar')); 314 | expect(foo.keys()).to.deep.equal(['foo']); 315 | }); 316 | 317 | it('provides thisArg as this to callback', () => { 318 | const foo = objectElement.filter(function filter(value, key) { 319 | expect(this).to.deep.equal(thisArg); 320 | return key.equals('foo'); 321 | }, thisArg); 322 | expect(foo.keys()).to.deep.equal(['foo']); 323 | }); 324 | }); 325 | 326 | describe('#reject', () => { 327 | it('allows for rejecting on keys', () => { 328 | const foo = objectElement.reject((value, key) => key.equals('foo')); 329 | expect(foo.keys()).to.deep.equal(['z']); 330 | }); 331 | 332 | it('allows for rejecting on values', () => { 333 | const foo = objectElement.reject(value => value.equals('bar')); 334 | expect(foo.keys()).to.deep.equal(['z']); 335 | }); 336 | 337 | it('allows for rejecting on members', () => { 338 | const foo = objectElement.reject((value, key, member) => member.value.equals('bar')); 339 | expect(foo.keys()).to.deep.equal(['z']); 340 | }); 341 | 342 | it('provides thisArg as this to callback', () => { 343 | const foo = objectElement.reject(function filter(value, key) { 344 | expect(this).to.deep.equal(thisArg); 345 | return key.equals('foo'); 346 | }, thisArg); 347 | expect(foo.keys()).to.deep.equal(['z']); 348 | }); 349 | }); 350 | 351 | describe('#reduce', () => { 352 | const numbers = new ObjectElement({ 353 | a: 1, 354 | b: 2, 355 | c: 3, 356 | d: 4, 357 | }); 358 | 359 | it('allows for reducing on keys', () => { 360 | const letters = numbers.reduce((result, item, key) => result.push(key), []); 361 | expect(letters.toValue()).to.deep.equal(['a', 'b', 'c', 'd']); 362 | }); 363 | 364 | it('sends member and object elements', () => { 365 | numbers.reduce((result, item, key, member, obj) => { 366 | expect(obj.content).to.contain(member); 367 | expect(obj).to.equal(numbers); 368 | }); 369 | }); 370 | 371 | context('when no beginning value is given', () => { 372 | it('correctly reduces the object', () => { 373 | const total = numbers.reduce((result, item) => result.toValue() + item.toValue()); 374 | expect(total.toValue()).to.equal(10); 375 | }); 376 | }); 377 | 378 | context('when a beginning value is given', () => { 379 | it('correctly reduces the object', () => { 380 | const total = numbers.reduce((result, item) => result.toValue() + item.toValue(), 20); 381 | expect(total.toValue()).to.equal(30); 382 | }); 383 | }); 384 | }); 385 | 386 | describe('#forEach', () => { 387 | it('provides the keys', () => { 388 | const keys = []; 389 | objectElement.forEach((value, key) => keys.push(key.toValue())); 390 | expect(keys).to.deep.equal(['foo', 'z']); 391 | }); 392 | 393 | it('provides the values', () => { 394 | const values = []; 395 | objectElement.forEach(value => values.push(value.toValue())); 396 | expect(values).to.deep.equal(['bar', 1]); 397 | }); 398 | 399 | it('provides the members', () => { 400 | const keys = []; 401 | objectElement.forEach((value, key, member) => keys.push(member.key.toValue())); 402 | expect(keys).to.deep.equal(['foo', 'z']); 403 | }); 404 | 405 | it('provides thisArg as this to callback', () => { 406 | const keys = []; 407 | 408 | objectElement.forEach(function forEach(value, key) { 409 | expect(this).to.deep.equal(thisArg); 410 | return keys.push(key.toValue()); 411 | }, thisArg); 412 | 413 | expect(keys).to.deep.equal(['foo', 'z']); 414 | }); 415 | }); 416 | 417 | describe('#find', () => { 418 | it('allows for searching based on the keys', () => { 419 | const search = objectElement.find((value, key) => key.toValue() === 'z'); 420 | expect(search.toValue()).to.deep.equal([1]); 421 | }); 422 | 423 | it('allows for searching based on the member', () => { 424 | const search = objectElement.find((value, key, member) => member.key.toValue() === 'z'); 425 | expect(search.toValue()).to.deep.equal([1]); 426 | }); 427 | }); 428 | 429 | // describe('#[Symbol.iterator]', function() { 430 | // it('can be used in a for ... of loop', function() { 431 | // var items = []; 432 | // for (let item of objectElement) { 433 | // items.push(item); 434 | // } 435 | // 436 | // expect(items).to.have.length(2); 437 | // }); 438 | // }); 439 | }); 440 | -------------------------------------------------------------------------------- /test/primitives/StringElement-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const minim = require('../../lib/minim').namespace(); 3 | 4 | const StringElement = minim.getElementClass('string'); 5 | 6 | describe('StringElement', () => { 7 | let stringElement; 8 | 9 | before(() => { 10 | stringElement = new StringElement('foobar'); 11 | }); 12 | 13 | describe('#element', () => { 14 | it('is a string', () => { 15 | expect(stringElement.element).to.equal('string'); 16 | }); 17 | }); 18 | 19 | describe('#primitive', () => { 20 | it('returns string as the Refract primitive', () => { 21 | expect(stringElement.primitive()).to.equal('string'); 22 | }); 23 | }); 24 | 25 | describe('#get', () => { 26 | it('returns the string value', () => { 27 | expect(stringElement.toValue()).to.equal('foobar'); 28 | }); 29 | }); 30 | 31 | describe('#set', () => { 32 | it('sets the value of the string', () => { 33 | stringElement.set('hello world'); 34 | expect(stringElement.toValue()).to.equal('hello world'); 35 | }); 36 | }); 37 | 38 | describe('#length', () => { 39 | it('returns the length of the string', () => { 40 | expect(stringElement.length).to.equal(11); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/refract-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('./spec-helper'); 2 | const minim = require('../lib/minim'); 3 | const { refract } = require('../lib/minim'); 4 | 5 | describe('refract', () => { 6 | it('returns any given element without refracting', () => { 7 | const element = new minim.StringElement('hello'); 8 | const refracted = refract(element); 9 | 10 | expect(refracted).to.equal(element); 11 | }); 12 | 13 | it('can refract a string into a string element', () => { 14 | const element = refract('Hello'); 15 | 16 | expect(element).to.be.instanceof(minim.StringElement); 17 | expect(element.content).to.equal('Hello'); 18 | }); 19 | 20 | it('can refract a number into a number element', () => { 21 | const element = refract(1); 22 | 23 | expect(element).to.be.instanceof(minim.NumberElement); 24 | expect(element.content).to.equal(1); 25 | }); 26 | 27 | it('can refract a boolean into a boolean element', () => { 28 | const element = refract(true); 29 | 30 | expect(element).to.be.instanceof(minim.BooleanElement); 31 | expect(element.content).to.equal(true); 32 | }); 33 | 34 | it('can refract a null value into a null element', () => { 35 | const element = refract(null); 36 | 37 | expect(element).to.be.instanceof(minim.NullElement); 38 | expect(element.content).to.equal(null); 39 | }); 40 | 41 | it('can refract an array of values into an array element', () => { 42 | const element = refract(['Hi', 1]); 43 | 44 | expect(element).to.be.instanceof(minim.ArrayElement); 45 | expect(element.length).to.be.equal(2); 46 | expect(element.get(0)).to.be.instanceof(minim.StringElement); 47 | expect(element.get(0).content).to.equal('Hi'); 48 | expect(element.get(1)).to.be.instanceof(minim.NumberElement); 49 | expect(element.get(1).content).to.equal(1); 50 | }); 51 | 52 | it('can refract an object into an object element', () => { 53 | const element = refract({ name: 'Doe' }); 54 | 55 | expect(element).to.be.instanceof(minim.ObjectElement); 56 | expect(element.length).to.equal(1); 57 | 58 | const member = element.content[0]; 59 | expect(member).to.be.instanceof(minim.MemberElement); 60 | expect(member.key).to.be.instanceof(minim.StringElement); 61 | expect(member.key.content).to.equal('name'); 62 | expect(member.value).to.be.instanceof(minim.StringElement); 63 | expect(member.value.content).to.equal('Doe'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/serialisers/JSONSerialiser-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../spec-helper'); 2 | const { Namespace } = require('../../lib/minim'); 3 | const minim = require('../../lib/minim').namespace(); 4 | const KeyValuePair = require('../../lib/KeyValuePair'); 5 | const JSONSerialiser = require('../../lib/serialisers/JSONSerialiser'); 6 | 7 | describe('JSON Serialiser', () => { 8 | let serialiser; 9 | 10 | beforeEach(() => { 11 | serialiser = new JSONSerialiser(minim); 12 | }); 13 | 14 | describe('initialisation', () => { 15 | it('uses given namespace', () => { 16 | expect(serialiser.namespace).to.equal(minim); 17 | }); 18 | 19 | it('creates a default namespace when no namespace is given', () => { 20 | serialiser = new JSONSerialiser(); 21 | expect(serialiser.namespace).to.be.instanceof(Namespace); 22 | }); 23 | }); 24 | 25 | describe('serialisation', () => { 26 | describe('#serialiseObject', () => { 27 | it('can serialise an ObjectElement', () => { 28 | const object = new minim.elements.Object({ id: 'Example' }); 29 | const result = serialiser.serialiseObject(object); 30 | 31 | expect(result).to.deep.equal({ 32 | id: { 33 | element: 'string', 34 | content: 'Example', 35 | }, 36 | }); 37 | }); 38 | 39 | it('can serialise an ObjectElement containg undefined key', () => { 40 | const object = new minim.elements.Object({ key: undefined }); 41 | const result = serialiser.serialiseObject(object); 42 | 43 | expect(result).to.deep.equal(undefined); 44 | }); 45 | }); 46 | 47 | it('errors when serialising a non-element', () => { 48 | expect(() => { 49 | serialiser.serialise('Hello'); 50 | }).to.throw(TypeError, 'Given element `Hello` is not an Element instance'); 51 | }); 52 | 53 | it('serialises a primitive element', () => { 54 | const element = new minim.elements.String('Hello'); 55 | const object = serialiser.serialise(element); 56 | 57 | expect(object).to.deep.equal({ 58 | element: 'string', 59 | content: 'Hello', 60 | }); 61 | }); 62 | 63 | it('serialises an element containing element', () => { 64 | const string = new minim.elements.String('Hello'); 65 | const element = new minim.Element(string); 66 | element.element = 'custom'; 67 | 68 | const object = serialiser.serialise(element); 69 | 70 | expect(object).to.deep.equal({ 71 | element: 'custom', 72 | content: { 73 | element: 'string', 74 | content: 'Hello', 75 | }, 76 | }); 77 | }); 78 | 79 | it('serialises an element containing element array', () => { 80 | const string = new minim.elements.String('Hello'); 81 | const element = new minim.elements.Array([string]); 82 | 83 | const object = serialiser.serialise(element); 84 | 85 | expect(object).to.deep.equal({ 86 | element: 'array', 87 | content: [ 88 | { 89 | element: 'string', 90 | content: 'Hello', 91 | }, 92 | ], 93 | }); 94 | }); 95 | 96 | it('serialises an element containing an empty array', () => { 97 | const element = new minim.elements.Array(); 98 | 99 | const object = serialiser.serialise(element); 100 | 101 | expect(object).to.deep.equal({ 102 | element: 'array', 103 | }); 104 | }); 105 | 106 | it('serialises an element containing a pair', () => { 107 | const name = new minim.elements.String('name'); 108 | const doe = new minim.elements.String('Doe'); 109 | const element = new minim.elements.Member(name, doe); 110 | 111 | const object = serialiser.serialise(element); 112 | 113 | expect(object).to.deep.equal({ 114 | element: 'member', 115 | content: { 116 | key: { 117 | element: 'string', 118 | content: 'name', 119 | }, 120 | value: { 121 | element: 'string', 122 | content: 'Doe', 123 | }, 124 | }, 125 | }); 126 | }); 127 | 128 | it('serialises an element containing a pair without a value', () => { 129 | const name = new minim.elements.String('name'); 130 | const element = new minim.elements.Member(name); 131 | 132 | const object = serialiser.serialise(element); 133 | 134 | expect(object).to.deep.equal({ 135 | element: 'member', 136 | content: { 137 | key: { 138 | element: 'string', 139 | content: 'name', 140 | }, 141 | }, 142 | }); 143 | }); 144 | 145 | it('serialises an elements meta', () => { 146 | const doe = new minim.elements.String('Doe'); 147 | doe.title = 'Name'; 148 | 149 | const object = serialiser.serialise(doe); 150 | 151 | expect(object).to.deep.equal({ 152 | element: 'string', 153 | meta: { 154 | title: { 155 | element: 'string', 156 | content: 'Name', 157 | }, 158 | }, 159 | content: 'Doe', 160 | }); 161 | }); 162 | 163 | it('serialises an elements attributes', () => { 164 | const element = new minim.elements.String('Hello World'); 165 | element.attributes.set('thread', 123); 166 | 167 | const object = serialiser.serialise(element); 168 | 169 | expect(object).to.deep.equal({ 170 | element: 'string', 171 | attributes: { 172 | thread: { 173 | element: 'number', 174 | content: 123, 175 | }, 176 | }, 177 | content: 'Hello World', 178 | }); 179 | }); 180 | 181 | it('serialises an element with custom element attributes', () => { 182 | const element = new minim.elements.String('Hello World'); 183 | element.attributes.set('thread', new minim.Element(123)); 184 | 185 | const object = serialiser.serialise(element); 186 | 187 | expect(object).to.deep.equal({ 188 | element: 'string', 189 | attributes: { 190 | thread: { 191 | element: 'element', 192 | content: 123, 193 | }, 194 | }, 195 | content: 'Hello World', 196 | }); 197 | }); 198 | }); 199 | 200 | describe('deserialisation', () => { 201 | it('errors when deserialising value without element name', () => { 202 | expect(() => serialiser.deserialise({})).to.throw(); 203 | }); 204 | 205 | it('deserialise from a JSON object', () => { 206 | const element = serialiser.deserialise({ 207 | element: 'string', 208 | content: 'Hello', 209 | }); 210 | 211 | expect(element).to.be.instanceof(minim.elements.String); 212 | expect(element.content).to.equal('Hello'); 213 | }); 214 | 215 | it('deserialise from a JSON object containing an sub-element', () => { 216 | const element = serialiser.deserialise({ 217 | element: 'custom', 218 | content: { 219 | element: 'string', 220 | content: 'Hello', 221 | }, 222 | }); 223 | 224 | expect(element).to.be.instanceof(minim.Element); 225 | expect(element.content).to.be.instanceof(minim.elements.String); 226 | expect(element.content.content).to.equal('Hello'); 227 | }); 228 | 229 | it('deserialise from a JSON object containing an array of elements', () => { 230 | const element = serialiser.deserialise({ 231 | element: 'array', 232 | content: [ 233 | { 234 | element: 'string', 235 | content: 'Hello', 236 | }, 237 | ], 238 | }); 239 | 240 | expect(element).to.be.instanceof(minim.elements.Array); 241 | expect(element.content[0]).to.be.instanceof(minim.elements.String); 242 | expect(element.content[0].content).to.equal('Hello'); 243 | }); 244 | 245 | it('deserialise from a JSON object containing a key-value pair', () => { 246 | const element = serialiser.deserialise({ 247 | element: 'member', 248 | content: { 249 | key: { 250 | element: 'string', 251 | content: 'name', 252 | }, 253 | value: { 254 | element: 'string', 255 | content: 'Doe', 256 | }, 257 | }, 258 | }); 259 | 260 | expect(element).to.be.instanceof(minim.elements.Member); 261 | expect(element.content).to.be.instanceof(KeyValuePair); 262 | expect(element.key).to.be.instanceof(minim.elements.String); 263 | expect(element.key.content).to.equal('name'); 264 | expect(element.value).to.be.instanceof(minim.elements.String); 265 | expect(element.value.content).to.equal('Doe'); 266 | }); 267 | 268 | it('deserialise from a JSON object containing a key-value pair without value', () => { 269 | const element = serialiser.deserialise({ 270 | element: 'member', 271 | content: { 272 | key: { 273 | element: 'string', 274 | content: 'name', 275 | }, 276 | }, 277 | }); 278 | 279 | expect(element).to.be.instanceof(minim.elements.Member); 280 | expect(element.content).to.be.instanceof(KeyValuePair); 281 | expect(element.key).to.be.instanceof(minim.elements.String); 282 | expect(element.key.content).to.equal('name'); 283 | expect(element.value).to.be.undefined; 284 | }); 285 | 286 | it('deserialise meta', () => { 287 | const element = serialiser.deserialise({ 288 | element: 'string', 289 | meta: { 290 | title: { 291 | element: 'string', 292 | content: 'hello', 293 | }, 294 | }, 295 | }); 296 | 297 | expect(element.title).to.be.instanceof(minim.elements.String); 298 | expect(element.title.content).to.equal('hello'); 299 | }); 300 | 301 | it('deserialise attributes', () => { 302 | const element = serialiser.deserialise({ 303 | element: 'string', 304 | attributes: { 305 | thing: { 306 | element: 'string', 307 | content: 'hello', 308 | }, 309 | }, 310 | }); 311 | 312 | const attribute = element.attributes.get('thing'); 313 | expect(attribute).to.be.instanceof(minim.elements.String); 314 | expect(attribute.content).to.equal('hello'); 315 | }); 316 | 317 | describe('deserialising base elements', () => { 318 | it('deserialise string', () => { 319 | const element = serialiser.deserialise({ 320 | element: 'string', 321 | content: 'Hello', 322 | }); 323 | 324 | expect(element).to.be.instanceof(minim.elements.String); 325 | expect(element.content).to.equal('Hello'); 326 | }); 327 | 328 | it('deserialise number', () => { 329 | const element = serialiser.deserialise({ 330 | element: 'number', 331 | content: 15, 332 | }); 333 | 334 | expect(element).to.be.instanceof(minim.elements.Number); 335 | expect(element.content).to.equal(15); 336 | }); 337 | 338 | it('deserialise boolean', () => { 339 | const element = serialiser.deserialise({ 340 | element: 'boolean', 341 | content: true, 342 | }); 343 | 344 | expect(element).to.be.instanceof(minim.elements.Boolean); 345 | expect(element.content).to.equal(true); 346 | }); 347 | 348 | it('deserialise null', () => { 349 | const element = serialiser.deserialise({ 350 | element: 'null', 351 | }); 352 | 353 | expect(element).to.be.instanceof(minim.elements.Null); 354 | }); 355 | 356 | it('deserialise an array', () => { 357 | const object = serialiser.deserialise({ 358 | element: 'array', 359 | content: [], 360 | }); 361 | 362 | expect(object).to.be.instanceof(minim.elements.Array); 363 | expect(object.content).to.deep.equal([]); 364 | }); 365 | 366 | it('deserialise an object', () => { 367 | const object = serialiser.deserialise({ 368 | element: 'object', 369 | content: [], 370 | }); 371 | 372 | expect(object).to.be.instanceof(minim.elements.Object); 373 | expect(object.content).to.deep.equal([]); 374 | }); 375 | 376 | it('deserialise string without content', () => { 377 | const element = serialiser.deserialise({ 378 | element: 'string', 379 | }); 380 | 381 | expect(element).to.be.instanceof(minim.elements.String); 382 | expect(element.content).to.be.undefined; 383 | }); 384 | 385 | it('deserialise number without content', () => { 386 | const element = serialiser.deserialise({ 387 | element: 'number', 388 | }); 389 | 390 | expect(element).to.be.instanceof(minim.elements.Number); 391 | expect(element.content).to.be.undefined; 392 | }); 393 | 394 | it('deserialise boolean without content', () => { 395 | const element = serialiser.deserialise({ 396 | element: 'boolean', 397 | }); 398 | 399 | expect(element).to.be.instanceof(minim.elements.Boolean); 400 | expect(element.content).to.be.undefined; 401 | }); 402 | 403 | it('deserialise an array', () => { 404 | const object = serialiser.deserialise({ 405 | element: 'array', 406 | }); 407 | 408 | expect(object).to.be.instanceof(minim.elements.Array); 409 | expect(object.content).to.deep.equal([]); 410 | }); 411 | 412 | it('deserialise an object without content', () => { 413 | const object = serialiser.deserialise({ 414 | element: 'object', 415 | }); 416 | 417 | expect(object).to.be.instanceof(minim.elements.Object); 418 | expect(object.content).to.deep.equal([]); 419 | }); 420 | }); 421 | }); 422 | }); 423 | -------------------------------------------------------------------------------- /test/spec-helper.js: -------------------------------------------------------------------------------- 1 | exports.chai = require('chai'); 2 | 3 | exports.expect = exports.chai.expect; 4 | -------------------------------------------------------------------------------- /test/subclass-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('./spec-helper'); 2 | const minim = require('../lib/minim').namespace(); 3 | 4 | const ArrayElement = minim.getElementClass('array'); 5 | const StringElement = minim.getElementClass('string'); 6 | 7 | describe('Minim subclasses', () => { 8 | class MyElement extends minim.elements.String { 9 | constructor(content, meta, attributes) { 10 | super(content, meta, attributes); 11 | this.element = 'myElement'; 12 | } 13 | 14 | ownMethod() { 15 | return 'It works!'; 16 | } 17 | } 18 | minim.register(MyElement); 19 | 20 | it('can extend the base element with its own method', () => { 21 | const myElement = new MyElement(); 22 | expect(myElement.ownMethod()).to.equal('It works!'); 23 | }); 24 | 25 | context('when initializing', () => { 26 | const myElement = new MyElement(); 27 | 28 | it('can overwrite the element name', () => { 29 | expect(myElement.element).to.equal('myElement'); 30 | }); 31 | 32 | it('returns the correct primitive element', () => { 33 | expect(myElement.primitive()).to.equal('string'); 34 | }); 35 | }); 36 | 37 | describe('deserializing attributes', () => { 38 | const myElement = minim.fromRefract({ 39 | element: 'myElement', 40 | attributes: { 41 | headers: { 42 | element: 'array', 43 | content: [ 44 | { 45 | element: 'string', 46 | meta: { 47 | name: { 48 | element: 'string', 49 | content: 'Content-Type', 50 | }, 51 | }, 52 | content: 'application/json', 53 | }, 54 | ], 55 | }, 56 | foo: { 57 | element: 'string', 58 | content: 'bar', 59 | }, 60 | sourceMap: { 61 | element: 'sourceMap', 62 | content: [ 63 | { 64 | element: 'string', 65 | content: 'test', 66 | }, 67 | ], 68 | }, 69 | }, 70 | }); 71 | 72 | it('should create headers element instance', () => { 73 | expect(myElement.attributes.get('headers')).to.be.instanceof(ArrayElement); 74 | }); 75 | 76 | it('should leave foo alone', () => { 77 | expect(myElement.attributes.get('foo')).to.be.instanceof(StringElement); 78 | }); 79 | 80 | it('should create array of source map elements', () => { 81 | const sourceMaps = myElement.attributes.get('sourceMap'); 82 | expect(sourceMaps.content).to.have.length(1); 83 | expect(sourceMaps.content[0]).to.be.instanceOf(StringElement); 84 | expect(sourceMaps.content[0].toValue()).to.equal('test'); 85 | }); 86 | }); 87 | 88 | describe('serializing attributes', () => { 89 | const myElement = new MyElement(); 90 | 91 | myElement.attributes.set('headers', new ArrayElement(['application/json'])); 92 | myElement.attributes.get('headers').content[0].meta.set('name', 'Content-Type'); 93 | 94 | myElement.attributes.set('sourceMap', ['string1', 'string2']); 95 | 96 | it('should serialize element to JSON', () => { 97 | const refracted = minim.serialiser.serialise(myElement); 98 | 99 | expect(refracted).to.deep.equal({ 100 | element: 'myElement', 101 | attributes: { 102 | headers: { 103 | element: 'array', 104 | content: [ 105 | { 106 | element: 'string', 107 | meta: { 108 | name: { 109 | element: 'string', 110 | content: 'Content-Type', 111 | }, 112 | }, 113 | content: 'application/json', 114 | }, 115 | ], 116 | }, 117 | sourceMap: { 118 | element: 'array', 119 | content: [ 120 | { 121 | element: 'string', 122 | content: 'string1', 123 | }, 124 | { 125 | element: 'string', 126 | content: 'string2', 127 | }, 128 | ], 129 | }, 130 | }, 131 | }); 132 | }); 133 | 134 | it('should round-trip using JSON serialiser', () => { 135 | const object = minim.serialiser.serialise(myElement); 136 | const element = minim.serialiser.deserialise(object); 137 | const serialised = minim.serialiser.serialise(element); 138 | 139 | expect(serialised).to.deep.equal(object); 140 | }); 141 | }); 142 | }); 143 | --------------------------------------------------------------------------------