@muze-nl/assert 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Auke van Slooten
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Asserts
2
+
3
+ [![Project stage: Experimental][project-stage-badge: Experimental]][project-stage-page]
4
+
5
+ This is a light-weight library to do optional assertion checking. By default any assertions made are not tested. Assertion code is not run. Unless you toggle assertion checking, usually in developer mode, by calling `enable`. Now your assertions are run, and if any assertions fail, an error is thrown with information about the specific failure.
6
+
7
+ ## Asserting preconditions
8
+
9
+ This library was created as part of the [@muze-nl/metro](https://github.com/muze-nl/metro/) package initially, but has escaped its confines.
10
+
11
+ When writing middleware there is usually quite a lot of preconditions to check. When a developer wants to use your middleware, it is nice to have explicit feedback about what he or she is doing wrong. However this is only useful during development. Once in production you should assume that there are no developer mistakes anymore... or at least that the end user has no use for detailed error reports about your middleware.
12
+
13
+ This is especially true about mock middleware. Mock middleware is middleware that blocks the actual transmission of a request, and returns a mock response instead. Your browser doesn't actually fetch the requests URL.
14
+
15
+ The [oauth2 middleware](https://github.com/muze-nl/metro-oauth2) for example, has unit tests that use the oauth2 mock-server middleware to mimick a server. This way you can be sure that the oauth2 client implementation works, without having to setup a real oauth2 server anywhere.
16
+
17
+ Since these mock middleware servers are especially meant for the initial development of new middleware, they should assert as much as they can. And send comprehensive error messages to the console. Here the [`assert.fails()`](./docs/fails.md) method comes in handy.
18
+
19
+ `assert.fails()` returns `false` if there are no problems. If one or more assertions do fail, it will return an array with messages about each failed assertion. So one way of using it is like this:
20
+
21
+ ```javascript
22
+ let error
23
+
24
+ if (error = assert.fails(url, {
25
+ searchParams: {
26
+ response_type: 'code',
27
+ client_id: 'mockClientId',
28
+ state: assert.Optional(/.+/)
29
+ }
30
+ })) {
31
+ return metro.response({
32
+ url: req.url,
33
+ status: 400,
34
+ statusText: 'Bad Request',
35
+ body: '400 Bad Request'
36
+ })
37
+ }
38
+ ```
39
+
40
+ The first parameter to `assert.fails` contains the data you want to check. The second (or third, fourth, etc.) contain the assertions. If the data is an object, the assertions can use the same property names to add assertions for those specific properties. Here the `url.searchParams.response_type` must be equal to `code`, or the assertion will fail. You can also use numbers and booleans like this.
41
+
42
+ You can also add functions to the assertions. In this case the `assert.optional()` method adds a function that will only fail if the property is set and not `null`, but does not match the assertions passed to `assert.optional()`.
43
+
44
+ An assertion may also be a regular expression. If the property value fails to match that expression, the assertion fails. Here the `url.searchParams.state` is tested to make sure that, if it is set, it must not be empty.
45
+
46
+ In a mock middleware function, it is all well and good to always test your preconditions. But in production many preconditions may be assumed to be valid. These preconditions are not expected to fail in production, only in development. In that case you may use [`assert.check()`]('./docs/check.md'). This function by default does nothing. Only when you enable assertions does this function do anything. This allows you to selectively turn on assertions only in a development context. And avoid doing unnecessary work while in production. This is how it is used in the [oauth2 middleware](./middleware/oauth2.md) (not the mock server, the actual client code):
47
+
48
+ ```javascript
49
+ assert.assert(oauth2, {
50
+ client_id: /.+/,
51
+ authRedirectURL: /.+/,
52
+ scope: /.*/
53
+ })
54
+ ```
55
+
56
+ This makes sure that the `client_id` and `authRedirectURL` configuration options have been set and are not empty. But when the code is used in production, this should never happen. There is no need to constantly test for this. And in production it won't actually get checked. Only when you enable assertions will this code actually perform the tests:
57
+
58
+ ```javascript
59
+ assert.enable()
60
+ ```
61
+
62
+ Once the [`assert.enable()`](./docs/enable.md) function is called, now `assert.check()` will throw an error if any assertion fails. The error is also logged to the console.
63
+
64
+
65
+ [project-stage-badge: Experimental]: https://img.shields.io/badge/Project%20Stage-Experimental-yellow.svg
66
+ [project-stage-page]: https://blog.pother.ca/project-stages/
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@muze-nl/assert",
3
+ "version": "0.1.0",
4
+ "description": "light optional assert library",
5
+ "type": "module",
6
+ "main": "src/assert.mjs",
7
+ "scripts": {
8
+ "test": "tap test/*.mjs",
9
+ "tap": "tap"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/muze-nl/assert.git"
14
+ },
15
+ "author": "auke@muze.nl",
16
+ "license": "MIT",
17
+ "bugs": {
18
+ "url": "https://github.com/muze-nl/assert/issues"
19
+ },
20
+ "homepage": "https://github.com/muze-nl/assert/#readme",
21
+ "devDependencies": {
22
+ "tap": "^16.0.1"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "files": [
28
+ "/src",
29
+ "/LICENSE",
30
+ "/README.md"
31
+ ]
32
+ }
package/src/assert.mjs ADDED
@@ -0,0 +1,197 @@
1
+ // global state switch to enable/disable assert
2
+ // uses globalThis so that even if npm installs multiple
3
+ // versions of this library, all use this same state
4
+ globalThis.assertEnabled = false
5
+
6
+ export function enable() {
7
+ globalThis.assertEnabled = true
8
+ }
9
+
10
+ export function disable() {
11
+ globalThis.assertEnabled = false
12
+ }
13
+
14
+ export function error(message, found, expected) {
15
+ return {
16
+ message,
17
+ found,
18
+ expected
19
+ }
20
+ }
21
+
22
+ /**
23
+ * returns new Boolean(true) if data does not match pattern
24
+ * you can't return new Boolean(false), or at least that evaluates
25
+ * to true, so if the data does match, it returns a primitive false
26
+ * the Boolean(true) has an extra property called 'problems', which is
27
+ * an array with a list of all fields that do not match, and why
28
+ * @param {any} data The data to match
29
+ * @param {any} pattern The pattern to match
30
+ * @return {Array|false} Array with problems if the pattern fails, false
31
+ */
32
+ export function fails(data, pattern, container) {
33
+ let problems = []
34
+ if (pattern === Boolean) {
35
+ if (typeof data != 'boolean') {
36
+ problems.push(error('data is not a boolean', data, pattern))
37
+ }
38
+ } else if (pattern === Number) {
39
+ if (typeof data != 'number') {
40
+ problems.push(error('data is not a number', data, pattern))
41
+ }
42
+ } else if (pattern instanceof RegExp) {
43
+ if (Array.isArray(data)) {
44
+ let index = data.findIndex(element => fails(element,pattern))
45
+ if (index>-1) {
46
+ problems.push(error('data['+index+'] does not match pattern', data[index], pattern))
47
+ }
48
+ } else if (!pattern.test(data)) {
49
+ problems.push(error('data does not match pattern', data, pattern))
50
+ }
51
+ } else if (pattern instanceof Function) {
52
+ if (pattern(data, container)) {
53
+ problems.push(error('data does not match function', data, pattern))
54
+ }
55
+ } else if (Array.isArray(pattern)) {
56
+ if (!Array.isArray(data)) {
57
+ problems.push(error('data is not an array',data,[]))
58
+ }
59
+ for (p of pattern) {
60
+ let problem = fails(data, p)
61
+ if (Array.isArray(problem)) {
62
+ problems.concat(problem)
63
+ } else if (problem) {
64
+ problems.push(problem)
65
+ }
66
+ }
67
+ } else if (pattern && typeof pattern == 'object') {
68
+ if (Array.isArray(data)) {
69
+ let index = data.findIndex(element => fails(element,pattern))
70
+ if (index>-1) {
71
+ problems.push(error('data['+index+'] does not match pattern', data[index], pattern))
72
+ }
73
+ } else if (!data || typeof data != 'object') {
74
+ problems.push(error('data is not an object, pattern is', data, pattern))
75
+ } else {
76
+ if (data instanceof URLSearchParams) {
77
+ data = Object.fromEntries(data)
78
+ }
79
+ let p = problems[problems.length-1]
80
+ for (const [wKey, wVal] of Object.entries(pattern)) {
81
+ let result = fails(data[wKey], wVal, data)
82
+ if (result) {
83
+ if (!p || typeof p == 'string') {
84
+ p = {}
85
+ problems.push(error(p, data[wKey], wVal))
86
+ }
87
+ p[wKey] = result.problems
88
+ }
89
+ }
90
+ }
91
+ } else {
92
+ if (pattern!=data) {
93
+ problems.push(error('data and pattern are not equal', data, pattern))
94
+ }
95
+ }
96
+ if (problems.length) {
97
+ return problems
98
+ }
99
+ return false
100
+ }
101
+
102
+ export function assert(source, test) {
103
+ if (!globalThis.assertEnabled) {
104
+ return
105
+ }
106
+ let result = fails(source,test)
107
+ if (result) {
108
+ throw new assertError(result,source)
109
+ }
110
+ }
111
+
112
+ export function Optional(pattern) {
113
+ return function(data) {
114
+ if (data==null || typeof data == 'undefined') {
115
+ return false
116
+ }
117
+ return fails(data, pattern)
118
+ }
119
+ }
120
+
121
+ export function Required(pattern) {
122
+ return function(data) {
123
+ return fails(data, pattern)
124
+ }
125
+ }
126
+
127
+ export function Recommended(pattern) {
128
+ if (data==null || typeof data == 'undefined') {
129
+ console.warning('data does not contain recommended value', data, pattern)
130
+ return false
131
+ }
132
+ return fails(data, pattern)
133
+ }
134
+
135
+ export function oneOf(...patterns) {
136
+ return function(data) {
137
+ for(let pattern of patterns) {
138
+ if (!fails(data, pattern)) {
139
+ return false
140
+ }
141
+ }
142
+ return error('data does not match oneOf patterns',data,patterns)
143
+ }
144
+ }
145
+
146
+ export function anyOf(...patterns) {
147
+ return function(data) {
148
+ if (!Array.isArray(data)) {
149
+ return error('data is not an array',data,'anyOf')
150
+ }
151
+ for (let value of data) {
152
+ if (oneOf(...patterns)(value)) {
153
+ return error('data does not match anyOf patterns',value,patterns)
154
+ }
155
+ }
156
+ return false
157
+ }
158
+ }
159
+
160
+ export function validURL(data) {
161
+ try {
162
+ let url = new URL(data)
163
+ if (url.href!=data) {
164
+ return error('data is not a fully qualified url',data,'validURL')
165
+ }
166
+ } catch(e) {
167
+ return error('data is not a valid url',data,'validURL')
168
+ }
169
+ return false
170
+ }
171
+
172
+ export function instanceOf(constructor) {
173
+ return function(data) {
174
+ if (!(data instanceof constructor)) {
175
+ return error('data is not an instanceof pattern',data,constructor)
176
+ }
177
+ return false
178
+ }
179
+ }
180
+
181
+ export function not(pattern) {
182
+ return function(data) {
183
+ let problem = fails(data, pattern)
184
+ if (problem) {
185
+ return false
186
+ }
187
+ return error('data matches pattern, when required not to', data, pattern)
188
+ }
189
+ }
190
+
191
+ class assertError {
192
+ constructor(problems, ...details) {
193
+ this.problems = problems
194
+ this.details = details
195
+ console.trace()
196
+ }
197
+ }