From 9ef3c31b8b95df4c0abff3d3d5728b5dd679a314 Mon Sep 17 00:00:00 2001 From: Aldwin Vlasblom Date: Mon, 8 May 2017 14:18:26 +0200 Subject: [PATCH] Add a parse function to the library Closes #4 Closes #8 (supersedes) --- Makefile | 7 +++ index.js | 142 +++++++++++++++++++++++++++++++++++--------------- package.json | 1 + test/index.js | 23 +++++++- 4 files changed, 129 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 36d520a..085b0a9 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +DOCTEST = node_modules/.bin/doctest --module commonjs --prefix . ESLINT = node_modules/.bin/eslint --config node_modules/sanctuary-style/eslint-es3.json --env es3 MOCHA = node_modules/.bin/mocha --reporter dot --ui tdd NPM = npm @@ -34,6 +35,7 @@ lint: --env node \ --global suite \ --global test \ + --rule 'max-len: [off]' \ -- test/index.js $(REMEMBER_BOWER) $(shell pwd) @@ -51,3 +53,8 @@ setup: .PHONY: test test: $(MOCHA) -- test/index.js +ifeq ($(shell node --version | cut -d . -f 1),v6) + $(DOCTEST) -- index.js +else + @echo '[WARN] Doctests are only run in Node v6.x.x (current version is $(shell node --version))' >&2 +endif diff --git a/index.js b/index.js index 7cf8a34..29d25f8 100644 --- a/index.js +++ b/index.js @@ -20,39 +20,38 @@ //. purpose: the [`typeof`][1] operator and [`Object.prototype.toString`][2]. //. Each has pros and cons, but neither supports user-defined types. //. -//. This package specifies an [algorithm][3] for deriving a _type identifier_ -//. from any JavaScript value, and exports an implementation of the algorithm. -//. Authors of algebraic data types may follow this specification in order to -//. make their data types compatible with the algorithm. +//. This package exports a function which derives a _type identifier_ from any +//. JavaScript value, and a [specification][3] for customizing the type +//. identifier of a value. //. -//. ### Algorithm +//. ### Specification //. -//. 1. Take any JavaScript value `x`. +//. For a type to be compatible with the algorithm: //. -//. 2. If `x` is `null` or `undefined`, go to step 6. -//. -//. 3. If `x.constructor` evaluates to `null` or `undefined`, go to step 6. -//. -//. 4. If `x.constructor.prototype === x`, go to step 6. This check prevents a -//. prototype object from being considered a member of its associated type. +//. - every member of the type MUST have a `constructor` property pointing +//. to an object known as the _type representative_; //. -//. 5. If `typeof x.constructor['@@type']` evaluates to `'string'`, return -//. the value of `x.constructor['@@type']`. +//. - the type representative MUST have a `@@type` property; //. -//. 6. Return the [`Object.prototype.toString`][2] representation of `x` -//. without the leading `'[object '` and trailing `']'`. +//. - the type representative's `@@type` property (the _type identifier_) +//. MUST be a string primitive and SHOULD have format: +//. `'/[@]'`, where: //. -//. ### Compatibility +//. - The `namespace` MUST consist of one or more characters, and SHOULD +//. equal the npm package name which defines the type (including +//. [scope][4] where appropriate); and //. -//. For an algebraic data type to be compatible with the [algorithm][3]: +//. - the `name` MUST consist of one or more characters, and SHOULD be +//. the unique name of the type; and //. -//. - every member of the type must have a `constructor` property pointing -//. to an object known as the _type representative_; +//. - the `version` MUST be a numeric value consisting of one or more +//. digits, and it SHOULD represent the version of the type. //. -//. - the type representative must have a `@@type` property; and +//. - If the property does not conform to the format specified above, it is +//. assumed that the entire string represents the _name_ of the type, and +//. _namespace_ will be `null`. //. -//. - the type representative's `@@type` property (the _type identifier_) -//. must be a string primitive, ideally `'/'`. +//. - If the version is not given, it is assumed to be `0`. //. //. For example: //. @@ -81,24 +80,6 @@ //. return {constructor: IdentityTypeRep, value: x}; //. } //. ``` -//. -//. ### Usage -//. -//. ```javascript -//. var Identity = require('my-package').Identity; -//. var type = require('sanctuary-type-identifiers'); -//. -//. type(null); // => 'Null' -//. type(true); // => 'Boolean' -//. type([1, 2, 3]); // => 'Array' -//. type(Identity); // => 'Function' -//. type(Identity(0)); // => 'my-package/Identity' -//. ``` -//. -//. -//. [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof -//. [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString -//. [3]: #algorithm (function(f) { @@ -119,7 +100,46 @@ // $$type :: String var $$type = '@@type'; - // type :: Any -> String + // RPARSE :: RegExp + var RPARSE = /^([^]+)\/([^]+?)(?:@(\d+))?$/; + + // TypeIdentifier :: (Nullable String, String, Number) + // -> TypeIdentifier + function TypeIdentifier(namespace, name, version) { + return {namespace: namespace, name: name, version: version}; + } + + TypeIdentifier[$$type] = 'sanctuary-type-identifiers/TypeIdentifier@1'; + + //. ### Usage + //. + //. `const {parse, type} = require('sanctuary-type-identifiers')` + //. + //. ```javascript + //. > parse(type([1, 2, 3])); + //. {namespace: null, name: 'Array', version: 0} + //. ``` + //. + //. ### API + + //# type :: Any -> String + //. + //. Takes any value and produces a String which represents its type. If the + //. value conforms to the [specification][3], the custom type representation + //. is returned. + //. + //. ```javascript + //. > type(null); + //. 'Null' + //. + //. > type(true); + //. 'Boolean' + //. + //. > function Identity(x) { this.value = x; } + //. > Identity['@@type'] = 'my-package/Identity@1'; + //. > type(new Identity(0)); + //. 'my-package/Identity@1' + //. ``` function type(x) { return x != null && x.constructor != null && @@ -129,6 +149,42 @@ Object.prototype.toString.call(x).slice('[object '.length, -']'.length); } - return type; + //# parse :: String -> TypeIdentifier + //. + //. Takes any String and parses it according to the [specification][3], + //. returning a TypeIdentifier: An object with a `namespace`, a `name` and + //. a `version` field. + //. + //. ```javascript + //. > parse('my-package/List@2'); + //. {namespace: 'my-package', name: 'List', version: 2} + //. + //. > parse('nonsense!'); + //. {namespace: null, name: 'nonsense!', version: 0} + //. + //. > function Identity(x) { this.value = x; } + //. > Identity['@@type'] = 'my-package/Identity@1'; + //. > parse(type(new Identity(0))); + //. {namespace: 'my-package', name: 'Identity', version: 1} + //. ``` + function parse(s) { + var parsed = RPARSE.exec(s); + if (parsed == null) return TypeIdentifier(null, s, 0); + return TypeIdentifier( + parsed[1] == null ? null : parsed[1], + parsed[2], + parsed[3] == null ? 0 : Number(parsed[3]) + ); + } + + return { + type: type, + parse: parse + }; })); + +//. [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof +//. [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString +//. [3]: #specification +//. [4]: https://docs.npmjs.com/misc/scope diff --git a/package.json b/package.json index 49c4e9a..f4be1e0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": {}, "devDependencies": { + "doctest": "^0.12.0", "eslint": "3.19.x", "mocha": "3.2.x", "remember-bower": "0.1.x", diff --git a/test/index.js b/test/index.js index 259f71c..cc59cbc 100644 --- a/test/index.js +++ b/test/index.js @@ -4,7 +4,9 @@ var assert = require('assert'); var Z = require('sanctuary-type-classes'); -var type = require('..'); +var id = require('..'); +var type = id.type; +var parse = id.parse; function eq(actual, expected) { @@ -33,6 +35,10 @@ function Just(x) { return {constructor: MaybeTypeRep, isNothing: false, isJust: true, value: x}; } +function TypeIdentifier(namespace, name, version) { + return {namespace: namespace, name: name, version: version}; +} + test('type', function() { eq(type(null), 'Null'); @@ -55,3 +61,18 @@ test('type', function() { eq(type(new Number(0)), 'Number'); eq(type(new String('')), 'String'); }); + +test('parse', function() { + eq(parse('package/Type'), TypeIdentifier('package', 'Type', 0)); + eq(parse('package/Type/X'), TypeIdentifier('package/Type', 'X', 0)); + eq(parse('@scope/package/Type'), TypeIdentifier('@scope/package', 'Type', 0)); + eq(parse(''), TypeIdentifier(null, '', 0)); + eq(parse('/Type'), TypeIdentifier(null, '/Type', 0)); + eq(parse('@0'), TypeIdentifier(null, '@0', 0)); + eq(parse('foo/\n@1'), TypeIdentifier('foo', '\n', 1)); + eq(parse('Type@1'), TypeIdentifier(null, 'Type@1', 0)); + eq(parse('package/Type@1'), TypeIdentifier('package', 'Type', 1)); + eq(parse('package/Type@999'), TypeIdentifier('package', 'Type', 999)); + eq(parse('package/Type@X'), TypeIdentifier('package', 'Type@X', 0)); + eq(parse('package////@3@2@1@1'), TypeIdentifier('package///', '@3@2@1', 1)); +});