├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE.md ├── README.md ├── doc └── braindump.md ├── examples └── httpserver.coffee ├── index.js ├── nodeindex.js ├── package.json ├── spec ├── conditions.coffee ├── contracts.coffee ├── examples.coffee ├── introspection.coffee └── runner.html ├── src ├── agree.coffee ├── chain.coffee ├── common.coffee ├── conditions.coffee ├── express.coffee ├── introspection.coffee └── schema.coffee └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | test/ 4 | browser/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | addons: 5 | apt: 6 | sources: 7 | - ubuntu-toolchain-r-test 8 | packages: 9 | - g++-4.8 10 | env: 11 | matrix: 12 | - CXX=g++-4.8 13 | script: npm test 14 | deploy: 15 | provider: npm 16 | email: jononor@gmail.com 17 | api_key: 18 | secure: c1fwjwpTeM54SI0BtLDet+lB8JidQriSWUPPIrQfxVdr+0G0R6QXxjnPzxK1jcNb3icCbZl5p8+RVEyZZnJcCuWP022ZYBUvWFVZbDgQBAYDOWg+D6x5vQWU4fDrJGUo4a/CF7RZ1LTWvTCGAiQKvfYCDIsp5QTBSaezd2KsUOM= 19 | on: 20 | tags: true 21 | repo: jonnor/agree 22 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | webpackConfig = require "./webpack.config.js" 4 | 5 | module.exports = -> 6 | # Project configuration 7 | pkg = @file.readJSON 'package.json' 8 | 9 | @initConfig 10 | 11 | # Build for browser 12 | webpack: 13 | options: webpackConfig 14 | build: 15 | plugins: webpackConfig.plugins.concat() 16 | name: 'agree.js' 17 | 18 | # Web server for the browser tests 19 | connect: 20 | server: 21 | options: 22 | port: 8000 23 | livereload: true 24 | 25 | # Coding standards 26 | coffeelint: 27 | components: ['Gruntfile.coffee', 'spec/*.coffee'] 28 | options: 29 | 'max_line_length': 30 | 'level': 'ignore' 31 | 32 | # Tests 33 | coffee: 34 | spec: 35 | options: 36 | bare: true 37 | expand: true 38 | cwd: 'spec' 39 | src: '*.coffee' 40 | dest: 'browser/spec' 41 | ext: '.js' 42 | 43 | # Node.js 44 | mochaTest: 45 | nodejs: 46 | src: ['spec/*.coffee'] 47 | options: 48 | reporter: 'spec' 49 | require: 'coffee-script/register' 50 | grep: process.env.TESTS 51 | 52 | # BDD tests on browser 53 | mocha_phantomjs: 54 | all: 55 | options: 56 | output: 'test/result.xml' 57 | reporter: 'spec' 58 | urls: ['./spec/runner.html'] 59 | 60 | # Grunt plugins used for building 61 | @loadNpmTasks 'grunt-webpack' 62 | 63 | # Grunt plugins used for testing 64 | @loadNpmTasks 'grunt-mocha-phantomjs' 65 | @loadNpmTasks 'grunt-contrib-coffee' 66 | @loadNpmTasks 'grunt-mocha-test' 67 | #@loadNpmTasks 'grunt-coffeelint' 68 | #@loadNpmTasks 'grunt-contrib-connect' 69 | 70 | 71 | # Grunt plugins used for deploying 72 | # 73 | 74 | # Our local tasks 75 | @registerTask 'build', ['webpack'] 76 | @registerTask 'test', ['build', 'coffee', 'mochaTest', 'mocha_phantomjs'] 77 | 78 | @registerTask 'default', ['test'] 79 | 80 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015-2016 Jon Nordby 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jonnor/agree.svg?branch=master)](https://travis-ci.org/jonnor/agree) 2 | # Agree: Contract programming for JavaScript 3 | 4 | Agree is a library for implementing Contract Programming / 5 | [Design by contract](http://en.wikipedia.org/wiki/Design_by_contract) in JavaScript, 6 | including `preconditions`, `postconditions` and `class invariants`. 7 | 8 | It is inspired by projects like [contracts.coffee](http://disnetdev.com/contracts.coffee), 9 | but requires *no build steps*, *no non-standard language features*, and is *introspectable*. 10 | 11 | ## Status 12 | 13 | **Experimental** as of November 2016. 14 | 15 | * Functions, method and class invariants work 16 | * Support for asyncronous functions using Promise (ES6/A+ compatible) 17 | * Contracts can be reusable, and used to define interfaces (having multiple implementations) 18 | * Some proof-of-concept documentation and testing tools exists 19 | * Library has not been used in any real applications yet 20 | 21 | ### TODO 0.1 "minimally useful" 22 | 23 | * Lock down the core contracts API 24 | * Add more tests for core functionality 25 | * Do use-case exploration of a browser/frontend example 26 | * Run all automated tests under browser 27 | * Stabilize and document the testing and documentation tools 28 | 29 | Future 30 | 31 | * Support postcondition expressions that contain function inputs 32 | * Support postconditions expressions matching against 'old' instance members 33 | * Support invariants on properties 34 | 35 | For details see TODO/FIXME/XXX/MAYBE comments in the code. 36 | 37 | ## Installing 38 | 39 | Add Agree to your project using [NPM](http://npmjs.org) 40 | 41 | npm install --save agree 42 | npm install --save-dev agree-tools 43 | 44 | ## License 45 | 46 | MIT, see [LICENSE.md](./LICENSE.md) 47 | 48 | ## Examples 49 | 50 | [HTTP server](./examples/httpserver.coffee) 51 | 52 | See the tests under [./spec/](./spec) for full reference. 53 | 54 | ## Tools 55 | 56 | [agree-tools](https://github.com/jonnor/agree-tools) uses the introspection features of Agree 57 | to provide support for testing and documentation, driven by the contracts/code. 58 | 59 | 60 | ## Goals 61 | 62 | - No special dependencies, works anywhere (browser, node.js, etc), usable as library 63 | - That contracts are used is completely transparent to consuming code 64 | - Can start using contracts stepwise, starting with just some functions/methods 65 | - JavaScript-friendly fluent API (even if written with CoffeeScript) 66 | - Preconditions can, and are encouraged to, be used for input validation 67 | 68 | Usecases 69 | 70 | - HTTP REST apis: specifying behavior, validating request, consistent error handling 71 | - [NoFlo](http://noflojs.org) components: verifying data on inports, specifying component behavior 72 | - Interfaces: multiple implementations of same interface fully described 73 | 74 | 75 | ## Introspection 76 | 77 | Functions, classes and instances built with Agree know their associated contracts. 78 | This allows to query the code about its properties, to generate documentation, 79 | test-cases and aid in debugging of failures. 80 | 81 | Agree is partially related other work by [author](http://jonnor.com) on introspectable programming, 82 | including [Finito](http://finitosm.org) (finite state machines) 83 | and [NoFlo](http://noflojs.org)/[MicroFlo](http://microflo.org) (dataflow). 84 | 85 | -------------------------------------------------------------------------------- /doc/braindump.md: -------------------------------------------------------------------------------- 1 | # Benefits of contracts 2 | 3 | Since contract programming / design-by-contracts is not a common tool/technique, 4 | will likely need to explain the benefits to convince someone to try it out. 5 | Especially relation, and differences to, (static) typing, and automated testing 6 | may be revealing. 7 | 8 | 9 | Important to note, that contracts does not exclude (static) typing nor automated testing. 10 | In fact, probably best seen as complimentary. Use together, especially with automated tests! 11 | 12 | * Unlike automated tests, validates each and every execution, including in production. 13 | More exhaustive coverage, reducing things that slip through. 14 | * Can encode/enforce more info than (conventional) typing systems. 15 | Reaching expressitivity only available with algebraic types, dependent types and effect typing. 16 | Neither of these are commonly available for JavaScript right now. 17 | * Produces informational error messages as side-effect of verification 18 | * (should) Less effort spent for same verification/coverage level 19 | * (maybe) Allow for static reasoning of programs. 20 | Proved achievable for static systems, like Code Contract for .NET. 21 | Yet unproven for JavaScript, see section 'quasi-static analysis' for details. 22 | 23 | ## Benefits of Agree approach 24 | 25 | Fact that we're applying to a highly dynamic language, that it is a library, 26 | and focused on introspection are probably the key elements here. 27 | 28 | Note: Not all these realized yet! Some are hypothetical, possibly even theoretical. 29 | 30 | Library 31 | 32 | * No special language features required 33 | * No special build or compiler required 34 | * Contracts are just code, can be manipulated programatically with JS 35 | * Can be applied to existing code without changing it (only adding) 36 | * Invisible to calling code, can use inside libraries/modules 37 | * Can be introduced gradually in codebase 38 | 39 | Introspection 40 | 41 | * Test/code-coverage 42 | * Documentation generation 43 | * Self-documenting 44 | * Automated test-generation 45 | * Self-testing 46 | * Tracing 47 | * (maybe) Quasi-static analysis 48 | 49 | 50 | ## Limitations and Drawbacks 51 | 52 | Nothing is perfect. What are they? How to mitigate? 53 | 54 | * Requires using a/this library, must be included at runtime. 55 | Mitigation: few dependencies, minimize code size. 56 | * Wraps functions at runtime, using functions for conditions 57 | Mitigation: test, minimize and document performance impact. 58 | Provide best-practices on how to check if this is a problem, and what to do if it is. 59 | * Has/suggests a particular coding style 60 | 61 | # Best practices with contracts 62 | 63 | For public/external APIs, contracts should be declared in an file external from the implementation. 64 | For instance in a file under ./constracts, then used by files in ./src (implementation) and ./test or ./spec (tests). 65 | This makes sure that changes to publically relied-upon contracts, and get due attention during code review. 66 | 67 | 68 | # Quasi-static checking 69 | 70 | > An approach to static verification in dynamic languages 71 | 72 | ## Background 73 | 74 | [Static analysis](https://en.wikipedia.org/wiki/Static_program_analysis) 75 | and verification is done at 'compile time', by a compiler or a static analysis tool. 76 | Static analysis tools must generally implement a fully-featured parser 77 | for the target language, which for some languages (like C++) is very hard. 78 | Even then, the amount of things that can be verified are very limited, 79 | due to missing guarantees from the programming language - and no built-in way 80 | for programmer to state further guarantees. 81 | 82 | Exceptions are some statically typed languages implementing [algebraic types](https://en.wikipedia.org/wiki/Algebraic_data_type) 83 | (like Rust, Haskell) and [dependent types](https://en.wikipedia.org/wiki/Dependent_type) (like Idris, Agda), 84 | where one can encode user-decided code properties in a type, and the compiler will enforce them. 85 | 86 | Most dynamic languages (like JavaScript), have even less things that can be statically verified 87 | - due to their dynamic and dynamically typed nature. 88 | Even something trivial, like referencing undefined variables is not commonly done. 89 | 90 | Another verification technique is [dynamic analysis](https://en.wikipedia.org/wiki/Dynamic_program_analysis), 91 | which is done at run-time and with all real-life/external/side effects of the program also being caused, 92 | and requring to trigger all the relevant code paths for good coverage. 93 | 94 | ## Concept 95 | 96 | Quasi-static verification is a mix: We execute code (dynamic analysis), 97 | instead of using a compiler/parser. However, the code is written in a way that 98 | allows us to reason about it, without needing external inputs to trigger, 99 | and without causing external effects during the analysis pass. 100 | 101 | The following components are needed 102 | 103 | 1. A way to describe guaranteed and required code properties. 104 | 2. A way to load code and properties - without causing side-effects. 105 | 3. Tool(s) that reason about whether code is in violation. 106 | 107 | Since we're using the host language directly, this is very related to the concept 108 | of an [embedded DSL](http://c2.com/cgi/wiki?EmbeddedDomainSpecificLanguage). 109 | 110 | ## Agree and quasi-static checking 111 | 112 | As a particular implementation of this concept could use 113 | 114 | 1. Contracts: pre/post-conditions & invariants 115 | 2. Promise/function chains, exported on module-level 116 | Would have contracts attached, and the 'body' of the code 117 | be captured but not executed to prevent side-effects. 118 | 119 | ### Example propagation 120 | 121 | * use predicate valid/examples, insert into chain 122 | * verify passing precondition, 123 | * generate new example(s) based on post-condition 124 | * feed into next step 125 | 126 | This strategy likely requires that steps are not stateful 127 | (all state must enter through function arguments, exit through return/this). 128 | 129 | This assumes that the pre/postconditions of a step/function is well formed. 130 | So for full verification, the step must also be verified. 131 | This could be done through unit testing, local static or dynamic analysis. 132 | 133 | If the step is marked as side-effect free (maybe it should be opt-out?), 134 | then we could automatically create unit-tests which ensure that none of 135 | valid pre-condition examples cause post-conditions to fail. 136 | 137 | Input data normally used in (unit/behaviour) tests could be good real-life 138 | examples for predicates. Ideally one could declare them one place, 139 | and would be able to act in both capabilities. 140 | 141 | ### Pairwise coupling 142 | 143 | Instead of doing propagation from beginning of a long chain, 144 | try to check the pairs connected together. That is, if data exists 145 | that passes a post-condition of source, but fails pre-condition of target. 146 | If this exists, then is an indicator of broken combination, or wrong pre/post-conditions. 147 | If no examples can be found, no reasoning can be performed, which could 148 | also be considered a failure (at least in stricter modes). 149 | 150 | ## References 151 | 152 | * Probably the outlined technique is a form of symbolic execution? 153 | * [Embedded-computing.com: Advanced static analysis meets contract-based programming] 154 | (http://embedded-computing.com/articles/advanced-meets-contract-based-programming/), 155 | explains motivation/concept of combining static analysis and contracts. 156 | * [Contracts as a support to static analysis of open systems] 157 | (http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.160.8164&rep=rep1&type=pdf) 158 | 159 | Related concepts 160 | 161 | * [Sagas](http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf) 162 | and [redux-saga](http://yelouafi.github.io/redux-saga/docs/basics/DeclarativeEffects.html), 163 | where instead of performing async operations with some callback at completion, code yields an *Effect*: 164 | An object that describes the async action to perform. The motivation seems to be primarily testability. 165 | Could possibly be used instead of the PromiseChain thing. 166 | 167 | # Predicate examples and generative testing 168 | 169 | The manually provided examples are good, and neccesary basis, 170 | for being able to generate testcases and to reason about. 171 | 172 | However, it is easy to miss some cases this way. Or put alternatively, 173 | it is unreasonably tedious to ensure (up front) that manually written examples covers everything. 174 | 175 | TODO: move fuzz/mutation tools from poly-test out to a dedicated library, that Agree can use? 176 | 177 | If doing this well, hopefully get to the point where primarily doing `automated-testing-by-example`. 178 | That is, instead of: setting up a testing framework, writing testcases in imperative code, 179 | somewhere else than the code-under-test, one: 180 | - writes code in a way that describes what is valid & not 181 | - providing a couple of representative examples (some valid, some invalid) 182 | - tools use this information to create & run testcases 183 | - a whole set of testcases is created automatically 184 | - they can be found & ran without any further setup 185 | 186 | The area of 'property-based testing' is useful to integrate here. 187 | 188 | # Agree and static typing 189 | 190 | There are now several production quality tools offering opt-in, gradual static typing for JavaScript. 191 | Notably: 192 | 193 | * [Flow](https://flowtype.org/) 194 | * [TypeScript](https://www.typescriptlang.org/) 195 | 196 | Does this make contracts unneccesary? Or their value to 197 | Are there benefits of using static typechecking and contracts together? 198 | 199 | Advantages: 200 | 201 | * Very advanced dataflow analysis, understands arbitrary JS 202 | * Type inference gives needs less typing to get checking 203 | * Typing is a well-known concept (compared to contracts) from popular languages, like Java/C/C++/C# 204 | * Zero runtime overhead 205 | 206 | Disadvantages: 207 | 208 | * Only works with plain JavaScript? Not dialects like CoffeeScript. 209 | May be possible to hack using annotations in comments 210 | * Not used for runtime checking. Ie. when called from plain JS, or for checking incoming JSON data. 211 | Note, there are some Json Schema <-> TypeScript conversion tools, allowing one source for both. 212 | * Cannot express things like dependent-types 213 | * Cannot be introspected at runtime 214 | * Cannot be used as oracle for property-based testing? 215 | * No example-based-testing, or fault-injection tools. But maybe those should be separate anyways? 216 | 217 | # Fault injection 218 | 219 | Since we know the preconditions, and have in/valid examples, we can use these as basis for 220 | [fault injection](https://en.wikipedia.org/wiki/Fault_injection). 221 | 222 | For instance in a msgflo-noflo scenario: 223 | * Agree is used for/inside a NoFlo component 224 | * NoFlo component is used in a (possibly multi-level) NoFlo graph 225 | * The NoFlo graph is exposed as a MsgFlo participant on AMQP 226 | 227 | We can pick out functions with Agree contracts at lowest level, 228 | cause a fault there, and then have a test that ensures that 229 | this causes an error to be bubbled all the way up to AMQP. 230 | 231 | Can be seen as a kind of [mutation testing](https://en.wikipedia.org/wiki/Mutation_testing), 232 | but where we can make stronger inferences because we know more about 233 | the structure and semantics of the program. 234 | 235 | # Composition of code that uses contracts 236 | 237 | Ideally any standard JavaScript way of combining the functions/classes using contracts (mostly imperative) 238 | would give all benefits of the contracts, like static verification and test generation. 239 | 240 | However this will require very complex [flow-analysis](https://en.wikipedia.org/wiki/Data-flow_analysis), 241 | to determine how values travel through a program, and which values it may take on. 242 | For a dynamic language like JavaScript, the latter is especially hard. 243 | 244 | It may be easier if we provide also 245 | a library which guides or enforces best-practices, and maybe support introspection in similar 246 | ways as individual Agree-using functions/classes/instances do. That way, we may be able 247 | to provide the same information 248 | 249 | Possible approaches include `higher-order functions`, taking the contracted functions as input. 250 | Typically limited to working with syncronous functions (returning results), 251 | but approach can also work with node.js-style call-continuation-passing (like [Async.js](https://github.com/caolan/async)). 252 | An as special flavor of this, may consider currying-style (like [Ramda](http://ramdajs.com/docs/)). 253 | 254 | As a lot of JavaScript code (especially in node.js, but also in browser) is async/deferred, 255 | Promise chains (like [bluebird](http://bluebirdjs.com) or [FlowerFlip](https://github.com/the-grid/Flowerflip)) 256 | may be particularly interesting. 257 | 258 | Another composition techniques, include dataflow/FBP and finite state machines (see separate sections). 259 | 260 | Related 261 | 262 | * [Composability in JS using categories](https://medium.com/@homam/composability-from-callbacks-to-categories-in-es6-f3d91e62451e). 263 | Very similar abstraction as Promise, but is instead deferred by default. Also got some notes on relationships to monads. 264 | 265 | # Contracts & dataflow/FBP 266 | 267 | For projects like [NoFlo](http://noflojs.org) and [MicroFlo](http://microflo.org), 268 | we may want to applying contracts for specifying and verifying dataflow / FBP programming. 269 | 270 | Ideally this would allow us to reason about whole (hierarchical) graphs, aided by contracts. 271 | 272 | There are a couple different levels one could integrate: 273 | 274 | 1. On ports. Check conditions against data on inport, and check conditions on data send on outport. 275 | This is basically a type system. 276 | 2. In the runtime, for verifying components are generally well-behaved. 277 | 3. On individual components. Primarily for ensuring they perform their stated function correctly. 278 | 279 | ## 1. Contracts on port data 280 | 281 | Schema (JSON) and similar conditions are the most relevant here. 282 | 283 | ## 2. Runtime enforcing contracts on component behavior 284 | 285 | For instance, component may declares they are of some kind / has a certain trait, 286 | so that the runtime can know what to expect of the component. Examples may include: 287 | 288 | * sync: Sends packets directly upon activation. 289 | * async: Send packets some time after activation. 290 | * generator: Has a port for activating and one for de-activating. 291 | Sends packets at arbitrary times, as long as is active. 292 | 293 | May also consider having traits like: 294 | 295 | * one-outpacket: Each activation causes one packet to be sent, on a single port. 296 | * one-on-each: Each activation causes one packet to be sent on each (non-error) outport. 297 | * output-or-error: Each activation either causes normal output OR an error, never both. 298 | 299 | If the component disobeys any of these constraints, the runtime raises an error. 300 | 301 | In general it may be better to avoid most of the potential issues by having an 302 | API which is hard or impossible to misuse. But when the range of 'valid' behavior is 303 | large, this approach may have benefits. 304 | It can help pin-point which component caused a problem instead of only seeing it down the flow, 305 | which massively reduces debugging time. 306 | 307 | ## 3. Component contracts 308 | 309 | For the particular functionality provided by a component, 310 | we need more fine-grained contracts than component traits/types. 311 | 312 | The type of contracts most interesting is probably those specifying relations between input and output. 313 | Examples: 314 | 315 | * Output value is always a multiple of the input value 316 | * Output value is always higher or lower than input value 317 | * Always sends as many packets as the length of input array 318 | 319 | The constracts would checked at runtime, but also be the basis for generating automated tests. 320 | 321 | ## NoFlo integration 322 | 323 | A challenge is that unlike with call-return and call-continuationcall, 324 | it is not apparent when an execution of a component is 'done'. 325 | And with NoFlo 0.5, components can act on packets send in an out-of-order manner, 326 | because it is the components (not the runtime) which queue up data to check whether groups etc match 327 | what is needed to 'fire' / activate. 328 | 329 | Right now these state transitions are implicit and unobservable. 330 | This would need to change in order to apply contracts like in 2) or 3). 331 | 332 | The contracts conditions would need access to all the data for one activation. 333 | This could be a datastructure like: 334 | 335 | ``` 336 | inputs: 337 | inport1: [ valueA, ... ] 338 | inport2: [ valueB, ... ] 339 | outputs: 340 | out1: [ outValue, ... ] 341 | ``` 342 | This assumes that sending/receiving order is irrelevant, which is arguably a desirable property anyway. 343 | If this is not feasible, the datastructure could be `inputs: [ port: inport1, data: valueA ]`. 344 | 345 | Since NoFlo 0.6 has IP objects which carry the actual values, 346 | it may be that this should be exposed here instead of the raw values. 347 | This would allow verifying things like scoping and groups behavior. 348 | 349 | ## Using contracted function as component 350 | 351 | It may also be interesting to be able to easily create NoFlo components from (especially async), 352 | functions with Agree contracts. Can we and do we want to provide integration tools for this? 353 | Like a NoFlo ComponentLoader or similar... 354 | 355 | A tricky part is handling of multiple inputs, can this be done in an automatic way? 356 | If component handles activation strategy, it can collect multiple inputs keyed by the port name. 357 | Promises can only have one input value, so it needs to be objects anyways. 358 | 359 | Can we retain the ability to reason about chains of Promises this way, when individual 360 | chains are plugged together by NoFlo graph connections? 361 | 362 | ## References 363 | 364 | * [Contract-based Specification and Verification of Dataflow Programs](http://icetcs.ru.is/nwpt2015/SLIDES/JWiik_NWPT15_presentation.pdf). 365 | Defines concept of `network invariants` and `channel invariants`, and verification strategy both for 366 | individual actors (components) and for networks. "To make the approach usable in practice, channel invariants 367 | should be inferred automatically whenever possible" cited as future work. 368 | 369 | 370 | # Contracts & finite automata / FSM 371 | 372 | For projects like [finito](http://finitosm.org), we may want to apply contracts for specifying & verifying 373 | Finite State Machines. 374 | 375 | Ideally this would allow us to reason about whole (hierarchical) machines, aided by the contracts. 376 | 377 | References 378 | 379 | * [How to prove properties of finite state machines with cccheck] 380 | (http://blogs.msdn.com/b/francesco/archive/2014/09/20/how-to-prove-properties-of-finite-state-machines-with-cccheck.aspx). 381 | References '[lemmas](https://en.wikipedia.org/wiki/Theorem#Terminology)', a theorem-prover concept, as part of their solution. 382 | 383 | # Contracts as executable, provable coding style 384 | 385 | Mostly `coding style` today is about fairly trivial things like syntax, 386 | including rules around naming, whitespace, blocks etc. 387 | These can to an extent be enforced (or normalized) using modern syntax tools. 388 | 389 | However, such tools cannot enforce things beyond syntax. For example: 390 | 391 | * Consistent error handling 392 | * Consistent / predictable ordering of arguments 393 | * Consistent handling of options 394 | * Completeness in functionality involving setup/teardown, back/forth, etc.. 395 | 396 | Possibly this could be done by having a set of contracts, 397 | which all code in a library/module/class obeys? 398 | 399 | 400 | # User interfaces 401 | 402 | As of Jan 2016, most thinking/testing has been on applying to web backend services or small (domain-independent) units. 403 | However, user interfaces, especially in browsers, is an area where JavaScript is even bigger. 404 | In particular, application to functional/reactive styles as popularized by React is worth some consideration. 405 | 406 | * Forms/fields. Input validation, whether/how to shown UI elements. 407 | * Model data validation. Both coming from view, and going to view. 408 | * Ensuring that data is shown on screen, possibly in a particular manner. 409 | * Authentication and roles, and their implications on available UI/actions. 410 | * Integration with API/services. Very similar as concerns on backend side, just inverted? 411 | 412 | For highly interactive UIs, like in games, availability of actions may depend on particular game states, 413 | which could also be interesting to model as contracts. 414 | 415 | 416 | # Embedded devices 417 | How to apply introspectable contracts to embedded devices? 418 | 419 | Challenges come from: 420 | 421 | * Poor first-class function support in popular languages like C/C++ 422 | * contrained CPU resources of the target devices 423 | * difficulty of writing target-independent code/tests 424 | * difficulty/inconvenience of running tests on-device 425 | * very limited program memory, hard to store lots of introspection data 426 | * often not directly connected to device with the debugging UI 427 | 428 | Possible approaches 429 | 430 | * Using macros in C/C++. Would both insert pre/post/invart checking code, 431 | and act as markers for tool to build the contract instrospection data. 432 | * Use custom clang/LLVM to compile C/C++, transparently inserting checking code. 433 | Would output contract introspection data. 434 | * Use a modern language with compiler hooks. Maybe Rust? 435 | * DSL... 436 | 437 | ## References 438 | 439 | * [C++ standard proposal: Simple contracts for C++](http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4415.pdf). 440 | Uses the C++11 [generalized attribute](http://www.codesynthesis.com/~boris/blog/2012/04/18/cxx11-generalized-attributes/) mechanism, 441 | contracts to be implemented by compilers. 442 | 443 | # Cross-language 444 | 445 | Useful when doing polyglot programming, 446 | to be able to model things in the same way. 447 | 448 | Most beneficial if one can share tools between different languages: 449 | for documentation, testing, debugging. 450 | 451 | Serialize contract to standard .json representation? 452 | All tools operate on this? 453 | 454 | # Tools wishlist 455 | 456 | Debugging (in-situ and retroactive) 457 | 458 | * Ability to trace async/Promise code 459 | Related: http://h14s.p5r.org/2012/05/tracing-promises.html 460 | * Ability to compare multiple different runs, 461 | looking at differences in input/output/conditions 462 | * Ability to see all contracts in 463 | * Ability to see relationships between contracts, 464 | including same-level and hierarchies 465 | 466 | Testing/QA 467 | 468 | * Ability to know code test coverage, 469 | including verification exhaustiveness 470 | * Ability to calculate complexity 471 | 472 | ## Documentation 473 | 474 | * Integration with HTTP api docs tools, 475 | like [Blueprint](https://apiblueprint.org/), with [Aglio](https://github.com/danielgtaylor/aglio). 476 | Alternatives include [Swagger](http://swagger.io/) (now Open API Initiative). 477 | [Matic](https://github.com/mattyod/matic) possibly useful for JSON Schema documentation. 478 | 479 | 480 | # Related 481 | 482 | Design-with-contracts / contracts-programming 483 | 484 | * [CodeContracts](http://research.microsoft.com/en-us/projects/contracts/), [open source](https://github.com/Microsoft/CodeContracts) contracts for .NET (C# etc). 485 | Has [static checking](http://research.microsoft.com/en-US/projects/contracts/cccheck.pdf) and test-generation capabilities. 486 | [Developer blog](http://blogs.msdn.com/b/francesco/). Interesting feature: `Assume` allows to add (missing) postconditions 487 | to third-party APIs. 488 | * [libhoare.rs](https://github.com/nrc/libhoare), contracts in Rust 489 | * [OpenJML/Java Modelling Language](http://jmlspecs.sourceforge.net/), 490 | combines contracts with [Larch](http://www.eecs.ucf.edu/~leavens/larch-faq.html). 491 | [Design by Contract with JML](http://www.eecs.ucf.edu/~leavens/JML//jmldbc.pdf). [Wikipedia](https://en.wikipedia.org/wiki/Java_Modeling_Language) 492 | * [Plumatic Schema](https://github.com/plumatic/schema), schema-based type validation on functions for Closure(Script). 493 | Interesting connections using schema generators together with property-based / generative testing. 494 | 495 | Testing and contracts 496 | 497 | * [Enhancing Design by Contract with Know-ledge about Equivalence Partitions](http://www.jot.fm/issues/issue_2004_04/article1.pdf). 498 | Introduces ability to declare partitions for invariants, which a value can belong to. 499 | Then uses a test generator based on the transitions between such partitions. 500 | Also uses an `old` keyword to represent previous value of an instance variable/member, to allow postconditions expressions on it. 501 | * [Testing by Contract - Combining Unit Testing and Design by Contract](http://www.itu.dk/people/kasper/NWPER2002/papers/madsen.pdf), 502 | uses the equivalence partitions implied by preconditions to generate inputs to test. 503 | * [Automatic Testing Based on Design by Contract](http://se.inf.ethz.ch/old/people/ciupa/papers/soqua05.pdf), 504 | tool for fully automated testing for Eiffel by introspection of the code contracts. 505 | * [Seven principles of automated testing, B.Meyer](http://se.ethz.ch/~meyer/publications/testing/principles.pdf). Creator of Design By Contracts and Eiffel. 506 | Definition of a 'Test Oracle', as a piece of code which can automatically. DbC is one way that enables oracles. Property-based-testing is another. 507 | * [](https://arxiv.org/pdf/1512.02102.pdf). Argues for lifting contracts from assertions into (dependent) types. 508 | For instance fully specified types can avoiding needing to take failure into account in signature and implementation, because failing inputs are enforced as impossible by compiler. 509 | 510 | Use serialized representation for checking complex types in fail/pass examples. "RgbColor(#aabbcc)" 511 | Very many languages allow to attach custom serializers, and tools which benefit from them. Debuggers etc 512 | Reduces burden on grammar, as any string can be used. Can also be used to hide some examples of equivalence. 513 | 514 | 515 | JavaScript verification approaches 516 | 517 | * [Towards JavaScript Verification with the Dijkstra State Monad](http://research.microsoft.com/en-us/um/people/nswamy/papers/js2fs-dijkstra.pdf) 518 | * [Dependent Types for JavaScript](http://goto.ucsd.edu/~ravi/research/oopsla12-djs.pdf) 519 | * [SymJS: Automatic Symbolic Testing of JavaScript Web Applications](http://www.cs.utah.edu/~ligd/publications/SymJS-FSE14.pdf) 520 | * [Jalangi2: Symbolic execution / dynamic analysis](https://github.com/Samsung/jalangi2) 521 | * [Flow: static type checker for JavaScript](http://flowtype.org/). Gradual typing, manual opt-in annotations 522 | 523 | 524 | Artificial intelligence 525 | 526 | * [STRIPS](https://en.wikipedia.org/wiki/STRIPS)-style planning and answer-set programming, rely on pre- and post-conditions. 527 | See [Programming as planning](http://www.vpri.org/pdf/m2009001_prog_as.pdf) for a recent, integrated example. 528 | 529 | Interesting verification approaches 530 | 531 | * [Effect typing, inferred types based on their (side)effects](https://research.microsoft.com/en-us/um/people/daan/madoko/doc/koka-effects-2014.html) 532 | * Dependent typing,. Example are Idris and Agda. 533 | * [Extended static checking](https://en.wikipedia.org/wiki/Extended_static_checking). Often using propagations of weakest-precondition / strongest-postcondition from 534 | [Predicate transformer semantics](https://en.wikipedia.org/wiki/Predicate_transformer_semantics), a sound framework for validating programs. 535 | * Symbolic execution 536 | * [Pex, automatic unit test generation](https://www.microsoft.com/en-us/research/publication/pex-white-box-test-generation-for-net/). Does not require any code annotations 537 | 538 | Contracts and embedded systems 539 | 540 | * Eiffel, Ada, SPARK 541 | * [Contract Testing for Reliable Embedded Systems](http://archiv.ub.uni-heidelberg.de/volltextserver/15941/1/fajardo_thesis_submit.pdf), uses contracts also for specifying hardware, 542 | including environment, input levels, timing and logic set restrictions. Demonstrates an enforcing implementation of this for I2C devices, using FPGA. 543 | * [Executable Contracts for Incremental Prototypes of Embedded Systems](http://www.sciencedirect.com/science/article/pii/S1571066109001091) 544 | Includes a formal speification for reactive systems, based on step-relations (relations between consecutive changes in program environment), 545 | a component model with input/outputs, contracts composed of assume-guarantee constraints. And a simulation methodology which generates 'traces' (kinda symbolic execution) between such components. 546 | 547 | ## Relation to theorem provers 548 | 549 | Many existing static verification tools translate contracts into a 550 | [SMT problem](https://en.wikipedia.org/wiki/Satisfiability_modulo_theories), using a standard solvers to. 551 | Some of these solver are again based on translating the problem into a 552 | [SAT problem](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem), 553 | though these are often unefficient when applied to software verification. 554 | 555 | (possibly) JavaScript-friendly solvers 556 | 557 | * [Logictools](https://github.com/tammet/logictools). Standalone JS solver(s), including DPLL SAT solver 558 | * [Boolector, compiled to JS](https://github.com/jgalenson/research.js/tree/master/boolector) 559 | * [MiniSAT, compiled to JS](http://www.msoos.org/2013/09/minisat-in-your-browser/) 560 | * [STP, compiled to JS](https://github.com/stp/stp/issues/191) (seemingly built on MiniSAT) 561 | * [Z3](https://github.com/Z3Prover/z3), could probably be compiled to JS with Emscripten 562 | * [CVC4](https://github.com/CVC4/CVC4), could probably be compiled to JS with Emscripten 563 | * [MiniZinc](https://github.com/MiniZinc/libminizinc), medium-level constraint language with solver. Could probably compile to JS with Emscripten. [Large example list](http://www.hakank.org/minizinc/) 564 | * [google or-tools](https://developers.google.com/optimization/), could probably compile to JS with Emscripten 565 | * [gecode](http://www.gecode.org), has been [compiled to JS with Esmcripten before](http://www.gecode.org/pipermail/users/2015-April/004665.html) 566 | * [backtrack](https://github.com/rf/backtrack), experiemental JavaScript CNF SAT solver 567 | * [condensate](https://github.com/malie/condensate), experiemental JavaScript DPLL SAT solver, with basic CDCL 568 | * [picoSAT](http://fmv.jku.at/picosat/), MIT-like, no-dependencies C code. Should be compilable to JavaScript using Emscripten 569 | 570 | Verification languages 571 | 572 | * [Boogie](https://github.com/boogie-org/boogie), intermediate verification language, used for C# etc. 573 | 574 | Related 575 | 576 | * [There are no CNF problems](http://sat2013.cs.helsinki.fi/slides/SAT2013-stuckey.pdf). 577 | Talk about how CNF/SAT has problems compared to medium/high-level modelling approaches for constraint programming. 578 | * [SAT tutorial](http://crest.cs.ucl.ac.uk/readingGroup/satSolvingTutorial-Justyna.pdf). 579 | With focus on Conflict-Driven Clause Learning (CDCL) 580 | 581 | ## Relation to Hoare logic 582 | 583 | Hoare logic 584 | 585 | References 586 | 587 | * [A Hoare Logic for Rust](http://ticki.github.io/blog/a-hoare-logic-for-rust/). 588 | Includes an introduction to Hoare logic, applying it for MIR intermediate representation in Rust compiler. 589 | 590 | # Ideas 591 | 592 | * Online integrated editor, allows to write JS w/Agree, automatically run check/test/doc tools. 593 | Can one build it on one of the code-sharing sites? 594 | 595 | # Thoughs by others 596 | 597 | > For a multi paradigm language to derive benefits from functional programming, 598 | > it should allow developers to explicitly write guarantees that can be enforced at compile time. 599 | > This is a promising direction but, to my knowledge, it's not available in any of the languages you mentioned. 600 | 601 | [Hacker News: murbard2 on functional programming](https://news.ycombinator.com/item?id=10812198) 602 | 603 | > I really want a tool that lets me mix and match operational code with proofs. 604 | > It's common now to write a unit test while debugging something, 605 | > less common to write a randomized property tester, 606 | > and rare to see a theorem prover being used. 607 | > It would be fantastic if I could casually throw in a proof while debugging a hard problem. 608 | 609 | [Hacker News: MichaelBurge on Idris](https://news.ycombinator.com/item?id=10856929) 610 | 611 | 612 | > Often I hear or read that just few tests covering a large portion of the code is not a valuable thing, 613 | > since few tests contain few assertions and thus, there is a high chance for a bug to remain undetected. 614 | > In other words, some pretend that high test coverage ratio of the code is not so much valuable. 615 | > But if the large portion of the code contains a well-written set of code contracts assertions, the situation is completely different. 616 | > During the few tests execution, tons of assertions (mainly Code Contracts assertions) have been checked. 617 | > In this condition, chances that a bug remains undetected are pretty low. 618 | [](http://codebetter.com/patricksmacchia/2010/07/26/code-contracts-and-automatic-testing-are-pretty-much-the-same-thing) 619 | 620 | -------------------------------------------------------------------------------- /examples/httpserver.coffee: -------------------------------------------------------------------------------- 1 | ## Case study and example of implementing a HTTP server with JSON-based REST API, using Agree 2 | try 3 | agree = require '..' # when running in ./examples of git 4 | catch e 5 | agree = require 'agree' # when running as standalone example 6 | agreeExpress = agree.express 7 | conditions = agreeExpress.conditions 8 | Promise = agree.Promise # polyfill for node.js 0.10 compat 9 | 10 | ## Contracts 11 | # In production, Contracts for public APIs should be kept in a separate file from implementation 12 | contracts = {} 13 | # Shared contract setup 14 | jsonApiFunction = (method, path) -> 15 | agree.function "#{method.toUpperCase()} #{path}" 16 | .attr 'http_method', method 17 | .attr 'http_path', path 18 | .error agreeExpress.requestFail 19 | .requires conditions.requestContentType 'application/json' 20 | .ensures conditions.responseEnded 21 | 22 | contracts.getSomeData = jsonApiFunction 'GET', '/somedata' 23 | .ensures conditions.responseStatus 200 24 | .ensures conditions.responseContentType 'application/json' 25 | .ensures conditions.responseSchema 26 | id: 'somedata.json' 27 | type: 'object' 28 | required: ['initial'] 29 | properties: 30 | initial: { type: 'string' } 31 | nonexist: { type: 'number' } 32 | .successExample 'All headers correct', 33 | _type: 'http-request-response' 34 | headers: 35 | 'Content-Type': 'application/json' 36 | responseCode: 200 37 | .failExample 'Wrong Content-Type', 38 | _type: 'http-request-response' 39 | headers: 40 | 'Content-Type': 'text/html' 41 | responseCode: 422 42 | 43 | contracts.createResource = jsonApiFunction 'POST', '/newresource' 44 | .requires conditions.requestSchema 45 | id: 'newresource.json' 46 | type: 'object' 47 | required: ['name', 'tags'] 48 | properties: 49 | name: { type: 'string' } 50 | tags: { type: 'array', uniqueItems: true, items: { type: 'string' } } 51 | .ensures conditions.responseStatus 201 52 | .ensures conditions.responseContentType 'application/json' # even if we don't have body 53 | .ensures conditions.responseHeaderMatches 'Location', /\/newresource\/[\d]+/ 54 | .successExample 'Valid data in body', 55 | _type: 'http-request-response' 56 | headers: 57 | 'Content-Type': 'application/json' 58 | body: 59 | name: 'myname' 60 | tags: ['first', 'second'] 61 | responseCode: 201 62 | .failExample 'Invalid data', 63 | _type: 'http-request-response' 64 | headers: 65 | 'Content-Type': 'application/json' 66 | body: 67 | name: 'valid' 68 | tags: [1, 2, 3] 69 | responseCode: 422 70 | 71 | ## Database access 72 | # Simulated example of DB or key-value store, for keeping state. SQL or no-SQL in real-life 73 | db = 74 | state: 75 | somekey: { initial: 'Foo' } 76 | get: (key) -> 77 | return new Promise (resolve, reject) -> 78 | data = db.state[key] 79 | return resolve data 80 | set: (key, data) -> 81 | return new Promise (resolve, reject) -> 82 | db.state[key] = data 83 | return resolve key 84 | add: (key, data) -> 85 | return new Promise (resolve, reject) -> 86 | db.state[key] = [] if not db.state[key]? 87 | db.state[key].push data 88 | sub = db.state[key].length 89 | return resolve sub 90 | 91 | ## Implementation 92 | routes = {} 93 | # Using regular Promises 94 | routes.createResource = contracts.createResource.implement (req, res) -> 95 | db.add 'newresource', req.body 96 | .then (key) -> 97 | res.set 'Location', "/newresource/#{key}" 98 | res.set 'Content-Type', 'application/json' # we promised.. 99 | res.status(201).end() 100 | Promise.resolve res 101 | 102 | # Using static Promise chain 103 | routes.getSomeData = contracts.getSomeData.implement( agree.Chain() 104 | .describe 'respond with "somekey" data from DB as JSON' 105 | .start (req, res) -> 106 | @res = res 107 | return 'somekey' 108 | .then 'db.get', db.get 109 | .then 'respond with JSON', (data) -> 110 | @res.json data 111 | Promise.resolve @res 112 | .toFunction() # function with signature like start.. (res, req) -> 113 | ) 114 | 115 | ## Setup 116 | express = require 'express' 117 | bodyparser = require 'body-parser' 118 | app = express() 119 | app.use bodyparser.json() 120 | app.use agreeExpress.mockingMiddleware 121 | agreeExpress.selfDocument routes, '/' 122 | agreeExpress.installExpressRoutes app, routes 123 | module.exports = routes # for introspection by Agree tools 124 | agree.testing.registerTester 'http-request-response', new agreeExpress.Tester app 125 | 126 | ## Run 127 | main = () -> 128 | port = process.env.PORT 129 | port = 3333 if not port 130 | app.listen port, (err) -> 131 | throw err if err 132 | console.log "#{process.argv[1]}: running on port #{port}" 133 | 134 | main() if not module.parent 135 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var agree = require('./src/agree'); 3 | agree.conditions = require('./src/conditions'); 4 | agree.introspection = require('./src/introspection'); 5 | agree.schema = require('./src/schema'); // XXX: core or no core? 6 | 7 | agree.express = require('./src/express'); // TEMP 8 | agree.chain = require('./src/chain'); // TEMP 9 | agree.Chain = agree.chain.Chain 10 | 11 | module.exports = agree; 12 | -------------------------------------------------------------------------------- /nodeindex.js: -------------------------------------------------------------------------------- 1 | 2 | var agree = null; 3 | try { 4 | agree = require("./dist/agree.js"); 5 | } catch (e) { 6 | require('coffee-script/register'); 7 | agree = require('./index.js'); 8 | } 9 | module.exports = agree; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agree", 3 | "version": "0.0.8", 4 | "description": "Introspectable contracts programming for JavaScript", 5 | "author": "Jon Nordby (http://www.jonnor.com)", 6 | "repository": { 7 | "type": "git", 8 | "url": "http://github.com/jonnor/agree.git" 9 | }, 10 | "bugs": "http://github.com/jonnor/agree/issues", 11 | "license": "MIT", 12 | "main": "nodeindex.js", 13 | "files": [ 14 | "dist", 15 | "src", 16 | "examples", 17 | "nodeindex.js", 18 | "index.js" 19 | ], 20 | "scripts": { 21 | "test": "grunt test" 22 | }, 23 | "engines": { 24 | "node": ">=0.10.0" 25 | }, 26 | "dependencies": { 27 | "bluebird": "^3.1.1", 28 | "tv4": "^1.2.7" 29 | }, 30 | "devDependencies": { 31 | "body-parser": "^1.14.2", 32 | "chai": "^1.9.1", 33 | "coffee-script": "^1.8.0", 34 | "coffee-loader": "^0.7.2", 35 | "express": "^4.13.3", 36 | "grunt": "^1.0.1", 37 | "grunt-cli": "^1.2.0", 38 | "grunt-contrib-coffee": "^1.0.0", 39 | "grunt-mocha-phantomjs": "^4.0.0", 40 | "grunt-mocha-test": "^0.13.2", 41 | "grunt-webpack": "^1.0.18", 42 | "mocha": "^1.21.4", 43 | "webpack": "^1.13.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/conditions.coffee: -------------------------------------------------------------------------------- 1 | 2 | agree = require '../' if not agree 3 | chai = require 'chai' if not chai 4 | 5 | describe 'Conditions', -> 6 | 7 | # Data-driven test based on introspecting the examples 8 | Object.keys(agree.conditions).forEach (name) -> 9 | condition = agree.conditions[name] 10 | 11 | describe "#{name}", -> 12 | return it.skip('missing examples', () ->) if not condition.examples?.length 13 | 14 | condition.examples.forEach (example) -> 15 | describe "#{example.name}", -> 16 | testcase = if example.valid then 'should pass' else 'should fail' 17 | it testcase, -> 18 | cond = example.create() 19 | error = cond.check.apply example.context(), example.args 20 | pass = not error? 21 | chai.expect(pass).to.equal example.valid 22 | 23 | -------------------------------------------------------------------------------- /spec/contracts.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | agree = require '../' 3 | examples = require './examples' 4 | 5 | conditions = agree.conditions 6 | 7 | # TODO: find way to avoid having to duplicate name when assigning. agree.export 8 | # TODO: allow to have separate name on function as contract 9 | agree.Class 'Foo' 10 | .add examples 11 | .init () -> 12 | @prop1 = "foo" 13 | @numberProp = 1 14 | .invariant conditions.neverNull 'prop1' 15 | .invariant conditions.attributeTypeEquals 'numberProp', 'number' 16 | 17 | .method 'setNumberWrong' 18 | .precondition conditions.noUndefined 19 | .postcondition [conditions.attributeEquals 'prop1', 'bar'] 20 | .body (arg1, arg2) -> 21 | @prop1 = null 22 | 23 | .method 'setPropNull' 24 | .precondition conditions.noUndefined 25 | .body (arg1, arg2) -> 26 | @prop1 = null 27 | 28 | .method 'addNumbers' 29 | .precondition conditions.noUndefined 30 | .body (arg1, arg2) -> 31 | return arg1+arg2 32 | 33 | 34 | .method 'setPropCorrect' 35 | .requires conditions.noUndefined 36 | .ensures [conditions.attributeEquals 'prop1', 'bar'] 37 | .body () -> 38 | @prop1 = 'bar' 39 | 40 | .method 'setPropWrong' 41 | .precondition conditions.noUndefined 42 | .postcondition [conditions.attributeEquals 'prop1', 'bar'] 43 | .body () -> 44 | @prop1 = 'nobar' 45 | 46 | describe 'FunctionContract', -> 47 | f = null 48 | beforeEach (done) -> 49 | try 50 | f = new examples.Foo 51 | catch e 52 | null 53 | done e 54 | 55 | it 'function with valid arguments should succeed', -> 56 | chai.expect(examples.multiplyByTwo 13).to.equal 26 57 | it 'function with failing precondition should throw', -> 58 | chai.expect(() -> examples.multiplyByTwo undefined).to.throw agree.PreconditionFailed 59 | it 'method with valid arguments should succeed', -> 60 | chai.expect(f.addNumbers(1, 2)).to.equal 3 61 | it 'method with failing precondition should throw', -> 62 | cons = () -> 63 | f.addNumbers undefined, 0 64 | chai.expect(cons).to.throw agree.PreconditionFailed 65 | it 'method violating postcondition should throw', -> 66 | chai.expect(() -> f.setPropWrong 1).to.throw agree.PostconditionFailed 67 | it 'method not violating postcondition should succeed', -> 68 | chai.expect(f.setPropCorrect()).to.equal "bar" 69 | 70 | describe 'as reused interface', -> 71 | c = agree.function 'shared contract' 72 | .postcondition conditions.noUndefined 73 | it 'function not obeying contract should fail', -> 74 | fail = c.implement () -> return undefined 75 | chai.expect(() -> fail true).to.throw agree.PostconditionFailed 76 | it 'function obeying contract should pass', -> 77 | pass = c.implement () -> return true 78 | chai.expect(() -> pass true).to.not.throw 79 | 80 | describe 'Contracts on function returning Promise', -> 81 | Promise = agree.Promise 82 | c = agree.function 'shared contract' 83 | .postcondition conditions.noUndefined 84 | 85 | it 'function obeying contract returns results in .then', (done) -> 86 | pass = c.implement () -> 87 | return new Promise (resolve) -> 88 | resolve 42 89 | p = pass() 90 | .then (v) -> 91 | return done() 92 | 93 | it 'function erroring returns error in .catch', (done) -> 94 | pass = c.implement () -> 95 | return new Promise (resolve, reject) -> 96 | reject new Error 'rejection hurts' 97 | p = pass() 98 | p.catch (e) -> 99 | return done new Error 'not right error' if not e.message == 'rejection hurts' 100 | return done() 101 | 102 | it 'function not obeying post-condition should fail', (done) -> 103 | func = c.implement () -> 104 | return new Promise (resolve) -> 105 | resolve undefined 106 | p = func() 107 | .then (v) -> 108 | return done new Error 'did not fail' 109 | .catch (err) -> 110 | return new Error 'not right error' if not err.constructor.name == 'PostconditionFailed' 111 | return done null 112 | 113 | 114 | describe 'precondition failure callbacks', -> 115 | c = null 116 | onError = null 117 | PreconditionCallbacks = agree.Class 'PreconditionCallbacks' 118 | .method 'callMe' 119 | .precondition(conditions.noUndefined) 120 | .precondition(conditions.numbersOnly) 121 | .error () -> 122 | onError() 123 | .body (f) -> 124 | chai.expect(false).to.equal true, 'body called' 125 | .getClass() 126 | beforeEach () -> 127 | c = new PreconditionCallbacks 128 | it 'failing first precondition should call error callback once', (done) -> 129 | onError = done 130 | c.callMe undefined 131 | it 'failing second precondition should call error callback once', (done) -> 132 | onError = done 133 | c.callMe "a string" 134 | 135 | describe 'ClassContract', -> 136 | f = null 137 | beforeEach -> 138 | f = new examples.Foo 139 | 140 | it 'initializer shall be called', -> 141 | chai.expect(f.prop1).to.equal "foo" 142 | it 'initializer violating class invariant should throw', -> 143 | chai.expect(() -> new examples.InvalidInit).to.throw agree.ClassInvariantViolated 144 | it 'method violating class invariant should throw', -> 145 | chai.expect(() -> f.setPropNull 2, 3).to.throw agree.ClassInvariantViolated 146 | 147 | -------------------------------------------------------------------------------- /spec/examples.coffee: -------------------------------------------------------------------------------- 1 | 2 | agree = require '../' 3 | 4 | exports.multiplyByTwo = agree.function 'multiplyByTwo' 5 | .requires agree.conditions.noUndefined 6 | .requires agree.conditions.numbersOnly 7 | .ensures agree.conditions.numbersOnly 8 | .implement (input) -> 9 | return input*2 10 | 11 | 12 | # Invalid init 13 | agree.Class 'InvalidInit' 14 | .add exports 15 | .invariant agree.conditions.neverNull('prop1') 16 | .init () -> 17 | @prop1 = null 18 | 19 | agree.Class 'Initable' 20 | .add exports 21 | .invariant agree.conditions.neverNull('prop1') 22 | .init () -> 23 | @prop1 = "valid" 24 | .method 'dontcallme' 25 | .body (ignor) -> 26 | 27 | 28 | -------------------------------------------------------------------------------- /spec/introspection.coffee: -------------------------------------------------------------------------------- 1 | 2 | chai = require 'chai' 3 | agree = require '../' 4 | examples = require './examples' 5 | 6 | describe 'Introspection', -> 7 | 8 | describe 'a function', () -> 9 | contract = agree.getContract examples.multiplyByTwo 10 | it 'knows its Contract', -> 11 | chai.expect(contract).to.be.instanceof agree.FunctionContract 12 | it 'has a name', -> 13 | chai.expect(contract.name).to.equal 'multiplyByTwo' 14 | it 'has .toString() description', -> 15 | desc = examples.multiplyByTwo.toString() 16 | chai.expect(desc).to.contain 'multiplyByTwo' 17 | chai.expect(desc).to.contain 'no undefined' 18 | chai.expect(desc).to.contain 'must be numbers' 19 | chai.expect(desc).to.contain 'body' 20 | chai.expect(desc).to.contain 'function' 21 | describe 'a method', () -> 22 | instance = new examples.Initable 23 | contract = agree.getContract instance.dontcallme 24 | it 'knows its Contract', -> 25 | chai.expect(contract).to.be.instanceof agree.FunctionContract 26 | it 'has a name', -> 27 | chai.expect(contract.name).to.equal 'Initable.dontcallme' 28 | it 'knows the Contract of its class', -> 29 | chai.expect(contract.parent).to.be.instanceof agree.ClassContract 30 | chai.expect(contract.parent.name).to.equal 'Initable' 31 | it 'has .toString() description', -> 32 | desc = agree.introspection.describe instance.dontcallme 33 | chai.expect(desc).to.contain 'method' 34 | chai.expect(desc).to.contain 'dontcallme' 35 | chai.expect(desc).to.contain 'Initable' 36 | chai.expect(desc).to.contain 'body' 37 | describe 'a class', () -> 38 | contract = agree.getContract examples.InvalidInit 39 | it 'knows its Contract', -> 40 | chai.expect(contract).to.be.instanceof agree.ClassContract 41 | it 'has a name', -> 42 | chai.expect(contract.name).to.equal 'InvalidInit' 43 | it 'has .toString() description', -> 44 | desc = examples.Initable.toString() 45 | chai.expect(desc).to.contain 'class' 46 | chai.expect(desc).to.contain 'Initable' 47 | chai.expect(desc).to.contain 'method' 48 | chai.expect(desc).to.contain 'Initable.dontcallme' 49 | describe 'a class instance', -> 50 | it 'knows its Contract', -> 51 | instance = new examples.Initable 52 | contract = agree.getContract instance 53 | chai.expect(contract).to.be.instanceof agree.ClassContract 54 | it 'has .toString() description', -> 55 | instance = new examples.Initable 56 | desc = instance.toString() 57 | chai.expect(desc).to.contain 'instance' 58 | chai.expect(desc).to.contain 'Initable' 59 | chai.expect(desc).to.contain 'method' 60 | chai.expect(desc).to.contain 'Initable.dontcallme' 61 | describe 'preconditions', -> 62 | contract = agree.getContract examples.multiplyByTwo 63 | it 'can be enumerated', -> 64 | chai.expect(contract.preconditions).to.have.length 2 65 | it 'has description', -> 66 | chai.expect(contract.preconditions[0].name).to.equal 'no undefined arguments' 67 | 68 | describe 'postcondititions', -> 69 | contract = agree.getContract examples.multiplyByTwo 70 | it 'can be enumerated', -> 71 | chai.expect(contract.postconditions).to.have.length 1 72 | it 'has description', -> 73 | chai.expect(contract.postconditions[0].name).to.equal 'all arguments must be numbers' 74 | 75 | describe 'class invariants', -> 76 | contract = agree.getContract examples.InvalidInit 77 | it 'can be enumerated', -> 78 | chai.expect(contract.invariants).to.have.length 1 79 | it 'has description', -> 80 | chai.expect(contract.invariants[0].name).to.equal 'prop1 must not be null' 81 | 82 | 83 | describe 'Observing a function', -> 84 | observer = null 85 | func = examples.multiplyByTwo 86 | beforeEach () -> 87 | observer = agree.introspection.observe func 88 | afterEach () -> 89 | observer.reset() 90 | 91 | describe 'all preconditions fulfilled', -> 92 | beforeEach () -> 93 | func 42 94 | it 'causes body-enter and body-leave event', -> 95 | names = observer.events.map (e) -> return e.name 96 | chai.expect(names).to.include 'body-enter' 97 | chai.expect(names).to.include 'body-leave' 98 | it 'body-enter event has function arguments', -> 99 | events = observer.events.filter (e) -> return e.name == 'body-enter' 100 | chai.expect(events).to.have.length 1 101 | chai.expect(events[0].data.arguments).to.eql [42], events[0] 102 | it 'body-leave event has function return values', -> 103 | events = observer.events.filter (e) -> return e.name == 'body-leave' 104 | chai.expect(events).to.have.length 1 105 | chai.expect(events[0].data.returns).to.eql 84 106 | it 'Observer.toString() has description of events', -> 107 | desc = observer.toString() 108 | chai.expect(desc).to.contain 'body-enter' 109 | chai.expect(desc).to.contain 'body-leave' 110 | chai.expect(desc).to.contain 'preconditions-checked' 111 | chai.expect(desc).to.contain 'postconditions-checked' 112 | 113 | describe 'some preconditions failing', -> 114 | beforeEach () -> 115 | try 116 | func "notnumber" 117 | catch e 118 | # ignored, observing it 119 | it 'can get failed precondition', -> 120 | events = observer.events.filter (e) -> return e.name == 'preconditions-checked' 121 | failing = events[0].data.filter (c) -> return c.error != null 122 | chai.expect(failing).to.be.length 1 123 | chai.expect(failing[0].condition.name).to.equal "all arguments must be numbers" 124 | it 'can get passing precondition', -> 125 | events = observer.events.filter (e) -> return e.name == 'preconditions-checked' 126 | passing = events[0].data.filter (c) -> return c.error == null 127 | chai.expect(passing).to.be.length 1 128 | chai.expect(passing[0].condition.name).to.equal "no undefined arguments" 129 | 130 | describe 'postcondition failing', -> 131 | it 'can be observed' 132 | 133 | describe 'postcondition never called', -> 134 | it 'can be observed' 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /spec/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Agree.js browser tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/agree.coffee: -------------------------------------------------------------------------------- 1 | # Agree - Introspectable Contracts Programming for JavaScript 2 | # * Copyright (c) 2016 Jon Nordby 3 | # * Agree may be freely distributed under the MIT license 4 | 5 | ## Contracts 6 | # Allows specifying pre/post-conditions, class invariants on function, methods and classes. 7 | # 8 | # Contracts core 9 | # FIXME: add more flexible default error/precondfail reporting. 10 | # - .error 'throws' | 'callback' | 'return' ? 11 | # FIXME: make post-condition and invariants also obey/use onError callback 12 | # TODO: add first-class support for Promises, wrapper for node.js type async callbacks 13 | # - initially should be just for the body. Secondarily we could try to support async preconditions? 14 | # - since preconditions may rely on state not available sync, one should write a wrapper which 15 | # fetches all data to be asserted in preconditions, then pass it in as arguments 16 | # Should work with any A+/ES6/ES2015-compatible promises 17 | # 18 | # Open questions 19 | # - when a function body errors, should we then still evaluate the post-conditions? 20 | # - if we do, should we report these in onError? should we pass the body error, or not fire at all? 21 | # should there be a way to indicate functions which may fail (without it being a bug) 22 | # 23 | # API considerations 24 | # 25 | # - functions and classes/objects have-a (set of) contract(s) 26 | # - contracts should be declarable as an entity, and then later, associated with zero or more functions/classes 27 | # - as a convenience, should be possible to declare the contract for function/class 'inline' 28 | # - but for public APIs, contracts should always be declared separately - to encourage tracking them closely 29 | # - MAYBE: have a way to create contracts/functions which inherit others 30 | # 31 | # Later 32 | # 33 | # TODO: allow to compose Contracts and/or have multiple on one function/class 34 | # TODO: allow pre/postconditions on init/constructor functions 35 | # TODO: allow to declare properties, and invariants on them, using ES5 Object.defineProperty 36 | # https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 37 | # TODO: A way to declare and verify symmetrical functions, ie ones which cancel eachother out 38 | # requires to defined equality operators, over some context/domain? 39 | # example: increment()/decrement(), init()/reset(), push()/pop() 40 | # MAYBE: allow to 'inherit' when replacing a function, on object or prototype 41 | # 42 | # Debugging 43 | # - TODO: ability to log failing predicates, including description, location of fail, reason for fail 44 | # - TODO: ability to cause failing predicate to cause breakpoint using `debugger` statement 45 | # 46 | # Performance 47 | # - MAYBE: allow opt-out of postcondition and class invariant checking 48 | # 49 | # Research: See ./doc/braindump.md 50 | # 51 | # Random/ideas: 52 | # - Should contracts and their use be registered globally, for dependency tracking? 53 | # Could allow tracking whether they are all set up statically or not 54 | # 55 | # References: 56 | # 57 | # - http://c2.com/cgi/wiki?DesignByContract 58 | # - http://disnetdev.com/contracts.coffee/ custom syntax+compiler based on coffee-script 59 | # - http://dlang.org/contracts.html built-in support for contracts in D 60 | 61 | agree = {} 62 | 63 | # Export our Promise variant. For compat with old JS runtimes 64 | # Alternatively, consumers of the library can override agree.Promise to be a specific implementation 65 | if typeof Promise == 'undefined' 66 | agree.Promise = require 'bluebird' 67 | else 68 | agree.Promise = Promise 69 | 70 | introspection = require './introspection' 71 | common = require './common' 72 | agree.getContract = common.getContract 73 | 74 | # Framework 75 | class ContractFailed extends Error 76 | 77 | # TODO: attach contract and failure info to the Error object 78 | class PreconditionFailed extends ContractFailed 79 | constructor: (name, cond) -> 80 | @message = "#{name}: #{cond?.name}" 81 | 82 | class PostconditionFailed extends ContractFailed 83 | constructor: (name, cond) -> 84 | @message = "#{name}: #{cond?.name}" 85 | 86 | class ClassInvariantViolated extends ContractFailed 87 | constructor: (name, cond) -> 88 | @message = "#{name}: #{cond?.name}" 89 | 90 | class NotImplemented extends Error 91 | constructor: () -> 92 | @message = 'Body function not implemented' 93 | 94 | agree.ContractFailed = ContractFailed 95 | agree.PostconditionFailed = PostconditionFailed 96 | agree.ClassInvariantViolated = ClassInvariantViolated 97 | 98 | runInvariants = (invariants, instance, args) -> 99 | return [] if not agree.getContract(instance)? # XXX: is this a programming error? 100 | results = [] 101 | for invariant in invariants 102 | results.push 103 | error: invariant.check.apply instance 104 | invariant: invariant # TODO: remove, use condition instead 105 | condition: invariant 106 | return results 107 | 108 | runConditions = (conditions, instance, args, retvals) -> 109 | results = [] 110 | for cond in conditions 111 | target = if retvals? and cond.target != 'arguments' then retvals else args 112 | results.push 113 | error: cond.check.apply instance, target 114 | condition: cond 115 | return results 116 | 117 | isPromise = (obj) -> 118 | p = typeof obj?.then == 'function' 119 | return p 120 | 121 | class FunctionEvaluator 122 | constructor: (@bodyFunction, onError, @options) -> 123 | defaultFail = (instance, args, failures, stage) -> 124 | errors = failures.map (f) -> f.condition.name + ' ' + f.error.toString() 125 | msg = errors.join('\n') 126 | 127 | # FIXME: include standard, structured info with the Error objects 128 | if stage == 'preconditions' 129 | err = new PreconditionFailed msg 130 | else if stage == 'invariants-pre' or stage == 'invariants-post' 131 | err = new ClassInvariantViolated msg 132 | else if stage == 'postconditions' 133 | err = new PostconditionFailed msg 134 | else 135 | err = new Error "Agree.FunctionEvaluator: Unknown stage #{stage}" 136 | throw err 137 | 138 | @onError = if onError then onError else defaultFail 139 | # TODO: support callbacking with the error object instead of throwing 140 | # TODO: also pass invariant and post-condition failures through (user-overridable) function 141 | 142 | emit: (eventName, payload) -> 143 | # payload.context = undefined 144 | # console.log 'e', eventName, payload 145 | @observer eventName, payload if @observer 146 | observe: (eventHandler) -> 147 | @observer = eventHandler 148 | 149 | run: (instance, args, contract) -> 150 | instanceContract = agree.getContract instance 151 | invariants = if instanceContract? then instanceContract.invariants else [] 152 | argsArray = Array.prototype.slice.call args 153 | 154 | preChecks = () => 155 | # preconditions 156 | preconditions = if not @options.checkPrecond then [] else runConditions contract.preconditions, instance, args 157 | @emit 'preconditions-checked', preconditions 158 | prefailures = preconditions.filter (r) -> return r.error? 159 | if prefailures.length 160 | erret = @onError instance, args, prefailures, 'preconditions' 161 | return [true, errret] 162 | 163 | # invariants pre-check 164 | invs = if not @options.checkClassInvariants then [] else runInvariants invariants, instance, args 165 | @emit 'invariants-pre-checked', { invariants: invs, context: instance, arguments: argsArray } 166 | invprefailures = invs.filter (r) -> return r.error? 167 | if invprefailures.length 168 | errret = @onError instance, args, invprefailures, 'invariants-pre' 169 | return [true, errret] 170 | 171 | return [false, null] 172 | 173 | postChecks = (ret) => 174 | # invariants post-check 175 | invs = if not @options.checkClassInvariants then [] else runInvariants invariants, instance, args 176 | @emit 'invariants-post-checked', { invariants: invs, context: instance, arguments: argsArray } 177 | invpostfailures = invs.filter (r) -> return r.error? 178 | if invpostfailures.length 179 | errret = @onError instance, args, invpostfailures, 'invariants-post' 180 | return [true, errret] 181 | 182 | # postconditions 183 | postconditions = if not @options.checkPostcond then [] else runConditions contract.postconditions, instance, args, [ret] 184 | @emit 'postconditions-checked', postconditions 185 | postfailures = postconditions.filter (r) -> return r.error? 186 | if postfailures.length 187 | errret = @onError instance, args, postfailures, 'postconditions' 188 | return [true, erret] 189 | 190 | return [false, null] 191 | 192 | # pre-checks 193 | [stop, checkErr] = preChecks() 194 | return if stop 195 | 196 | # function body 197 | @emit 'body-enter', { context: instance, arguments: argsArray } 198 | ret = @bodyFunction.apply instance, args 199 | @emit 'body-leave', { context: instance, arguments: argsArray, returns: ret } 200 | 201 | # post-checks 202 | if isPromise ret 203 | ret = ret.then (value) -> 204 | [stop, checkErr] = postChecks value 205 | if stop 206 | return agree.Promise.reject checkErr 207 | else 208 | return agree.Promise.resolve value 209 | else 210 | postChecks ret 211 | return ret 212 | 213 | 214 | ### Condition 215 | # Can be used as precondition, postcondition or invariant in a FunctionContract or ClassContract 216 | # The predicate function @check should return an Error object on failure, or null on pass 217 | # 218 | # Functions which returns a Condition, can be used to provide a family of parametric conditions 219 | ### 220 | class Condition 221 | constructor: (@check, @name, @details) -> 222 | @name = 'unnamed condition' if not @name 223 | 224 | agree.Condition = Condition 225 | 226 | wrapFunc = (self, evaluator) -> 227 | return () -> 228 | instance = this 229 | evaluator.run instance, arguments, self 230 | 231 | class FunctionContract 232 | constructor: (@name, @parent, @options = {}, @parentname) -> 233 | @name = 'anonymous function' if not @name 234 | @postconditions = [] 235 | @preconditions = [] 236 | @attributes = {} 237 | @examples = [] 238 | @_agreeType = 'FunctionContract' 239 | 240 | defaultOptions = 241 | checkPrecond: true 242 | checkClassInvariants: true 243 | checkPostcond: true 244 | for k,v of defaultOptions 245 | @options[k] = v if not @options[k]? 246 | 247 | # implement this Contract in a external function 248 | implement: (original) -> 249 | evaluator = new FunctionEvaluator null, @onError, @options 250 | func = wrapFunc this, evaluator 251 | func._agreeContract = this # back-reference for introspection 252 | func._agreeEvaluator = evaluator # back-reference for introspection 253 | func.toString = () -> 254 | return introspection.describe this 255 | func._agreeEvaluator.bodyFunction = original 256 | func._agreeChain = original._agreeChain 257 | return func 258 | 259 | body: (func) -> 260 | f = @implement func 261 | if @parent and @parentname 262 | @parent.klass.prototype[@parentname] = f 263 | return this 264 | 265 | ## Fluent construction 266 | ensures: () -> @postcondition.apply @, arguments 267 | postcondition: (conditions, target) -> 268 | conditions = [conditions] if not conditions.length 269 | for c in conditions 270 | c = new Condition c, '' if typeof c == 'function' # inline predicate. TODO: allow name? 271 | c.target = target if target? 272 | @postconditions.push c 273 | return this 274 | 275 | requires: () -> @precondition.apply @, arguments 276 | precondition: (conditions) -> 277 | conditions = [conditions] if not conditions.length 278 | for c in conditions 279 | c = new Condition c, '' if typeof c == 'function' # inline predicate. TODO: allow name? 280 | @preconditions.push c 281 | return this 282 | 283 | attr: (key, val) -> 284 | @attributes[key] = val 285 | return this 286 | 287 | error: (onError) -> 288 | # FIXME: should only be for FunctionEvaluator? 289 | @onError = onError 290 | return this 291 | 292 | # TODO: Error if example does not pass pre and post-conditions 293 | successExample: (name, payload) -> 294 | type = payload._type if payload._type? 295 | type = 'function-call' if not type? 296 | @examples.push 297 | valid: true 298 | name: name 299 | payload: payload 300 | type: type 301 | return this 302 | 303 | # TODO: Error if example passes pre-conditions 304 | # XXX: Do we need another .type of failing examples, post-fail.. 305 | # causes post-conditions to fail, but has valid input? 306 | failExample: (name, payload) -> 307 | type = payload._type if payload._type? 308 | type = 'function-call' if not type? 309 | @examples.push 310 | valid: false 311 | name: name 312 | payload: payload 313 | type: type 314 | return this 315 | 316 | 317 | # Chain up to parent to continue fluent flow there 318 | method: () -> 319 | return @parent.method.apply @parent, arguments if @parent 320 | 321 | # Up 322 | getClass: () -> 323 | return @parent?.getClass() 324 | 325 | agree.FunctionContract = FunctionContract 326 | agree.function = (name, parent, options, pname) -> 327 | return new FunctionContract name, parent, options, pname 328 | 329 | # TODO: allow ClassContract to be used as interface 330 | class ClassContract 331 | constructor: (@name, @options = {}) -> 332 | @name = 'anonymous class' if not @name 333 | @invariants = [] 334 | @initializer = () -> 335 | # console.log 'ClassContract default initializer' 336 | @attributes = {} 337 | @_agreeType = 'ClassContract' 338 | 339 | self = this 340 | construct = (instance, args) => 341 | @construct instance, args 342 | @klass = () -> 343 | this.toString = () -> return introspection.describe this 344 | this._agreeContract = self # back-reference for introspection 345 | construct this, arguments 346 | @klass._agreeContract = this # back-reference for introspection 347 | @klass.toString = () -> return introspection.describe this 348 | 349 | defaultOptions = 350 | checkPrecond: true 351 | checkClassInvariants: true 352 | checkPostcond: true 353 | for k, v of defaultOptions 354 | @options[k] = v if not @options[k]? 355 | 356 | # add a method 357 | method: (name, opts) -> 358 | f = agree.function "#{@name}.#{name}", this, opts, name 359 | return f 360 | 361 | # add constructor 362 | init: (f) -> 363 | @initializer = f 364 | return this 365 | 366 | # add class invariant 367 | invariant: (conditions) -> 368 | conditions = [conditions] if not conditions.length 369 | for c in conditions 370 | c = new Condition c, '' if typeof c == 'function' # inline predicate. TODO: allow description? 371 | @invariants.push c 372 | return this 373 | 374 | attr: (key, val) -> 375 | @attributes[key] = val 376 | return this 377 | 378 | # register ordinary constructor 379 | add: (context, name) -> 380 | name = @name if not name 381 | context[name] = @klass 382 | return this 383 | 384 | construct: (instance, args) -> 385 | @initializer.apply instance, args 386 | 387 | # Check class invariants 388 | # FIXME: share this code with FunctionContract.runInvariants 389 | if @options.checkClassInvariants 390 | for invariant in agree.getContract(instance)?.invariants 391 | error = invariant.check.apply instance 392 | throw new ClassInvariantViolated "Constructor violated invariant: #{error}" if error 393 | return instance 394 | 395 | getClass: -> 396 | return @klass 397 | 398 | agree.ClassContract = ClassContract 399 | 400 | agree.Class = (name) -> 401 | return new ClassContract name 402 | 403 | module.exports = agree 404 | -------------------------------------------------------------------------------- /src/chain.coffee: -------------------------------------------------------------------------------- 1 | # Agree - Introspectable Contracts Programming for JavaScript 2 | # * Copyright (c) 2016 Jon Nordby 3 | # * Agree may be freely distributed under the MIT license 4 | 5 | # NOTE: move to separate library? Should be able to operate on any type of promises 6 | # TODO: Find a better name. Technically this is a 'promise factory', 7 | # but this does not explain about what problem it solves / why it was made / should be used. 8 | # 9 | # Which is to create a way of composing async/Promise functions which can be set up (quasi)statically, 10 | # then later used for actually performing a computation. 11 | # 12 | # Problem with Promise is that: 13 | # it starts executing immediately on .then(), which is also the mechanism to constructs chain / compose promise functions 14 | # it is single-use, keeping state of an execution 15 | # The need to pass in an object which is then populated with props is also quite horrible API 16 | # 17 | # Existing Promise composition libraries/operators 18 | # https://github.com/kriskowal/q 19 | # http://bluebirdjs.com/docs/api-reference.html 20 | # 21 | # Common mistakes with Promises https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html 22 | 23 | agree = require './agree' 24 | Promise = agree.Promise 25 | 26 | # TODO: support introspecting 'child' functions from a chained function 27 | # TODO: support introspecting 'parent' chain from a child function 28 | 29 | # TODO: support other Promise composition operators than .then 30 | # MAYBE: support custom Promise composition operators 31 | class PromiseChain 32 | ## describing 33 | constructor: (@name) -> 34 | @_agreeType = 'PromiseChain' 35 | @startFunction = null 36 | @chain = [] # { type: 'then', description: "", thenable: function } 37 | 38 | # alternative to setting @name in constructor 39 | describe: (@name) -> 40 | return this 41 | 42 | # Chaining up operations 43 | # FIXME: don't specialcase start internally 44 | start: (f) -> 45 | @startFunction = f 46 | return this 47 | 48 | then: (description, thenable) -> 49 | if not thenable 50 | thenable = description 51 | description = null 52 | 53 | @chain.push 54 | type: 'then' 55 | description: description 56 | thenable: thenable 57 | 58 | return this 59 | 60 | # Render chain into a function. Calling returned function executes the whole chain 61 | toFunction: () -> 62 | start = @startFunction 63 | if not start 64 | start = () -> 65 | return arguments[0] 66 | chainSelf = this 67 | 68 | func = () -> 69 | context = {} 70 | 71 | args = arguments 72 | promise = new Promise (resolve, reject) -> 73 | ret = start.apply context, args 74 | return resolve ret 75 | 76 | for step in chainSelf.chain 77 | promise = promise.then step.thenable.bind(context) 78 | return promise 79 | 80 | func._agreeChain = chainSelf # for introspection 81 | return func 82 | 83 | # Render chain into a function, and call it with provided @arguments 84 | call: (a, ...) -> 85 | #args = Array.prototype.slice.call arguments 86 | f = @toFunction() 87 | return f.apply this, arguments 88 | 89 | Chain = (name) -> 90 | return new PromiseChain name 91 | 92 | exports.Chain = Chain 93 | exports.PromiseChain = PromiseChain 94 | -------------------------------------------------------------------------------- /src/common.coffee: -------------------------------------------------------------------------------- 1 | # Agree - Introspectable Contracts Programming for JavaScript 2 | # * Copyright (c) 2016 Jon Nordby 3 | # * Agree may be freely distributed under the MIT license 4 | 5 | exports.getContract = (thing) -> 6 | if thing?._agreeType == 'FunctionContract' or thing?._agreeType == 'ClassContract' 7 | return thing 8 | return thing?._agreeContract 9 | 10 | exports.asyncSeries = (items, func, callback) -> 11 | items = items.slice 0 12 | results = [] 13 | next = () -> 14 | if items.length == 0 15 | return callback null, results 16 | item = items.shift() 17 | func item, (err, result) -> 18 | return callback err if err 19 | results.unshift result 20 | return next() 21 | next() 22 | -------------------------------------------------------------------------------- /src/conditions.coffee: -------------------------------------------------------------------------------- 1 | # Agree - Introspectable Contracts Programming for JavaScript 2 | # * Copyright (c) 2016 Jon Nordby 3 | # * Agree may be freely distributed under the MIT license 4 | 5 | ## Conditions/Predicates, can be used as class invariants, pre- or post-conditions 6 | # Some of these can be generic and provided by framework, parametriced to 7 | # tailor to particular program need. 8 | # 9 | # TODO: allow/encourage to attach failing and passing examples to condition (not just to contract) 10 | # - use for tests/doc of the contract/predicate itself 11 | # - basis for further reasoning, ref doc/brainstorm.md 12 | # 13 | # TODO: generalize the composition/parametrization of predicates? 14 | # - look up an identifier (string, number) in some context (arguments, this) 15 | # - take a value for the instance of a set (types, values) to check for 16 | # TODO: considering integration with common expect/assert libraries for checks 17 | # 18 | # TODO: find a way to ensure that conditions don't have side-effects! 19 | # 20 | # MAYBE: allow pre (and post) conditions to be async by returning a Promise 21 | # This may be needed for some things, like validating preconditions or ensuring post-conditions 22 | # which cannot be accessed sync, like in a database, external filesystem 23 | # Right now, solution for these things is to have a dedicated function before/after, which 24 | # fetches relevant state, and then do pre/post-conditions on thats. 25 | # With both ways, this opens up for race conditions, between fetching/checking preconditions and 26 | # executing the body of functions, because it is no longer in one iteration of eventloop 27 | # This would still be possible even if we do support Promises though 28 | # Need to see how it plays out in practical examples... 29 | 30 | agree = require './agree' 31 | Condition = agree.Condition 32 | 33 | conditions = {} 34 | 35 | checkUndefined = () -> 36 | index = 0 37 | for a in arguments 38 | isA = a? 39 | if not isA 40 | return new Error "Argument number #{index} is undefined" 41 | return null 42 | conditions.noUndefined = new Condition checkUndefined, 'no undefined arguments' 43 | 44 | conditions.noUndefined.examples = [ 45 | name: 'one undefined argument' 46 | valid: false 47 | create: () -> return conditions.noUndefined 48 | context: () -> return null 49 | args: [ undefined, 2, 3 ] 50 | ] 51 | 52 | checkNumbers = () -> 53 | index = 0 54 | for a in arguments 55 | if typeof a != 'number' 56 | return new Error "Argument number #{index} is not a number" 57 | return null 58 | conditions.numbersOnly = new Condition checkNumbers, "all arguments must be numbers" 59 | 60 | # parametric functions, returns a Condition 61 | conditions.neverNull = (attribute) -> 62 | p = () -> 63 | return if not this[attribute]? then new Error "Attribute #{attribute} is null" else null 64 | return new Condition p, "#{attribute} must not be null" 65 | 66 | conditions.attributeEquals = (attribute, value) -> 67 | p = () -> 68 | return if this[attribute] != value then new Error "Attribute #{attribute} does not equal #{value}" else null 69 | return new Condition p, "Attribute #{attribute} must equal value" 70 | 71 | conditions.attributeTypeEquals = (attribute, type) -> 72 | p = () -> 73 | actualType = typeof this[attribute] 74 | return if actualType != type then new Error "typeof this.#{attribute} != #{type}: #{actualType}" else null 75 | return new Condition p, "Attribute #{attribute} must be type #{type}" 76 | 77 | module.exports = conditions 78 | -------------------------------------------------------------------------------- /src/express.coffee: -------------------------------------------------------------------------------- 1 | # Agree - Introspectable Contracts Programming for JavaScript 2 | # * Copyright (c) 2016 Jon Nordby 3 | # * Agree may be freely distributed under the MIT license 4 | 5 | ## Convenience stuff around Express.JS 6 | # Will eventually be moved into its own library 7 | 8 | agree = require('./agree') 9 | 10 | conditions = {} 11 | conditions.requestContentType = (type) -> 12 | check = (req, res) -> 13 | actual = req.get 'Content-Type' 14 | err = if actual != type then new Error "Request must have Content-Type: '#{type}', got '#{actual}'" else null 15 | return err 16 | 17 | return new agree.Condition check, "Request must have Content-Type '#{type}'", { 'content-type': type } 18 | 19 | 20 | conditions.requestSchema = (schema, options={}) -> 21 | 22 | options.allowUnknown = false if not options.allowUnknown 23 | schema = agree.schema.normalize schema 24 | schemaDescription = schema.id 25 | schemaDescription = JSON.stringify schema if not schemaDescription? 26 | 27 | check = (req, res) -> 28 | return agree.schema.validate req.body, schema, options 29 | 30 | return new agree.Condition check, "Request body must follow schema '#{schemaDescription}'", { jsonSchema: schema } 31 | 32 | conditions.responseStatus = (code) -> 33 | check = (req, res) -> 34 | actual = res.statusCode 35 | err = if actual != code then new Error "Response did not have statusCode '#{code}', instead '#{actual}'" else null 36 | return err 37 | 38 | c = new agree.Condition check, "Response has statusCode '#{code}'", { 'statusCode': code } 39 | c.target = 'arguments' 40 | return c 41 | 42 | conditions.responseHeaderMatches = (header, regexp) -> 43 | check = (req, res) -> 44 | regexp = new RegExp regexp if typeof regexp == 'string' 45 | actual = res._headers[header.toLowerCase()] 46 | err = if actual? and regexp.test actual then null else new Error "Response header '#{header}':'#{actual}' did not match '#{regexp}'" 47 | return err 48 | 49 | c = new agree.Condition check, "Response header '#{header}' matches '#{regexp}'", { header: header, regexp: regexp } 50 | c.target = 'arguments' 51 | return c 52 | 53 | conditions.responseContentType = (type) -> 54 | check = (req, res) -> 55 | header = res._headers['content-type'] 56 | actual = header?.split(';')[0] 57 | err = if actual != type then new Error "Response has wrong Content-Type. Expected '#{type}', got '#{actual}'" else null 58 | return err 59 | 60 | c = new agree.Condition check, "Response has Content-Type '#{type}'", { 'content-type': type } 61 | c.target = 'arguments' 62 | return c 63 | 64 | checkResponseEnded = (req, res) -> 65 | return if not res._agreeFinished then new Error 'Response was not finished' else null 66 | conditions.responseEnded = new agree.Condition checkResponseEnded, "Reponse is sent" 67 | conditions.responseEnded.target = 'arguments' 68 | 69 | conditions.responseSchema = (schema, options = {}) -> 70 | options.allowUnknown = false if not options.allowUnknown 71 | schemaDescription = schema.id 72 | schemaDescription = JSON.stringify schema if not schemaDescription? 73 | schema = agree.schema.normalize schema 74 | check = (req, res) -> 75 | return agree.schema.validate res._jsonData, schema, options 76 | c = new agree.Condition check, "Response body follows schema '#{schemaDescription}'", { jsonSchema: schema } 77 | c.target = 'arguments' 78 | return c 79 | 80 | exports.installExpressRoutes = (app, routes) -> 81 | for name, route of routes 82 | contract = agree.getContract route 83 | method = contract.attributes.http_method?.toLowerCase() 84 | path = contract.attributes.http_path 85 | if method and path 86 | app[method] path, route 87 | else 88 | console.log "WARN: Contract '#{contract.name}' missing HTTP method/path" 89 | 90 | exports.mockingMiddleware = (req, res, next) -> 91 | original = 92 | json: res.json 93 | end: res.end 94 | # attache data sent with json() function to response, so we can validate 95 | res.json = (obj) -> 96 | res._jsonData = obj 97 | original.json.apply res, [obj] 98 | # defer end of response to next mainloop, so failing post-conditions can respond instead 99 | res.end = (data, enc, cb) -> 100 | res._agreeFinished = true 101 | setTimeout () -> 102 | original.end.apply res, [data, enc, cb] 103 | , 0 104 | next() 105 | 106 | exports.requestFail = (i, args, failures, reason) -> 107 | [req, res] = args 108 | statusCode = if reason == 'preconditions' then 422 else 500 109 | res.status statusCode 110 | errors = failures.map (f) -> { condition: f.condition.name, message: f.error.toString() } 111 | res.json { errors: errors } 112 | 113 | exports.selfDocument = (routes, path) -> 114 | f = new agree.FunctionContract "Documentation: GET #{path}" 115 | .attr 'http_method', 'GET' 116 | .attr 'http_path', path 117 | .attr 'http_resource', 'HTTP API Documentation' 118 | .ensures conditions.responseStatus 200 119 | .ensures conditions.responseContentType 'text/html' 120 | .implement (req, res) -> 121 | agree.doc.document routes, 'blueprint-html', (err, doc) -> 122 | throw err if err # XXX: HACK 123 | res.set 'Content-Type', 'text/html' 124 | res.end doc 125 | 126 | routes['selfDocument'] = f 127 | 128 | class Tester 129 | constructor: (@app) -> 130 | @port = process.env.PORT or 3334 131 | @host = process.env.HOST or 'localhost' 132 | @server = null 133 | setup: (callback) -> 134 | return @server = @app.listen @port, callback 135 | teardown: (callback) -> 136 | @server.close() if @server 137 | return callback null 138 | run: (thing, contract, example, callback) -> 139 | http = require 'http' 140 | method = contract.attributes.http_method 141 | path = example.path or contract.attributes.http_path 142 | test = example.payload 143 | r = 144 | host: @host 145 | port: @port 146 | method: method 147 | path: path 148 | r.headers = test.headers if test.headers? 149 | req = http.request r 150 | if typeof test.body == 'object' 151 | json = JSON.stringify test.body 152 | req.write json 153 | req.on 'response', (res) -> 154 | responseBody = "" 155 | res.on 'data', (chunk) -> 156 | responseBody += chunk.toString 'utf-8' 157 | res.on 'end', () -> 158 | checks = [] 159 | if test.responseCode? 160 | if test.responseCode != res.statusCode 161 | err = new Error "Wrong response status. Expected #{test.responseCode}, got #{res.statusCode}: \n#{responseBody}" 162 | checks.push { name: 'responseStatusCode', error: err} 163 | return callback null, checks 164 | req.on 'error', (err) -> 165 | console.log 'request error', err 166 | return if not callback 167 | callback err 168 | return callback = null 169 | req.end() 170 | exports.Tester = Tester 171 | 172 | exports.conditions = conditions 173 | -------------------------------------------------------------------------------- /src/introspection.coffee: -------------------------------------------------------------------------------- 1 | # Agree - Introspectable Contracts Programming for JavaScript 2 | # * Copyright (c) 2016 Jon Nordby 3 | # * Agree may be freely distributed under the MIT license 4 | 5 | common = require './common' 6 | 7 | # TODO: add ability to describe objects as HTML 8 | # MAYBE: let console/string describe just render the HTML with super simple style 9 | 10 | # TODO: add a test for function/method must have preconditions 11 | # TODO: add a test for function/method must have postconditions 12 | 13 | nl = "\n" 14 | ind = " " 15 | 16 | tryDescribeFunction = (thing, prefix) -> 17 | contract = common.getContract thing 18 | return null if not contract 19 | return null if contract.constructor.name != 'FunctionContract' 20 | 21 | type = if contract.parent then "method" else "function" 22 | 23 | output = nl+prefix+"#{type} '#{contract.name}'" 24 | # precond 25 | output += nl+prefix+ind+'preconditions:' if contract.preconditions.length 26 | for cond in contract.preconditions 27 | d = cond.name or cond.check?.description or cond.description or "unknown" 28 | output += nl+prefix+ind+ind+d 29 | 30 | # postcond 31 | output += nl+prefix+ind+'postconditions:' if contract.postconditions.length 32 | for cond in contract.postconditions 33 | d = cond.name or cond.check?.description or cond.description or "unknown" 34 | output += nl+prefix+ind+ind+d 35 | 36 | # body 37 | evaluator = thing._agreeEvaluator 38 | if evaluator? and evaluator.bodyFunction 39 | output += nl+prefix+ind+'body:' 40 | for line in evaluator.bodyFunction.toString().split '\n' 41 | output += nl+prefix+ind+ind+line 42 | 43 | return output 44 | 45 | tryDescribeClass = (thing, prefix) -> 46 | contract = common.getContract thing 47 | return null if not contract 48 | return null if contract.constructor.name != 'ClassContract' 49 | 50 | type = if typeof thing == 'object' then "instance" else "class" 51 | 52 | output = prefix+"#{type} #{contract.name}" 53 | for name, prop of thing.prototype 54 | out = tryDescribeFunction prop, prefix+ind 55 | output += out if out 56 | for name, prop of thing 57 | out = tryDescribeFunction prop, prefix+ind 58 | output += out if out 59 | 60 | return output 61 | 62 | # Return information 63 | exports.describe = (thing) -> 64 | contract = common.getContract thing 65 | return "No contract" if not contract? 66 | 67 | output = tryDescribeFunction thing, "" 68 | return output if output 69 | 70 | output = tryDescribeClass thing, "" 71 | return output if output 72 | 73 | output = 'Unknown contract' 74 | return output 75 | 76 | fbpSafeComponentName = (s) -> 77 | return null if not s 78 | return s.split(' ').join('_') 79 | 80 | # XXX: hacky representation of PromiseChain as an FBP graph 81 | chainToFBP = (chain) -> 82 | graph = 83 | inports: {} 84 | outports: {} 85 | processes: {} 86 | connections: [] 87 | 88 | names = [] 89 | chain.chain.forEach (step, idx) -> 90 | name = fbpSafeComponentName(step.description) or "then-#{idx}" 91 | names.push name 92 | component = 'agree/Function' # TODO: use name of Contract if known 93 | graph.processes[name] = 94 | component: component 95 | metadata: {} 96 | conn = 97 | src: 98 | process: names[idx-1] 99 | port: 'out' 100 | tgt: 101 | process: name 102 | port: 'in' 103 | if idx 104 | graph.connections.push conn 105 | return graph 106 | 107 | executionToFlowtrace = (chain) -> 108 | # FIXME: actually include events from execution 109 | # TODO: allow to synthesize graph from functions 110 | trace = 111 | header: 112 | graphs: {} 113 | events: [] 114 | trace.header.graphs['default'] = chainToFBP chain 115 | 116 | return trace 117 | 118 | # TODO: support transforming observed events to FBP runtime network:data, 119 | # for use with FBP clients like Flowhub and tools like Flowtrace 120 | exports.toFBP = (thing) -> 121 | if thing._agreeType == 'PromiseChain' 122 | return chainToFBP thing 123 | else if typeof thing == 'function' and thing._agreeChain 124 | return chainToFBP thing._agreeChain 125 | return null 126 | 127 | # XXX: right now can only observe one thing, which might be too inconvenient in practice 128 | # should possibly observe a set of things. Issue is that then event monitoring / analysis also needs to take filter 129 | class Observer 130 | constructor: (@thing) -> 131 | @reset() 132 | @contract = common.getContract @thing 133 | @evaluator = @thing?._agreeEvaluator 134 | 135 | if @evaluator 136 | @evaluator.observe (event, data) => 137 | @onEvent event, data 138 | 139 | reset: () -> 140 | @events = [] 141 | @evaluator.observe null if @evaluator 142 | 143 | onEvent: (eventName, payload) -> 144 | @events.push 145 | name: eventName 146 | data: payload 147 | @emit 'event', eventName, payload 148 | # MAYBE: emit specific events? 'precondition-failed' etc 149 | 150 | emit: (m, args) -> 151 | # TODO: allow to follow events as they happen 152 | 153 | toString: () -> 154 | # TODO: event-aware toString() formatting 155 | # TODO: colorize failures 156 | lines = [] 157 | lines.push "agree.Observer: #{@events.length} events" 158 | for event in @events 159 | data = JSON.stringify event.data, (key, val) -> 160 | # avoid circular reference when context is global 161 | return if key == 'context' and val.global? then 'global' else val 162 | lines.push " #{event.name}: #{data}" 163 | return lines.join '\n' 164 | 165 | exports.Observer = Observer 166 | exports.observe = (thing) -> 167 | return new Observer thing 168 | 169 | -------------------------------------------------------------------------------- /src/schema.coffee: -------------------------------------------------------------------------------- 1 | # Agree - Introspectable Contracts Programming for JavaScript 2 | # * Copyright (c) 2016 Jon Nordby 3 | # * Agree may be freely distributed under the MIT license 4 | 5 | ## JSON Schema support 6 | # 7 | # TODO: allow to infer schema from example object(s). TODO: check existing libraries for this feature 8 | # TODO: allow registering and referencing named schemas 9 | # MAYBE: combine inferred schema, with class-invariant, 10 | # to ensure all properties are declared in constructor with defaults? 11 | # if used as pre-condition on other functions, basically equivalent to a traditional class type! 12 | # TODO: allow to infer schema from Knex schema/queries or Postgres/*SQL schemas 13 | 14 | exports.validate = (data, schema, options) -> 15 | tv4 = require 'tv4' 16 | 17 | result = tv4.validateMultiple data, schema, !options.allowUnknown 18 | #console.log 'd', data, result 19 | if result.valid 20 | return null 21 | else 22 | message = [] 23 | for e in result.errors 24 | message.push "#{e.message} for path '#{e.dataPath}'" 25 | return new Error message.join('\n') 26 | 27 | exports.normalize = (schema) -> 28 | schema['$schema'] = 'http://json-schema.org/draft-04/schema' if not schema['$schema'] 29 | 30 | return schema 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | module.exports = { 4 | cache: true, 5 | entry: './index.js', 6 | output: { 7 | path: path.join(__dirname, "dist"), 8 | publicPath: "dist/", 9 | filename: "agree.js", 10 | library: 'agree', 11 | libraryTarget: 'umd' 12 | }, 13 | externals: [{ 14 | 'http' : 'commonjs http', 15 | }], 16 | module: { 17 | loaders: [ 18 | { test: /\.coffee$/, loader: "coffee-loader" }, 19 | ] 20 | }, 21 | resolve: { 22 | extensions: ["", ".coffee", ".js"] 23 | }, 24 | plugins: [ 25 | // none 26 | ] 27 | }; 28 | --------------------------------------------------------------------------------