{"action":"created","issue":{"url":"https://api.github.com/repos/chaijs/chai/issues/585","repository_url":"https://api.github.com/repos/chaijs/chai","labels_url":"https://api.github.com/repos/chaijs/chai/issues/585/labels{/name}","comments_url":"https://api.github.com/repos/chaijs/chai/issues/585/comments","events_url":"https://api.github.com/repos/chaijs/chai/issues/585/events","html_url":"https://github.com/chaijs/chai/issues/585","id":124953583,"number":585,"title":"Refactor plugin declarations","user":{"login":"keithamus","id":118266,"avatar_url":"https://avatars.githubusercontent.com/u/118266?v=3","gravatar_id":"","url":"https://api.github.com/users/keithamus","html_url":"https://github.com/keithamus","followers_url":"https://api.github.com/users/keithamus/followers","following_url":"https://api.github.com/users/keithamus/following{/other_user}","gists_url":"https://api.github.com/users/keithamus/gists{/gist_id}","starred_url":"https://api.github.com/users/keithamus/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/keithamus/subscriptions","organizations_url":"https://api.github.com/users/keithamus/orgs","repos_url":"https://api.github.com/users/keithamus/repos","events_url":"https://api.github.com/users/keithamus/events{/privacy}","received_events_url":"https://api.github.com/users/keithamus/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/chaijs/chai/labels/more-discussion-needed","name":"more-discussion-needed","color":"bfd4f2"}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":21,"created_at":"2016-01-05T12:00:37Z","updated_at":"2016-08-24T05:23:19Z","closed_at":null,"body":"I'm going to put this document here, as the start of a design discussion around how plugins could be written for a future version of chai. Similar discussions have been had before, over in [Google Groups](https://groups.google.com/forum/#!msg/chaijs/TXtkOAGyetU/tE0iC_WJxboJ), and issues #117 and #457; but I wanted to start a new issue which can focus on the design of a new plugin architecture, and hopefully get some feedback.\r\n\r\n### Current Implementation\r\n\r\nSo, just to get everyone on track - right now, you can make a chai plugin by writing a little bit of code, like this:\r\n\r\n```js\r\nexport default (chai, utils) => {\r\n  chai.Assertion.addMethod('equal', function (val, msg) {\r\n    this.assert(\r\n      val === obj,\r\n      'expected #{this} to equal #{exp}',\r\n      'expected #{this} to not equal #{exp}',\r\n      val,\r\n      this._obj,\r\n      true\r\n    );\r\n  });\r\n\r\n}\r\n```\r\n\r\n### Motivation\r\n\r\nRight now we have a nicely expressive interface for creating plugins - but it has a few problems, which can be addressed by making working on a new interface. The problems it has are:\r\n\r\n  - It caters to `expect` and `should` interfaces as first class, but does not create methods for the `assert` interface.\r\n  - Plugins define whether or not an assertion is a property or a method - there is `addMethod`, `addProperty`, and `addChainableMethod`. These in themselves aren't bad, but it does create a bit of an inconsistent interface, as it is an opt-in convention to use property assertions and chainable methods. Similarly, for users who only want to use method assertions, they have to use plugins like [dirty-chai].\r\n  - Also around convention, it's up to the plugin author whether or not a method supports reassignment of the message. Chai's core assertions allow you to overwrite the assertion message by passing in the last argument (e.g. `expect(true).to.equal(true, 'True should be true!')`). Some plugins dont do this.\r\n  - Flags are imperative - and it's up to the methods as to whether or not they support the flag, and as such they work for some methods but not for others, and there is no way to determine which assertions support which flags.\r\n  - Many plugins, such as [chai-things] or [chai-as-promised] effectively add one or two \"interceptor\" flags - for example [chai-as-promised] adds `eventually` to every method, which means programmatically rewriting every function. We should have an expressive syntax for this.\r\n  - Some flags are \"first class\" flags, like `not`. In fact, `not` is so special that every assertion is __required__ to support it. This should not be the case.\r\n  - We have a strange and somewhat limited templating language for messages [some work took place to refactor it and add lots of functionality](https://github.com/chaijs/chai/pull/393) - but this pushed us further into having specific syntaxes.\r\n  - `chai.Assertion.addMethod` passes a `this` value, and a `this.assert` must be called within an assertion, to assert on anything. This could be simplified, and made less dependant on magic values like `this`.\r\n  - `this.assert` has a list of ambiguous arguments, including boolean flags, and so it can be hard to decipher what is actually happening. If you don't pass `true` as the 6th argument - a diff wont show up for the assertion. This is a gotcha for even some of our best plugin authors.\r\n  - Aliases are very common, but there is no syntax or helpers to support this - and so aliases have to be created manually.\r\n\r\n\r\n### Requirements\r\n\r\nGiven the issues, we can set out a list of requirements that a new architecture should provide:\r\n\r\n#### Must haves\r\n\r\n  - Adding methods must be made more declarative, to support pushing it to different interfaces, rather than having an imperative concatenation of methods via the existing `addMethod`/`addProperty`/`addChainableMethod` functions.\r\n  - Flags should also be declarative. Flags are mainly used to alter the behaviour of one or more methods. We should provide a cleaner way to facilitate this.\r\n  - Methods should know as little as possible about flags (maybe nothing at all?). Separation of responsibilities.\r\n  - The methods that are passed to `addMethod` should be able to be implemented much simpler - most small methods probably be distilled down to returning a String error message. Making them \"pure functions\" (arguments are the only input, we determine what to do on the output, no side effects) would be an ideal.\r\n  - Methods should not have to determine details such as whether or not to display a diff, this should be dealt with much more on the library level.\r\n\r\n#### Nice to haves\r\n\r\n  - Drastically simplifying error messages would be nice. Perhaps we could just rely on ES6 template strings? Since almost every error message follows the same format of `expected {object} to {some expectation} but got {value}` - perhaps we could just have assertions declare the `{some expectation}` part.\r\n  - If methods also declared their documentation, types, and examples using JavaScript, we could have some fantastic tools to generate docs and provide more helpful information with errors.\r\n\r\n\r\n### Draft concept\r\n\r\nTo start really thinking about a design - here is a draft concept I think could work when defining new assertions (this is an iteration on [a previous draft in #117](https://github.com/chaijs/chai/issues/117#issuecomment-90508208)\r\n\r\nPlease note:\r\n\r\n> #### This is an opportunity to comment, pick apart, bikeshed, this API. Let's get this absolutely right to make sure we have the best plugin system for a truly extensible chai framework :wink: \r\n\r\n```js\r\n// All chai plugins must export an Object.\r\n// The reason we export an object, rather than a function - is that plugins should\r\n// not have access to chai or chai's utils. Why? Well, because we aim to modularise\r\n// all of them into separate packages, and plugins should rely on npm modules instead\r\n// of chai's utils to write assertions.\r\nexport default {\r\n\r\n  // The plugin object has 4 main keys:\r\n  //  - namespace: The namespace of the plugin, usually blank but could be related\r\n  //    to the plugin.\r\n  //  - assertions: A list of assertions that extend Assert API. These assertions must\r\n  //    be uniquely named, and trying to use a plugin that conflicts will throw.\r\n  //  - interceptors, which are methods (which may include flags) that can alter an\r\n  //    object before it is passed to an assertion\r\n  //  - modifiers: Methods (which include flags) that can alter the result of an\r\n  //    assertion before it is finally reported\r\n  // Any other property can be added to these - which could be useful for authors\r\n  // who want to provide, for example, utility functions to a user. For example a\r\n  // spy/mocking tool might add a `spy()` method to this object, so that users can\r\n  // call:\r\n  // ```js\r\n  // chaiSpies = require('chai-spies');\r\n  // var spy = chaiSpies.spy;\r\n  // chai.use(chaiSpies);\r\n  // ```\r\n  // (or in ES6):\r\n  // ```js\r\n  // import chaiSpies, { spy } from 'chai-spies';\r\n  // chai.use(chaiSpies);\r\n  // ```\r\n\r\n  // Assertions is a key/value hash of name/method. These are read and added to\r\n  // the interface at runtime.\r\n  assertions: {\r\n\r\n    // Assertions are just properties on the Object literal.\r\n    // The assertion method is given 2 arguments, the actual and the expected.\r\n    // It can return one of two things:\r\n    //   - A boolean, which determines the result of the assertion\r\n    //   - An object, with the properties `result`, `actual`, and `expected`.\r\n    //     This is more useful if the `actual` or `expected` properties have changed\r\n    //     during the assertion, for example coercing a value to a type.\r\n    //     If they are not given, they are assumed to be the first and\r\n    //     second arguments, respectively.\r\n    //\r\n    // The add signature below will provide the following:\r\n    //\r\n    // - expect, expect-safe\r\n    //   expect('foo').to.equal('bar');\r\n    //   > expected \"foo\" to equal \"bar\"\r\n    //   expect('foo').to.equal('bar', 'mymessage');\r\n    //   > expected \"foo\" to equal \"bar\": mymessage\r\n    //\r\n    // - expect-legacy\r\n    //   expect('foo').toEqual('bar');\r\n    //   > expected \"foo\" to equal \"bar\"\r\n    //   expect('foo').toEqual('bar', 'mymessage');\r\n    //   > expected \"foo\" to equal \"bar\": mymessage\r\n    //\r\n    // - should, should-safe\r\n    //   'foo'.should.equal('bar');\r\n    //   > expected \"foo\" to equal \"bar\"\r\n    //   'foo'.should.equal('bar', 'mymessage');\r\n    //   > expected \"foo\" to equal \"bar\": mymessage\r\n    //\r\n    // - assert\r\n    //   assert.equal('foo', 'bar');\r\n    //   > expected \"foo\" to equal \"bar\"\r\n    //   assert.equal('foo', 'bar', 'mymessage');\r\n    //   > expected \"foo\" to equal \"bar\": mymessage\r\n    //\r\n    'equal': {\r\n      assert: (actual, expected) => (actual == expected),\r\n    },\r\n\r\n    // Properties like `.to.be.ok` use the same add interface, as the decision of\r\n    // whether or not they are property vs method assertions is up to the interface,\r\n    // and can be determined by the assertion's arity (`function.length`).\r\n    //\r\n    // Notice that the assertion's name includes spaces. This is for two reasons:\r\n    //  - The assertion's name is used to determine the final error message\r\n    //    in the case of a failure. The psuedo code for how a message is built up\r\n    //    might look something like:\r\n    //     if (assertionReturnedExpectedValue) {\r\n    //      return `expected ${inspectedValue}` to ${nameOfAssertion}`\r\n    //     } else {\r\n    //       return `expected ${inspectedValue}` to ${nameOfAssertion} but got ${expected};\r\n    //     }\r\n    //  - Any additional words within the assertions phrase like structure can be\r\n    //    added as additional grammars to interfaces - in this case, \"be ok\" tells\r\n    //    the interface that \"ok\" is the assertion, but it also needs to add \"be\"\r\n    //    as a no-op grammar.\r\n    //\r\n    //\r\n    // The add signature below will provide the following:\r\n    //\r\n    // - expect\r\n    //   expect(false).to.be.ok\r\n    //   > expected false to be ok\r\n    //\r\n    // - expect-safe\r\n    //   expect(false).to.be.ok();\r\n    //   > expected false to be ok\r\n    //   expect('foo').to.be.ok('mymessage');\r\n    //   > expected false to be ok: mymessage\r\n    //\r\n    // - expect-legacy\r\n    //   expect(false).toExist();\r\n    //   > expected false to be ok\r\n    //   expect('foo').toExist('mymessage');\r\n    //   > expected false to be ok: mymessage\r\n    //\r\n    // - should\r\n    //   (false).should.be.ok;\r\n    //   > expected false to be ok\r\n    //\r\n    // - should-safe\r\n    //   (false).should.be.ok();\r\n    //   > expected false to be ok\r\n    //   (false).should.be.ok('mymessage');\r\n    //   > expected false to be ok: mymessage\r\n    //\r\n    // - assert\r\n    //   assert.ok(false);\r\n    //   > expected false to be ok\r\n    //   assert.ok(false, 'mymessage');\r\n    //   > expected false to be ok: mymessage\r\n    'be ok': {\r\n      assert: (actual, expected) => Boolean(actual),\r\n    },\r\n\r\n    // Methods can define metadata about themselves by passing an Object literal\r\n    // instead of a function. For example, an assertion can also define aliases by\r\n    // passing an `aliases` array. Of course, one canonical name needs to be chosen\r\n    // for an assertion.\r\n    //\r\n    // Normally, the method name is written in a declarative style (`be a`),\r\n    // because most of the interfaces are declarative (\"expect().to.be.a\")\r\n    // however, an `imperative` String property can provide a naming hint for\r\n    // imperative interfaces (such as assert) to use instead.\r\n    //\r\n    // Also, while typically the assertions canonical name is used to generate an\r\n    // error message, sometimes thats not good enough for certain cases, and so\r\n    // the `message` property can be used to override this. The message property\r\n    // should always be a string.\r\n    //\r\n    // The below will provide the following:\r\n    //\r\n    // - expect, expect-safe\r\n    //   expect('foo').to.be.a('number');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\"\r\n    //   expect('foo').to.equal('number', 'mymessage');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\": mymessage\r\n    //\r\n    // - expect-legacy\r\n    //   expect('foo').toBeA('number');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\"\r\n    //   expect('foo').toBeA('number', 'mymessage');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\": mymessage\r\n    //\r\n    // - should, should-safe\r\n    //   'foo'.should.be.a('number');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\"\r\n    //   'foo'.should.equal('number', 'mymessage');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\": mymessage\r\n    //\r\n    // - assert\r\n    //   assert.type('foo', 'number');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\"\r\n    //   assert.type('foo', 'number', 'mymessage');\r\n    //   > expected \"foo\" to be a \"number\" but got \"string\"\r\n    //\r\n    'be an': {\r\n      aliases: ['be a'],\r\n      imperative: 'type',\r\n      assert: (actual, expected) => {\r\n        const actualType = utils.type(actual);\r\n        const expectedType = String(expected).toLowerCase();\r\n        return {\r\n          result: actualType != expectedType,\r\n          actual: actualType,\r\n          expected: expectedType,\r\n          message: (\r\n            [ 'a', 'e', 'i', 'o', 'u' ].indexOf(expectedType[0]) ? 'be an' : 'be a'\r\n          ),\r\n        }\r\n      }\r\n    }\r\n\r\n\r\n    // A method can also add docs and examples, and type checking to params, by\r\n    // adding additional properties. All of the properties an assertion can\r\n    // include are:\r\n    //  - assert: the assertion method\r\n    //  - aliases: alternative assertion names to use\r\n    //  - message: customise the message output (defaults to this key)\r\n    //  - description: describe the assertion\r\n    //  - params: use `chai.match` to assert on passed arguments.\r\n    //  - examples: provide arguments that will cause the assertion to pass\r\n    //  - badExamples: provide arguments that will cause the assertion to fail\r\n    // This allows for documentation generation, and more\r\n    // interesting features like live documentation if an assertion is\r\n    // misused, like so:\r\n    //\r\n    // expect('foo').to.be.greaterThan('a string');\r\n    // > Assertion 'be greaterThan' expected the first argument to be a number,\r\n    // > but was a string.\r\n    // > Here is the documentation for this method:\r\n    // >   ## be greaterThan\r\n    // >   .be.above(String/{ length: Number })\r\n    // >   .be.gt(String/{ length: Number })\r\n    // >   .be.greaterThan(String/{ length: Number })\r\n    // >\r\n    // >   Assert on the type of a given value\r\n    // >\r\n    // >   ### Examples:\r\n    // >   expect(10).to.be.above(5)\r\n    // >   expect([1,2,3]]).to.be.above(2)\r\n    // >   expect('test').to.be.above(5)\r\n    // >   ...\r\n    // >   ### Example failures:\r\n    // >   expect(5).to.be.above(10); // expected 5 to be above 10\r\n    // >   expect([1,2,3]).to.be.above(5); // expected [1,2,3] to have a length above 5 but got 3\r\n    //\r\n    // The `.example` and `.badExample` calls can also be used as tests themselves,\r\n    // meaning an assertion's code serves to be the complete reference for itself.\r\n    // We could introduce new tooling such as chai-plugin-test and chai-doc-gen which\r\n    // provide these functions and make plugin development almost trivial\r\n    'be above': {\r\n      aliases: ['have length above', 'be gt', 'be greaterThan'],\r\n      description: `Asserts that the target is greater than value.`,\r\n      params: [\r\n        'oneOfType': [\r\n          'number',\r\n          'shape': {\r\n            length: number\r\n          }\r\n        ],\r\n        'number',\r\n      }\r\n      examples: [\r\n        [ 10,      5  ],\r\n        [ [1,2,3], 2  ],\r\n        [ 5,       10 ],\r\n        [ 'test',  3  ],\r\n      ],\r\n      badExamples: [\r\n        [ [1,2,3], 5  ],\r\n        [ 'test',  5  ],\r\n      ],\r\n      assert(actual, expected) {\r\n        const useLength = typeof actual === 'number';\r\n        actual =  useLength ? actual : actual.length;\r\n        return {\r\n          result: actual > expected,\r\n          actual,\r\n          message: (useLength ? 'have a length above' : 'be above'),\r\n          expected\r\n          // Here we've added an additional property which we can use for the\r\n          // .message() function.\r\n        }\r\n      },\r\n    },\r\n  },\r\n\r\n\r\n  // modifiers\r\n  // modifiers are flags that can alter the result of an assertion before\r\n  // it is finally reported. For example `.not` is a modifier - it toggles the final\r\n  // result of an assertion.\r\n  // Modifiers are given the result of an assertion call, and get an opportunity to\r\n  // modify the output before it is sent to chai to report on.\r\n  // Importantly, the lifecycle of a modifier flag is:\r\n  //    result = modifier(assertion(actual, expected)).\r\n  modifiers: {\r\n\r\n    // .not is the classic example of a modifier flag: it flips the result boolean\r\n    // so `false` becomes `true` and `true` becomes `false`, changing the behaviour\r\n    // of a particular assertion.\r\n    'not': (output) => {\r\n      output.result = !output.result;\r\n      return output;\r\n    },\r\n\r\n  },\r\n\r\n  // Here is an example of an interceptor flag - something akin to Chai-Things\r\n  // \"all\" assertion. An interceptor is passed the method that would have been\r\n  // called if the flag wasn't present, and the arguments that were given to that.\r\n  // interceptors act as a kind of \"proxy assert\" - they can change the value of\r\n  // any given arguments, or run the assertion multiple times, or perhaps\r\n  // conditionally run the assertion.\r\n  interceptors: {\r\n\r\n    // all from chai-things is an example of an interceptor that calls the desired\r\n    // method on sub-properties of the given object (`actual`). In the case of .all,\r\n    // it calls the assertion for every property of the array. As soon as the first\r\n    // failing call occurs (result === false) it returns that failure.\r\n    'all': (assertion, actual, ...args) => {\r\n      let output;\r\n      for (const element in actual) {\r\n        output = assertion(element, ...args);\r\n        if (output.result === false) {\r\n          return output;\r\n        }\r\n      }\r\n      return output;\r\n    },\r\n\r\n    // deep from chai-core is an example of an interceptor that does different things\r\n    // for different methods, as such it has many different aliases, for example:\r\n\r\n    // deep.equal calls deep equality on a property, overriding the original assertion\r\n    'deep equal': (assertion, actual, expected) => {\r\n      return {\r\n        result: deepEqual(actual, expected),\r\n      };\r\n    },\r\n\r\n    // deep.property traverses the object using the given string, overriding the\r\n    // original property assertion\r\n    'deep property': (assertion, actual, expected) => {\r\n      return {\r\n        result: getDeepProperty(actual, expected),\r\n      };\r\n    },\r\n\r\n    // Interceptors can also override methods without using any flags, instead it can\r\n    // use ducktyping to toggle an assertion (kind of like one of those Railroad\r\n    // switches, redirecitng the assertion given a set of requirements).\r\n    // Here is an example that could be found in a chai-react plugin, that uses a\r\n    // different deep equality on two React elements\r\n    'equal': (assertion, actual, expected) => {\r\n      if (React.isElement(acutal) && React.isElement(expected)) {\r\n        return {\r\n          result: myReactDeepEqualityAlgo(actual, expected),\r\n        }\r\n      } else {\r\n        return assertion(actual, expected);\r\n      }\r\n    }\r\n\r\n  },\r\n\r\n};\r\n```\r\n\r\n[chai-things]: https://github.com/chaijs/Chai-Things\r\n[chai-as-promised]: https://github.com/domenic/chai-as-promised\r\n[dirty-chai]: https://github.com/prodatakey/dirty-chai\r\n"},"comment":{"url":"https://api.github.com/repos/chaijs/chai/issues/comments/241960573","html_url":"https://github.com/chaijs/chai/issues/585#issuecomment-241960573","issue_url":"https://api.github.com/repos/chaijs/chai/issues/585","id":241960573,"user":{"login":"Alhadis","id":2346707,"avatar_url":"https://avatars.githubusercontent.com/u/2346707?v=3","gravatar_id":"","url":"https://api.github.com/users/Alhadis","html_url":"https://github.com/Alhadis","followers_url":"https://api.github.com/users/Alhadis/followers","following_url":"https://api.github.com/users/Alhadis/following{/other_user}","gists_url":"https://api.github.com/users/Alhadis/gists{/gist_id}","starred_url":"https://api.github.com/users/Alhadis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Alhadis/subscriptions","organizations_url":"https://api.github.com/users/Alhadis/orgs","repos_url":"https://api.github.com/users/Alhadis/repos","events_url":"https://api.github.com/users/Alhadis/events{/privacy}","received_events_url":"https://api.github.com/users/Alhadis/received_events","type":"User","site_admin":false},"created_at":"2016-08-24T05:23:19Z","updated_at":"2016-08-24T05:23:19Z","body":"Ah right. No no, you understood my doubt correctly. :) Thanks for the speedy response! You guys rock."}}
{"actor":{"display_login":"Alhadis"}}