├── .npmrc ├── .gitignore ├── .github ├── FUNDING.yml └── CONTRIBUTING.md ├── .config ├── .eslintrc.json ├── .circleci └── config.yml ├── package.json ├── LICENSE ├── test └── index.js ├── README.md └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [davidchambers, Avaq] 2 | -------------------------------------------------------------------------------- /.config: -------------------------------------------------------------------------------- 1 | repo-owner = sanctuary-js 2 | repo-name = sanctuary-type-identifiers 3 | contributing-file = .github/CONTRIBUTING.md 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["./node_modules/sanctuary-style/eslint.json"], 4 | "parserOptions": {"ecmaVersion": 2020, "sourceType": "module"}, 5 | "overrides": [ 6 | { 7 | "files": ["test/**/*.js"], 8 | "rules": { 9 | "max-len": ["off"] 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@6.3.0 5 | 6 | workflows: 7 | test: 8 | jobs: 9 | - node/test: 10 | setup: 11 | # derive cache key from package.json 12 | - run: cp package.json package-lock.json 13 | override-ci-command: rm package-lock.json && npm install && git checkout -- package.json 14 | matrix: 15 | parameters: 16 | version: 17 | - 18.0.0 18 | - 20.0.0 19 | - 22.0.0 20 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Note: __README.md__ is generated from comments in __index.js__. Do not modify 4 | __README.md__ directly. 5 | 6 | 1. Update local main branch: 7 | 8 | $ git checkout main 9 | $ git pull upstream main 10 | 11 | 2. Create feature branch: 12 | 13 | $ git checkout -b feature-x 14 | 15 | 3. Make one or more atomic commits, and ensure that each commit has a 16 | descriptive commit message. Commit messages should be line wrapped 17 | at 72 characters. 18 | 19 | 4. Run `npm test`, and address any errors. Preferably, fix commits in place 20 | using `git rebase` or `git commit --amend` to make the changes easier to 21 | review. 22 | 23 | 5. Push: 24 | 25 | $ git push origin feature-x 26 | 27 | 6. Open a pull request. 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanctuary-type-identifiers", 3 | "version": "4.0.0", 4 | "description": "Specification for type identifiers", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/sanctuary-js/sanctuary-type-identifiers.git" 9 | }, 10 | "type": "module", 11 | "exports": { 12 | ".": "./index.js", 13 | "./package.json": "./package.json" 14 | }, 15 | "scripts": { 16 | "doctest": "sanctuary-doctest", 17 | "lint": "sanctuary-lint", 18 | "release": "sanctuary-release", 19 | "test": "npm run lint && sanctuary-test && npm run doctest" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": { 23 | "sanctuary-scripts": "7.0.x" 24 | }, 25 | "files": [ 26 | "/LICENSE", 27 | "/README.md", 28 | "/index.js", 29 | "/package.json" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Sanctuary 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or 10 | sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {deepStrictEqual as eq} from 'node:assert'; 2 | 3 | import test from 'oletus'; 4 | 5 | import {identifierOf, parseIdentifier} from '../index.js'; 6 | 7 | 8 | // Identity :: a -> Identity a 9 | function Identity(x) { 10 | if (!(this instanceof Identity)) return new Identity (x); 11 | this.value = x; 12 | } 13 | 14 | Identity.prototype['@@type'] = 'my-package/Identity'; 15 | 16 | 17 | const maybeTypeIdent = 'my-package/Maybe'; 18 | 19 | // Nothing :: Maybe a 20 | const Nothing = { 21 | '@@type': maybeTypeIdent, 22 | 'isNothing': true, 23 | 'isJust': false, 24 | }; 25 | 26 | // Just :: a -> Maybe a 27 | const Just = x => ({ 28 | '@@type': maybeTypeIdent, 29 | 'isNothing': false, 30 | 'isJust': true, 31 | 'value': x, 32 | }); 33 | 34 | const TypeIdentifier = (namespace, name, version) => ({ 35 | namespace, 36 | name, 37 | version, 38 | }); 39 | 40 | 41 | test ('identifierOf', () => { 42 | eq (identifierOf (null), 'Null'); 43 | eq (identifierOf (undefined), 'Undefined'); 44 | eq (identifierOf ({constructor: null}), 'Object'); 45 | eq (identifierOf ({constructor: {'@@type': null}}), 'Object'); 46 | eq (identifierOf ({constructor: {'@@type': new String ('')}}), 'Object'); 47 | eq (identifierOf (Identity (42)), 'my-package/Identity'); 48 | eq (identifierOf (Identity), 'Function'); 49 | eq (identifierOf (Identity.prototype), 'Object'); 50 | eq (identifierOf (Nothing), 'my-package/Maybe'); 51 | eq (identifierOf (Just (0)), 'my-package/Maybe'); 52 | eq (identifierOf (Nothing.constructor), 'Function'); 53 | 54 | eq (identifierOf (false), 'Boolean'); 55 | eq (identifierOf (0), 'Number'); 56 | eq (identifierOf (''), 'String'); 57 | 58 | eq (identifierOf (new Boolean (false)), 'Boolean'); 59 | eq (identifierOf (new Number (0)), 'Number'); 60 | eq (identifierOf (new String ('')), 'String'); 61 | }); 62 | 63 | test ('parseIdentifier', () => { 64 | eq (parseIdentifier ('package/Type'), TypeIdentifier ('package', 'Type', 0)); 65 | eq (parseIdentifier ('package/Type/X'), TypeIdentifier ('package/Type', 'X', 0)); 66 | eq (parseIdentifier ('@scope/package/Type'), TypeIdentifier ('@scope/package', 'Type', 0)); 67 | eq (parseIdentifier (''), TypeIdentifier (null, '', 0)); 68 | eq (parseIdentifier ('/Type'), TypeIdentifier (null, '/Type', 0)); 69 | eq (parseIdentifier ('@0'), TypeIdentifier (null, '@0', 0)); 70 | eq (parseIdentifier ('foo/\n@1'), TypeIdentifier ('foo', '\n', 1)); 71 | eq (parseIdentifier ('Type@1'), TypeIdentifier (null, 'Type@1', 0)); 72 | eq (parseIdentifier ('package/Type@1'), TypeIdentifier ('package', 'Type', 1)); 73 | eq (parseIdentifier ('package/Type@999'), TypeIdentifier ('package', 'Type', 999)); 74 | eq (parseIdentifier ('package/Type@X'), TypeIdentifier ('package', 'Type@X', 0)); 75 | eq (parseIdentifier ('package////@3@2@1@1'), TypeIdentifier ('package///', '@3@2@1', 1)); 76 | }); 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sanctuary-type-identifiers 2 | 3 | A type is a set of values. Boolean, for example, is the type comprising 4 | `true` and `false`. A value may be a member of multiple types (`42` is a 5 | member of Number, PositiveNumber, Integer, and many other types). 6 | 7 | In certain situations it is useful to divide JavaScript values into 8 | non-overlapping types. The language provides two constructs for this 9 | purpose: the [`typeof`][1] operator and [`Object.prototype.toString`][2]. 10 | Each has pros and cons, but neither supports user-defined types. 11 | 12 | sanctuary-type-identifiers comprises: 13 | 14 | - an npm and browser -compatible package for deriving the 15 | _type identifier_ of a JavaScript value; and 16 | - a specification which authors may follow to specify type 17 | identifiers for their types. 18 | 19 | ### Specification 20 | 21 | For a type to be compatible with the algorithm: 22 | 23 | - every member of the type MUST have a `@@type` property 24 | (the _type identifier_); and 25 | 26 | - the type identifier MUST be a string primitive and SHOULD have 27 | format `'/[@]'`, where: 28 | 29 | - `` MUST consist of one or more characters, and 30 | SHOULD equal the name of the npm package which defines the 31 | type (including [scope][3] where appropriate); 32 | 33 | - `` MUST consist of one or more characters, and SHOULD 34 | be the unique name of the type; and 35 | 36 | - `` MUST consist of one or more digits, and SHOULD 37 | represent the version of the type. 38 | 39 | If the type identifier does not conform to the format specified above, 40 | it is assumed that the entire string represents the _name_ of the type; 41 | _namespace_ will be `null` and _version_ will be `0`. 42 | 43 | If the _version_ is not given, it is assumed to be `0`. 44 | 45 | ### Usage 46 | 47 | ```javascript 48 | const type = require ('sanctuary-type-identifiers'); 49 | ``` 50 | 51 | ```javascript 52 | > const Identity$prototype = { 53 | . '@@type': 'my-package/Identity@1', 54 | . '@@show': function() { 55 | . return 'Identity (' + show (this.value) + ')'; 56 | . }, 57 | . } 58 | 59 | > const Identity = value => ( 60 | . Object.assign (Object.create (Identity$prototype), {value}) 61 | . ) 62 | 63 | > type (Identity (0)) 64 | 'my-package/Identity@1' 65 | 66 | > type.parse (type (Identity (0))) 67 | {namespace: 'my-package', name: 'Identity', version: 1} 68 | ``` 69 | 70 | ### API 71 | 72 | #### `type :: Any -⁠> String` 73 | 74 | Takes any value and returns a string which identifies its type. If the 75 | value conforms to the [specification][4], the custom type identifier is 76 | returned. 77 | 78 | ```javascript 79 | > type (null) 80 | 'Null' 81 | 82 | > type (true) 83 | 'Boolean' 84 | 85 | > type (Identity (0)) 86 | 'my-package/Identity@1' 87 | ``` 88 | 89 | #### `type.parse :: String -⁠> { namespace :: Nullable String, name :: String, version :: Number }` 90 | 91 | Takes any string and parses it according to the [specification][4], 92 | returning an object with `namespace`, `name`, and `version` fields. 93 | 94 | ```javascript 95 | > type.parse ('my-package/List@2') 96 | {namespace: 'my-package', name: 'List', version: 2} 97 | 98 | > type.parse ('nonsense!') 99 | {namespace: null, name: 'nonsense!', version: 0} 100 | 101 | > type.parse (type (Identity (0))) 102 | {namespace: 'my-package', name: 'Identity', version: 1} 103 | ``` 104 | 105 | [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof 106 | [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString 107 | [3]: https://docs.npmjs.com/misc/scope 108 | [4]: #specification 109 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | @@@@@@@ @@@@@@@ @@ 3 | @@ @@ @@ @@ @@@ 4 | @@ @@@ @@ @@ @@ @@@ @@ @@ @@@@@@ @@ @@@ @@ @@@ @@@@ 5 | @@ @@ @@@ @@ @@ @@ @@@ @@ @@@ @@ @@@ @@@ @@ @@@ @@ 6 | @@ @@ @@@ @@ @@ @@ @@@ @@ @@@ @@ @@@ @@@ @@ @@@@@@@@ 7 | @@ @@ @@@ @@ @@ @@ @@@ @@ @@@ @@ @@@ @@@ @@ @@@ 8 | @@ @@@ @@@@@ @@ @@@ @@@@@ @@@ @@@ @@ @@@@@@ @@@@@ 9 | @@ @@ @@ @@ 10 | @@@@@@@ @@@@@@@ @@@@@ @@ 11 | */ 12 | //. # sanctuary-type-identifiers 13 | //. 14 | //. A type is a set of values. Boolean, for example, is the type comprising 15 | //. `true` and `false`. A value may be a member of multiple types (`42` is a 16 | //. member of Number, PositiveNumber, Integer, and many other types). 17 | //. 18 | //. In certain situations it is useful to divide JavaScript values into 19 | //. non-overlapping types. The language provides two constructs for this 20 | //. purpose: the [`typeof`][1] operator and [`Object.prototype.toString`][2]. 21 | //. Each has pros and cons, but neither supports user-defined types. 22 | //. 23 | //. sanctuary-type-identifiers comprises: 24 | //. 25 | //. - an npm and browser -compatible package for deriving the 26 | //. _type identifier_ of a JavaScript value; and 27 | //. - a specification which authors may follow to specify type 28 | //. identifiers for their types. 29 | //. 30 | //. ### Specification 31 | //. 32 | //. For a type to be compatible with the algorithm: 33 | //. 34 | //. - every member of the type MUST have a `@@type` property 35 | //. (the _type identifier_); and 36 | //. 37 | //. - the type identifier MUST be a string primitive and SHOULD have 38 | //. format `'/[@]'`, where: 39 | //. 40 | //. - `` MUST consist of one or more characters, and 41 | //. SHOULD equal the name of the npm package which defines the 42 | //. type (including [scope][3] where appropriate); 43 | //. 44 | //. - `` MUST consist of one or more characters, and SHOULD 45 | //. be the unique name of the type; and 46 | //. 47 | //. - `` MUST consist of one or more digits, and SHOULD 48 | //. represent the version of the type. 49 | //. 50 | //. If the type identifier does not conform to the format specified above, 51 | //. it is assumed that the entire string represents the _name_ of the type; 52 | //. _namespace_ will be `null` and _version_ will be `0`. 53 | //. 54 | //. If the _version_ is not given, it is assumed to be `0`. 55 | 56 | export {identifierOf, parseIdentifier}; 57 | 58 | // $$type :: String 59 | const $$type = '@@type'; 60 | 61 | // pattern :: RegExp 62 | const pattern = /^(?.+)[/](?.+?)(@(?[0-9]+))?$/s; 63 | 64 | //. ### Usage 65 | //. 66 | //. ```javascript 67 | //. import {identifierOf, parseIdentifier} from 'sanctuary-type-identifiers'; 68 | //. ``` 69 | //. 70 | //. ```javascript 71 | //. > const Identity$prototype = { 72 | //. . '@@type': 'my-package/Identity@1', 73 | //. . '@@show': function() { 74 | //. . return 'Identity (' + show (this.value) + ')'; 75 | //. . }, 76 | //. . } 77 | //. 78 | //. > const Identity = value => ( 79 | //. . Object.assign (Object.create (Identity$prototype), {value}) 80 | //. . ) 81 | //. 82 | //. > identifierOf (Identity (0)) 83 | //. 'my-package/Identity@1' 84 | //. 85 | //. > parseIdentifier (identifierOf (Identity (0))) 86 | //. {namespace: 'my-package', name: 'Identity', version: 1} 87 | //. ``` 88 | //. 89 | //. ### API 90 | //. 91 | //# identifierOf :: Any -> String 92 | //. 93 | //. Takes any value and returns a string which identifies its type. If the 94 | //. value conforms to the [specification][4], the custom type identifier is 95 | //. returned. 96 | //. 97 | //. ```javascript 98 | //. > identifierOf (null) 99 | //. 'Null' 100 | //. 101 | //. > identifierOf (true) 102 | //. 'Boolean' 103 | //. 104 | //. > identifierOf (Identity (0)) 105 | //. 'my-package/Identity@1' 106 | //. ``` 107 | const identifierOf = x => ( 108 | x != null && 109 | x.constructor != null && 110 | x.constructor.prototype !== x && 111 | typeof x[$$type] === 'string' 112 | ? x[$$type] 113 | : (Object.prototype.toString.call (x)).slice ('[object '.length, -']'.length) 114 | ); 115 | 116 | //# parseIdentifier :: String -> { namespace :: Nullable String, name :: String, version :: Number } 117 | //. 118 | //. Takes any string and parses it according to the [specification][4], 119 | //. returning an object with `namespace`, `name`, and `version` fields. 120 | //. 121 | //. ```javascript 122 | //. > parseIdentifier ('my-package/List@2') 123 | //. {namespace: 'my-package', name: 'List', version: 2} 124 | //. 125 | //. > parseIdentifier ('nonsense!') 126 | //. {namespace: null, name: 'nonsense!', version: 0} 127 | //. 128 | //. > parseIdentifier (identifierOf (Identity (0))) 129 | //. {namespace: 'my-package', name: 'Identity', version: 1} 130 | //. ``` 131 | const parseIdentifier = s => { 132 | const match = pattern.exec (s); 133 | if (match == null) { 134 | return {namespace: null, name: s, version: 0}; 135 | } else { 136 | const {namespace, name, version = '0'} = match.groups; 137 | return {namespace, name, version: Number (version)}; 138 | } 139 | }; 140 | 141 | //. [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof 142 | //. [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString 143 | //. [3]: https://docs.npmjs.com/misc/scope 144 | //. [4]: #specification 145 | --------------------------------------------------------------------------------