@toa.io/extensions.exposition 0.8.3-dev.9 → 0.20.0-alpha.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/components/context.toa.yaml +15 -0
- package/components/identity.bans/manifest.toa.yaml +18 -0
- package/components/identity.basic/events/principal.js +9 -0
- package/components/identity.basic/manifest.toa.yaml +50 -0
- package/components/identity.basic/source/authenticate.ts +29 -0
- package/components/identity.basic/source/create.ts +19 -0
- package/components/identity.basic/source/transit.ts +64 -0
- package/components/identity.basic/source/types.ts +42 -0
- package/components/identity.basic/tsconfig.json +9 -0
- package/components/identity.roles/manifest.toa.yaml +31 -0
- package/components/identity.roles/source/list.ts +7 -0
- package/components/identity.roles/source/principal.ts +20 -0
- package/components/identity.roles/tsconfig.json +9 -0
- package/components/identity.tokens/manifest.toa.yaml +39 -0
- package/components/identity.tokens/receivers/identity.bans.updated.js +3 -0
- package/components/identity.tokens/source/authenticate.test.ts +56 -0
- package/components/identity.tokens/source/authenticate.ts +38 -0
- package/components/identity.tokens/source/decrypt.test.ts +59 -0
- package/components/identity.tokens/source/decrypt.ts +25 -0
- package/components/identity.tokens/source/encrypt.test.ts +35 -0
- package/components/identity.tokens/source/encrypt.ts +25 -0
- package/components/identity.tokens/source/revoke.ts +5 -0
- package/components/identity.tokens/source/types.ts +48 -0
- package/components/identity.tokens/tsconfig.json +9 -0
- package/cucumber.js +9 -0
- package/documentation/.assets/ia3-dark.jpg +0 -0
- package/documentation/.assets/ia3-light.jpg +0 -0
- package/documentation/.assets/overview-dark.jpg +0 -0
- package/documentation/.assets/overview-light.jpg +0 -0
- package/documentation/.assets/role-scopes-dark.jpg +0 -0
- package/documentation/.assets/role-scopes-light.jpg +0 -0
- package/documentation/.assets/rtd-dark.jpg +0 -0
- package/documentation/.assets/rtd-light.jpg +0 -0
- package/documentation/access.md +256 -0
- package/documentation/components.md +276 -0
- package/documentation/identity.md +156 -0
- package/documentation/protocol.md +15 -0
- package/documentation/query.md +226 -0
- package/documentation/tree.md +169 -0
- package/features/access.feature +442 -0
- package/features/annotation.feature +28 -0
- package/features/body.feature +21 -0
- package/features/directives.feature +56 -0
- package/features/dynamic.feature +89 -0
- package/features/errors.feature +208 -0
- package/features/identity.basic.feature +235 -0
- package/features/identity.feature +61 -0
- package/features/identity.roles.feature +51 -0
- package/features/identity.tokens.feature +116 -0
- package/features/queries.feature +214 -0
- package/features/routes.feature +49 -0
- package/features/steps/Common.ts +10 -0
- package/features/steps/Components.ts +43 -0
- package/features/steps/Database.ts +58 -0
- package/features/steps/Gateway.ts +105 -0
- package/features/steps/HTTP.ts +71 -0
- package/features/steps/Parameters.ts +12 -0
- package/features/steps/Workspace.ts +40 -0
- package/features/steps/components/greeter/manifest.toa.yaml +9 -0
- package/features/steps/components/greeter/operations/greet.js +7 -0
- package/features/steps/components/pots/manifest.toa.yaml +20 -0
- package/features/steps/components/users/manifest.toa.yaml +11 -0
- package/features/steps/tsconfig.json +9 -0
- package/package.json +31 -17
- package/readme.md +181 -0
- package/schemas/annotation.cos.yaml +4 -0
- package/schemas/directive.cos.yaml +3 -0
- package/schemas/method.cos.yaml +9 -0
- package/schemas/node.cos.yaml +5 -0
- package/schemas/query.cos.yaml +16 -0
- package/schemas/querystring.cos.yaml +5 -0
- package/schemas/range.cos.yaml +2 -0
- package/schemas/route.cos.yaml +2 -0
- package/source/Annotation.ts +6 -0
- package/source/Branch.ts +8 -0
- package/source/Composition.ts +58 -0
- package/source/Context.ts +6 -0
- package/source/Directive.test.ts +91 -0
- package/source/Directive.ts +120 -0
- package/source/Endpoint.ts +57 -0
- package/source/Factory.ts +53 -0
- package/source/Gateway.ts +93 -0
- package/source/HTTP/Server.fixtures.ts +45 -0
- package/source/HTTP/Server.test.ts +221 -0
- package/source/HTTP/Server.ts +135 -0
- package/source/HTTP/exceptions.ts +77 -0
- package/source/HTTP/formats/index.ts +19 -0
- package/source/HTTP/formats/json.ts +13 -0
- package/source/HTTP/formats/msgpack.ts +10 -0
- package/source/HTTP/formats/text.ts +9 -0
- package/source/HTTP/formats/yaml.ts +14 -0
- package/source/HTTP/index.ts +3 -0
- package/source/HTTP/messages.test.ts +116 -0
- package/source/HTTP/messages.ts +77 -0
- package/source/Mapping.ts +48 -0
- package/source/Query.test.ts +37 -0
- package/source/Query.ts +105 -0
- package/source/RTD/Context.ts +16 -0
- package/source/RTD/Directives.ts +9 -0
- package/source/RTD/Endpoint.ts +11 -0
- package/source/RTD/Match.ts +16 -0
- package/source/RTD/Method.ts +24 -0
- package/source/RTD/Node.ts +85 -0
- package/source/RTD/Route.ts +58 -0
- package/source/RTD/Tree.ts +57 -0
- package/source/RTD/factory.ts +47 -0
- package/source/RTD/index.ts +8 -0
- package/source/RTD/segment.test.ts +32 -0
- package/source/RTD/segment.ts +25 -0
- package/source/RTD/syntax/index.ts +2 -0
- package/source/RTD/syntax/parse.test.ts +188 -0
- package/source/RTD/syntax/parse.ts +153 -0
- package/source/RTD/syntax/types.ts +48 -0
- package/source/Remotes.test.ts +41 -0
- package/source/Remotes.ts +20 -0
- package/source/Tenant.ts +38 -0
- package/source/deployment.ts +42 -0
- package/source/directives/auth/Anonymous.ts +14 -0
- package/source/directives/auth/Echo.ts +12 -0
- package/source/directives/auth/Family.ts +145 -0
- package/source/directives/auth/Id.ts +19 -0
- package/source/directives/auth/Incept.ts +42 -0
- package/source/directives/auth/Role.test.ts +62 -0
- package/source/directives/auth/Role.ts +56 -0
- package/source/directives/auth/Rule.ts +28 -0
- package/source/directives/auth/Scheme.ts +26 -0
- package/source/directives/auth/index.ts +3 -0
- package/source/directives/auth/schemes.ts +8 -0
- package/source/directives/auth/split.ts +15 -0
- package/source/directives/auth/types.ts +37 -0
- package/source/directives/dev/Family.ts +34 -0
- package/source/directives/dev/Stub.ts +14 -0
- package/source/directives/dev/index.ts +3 -0
- package/source/directives/dev/types.ts +5 -0
- package/source/directives/index.ts +5 -0
- package/source/discovery.ts +1 -0
- package/source/exceptions.ts +17 -0
- package/source/index.test.ts +9 -0
- package/source/index.ts +6 -0
- package/source/manifest.test.ts +57 -0
- package/source/manifest.ts +35 -0
- package/source/root.ts +38 -0
- package/source/schemas.ts +9 -0
- package/transpiled/Annotation.d.ts +6 -0
- package/transpiled/Annotation.js +3 -0
- package/transpiled/Annotation.js.map +1 -0
- package/transpiled/Branch.d.ts +7 -0
- package/transpiled/Branch.js +3 -0
- package/transpiled/Branch.js.map +1 -0
- package/transpiled/Composition.d.ts +14 -0
- package/transpiled/Composition.js +43 -0
- package/transpiled/Composition.js.map +1 -0
- package/transpiled/Context.d.ts +5 -0
- package/transpiled/Context.js +3 -0
- package/transpiled/Context.js.map +1 -0
- package/transpiled/Directive.d.ts +32 -0
- package/transpiled/Directive.js +76 -0
- package/transpiled/Directive.js.map +1 -0
- package/transpiled/Endpoint.d.ts +20 -0
- package/transpiled/Endpoint.js +44 -0
- package/transpiled/Endpoint.js.map +1 -0
- package/transpiled/Factory.d.ts +11 -0
- package/transpiled/Factory.js +67 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/Gateway.d.ts +19 -0
- package/transpiled/Gateway.js +90 -0
- package/transpiled/Gateway.js.map +1 -0
- package/transpiled/HTTP/Server.d.ts +22 -0
- package/transpiled/HTTP/Server.fixtures.d.ts +12 -0
- package/transpiled/HTTP/Server.fixtures.js +36 -0
- package/transpiled/HTTP/Server.fixtures.js.map +1 -0
- package/transpiled/HTTP/Server.js +111 -0
- package/transpiled/HTTP/Server.js.map +1 -0
- package/transpiled/HTTP/exceptions.d.ts +39 -0
- package/transpiled/HTTP/exceptions.js +78 -0
- package/transpiled/HTTP/exceptions.js.map +1 -0
- package/transpiled/HTTP/formats/index.d.ts +8 -0
- package/transpiled/HTTP/formats/index.js +38 -0
- package/transpiled/HTTP/formats/index.js.map +1 -0
- package/transpiled/HTTP/formats/json.d.ts +4 -0
- package/transpiled/HTTP/formats/json.js +15 -0
- package/transpiled/HTTP/formats/json.js.map +1 -0
- package/transpiled/HTTP/formats/msgpack.d.ts +4 -0
- package/transpiled/HTTP/formats/msgpack.js +36 -0
- package/transpiled/HTTP/formats/msgpack.js.map +1 -0
- package/transpiled/HTTP/formats/text.d.ts +4 -0
- package/transpiled/HTTP/formats/text.js +13 -0
- package/transpiled/HTTP/formats/text.js.map +1 -0
- package/transpiled/HTTP/formats/yaml.d.ts +4 -0
- package/transpiled/HTTP/formats/yaml.js +39 -0
- package/transpiled/HTTP/formats/yaml.js.map +1 -0
- package/transpiled/HTTP/index.d.ts +3 -0
- package/transpiled/HTTP/index.js +20 -0
- package/transpiled/HTTP/index.js.map +1 -0
- package/transpiled/HTTP/messages.d.ts +27 -0
- package/transpiled/HTTP/messages.js +49 -0
- package/transpiled/HTTP/messages.js.map +1 -0
- package/transpiled/Mapping.d.ts +8 -0
- package/transpiled/Mapping.js +35 -0
- package/transpiled/Mapping.js.map +1 -0
- package/transpiled/Query.d.ts +13 -0
- package/transpiled/Query.js +107 -0
- package/transpiled/Query.js.map +1 -0
- package/transpiled/RTD/Context.d.ts +11 -0
- package/transpiled/RTD/Context.js +3 -0
- package/transpiled/RTD/Context.js.map +1 -0
- package/transpiled/RTD/Directives.d.ts +7 -0
- package/transpiled/RTD/Directives.js +3 -0
- package/transpiled/RTD/Directives.js.map +1 -0
- package/transpiled/RTD/Endpoint.d.ts +9 -0
- package/transpiled/RTD/Endpoint.js +3 -0
- package/transpiled/RTD/Endpoint.js.map +1 -0
- package/transpiled/RTD/Match.d.ts +11 -0
- package/transpiled/RTD/Match.js +3 -0
- package/transpiled/RTD/Match.js.map +1 -0
- package/transpiled/RTD/Method.d.ts +9 -0
- package/transpiled/RTD/Method.js +16 -0
- package/transpiled/RTD/Method.js.map +1 -0
- package/transpiled/RTD/Node.d.ts +21 -0
- package/transpiled/RTD/Node.js +61 -0
- package/transpiled/RTD/Node.js.map +1 -0
- package/transpiled/RTD/Route.d.ts +14 -0
- package/transpiled/RTD/Route.js +48 -0
- package/transpiled/RTD/Route.js.map +1 -0
- package/transpiled/RTD/Tree.d.ts +14 -0
- package/transpiled/RTD/Tree.js +45 -0
- package/transpiled/RTD/Tree.js.map +1 -0
- package/transpiled/RTD/factory.d.ts +6 -0
- package/transpiled/RTD/factory.js +36 -0
- package/transpiled/RTD/factory.js.map +1 -0
- package/transpiled/RTD/index.d.ts +8 -0
- package/transpiled/RTD/index.js +38 -0
- package/transpiled/RTD/index.js.map +1 -0
- package/transpiled/RTD/segment.d.ts +8 -0
- package/transpiled/RTD/segment.js +23 -0
- package/transpiled/RTD/segment.js.map +1 -0
- package/transpiled/RTD/syntax/index.d.ts +2 -0
- package/transpiled/RTD/syntax/index.js +19 -0
- package/transpiled/RTD/syntax/index.js.map +1 -0
- package/transpiled/RTD/syntax/parse.d.ts +4 -0
- package/transpiled/RTD/syntax/parse.js +128 -0
- package/transpiled/RTD/syntax/parse.js.map +1 -0
- package/transpiled/RTD/syntax/types.d.ts +41 -0
- package/transpiled/RTD/syntax/types.js +5 -0
- package/transpiled/RTD/syntax/types.js.map +1 -0
- package/transpiled/Remotes.d.ts +7 -0
- package/transpiled/Remotes.js +19 -0
- package/transpiled/Remotes.js.map +1 -0
- package/transpiled/Tenant.d.ts +12 -0
- package/transpiled/Tenant.js +30 -0
- package/transpiled/Tenant.js.map +1 -0
- package/transpiled/deployment.d.ts +3 -0
- package/transpiled/deployment.js +61 -0
- package/transpiled/deployment.js.map +1 -0
- package/transpiled/directives/auth/Anonymous.d.ts +6 -0
- package/transpiled/directives/auth/Anonymous.js +17 -0
- package/transpiled/directives/auth/Anonymous.js.map +1 -0
- package/transpiled/directives/auth/Echo.d.ts +6 -0
- package/transpiled/directives/auth/Echo.js +13 -0
- package/transpiled/directives/auth/Echo.js.map +1 -0
- package/transpiled/directives/auth/Family.d.ts +20 -0
- package/transpiled/directives/auth/Family.js +125 -0
- package/transpiled/directives/auth/Family.js.map +1 -0
- package/transpiled/directives/auth/Id.d.ts +7 -0
- package/transpiled/directives/auth/Id.js +17 -0
- package/transpiled/directives/auth/Id.js.map +1 -0
- package/transpiled/directives/auth/Incept.d.ts +10 -0
- package/transpiled/directives/auth/Incept.js +59 -0
- package/transpiled/directives/auth/Incept.js.map +1 -0
- package/transpiled/directives/auth/Role.d.ts +11 -0
- package/transpiled/directives/auth/Role.js +44 -0
- package/transpiled/directives/auth/Role.js.map +1 -0
- package/transpiled/directives/auth/Rule.d.ts +9 -0
- package/transpiled/directives/auth/Rule.js +22 -0
- package/transpiled/directives/auth/Rule.js.map +1 -0
- package/transpiled/directives/auth/Scheme.d.ts +7 -0
- package/transpiled/directives/auth/Scheme.js +47 -0
- package/transpiled/directives/auth/Scheme.js.map +1 -0
- package/transpiled/directives/auth/index.d.ts +2 -0
- package/transpiled/directives/auth/index.js +7 -0
- package/transpiled/directives/auth/index.js.map +1 -0
- package/transpiled/directives/auth/schemes.d.ts +3 -0
- package/transpiled/directives/auth/schemes.js +9 -0
- package/transpiled/directives/auth/schemes.js.map +1 -0
- package/transpiled/directives/auth/split.d.ts +2 -0
- package/transpiled/directives/auth/split.js +38 -0
- package/transpiled/directives/auth/split.js.map +1 -0
- package/transpiled/directives/auth/types.d.ts +31 -0
- package/transpiled/directives/auth/types.js +3 -0
- package/transpiled/directives/auth/types.js.map +1 -0
- package/transpiled/directives/dev/Family.d.ts +10 -0
- package/transpiled/directives/dev/Family.js +25 -0
- package/transpiled/directives/dev/Family.js.map +1 -0
- package/transpiled/directives/dev/Stub.d.ts +7 -0
- package/transpiled/directives/dev/Stub.js +14 -0
- package/transpiled/directives/dev/Stub.js.map +1 -0
- package/transpiled/directives/dev/index.d.ts +2 -0
- package/transpiled/directives/dev/index.js +7 -0
- package/transpiled/directives/dev/index.js.map +1 -0
- package/transpiled/directives/dev/types.d.ts +4 -0
- package/transpiled/directives/dev/types.js +3 -0
- package/transpiled/directives/dev/types.js.map +1 -0
- package/transpiled/directives/index.d.ts +2 -0
- package/transpiled/directives/index.js +10 -0
- package/transpiled/directives/index.js.map +1 -0
- package/transpiled/discovery.d.ts +1 -0
- package/transpiled/discovery.js +3 -0
- package/transpiled/discovery.js.map +1 -0
- package/transpiled/exceptions.d.ts +2 -0
- package/transpiled/exceptions.js +39 -0
- package/transpiled/exceptions.js.map +1 -0
- package/transpiled/index.d.ts +5 -0
- package/transpiled/index.js +12 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/manifest.d.ts +3 -0
- package/transpiled/manifest.js +30 -0
- package/transpiled/manifest.js.map +1 -0
- package/transpiled/root.d.ts +2 -0
- package/transpiled/root.js +39 -0
- package/transpiled/root.js.map +1 -0
- package/transpiled/schemas.d.ts +3 -0
- package/transpiled/schemas.js +14 -0
- package/transpiled/schemas.js.map +1 -0
- package/transpiled/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.json +12 -0
- package/src/.manifest/index.js +0 -7
- package/src/.manifest/normalize.js +0 -58
- package/src/.manifest/schema.yaml +0 -69
- package/src/.manifest/validate.js +0 -17
- package/src/constants.js +0 -3
- package/src/deployment.js +0 -23
- package/src/exposition.js +0 -68
- package/src/factory.js +0 -76
- package/src/index.js +0 -9
- package/src/manifest.js +0 -12
- package/src/query/criteria.js +0 -55
- package/src/query/enum.js +0 -35
- package/src/query/index.js +0 -17
- package/src/query/query.js +0 -60
- package/src/query/range.js +0 -28
- package/src/query/sort.js +0 -19
- package/src/remote.js +0 -88
- package/src/server.js +0 -83
- package/src/tenant.js +0 -29
- package/src/translate/etag.js +0 -14
- package/src/translate/index.js +0 -7
- package/src/translate/request.js +0 -68
- package/src/translate/response.js +0 -62
- package/src/tree.js +0 -109
- package/test/manifest.normalize.fixtures.js +0 -37
- package/test/manifest.normalize.test.js +0 -37
- package/test/manifest.validate.test.js +0 -40
- package/test/query.range.test.js +0 -18
- package/test/tree.fixtures.js +0 -21
- package/test/tree.test.js +0 -44
- package/types/annotations.d.ts +0 -10
- package/types/declarations.d.ts +0 -31
- package/types/exposition.d.ts +0 -13
- package/types/http.d.ts +0 -13
- package/types/query.d.ts +0 -16
- package/types/remote.d.ts +0 -19
- package/types/server.d.ts +0 -13
- package/types/tree.d.ts +0 -33
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { generate } from 'randomstring'
|
|
3
|
+
import * as msgpack from 'msgpackr'
|
|
4
|
+
import { type OutgoingMessage, read, write } from './messages'
|
|
5
|
+
import { createRequest, res } from './Server.fixtures'
|
|
6
|
+
import { BadRequest, NotAcceptable, UnsupportedMediaType } from './exceptions'
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('read', () => {
|
|
13
|
+
it('should parse application/json', async () => {
|
|
14
|
+
const path = generate()
|
|
15
|
+
const headers = { 'content-type': 'application/json' }
|
|
16
|
+
const input = { [generate()]: generate() }
|
|
17
|
+
const json = JSON.stringify(input)
|
|
18
|
+
const request = createRequest({ path, headers }, json)
|
|
19
|
+
const output = await read(request)
|
|
20
|
+
|
|
21
|
+
expect(output).toStrictEqual(input)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should parse application/yaml', async () => {
|
|
25
|
+
const path = generate()
|
|
26
|
+
const headers = { 'content-type': 'application/yaml' }
|
|
27
|
+
const yaml = 'foo: 1'
|
|
28
|
+
const request = createRequest({ path, headers }, yaml)
|
|
29
|
+
const value = await read(request)
|
|
30
|
+
|
|
31
|
+
expect(value).toStrictEqual({ foo: 1 })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should parse application/mskpack', async () => {
|
|
35
|
+
const path = generate()
|
|
36
|
+
const headers = { 'content-type': 'application/msgpack' }
|
|
37
|
+
const input = { [generate()]: generate() }
|
|
38
|
+
const msg = msgpack.encode(input)
|
|
39
|
+
const request = createRequest({ path, headers }, msg)
|
|
40
|
+
const output = await read(request)
|
|
41
|
+
|
|
42
|
+
expect(output).toStrictEqual(input)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should parse text/plain', async () => {
|
|
46
|
+
const path = generate()
|
|
47
|
+
const headers = { 'content-type': 'text/plain' }
|
|
48
|
+
const input = generate()
|
|
49
|
+
const request = createRequest({ path, headers }, input)
|
|
50
|
+
const output = await read(request)
|
|
51
|
+
|
|
52
|
+
expect(output).toStrictEqual(input)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should throw on unsupported request media type', async () => {
|
|
56
|
+
const path = generate()
|
|
57
|
+
const headers = { 'content-type': 'wtf/' + generate() }
|
|
58
|
+
const request = createRequest({ path, headers })
|
|
59
|
+
|
|
60
|
+
await expect(read(request)).rejects.toThrow(UnsupportedMediaType)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should throw on malformed content', async () => {
|
|
64
|
+
const path = generate()
|
|
65
|
+
const text = '{ "foo": "val... oops '
|
|
66
|
+
const headers = { 'content-type': 'application/json' }
|
|
67
|
+
const request = createRequest({ path, headers }, text)
|
|
68
|
+
|
|
69
|
+
await expect(read(request)).rejects.toThrow(BadRequest)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('write', () => {
|
|
74
|
+
it('should write encoded response', async () => {
|
|
75
|
+
const value = { [generate()]: generate() }
|
|
76
|
+
const json = JSON.stringify(value)
|
|
77
|
+
const buf = Buffer.from(json)
|
|
78
|
+
const headers = { accept: 'application/json' }
|
|
79
|
+
const request = createRequest({ headers }, buf)
|
|
80
|
+
|
|
81
|
+
write(request, res, value)
|
|
82
|
+
|
|
83
|
+
expect(res.set).toHaveBeenCalledWith('content-type', 'application/json')
|
|
84
|
+
expect(res.send).toHaveBeenCalledWith(buf)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should throw on unsupported response media type', async () => {
|
|
88
|
+
const headers = { accept: 'wtf/' + generate() }
|
|
89
|
+
const request = createRequest({ headers })
|
|
90
|
+
const value = generate()
|
|
91
|
+
|
|
92
|
+
expect(() => {
|
|
93
|
+
write(request, res, value)
|
|
94
|
+
}).toThrow(NotAcceptable)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should use application/yaml as default', async () => {
|
|
98
|
+
const request = createRequest()
|
|
99
|
+
const message: OutgoingMessage = { headers: {}, body: 'hello' }
|
|
100
|
+
|
|
101
|
+
write(request, res, message)
|
|
102
|
+
|
|
103
|
+
expect(res.set).toHaveBeenCalledWith('content-type', 'application/yaml')
|
|
104
|
+
expect(res.send).toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should negotiate', async () => {
|
|
108
|
+
const headers = { accept: 'text/html, application/*;q=0.2, image/jpeg;q=0.8' }
|
|
109
|
+
const request = createRequest({ headers })
|
|
110
|
+
const message: OutgoingMessage = { headers: {}, body: 'hello' }
|
|
111
|
+
|
|
112
|
+
write(request, res, message)
|
|
113
|
+
|
|
114
|
+
expect(res.set).toHaveBeenCalledWith('content-type', 'application/yaml')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http'
|
|
2
|
+
import { type Request, type Response } from 'express'
|
|
3
|
+
import Negotiator from 'negotiator'
|
|
4
|
+
import { buffer } from '@toa.io/generic'
|
|
5
|
+
import { formats, types } from './formats'
|
|
6
|
+
import { BadRequest, NotAcceptable, UnsupportedMediaType } from './exceptions'
|
|
7
|
+
|
|
8
|
+
export function write (request: Request, response: Response, body: any): void {
|
|
9
|
+
const buf = format(body, request, response)
|
|
10
|
+
|
|
11
|
+
response.send(buf)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function read (request: Request): Promise<any> {
|
|
15
|
+
const type = request.headers['content-type']
|
|
16
|
+
|
|
17
|
+
if (type === undefined) return undefined
|
|
18
|
+
|
|
19
|
+
if (!(type in formats)) throw new UnsupportedMediaType()
|
|
20
|
+
|
|
21
|
+
const format = formats[type]
|
|
22
|
+
|
|
23
|
+
const buf = await buffer(request)
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
return format.decode(buf)
|
|
27
|
+
} catch {
|
|
28
|
+
throw new BadRequest()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function format (body: any, request: Request, response: Response): Buffer | undefined {
|
|
33
|
+
if (body === undefined || body?.length === 0) return
|
|
34
|
+
|
|
35
|
+
const type = negotiate(request)
|
|
36
|
+
const format = formats[type]
|
|
37
|
+
const buf = format.encode(body)
|
|
38
|
+
|
|
39
|
+
// content-length and etag are set by Express
|
|
40
|
+
response.set('content-type', type)
|
|
41
|
+
|
|
42
|
+
return buf
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function negotiate (request: Request): string {
|
|
46
|
+
const negotiator = new Negotiator(request)
|
|
47
|
+
const mediaType = negotiator.mediaType(types)
|
|
48
|
+
|
|
49
|
+
if (mediaType === undefined) throw new NotAcceptable()
|
|
50
|
+
|
|
51
|
+
return mediaType
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Message {
|
|
55
|
+
body: any
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IncomingMessage extends Message {
|
|
59
|
+
method: string
|
|
60
|
+
path: string
|
|
61
|
+
headers: IncomingHttpHeaders
|
|
62
|
+
query: Query
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface OutgoingMessage {
|
|
66
|
+
status?: number
|
|
67
|
+
headers?: OutgoingHttpHeaders
|
|
68
|
+
body?: any
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Query {
|
|
72
|
+
id?: string
|
|
73
|
+
criteria?: string
|
|
74
|
+
sort?: string
|
|
75
|
+
omit?: string
|
|
76
|
+
limit?: string
|
|
77
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Parameter } from './RTD'
|
|
2
|
+
import { Query } from './Query'
|
|
3
|
+
import type * as http from './HTTP'
|
|
4
|
+
import type * as syntax from './RTD/syntax'
|
|
5
|
+
import type * as core from '@toa.io/core'
|
|
6
|
+
|
|
7
|
+
export abstract class Mapping {
|
|
8
|
+
public static create (verb: string, query?: syntax.Query): Mapping {
|
|
9
|
+
if (verb === 'POST')
|
|
10
|
+
return new InputMapping()
|
|
11
|
+
|
|
12
|
+
if (query === undefined)
|
|
13
|
+
throw new Error(`Query constraints must be defined for ${verb}`)
|
|
14
|
+
|
|
15
|
+
const q = new Query(query)
|
|
16
|
+
|
|
17
|
+
return new QueryableMapping(q)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public abstract fit (input: any, qs: http.Query, parameters: Parameter[]): core.Request
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class QueryableMapping extends Mapping {
|
|
24
|
+
private readonly query: Query
|
|
25
|
+
|
|
26
|
+
public constructor (query: Query) {
|
|
27
|
+
super()
|
|
28
|
+
|
|
29
|
+
this.query = query
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public fit (input: any, qs: http.Query, parameters: Parameter[]): core.Request {
|
|
33
|
+
const query = this.query.fit(qs, parameters)
|
|
34
|
+
|
|
35
|
+
return { input, query }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class InputMapping extends Mapping {
|
|
40
|
+
public fit (body: any, _: unknown, parameters: Parameter[]): core.Request {
|
|
41
|
+
const input = { ...body }
|
|
42
|
+
|
|
43
|
+
for (const parameter of parameters)
|
|
44
|
+
input[parameter.name] = parameter.value
|
|
45
|
+
|
|
46
|
+
return { input }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Query } from './Query'
|
|
2
|
+
import { type Parameter, type syntax } from './RTD'
|
|
3
|
+
|
|
4
|
+
it('should combine request criteria', async () => {
|
|
5
|
+
const query: syntax.Query = {
|
|
6
|
+
criteria: 'foo==1;',
|
|
7
|
+
omit: { range: [0, 1] },
|
|
8
|
+
limit: { range: [0, 1] }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const parameters: Parameter[] = [
|
|
12
|
+
{ name: 'bar', value: '2' },
|
|
13
|
+
{ name: 'baz', value: '3' }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const instance = new Query(query)
|
|
17
|
+
const result = instance.fit({ criteria: 'qux==4' }, parameters)
|
|
18
|
+
|
|
19
|
+
expect(result.criteria).toStrictEqual('(foo==1);(bar==2;baz==3);(qux==4)')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should set id parameter as query.id', async () => {
|
|
23
|
+
const query: syntax.Query = {
|
|
24
|
+
omit: { range: [0, 1] },
|
|
25
|
+
limit: { range: [0, 1] }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const id = '87782631058445da81cb82f78b20c223'
|
|
29
|
+
|
|
30
|
+
const parameters: Parameter[] = [{ name: 'id', value: id }]
|
|
31
|
+
|
|
32
|
+
const instance = new Query(query)
|
|
33
|
+
const result = instance.fit({}, parameters)
|
|
34
|
+
|
|
35
|
+
expect(result.criteria).toBeUndefined()
|
|
36
|
+
expect(result.id).toStrictEqual(id)
|
|
37
|
+
})
|
package/source/Query.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as http from './HTTP'
|
|
2
|
+
import { type Parameter } from './RTD'
|
|
3
|
+
import * as schemas from './schemas'
|
|
4
|
+
import type * as syntax from './RTD/syntax'
|
|
5
|
+
import type * as core from '@toa.io/core'
|
|
6
|
+
|
|
7
|
+
export class Query {
|
|
8
|
+
private readonly query: syntax.Query
|
|
9
|
+
private readonly closed: boolean = false
|
|
10
|
+
|
|
11
|
+
public constructor (query: syntax.Query) {
|
|
12
|
+
if (query.criteria !== undefined) {
|
|
13
|
+
const open = query.criteria[query.criteria.length - 1] === ';'
|
|
14
|
+
|
|
15
|
+
if (open) query.criteria = query.criteria.slice(0, -1)
|
|
16
|
+
else this.closed = true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.query = query
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public fit (query: http.Query, parameters: Parameter[]): core.Query {
|
|
23
|
+
const error = schemas.querystring.fit(query)
|
|
24
|
+
|
|
25
|
+
if (error !== null)
|
|
26
|
+
throw new http.BadRequest('Query ' + error.message)
|
|
27
|
+
|
|
28
|
+
this.fitCriteria(query, parameters)
|
|
29
|
+
this.fitRanges(query)
|
|
30
|
+
this.fitSort(query)
|
|
31
|
+
|
|
32
|
+
return query as core.Query
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fitCriteria (query: http.Query, parameters: Parameter[]): void {
|
|
36
|
+
const criteria: string[] = []
|
|
37
|
+
|
|
38
|
+
if (this.query.criteria !== undefined)
|
|
39
|
+
criteria.push(this.query.criteria)
|
|
40
|
+
|
|
41
|
+
const idx = parameters.findIndex((parameter) => parameter.name === 'id')
|
|
42
|
+
|
|
43
|
+
if (idx !== -1) {
|
|
44
|
+
query.id = parameters[idx].value
|
|
45
|
+
|
|
46
|
+
parameters.splice(idx, 1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (parameters.length > 0) {
|
|
50
|
+
const chunks = parameters
|
|
51
|
+
.map(({ name, value }) => `${name}==${value}`)
|
|
52
|
+
.join(';')
|
|
53
|
+
|
|
54
|
+
criteria.push(chunks)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (query.criteria !== undefined)
|
|
58
|
+
if (this.closed) throw new http.BadRequest('Query criteria is closed.')
|
|
59
|
+
else criteria.push(query.criteria)
|
|
60
|
+
|
|
61
|
+
switch (criteria.length) {
|
|
62
|
+
case 0:
|
|
63
|
+
break
|
|
64
|
+
case 1:
|
|
65
|
+
query.criteria = criteria[0]
|
|
66
|
+
break
|
|
67
|
+
default:
|
|
68
|
+
query.criteria = '(' + criteria.join(');(') + ')'
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fitRanges (qs: http.Query): void {
|
|
74
|
+
const query = qs as core.Query
|
|
75
|
+
|
|
76
|
+
if (qs.limit !== undefined)
|
|
77
|
+
query.limit = fit(qs.limit, this.query.limit.range, 'limit')
|
|
78
|
+
else
|
|
79
|
+
query.limit = this.query.limit.value
|
|
80
|
+
|
|
81
|
+
if (qs.omit !== undefined)
|
|
82
|
+
query.omit = fit(qs.omit, this.query.omit.range, 'omit')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private fitSort (qs: http.Query): void {
|
|
86
|
+
const query = qs as core.Query
|
|
87
|
+
|
|
88
|
+
if (qs.sort === undefined && this.query.sort === undefined)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
const sort = (this.query.sort ?? '') + (qs.sort ?? '')
|
|
92
|
+
|
|
93
|
+
query.sort = sort.split(';')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fit (string: string, range: [number, number], name: string): number {
|
|
98
|
+
const number = parseInt(string)
|
|
99
|
+
|
|
100
|
+
if (number < range[0] || number > range[1])
|
|
101
|
+
throw new http.BadRequest(`Query ${name} must be between ` +
|
|
102
|
+
`${range[0]} and ${range[1]} inclusive.`)
|
|
103
|
+
|
|
104
|
+
return number
|
|
105
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Directives, type DirectivesFactory } from './Directives'
|
|
2
|
+
import { type Endpoint, type EndpointsFactory } from './Endpoint'
|
|
3
|
+
|
|
4
|
+
export interface Context<
|
|
5
|
+
TEndpoint extends Endpoint = any,
|
|
6
|
+
TDirectives extends Directives<TDirectives> = any,
|
|
7
|
+
TExtension = any
|
|
8
|
+
> {
|
|
9
|
+
readonly protected: boolean
|
|
10
|
+
readonly endpoints: EndpointsFactory<TEndpoint>
|
|
11
|
+
readonly directives: {
|
|
12
|
+
readonly factory: DirectivesFactory
|
|
13
|
+
stack: TDirectives[]
|
|
14
|
+
}
|
|
15
|
+
readonly extension?: TExtension
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Context } from './Context'
|
|
2
|
+
import type * as syntax from './syntax'
|
|
3
|
+
|
|
4
|
+
export interface Endpoint<T extends Endpoint = any> {
|
|
5
|
+
call: T['call']
|
|
6
|
+
close: () => Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface EndpointsFactory<T extends Endpoint<T> = any> {
|
|
10
|
+
create: (method: syntax.Method, context: Context) => T
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Node } from './Node'
|
|
2
|
+
import { type Directives } from './Directives'
|
|
3
|
+
import { type Endpoint } from './Endpoint'
|
|
4
|
+
|
|
5
|
+
export interface Match<
|
|
6
|
+
TEndpoint extends Endpoint<TEndpoint> = any,
|
|
7
|
+
TDirectives extends Directives<TDirectives> = any
|
|
8
|
+
> {
|
|
9
|
+
node: Node<TEndpoint, TDirectives>
|
|
10
|
+
parameters: Parameter[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Parameter {
|
|
14
|
+
name: string
|
|
15
|
+
value: string
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Directives } from './Directives'
|
|
2
|
+
import { type Endpoint } from './Endpoint'
|
|
3
|
+
|
|
4
|
+
export class Method<
|
|
5
|
+
TEndpoint extends Endpoint<TEndpoint> = any,
|
|
6
|
+
TDirectives extends Directives<TDirectives> = any
|
|
7
|
+
> {
|
|
8
|
+
public readonly endpoint: TEndpoint | null
|
|
9
|
+
public readonly directives: TDirectives
|
|
10
|
+
|
|
11
|
+
public constructor (endpoint: TEndpoint | null, directives: TDirectives) {
|
|
12
|
+
this.endpoint = endpoint
|
|
13
|
+
this.directives = directives
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async close (): Promise<void> {
|
|
17
|
+
await this.endpoint?.close()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Methods<
|
|
22
|
+
TEndpoint extends Endpoint<TEndpoint> = any,
|
|
23
|
+
TDirectives extends Directives<TDirectives> = any
|
|
24
|
+
> = Record<string, Method<TEndpoint, TDirectives>>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type Route } from './Route'
|
|
2
|
+
import { type Methods } from './Method'
|
|
3
|
+
import { type Parameter } from './Match'
|
|
4
|
+
import { type Directives } from './Directives'
|
|
5
|
+
import { type Endpoint } from './Endpoint'
|
|
6
|
+
|
|
7
|
+
export class Node<
|
|
8
|
+
TEndpoint extends Endpoint<TEndpoint> = any,
|
|
9
|
+
TDirectives extends Directives<TDirectives> = any
|
|
10
|
+
> {
|
|
11
|
+
public intermediate: boolean
|
|
12
|
+
public methods: Methods<TEndpoint, TDirectives>
|
|
13
|
+
private readonly protected: boolean
|
|
14
|
+
private routes: Route[]
|
|
15
|
+
|
|
16
|
+
public constructor
|
|
17
|
+
(routes: Route[], methods: Methods<TEndpoint, TDirectives>, properties: Properties) {
|
|
18
|
+
this.routes = routes
|
|
19
|
+
this.methods = methods
|
|
20
|
+
this.protected = properties.protected
|
|
21
|
+
this.intermediate = this.routes.findIndex((route) => route.root) !== -1
|
|
22
|
+
|
|
23
|
+
this.sort()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public match (fragments: string[], parameters: Parameter[]): Node<TEndpoint, TDirectives> | null {
|
|
27
|
+
for (const route of this.routes) {
|
|
28
|
+
const node = route.match(fragments, parameters)
|
|
29
|
+
|
|
30
|
+
if (node !== null)
|
|
31
|
+
return node
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public merge (node: Node<TEndpoint, TDirectives>): void {
|
|
38
|
+
this.intermediate = node.intermediate
|
|
39
|
+
|
|
40
|
+
if (!this.protected) this.replace(node)
|
|
41
|
+
else this.append(node)
|
|
42
|
+
|
|
43
|
+
this.sort()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private replace (node: Node<TEndpoint, TDirectives>): void {
|
|
47
|
+
const methods = Object.values(this.methods)
|
|
48
|
+
|
|
49
|
+
this.routes = node.routes
|
|
50
|
+
this.methods = node.methods
|
|
51
|
+
|
|
52
|
+
for (const method of methods)
|
|
53
|
+
void method.close() // race condition is really unlikely
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private append (node: Node<TEndpoint, TDirectives>): void {
|
|
57
|
+
for (const route of node.routes)
|
|
58
|
+
this.mergeRoute(route)
|
|
59
|
+
|
|
60
|
+
for (const [verb, method] of Object.entries(node.methods))
|
|
61
|
+
if (verb in this.methods)
|
|
62
|
+
console.warn(`Overriding of the protected method ${verb} is not permitted.`)
|
|
63
|
+
else
|
|
64
|
+
this.methods[verb] = method
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private mergeRoute (candidate: Route): void {
|
|
68
|
+
for (const route of this.routes)
|
|
69
|
+
if (candidate.equals(route)) {
|
|
70
|
+
route.merge(candidate)
|
|
71
|
+
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.routes.push(candidate)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private sort (): void {
|
|
79
|
+
this.routes.sort((a, b) => a.variables - b.variables)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface Properties {
|
|
84
|
+
protected: boolean
|
|
85
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Node } from './Node'
|
|
2
|
+
import { type Segment } from './segment'
|
|
3
|
+
import { type Parameter } from './Match'
|
|
4
|
+
|
|
5
|
+
export class Route {
|
|
6
|
+
public readonly root: boolean
|
|
7
|
+
public readonly variables: number = 0
|
|
8
|
+
private readonly segments: Segment[]
|
|
9
|
+
private readonly node: Node
|
|
10
|
+
|
|
11
|
+
public constructor (segments: Segment[], node: Node) {
|
|
12
|
+
this.root = segments.length === 0
|
|
13
|
+
this.segments = segments
|
|
14
|
+
this.node = node
|
|
15
|
+
|
|
16
|
+
for (const segment of segments)
|
|
17
|
+
if (segment.fragment === null)
|
|
18
|
+
this.variables++
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public match (fragments: string[], parameters: Parameter[]): Node | null {
|
|
22
|
+
for (let i = 0; i < this.segments.length; i++) {
|
|
23
|
+
const segment = this.segments[i]
|
|
24
|
+
|
|
25
|
+
if (segment.fragment !== null && segment.fragment !== fragments[i])
|
|
26
|
+
return null
|
|
27
|
+
|
|
28
|
+
if (segment.fragment === null)
|
|
29
|
+
parameters.push({ name: segment.placeholder, value: fragments[i] })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const exact = this.segments.length === fragments.length
|
|
33
|
+
|
|
34
|
+
if (exact && !this.node.intermediate) return this.node
|
|
35
|
+
else return this.matchNested(fragments, parameters)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public equals (route: Route): boolean {
|
|
39
|
+
if (route.segments.length !== this.segments.length)
|
|
40
|
+
return false
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < this.segments.length; i++)
|
|
43
|
+
if (this.segments[i].fragment !== route.segments[i].fragment)
|
|
44
|
+
return false
|
|
45
|
+
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public merge (route: Route): void {
|
|
50
|
+
this.node.merge(route.node)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private matchNested (fragments: string[], parameters: Parameter[]): Node | null {
|
|
54
|
+
fragments = fragments.slice(this.segments.length)
|
|
55
|
+
|
|
56
|
+
return this.node.match(fragments, parameters)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Node } from './Node'
|
|
2
|
+
import { createNode } from './factory'
|
|
3
|
+
import { fragment } from './segment'
|
|
4
|
+
import { type Match, type Parameter } from './Match'
|
|
5
|
+
import { type Context } from './Context'
|
|
6
|
+
import { type Directives, type DirectivesFactory } from './Directives'
|
|
7
|
+
import { type Endpoint, type EndpointsFactory } from './Endpoint'
|
|
8
|
+
import type * as syntax from './syntax'
|
|
9
|
+
|
|
10
|
+
export class Tree<
|
|
11
|
+
TEndpoint extends Endpoint<TEndpoint> = any,
|
|
12
|
+
TDirectives extends Directives<TDirectives> = any
|
|
13
|
+
> {
|
|
14
|
+
private readonly root: syntax.Node
|
|
15
|
+
private readonly trunk: Node<TEndpoint, TDirectives>
|
|
16
|
+
private readonly endpoints: EndpointsFactory<TEndpoint>
|
|
17
|
+
private readonly directives: DirectivesFactory
|
|
18
|
+
|
|
19
|
+
public constructor
|
|
20
|
+
(node: syntax.Node, endpoints: EndpointsFactory, directives: DirectivesFactory) {
|
|
21
|
+
this.endpoints = endpoints
|
|
22
|
+
this.directives = directives
|
|
23
|
+
this.trunk = this.createNode(node, PROTECTED)
|
|
24
|
+
this.root = node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public match (path: string): Match<TEndpoint, TDirectives> | null {
|
|
28
|
+
const fragments = fragment(path)
|
|
29
|
+
const parameters: Parameter[] = []
|
|
30
|
+
const node = this.trunk.match(fragments, parameters)
|
|
31
|
+
|
|
32
|
+
if (node === null) return null
|
|
33
|
+
else return { node, parameters }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public merge (node: syntax.Node, extension: any): void {
|
|
37
|
+
const branch = this.createNode(node, !PROTECTED, extension)
|
|
38
|
+
|
|
39
|
+
this.trunk.merge(branch)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private createNode (node: syntax.Node, protect: boolean, extension?: any): Node {
|
|
43
|
+
const context: Context = {
|
|
44
|
+
protected: protect,
|
|
45
|
+
endpoints: this.endpoints,
|
|
46
|
+
directives: {
|
|
47
|
+
factory: this.directives,
|
|
48
|
+
stack: this.root?.directives ?? []
|
|
49
|
+
},
|
|
50
|
+
extension
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return createNode(node, context)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PROTECTED = true
|