├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── ReleaseNote.md ├── doc ├── builtInOp.md └── extPackage.md ├── lib ├── lamdaApp.js └── modules │ ├── arithmetic.js │ ├── arrays.js │ ├── collections.js │ ├── comparators.js │ ├── stream.js │ └── test │ ├── testArithmetic.js │ ├── testArrays.js │ ├── testCollections.js │ ├── testComparators.js │ └── testCtrlFlow.js ├── package.json └── test ├── testLamda.js ├── testMeta.js ├── testObjQuery.js ├── testOp.js ├── testRecursion.js ├── testStream.js ├── testSyntax.js └── testVariables.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .svn 3 | .*.swp 4 | .git 5 | .project 6 | .settings 7 | node_modules 8 | npm-debug.log 9 | example.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | ._* 3 | .DS_Store 4 | .git 5 | .hg 6 | .lock_wscript 7 | .svn 8 | .project 9 | .settings 10 | npm-debug.log 11 | example.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSON-FP 2 | ======= 3 | 4 | In a decentralized computing environment, when applications are dealing with huge amount of data it's a better practice to pass programming codes to various machines than moving data. However, how can machines of various configurations understand each other? Also, the "moving code, least moving data" policy would arguably work better in the functional programming paradigm than the imperative one. 5 | 6 | Those questions/issues lead to the idea of doing functional programming in JSON. If programs can be coded in JSON, they can be easily shipped around and understood by machines of vaious settings. Combining JSON and functional programming also makes security issues easier to track or manage. 7 | 8 | JSON-FP is part of an attempt to make data freely and easily accessed, distributed, annotated, meshed, even re-emerged with new values. To achieve that, it's important to be able to ship codes to where data reside, and that's what JSON-FP is trying to achieve. 9 | 10 | ## JSON-FP playground 11 | Want to play with JSON-FP to see how it works? You can test your JSON-FP code online and find examples at [JSON-FP playground](http://playground.jsonfp.org). 12 | 13 | ## What's new 14 | 15 | + Four new operators (head, tail, bucket and infix) are introduced to the runtime core. The _bucket_ operator can be used to distribute input list into sub-list (buckets) based on the bucket conditions or predicates (v-0.2.2). 16 | 17 | + The _infix_ operator is introduced to solve the problem that JSON-FP always work on input as a whole. Consider adding two numbers x and y. This could be tricky if you don't want to introduce any side effect to your JSON-FP expression. With the _infix_ operator, the problem can be easily solved by doing: _infix: ['$in.x', {add: '$in.y'}]_. 18 | 19 | + The concept of streaming data is introduced in v-0.2.1. For descriptions and examples, please check [Using "Streams" As Data Generators](https://github.com/benlue/jsonfp-examples/tree/master/examples/stream). 20 | 21 | + Adding customized operators to the JSON-FP runtime has been standardized (v-0.1.1). Please check [Developing and Installing Packages](https://github.com/benlue/jsonfp/blob/master/doc/extPackage.md) for details. 22 | 23 | + Added the **formula** operator for metaprogramming. Developers now can use the **formula** operator to define a JSON-FP formula and use the **convert** operator to apply a formula (v-0.1.0). 24 | 25 | For details about what's new in the current release, please check the [release note](https://github.com/benlue/jsonfp/blob/master/ReleaseNote.md). 26 | 27 | ## Install 28 | 29 | npm install -g jsonfp 30 | 31 | ## Contents 32 | 33 | + [Getting started](#started) 34 | + [Run programs](#run) 35 | + [Promise or callback](#proCb) 36 | + [JSON-FP expression](#format) 37 | + [Expression input](#input) 38 | + [Evaluation](#evaluation) 39 | + [Variables](#variables) 40 | + [Setting variables](#setVar) 41 | + [Operators](#operators) 42 | + [API](#api) 43 | + [jsonfp.init()](#jfpInit) 44 | + [jsonfp.apply()](#jfpApply) 45 | + [jsonfp.isExpression()](#isExp) 46 | + [jsonfp.addMethod()](#jfpAddMethod) 47 | + [jsonfp.removeMethod()](#jfpRemoveMethod) 48 | + [Customizing JSON-FP](#customization) 49 | + [Metaprogramming](#meta) 50 | 51 | 52 | ## Getting started 53 | An [example project](https://github.com/benlue/jsonfp-examples) has been created to demonstrate how to write JSON-FP expressions. In addition to that, test files under the _test_ directory is also a good place to start. Below are some examples you may want to check: 54 | 55 | + **[simple](https://github.com/benlue/jsonfp-examples/blob/master/examples/simple/simple.js)**: very simple examples to help you getting familiar with JSON-FP. 56 | 57 | + **[stream](https://github.com/benlue/jsonfp-examples/tree/master/examples/stream)**: showing how to use streams as input to JSON-FP expressions. In particular, the example shows how to use the iterator stream to replace the for-loop statement. 58 | 59 | + **[quick sort](https://github.com/benlue/jsonfp-examples/blob/master/examples/quickSort/quickSort.js)**: the classic quick sort algorithm implemented in JSON-FP. 60 | 61 | + **[object query](https://github.com/benlue/jsonfp-examples/blob/master/examples/ObjectQuery)**: showing how to query a list of JSON objects with various conditions. 62 | 63 | + **[metaprogramming](https://github.com/benlue/jsonfp-examples/tree/master/examples/metapro)**: showing how to do alpha-conversion in JSON-FP. 64 | 65 | 66 | ### Run programs 67 | Below is how you can run or evaluate a JSON-FP program: 68 | 69 | var jsonfp = require('jsonfp'); 70 | jsonfp.init(); 71 | 72 | // providing a context(ctx) to run 'program' with 'input' 73 | jsonfp.apply(ctx, input, program); 74 | 75 | // or simply 76 | jsonfp.apply(input, program); 77 | 78 | _program_ should be a JSON-FP program and _input_ can be any value. _Context_ is a plain object to act as an additional data channel to a program. 79 | 80 | 81 | ### Promise or callback 82 | Built-in operators of the current implementation will do their jobs synchronously. However, if you add your own customized operators and they would do things asynchronously, those asynchronous operators should return a promise to JSON-FP. JSON-FP knows how to deal with promise. 83 | 84 | Other than operators, JSON-FP supports both promise and callbacks to deal with those asynchronous situations. In other words, if you expect the computation of your JSON-FP expression will be done asynchroously, you can either use the promise style: 85 | 86 | jsonfp.apply(input, expr).then(function(value) { 87 | // value is the result 88 | }); 89 | 90 | or the callback style: 91 | 92 | jsonfp.apply(input, expr, function(err, value) { 93 | // value is the result 94 | }); 95 | 96 | 97 | 98 | ## JSON-FP expression 99 | A JSON-FP expression is a JSON object with a single property. The property key is the "operator" which works on the input data while the property value specifies options to that operator. So a JSON-FP expression is as simple as: 100 | 101 | {op: options} 102 | 103 | The interesting part is that _options_ can be yet another JSON-FP expression. A typical example would be the case of applying the "map" operator. Assuming we have a list of documents and we want to remove all properties but the title property for each document. Below is what you can do with JSON-FP: 104 | 105 | {"map": 106 | {"pick": "title"} 107 | } 108 | 109 | By repeatedly substituting expression value with another JSON-FP expression, an expression as simple as {op: options} can turn into a really sophisticated program. 110 | 111 | 112 | ### Expression input 113 | When running a JSON-FP program, you have to provide the initial input data. After that, the data flow will become implicit. For example, when operators are chained, input data will be fed to the head of the chain and every succeeding operator will get its input from the output of its predecesor. Another example is the 'map' operator which will iterate through its input (could be an array or a collection) and feed each element as the input to its child expression. In other words, the parent expression will decide how to provide input to its child expressions without needing explicit specifications by application developers. 114 | 115 | However, if you want to change the implicit data flow, you can use the 'chain' operator to achieve that. Below is an example: 116 | 117 | { 118 | '->': [ 119 | {name: 'David', age: 28}, 120 | {getter: 'age'} 121 | ] 122 | } 123 | 124 | If you evaluate the above expression, the result will be 28. The first one of the chained expressions is becoming the input to the second expression. With that technique, you can change expression input as you wish. 125 | 126 | 127 | ### Evaluation 128 | Anything that you send as an expression to JSON-FP will be evaluated recursively until a scalar is found. For example: 129 | 130 | var expr = {name: 'David'}, 131 | result = jsonfp.apply('Jones', expr); 132 | console.log( JSON.stringify(result) ); 133 | 134 | In the above example, JSON-FP will try to evalute {name: 'David'} but figure out there is nothing it should do. So the output will be exactly the same as the input. 135 | 136 | However, if you do something like: 137 | 138 | var expr = {name: {add: ' Cooper'}}, 139 | result = jsonfp.apply('David', expr); 140 | console.log( JSON.stringify(result) ); 141 | 142 | This time JSON-FP finds something to work on and {name: 'David Cooper'} will be the output. 143 | 144 | 145 | ### Variables 146 | It's possible to refer to variables in a JSON-FP expression. They are expressed as a string with a leading '$' sign. For example, '$title' and '$in.id' are all valid variables. The syntax of JSON-FP variables is defined as such because we do not want to break the JSON syntax. 147 | 148 | Input to an JSON-FP expression can be referred as '$in'. If input is a plain object with a 'title' property, you can refe to that title property as '$in.title'. 149 | 150 | Besides input, you can put all other variables in the context variable. Below is an exmaple showing you how to use context variables: 151 | 152 | var expr = {name: {add: '$firstName'}}, 153 | ctx = {firstName: 'David '}, 154 | result = jsonfp.apply(ctx, 'Jones', expr); 155 | 156 | // Below should print out 'David Jones' 157 | console.log( JSON.stringify(result) ); 158 | 159 | Note that in the above example we provide a context variable (ctx) to supply the first name to the expression. The result will be printed out on console as {name: 'David Jones'}. 160 | 161 | 162 | #### Setting variables 163 | A JSON-FP expression can unfold itself as a series (or a tree of series) of expressions. Input to the expression will water fall through or be transformed by those series of expressions. Sometimes we need to keep the intermediate results and recall them when necessary. To do so, the intermediate results should be able to be saved. 164 | 165 | You ca save the intermediate results in the context variable like the following: 166 | 167 | { 168 | $name: {getter: 'name'} 169 | } 170 | 171 | That will pick up the _name_ property of input and store it into the context variable. The saved value will be visible to successive expressions. For example: 172 | 173 | var expr = 174 | {eval: 175 | { 176 | $name: {getter: 'name'}, 177 | $hobby: {getter: 'hobby'}, 178 | response: { 179 | "->" : [ 180 | ['$name', ' likes ', '$hobby'], 181 | {reduce: 'add'} 182 | ] 183 | } 184 | } 185 | }; 186 | 187 | will save the name and hobby of a person to the _'$name'_ and _'$hobby'_ variables respectively. However, if you do: 188 | 189 | var expr = 190 | {eval: 191 | { 192 | response: { 193 | "->" : [ 194 | ['$name', ' likes ', '$hobby'], 195 | {reduce: 'add'} 196 | ] 197 | }, 198 | $name: {getter: 'name'}, 199 | $hobby: {getter: 'hobby'} 200 | } 201 | }; 202 | 203 | You'll **not** get the expected result because of the reversed execution sequence. 204 | 205 | Also note that, variables will not show up in results. The above example if done correctly will generate the result as (assuming input is {name: 'David', hobby: 'meditation'}) 206 | 207 | { 208 | response: 'David likes meditation' 209 | } 210 | 211 | rather than 212 | 213 | { 214 | $name: 'David', 215 | $hobby: 'meditation', 216 | response: 'David likes meditation' 217 | } 218 | 219 | You won't have _'$name'_ and _'$hobby'_ as part of the return value. 220 | 221 | 222 | ### Operators 223 | What operators are available in a JSON-FP runtime will decide its capabilities, and that can be fully customized. Customizing the set of supported operators is a very important feature because it allows a server (or any JSON-FP runtime) to gauge what capacity it's willing to offer. 224 | 225 | The current implementation comes with more than 30 operators. To view the list and usage of these built-in operators, please refre to this [page](https://github.com/benlue/jsonfp/blob/master/doc/builtInOp.md) for details. 226 | 227 | If you need some functions not supported by the built-in operators, you can simply add your own! Below is an example: 228 | 229 | var jsonfp = require('jsonfp'); 230 | 231 | jsonfp.addMethod('average', function(input, x) { 232 | return (input + x) / 2; 233 | }); 234 | 235 | JSON-FP by default will evaluate the expression option before feeding the option to an operator. If you do not want JSON-FP to evaluate the expression option, you could do the following: 236 | 237 | jsonfp.addMethod('getter', {op: doGetter, defOption: true}); 238 | 239 | function doGetter(input, expr) { 240 | return input[expr]; 241 | }; 242 | 243 | If you specify the _defOption_ property as true when adding methods to the JSON-FP runtime, that will stop JSON-FP from automaticaly evaluating the option. 244 | 245 | 246 | ### API 247 | 248 | 249 | #### jsonfp.init(options) 250 | Before you evaluate any JSON-FP expressions, you should call jsonfp.init() to preload the built-in operators. You can also preload just part of the built-in operators by specifying the needed operators in the _options_ parameter. For example: 251 | 252 | jsonfp.init(['arithmetic', 'arrays', 'collections']); 253 | 254 | The above example does not load "comparators". 255 | 256 | 257 | #### jsonfp.apply(ctx, input, expr, cb) 258 | Evaluates a JSON-FP expression and returns the result. If any JSON-FP expression is performed asynchronously, the return value will be a promise. If you prefer the callback style, you can put the callback function as the 4th parameter to the function call. _cb(err, value)_ is an optional callback function which will take an error object (if any) and the result as the second parameter. Note that if invoked with a callback function, _jsonfp.apply()_ will no longer return a value. 259 | 260 | Parameter _ctx_ is a context variable, _input_ will be fed to the expression, and _expr_ is the JSON-FP expression to be evaluated. The _ctx_ parameter is optional. 261 | 262 | 263 | #### jsonfp.isExpression(expr) 264 | Checks to see if _expr_ is a evaluable JSON-FP expression. 265 | 266 | 267 | #### jsonfp.addMethod(name, func) 268 | Adds an customized operator to the JSON-FP runtime. _name_ is the operator name, and _func_ can be a Javascript function or a plain object with the following properties: 269 | 270 | + op: a Javascript function to carry out the operator functions. 271 | + defOption: if true, the expression option will not be evaluated before the expression is evaluated. For operators such as "map" or "filter" which will treat the option as a JSON-FP expression, this property should be set to true. 272 | 273 | 274 | #### jsonfp.removeMethod(name) 275 | Removes an operator from a JSON-FP runtime. 276 | 277 | 278 | ## Customizing JSON-FP 279 | One of JSON-FP's great features is the ability to extend the language by adding customized operators. Since v0.1.1, a recommended way to develop and install customized operators have been introduced. With that, third party functions can be grouped and installed as packages, and you can freely customize the JSONF-FP runtime as you need. For details, you can check "[Developing and Installing Packages](https://github.com/benlue/jsonfp/blob/master/doc/extPackage.md)" for details. 280 | 281 | 282 | ## Metaprogramming 283 | JSON-FP is homoiconic. That is you can manipulate JSON-FP programs the exact same way as you manipulate JSON objects. Becuase of that, using JSON-FP to do metaprogramming could be very easy. 284 | 285 | The latest release (0.1.0) supports two metaprogramming related operators: **convert** and **formula**. The **formula** operator helps developers to define expression formula which can be reused and served as building blocks for more complicated JSON-FP programs. The **convert** operator can be used to apply those formula defined by the **formula** operator. To know more about how to use these two operators, you may want to check this [example](https://github.com/benlue/jsonfp-examples/blob/master/examples/metapro/metaProgram.js). 286 | 287 | There are some simple examples in [testLamda.js](https://github.com/benlue/jsonfp/blob/master/test/testLamda.js) if you're interested. 288 | 289 | 290 | -------------------------------------------------------------------------------- /ReleaseNote.md: -------------------------------------------------------------------------------- 1 | ## 0.2.2 2 | 3 | + The 'infix' operator was introduced. Normally JSON-FP will evaluate the expression option before evaluating the expression itself. That may lead to a problem that we can only work on input as a whole instead of working on part of the input. The 'infix' operator can solve the problem. 4 | 5 | + Added the 'head', 'tail' and 'bucket' operator. 6 | 7 | + Cleaned up error handling. 8 | 9 | + Added the quicksort and word-count test cases. 10 | 11 | ## 0.2.1 12 | 13 | + The concept of streaming data was introduced. In particular, the iterator stream was introduced to void the usage of for-loop. 14 | 15 | + The supprot of the {_input: , _expr: } format to change expression input has been dropped in favor of the chain operator. The same effect can be achieved by specifying input as the first expression of the chained expressions. 16 | 17 | ## 0.2.0 18 | 19 | + Starting from this release, it's required to run _jsonfp.init()_ before the runtime engine can start evaluating expressions. 20 | 21 | + Using **layers** to provide customized functions to a JSON-FP runtime. 22 | 23 | + Added the 'random' and 'clone' operator. Check [Built-in Operators](https://github.com/benlue/jsonfp/blob/master/doc/builtInOp.md) for details. 24 | 25 | + The 'true' or 'false' expression of the 'if' operator should be lazily evaluated. This release correctly made it so. 26 | 27 | + Fixed bugs where the value '0' was mistaken as null or undefined. 28 | 29 | ## 0.1.2 30 | 31 | + The following operators are added: bitwise and, or, exclusive-or. 32 | 33 | ## 0.1.1 34 | 35 | + Formalized how customized operators are implemented and installed to a JSON-FP runtime. 36 | 37 | ## 0.1.0 38 | 39 | + Added the **formula** operator for metaprogramming. Developers now can use the **formula** operator to define a JSON-FP formula and use the **convert** operator to apply a formula. 40 | 41 | ## 0.0.9 42 | 43 | + Added the [if](https://github.com/benlue/jsonfp/blob/master/doc/builtInOp.md#if) operator. 44 | + Allowed variable settings through the context variable. 45 | + Allowed expression option to be null. 46 | 47 | ## 0.0.8 48 | 49 | + '->' is an alias of the 'chain' operator, if that makes things more readable. 50 | + The _reduce_ operator now also has a shorter format. Check [builtinOp](https://github.com/benlue/jsonfp/blob/master/doc/builtInOp.md#reduce) for details. 51 | 52 | ## 0.0.7 53 | 54 | + Allowed expression input to be null. 55 | + Besides promise, developers can also use callback to handle asynchronous calls. 56 | 57 | ## 0.0.6 58 | 59 | + Built-in operators have been substantially expanded and those operators are grouped into 'arithmetic', 'arrays', 'collections' and 'comparators' modules. 60 | + An operator can specify if JSON-FP should evaluate its expression value or not. With such a feature, JSON-FP developers could omit the 'def' operator and make JSON-FP expressions more succint. 61 | 62 | ## 0.0.5 63 | 64 | + Added promise support. With the new feature, a JSON-FP program can intake promised input. Also, if you add customized JSON-FP operators (transformations) whose output is a promise, JSON-FP can handle them properly. 65 | 66 | ## 0.0.4 67 | 68 | + cleared up syntax (please check README for details) 69 | + supported alpha-conversion 70 | + more operators are supported (add, def, convert, eval, pluck, reduce). 71 | + much more detailed description in README 72 | 73 | ## 0.0.3 74 | 75 | + provided some basic documentation in README 76 | + built-in operators are tested 77 | + it's a working prototype now. 78 | 79 | ## 0.0.1 80 | 81 | + worked with test cases. 82 | -------------------------------------------------------------------------------- /doc/builtInOp.md: -------------------------------------------------------------------------------- 1 | JSON-FP Built-In Operators 2 | ========================== 3 | 4 | JSON-FP comes with built-in operators. These operators are categorized into 5 different groups. 5 | 6 | + [Core](#core) 7 | + [->](#chainAbbr) 8 | + [chain](#chain) 9 | + [convert](#convert) 10 | + [formula](#formula) 11 | + [eval](#eval) 12 | + [if](#if) 13 | + [map](#map) 14 | + [Arithmetic](#arithmetic) 15 | + [add](#add) 16 | + [subtract](#subtract) 17 | + [multiply](#multiply) 18 | + [divide](#divide) 19 | + [random](#ramdon) 20 | + [min](#min) 21 | + [max](#max) 22 | + [and](#and) 23 | + [or](#or) 24 | + [bitwise and](#bwAnd) 25 | + [bitwise or](#bwOr) 26 | + [bitwise exclusive-or](#bwXor) 27 | + [Arrays](#arrays) 28 | + [compact](#compact) 29 | + [difference](#difference) 30 | + [flatten](#flatten) 31 | + [intersection](#intersection) 32 | + [take](#take) 33 | + [union](#union) 34 | + [zipObject](#zipObject) 35 | + [Collections](#collections) 36 | + [clone](#clone) 37 | + [filter](#filter) 38 | + [find](#find) 39 | + [getter](#getter) 40 | + [merge](#merge) 41 | + [omit](#omit) 42 | + [pick](#pick) 43 | + [pluck](#pluck) 44 | + [reduce](#reduce) 45 | + [size](#size) 46 | + [where](#where) 47 | + [Comparators](#comparators) 48 | + [==](#equal) 49 | + [!=](#notEqual) 50 | + [>](#gt) 51 | + [>=](#gte) 52 | + [<](#lt) 53 | + [<=](#lte) 54 | 55 | 56 | ## Core 57 | The core group provides the minimum operators for a JSON-FP runtime. 58 | 59 | 60 | ### -> 61 | This is the shorthand notation of the "chain operator" which is explained below. 62 | 63 | 64 | ### chain 65 | The chain operator can take an array of JSON-FP expressions and sequentially evaluate each expression with each operator taking its input from the output of its predecessor. Its syntax is as: _[{op1: option1}, {op2: option2}, ...{opN, optionN}]_. The output of a chain operator is the output of the last operator in the chain. 66 | 67 | Release 0.0.8 makes '->' the alias of 'chain', so 68 | 69 | {chain: [ 70 | {getter: 'salary'}, 71 | {'>': 100000} 72 | ]} 73 | 74 | is the same as 75 | 76 | {'->': [ 77 | {getter: 'salary'}, 78 | {'>': 100000} 79 | ]} 80 | 81 | 82 | ### convert 83 | The convert operator can be used to perform variable substitutions (this is actually JSON-FP's way of suppoting the alpha-conversion in lamda calculus). The convert option has the following properties: 84 | 85 | + var: this property value contains the mapping between each substitutable variable and its value. 86 | + formula: the formula expression defined by the **formula** operator. See [below](#formula) about how to define a formula. 87 | 88 | Example: 89 | 90 | var f = { 91 | formula: { 92 | var: ['@prop'], 93 | expr: {getter: '@prop'} 94 | } 95 | }; 96 | 97 | var expr = { 98 | convert: { 99 | var: {'@prop': 'name'}, 100 | formula: f 101 | } 102 | }, 103 | input = { 104 | name: 'John', 105 | title: 'CEO' 106 | }, 107 | result = jsonfp.apply(input, expr); 108 | 109 | The above example will take the _name_ property from the input object, so the result will be 'John'. 110 | 111 | 112 | ### formula 113 | The formula operator can be used to define a formula. A formula is a JSON-FP expression with substitutable variables. By subsittuting variables in a JSON-FP expression, we'll be able to convert a formula into various expressions suitable for different tasks. A formula expression is usually in the following format: 114 | 115 | { 116 | formula: { 117 | var: ['@prop1', '@prop2', ...], 118 | expr: { 119 | op1: '@prop1' 120 | } 121 | } 122 | } 123 | 124 | That is the option to the formula operator is another JSON object with _var_ and _expr_ properties. The _var_ property should be an array of substitutable variable names. It's recommended to put a '@' sign in front of the variable name so we can easily identify them as substitutable variables. The _expr_ property is the JSON-FP expression containing variables to be substituted. 125 | 126 | 127 | ### eval 128 | This operator will iterate through every property of its _option_ and try to evaluate each property as a JSON-FP expression. Note that JSON-FP will automatically evaluate _option_ so the **eval** operator is not needed in most cases. It will be used mostly in meta-programming. For example, evaluating a JSON-FP expression produced by alpha-conversion like: _{eval: {convert: {...}}_. 129 | 130 | 131 | ### if 132 | indicates a conditional statement as described in the option. _option_ should be an array with at least two elements. The first element (JSON-FP expression) in the array is the conditional expression. The second element (JSON-FP expression) is the expression to be evaluated when the condition is true, and the third element (JSON-FP expression) will be evaluated when the condition is false. The third element can be missing if the **else** statement is not needed. 133 | 134 | When the **else** expression is not needed, the input will be the return value of this **if** expression. 135 | 136 | 137 | ### map 138 | This operator will take each element of the input array and apply it to the given JSON-FP expression. 139 | 140 | 141 | ## Arithmetic 142 | 143 | 144 | ### add 145 | This operator will add _option_ to _input_. The order may be important when adding strings. It's _input_ + _option_. 146 | 147 | 148 | ### subtract 149 | The subtract operator subtracts _option_ from _input_. It only support numeric values. 150 | 151 | 152 | ### multiply 153 | Multiplying _input_ with _option_. Both operands should be numeric values. 154 | 155 | 156 | ### divide 157 | Dividing _input_ by _option_. 158 | 159 | 160 | ### random 161 | Returning a random number by calling Math.random(). 162 | 163 | 164 | ### min 165 | Returning the minimum of _input_ and _option_. 166 | 167 | 168 | ### max 169 | Returning the maximum of _input_ and _option_. 170 | 171 | 172 | ### and 173 | This operator will return the logical "and" of _input_ and _option_. However, if _input_ is an object and _option_ is an array of strings, each element of the _option_ array will be used as the property key to the _input_ object and logical "and" is applied on property values. For example, consider the following sample code: 174 | 175 | var input = {'name: 'David', 'age': 28, 'isMarried': false}, 176 | expr = {and: ['age', 'isMarried']; 177 | 178 | var result = jsonfp.apply(input, expr); 179 | 180 | The result should be false because _(28 && false)_ should yield false. 181 | 182 | 183 | ### or 184 | This operator will return the logical "or" of _input_ and _option_. Similar to the "and" operator, if _input_ is an object and _option_ is an array of strings, each element of the _option_ array will be used as the property key to the _input_ object and logical "or" is applied on property values. 185 | 186 | 187 | ### bitwise and 188 | This operator will return value of bitwise ANDing _input_ and _option_. 189 | 190 | 191 | ### bitwise or 192 | This operator will return value of bitwise ORing _input_ and _option_. 193 | 194 | 195 | ### bitwise exclusive-or 196 | This operator will return the bitwise exclusive or of _input_ and _option_. 197 | 198 | 199 | ## Arrays 200 | 201 | 202 | ### compact 203 | Returns an array with false values (false, null, 0 or '') removed. 204 | 205 | 206 | ### difference 207 | Output an array by excluding all values specified in the option array. Syntax: _{difference: [...]}_ 208 | 209 | 210 | ### flatten 211 | Flats the input array to be an array of single level. 212 | 213 | 214 | ### intersection 215 | Intersects the input array with the option array. 216 | 217 | 218 | ### take 219 | This operator returns the first _n_ elements of the input array. _n_ is specified in the option. 220 | 221 | 222 | ### union 223 | Output an array of unique values from the _input_ and _option_ arrays. 224 | 225 | 226 | ### zipObject 227 | Creates an object with keys from the input array and values from the option array. 228 | 229 | 230 | ## Collections 231 | 232 | 233 | ### clone 234 | Cloning the input object. 235 | 236 | 237 | ### filter 238 | Returns an array of elements filtered by _option_. If _option_ is a plain object, the saved element should contain the same property as _option_. If _option_ is a JSON-FP expression, the input elements will become the input to the JSON-FP expression and the evaluation result will decide if that element will be kept. 239 | 240 | 241 | ### find 242 | Similar to filter except that the first matched element will be returned. 243 | 244 | 245 | ### getter 246 | Returns a property value of an input object. For example: 247 | 248 | var input = { 249 | project: 'JSON-FP', 250 | language: 'Javascript' 251 | }, 252 | expr = {getter: 'language'}; 253 | 254 | var result = jsonfp.apply(input, expr); 255 | 256 | The result is 'Javascript'. 257 | 258 | 259 | ### merge 260 | This operator will recursively merge the option object into the input object. 261 | 262 | 263 | ### omit 264 | Creates an object by removing properties specified by _option_ from the _input_ object. Besides being a string value, _option_ can also be a JSON-FP expression. If _option_ is a JSON-FP expression, the omit operator will iterate every key/value pair of the input object, and send the pair as input to the JSON-FP expression. Below is an example: 265 | 266 | var data = {name: 'David', project: 'newsql', age: 42}, 267 | expr = {omit: 268 | {chain: [ 269 | {getter: 'key'}, 270 | {'==': 'project'} 271 | ]} 272 | }, 273 | result = jsonfp.apply( data, expr ); 274 | 275 | In the above example, the {chain: ...} expression will be invoked three times with each time receving input as {key: 'name', value: 'David'}, {key: 'project', value: 'newsql'} and {key: 'age', value: 42} respectively. The execution result will be {name: 'David', age: 42}. 276 | 277 | 278 | ### pick 279 | This operator creates a new object with properties specified in the option. Option can be a single string of an array of strings. 280 | 281 | 282 | ### pluck 283 | retrieves a property from the input. The property name is specified in _option_. 284 | 285 | 286 | ### reduce 287 | This operator performs the reduce funtion. _option_ is usually a JSON-FP expression. Starting from v0.0.8, you can simply use an operator name to replace the intended JSON-FP expression. That is: 288 | 289 | {reduce: 290 | add: '$reduceValue' 291 | } 292 | 293 | is now the same as: 294 | 295 | {reduce: 'add'} 296 | 297 | 298 | 299 | ### size 300 | This operator returns the size of the input collection (array). 301 | 302 | 303 | ### where 304 | This operator will perform deep comparison on each element with the option object and returns those elements having the equivalent property value as the option object. 305 | 306 | 307 | ## Comparators 308 | 309 | 310 | ### == 311 | This operator compares if _input_ and _option_ is equivalent (don't have to be identical). 312 | 313 | 314 | ### != 315 | Checking if _input_ and _option_ are not equal. 316 | 317 | 318 | ### > 319 | if _input_ is greater than _option_. 320 | 321 | 322 | ### >= 323 | if _input_ is greater than or equal to _option_. 324 | 325 | 326 | ### < 327 | if _input_ is less than _option_. 328 | 329 | 330 | ### <= 331 | if _input_ is less than or equal to _option_. 332 | 333 | If the above description is too brief, you may want to refer [Lo-Dash](https://lodash.com/docs#pick) documentation. Most array and collection operators listed above are realized by Lo-Dash. -------------------------------------------------------------------------------- /doc/extPackage.md: -------------------------------------------------------------------------------- 1 | Developing and Installing Packages 2 | ================================== 3 | 4 | One of JSON-FP's many interesting features is the ability to extend the language by adding customized operators. Before release 0.1.1, this was done by the _addMethod()_ function. Even though the _addMethod()_ function can effectively make the job done, things may go wild if you have to add customized operators from various parties. Below we'll describe a recommended way to develop and install customized operators. 5 | 6 | ## Put Operators In A Package 7 | Instead of freely adding customized operators one by one to a JSON-FP runtime, you may want to group operators in a package and install them all at once. A package is nothing more than a node module with an exported _install()_ function. The sample code below can be used as a boilerplate for writing your own customized package: 8 | 9 | exports.install = function(jsonfp, pkgName) { 10 | pkgName = (pkgName || 'default_pkg_name') + '/'; 11 | 12 | // now one by one install each operator to the JSON-FP runtime 13 | jsonfp.addMethod( pkgPath + 'opName1', opFunction1); 14 | ... 15 | }; 16 | 17 | where **jsonfp** is a JSON-FP runtime and **pkgName** is the given package name. As you can see from the sample code, if the package name is not given, the package itself can offer the default name. 18 | 19 | ## Install A Package 20 | If a JSON-FP package is implmented as described above, installation will be very easy. A package can be installed into a JSON-FP runtime as: 21 | 22 | var jsonfp = require('jsonfp'), 23 | myPackage = require('./myPackage.js'); 24 | 25 | jsonfp.install('myPack', myPackage); 26 | 27 | 28 | ## Invoke The Customized Operators 29 | Once a package is installed, you can easily invoke those package operators in your JSON-FP expressions. Use the above example, the installed operators can be invoked like: 30 | 31 | var expr = {'myPack/opName1': op_options} 32 | 33 | That's it. Using packages to group operators not just makes customization more managable, but also creates name spaces for each group of operators. That allows us to safely incorporate additional functions from third parties. 34 | 35 | Finally, if you wonder why are we using '/' instead of '.' as a name seperator for a full operator name, the answer is we're preserving '.' for future use. To be compatible with future releases, we recommend to use '/' as the name seperator for a full operator name. -------------------------------------------------------------------------------- /lib/lamdaApp.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-fp 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var _ = require('lodash'), 8 | Promise = require('bluebird'); 9 | 10 | var coreMethods = { 11 | '->': {op: doChain, streamOk: true}, 12 | chain: {op: doChain, streamOk: true}, 13 | convert: {op: doConvert, defOption: true}, 14 | formula: {op: doFormula, defOption: true}, 15 | eval: {op: doEval}, 16 | if: {op: doIf, defOption: true}, 17 | infix: {op: doInfix, defOption: true}, 18 | map: {op: doMap, defOption: true, streamOk: true}, 19 | print: {op: doPrint, streamOk: true} 20 | }, 21 | knownMethods, 22 | knownLayers = []; 23 | 24 | exports.init = function(options) { 25 | options = options || {}; 26 | 27 | if (options.modules) { 28 | // if options given, will force a reset 29 | knownMethods = _.clone(coreMethods); 30 | for (var i in options.modules) { 31 | var modName = options.modules[i], 32 | idx = modName.lastIndexOf('.'); 33 | 34 | if (idx > 0) { 35 | var pkgName = modName.substring(0, idx); 36 | exports.install(pkgName, require(pkgName)); 37 | } 38 | else { 39 | var fpath = util.format('./modules/%s.js', modName); 40 | require(fpath).install(this); 41 | } 42 | } 43 | } 44 | 45 | if (options.layers) { 46 | for (var i in options.layers) { 47 | var layerInfo = options.layers[i]; 48 | 49 | // make sure the layer info is correct 50 | if (layerInfo.prefix && layerInfo.module && layerInfo.module.invoke) { 51 | layerInfo.module.install(this); 52 | knownLayers[layerInfo.prefix] = layerInfo.module; 53 | } 54 | } 55 | } 56 | 57 | if (!knownMethods) { 58 | knownMethods = _.clone(coreMethods); 59 | require('./modules/arithmetic.js').install(this); 60 | require('./modules/arrays.js').install(this); 61 | require('./modules/collections.js').install(this); 62 | require('./modules/comparators.js').install(this); 63 | require('./modules/stream.js').install(this, 'stream'); 64 | } 65 | }; 66 | 67 | 68 | /* 69 | * Install an external (customized) package to the JSON-FP runtime. 70 | */ 71 | exports.install = function(pkgName, nodeModule) { 72 | nodeModule.install( this, pkgName ); 73 | }; 74 | 75 | 76 | exports.apply = function(ctx, input, prog, cb) { 77 | if (arguments.length < 3) { 78 | prog = input; 79 | input = ctx; 80 | ctx = null; 81 | } 82 | else if (arguments.length === 3) { 83 | if (typeof prog === 'function') { 84 | cb = prog; 85 | prog = input; 86 | input = ctx; 87 | } 88 | } 89 | 90 | var result; 91 | try { 92 | //result = checkPromised( evalExpr( ctx || {}, input, prog ) ); 93 | result = evalExpr( ctx || {}, input, prog ); 94 | result = checkPromised( result ); 95 | if (cb) { 96 | if (result.then) 97 | return result.then(function(data) { 98 | cb(null, data); 99 | }).catch(function(err) { 100 | cb(err); 101 | }); 102 | else 103 | return cb( null, result ); 104 | } 105 | } 106 | catch (err) { 107 | if (cb) 108 | return cb(err); 109 | 110 | return new Promise(function(resolve, reject) { 111 | reject( err ); 112 | }); 113 | } 114 | 115 | return result && result._next ? result._next(true) : result; 116 | }; 117 | 118 | 119 | exports.evaluate = evalExpr; 120 | 121 | 122 | function checkPromised(v) { 123 | if (_.isArray(v)) { 124 | for (var i in v) { 125 | if (v[i] && v[i].then) { 126 | v = Promise.all( v ); 127 | break; 128 | } 129 | } 130 | } 131 | else if (_.isPlainObject(v)) { 132 | for (var key in v) { 133 | if (v[key] && v[key].then) { 134 | v = Promise.props( v ); 135 | break; 136 | } 137 | } 138 | } 139 | 140 | return v; 141 | }; 142 | 143 | 144 | /** 145 | * Check to see if the input is a JSON-FP expression 146 | */ 147 | exports.isExpression = function(expr) { 148 | if (_.isPlainObject(expr)) { 149 | var keys = _.keys(expr); 150 | if (keys.length === 1) { 151 | var opName = keys[0]; 152 | return knownMethods[opName] || opName === 'def'; 153 | } 154 | } 155 | return false; 156 | }; 157 | 158 | 159 | exports.addMethod = function(name, f) { 160 | if (typeof f === 'function') 161 | f = {op: f}; 162 | knownMethods[name] = f; 163 | }; 164 | 165 | 166 | exports.removeMethod = function(name) { 167 | delete knownMethods[name]; 168 | }; 169 | 170 | 171 | function evalExpr(ctx, input, expr) { 172 | if (input === null || input === undefined) 173 | input = {}; 174 | 175 | if (input.then) { 176 | return input.then(function(data) { 177 | return evalExpr(ctx, data, expr); 178 | }); 179 | } 180 | 181 | if (_.isArray(expr)) { 182 | return checkPromised(_.map(expr, function(elem) { 183 | //console.log('sub-expression is\n%s', JSON.stringify(elem, null, 4)); 184 | return evalExpr(ctx, input, elem); 185 | })); 186 | } 187 | 188 | else if (_.isPlainObject(expr)) { 189 | //console.log('expresson is\n%s', JSON.stringify(expr, null, 4)); 190 | var keys = _.keys(expr); 191 | if (keys.length === 1) { 192 | var opName = keys[0]; 193 | if (opName === 'def') 194 | // do not evaluate. simply return the option 195 | return expr.def; 196 | else { 197 | var idx = opName.indexOf(':'); 198 | if (idx > 0) { 199 | // should relay to a layer 200 | var layerName = opName.substring(0, idx); 201 | if (knownLayers[layerName]) { 202 | var layerExpr = {}, 203 | option = expr[opName]; 204 | opName = opName.substring(idx+1); 205 | layerExpr[opName] = option; 206 | 207 | return knownLayers[layerName].invoke(ctx, input, layerExpr); 208 | } 209 | else 210 | //throw new Error('Unknown layer[' + layerName + ']'); 211 | throw {code: 1, name: 'runtime', message: 'Unknown layer[' + layerName + ']'}; 212 | } 213 | else { 214 | var f = knownMethods[opName]; 215 | if (f) { 216 | //console.log('executing [%s]', opName); 217 | var option = expr[opName]; 218 | if (option === null || option === undefined) 219 | option = {}; 220 | 221 | if (input._type === 'stream' && !f.streamOk) 222 | input = input._next(true); 223 | 224 | if (opName !== 'chain' && opName !== '->') { 225 | if (f.defOption) { 226 | // skip 'def' 227 | if (option.def) 228 | option = option.def; 229 | } 230 | else 231 | option = evalExpr(ctx, input, option); 232 | 233 | if (option === undefined || option === null) 234 | option = {}; 235 | } 236 | 237 | if (option.then) { 238 | return option.then(function(data) { 239 | return f.op.call(ctx, input, data); 240 | }); 241 | } 242 | 243 | return f.op.call(ctx, input, option); 244 | } 245 | } 246 | } 247 | } 248 | 249 | var obj = {}; 250 | _.keys(expr). 251 | map(function(k) { 252 | if (k[0] === '$') 253 | ctx[k.substring(1)] = evalExpr(ctx, input, expr[k]); 254 | else 255 | obj[k] = evalExpr(ctx, input, expr[k]); 256 | }); 257 | expr = obj; 258 | } 259 | else if (_.isString(expr)) { 260 | expr = evalString(ctx, input, expr); 261 | if (_.isPlainObject(expr) || _.isArray(expr)) 262 | return evalExpr( _.clone(ctx), input, expr ); 263 | } 264 | 265 | return expr; 266 | }; 267 | 268 | 269 | function evalString(ctx, input, expr) { 270 | if (expr.indexOf('$') >= 0) { 271 | //console.log('target expression: %s', expr); 272 | var varState = false, 273 | phrase = '', 274 | sum = ''; 275 | 276 | for (var i = 0, len = expr.length; i < len; i++) { 277 | var c = expr[i]; 278 | if (varState) { 279 | if (' /,?!@#%&*()-+<>'.indexOf(c) >= 0) { 280 | //console.log('phrase is: %s', phrase); 281 | if (phrase === 'in') 282 | sum += input + c; 283 | else if (phrase.indexOf('in.') === 0) 284 | sum += evalVar(input, phrase.substring(3)) + c; 285 | else 286 | sum += evalVar(ctx, phrase) + c; 287 | phrase = ''; 288 | varState = false; 289 | } 290 | else 291 | phrase += c; 292 | } 293 | else { 294 | if (c === '$') 295 | varState = true; 296 | else 297 | sum += c; 298 | } 299 | } 300 | 301 | if (varState) { 302 | //console.log('**phrase is: %s', phrase); 303 | var value = null; 304 | 305 | if (phrase === 'in') 306 | value = input ? (input._type === 'stream' ? input._next(true) : input) : null; 307 | else if (phrase.indexOf('in.') === 0) { 308 | if (input) 309 | value = evalVar(input._type === 'stream' ? input._next(true) : input, phrase.substring(3)); 310 | } 311 | else 312 | value = evalVar(ctx, phrase); 313 | 314 | expr = sum ? (sum + value) : value; 315 | } 316 | else 317 | expr = sum; 318 | //console.log('expr value is ' + expr); 319 | } 320 | return expr; 321 | }; 322 | 323 | 324 | function doChain(input, opArray) { 325 | //console.log('chainning input is\n%s', JSON.stringify(input, null, 4)); 326 | var ctx = this, 327 | i = 0, 328 | len = opArray.length; 329 | 330 | for (; i < len; i++) { 331 | //console.log('chain program......\n%s', JSON.stringify(opArray[i], null, 4)); 332 | var isStreamIn = input && input._type === 'stream', 333 | accu = isStreamIn ? input._accu : null; 334 | 335 | if (isStreamIn && input._isSource()) { 336 | var item; 337 | 338 | while (item = input._next()) { 339 | item._accu = accu; 340 | 341 | //console.log('stream item is\n%s', JSON.stringify(item, null, 4)); 342 | for (var j = i; j < len; j++) { 343 | item = input._asStream( evalExpr( ctx, item, opArray[j] ) ); 344 | item._accu = accu; 345 | //console.log('resulting item is\n%s', JSON.stringify(item, null, 4)); 346 | } 347 | accu = item._next(true); 348 | } 349 | return input._asStream(accu); 350 | } 351 | else { 352 | input = evalExpr( ctx, input, opArray[i] ); 353 | if (input && input._type === 'stream') 354 | input._accu = accu; 355 | } 356 | //console.log('chain result......\n%s', JSON.stringify(result, null, 4)); 357 | } 358 | 359 | return input; 360 | }; 361 | 362 | 363 | function doConvert(input, option) { 364 | //console.log('convert input\n%s', JSON.stringify(input, null, 4)); 365 | var v = option.var, 366 | vars = [], 367 | formula = {expr: option.formula.formula.expr, var: vars}; 368 | 369 | _.keys(v).map(function(k) { 370 | vars.push(k); 371 | }) 372 | 373 | var expr = doFormula(v, formula); 374 | //console.log('converted expression is\n%s', JSON.stringify(expr, null, 4)); 375 | return evalExpr( this, input, expr ); 376 | }; 377 | 378 | 379 | function doFormula(input, func) { 380 | //console.log('convert input\n%s', JSON.stringify(input, null, 4)); 381 | var v = func.var, 382 | expr = func.expr; 383 | 384 | if (_.isArray(v)) { 385 | _.map(v, function(k) { 386 | expr = substituteVar(k, expr, input[k]); 387 | }); 388 | return expr; 389 | } 390 | 391 | return substituteVar(v, expr, input); 392 | }; 393 | 394 | 395 | /** 396 | * Check if property values of 'param' is a JSON-fp program, and 397 | * resolve it if it's so. 398 | */ 399 | function doEval(input, expr) { 400 | //console.log('eval input...\n%s', JSON.stringify(input, null, 4)); 401 | //console.log('eval expr...\n%s', JSON.stringify(expr, null, 4)); 402 | return evalExpr(this, input, expr); 403 | }; 404 | 405 | 406 | function doIf(input, expr) { 407 | if (!_.isArray(expr)) 408 | //throw new Error("The option to an 'if' operator should be an array."); 409 | throw {code: 10, name: 'if', message: "The option to an 'if' operator should be an array."}; 410 | 411 | if (expr.length < 2) 412 | //throw new Error("The option should be like [{condition_expr}, {true_expr}"); 413 | throw {code: 10, name: 'if', message: "The option should be like [{condition_expr}, {true_expr}"}; 414 | 415 | var cond = checkPromised( evalExpr(this, input, expr[0]) ); 416 | if (cond) { 417 | if (cond.then) { 418 | var ctx = this; 419 | return cond.then(function(c) { 420 | if (c) 421 | return evalExpr(ctx, input, expr[1]); 422 | return expr.length > 2 ? evalExpr(ctx, input, expr[2]) : input; 423 | }) 424 | } 425 | return evalExpr(this, input, expr[1]); 426 | } 427 | 428 | return expr.length > 2 ? evalExpr(this, input, expr[2]) : input; 429 | }; 430 | 431 | 432 | function doInfix(input, expr) { 433 | if (_.isArray(expr)) { 434 | var ctx = this, 435 | p = [], 436 | op = Object.keys(expr[1])[0]; 437 | 438 | p.push( evalExpr(ctx, input, expr[0]) ); 439 | p.push( evalExpr(ctx, input, expr[1][op]) ); 440 | 441 | return Promise.all(p).then(function(ary) { 442 | var nExpr = {}; 443 | nExpr[op] = ary[1]; 444 | 445 | return evalExpr( ctx, ary[0], nExpr ); 446 | }); 447 | } 448 | 449 | throw {code: 10, name: 'infix', message: 'The argument of infix operator should be an array of JSON-FP expressions'}; 450 | }; 451 | 452 | 453 | function doMap(input, p) { 454 | var ctx = this; 455 | 456 | if (input._type === 'stream') 457 | return input._asStream( evalExpr(ctx, input._next(), p) ); 458 | 459 | return checkPromised(_.map(input, function(item) { 460 | //console.log('map item is\n%s', JSON.stringify(item, null, 4)); 461 | //console.log('program is\n%s', JSON.stringify(p, null, 4)); 462 | return evalExpr( ctx, item, p ); 463 | })); 464 | }; 465 | 466 | 467 | function doPrint(input, expr) { 468 | var isPrinted = false; 469 | 470 | if (_.isArray(expr)) { 471 | console.log.apply(this, expr); 472 | isPrinted = true; 473 | } 474 | else if (_.isPlainObject(expr)) { 475 | if (expr.length > 0) { 476 | console.log( JSON.stringify(expr, null, 4) ); 477 | isPrinted = true; 478 | } 479 | } 480 | 481 | if (!isPrinted) { 482 | if (input) { 483 | if (input._type === 'stream') 484 | console.log( JSON.stringify(input._next(true), null, 4)); 485 | else 486 | console.log( JSON.stringify(input, null, 4)); 487 | } 488 | else 489 | console.log( 'null' ); 490 | } 491 | 492 | return input; 493 | }; 494 | 495 | 496 | function evalVar(ctx, s) { 497 | //console.log('context is \n%s', JSON.stringify(ctx, null, 4)); 498 | var parts = s.split('.'), 499 | v = ctx; 500 | for (var i in parts) 501 | v = v[parts[i]]; 502 | 503 | //console.log('evaluating [%s] into %s', s, JSON.stringify(v, null, 4)); 504 | return v; 505 | }; 506 | 507 | 508 | function substituteVar(v, expr, input) { 509 | //console.log('converting [%s] to [%s]', v, JSON.stringify(input, null, 4)); 510 | var nextExpr = expr; 511 | if (_.isString(expr)) 512 | nextExpr = expr === v ? input : expr; 513 | else if (_.isPlainObject(expr)) { 514 | nextExpr = {}; 515 | _.keys(expr).map(function(k) { 516 | nextExpr[k] = substituteVar(v, expr[k], input); 517 | }); 518 | } 519 | else if (_.isArray(expr)) 520 | nextExpr = _.map(expr, function(e) { 521 | return substituteVar(v, e, input); 522 | }); 523 | 524 | return nextExpr; 525 | }; 526 | -------------------------------------------------------------------------------- /lib/modules/arithmetic.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-fp 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var arithmetic = {}; 8 | arithmetic.install = function(jsonfp) { 9 | jsonfp.addMethod('add', doAdd); 10 | jsonfp.addMethod('random', doRandom); 11 | jsonfp.addMethod('subtract', doSubtract); 12 | jsonfp.addMethod('multiply', doMultiply); 13 | jsonfp.addMethod('divide', doDivide); 14 | jsonfp.addMethod('min', doMin); 15 | jsonfp.addMethod('max', doMax); 16 | jsonfp.addMethod('and', doAnd); 17 | jsonfp.addMethod('or', doOr); 18 | jsonfp.addMethod('&', doBitAnd); 19 | jsonfp.addMethod('|', doBitOr); 20 | jsonfp.addMethod('^', doBitXor); 21 | }; 22 | 23 | module.exports = arithmetic; 24 | 25 | function doRandom(input, range) { 26 | var r = Math.random(); 27 | return (typeof range === 'number') ? range * r : r; 28 | }; 29 | 30 | function doAdd(input, x) { 31 | if (input.constructor === Array && x.constructor === Array) 32 | return input.concat(x); 33 | 34 | return input + x; 35 | }; 36 | 37 | function doSubtract(input, x) { 38 | return input - x; 39 | }; 40 | 41 | function doMultiply(input, x) { 42 | return input * x; 43 | }; 44 | 45 | function doDivide(input, x) { 46 | return input / x; 47 | }; 48 | 49 | function doMin(input, x) { 50 | return Math.min(x, input); 51 | }; 52 | 53 | function doMax(input, x) { 54 | return Math.max(x, input); 55 | }; 56 | 57 | function doAnd(input, x) { 58 | //console.log('AND: input:%s, x:%s', input, JSON.stringify(x)); 59 | var arrayClaz = Object.prototype.toString.call(x).slice(8, -1); 60 | if (arrayClaz === 'Array') { 61 | var objClaz = Object.prototype.toString.call(input).slice(8, -1); 62 | if (objClaz !== 'Object') 63 | throw new Error('input to [AND] should be an object when the option is an array'); 64 | 65 | // using elements in array 'x' as the key to the input object 66 | var result = true; 67 | for (var i = 0, len = x.length; result && i < len; i++) { 68 | result &= input[x[i]]; 69 | } 70 | return result; 71 | } 72 | 73 | return input && x; 74 | }; 75 | 76 | function doOr(input, x) { 77 | var arrayClaz = Object.prototype.toString.call(x).slice(8, -1); 78 | if (arrayClaz === 'Array') { 79 | var objClaz = Object.prototype.toString.call(input).slice(8, -1); 80 | if (objClaz !== 'Object') 81 | throw new Error('input to [OR] should be an object when the option is an array'); 82 | 83 | // using elements in array 'x' as the key to the input object 84 | var result = false; 85 | for (var i = 0, len = x.length; !result && i < len; i++) { 86 | result |= input[x[i]]; 87 | } 88 | return result; 89 | } 90 | 91 | return input || x; 92 | }; 93 | 94 | function doBitAnd(input, x) { 95 | return input & x; 96 | }; 97 | 98 | function doBitOr(input, x) { 99 | return input | x; 100 | }; 101 | 102 | function doBitXor(input, x) { 103 | return input ^ x; 104 | }; -------------------------------------------------------------------------------- /lib/modules/arrays.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-fp 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var _ = require('lodash'); 8 | 9 | var arrays = {}, 10 | jsonfp; 11 | 12 | arrays.install = function(jfp) { 13 | jsonfp = jfp; 14 | jsonfp.addMethod('compact', doCompact); 15 | jsonfp.addMethod('difference', doDifference); 16 | jsonfp.addMethod('flatten', doFlatten); 17 | jsonfp.addMethod('head', doHead); 18 | jsonfp.addMethod('intersection', doIntersection); 19 | jsonfp.addMethod('take', doTake); 20 | jsonfp.addMethod('tail', doTail); 21 | jsonfp.addMethod('union', doUnion); 22 | jsonfp.addMethod('zipObject', doZipObject); 23 | }; 24 | 25 | module.exports = arrays; 26 | 27 | function doCompact(input, diff) { 28 | return _.compact(input); 29 | }; 30 | 31 | 32 | function doDifference(input, diff) { 33 | return _.difference(input, diff); 34 | }; 35 | 36 | 37 | function doFlatten(input, isDeep) { 38 | return _.flatten(input, isDeep); 39 | }; 40 | 41 | 42 | function doHead(input, option) { 43 | return _.first(input); 44 | }; 45 | 46 | 47 | function doIntersection(input, another) { 48 | return _.intersection(input, another); 49 | }; 50 | 51 | 52 | function doTake(input, count) { 53 | return _.take(input, count); 54 | }; 55 | 56 | 57 | function doTail(input, option) { 58 | return _.tail(input); 59 | }; 60 | 61 | 62 | function doUnion(input, another) { 63 | return _.union(input, another); 64 | }; 65 | 66 | 67 | function doZipObject(input, params) { 68 | return _.zipObject(input, params); 69 | }; -------------------------------------------------------------------------------- /lib/modules/collections.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-FP 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var _ = require('lodash'), 8 | Promise = require('bluebird'); 9 | 10 | var collections = {}, 11 | jsonfp; 12 | 13 | collections.install = function(jfp) { 14 | jsonfp = jfp; 15 | jsonfp.addMethod('bucket', {op: doBucket, defOption: true}); 16 | jsonfp.addMethod('clone', {op: doClone, defOption: true}); 17 | jsonfp.addMethod('filter', {op: doFilter, defOption: true}); 18 | jsonfp.addMethod('find', {op: doFind, defOption: true}); 19 | jsonfp.addMethod('getter', doGetter); 20 | jsonfp.addMethod('merge', doMerge); 21 | jsonfp.addMethod('omit', {op: doOmit, defOption: true}); 22 | jsonfp.addMethod('pick', doPick); 23 | jsonfp.addMethod('pluck', doPluck); 24 | jsonfp.addMethod('reduce', {op: doReduce, defOption: true, streamOk: true}); 25 | jsonfp.addMethod('size', doSize); 26 | jsonfp.addMethod('where', doWhere); 27 | }; 28 | 29 | module.exports = collections; 30 | 31 | function doBucket(input, expr) { 32 | if (_.isArray(expr)) { 33 | var ctx = _.clone(this), 34 | idx = []; 35 | 36 | for (var k in input) { 37 | var v = input[k], 38 | cond = [], 39 | isResolved = false; 40 | 41 | for (var i in expr) { 42 | if (isResolved) 43 | cond.push( false ); 44 | else { 45 | var e = expr[i]; 46 | if (_.isArray(e)) e = e[0]; 47 | 48 | var result = jsonfp.evaluate(ctx, v, e); 49 | cond.push( result ); 50 | if (!result.then) 51 | isResolved = result; 52 | } 53 | } 54 | 55 | idx.push( 56 | Promise.all(cond).then(function(cond) { 57 | for (var i in cond) 58 | if (cond[i]) 59 | return i; 60 | 61 | return -1; 62 | }) 63 | ); 64 | } 65 | 66 | return Promise.all(idx).then(function(idx) { 67 | var bucket = [], 68 | bucketCount = expr.length; 69 | for (var i = 0; i < bucketCount; i++) 70 | bucket.push( [] ); 71 | 72 | for (var k in input) 73 | bucket[idx[k]].push( input[k] ); 74 | 75 | var result = []; 76 | for (var i in bucket) { 77 | var e = expr[i]; 78 | if (_.isArray(e) && e.length > 1) { 79 | e = e[1]; 80 | result.push( jsonfp.evaluate(ctx, bucket[i], e) ); 81 | } 82 | else 83 | result.push( bucket[i] ); 84 | } 85 | return Promise.all( result ); 86 | }); 87 | } 88 | else 89 | //throw new Error('Bucket option should be an array'); 90 | throw {code: 40, name: 'bucket', message: 'Bucket option should be an array'}; 91 | }; 92 | 93 | 94 | function doClone(input, expr) { 95 | return input ? _.clone(input) : null; 96 | }; 97 | 98 | 99 | function doFilter(input, expr) { 100 | var ctx = _.clone(this); 101 | return _.filter(input, function(elem) { 102 | return jsonfp.evaluate( ctx, elem, expr ); 103 | }); 104 | }; 105 | 106 | 107 | function doFind(input, p) { 108 | var ctx = _.clone(this); 109 | return _.find(input, function(item) { 110 | return jsonfp.evaluate(ctx, item, p); 111 | }); 112 | }; 113 | 114 | 115 | function doGetter(input, expr) { 116 | //console.log('getter: key is %s, input is\n%s', expr, JSON.stringify(input, null, 4)); 117 | return input[expr]; 118 | }; 119 | 120 | 121 | function doMerge(input, source) { 122 | //console.log(JSON.stringify(this.item, null, 4)); 123 | return _.merge( input, source ); 124 | }; 125 | 126 | 127 | function doOmit(input, p) { 128 | if (jsonfp.isExpression(p)) { 129 | var ctx = _.clone(this); 130 | return _.omit(input, function(value, key) { 131 | return jsonfp.evaluate(ctx, {key: key, value: value}, p); 132 | }); 133 | } 134 | 135 | return _.omit( input, p ); 136 | }; 137 | 138 | 139 | function doPick(input, selectors) { 140 | return _.pick(input, selectors); 141 | }; 142 | 143 | 144 | function doPluck(input, prop) { 145 | return _.pluck(input, prop); 146 | }; 147 | 148 | 149 | function doReduce(input, p) { 150 | //console.log('reduce input is\n%s', JSON.stringify(input, null, 4)); 151 | var ctx = _.clone(this); 152 | 153 | if (_.isString(p)) { 154 | var expr = {}; 155 | expr[p] = '$reduceValue'; 156 | 157 | if (jsonfp.isExpression(expr)) 158 | p = expr; 159 | else 160 | //throw new Error(p + ' is not a known operator.'); 161 | throw {code: 40, name: 'reduce', message: p + ' is not a known operator.'}; 162 | } 163 | 164 | if (input._type === 'stream') { 165 | //console.log('reduce value...%d, accumulated: %d', input._next(), input._accu); 166 | 167 | if (input._accu) { 168 | ctx.reduceValue = input._accu; 169 | var res = jsonfp.evaluate(ctx, input._next(), p); 170 | if (res.then) 171 | return res.then(function(accu) { 172 | return accu; 173 | }); 174 | else 175 | return res; 176 | } 177 | else 178 | return input._next(); 179 | } 180 | else 181 | return _.reduce(input, function(accu, cur) { 182 | ctx.reduceValue = cur; 183 | return jsonfp.evaluate(ctx, accu, p); 184 | }); 185 | }; 186 | 187 | 188 | function doSize(input, type) { 189 | return type === 'each' ? _.map(input, function(i) {return _.size(i)}) : _.size(input); 190 | }; 191 | 192 | 193 | function doWhere(input, prop) { 194 | //console.log('where prop is\n%s', JSON.stringify(prop, null, 4)); 195 | return _.where(input, prop); 196 | }; -------------------------------------------------------------------------------- /lib/modules/comparators.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-fp 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var _ = require('lodash'); 8 | 9 | var comparators = {}; 10 | comparators.install = function(jsonfp) { 11 | jsonfp.addMethod('==', doEqual); 12 | jsonfp.addMethod('!=', doNotEqual); 13 | jsonfp.addMethod('>', doGreater); 14 | jsonfp.addMethod('>=', doGreaterEqual); 15 | jsonfp.addMethod('<', doLess); 16 | jsonfp.addMethod('<=', doLessEqual); 17 | }; 18 | 19 | module.exports = comparators; 20 | 21 | function doEqual(a, b) { 22 | var claz = Object.prototype.toString.call(b).slice(8, -1); 23 | if (claz === 'Object') { 24 | return _.isEqual(a, b); 25 | } 26 | else if (claz === 'Date') { 27 | if (Object.prototype.toString.call(a).slice(8, -1) !== 'Date') 28 | a = new Date(a); 29 | return a.getTime() === b.getTime(); 30 | } 31 | return a == b; 32 | }; 33 | 34 | function doNotEqual(a, b) { 35 | var claz = Object.prototype.toString.call(b).slice(8, -1); 36 | if (claz === 'Object') { 37 | return !_.isEqual(a, b); 38 | } 39 | else if (claz === 'Date') { 40 | if (Object.prototype.toString.call(a).slice(8, -1) !== 'Date') 41 | a = new Date(a); 42 | return a.getTime() !== b.getTime(); 43 | } 44 | 45 | return a != b; 46 | }; 47 | 48 | function doGreater(a, b) { 49 | if (b instanceof Date) { 50 | if (!(a instanceof Date)) 51 | a = new Date(a); 52 | return a.getTime() > b.getTime(); 53 | } 54 | return a > b; 55 | }; 56 | 57 | function doGreaterEqual(a, b) { 58 | if (b instanceof Date) { 59 | if (!(a instanceof Date)) 60 | a = new Date(a); 61 | return a.getTime() >=b.getTime(); 62 | } 63 | return a >= b; 64 | }; 65 | 66 | function doLess(a, b) { 67 | if (b instanceof Date) { 68 | if (!(a instanceof Date)) 69 | a = new Date(a); 70 | return a.getTime() < b.getTime(); 71 | } 72 | return a < b; 73 | }; 74 | 75 | function doLessEqual(a, b) { 76 | if (b instanceof Date) { 77 | if (!(a instanceof Date)) 78 | a = new Date(a); 79 | return a.getTime() <= b.getTime(); 80 | } 81 | return a <= b; 82 | }; -------------------------------------------------------------------------------- /lib/modules/stream.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-FP 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var _ = require('lodash'); 8 | 9 | var jsonfp; 10 | 11 | exports.install = function(jfp, pkgName) { 12 | jsonfp = jfp; 13 | pkgName = (pkgName || 'stream') + '/'; 14 | 15 | jsonfp.addMethod(pkgName + 'iterator', doIterator); 16 | }; 17 | 18 | 19 | function doIterator(input, option) { 20 | if (option.start === undefined || option.start === null) 21 | option.start = 1; 22 | if (option.end === undefined) 23 | throw {code: 60, name: 'stream/iterator', message: 'The end point of the sequence should be given.'}; 24 | 25 | return { 26 | _type: 'stream', 27 | _config: { 28 | start: option.start, 29 | end: option.end, 30 | inc: option.inc || 1, 31 | cur: null 32 | }, 33 | _isSource: function() { 34 | return true; 35 | }, 36 | _next: function(valueOnly) { 37 | var v; 38 | if (this._config.cur) { 39 | this._config.cur += this._config.inc; 40 | v = this._config.cur <= this._config.end ? this._config.cur : null; 41 | } 42 | else 43 | v = this._config.cur = this._config.start; 44 | return valueOnly ? v : (v ? toIteratorStream(v) : null); 45 | }, 46 | _asStream: function(v) { 47 | if (v && v.then) 48 | return v.then(function(data) { 49 | return toIteratorStream(data); 50 | }); 51 | 52 | return (v && v._type) ? v : toIteratorStream(v); 53 | } 54 | }; 55 | }; 56 | 57 | 58 | function toIteratorStream(v) { 59 | return { 60 | _type: 'stream', 61 | _isSource: function() { 62 | return false; 63 | }, 64 | _next: function(valueOnly) { 65 | if (valueOnly) 66 | return v; 67 | 68 | if (v) 69 | return v._type ? v : toIteratorStream(v); 70 | return toIteratorStream(v); 71 | }, 72 | _asStream: function(v) { 73 | if (v && v.then) 74 | return v.then(function(data) { 75 | return toIteratorStream(data); 76 | }); 77 | 78 | return (v && v._type) ? v : toIteratorStream(v); 79 | } 80 | }; 81 | }; -------------------------------------------------------------------------------- /lib/modules/test/testArithmetic.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-FP 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var assert = require('assert'), 8 | jsonfp = require('../../lamdaApp.js'); 9 | 10 | before(function() { 11 | // arithmetic is a built-in module 12 | jsonfp.init(); 13 | }); 14 | 15 | 16 | describe('Testing arithmetic...', function() { 17 | it('Add', function() { 18 | var p = {add: ' world'}, 19 | result = jsonfp.apply('Hello', p); 20 | assert.equal( result, 'Hello world', 'Hello world'); 21 | 22 | p.add = 4; 23 | assert.equal( jsonfp.apply(4, p), 8, 'result is 8'); 24 | }); 25 | 26 | it('Subtract', function() { 27 | var p = {subtract: 2}; 28 | assert.equal(jsonfp.apply(4, p), 2, 'result is 2'); 29 | }); 30 | 31 | it('Multiply', function() { 32 | var p = {multiply: 2}; 33 | assert.equal(jsonfp.apply(4, p), 8, 'result is 8'); 34 | }); 35 | 36 | it('Divide', function() { 37 | var p = {divide: 2}; 38 | assert.equal(jsonfp.apply(4, p), 2, 'result is 2'); 39 | }); 40 | 41 | it('Min', function() { 42 | var p = {min: 2}; 43 | assert.equal(jsonfp.apply(4, p), 2, 'result is 2'); 44 | }); 45 | 46 | it('Max', function() { 47 | var p = {max: 2}; 48 | assert.equal(jsonfp.apply(16, p), 16, 'result is 16'); 49 | }); 50 | 51 | it('Logical AND', function() { 52 | var p = {and: 2}, 53 | result = jsonfp.apply(16, p); 54 | //console.log('result is %s', result); 55 | assert(result, 'should be true'); 56 | assert(!jsonfp.apply(0, p), 'should be false'); 57 | 58 | p.and = false; 59 | assert(!jsonfp.apply(true, p), 'should be false'); 60 | }); 61 | 62 | it('Logical OR', function() { 63 | var p = {or: 2}, 64 | result = jsonfp.apply(16, p); 65 | //console.log('result is %s', result); 66 | assert(result, 'should be true'); 67 | assert(jsonfp.apply(0, p), 'should be true'); 68 | 69 | p.or = false; 70 | assert(jsonfp.apply(true, p), 'should be true'); 71 | }); 72 | }); -------------------------------------------------------------------------------- /lib/modules/test/testArrays.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-fp 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var assert = require('assert'), 8 | jsonfp = require('../../lamdaApp.js'); 9 | 10 | before(function() { 11 | // arithmetic is a built-in module 12 | jsonfp.init(); 13 | }); 14 | 15 | describe('Testing array operators...', function() { 16 | 17 | it('union', function() { 18 | var data = [1, 2, 3], 19 | expr = {union: [3, 4, 5]}, 20 | result = jsonfp.apply( data, expr ); 21 | //console.log(JSON.stringify(result, null, 4)); 22 | assert.equal(result.length, 5, '5 elements'); 23 | 24 | data = ['a', 'b', 'c']; 25 | expr.union = ['b', 'd']; 26 | result = jsonfp.apply( data, expr ); 27 | //console.log(JSON.stringify(result, null, 4)); 28 | assert.equal(result.length, 4, '4 elements'); 29 | }); 30 | }); -------------------------------------------------------------------------------- /lib/modules/test/testCollections.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-fp 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var assert = require('assert'), 8 | jsonfp = require('../../lamdaApp.js'); 9 | 10 | before(function() { 11 | // arithmetic is a built-in module 12 | jsonfp.init(); 13 | }); 14 | 15 | describe('Testing collections...', function() { 16 | it('filter', function() { 17 | var data = [1, 2, 3, 4, 5], 18 | p = {filter: {'==': 3}}, 19 | result = jsonfp.apply( data, p ); 20 | console.log( JSON.stringify(result, null, 4) ); 21 | assert.equal( result.length, 1, 'have 1 item'); 22 | assert.equal( result[0], 3, 'one element with value 3'); 23 | 24 | data = [ 25 | {name: 'John', project: 'jsonfp'}, 26 | {name: 'David', project: 'newsql'} 27 | ]; 28 | 29 | p = {filter: 30 | {chain: [ 31 | {pick: 'name'}, 32 | {'==': {name: 'John'}} 33 | ]} 34 | }; 35 | 36 | result = jsonfp.apply( data, p ); 37 | //console.log( JSON.stringify(result, null, 4) ); 38 | assert.equal( result[0].name, 'John', 'name is John'); 39 | assert.equal( result[0].project, 'jsonfp', 'project is jsonfp'); 40 | }); 41 | 42 | it('find', function() { 43 | var data = [ 44 | {name: 'John', project: 'jsonfp', age: 23}, 45 | {name: 'David', project: 'newsql', age: 42}, 46 | {name: 'Kate', project: 'jsonfp', age: 18} 47 | ], 48 | p = {find: {project: 'newsql'}}, 49 | result = jsonfp.apply( data, p ); 50 | //console.log( JSON.stringify(result, null, 4) ); 51 | assert.equal(result.name, 'John', 'Matched person is John'); 52 | 53 | p = {find: 54 | {chain: [ 55 | {getter: 'age'}, 56 | {'>' : 30} 57 | ]} 58 | }; 59 | result = jsonfp.apply( data, p ); 60 | //console.log( JSON.stringify(result, null, 4) ); 61 | assert.equal(result.name, 'David', 'David is matched.'); 62 | }); 63 | 64 | it('Getter', function() { 65 | var data = [ 66 | {name: 'John', project: 'jsonfp', age: 23}, 67 | {name: 'David', project: 'newsql', age: 42}, 68 | {name: 'Kate', project: 'jsonfp', age: 18} 69 | ], 70 | p = {filter: 71 | {chain: [ 72 | {project: 73 | {chain: [ 74 | {'getter': 'project'}, 75 | {'==': 'jsonfp'} 76 | ]}, 77 | age: 78 | {chain: [ 79 | {'getter': 'age'}, 80 | {'<': 30} 81 | ]} 82 | }, 83 | {'and': ['project', 'age']} 84 | ]} 85 | }, 86 | result = jsonfp.apply( data, p ); 87 | //console.log( JSON.stringify(result, null, 4) ); 88 | assert.equal(result.length, 2, '2 elements'); 89 | assert.equal(result[0].name, 'John', 'first match is John'); 90 | assert.equal(result[1].name, 'Kate', 'second match is Kate'); 91 | }); 92 | 93 | it('omit', function() { 94 | var data = {name: 'David', project: 'newsql', age: 42}, 95 | expr = {omit: 'age'}, 96 | result = jsonfp.apply( data, expr ); 97 | //console.log( JSON.stringify(result, null, 4) ); 98 | assert(!result.age, 'age property should be deleted'); 99 | 100 | expr = {omit: 101 | {chain: [ 102 | {getter: 'key'}, 103 | {'==': 'project'} 104 | ]} 105 | }; 106 | result = jsonfp.apply( data, expr ); 107 | //console.log( JSON.stringify(result, null, 4) ); 108 | assert.equal(result.name, 'David', 'name is David'); 109 | assert(!result.project, 'the project property should be deleted'); 110 | }); 111 | 112 | it('Pluck', function() { 113 | var data = [{name: 'John', project: 'coServ'}], 114 | p = {pluck: 'name'}, 115 | result = jsonfp.apply( data, p ); 116 | //console.log( JSON.stringify(result, null, 4) ); 117 | assert.equal( result.length, 1, 'have 1 item'); 118 | assert.equal( result[0], 'John', 'name is John'); 119 | }); 120 | }); -------------------------------------------------------------------------------- /lib/modules/test/testComparators.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-fp 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var assert = require('assert'), 8 | jsonfp = require('../../lamdaApp.js'); 9 | 10 | before(function() { 11 | // comparators is a built-in module 12 | jsonfp.init(); 13 | }); 14 | 15 | 16 | describe('Testing comparators...', function() { 17 | it('Equal', function() { 18 | var p = {'==': 4}; 19 | 20 | var result = jsonfp.apply( 4, p ); 21 | //console.log( JSON.stringify(result, null, 4) ); 22 | assert(result, 'is equal'); 23 | assert(!jsonfp.apply( 8, p ), 'is true'); 24 | 25 | var y = new Date('1/1/2015'); 26 | p = {'==': y}; 27 | assert(jsonfp.apply('1/1/2015', p), 'the same date'); 28 | 29 | p = {'==': {name: 'John', project: 'jsonfp'}}; 30 | result = jsonfp.apply({name: 'John', project: 'jsonfp'}, p); 31 | assert( result, 'the same object'); 32 | }); 33 | 34 | it('Not equal', function() { 35 | var p = {'!=': 4}; 36 | 37 | var result = jsonfp.apply( 4, p ); 38 | //console.log( JSON.stringify(result, null, 4) ); 39 | assert(!result, 'is equal'); 40 | assert(jsonfp.apply( 8, p ), 'is true'); 41 | 42 | var y = new Date('1/1/2015'); 43 | p = {'!=': y}; 44 | assert(!jsonfp.apply('1/1/2015', p), 'the same date'); 45 | }); 46 | 47 | it('Greater', function() { 48 | var p = {'>': 4}; 49 | 50 | var result = jsonfp.apply( 4, p ); 51 | //console.log( JSON.stringify(result, null, 4) ); 52 | assert(!result, 'is false'); 53 | assert(jsonfp.apply( 8, p ), 'is true'); 54 | 55 | var y = new Date('1/1/2015'); 56 | p = {'>': y}; 57 | assert(jsonfp.apply('2/1/2015', p), '2/1 is greater than 1/1'); 58 | }); 59 | 60 | it('Greater or equal', function() { 61 | var p = { 62 | '>=': 4 63 | }; 64 | 65 | var result = jsonfp.apply( 4, p ); 66 | //console.log( JSON.stringify(result, null, 4) ); 67 | assert(result, 'is true'); 68 | assert(jsonfp.apply( 8, p ), 'is true'); 69 | 70 | var y = new Date('1/1/2015'); 71 | p = {'>=': y}; 72 | assert(jsonfp.apply('1/1/2015', p), '1/1 is greater than or equal to 1/1'); 73 | }); 74 | 75 | it('Less than', function() { 76 | var p = { 77 | '<': 4 78 | }; 79 | 80 | var result = jsonfp.apply( 4, p ); 81 | //console.log( JSON.stringify(result, null, 4) ); 82 | assert(!result, 'is false'); 83 | assert(!jsonfp.apply( 8, p ), 'is false'); 84 | 85 | var y = new Date('1/1/2015'); 86 | p = {'<': y}; 87 | assert(!jsonfp.apply('2/1/2015', p), '2/1 is not less than 1/1'); 88 | }); 89 | 90 | it('Less than or equal', function() { 91 | var p = { 92 | '<=': 4 93 | }; 94 | 95 | var result = jsonfp.apply( 4, p ); 96 | //console.log( JSON.stringify(result, null, 4) ); 97 | assert(result, 'is true'); 98 | assert(!jsonfp.apply( 8, p ), 'is false'); 99 | 100 | var y = new Date('1/1/2015'); 101 | p = {'<=': y}; 102 | assert(jsonfp.apply('2/1/2014', p), '2014 is less than or equal to 2015'); 103 | }); 104 | }); -------------------------------------------------------------------------------- /lib/modules/test/testCtrlFlow.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-FP 3 | * authors: Ben Lue 4 | * license: GPL 2.0 5 | * Copyright(c) 2015 Gocharm Inc. 6 | */ 7 | var assert = require('assert'), 8 | jsonfp = require('../../lamdaApp.js'); 9 | 10 | before(function() { 11 | // arithmetic is a built-in module 12 | jsonfp.init(); 13 | }); 14 | 15 | 16 | describe('Testing control flow ...', function() { 17 | 18 | it('Infix', function() { 19 | var expr = { 20 | infix: ['$in.x', {add: '$in.y'}] 21 | }, 22 | input = {x: 2, y: 8}; 23 | 24 | jsonfp.apply(input, expr).then(function(result) { 25 | //console.log( result ); 26 | assert.equal(result, 10, '2 + 8 is 10'); 27 | }); 28 | }); 29 | 30 | it('Infix error', function() { 31 | var expr = { 32 | infix: {add: '$in.y'} 33 | }, 34 | input = {x: 2, y: 8}; 35 | 36 | jsonfp.apply(input, expr).then(function(result) { 37 | assert(false, 'shoud not reach here'); 38 | }). 39 | catch(function(err) { 40 | //console.log( JSON.stringify(err) ); 41 | assert('We should reach here'); 42 | }); 43 | }); 44 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonfp", 3 | "version": "0.2.2", 4 | "description": "Functional programming in JSON.", 5 | "engines": "0.10.21", 6 | "main": "lib/lamdaApp.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/benlue/jsonfp.git" 10 | }, 11 | "keywords": [ 12 | "node.js", 13 | "functional programming", 14 | "metaprogramming", 15 | "json" 16 | ], 17 | "author": { 18 | "name": "Ben Lue, Gocharm Inc." 19 | }, 20 | "license": "GPL 2.0", 21 | "readmeFilename": "README.md", 22 | "dependencies": { 23 | "bluebird": "^2.6.4", 24 | "lodash": "^3.6.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/testLamda.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | lamda = require('../lib/lamdaApp.js'), 3 | Promise = require('bluebird'); 4 | 5 | var sampleList = [ 6 | {title: 'UX Guide', psnID: 24, page_id: 33, price: 23.90}, 7 | {title: 'Bitcoin', psnID: 48, page_id: 88, price: 29.50} 8 | ], 9 | sampleCtx = {}; 10 | 11 | before(function() { 12 | // install built-in modules 13 | lamda.init(); 14 | 15 | // add customized methods (functions) 16 | lamda.addMethod('data', doData); 17 | lamda.addMethod('isBargin', doBarginPromised); 18 | lamda.addMethod('promise', doPromise); 19 | }); 20 | 21 | describe('JSON-FP programming...', function() { 22 | it('map and pick', function() { 23 | // the following program is equivalent to: 24 | // list.map(function(page) { 25 | // return pick(page, 'title'); 26 | // }); 27 | var p = { 28 | map: {pick: 'title'} 29 | }; 30 | 31 | var result = lamda.apply( sampleCtx, sampleList, p ); 32 | //console.log( JSON.stringify(result, null, 4) ); 33 | assert.equal( result.length, 2, 'still have 2 items'); 34 | assert.equal( result[0].title, 'UX Guide', 'title not match'); 35 | assert(!result[0].psnID, 'psnID should be removed'); 36 | }); 37 | 38 | it('test object query', function() { 39 | // this sample program will find out who is working on the 'jsonfp' project and 40 | // is under age 30. 41 | var data = [ 42 | {name: 'John', project: 'jsonfp', age: 23}, 43 | {name: 'David', project: 'newsql', age: 42}, 44 | {name: 'Kate', project: 'jsonfp', age: 18} 45 | ], 46 | expr = { 47 | filter: 48 | {chain: [ 49 | {project: 50 | {chain: [ 51 | {'getter': 'project'}, 52 | {'==': 'jsonfp'} 53 | ]}, 54 | age: 55 | {chain: [ 56 | {'getter': 'age'}, 57 | {'<': 30} 58 | ]} 59 | }, 60 | {'and': ['project', 'age']} 61 | ]} 62 | }, 63 | result = lamda.apply( data, expr ); 64 | //console.log( JSON.stringify(result, null, 4) ); 65 | assert.equal(result.length, 2, '2 elements'); 66 | assert.equal(result[0].name, 'John', 'first match is John'); 67 | assert.equal(result[1].name, 'Kate', 'second match is Kate'); 68 | 69 | // or do it the other way 70 | expr = { 71 | filter: { 72 | '->': [ 73 | [ 74 | {chain: [ 75 | {'getter': 'project'}, 76 | {'==': 'jsonfp'} 77 | ]}, 78 | {'->': [ 79 | {'getter': 'age'}, 80 | {'<': 30} 81 | ]} 82 | ], 83 | {reduce: 'and'} 84 | ] 85 | } 86 | }; 87 | result = lamda.apply( data, expr ); 88 | //console.log( JSON.stringify(result, null, 4) ); 89 | assert.equal(result.length, 2, '2 elements'); 90 | assert.equal(result[0].name, 'John', 'first match is John'); 91 | assert.equal(result[1].name, 'Kate', 'second match is Kate'); 92 | }); 93 | 94 | it('add tags to each page in a list', function() { 95 | // the following program is equivalent to: 96 | // list.map(function(page) { 97 | // page.tagList = 98 | // api('/pageTag/list', {page_id: page.page_id}). 99 | // take( $inData.showTag ). 100 | // flatten('tword'); 101 | // return page; 102 | // }); 103 | var p = {map: 104 | {merge: 105 | {tagList: 106 | {chain: [ 107 | { 108 | data: { 109 | api: "pageTag/list", 110 | option: {page_id: '$in.page_id'} 111 | } 112 | }, 113 | {take: '$showTag'}, 114 | {map: {getter: 'tword'}} 115 | ] 116 | } 117 | } 118 | } 119 | }; 120 | 121 | // the {take: } expression needs a number. 122 | // we'll feed the number using the context ('ctx') as shown below. 123 | var ctx = {showTag: 2}, 124 | result = lamda.apply( ctx, sampleList, p ); 125 | 126 | //console.log( JSON.stringify(result, null, 4) ); 127 | assert.equal(result[0].tagList.length, 2, 'a page has two tags'); 128 | assert.equal(result[0].tagList[0], 'COIMOTION', 'tag is not correct'); 129 | assert.equal(result[1].tagList[0], 'Open Data', 'tag is not correct'); 130 | }); 131 | 132 | it('qualify page list with specified tags', function() { 133 | // the following program is equivalent to: 134 | // list.map(function(page) { 135 | // page.tagList = 136 | // api('/pageTag/list', {page_id: page.page_id}). 137 | // flatten('tword'); 138 | // return page; 139 | // }). 140 | // where({tagList: $inData.tags}); 141 | var p = {chain: 142 | [ 143 | {map: 144 | {merge: 145 | {tagList: 146 | {chain: 147 | [ 148 | { 149 | data: { 150 | api: 'pageTag/list', 151 | option: {page_id: '$in.page_id'} 152 | } 153 | }, 154 | { 155 | map: {getter: 'tword'} 156 | } 157 | ] 158 | } 159 | } 160 | } 161 | }, 162 | {where: 163 | {tagList: '$tags'} 164 | } 165 | ] 166 | }; 167 | 168 | var ctx = {tags: ['API', 'reactive']}, 169 | result = lamda.apply( ctx, sampleList, p ); 170 | //console.log('---- result ----\n%s', JSON.stringify(result, null, 4)); 171 | assert.equal(result.length, 1, 'only one match'); 172 | assert.equal(result[0].page_id, 33, 'wrong page'); 173 | 174 | ctx = {tags: ['COIMOTION']}, 175 | result = lamda.apply( ctx, sampleList, p ); 176 | //console.log('---- result ----\n%s', JSON.stringify(result, null, 4)); 177 | assert.equal(result.length, 2, 'has two matches'); 178 | }); 179 | 180 | it('test reduction', function() { 181 | var p = {chain: [ 182 | {formula: 183 | { 184 | var: 'x', 185 | expr: ['/Person/query/', 'x'] 186 | } 187 | }, 188 | {reduce: {add: '$reduceValue'}} 189 | ]}, 190 | result = lamda.apply(3, p); 191 | //console.log('result is: %s', result); 192 | assert.equal( result, '/Person/query/3', 'x not converted to 3'); 193 | }); 194 | 195 | it('if', function() { 196 | // The following code will find out which books are selling more than $25 197 | var expr = { 198 | '->': [ 199 | {map: 200 | {if: 201 | [ 202 | {'->': 203 | [ 204 | {getter: 'price'}, 205 | {'>': 25} 206 | ] 207 | }, 208 | {getter: 'title'}, 209 | '' 210 | ] 211 | } 212 | }, 213 | {compact: null} 214 | ] 215 | }; 216 | 217 | var result = lamda.apply(sampleList, expr); 218 | //console.log('---- result ----\n%s', JSON.stringify(result, null, 4)); 219 | assert.equal(result.length, 1, 'One match'); 220 | assert.equal(result[0], 'Bitcoin', 'Title is Bitcoin'); 221 | }); 222 | }); 223 | 224 | 225 | describe('JSON-fp meta programming...', function() { 226 | it('convert and evaluate', function() { 227 | // this example is equivalent to {map: {def: {pick: 'title'}}} 228 | // except that the actual program is obtained by alpha-conversion. 229 | // Once the program is derived, we use 'eval' to apply it. 230 | // The nice thing is that if we change 'expr' in the context variable, 231 | // we can generate a different program. 232 | var p = {eval: 233 | {'->': [ 234 | '$expr', 235 | {formula: 236 | { 237 | var: 'e', 238 | expr: {map: 'e'} 239 | } 240 | } 241 | ]} 242 | }, 243 | expr = {def: {pick: 'title'}}, 244 | ctx = {expr: expr}, 245 | result = lamda.apply(ctx, sampleList, p); 246 | 247 | //console.log('result is: %s', JSON.stringify(result, null, 4)); 248 | assert.equal( result.length, 2, 'still have 2 items'); 249 | assert.equal( result[0].title, 'UX Guide', 'title not match'); 250 | assert(!result[0].psnID, 'psnID should be removed'); 251 | }); 252 | }); 253 | 254 | 255 | describe('Hanlding promise/callback...', function(done) { 256 | it('Simple promise', function(done) { 257 | var p = {promise: 2}, 258 | result = lamda.apply(1, p); 259 | 260 | result.then(function(v) { 261 | //console.log('result:\n%s', JSON.stringify(result, null, 4)); 262 | assert.equal(v, 3, 'the result is 3'); 263 | done();v 264 | }); 265 | 266 | }); 267 | 268 | it('Simple promise with array input', function(done) { 269 | var p = {map: {promise: 2}}, 270 | result = lamda.apply([1, 2, 3], p); 271 | result.then(function(v) { 272 | assert.equal(v[0], 3, 'elem #1 is 3'); 273 | assert.equal(v[1], 4, 'elem #2 is 4'); 274 | assert.equal(v[2], 5, 'elem #3 is 5'); 275 | done(); 276 | }); 277 | }); 278 | 279 | it('Simple callback', function(done) { 280 | var p = {promise: 2}; 281 | 282 | lamda.apply(1, p, function(err, v) { 283 | assert.equal(v, 3, 'the result is 3'); 284 | done(); 285 | }); 286 | }); 287 | 288 | it('Simple callback with array input', function(done) { 289 | var p = {map: {promise: 2}}; 290 | 291 | lamda.apply([1, 2, 3], p, function(err, v) { 292 | assert.equal(v[0], 3, 'elem #1 is 3'); 293 | assert.equal(v[1], 4, 'elem #2 is 4'); 294 | assert.equal(v[2], 5, 'elem #3 is 5'); 295 | done(); 296 | }); 297 | }); 298 | 299 | it('No blocked callback', function(done) { 300 | var p = {max: 3}; 301 | 302 | lamda.apply(8, p, function(err, result) { 303 | assert.equal(result, 8, 'max of 3 and 8 is 8'); 304 | done(); 305 | }); 306 | }); 307 | 308 | it('if condition is promised', function(done) { 309 | // The following code will find out which books are selling more than $25 310 | var expr = { 311 | '->': [ 312 | {map: 313 | {if: 314 | [ 315 | {'->': 316 | [ 317 | {getter: 'price'}, 318 | {isBargin: 25} 319 | ] 320 | }, 321 | {getter: 'title'}, 322 | '' 323 | ] 324 | } 325 | }, 326 | {compact: null} 327 | ] 328 | }; 329 | 330 | var result = lamda.apply(sampleList, expr); 331 | result.then(function(books) { 332 | //console.log('---- result ----\n%s', JSON.stringify(books, null, 4)); 333 | assert.equal(books.length, 1, 'One match'); 334 | assert.equal(books[0], 'UX Guide', 'Title is UX Guide'); 335 | done(); 336 | }); 337 | 338 | 339 | }); 340 | }); 341 | 342 | 343 | function doData(input, p) { 344 | // assuming we'll query the 'pageTag' table to find the tags of a page 345 | //console.log('data option is\n%s', JSON.stringify(p, null, 4)); 346 | var pageID = p.option.page_id, 347 | tagList; 348 | 349 | if (pageID === 33) 350 | tagList = [ 351 | {tag_id: 1, tword: 'COIMOTION'}, 352 | {tag_id: 2, tword: 'API'}, 353 | {tag_id: 3, tword: 'reactive'} 354 | ]; 355 | else 356 | tagList = [ 357 | {tag_id: 4, tword: 'Open Data'}, 358 | {tag_id: 5, tword: 'Cloud'}, 359 | {tag_id: 1, tword: 'COIMOTION'} 360 | ]; 361 | 362 | return tagList; 363 | }; 364 | 365 | 366 | function doPromise(input, v) { 367 | return new Promise(function(resolve, reject) { 368 | setTimeout(function() { 369 | resolve(input + v); 370 | }, 20); 371 | }); 372 | }; 373 | 374 | 375 | function doBarginPromised(input, v) { 376 | return new Promise(function(resolve, reject) { 377 | setTimeout(function() { 378 | resolve(input < v); 379 | }, 20); 380 | }); 381 | }; -------------------------------------------------------------------------------- /test/testMeta.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | jsonfp = require('../lib/lamdaApp.js'); 3 | 4 | var pitchers = [ 5 | {name: 'Ventura', era: 3.2, salary: 500500}, 6 | {name: 'Price', era: 3.26, salary: 19750000}, 7 | {name: 'Kershaw', era: 1.77, salary: 32517428}, 8 | {name: 'Gray', era: 3.08, salary: 505000}, 9 | {name: 'Liriano', era: 3.38, salary: 11666666}, 10 | {name: 'Hammel', era: 3.47, salary: 23500000}, 11 | {name: 'Lester', era: 2.46, salary: 20000000}, 12 | {name: 'Bumgarner', era: 2.98, salary: 6950000}, 13 | {name: 'Chen', era: 3.54, salary: 4750000}, 14 | {name: 'Norris', era: 3.65, salary: 8800000} 15 | ]; 16 | 17 | 18 | before(function() { 19 | // install built-in modules 20 | jsonfp.init(); 21 | }); 22 | 23 | 24 | describe('Expression formula and metaprogramming...', function() { 25 | it('Using eval to perform variable substitution', function() { 26 | // this defines a expression formula to calculate the average of a certain property 27 | var avgTemplate = { 28 | formula: { 29 | var: '@prop', 30 | expr: { 31 | '->': [ 32 | [ 33 | {'->': [ 34 | {map: {getter: '@prop'}}, 35 | {reduce: 'add'} 36 | ]}, 37 | {size: null} 38 | ], 39 | {reduce: 'divide'} 40 | ] 41 | } 42 | } 43 | }; 44 | 45 | // now we'll use eval to perform the variable substitution 46 | var program = { 47 | $stat: { 48 | eval: { 49 | era: { 50 | eval: { 51 | '->': [ 52 | 'era', 53 | avgTemplate 54 | ] 55 | } 56 | }, 57 | salary: { 58 | eval: { 59 | '->': [ 60 | 'salary', 61 | avgTemplate 62 | ] 63 | } 64 | } 65 | } 66 | }, 67 | pitchers: { 68 | filter: { 69 | '->': [ 70 | [ 71 | {'->': [ 72 | {getter: 'era'}, 73 | {'<': '$stat.era'} 74 | ]}, 75 | {'->': [ 76 | {getter: 'salary'}, 77 | {'<': '$stat.salary'} 78 | ]} 79 | ], 80 | {reduce: 'and'} 81 | ] 82 | } 83 | } 84 | }; 85 | 86 | var ctx = {}, 87 | result = jsonfp.apply(ctx, pitchers, program); 88 | assert.equal(result.pitchers.length, 1, 'one match'); 89 | assert.equal(result.pitchers[0].name, 'Bumgarner', 'who is Bumgarner'); 90 | }); 91 | 92 | it('Using convert', function() { 93 | // this defines a template expression to calculate the average of a property 94 | var avgTemplate = { 95 | formula: { 96 | var: '@prop', 97 | expr: { 98 | '->': [ 99 | [ 100 | {'->': [ 101 | {map: {getter: '@prop'}}, 102 | {reduce: 'add'} 103 | ]}, 104 | {size: null} 105 | ], 106 | {reduce: 'divide'} 107 | ] 108 | } 109 | } 110 | }; 111 | 112 | // now we'll use eval to perform the variable substitution 113 | var program = { 114 | $stat: { 115 | era: { 116 | convert: { 117 | var: {'@prop': 'era'}, 118 | formula: avgTemplate 119 | } 120 | }, 121 | salary: { 122 | convert: { 123 | var: {'@prop': 'salary'}, 124 | formula: avgTemplate 125 | } 126 | } 127 | }, 128 | pitchers: { 129 | filter: { 130 | '->': [ 131 | [ 132 | {'->': [ 133 | {getter: 'era'}, 134 | {'<': '$stat.era'} 135 | ]}, 136 | {'->': [ 137 | {getter: 'salary'}, 138 | {'<': '$stat.salary'} 139 | ]} 140 | ], 141 | {reduce: 'and'} 142 | ] 143 | } 144 | } 145 | }; 146 | 147 | var ctx = {}, 148 | result = jsonfp.apply(ctx, pitchers, program); 149 | assert.equal(result.pitchers.length, 1, 'one match'); 150 | assert.equal(result.pitchers[0].name, 'Bumgarner', 'who is Bumgarner'); 151 | }); 152 | }); -------------------------------------------------------------------------------- /test/testObjQuery.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | jsonfp = require('../lib/lamdaApp.js'); 3 | 4 | before(function() { 5 | // install built-in modules 6 | jsonfp.init(); 7 | }); 8 | 9 | 10 | describe('Test object query', function() { 11 | it('A really complicated object query', function() { 12 | var expr = { 13 | "chain": [ 14 | [ 15 | { 16 | "chain": [ 17 | {"getter": "Person_id"}, 18 | {"==": 1} 19 | ] 20 | }, 21 | { 22 | "chain": [ 23 | [ 24 | { 25 | "chain": [ 26 | [ 27 | { 28 | "chain": [ 29 | {"getter": "ownPsn"}, 30 | {"==": 1} 31 | ] 32 | }, 33 | { 34 | "chain": [ 35 | {"getter": "mode"}, 36 | {"&": 16} 37 | ] 38 | } 39 | ], 40 | {"reduce": "and"} 41 | ] 42 | }, 43 | { 44 | "chain": [ 45 | [ 46 | { 47 | "chain": [ 48 | {"getter": "ownGrp"}, 49 | {"==": 100} 50 | ] 51 | }, 52 | { 53 | "chain": [ 54 | {"getter": "mode"}, 55 | {"&": 4} 56 | ] 57 | } 58 | ], 59 | {"reduce": "and"} 60 | ] 61 | }, 62 | { 63 | "chain": [ 64 | {"getter": "mode"}, 65 | {"&": 1} 66 | ] 67 | } 68 | ], 69 | {"reduce": "or"} 70 | ] 71 | } 72 | ], 73 | {"reduce": "and"} 74 | ] 75 | }, 76 | data = [ 77 | { 78 | "Person_id": 1, 79 | "name": "Mike", 80 | "dob": "1978-02-11T16:00:00.000Z", 81 | "gender": 1, 82 | "ownPsn": 1, 83 | "ownGrp": 100, 84 | "mode": 60, 85 | "salary": 110000 86 | } 87 | ]; 88 | var result = jsonfp.apply(data, {filter: expr}); 89 | //console.log(JSON.stringify(result, null, 4)); 90 | assert.equal(result.length, 1, '1 match'); 91 | }); 92 | }); -------------------------------------------------------------------------------- /test/testOp.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | lamda = require('../lib/lamdaApp.js'); 3 | 4 | describe('Test build-in operators...', function() { 5 | it('formula', function() { 6 | // alpha-conversion 7 | // case 1: expression is just a variable 8 | var p = {formula: 9 | { 10 | var: 'e', 11 | expr: 'e' 12 | } 13 | }; 14 | 15 | var result = lamda.apply( 'Hello', p ); 16 | assert.equal( result, 'Hello', 'it should become "Hello"'); 17 | 18 | // case 2: expression is an object 19 | p = {formula: 20 | { 21 | var: 'e', 22 | expr: {map: 'e'} 23 | } 24 | }; 25 | result = lamda.apply( {def: {pick: 'name'}}, p ); 26 | //console.log( JSON.stringify(result, null, 4) ); 27 | assert.equal(result.map.def.pick, 'name', 'pick names'); 28 | 29 | // case 3: expression is an array 30 | p = {formula: 31 | { 32 | var: 'e', 33 | expr: [123, 'e', 'xyz', 'e'] 34 | } 35 | }; 36 | result = lamda.apply( 'Hello', p ); 37 | assert.equal(result[1], 'Hello', 'elem #1 is hello'); 38 | assert.equal(result[3], 'Hello', 'elem #3 is hello'); 39 | 40 | // case 4: multiple substitution 41 | p = {formula: 42 | { 43 | var: ['x', 'y'], 44 | expr: [123, 'x', 'xyz', 'y'] 45 | } 46 | }; 47 | 48 | result = lamda.apply({x: 'Hello', y: 'world'}, p); 49 | assert.equal(result[1], 'Hello', 'elem #1 is hello'); 50 | assert.equal(result[3], 'world', 'elem #3 is world'); 51 | }); 52 | 53 | it('difference', function() { 54 | var list = [1, 2, 3, 4, 5], 55 | p = { 56 | difference: [2, 4] 57 | }; 58 | 59 | var result = lamda.apply( list, p ); 60 | assert.equal(result.length, 3, 'should have 3 elements'); 61 | //console.log( JSON.stringify(result, null, 4) ); 62 | 63 | p.difference = '$diffAry'; 64 | var ctx = {diffAry: [2,4]}; 65 | result = lamda.apply( ctx, list, p ); 66 | //console.log( JSON.stringify(result, null, 4) ); 67 | assert.equal(result.length, 3, 'should have 3 elements'); 68 | }); 69 | 70 | it('where', function() { 71 | var list = [ 72 | {name: 'John', project: 'coServ'}, 73 | {name: 'David', project: 'react'} 74 | ], 75 | p = { 76 | where: {project: 'coServ'} 77 | }; 78 | 79 | var result = lamda.apply( list, p ); 80 | //console.log( JSON.stringify(result, null, 4) ); 81 | assert(result[0], 'should be true'); 82 | assert(!result[1], 'should be false'); 83 | 84 | p.where = {project: '$project'}; 85 | var ctx = {project: 'coServ'}; 86 | result = lamda.apply( ctx, list, p ); 87 | assert(result[0], 'should be true'); 88 | assert(!result[1], 'should be false'); 89 | }); 90 | 91 | it('intersection', function() { 92 | var list = [1, 2, 3, 4, 5], 93 | p = {intersection: [1, 5]}; 94 | 95 | var result = lamda.apply( list, p ); 96 | assert.equal(result.length, 2, 'should have 2 elements'); 97 | //console.log( JSON.stringify(result, null, 4) ); 98 | 99 | var ctx = {target: [1, 5]}; 100 | p.intersection = '$target'; 101 | result = lamda.apply( ctx, list, p ); 102 | assert.equal(result.length, 2, 'should have 2 elements'); 103 | }); 104 | 105 | it('merge', function() { 106 | var list = [ 107 | {name: 'John'}, 108 | {name: 'Kate'}], 109 | p = {merge: [{project: 'coServ'}, {project: 'JSON-fp'}]}; 110 | var result = lamda.apply( list, p ); 111 | assert.equal(result[0].project, 'coServ', 'contribute to the coServ proj'); 112 | assert.equal(result[1].project, 'JSON-fp', 'contribute to the JSON-fp proj'); 113 | //console.log( JSON.stringify(result, null, 4) ); 114 | }); 115 | 116 | it('pluck', function() { 117 | var list = [ 118 | {name: 'John', project: 'coServ'}, 119 | {name: 'Kate', project: 'JSON-fp'}], 120 | p = {pluck: 'name'}; 121 | var result = lamda.apply( list, p ); 122 | //console.log( JSON.stringify(result, null, 4) ); 123 | assert.equal( result[0], 'John', 'first person is John'); 124 | assert.equal( result[1], 'Kate', 'second person is Kate'); 125 | 126 | var ctx = {name: 'name'}; 127 | p.pluck = '$name'; 128 | 129 | result = lamda.apply( ctx, list, p ); 130 | //console.log( JSON.stringify(result, null, 4) ); 131 | assert.equal( result[0], 'John', 'first person is John'); 132 | assert.equal( result[1], 'Kate', 'second person is Kate'); 133 | }); 134 | 135 | it('where', function() { 136 | var list = [ 137 | {name: 'John', project: 'coServ'}, 138 | {name: 'Kate', project: 'JSON-fp'}], 139 | p = {where: {project: 'coServ'}}; 140 | var result = lamda.apply( list, p ); 141 | assert.equal(result.length, 1, 'should have one element'); 142 | //console.log( JSON.stringify(result, null, 4) ); 143 | }); 144 | 145 | it('zipObject', function() { 146 | var list = ['name', 'project'], 147 | p = {zipObject: ['John', 'coServ']}; 148 | 149 | var result = lamda.apply( list, p ); 150 | assert.equal(result.name, 'John'); 151 | assert.equal(result.project, 'coServ'); 152 | //console.log( JSON.stringify(result, null, 4) ); 153 | 154 | list = ['intersection']; 155 | p.zipObject = [[1, 5]]; 156 | var p2 = lamda.apply( list, p ); 157 | 158 | var nlist = [1, 3, 5]; 159 | result = lamda.apply(nlist, p2); 160 | assert.equal(result.length, 2, 'has two elements: 1 & 5'); 161 | //console.log( JSON.stringify(result, null, 4) ); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /test/testRecursion.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | jsonfp = require('../lib/lamdaApp.js'); 3 | 4 | before(function() { 5 | // install built-in modules 6 | jsonfp.init(); 7 | }); 8 | 9 | 10 | describe('Test recursion examples...', function() { 11 | 12 | it('Factorial', function() { 13 | var facExpr = { 14 | '$expr': { 15 | def: { 16 | if: [{'<=': 1}, 17 | 1, 18 | {multiply: 19 | {'->': [ 20 | {subtract: 1}, 21 | '$expr' 22 | ]} 23 | } 24 | ] 25 | } 26 | }, 27 | factorial: '$expr' 28 | }; 29 | 30 | var fac = jsonfp.apply(4, facExpr); 31 | assert.equal(fac.factorial, 24, '4! = 24'); 32 | }); 33 | 34 | it('Fibonacci', function() { 35 | var fibExpr = { 36 | '$expr': { 37 | def: { 38 | if: [{'<': 2}, 39 | 1, 40 | {'->': [ 41 | [ 42 | {'->': 43 | [{subtract: 2}, '$expr'] 44 | }, 45 | {'->': 46 | [{subtract: 1}, '$expr'] 47 | } 48 | ], 49 | {reduce: 'add'} 50 | ]} 51 | ] 52 | } 53 | }, 54 | fibonacci: '$expr' 55 | }; 56 | 57 | var fib = jsonfp.apply(6, fibExpr); 58 | assert.equal(fib.fibonacci, 13, 'fib(6) = 13'); 59 | }); 60 | 61 | it('Quicksort', function() { 62 | var seq = [5, 7, 3, 20, 88, 15, 6, 2, 50]; 63 | 64 | var qsortExpr = { 65 | '$expr': { 66 | def: { 67 | if: [{'->': [{size: null}, {'>': 1}]}, 68 | {'->': [ 69 | { 70 | '$pivot': {head: null}, 71 | qsort: 72 | {'->': [ 73 | [ 74 | {'->': [ 75 | {filter: {'<': '$pivot'}}, 76 | '$expr' 77 | ]}, 78 | {filter: {'==': '$pivot'}}, 79 | {'->': [ 80 | {filter: {'>': '$pivot'}}, 81 | '$expr' 82 | ]} 83 | ], 84 | {reduce: "add"} 85 | ]} 86 | }, 87 | {getter: "qsort"} 88 | ]}, 89 | '$in' 90 | ] 91 | } 92 | }, 93 | qsort: '$expr' 94 | }; 95 | 96 | var qsort = jsonfp.apply(seq, qsortExpr).qsort; 97 | //console.log( JSON.stringify(qsort, null, 4) ); 98 | assert.equal(qsort[0], 2, 'elem #1 is 2'); 99 | assert.equal(qsort[1], 3, 'elem #2 is 3'); 100 | assert.equal(qsort[8], 88, 'elem #9 is 88'); 101 | }); 102 | 103 | it("Word count", function() { 104 | var words = ['json', 'node', 'jumbo', 'functional', 'json', 'node', 'json']; 105 | 106 | var wcExpr = { 107 | "->": [ 108 | {map: 109 | { 110 | key: '$in', 111 | count: 1 112 | } 113 | }, 114 | { 115 | "$expr": { 116 | def: { 117 | "if": [{"->": [{size: null}, {">": 1}]}, 118 | { 119 | "->": [ 120 | { 121 | "$pivot": {head: null}, 122 | mapred: { 123 | "->": [ 124 | [ 125 | [ 126 | { 127 | key: "$pivot.key", 128 | count: { 129 | "->": [ 130 | {filter: 131 | {"->": [ 132 | {getter: "key"}, 133 | {"==": "$pivot.key"} 134 | ]} 135 | }, 136 | {map: {getter: "count"}}, 137 | {reduce: "add"} 138 | ] 139 | } 140 | } 141 | ], 142 | {"->": [ 143 | {filter: 144 | {"->": [ 145 | {getter: "key"}, 146 | {"!=": "$pivot.key"} 147 | ]} 148 | }, 149 | "$expr" 150 | ]} 151 | ], 152 | {reduce: "add"} 153 | ] 154 | } 155 | }, 156 | {"getter": "mapred"} 157 | ] 158 | }, 159 | "$in" 160 | ] 161 | } 162 | }, 163 | wcount: "$expr" 164 | } 165 | ] 166 | }; 167 | 168 | var wcount = jsonfp.apply(words, wcExpr).wcount; 169 | assert.equal(wcount[0].key, 'json', "first word is json"); 170 | assert.equal(wcount[0].count, 3, 'json appeared 3 times'); 171 | }); 172 | }); -------------------------------------------------------------------------------- /test/testStream.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | jsonfp = require('../lib/lamdaApp.js'); 3 | 4 | before(function() { 5 | // install built-in modules 6 | jsonfp.init(); 7 | }); 8 | 9 | describe('Test various streams', function() { 10 | 11 | it('Using iterator streams to calculate PI', function() { 12 | var jsquare = { 13 | '->': [ 14 | {random: null}, 15 | {multiply: 16 | {clone: null} 17 | } 18 | ] 19 | }; 20 | var jexpr = { 21 | '->': [ 22 | [ 23 | {'->': [// input to this chain is an iterator stream 24 | {'stream/iterator': {start: 1, end: '$in'}}, 25 | {'->': [ 26 | { 27 | map: { 28 | '->': [ 29 | [jsquare, jsquare], 30 | {reduce: 'add'}, 31 | {subtract: 1}, 32 | {'<=': 0} 33 | ] 34 | } 35 | }, 36 | {reduce: 'add'} 37 | ]} 38 | ]}, 39 | '$in' 40 | ], 41 | {reduce: 'divide'} 42 | ] 43 | }, 44 | ctx = {}; 45 | 46 | // iterate 2000 times 47 | var pi = jsonfp.apply(ctx, 2000, jexpr) * 4, 48 | isOk = pi > 3 && pi < 3.3; 49 | assert(isOk, 'not a reasonable PI estimate'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/testSyntax.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | lamda = require('../lib/lamdaApp.js'); 3 | 4 | before(function() { 5 | // install built-in modules 6 | lamda.init(); 7 | }); 8 | 9 | 10 | describe('Test syntax...', function() { 11 | it('evaluate constants', function() { 12 | var result = lamda.apply('', 'Hello World'); 13 | assert.equal(result, 'Hello World'); 14 | 15 | result = lamda.apply('whatever input', 99); 16 | assert.equal(result, 99); 17 | 18 | result = lamda.apply('whatever input', {name: 'John', project: 'JSON-fp'}); 19 | assert.equal(result.name, 'John', 'name is John'); 20 | 21 | result = lamda.apply('what ever input', [1, 2, 3, 4]); 22 | assert.equal( result.length, 4, 'array of 4 elements'); 23 | assert.equal( result[0], 1, 'first element is 1'); 24 | }); 25 | 26 | it('evaluate variables', function() { 27 | var result = lamda.apply('Hello', '$in'); 28 | assert.equal(result, 'Hello', "should be 'Hello'"); 29 | 30 | result = lamda.apply('Hello', '$in World!'); 31 | assert.equal(result, 'Hello World!', "should be 'Hello World!'"); 32 | 33 | result = lamda.apply('World', 'Hello $in!'); 34 | assert.equal(result, 'Hello World!', "should be 'Hello World!'"); 35 | 36 | result = lamda.apply({hello: 'Hello'}, '$in.hello World!'); 37 | assert.equal(result, 'Hello World!', "should be 'Hello World!'"); 38 | 39 | result = lamda.apply({hello: 'Hello'}, {world: 'World'}, '$hello $in.world'); 40 | assert.equal(result, 'Hello World', "should be 'Hello World'"); 41 | //console.log( result ); 42 | }); 43 | 44 | it('evaluate expressions', function() { 45 | var expr = {add: 4}, 46 | result = lamda.apply(12, expr); 47 | assert.equal( result, 16, 'sum should be 16'); 48 | 49 | expr = {def: 4}; 50 | result = lamda.apply(12, expr); 51 | assert.equal( result, 4, 'should just return the definition'); 52 | 53 | // evaluation stops at 'def' 54 | expr = {def: {add: 4}}; 55 | result = lamda.apply(12, expr); 56 | assert.equal(result.add, 4, 'should be {add: 4}'); 57 | 58 | // we can explicitly specify input to an expression 59 | expr = { 60 | name: { 61 | '->': [ 62 | ['John', ' Lennon'], 63 | {reduce: 'add'} 64 | ] 65 | }, 66 | path: { 67 | '->': [ 68 | ['/usr', '/lib'], 69 | {reduce: 'add'} 70 | ] 71 | } 72 | }; 73 | result = lamda.apply(null, expr); 74 | //console.log(JSON.stringify(result, null, 4)); 75 | assert.equal(result.name, 'John Lennon', 'name is John Lennon'); 76 | assert.equal(result.path, '/usr/lib', 'path is /usr/lib'); 77 | }); 78 | 79 | it('evaluate array', function() { 80 | var expr = [{add: 2}, {add: 4}], 81 | result = lamda.apply(8, expr); 82 | assert.equal(result[0], 10, 'first elem is 10'); 83 | assert.equal(result[1], 12, 'second elem is 12'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/testVariables.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | jsonfp = require('../lib/lamdaApp.js'); 3 | 4 | var pitchers = [ 5 | {name: 'Ventura', era: 3.2, salary: 500500}, 6 | {name: 'Price', era: 3.26, salary: 19750000}, 7 | {name: 'Kershaw', era: 1.77, salary: 32517428}, 8 | {name: 'Gray', era: 3.08, salary: 505000}, 9 | {name: 'Liriano', era: 3.38, salary: 11666666}, 10 | {name: 'Hammel', era: 3.47, salary: 23500000}, 11 | {name: 'Lester', era: 2.46, salary: 20000000}, 12 | {name: 'Bumgarner', era: 2.98, salary: 6950000}, 13 | {name: 'Chen', era: 3.54, salary: 4750000}, 14 | {name: 'Norris', era: 3.65, salary: 8800000} 15 | ]; 16 | 17 | before(function() { 18 | // install built-in modules 19 | jsonfp.init(); 20 | }); 21 | 22 | describe('JSON-FP variables...', function() { 23 | it('Using the context variable as a storage', function() { 24 | var expr = { 25 | $stat: { 26 | era: { 27 | '->': [ 28 | [ 29 | {'->': [ 30 | {map: {getter: 'era'}}, 31 | {reduce: 'add'} 32 | ]}, 33 | {size: null} 34 | ], 35 | {reduce: 'divide'} 36 | ] 37 | }, 38 | salary: { 39 | '->': [ 40 | [ 41 | {'->': [ 42 | {map: {getter: 'salary'}}, 43 | {reduce: 'add'} 44 | ]}, 45 | {size: null} 46 | ], 47 | {reduce: 'divide'} 48 | ] 49 | } 50 | }, 51 | pitchers: { 52 | filter: { 53 | '->': [ 54 | [ 55 | {'->': [ 56 | {getter: 'era'}, 57 | {'<': '$stat.era'} 58 | ]}, 59 | {'->': [ 60 | {getter: 'salary'}, 61 | {'<': '$stat.salary'} 62 | ]} 63 | ], 64 | {reduce: 'and'} 65 | ] 66 | } 67 | } 68 | }; 69 | var ctx = {}, 70 | result = jsonfp.apply(ctx, pitchers, expr); 71 | //console.log(JSON.stringify(result, null, 4)); 72 | 73 | assert.equal(result.pitchers.length, 1, 'One pitcher matched'); 74 | assert.equal(result.pitchers[0].name, 'Bumgarner', 'who is bumgarner'); 75 | assert(ctx.stat.era, 'the average era'); 76 | }); 77 | 78 | it('variable reference', function() { 79 | var input = { 80 | name: 'David', 81 | hobby: 'meditation' 82 | }; 83 | 84 | var expr = { 85 | eval: { 86 | $name: {getter: 'name'}, 87 | $hobby: {getter: 'hobby'}, 88 | response: { 89 | '->': [ 90 | ['$name', ' likes ', '$hobby'], 91 | {reduce: 'add'} 92 | ] 93 | } 94 | } 95 | }; 96 | 97 | var result = jsonfp.apply(input, expr); 98 | //console.log( JSON.stringify(result, null, 2) ); 99 | assert.equal(result.response, 'David likes meditation', 'wrong sentence'); 100 | }); 101 | }); --------------------------------------------------------------------------------