@toa.io/extensions.exposition 0.9.0-canary.2 → 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.
Files changed (363) hide show
  1. package/components/context.toa.yaml +15 -0
  2. package/components/identity.bans/manifest.toa.yaml +18 -0
  3. package/components/identity.basic/events/principal.js +9 -0
  4. package/components/identity.basic/manifest.toa.yaml +50 -0
  5. package/components/identity.basic/source/authenticate.ts +29 -0
  6. package/components/identity.basic/source/create.ts +19 -0
  7. package/components/identity.basic/source/transit.ts +64 -0
  8. package/components/identity.basic/source/types.ts +42 -0
  9. package/components/identity.basic/tsconfig.json +9 -0
  10. package/components/identity.roles/manifest.toa.yaml +31 -0
  11. package/components/identity.roles/source/list.ts +7 -0
  12. package/components/identity.roles/source/principal.ts +20 -0
  13. package/components/identity.roles/tsconfig.json +9 -0
  14. package/components/identity.tokens/manifest.toa.yaml +39 -0
  15. package/components/identity.tokens/receivers/identity.bans.updated.js +3 -0
  16. package/components/identity.tokens/source/authenticate.test.ts +56 -0
  17. package/components/identity.tokens/source/authenticate.ts +38 -0
  18. package/components/identity.tokens/source/decrypt.test.ts +59 -0
  19. package/components/identity.tokens/source/decrypt.ts +25 -0
  20. package/components/identity.tokens/source/encrypt.test.ts +35 -0
  21. package/components/identity.tokens/source/encrypt.ts +25 -0
  22. package/components/identity.tokens/source/revoke.ts +5 -0
  23. package/components/identity.tokens/source/types.ts +48 -0
  24. package/components/identity.tokens/tsconfig.json +9 -0
  25. package/cucumber.js +9 -0
  26. package/documentation/.assets/ia3-dark.jpg +0 -0
  27. package/documentation/.assets/ia3-light.jpg +0 -0
  28. package/documentation/.assets/overview-dark.jpg +0 -0
  29. package/documentation/.assets/overview-light.jpg +0 -0
  30. package/documentation/.assets/role-scopes-dark.jpg +0 -0
  31. package/documentation/.assets/role-scopes-light.jpg +0 -0
  32. package/documentation/.assets/rtd-dark.jpg +0 -0
  33. package/documentation/.assets/rtd-light.jpg +0 -0
  34. package/documentation/access.md +256 -0
  35. package/documentation/components.md +276 -0
  36. package/documentation/identity.md +156 -0
  37. package/documentation/protocol.md +15 -0
  38. package/documentation/query.md +226 -0
  39. package/documentation/tree.md +169 -0
  40. package/features/access.feature +442 -0
  41. package/features/annotation.feature +28 -0
  42. package/features/body.feature +21 -0
  43. package/features/directives.feature +56 -0
  44. package/features/dynamic.feature +89 -0
  45. package/features/errors.feature +208 -0
  46. package/features/identity.basic.feature +235 -0
  47. package/features/identity.feature +61 -0
  48. package/features/identity.roles.feature +51 -0
  49. package/features/identity.tokens.feature +116 -0
  50. package/features/queries.feature +214 -0
  51. package/features/routes.feature +49 -0
  52. package/features/steps/Common.ts +10 -0
  53. package/features/steps/Components.ts +43 -0
  54. package/features/steps/Database.ts +58 -0
  55. package/features/steps/Gateway.ts +105 -0
  56. package/features/steps/HTTP.ts +71 -0
  57. package/features/steps/Parameters.ts +12 -0
  58. package/features/steps/Workspace.ts +40 -0
  59. package/features/steps/components/greeter/manifest.toa.yaml +9 -0
  60. package/features/steps/components/greeter/operations/greet.js +7 -0
  61. package/features/steps/components/pots/manifest.toa.yaml +20 -0
  62. package/features/steps/components/users/manifest.toa.yaml +11 -0
  63. package/features/steps/tsconfig.json +9 -0
  64. package/package.json +31 -17
  65. package/readme.md +181 -0
  66. package/schemas/annotation.cos.yaml +4 -0
  67. package/schemas/directive.cos.yaml +3 -0
  68. package/schemas/method.cos.yaml +9 -0
  69. package/schemas/node.cos.yaml +5 -0
  70. package/schemas/query.cos.yaml +16 -0
  71. package/schemas/querystring.cos.yaml +5 -0
  72. package/schemas/range.cos.yaml +2 -0
  73. package/schemas/route.cos.yaml +2 -0
  74. package/source/Annotation.ts +6 -0
  75. package/source/Branch.ts +8 -0
  76. package/source/Composition.ts +58 -0
  77. package/source/Context.ts +6 -0
  78. package/source/Directive.test.ts +91 -0
  79. package/source/Directive.ts +120 -0
  80. package/source/Endpoint.ts +57 -0
  81. package/source/Factory.ts +53 -0
  82. package/source/Gateway.ts +93 -0
  83. package/source/HTTP/Server.fixtures.ts +45 -0
  84. package/source/HTTP/Server.test.ts +221 -0
  85. package/source/HTTP/Server.ts +135 -0
  86. package/source/HTTP/exceptions.ts +77 -0
  87. package/source/HTTP/formats/index.ts +19 -0
  88. package/source/HTTP/formats/json.ts +13 -0
  89. package/source/HTTP/formats/msgpack.ts +10 -0
  90. package/source/HTTP/formats/text.ts +9 -0
  91. package/source/HTTP/formats/yaml.ts +14 -0
  92. package/source/HTTP/index.ts +3 -0
  93. package/source/HTTP/messages.test.ts +116 -0
  94. package/source/HTTP/messages.ts +77 -0
  95. package/source/Mapping.ts +48 -0
  96. package/source/Query.test.ts +37 -0
  97. package/source/Query.ts +105 -0
  98. package/source/RTD/Context.ts +16 -0
  99. package/source/RTD/Directives.ts +9 -0
  100. package/source/RTD/Endpoint.ts +11 -0
  101. package/source/RTD/Match.ts +16 -0
  102. package/source/RTD/Method.ts +24 -0
  103. package/source/RTD/Node.ts +85 -0
  104. package/source/RTD/Route.ts +58 -0
  105. package/source/RTD/Tree.ts +57 -0
  106. package/source/RTD/factory.ts +47 -0
  107. package/source/RTD/index.ts +8 -0
  108. package/source/RTD/segment.test.ts +32 -0
  109. package/source/RTD/segment.ts +25 -0
  110. package/source/RTD/syntax/index.ts +2 -0
  111. package/source/RTD/syntax/parse.test.ts +188 -0
  112. package/source/RTD/syntax/parse.ts +153 -0
  113. package/source/RTD/syntax/types.ts +48 -0
  114. package/source/Remotes.test.ts +41 -0
  115. package/source/Remotes.ts +20 -0
  116. package/source/Tenant.ts +38 -0
  117. package/source/deployment.ts +42 -0
  118. package/source/directives/auth/Anonymous.ts +14 -0
  119. package/source/directives/auth/Echo.ts +12 -0
  120. package/source/directives/auth/Family.ts +145 -0
  121. package/source/directives/auth/Id.ts +19 -0
  122. package/source/directives/auth/Incept.ts +42 -0
  123. package/source/directives/auth/Role.test.ts +62 -0
  124. package/source/directives/auth/Role.ts +56 -0
  125. package/source/directives/auth/Rule.ts +28 -0
  126. package/source/directives/auth/Scheme.ts +26 -0
  127. package/source/directives/auth/index.ts +3 -0
  128. package/source/directives/auth/schemes.ts +8 -0
  129. package/source/directives/auth/split.ts +15 -0
  130. package/source/directives/auth/types.ts +37 -0
  131. package/source/directives/dev/Family.ts +34 -0
  132. package/source/directives/dev/Stub.ts +14 -0
  133. package/source/directives/dev/index.ts +3 -0
  134. package/source/directives/dev/types.ts +5 -0
  135. package/source/directives/index.ts +5 -0
  136. package/source/discovery.ts +1 -0
  137. package/source/exceptions.ts +17 -0
  138. package/source/index.test.ts +9 -0
  139. package/source/index.ts +6 -0
  140. package/source/manifest.test.ts +57 -0
  141. package/source/manifest.ts +35 -0
  142. package/source/root.ts +38 -0
  143. package/source/schemas.ts +9 -0
  144. package/transpiled/Annotation.d.ts +6 -0
  145. package/transpiled/Annotation.js +3 -0
  146. package/transpiled/Annotation.js.map +1 -0
  147. package/transpiled/Branch.d.ts +7 -0
  148. package/transpiled/Branch.js +3 -0
  149. package/transpiled/Branch.js.map +1 -0
  150. package/transpiled/Composition.d.ts +14 -0
  151. package/transpiled/Composition.js +43 -0
  152. package/transpiled/Composition.js.map +1 -0
  153. package/transpiled/Context.d.ts +5 -0
  154. package/transpiled/Context.js +3 -0
  155. package/transpiled/Context.js.map +1 -0
  156. package/transpiled/Directive.d.ts +32 -0
  157. package/transpiled/Directive.js +76 -0
  158. package/transpiled/Directive.js.map +1 -0
  159. package/transpiled/Endpoint.d.ts +20 -0
  160. package/transpiled/Endpoint.js +44 -0
  161. package/transpiled/Endpoint.js.map +1 -0
  162. package/transpiled/Factory.d.ts +11 -0
  163. package/transpiled/Factory.js +67 -0
  164. package/transpiled/Factory.js.map +1 -0
  165. package/transpiled/Gateway.d.ts +19 -0
  166. package/transpiled/Gateway.js +90 -0
  167. package/transpiled/Gateway.js.map +1 -0
  168. package/transpiled/HTTP/Server.d.ts +22 -0
  169. package/transpiled/HTTP/Server.fixtures.d.ts +12 -0
  170. package/transpiled/HTTP/Server.fixtures.js +36 -0
  171. package/transpiled/HTTP/Server.fixtures.js.map +1 -0
  172. package/transpiled/HTTP/Server.js +111 -0
  173. package/transpiled/HTTP/Server.js.map +1 -0
  174. package/transpiled/HTTP/exceptions.d.ts +39 -0
  175. package/transpiled/HTTP/exceptions.js +78 -0
  176. package/transpiled/HTTP/exceptions.js.map +1 -0
  177. package/transpiled/HTTP/formats/index.d.ts +8 -0
  178. package/transpiled/HTTP/formats/index.js +38 -0
  179. package/transpiled/HTTP/formats/index.js.map +1 -0
  180. package/transpiled/HTTP/formats/json.d.ts +4 -0
  181. package/transpiled/HTTP/formats/json.js +15 -0
  182. package/transpiled/HTTP/formats/json.js.map +1 -0
  183. package/transpiled/HTTP/formats/msgpack.d.ts +4 -0
  184. package/transpiled/HTTP/formats/msgpack.js +36 -0
  185. package/transpiled/HTTP/formats/msgpack.js.map +1 -0
  186. package/transpiled/HTTP/formats/text.d.ts +4 -0
  187. package/transpiled/HTTP/formats/text.js +13 -0
  188. package/transpiled/HTTP/formats/text.js.map +1 -0
  189. package/transpiled/HTTP/formats/yaml.d.ts +4 -0
  190. package/transpiled/HTTP/formats/yaml.js +39 -0
  191. package/transpiled/HTTP/formats/yaml.js.map +1 -0
  192. package/transpiled/HTTP/index.d.ts +3 -0
  193. package/transpiled/HTTP/index.js +20 -0
  194. package/transpiled/HTTP/index.js.map +1 -0
  195. package/transpiled/HTTP/messages.d.ts +27 -0
  196. package/transpiled/HTTP/messages.js +49 -0
  197. package/transpiled/HTTP/messages.js.map +1 -0
  198. package/transpiled/Mapping.d.ts +8 -0
  199. package/transpiled/Mapping.js +35 -0
  200. package/transpiled/Mapping.js.map +1 -0
  201. package/transpiled/Query.d.ts +13 -0
  202. package/transpiled/Query.js +107 -0
  203. package/transpiled/Query.js.map +1 -0
  204. package/transpiled/RTD/Context.d.ts +11 -0
  205. package/transpiled/RTD/Context.js +3 -0
  206. package/transpiled/RTD/Context.js.map +1 -0
  207. package/transpiled/RTD/Directives.d.ts +7 -0
  208. package/transpiled/RTD/Directives.js +3 -0
  209. package/transpiled/RTD/Directives.js.map +1 -0
  210. package/transpiled/RTD/Endpoint.d.ts +9 -0
  211. package/transpiled/RTD/Endpoint.js +3 -0
  212. package/transpiled/RTD/Endpoint.js.map +1 -0
  213. package/transpiled/RTD/Match.d.ts +11 -0
  214. package/transpiled/RTD/Match.js +3 -0
  215. package/transpiled/RTD/Match.js.map +1 -0
  216. package/transpiled/RTD/Method.d.ts +9 -0
  217. package/transpiled/RTD/Method.js +16 -0
  218. package/transpiled/RTD/Method.js.map +1 -0
  219. package/transpiled/RTD/Node.d.ts +21 -0
  220. package/transpiled/RTD/Node.js +61 -0
  221. package/transpiled/RTD/Node.js.map +1 -0
  222. package/transpiled/RTD/Route.d.ts +14 -0
  223. package/transpiled/RTD/Route.js +48 -0
  224. package/transpiled/RTD/Route.js.map +1 -0
  225. package/transpiled/RTD/Tree.d.ts +14 -0
  226. package/transpiled/RTD/Tree.js +45 -0
  227. package/transpiled/RTD/Tree.js.map +1 -0
  228. package/transpiled/RTD/factory.d.ts +6 -0
  229. package/transpiled/RTD/factory.js +36 -0
  230. package/transpiled/RTD/factory.js.map +1 -0
  231. package/transpiled/RTD/index.d.ts +8 -0
  232. package/transpiled/RTD/index.js +38 -0
  233. package/transpiled/RTD/index.js.map +1 -0
  234. package/transpiled/RTD/segment.d.ts +8 -0
  235. package/transpiled/RTD/segment.js +23 -0
  236. package/transpiled/RTD/segment.js.map +1 -0
  237. package/transpiled/RTD/syntax/index.d.ts +2 -0
  238. package/transpiled/RTD/syntax/index.js +19 -0
  239. package/transpiled/RTD/syntax/index.js.map +1 -0
  240. package/transpiled/RTD/syntax/parse.d.ts +4 -0
  241. package/transpiled/RTD/syntax/parse.js +128 -0
  242. package/transpiled/RTD/syntax/parse.js.map +1 -0
  243. package/transpiled/RTD/syntax/types.d.ts +41 -0
  244. package/transpiled/RTD/syntax/types.js +5 -0
  245. package/transpiled/RTD/syntax/types.js.map +1 -0
  246. package/transpiled/Remotes.d.ts +7 -0
  247. package/transpiled/Remotes.js +19 -0
  248. package/transpiled/Remotes.js.map +1 -0
  249. package/transpiled/Tenant.d.ts +12 -0
  250. package/transpiled/Tenant.js +30 -0
  251. package/transpiled/Tenant.js.map +1 -0
  252. package/transpiled/deployment.d.ts +3 -0
  253. package/transpiled/deployment.js +61 -0
  254. package/transpiled/deployment.js.map +1 -0
  255. package/transpiled/directives/auth/Anonymous.d.ts +6 -0
  256. package/transpiled/directives/auth/Anonymous.js +17 -0
  257. package/transpiled/directives/auth/Anonymous.js.map +1 -0
  258. package/transpiled/directives/auth/Echo.d.ts +6 -0
  259. package/transpiled/directives/auth/Echo.js +13 -0
  260. package/transpiled/directives/auth/Echo.js.map +1 -0
  261. package/transpiled/directives/auth/Family.d.ts +20 -0
  262. package/transpiled/directives/auth/Family.js +125 -0
  263. package/transpiled/directives/auth/Family.js.map +1 -0
  264. package/transpiled/directives/auth/Id.d.ts +7 -0
  265. package/transpiled/directives/auth/Id.js +17 -0
  266. package/transpiled/directives/auth/Id.js.map +1 -0
  267. package/transpiled/directives/auth/Incept.d.ts +10 -0
  268. package/transpiled/directives/auth/Incept.js +59 -0
  269. package/transpiled/directives/auth/Incept.js.map +1 -0
  270. package/transpiled/directives/auth/Role.d.ts +11 -0
  271. package/transpiled/directives/auth/Role.js +44 -0
  272. package/transpiled/directives/auth/Role.js.map +1 -0
  273. package/transpiled/directives/auth/Rule.d.ts +9 -0
  274. package/transpiled/directives/auth/Rule.js +22 -0
  275. package/transpiled/directives/auth/Rule.js.map +1 -0
  276. package/transpiled/directives/auth/Scheme.d.ts +7 -0
  277. package/transpiled/directives/auth/Scheme.js +47 -0
  278. package/transpiled/directives/auth/Scheme.js.map +1 -0
  279. package/transpiled/directives/auth/index.d.ts +2 -0
  280. package/transpiled/directives/auth/index.js +7 -0
  281. package/transpiled/directives/auth/index.js.map +1 -0
  282. package/transpiled/directives/auth/schemes.d.ts +3 -0
  283. package/transpiled/directives/auth/schemes.js +9 -0
  284. package/transpiled/directives/auth/schemes.js.map +1 -0
  285. package/transpiled/directives/auth/split.d.ts +2 -0
  286. package/transpiled/directives/auth/split.js +38 -0
  287. package/transpiled/directives/auth/split.js.map +1 -0
  288. package/transpiled/directives/auth/types.d.ts +31 -0
  289. package/transpiled/directives/auth/types.js +3 -0
  290. package/transpiled/directives/auth/types.js.map +1 -0
  291. package/transpiled/directives/dev/Family.d.ts +10 -0
  292. package/transpiled/directives/dev/Family.js +25 -0
  293. package/transpiled/directives/dev/Family.js.map +1 -0
  294. package/transpiled/directives/dev/Stub.d.ts +7 -0
  295. package/transpiled/directives/dev/Stub.js +14 -0
  296. package/transpiled/directives/dev/Stub.js.map +1 -0
  297. package/transpiled/directives/dev/index.d.ts +2 -0
  298. package/transpiled/directives/dev/index.js +7 -0
  299. package/transpiled/directives/dev/index.js.map +1 -0
  300. package/transpiled/directives/dev/types.d.ts +4 -0
  301. package/transpiled/directives/dev/types.js +3 -0
  302. package/transpiled/directives/dev/types.js.map +1 -0
  303. package/transpiled/directives/index.d.ts +2 -0
  304. package/transpiled/directives/index.js +10 -0
  305. package/transpiled/directives/index.js.map +1 -0
  306. package/transpiled/discovery.d.ts +1 -0
  307. package/transpiled/discovery.js +3 -0
  308. package/transpiled/discovery.js.map +1 -0
  309. package/transpiled/exceptions.d.ts +2 -0
  310. package/transpiled/exceptions.js +39 -0
  311. package/transpiled/exceptions.js.map +1 -0
  312. package/transpiled/index.d.ts +5 -0
  313. package/transpiled/index.js +12 -0
  314. package/transpiled/index.js.map +1 -0
  315. package/transpiled/manifest.d.ts +3 -0
  316. package/transpiled/manifest.js +30 -0
  317. package/transpiled/manifest.js.map +1 -0
  318. package/transpiled/root.d.ts +2 -0
  319. package/transpiled/root.js +39 -0
  320. package/transpiled/root.js.map +1 -0
  321. package/transpiled/schemas.d.ts +3 -0
  322. package/transpiled/schemas.js +14 -0
  323. package/transpiled/schemas.js.map +1 -0
  324. package/transpiled/tsconfig.tsbuildinfo +1 -0
  325. package/tsconfig.json +12 -0
  326. package/src/.manifest/index.js +0 -7
  327. package/src/.manifest/normalize.js +0 -58
  328. package/src/.manifest/schema.yaml +0 -71
  329. package/src/.manifest/validate.js +0 -17
  330. package/src/constants.js +0 -3
  331. package/src/deployment.js +0 -23
  332. package/src/exposition.js +0 -68
  333. package/src/factory.js +0 -76
  334. package/src/index.js +0 -9
  335. package/src/manifest.js +0 -12
  336. package/src/query/criteria.js +0 -55
  337. package/src/query/enum.js +0 -35
  338. package/src/query/index.js +0 -17
  339. package/src/query/query.js +0 -60
  340. package/src/query/range.js +0 -28
  341. package/src/query/sort.js +0 -19
  342. package/src/remote.js +0 -88
  343. package/src/server.js +0 -83
  344. package/src/tenant.js +0 -29
  345. package/src/translate/etag.js +0 -14
  346. package/src/translate/index.js +0 -7
  347. package/src/translate/request.js +0 -68
  348. package/src/translate/response.js +0 -62
  349. package/src/tree.js +0 -109
  350. package/test/manifest.normalize.fixtures.js +0 -37
  351. package/test/manifest.normalize.test.js +0 -37
  352. package/test/manifest.validate.test.js +0 -40
  353. package/test/query.range.test.js +0 -18
  354. package/test/tree.fixtures.js +0 -21
  355. package/test/tree.test.js +0 -44
  356. package/types/annotations.d.ts +0 -10
  357. package/types/declarations.d.ts +0 -31
  358. package/types/exposition.d.ts +0 -13
  359. package/types/http.d.ts +0 -13
  360. package/types/query.d.ts +0 -16
  361. package/types/remote.d.ts +0 -19
  362. package/types/server.d.ts +0 -13
  363. 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
+ })
@@ -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,9 @@
1
+ import type * as syntax from './syntax'
2
+
3
+ export interface Directives<T = any> {
4
+ merge: (directive: T) => void
5
+ }
6
+
7
+ export interface DirectivesFactory<T = any> {
8
+ create: (directives: syntax.Directive[]) => T
9
+ }
@@ -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