├── .gitignore ├── LICENSE ├── README.md ├── browser └── doxl.js ├── index.js ├── package.json └── test ├── index.html └── index.js /.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 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simon Y. Blackwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doxl v0.1.7 2 | 3 | Kind-of like GraphQL except for Javascript objects, pattern matching extraction and transformation of sub-objects from Javascript objects. 4 | 5 | Just 973 bytes compressed and gzipped. 6 | 7 | # Installation 8 | 9 | npm install doxl 10 | 11 | The browser version exists at `browser/doxl.js`. 12 | 13 | # Usage 14 | 15 | Either include `doxl.js` through an import statement, script tag or require statement. Then it is pretty darn simple ... 16 | 17 | Just call `doxl(query,source)` where query and source are both JavaScript objects. The value returned will be the 18 | subset of source, if any, such that its properties satisfy at least one of the following: 19 | 20 | 1) the value was an exact match for the same property on the query 21 | 22 | 2) the value satisfied a function test provided in the query, i.e. the function returns truthy or null or zero, not undefined or false. 23 | 24 | 3) the value results from calling a function on the source with the query property value as arguments 25 | 26 | 27 | ```javascript 28 | const match = doxl({name:doxl.any(),age:value => value >= 21},{name:"joe",age:21,gender:"male",address:{city:"seattle"}}); 29 | ``` 30 | 31 | will return 32 | 33 | ```javascript 34 | {name:"joe",age:21} 35 | ``` 36 | 37 | # API 38 | 39 | `doxl(query,source,{partial,constructorMatch,transform,schema}={})` - 40 | 41 | `query` - An object, possibly nested, that contains properties to extract and values to literally match or functions to test for a match. 42 | 43 | `source` - An object from which to extract data. Any functions on the source object will be called with the arguments 44 | `(...queryValue)`, with `this` bound to `source`. For fat arrow functions, `source` is not available as `this`. Mapping single arguments into 45 | the array required for `...queryValue` is done automatically. If your function takes a single argument that is an array, then you must nest 46 | it one level, e.g. `f(someArray)` should use the query `{f:[matchingArray]}` not `{f:matchingArray}`. 47 | 48 | `partial` - The default behavior is to return only full matches. If `partial` is truthy, then a value will returned if any properties match. 49 | 50 | `constructorMatch` - Typically the `query` and `source` will be POJOs and with the exception of `Array`, `Date`, `RegExp`, `Map`, 51 | and `Set` the class of a source or its nested objects is ignored. If `constructorMatch` is truthy, then the constructors for the `query` 52 | and `source` must match. 53 | 54 | `transform` - Typically any functions in the `query` are treated as predicates that return a truthy or falsy value; however, if `transform` is 55 | truthy then these functions will consume the value from the `source` and return the same or a different value for use in the result. If the value returned is `undefined`, 56 | then it is assumed no match occured. 57 | 58 | `schema` - Reserved for future use. 59 | 60 | `doxl.any([...args])` - A utility function to match any value. If the optional `...args` are passed in and the value on the underlying object being queried is a function, 61 | then it will be called with the args and the `this` scope set to the object being queried. The return value will be used in the result. 62 | 63 | `doxl.skip([count])` - A utility function to skip indexes in a source array without retaining their values, e.g. `[1,doxl.skip(2),1]` will match both `[1,2,3,1]` and `[1,3,3,1]` by returning `[1,1]`. If you don't pass an argument, the number to skip will be computed from the remaining count of values in the query and the length of the source array. 64 | 65 | `doxl.slice([count])` - A utility function to skip indexes in a source array and retain their values, e.g. `[1,doxl.slice(2),1]` will match both `[1,2,3,1]` and `[1,3,3,1]` by returning `[1,2,3,1]` and `[1,3,3,1]`. If you don't pass an argument, the number to skip will be computed from the remaining count of values in the query and the length of the source array. 66 | 67 | `doxl.var(name)` - A utility function to support variable binding and matching across a pattern, e.g. `[doxl.var("val"),2,doxl.var("val")]` will match arrays with the same first and last values. Variables bind from left to right and nested variables are available to higher level classes to their right, e.g, `{nested:{num:doxl.var("n")},num:doxl.var("n")}` will match `{num:1,nested:{num: 1}}`. 68 | 69 | `doxl.undefined(default[,...args])` - A utility function that will match undefined properties in the `source`. If `default` is provided, it will be 70 | returned as the value for the undefined property. If the optional `...args` are passed in and the value on the underlying object being queried is a function, 71 | then it will be called with the args and the `this` scope set to the object being queried. The return value will be used in the result. 72 | 73 | # Application Techniques 74 | 75 | 76 | ## Query Functions 77 | 78 | Functions defined on the query should have one of these two signatures: 79 | 80 | `function(sourceValue,property,source,query) { ...; }` 81 | 82 | `(sourceValue,property,source,query) => { ...; }` 83 | 84 | When doing regular queries, they should return `true` if the `property` and `sourceValue` should be included in the result. Normally, similar to the use of `forEach` 85 | in JavaScript, all but the first value is ignored, the additional arguments are for advanced use, e.g. 86 | 87 | ``` 88 | {age: value => value >= 21} 89 | ``` 90 | 91 | Behind the scenes, the functions `doxl.any` and `doxl.undefined` are implemented in a manner that uses these extended arguments. 92 | 93 | If `doxl` is invoked with a query and the option `transform:true`, then the return value of the function is used as the property value rather than the value on the source object, unless it is `undefined`. 94 | 95 | ## Handling `undefined` 96 | 97 | A `source` can have an undefined property and still have a successful match by using `doxl.undefined`. 98 | 99 | 100 | ```javascript 101 | doxl({name:olx.any,age:doxl.any(),gender:doxl.undefined()},{age:21,name:"joe"}); 102 | ``` 103 | 104 | will match: 105 | 106 | ```javascript 107 | {name:"joe",age:21} 108 | ``` 109 | 110 | While, 111 | 112 | ```javascript 113 | doxl({name:olx.any(),age:doxl.any(),gender:doxl.undefined()},{age:21,name:"joe",gender:"male"}); 114 | ``` 115 | 116 | will return: 117 | 118 | ```javascript 119 | {name:"joe",age:21,gender"male"} 120 | ``` 121 | 122 | `doxl.undefined` can also take as a second argument a default value. 123 | 124 | For instance, 125 | 126 | ```javascript 127 | doxl({name:olx.any(),age:doxl.any(),gender:doxl.undefined("undeclared")},{age:21,name:"joe"}); 128 | ``` 129 | 130 | will return: 131 | 132 | ```javascript 133 | {name:"joe",age:21,gender:"undeclared"} 134 | ``` 135 | 136 | Finally, `doxl.undefined` can take additional arguments which are passed to underlying target functions that match query property names. 137 | 138 | ## Processing Arrays Of Possible Matches 139 | 140 | Assume you have an array of objects you wish to search, you can reduce it using `reduce` and `doxl`: 141 | 142 | ```javascript 143 | [{name:"joe",age:21,employed:true}, 144 | {name:"mary",age:20,employed:true}, 145 | {name:"jack",age:22,employed:false} 146 | ].reduce(item => { 147 | const match = doxl({name:doxl.any(),age:value => value > 21,employed:false},item)); 148 | if(match) accum.push(match); 149 | return accum; 150 | },[]); 151 | ``` 152 | 153 | will return: 154 | 155 | ```javascript 156 | [{name:"jack",age:22,employed:false}] 157 | ``` 158 | 159 | There is a utility function, `doxl.reduce(array,query)` that does this for you, e.g. 160 | 161 | ``` 162 | doxl.reduce([{name:"joe",age:21,employed:true}, 163 | {name:"mary",age:20,employed:true}, 164 | {name:"jack",age:22,employed:false} 165 | ],{name:doxl.any(),age:value => value >= 21,employed:false}) 166 | ``` 167 | 168 | 169 | ## Re-Ordering Keys 170 | 171 | ```javascript 172 | doxl({name:olx.any(),age:doxl.any()},{age:21,name:"joe"}); 173 | ``` 174 | 175 | will return: 176 | 177 | ```javascript 178 | {name:"joe",age:21} 179 | ``` 180 | 181 | ## Dynamic Property Values 182 | 183 | If your source objects are class instances with methods or objects containing functions, they will get resolved using the value as the arguments: 184 | 185 | ```javascript 186 | class Person { 187 | constructor({firstName,lastName,favoriteNumbers=[]}) { 188 | this.firstName = firstName; 189 | this.lastName = lastName; 190 | this.favoriteNumbers = favoriteNumbers; 191 | } 192 | name(salutation="") { 193 | return `${salutation ? salutation+ " " : ""}${this.lastName}, ${this.firstName}`; 194 | } 195 | someFavoriteNumber(number) { 196 | if(this.favoriteNumbers.includes(number)) { 197 | return number; 198 | } 199 | } 200 | } 201 | 202 | const people = [ 203 | new Person({firstName:"joe",lastName:"jones",favoriteNumbers:[5,15]}), 204 | new Person({firstName:"mary",lastName:"contrary",favoriteNumbers:[7,14]}) 205 | ]; 206 | 207 | const matches = people.reduce((accum,item) => { 208 | const match = doxl({name:doxl.any(),someFavoriteNumber:7},item,{all:true}); 209 | if(match) accum.push(match); 210 | return accum; 211 | },[]); 212 | ``` 213 | 214 | will return: 215 | 216 | ```javascript 217 | [{name:"contrary, mary",someFavoriteNumber:7}] 218 | ``` 219 | 220 | `doxl.any` and `doxl.undefined` can also take arguments to pass to underlying functions, e.g. 221 | 222 | ```javascript 223 | {name:doxl.any("M.")} 224 | ``` 225 | 226 | ```javascript 227 | {name:doxl.undefined("Secret","M.")} 228 | ``` 229 | 230 | ## Transformations 231 | 232 | If the option `transform` is truthy, object transformations can occur: 233 | 234 | ```javascript 235 | doxl({size: value => size * 2},{size: 2},{transform:true}); 236 | ``` 237 | 238 | results in: 239 | 240 | ```javascript 241 | {size: 4} 242 | ``` 243 | 244 | ## Shorter Code 245 | 246 | If you assign `doxl` to the variable `$` or `_`, you can shorten your code, e.g. 247 | 248 | ```javascript 249 | {name:$.undefined("Secret","M.")} 250 | ``` 251 | 252 | # Why doxl 253 | 254 | There are other extraction and transformation libraries, but most require using strings that need to be parsed, increasing the library size and the chance for typographical errors. In the extreme case, they introduce domain specific languages that force the developer to learn an entirely new syntax and semantics. The DOXL library allows the expression of extraction directives as pure JavaScript and is very small. Furthermore, few libraries support variable binding within extraction patterns. 255 | 256 | # Release History (reverse chronological order) 257 | 258 | 2018-08-28 v0.1.7 Added `doxl.skip` and `doxl.slice`. 259 | 260 | 2018-08-28 v0.1.6 Corrected binding issues for complex patterns with variables. 261 | 262 | 2018-08-28 v0.1.5 Documentation updates. Deprecating `doxl.UNDEFINED` and `doxl.ANY` in favor of `doxl.undefined` and `doxl.any`. Will be unsupported as of v0.1.8. Added support for `doxl.var`. 263 | 264 | 2018-08-24 v0.1.4 Documentation updates. 265 | 266 | 2018-08-24 v0.1.3 Documentation updates. 267 | 268 | 2018-08-23 v0.1.2 Documentation updates. Changes to behavior of `doxl.ANY`, which is now a function and can pass arguments to underlying query targets. 269 | Enhancements to `doxl.UNDEFINED` to do the same. Also, renamed `doxl.UNDEFINEDOK` to simply `doxl.UNDEFINED`. 270 | 271 | 2018-08-02 v0.1.1 Documentation updates. 272 | 273 | 2018-08-02 v0.1.0 First public release as independent module. 274 | 275 | # License 276 | 277 | MIT License 278 | 279 | Copyright (c) 2018 Simon Y. Blackwell, AnyWhichWay, LLC 280 | 281 | Permission is hereby granted, free of charge, to any person obtaining a copy 282 | of this software and associated documentation files (the "Software"), to deal 283 | in the Software without restriction, including without limitation the rights 284 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 285 | copies of the Software, and to permit persons to whom the Software is 286 | furnished to do so, subject to the following conditions: 287 | 288 | The above copyright notice and this permission notice shall be included in all 289 | copies or substantial portions of the Software. 290 | 291 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 292 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 293 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 294 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 295 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 296 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 297 | SOFTWARE. 298 | -------------------------------------------------------------------------------- /browser/doxl.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | class Variable { 4 | constructor(name,value) { 5 | this.name = name; 6 | this.value = value; 7 | } 8 | valueOf() { 9 | return this.value; 10 | } 11 | toJSON() { 12 | return this.value; 13 | } 14 | } 15 | 16 | class Skip { 17 | constructor(count) { 18 | this.count = count; 19 | } 20 | } 21 | 22 | class Slice { 23 | constructor(count) { 24 | this.count = count; 25 | } 26 | } 27 | 28 | const doxl = (query,source,{partial,constructorMatch,transform,schema}={},variables={}) => { 29 | let skip = 0; 30 | return Object.keys(query).reduce((accum,key,i) => { 31 | let qvalue = skip ? query[i+skip] : query[key], 32 | qtype = typeof(qvalue), 33 | svalue = skip ? source[i+skip] : source[key], 34 | stype = typeof(svalue); 35 | if(qvalue===undefined || (svalue===undefined && qtype!=="function")) { 36 | return accum; 37 | } 38 | if(qvalue && qtype==="object" && (qvalue instanceof Skip || qvalue instanceof Slice)) { 39 | if(isNaN(qvalue.count)) { 40 | qvalue.count = ((source.length - i) - (query.length - i)) + 1; 41 | } 42 | if(qvalue instanceof Slice) { 43 | accum || (accum = []); 44 | accum = accum.concat(source.slice(i,i+qvalue.count)); 45 | key = i + qvalue.count; 46 | } 47 | skip += qvalue.count; 48 | qvalue = svalue = source[i + qvalue.count]; 49 | qtype = stype = typeof(svalue); 50 | } 51 | if(qvalue===undefined || (svalue===undefined && qtype!=="function")) { 52 | return accum; 53 | } 54 | let value = qvalue, 55 | vtype = typeof(value); 56 | if(qtype==="function") { 57 | value = qvalue.call(source,svalue,key,source,query); 58 | } else if(stype==="function") { 59 | value = svalue.call(source,...(Array.isArray(value) ? value : [value])); 60 | if(value!==undefined) { 61 | accum || (accum = Array.isArray(query) ? [] : {}); 62 | accum[key] = value; 63 | } else if(!partial) { 64 | return null; 65 | } 66 | return accum; 67 | } 68 | if(value instanceof Variable) { 69 | if(variables[value.name]===undefined) { 70 | variables[value.name] = svalue; 71 | } 72 | value.value = variables[value.name]; 73 | if(variables[value.name]!==svalue) { 74 | if(!partial) { 75 | return null; 76 | } 77 | return accum; 78 | } 79 | value = svalue; 80 | vtype = stype; 81 | } 82 | if(value && svalue && qtype==="object" && vtype==="object") { 83 | if(constructorMatch && svalue.constructor!==value.constructor) { 84 | return; 85 | } 86 | if(value instanceof Date) { 87 | if(stype instanceof Date && svalue.getTime()===value.getTime()) { 88 | accum || (accum = Array.isArray(query) ? [] : {}); 89 | accum[key] = svalue; 90 | } else if(!partial) { 91 | return null; 92 | } 93 | return accum; 94 | } 95 | if(value instanceof RegExp) { 96 | if(svalue instanceof RegExp && svalue.flags==value.flags && svalue.source===value.source) { 97 | accum || (accum = Array.isArray(query) ? [] : {}); 98 | accum[key] = svalue; 99 | } else if(!partial) { 100 | return null; 101 | } 102 | return accum; 103 | } 104 | if(value instanceof Array) { 105 | if(svalue instanceof Array && svalue.length===value.length) { 106 | const subdoc = doxl(value,svalue,{partial,constructorMatch,transform,schema},variables); 107 | if(subdoc!==null) { 108 | accum || (accum = Array.isArray(query) ? [] : {}); 109 | accum[key] = subdoc; 110 | } else if(!partial) { 111 | return null; 112 | } 113 | } else if(!partial) { 114 | return null; 115 | } 116 | return accum; 117 | } 118 | if(value instanceof Set || value instanceof Map) { 119 | if(svalue.constructor===value.constructor && svalue.size===value.size) { 120 | const values = value.values(), 121 | svalues = svalue.values(); 122 | if(values.every(value => { 123 | return svalues.some(svalue => { 124 | return doxl(value,svalue,{partial,constructorMatch,transform,schema},variables); 125 | }) 126 | })) { 127 | accum || (accum = Array.isArray(query) ? [] : {}); 128 | accum[key] = svalue; 129 | } else if(!partial) { 130 | return null; 131 | } 132 | } 133 | return accum; 134 | } 135 | const subdoc = doxl(value,svalue,{partial,constructorMatch,transform,schema},variables); 136 | if(subdoc!==null) { 137 | accum || (accum = Array.isArray(query) ? [] : {}); 138 | accum[key] = subdoc; 139 | } else if(!partial) { 140 | return null; 141 | } 142 | return accum; 143 | } 144 | if(qtype==="function") { 145 | if(qvalue.name==="any" || qvalue.name==="undfnd" || (value!==undefined && value!==false)) { // allow zero 146 | accum || (accum = Array.isArray(query) ? [] : {}); 147 | accum[key] = (qvalue.name==="any" || qvalue.name==="undfnd" || transform) ? value : svalue; 148 | } else if(!partial) { 149 | return null; 150 | } 151 | return accum; 152 | } 153 | if(value===svalue) { 154 | accum || (accum = Array.isArray(query) ? [] : {}); 155 | accum[key] = svalue; 156 | } else if(!partial) { 157 | return null; 158 | } 159 | return accum; 160 | },null) 161 | } 162 | doxl.any = (...args) => function any(sourceValue) { return typeof(sourceValue)==="function" ? sourceValue.call(this,...args) : sourceValue; }; 163 | doxl.undefined = (deflt,...args) => function undfnd(sourceValue) { let value = typeof(sourceValue)==="function" ? sourceValue.call(this,...args) : sourceValue; return value===undefined ? deflt : value; } 164 | doxl.var = (name) => new Variable(name); 165 | doxl.skip = (count) => new Skip(count); 166 | doxl.slice = (count) => new Slice(count); 167 | doxl.ANY = doxl.any; 168 | doxl.UNDEFINED = doxl.undefined; 169 | doxl.reduce = (array,query) => { 170 | return array.reduce((accum,item) => { 171 | const match = doxl(query,item); 172 | if(match) accum.push(match); 173 | return accum; 174 | },[]); 175 | } 176 | 177 | if(typeof(module)!=="undefined") module.exports = doxl; 178 | if(typeof(window)!=="undefined") window.doxl = doxl; 179 | }).call(this); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | class Variable { 4 | constructor(name,value) { 5 | this.name = name; 6 | this.value = value; 7 | } 8 | valueOf() { 9 | return this.value; 10 | } 11 | toJSON() { 12 | return this.value; 13 | } 14 | } 15 | 16 | class Skip { 17 | constructor(count) { 18 | this.count = count; 19 | } 20 | } 21 | 22 | class Slice { 23 | constructor(count) { 24 | this.count = count; 25 | } 26 | } 27 | 28 | const doxl = (query,source,{partial,constructorMatch,transform,schema}={},variables={}) => { 29 | let skip = 0; 30 | return Object.keys(query).reduce((accum,key,i) => { 31 | let qvalue = skip ? query[i+skip] : query[key], 32 | qtype = typeof(qvalue), 33 | svalue = skip ? source[i+skip] : source[key], 34 | stype = typeof(svalue); 35 | if(qvalue===undefined || (svalue===undefined && qtype!=="function")) { 36 | return accum; 37 | } 38 | if(qvalue && qtype==="object" && (qvalue instanceof Skip || qvalue instanceof Slice)) { 39 | if(isNaN(qvalue.count)) { 40 | qvalue.count = ((source.length - i) - (query.length - i)) + 1; 41 | } 42 | if(qvalue instanceof Slice) { 43 | accum || (accum = []); 44 | accum = accum.concat(source.slice(i,i+qvalue.count)); 45 | key = i + qvalue.count; 46 | } 47 | skip += qvalue.count; 48 | qvalue = svalue = source[i + qvalue.count]; 49 | qtype = stype = typeof(svalue); 50 | } 51 | if(qvalue===undefined || (svalue===undefined && qtype!=="function")) { 52 | return accum; 53 | } 54 | let value = qvalue, 55 | vtype = typeof(value); 56 | if(qtype==="function") { 57 | value = qvalue.call(source,svalue,key,source,query); 58 | } else if(stype==="function") { 59 | value = svalue.call(source,...(Array.isArray(value) ? value : [value])); 60 | if(value!==undefined) { 61 | accum || (accum = Array.isArray(query) ? [] : {}); 62 | accum[key] = value; 63 | } else if(!partial) { 64 | return null; 65 | } 66 | return accum; 67 | } 68 | if(value instanceof Variable) { 69 | if(variables[value.name]===undefined) { 70 | variables[value.name] = svalue; 71 | } 72 | value.value = variables[value.name]; 73 | if(variables[value.name]!==svalue) { 74 | if(!partial) { 75 | return null; 76 | } 77 | return accum; 78 | } 79 | value = svalue; 80 | vtype = stype; 81 | } 82 | if(value && svalue && qtype==="object" && vtype==="object") { 83 | if(constructorMatch && svalue.constructor!==value.constructor) { 84 | return; 85 | } 86 | if(value instanceof Date) { 87 | if(stype instanceof Date && svalue.getTime()===value.getTime()) { 88 | accum || (accum = Array.isArray(query) ? [] : {}); 89 | accum[key] = svalue; 90 | } else if(!partial) { 91 | return null; 92 | } 93 | return accum; 94 | } 95 | if(value instanceof RegExp) { 96 | if(svalue instanceof RegExp && svalue.flags==value.flags && svalue.source===value.source) { 97 | accum || (accum = Array.isArray(query) ? [] : {}); 98 | accum[key] = svalue; 99 | } else if(!partial) { 100 | return null; 101 | } 102 | return accum; 103 | } 104 | if(value instanceof Array) { 105 | if(svalue instanceof Array && svalue.length===value.length) { 106 | const subdoc = doxl(value,svalue,{partial,constructorMatch,transform,schema},variables); 107 | if(subdoc!==null) { 108 | accum || (accum = Array.isArray(query) ? [] : {}); 109 | accum[key] = subdoc; 110 | } else if(!partial) { 111 | return null; 112 | } 113 | } else if(!partial) { 114 | return null; 115 | } 116 | return accum; 117 | } 118 | if(value instanceof Set || value instanceof Map) { 119 | if(svalue.constructor===value.constructor && svalue.size===value.size) { 120 | const values = value.values(), 121 | svalues = svalue.values(); 122 | if(values.every(value => { 123 | return svalues.some(svalue => { 124 | return doxl(value,svalue,{partial,constructorMatch,transform,schema},variables); 125 | }) 126 | })) { 127 | accum || (accum = Array.isArray(query) ? [] : {}); 128 | accum[key] = svalue; 129 | } else if(!partial) { 130 | return null; 131 | } 132 | } 133 | return accum; 134 | } 135 | const subdoc = doxl(value,svalue,{partial,constructorMatch,transform,schema},variables); 136 | if(subdoc!==null) { 137 | accum || (accum = Array.isArray(query) ? [] : {}); 138 | accum[key] = subdoc; 139 | } else if(!partial) { 140 | return null; 141 | } 142 | return accum; 143 | } 144 | if(qtype==="function") { 145 | if(qvalue.name==="any" || qvalue.name==="undfnd" || (value!==undefined && value!==false)) { // allow zero 146 | accum || (accum = Array.isArray(query) ? [] : {}); 147 | accum[key] = (qvalue.name==="any" || qvalue.name==="undfnd" || transform) ? value : svalue; 148 | } else if(!partial) { 149 | return null; 150 | } 151 | return accum; 152 | } 153 | if(value===svalue) { 154 | accum || (accum = Array.isArray(query) ? [] : {}); 155 | accum[key] = svalue; 156 | } else if(!partial) { 157 | return null; 158 | } 159 | return accum; 160 | },null) 161 | } 162 | doxl.any = (...args) => function any(sourceValue) { return typeof(sourceValue)==="function" ? sourceValue.call(this,...args) : sourceValue; }; 163 | doxl.undefined = (deflt,...args) => function undfnd(sourceValue) { let value = typeof(sourceValue)==="function" ? sourceValue.call(this,...args) : sourceValue; return value===undefined ? deflt : value; } 164 | doxl.var = (name) => new Variable(name); 165 | doxl.skip = (count) => new Skip(count); 166 | doxl.slice = (count) => new Slice(count); 167 | doxl.ANY = doxl.any; 168 | doxl.UNDEFINED = doxl.undefined; 169 | doxl.reduce = (array,query) => { 170 | return array.reduce((accum,item) => { 171 | const match = doxl(query,item); 172 | if(match) accum.push(match); 173 | return accum; 174 | },[]); 175 | } 176 | 177 | if(typeof(module)!=="undefined") module.exports = doxl; 178 | if(typeof(window)!=="undefined") window.doxl = doxl; 179 | }).call(this); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doxl", 3 | "version": "0.1.7", 4 | "description": "Kind of like GraphQL except for Javascript objects, pattern matching extraction and transformation of sub-objects from Javascript objects.", 5 | "private": false, 6 | "isomorphic": true, 7 | "keywords": [ 8 | "GraphQL" 9 | ], 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/doxl/doxl.git" 14 | }, 15 | "scripts": { 16 | "prepare": "cp index.js browser/doxl.js" 17 | }, 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "chai": "^4.1.0", 21 | "mocha": "^3.4.2" 22 | }, 23 | "engines": { 24 | "node": "9.x" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var chai, 2 | expect, 3 | doxl; 4 | 5 | if(typeof(module)!=="undefined") { 6 | doxl = require("../index.js"); 7 | chai = require("chai"); 8 | expect = chai.expect; 9 | } 10 | 11 | const $ = doxl; 12 | 13 | console.log("Testing ..."); 14 | 15 | const SOURCE = {f:function(v=1) { return this.str+v; },bool:true,num:1,nil:null,str:"str",nested:{num:1},array:[1,"1"]}; 16 | 17 | describe("all tests",function() { 18 | it("literal boolean match",function(done) { 19 | const query = {bool:true}, 20 | match = $(query,SOURCE); 21 | expect(JSON.stringify(match)).equal(JSON.stringify(query)); 22 | done(); 23 | }); 24 | it("literal null match",function(done) { 25 | const query = {nil:null}, 26 | match = $(query,SOURCE); 27 | expect(JSON.stringify(match)).equal(JSON.stringify(query)); 28 | done(); 29 | }); 30 | it("literal number match",function(done) { 31 | const query = {num:1}, 32 | match = $(query,SOURCE); 33 | expect(JSON.stringify(match)).equal(JSON.stringify(query)); 34 | done(); 35 | }); 36 | it("literal string match",function(done) { 37 | const query = {str:"str"}, 38 | match = $(query,SOURCE); 39 | expect(JSON.stringify(match)).equal(JSON.stringify(query)); 40 | done(); 41 | }); 42 | it("nested match",function(done) { 43 | const query = {nested:{num:1}}, 44 | match = $(query,SOURCE); 45 | expect(JSON.stringify(match)).equal(JSON.stringify(query)); 46 | done(); 47 | }); 48 | it("array match",function(done) { 49 | const query = {array:[1,"1"]}, 50 | match = $(query,SOURCE); 51 | expect(JSON.stringify(match)).equal(JSON.stringify(query)); 52 | done(); 53 | }); 54 | it("functional match",function(done) { 55 | const query = {num:value => value===1}, 56 | match = $(query,SOURCE); 57 | expect(JSON.stringify(match)).equal(JSON.stringify({num:1})); 58 | done(); 59 | }); 60 | it("functional mis-match",function(done) { 61 | const query = {num:value => value!==1}, 62 | match = $(query,SOURCE); 63 | expect(match).equal(null); 64 | done(); 65 | }); 66 | it("functional match",function(done) { 67 | const query = {num:value => value===1}, 68 | match = $(query,SOURCE); 69 | expect(JSON.stringify(match)).equal(JSON.stringify({num:1})); 70 | done(); 71 | }); 72 | it("all multi-match",function(done) { 73 | const query = Object.keys(SOURCE).reduce((accum,key) => { accum[key] = $.any(); return accum; },{}); 74 | match = $(query,SOURCE), 75 | source = Object.assign({},SOURCE); 76 | source.f = source.f(); 77 | expect(JSON.stringify(match)).equal(JSON.stringify(source)); 78 | done(); 79 | }); 80 | it("all multi-match with args",function(done) { 81 | const query = Object.keys(SOURCE).reduce((accum,key) => { accum[key] = $.any(2); return accum; },{}); 82 | match = $(query,SOURCE), 83 | source = Object.assign({},SOURCE); 84 | source.f = source.f(2); 85 | expect(JSON.stringify(match)).equal(JSON.stringify(source)); 86 | done(); 87 | }); 88 | it("undefined",function(done) { 89 | const match = $({name:$.any(),age:$.any,gender:$.undefined()},{age:21,name:"joe"}); 90 | expect(JSON.stringify(match)).equal(JSON.stringify({name:"joe",age:21})); 91 | done(); 92 | }); 93 | it("not undefined",function(done) { 94 | const match = $({name:$.any,age:$.any,gender:$.undefined()},{age:21,name:"joe",gender:"male"}); 95 | expect(JSON.stringify(match)).equal(JSON.stringify({name:"joe",age:21,gender:"male"})); 96 | done(); 97 | }); 98 | it("undefined default",function(done) { 99 | const match = $({name:$.any,age:$.any,gender:$.undefined("undeclared")},{age:21,name:"joe"}); 100 | expect(JSON.stringify(match)).equal(JSON.stringify({name:"joe",age:21,gender:"undeclared"})); 101 | done(); 102 | }); 103 | it("variable match",function(done) { 104 | const query = {num:$.var("n"),nested:{num:$.var("n")}}, 105 | match = $(query,SOURCE); 106 | expect(JSON.stringify(match)).equal(JSON.stringify(query)); 107 | done(); 108 | }); 109 | it("complex variable match",function(done) { 110 | const matches = $.reduce([ 111 | {partners:[{name:"joe"},{name:"joe"}]}, 112 | {partners:[{name:"mary"},{name:"john"}]} 113 | ], 114 | {partners:[{name:$.var("n")},{name:$.var("n")}]}); 115 | expect(matches.length).equal(1); 116 | expect(JSON.stringify({partners:[{name:"joe"},{name:"joe"}]})).equal(JSON.stringify(matches[0])); 117 | done(); 118 | }); 119 | it("skip",function(done) { 120 | const query = [1,$.skip(2),1], 121 | source = [1,2,3,1], 122 | match = $(query,source); 123 | expect(JSON.stringify(match)).equal(JSON.stringify([1,1])); 124 | done(); 125 | }); 126 | it("computed skip",function(done) { 127 | const query = [1,$.skip(),1], 128 | source = [1,2,3,1], 129 | match = $(query,source); 130 | expect(JSON.stringify(match)).equal(JSON.stringify([1,1])); 131 | done(); 132 | }); 133 | it("short computed skip",function(done) { 134 | const query = [1,$.skip(),1], 135 | source = [1,1], 136 | match = $(query,source); 137 | expect(JSON.stringify(match)).equal(JSON.stringify([1,1])); 138 | done(); 139 | }); 140 | it("slice",function(done) { 141 | const query = [1,$.slice(2),1], 142 | source = [1,2,3,1], 143 | match = $(query,source); 144 | expect(JSON.stringify(match)).equal(JSON.stringify(source)); 145 | done(); 146 | }); 147 | it("computed slice",function(done) { 148 | const query = [1,$.slice(),1], 149 | source = [1,2,3,1], 150 | match = $(query,source); 151 | expect(JSON.stringify(match)).equal(JSON.stringify(source)); 152 | done(); 153 | }); 154 | it("longhand reduce",function(done) { 155 | const matches = [{name:"joe",age:21,employed:true},{name:"mary",age:20,employed:true},{name:"jack",age:22,employed:false}].reduce((accum,item) => { 156 | const match = $({name:$.any,age:value => value >= 21,employed:false},item); 157 | if(match) accum.push(match); 158 | return accum; 159 | },[]); 160 | console.log(matches) 161 | expect(matches.length).equal(1); 162 | expect(JSON.stringify(matches[0])).equal(JSON.stringify({name:"jack",age:22,employed:false})); 163 | done(); 164 | }); 165 | it("reduce",function(done) { 166 | const matches = $.reduce([{name:"joe",age:21,employed:true},{name:"mary",age:20,employed:true},{name:"jack",age:22,employed:false}],{name:$.any,age:value => value >= 21,employed:false}); 167 | console.log(matches) 168 | expect(matches.length).equal(1); 169 | expect(JSON.stringify(matches[0])).equal(JSON.stringify({name:"jack",age:22,employed:false})); 170 | done(); 171 | }); 172 | it("dynamic properties",function(done) { 173 | class Person { 174 | constructor({firstName,lastName,favoriteNumbers=[]}) { 175 | this.firstName = firstName; 176 | this.lastName = lastName; 177 | this.favoriteNumbers = favoriteNumbers; 178 | } 179 | name() { 180 | return `${this.lastName}, ${this.firstName}`; 181 | } 182 | someFavoriteNumber(number) { 183 | if(this.favoriteNumbers.includes(number)) { 184 | return number; 185 | } 186 | } 187 | } 188 | 189 | const people = [ 190 | new Person({firstName:"joe",lastName:"jones",favoriteNumbers:[5,15]}), 191 | new Person({firstName:"mary",lastName:"contrary",favoriteNumbers:[7,14]}) 192 | ]; 193 | 194 | const matches = people.reduce((accum,item) => { 195 | const match = $({name:$.any(),someFavoriteNumber:7},item); 196 | if(match) accum.push(match); 197 | return accum; 198 | },[]); 199 | expect(matches.length).equal(1); 200 | expect(JSON.stringify(matches[0])).equal(JSON.stringify({name:"contrary, mary",someFavoriteNumber:7})) 201 | done(); 202 | }); 203 | it("transform",function(done) { 204 | const match = $({size: value => value * 2},{size: 2},{transform:true}); 205 | expect(JSON.stringify(match)).equal(JSON.stringify({size:4})); 206 | done(); 207 | }); 208 | }); 209 | 210 | 211 | if(typeof(window)!=="undefined") { 212 | mocha.run(); 213 | } 214 | 215 | 216 | 217 | 218 | 219 | --------------------------------------------------------------------------------