@planet-matrix/mobius-model 0.6.0 → 0.10.1

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 (258) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/oxlint.config.ts +1 -2
  3. package/package.json +29 -17
  4. package/scripts/build.ts +2 -52
  5. package/src/ai/README.md +1 -0
  6. package/src/ai/ai.ts +107 -0
  7. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  8. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  9. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  10. package/src/ai/chat-completion-ai/index.ts +7 -0
  11. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  12. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  13. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  14. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  15. package/src/ai/embedding-ai/embedding.ts +50 -0
  16. package/src/ai/embedding-ai/index.ts +4 -0
  17. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  18. package/src/ai/index.ts +4 -0
  19. package/src/aio/README.md +100 -0
  20. package/src/aio/content.ts +141 -0
  21. package/src/aio/index.ts +3 -0
  22. package/src/aio/json.ts +127 -0
  23. package/src/aio/prompt.ts +246 -0
  24. package/src/basic/README.md +20 -15
  25. package/src/basic/error.ts +19 -5
  26. package/src/basic/function.ts +2 -2
  27. package/src/basic/index.ts +1 -0
  28. package/src/basic/promise.ts +141 -71
  29. package/src/basic/schedule.ts +111 -0
  30. package/src/basic/stream.ts +135 -25
  31. package/src/credential/README.md +107 -0
  32. package/src/credential/api-key.ts +158 -0
  33. package/src/credential/bearer.ts +73 -0
  34. package/src/credential/index.ts +4 -0
  35. package/src/credential/json-web-token.ts +96 -0
  36. package/src/credential/password.ts +170 -0
  37. package/src/cron/README.md +86 -0
  38. package/src/cron/cron.ts +87 -0
  39. package/src/cron/index.ts +1 -0
  40. package/src/drizzle/README.md +1 -0
  41. package/src/drizzle/drizzle.ts +1 -0
  42. package/src/drizzle/helper.ts +47 -0
  43. package/src/drizzle/index.ts +5 -0
  44. package/src/drizzle/infer.ts +52 -0
  45. package/src/drizzle/kysely.ts +8 -0
  46. package/src/drizzle/pagination.ts +198 -0
  47. package/src/email/README.md +1 -0
  48. package/src/email/index.ts +1 -0
  49. package/src/email/resend.ts +25 -0
  50. package/src/event/class-event-proxy.ts +5 -6
  51. package/src/event/common.ts +13 -3
  52. package/src/event/event-manager.ts +3 -3
  53. package/src/event/instance-event-proxy.ts +5 -6
  54. package/src/event/internal.ts +4 -4
  55. package/src/exception/README.md +28 -19
  56. package/src/exception/error/error.ts +123 -0
  57. package/src/exception/error/index.ts +2 -0
  58. package/src/exception/error/match.ts +38 -0
  59. package/src/exception/error/must-fix.ts +17 -0
  60. package/src/exception/index.ts +2 -0
  61. package/src/file-system/find.ts +53 -0
  62. package/src/file-system/index.ts +2 -0
  63. package/src/file-system/path.ts +76 -0
  64. package/src/file-system/resolve.ts +22 -0
  65. package/src/form/README.md +25 -0
  66. package/src/form/index.ts +1 -0
  67. package/src/form/inputor-controller/base.ts +861 -0
  68. package/src/form/inputor-controller/boolean.ts +39 -0
  69. package/src/form/inputor-controller/file.ts +39 -0
  70. package/src/form/inputor-controller/form.ts +179 -0
  71. package/src/form/inputor-controller/helper.ts +117 -0
  72. package/src/form/inputor-controller/index.ts +17 -0
  73. package/src/form/inputor-controller/multi-select.ts +99 -0
  74. package/src/form/inputor-controller/number.ts +116 -0
  75. package/src/form/inputor-controller/select.ts +109 -0
  76. package/src/form/inputor-controller/text.ts +82 -0
  77. package/src/http/READMD.md +1 -0
  78. package/src/http/api/api-core.ts +84 -0
  79. package/src/http/api/api-handler.ts +79 -0
  80. package/src/http/api/api-host.ts +47 -0
  81. package/src/http/api/api-result.ts +56 -0
  82. package/src/http/api/api-schema.ts +154 -0
  83. package/src/http/api/api-server.ts +130 -0
  84. package/src/http/api/api-test.ts +142 -0
  85. package/src/http/api/api-type.ts +34 -0
  86. package/src/http/api/api.ts +81 -0
  87. package/src/http/api/index.ts +11 -0
  88. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  89. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  90. package/src/http/api-adapter/api-result-arktype.ts +294 -0
  91. package/src/http/api-adapter/api-result-zod.ts +286 -0
  92. package/src/http/api-adapter/index.ts +5 -0
  93. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  94. package/src/http/bin/gen-api-list/index.ts +1 -0
  95. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  96. package/src/http/bin/gen-api-test/index.ts +1 -0
  97. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  98. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  99. package/src/http/bin/gen-api-type/index.ts +2 -0
  100. package/src/http/bin/index.ts +2 -0
  101. package/src/http/index.ts +3 -0
  102. package/src/huawei/README.md +1 -0
  103. package/src/huawei/index.ts +2 -0
  104. package/src/huawei/moderation/index.ts +1 -0
  105. package/src/huawei/moderation/moderation.ts +355 -0
  106. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  107. package/src/huawei/obs/index.ts +1 -0
  108. package/src/huawei/obs/obs.ts +42 -0
  109. package/src/index.ts +21 -2
  110. package/src/json/README.md +92 -0
  111. package/src/json/index.ts +1 -0
  112. package/src/json/repair.ts +18 -0
  113. package/src/log/logger.ts +15 -4
  114. package/src/openai/README.md +1 -0
  115. package/src/openai/index.ts +1 -0
  116. package/src/openai/openai.ts +509 -0
  117. package/src/orchestration/README.md +9 -7
  118. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  119. package/src/orchestration/dispatching/index.ts +2 -0
  120. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  121. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  122. package/src/orchestration/dispatching/selector/index.ts +2 -0
  123. package/src/orchestration/index.ts +2 -0
  124. package/src/orchestration/scheduling/index.ts +2 -0
  125. package/src/orchestration/scheduling/scheduler.ts +103 -0
  126. package/src/orchestration/scheduling/task.ts +32 -0
  127. package/src/random/README.md +8 -7
  128. package/src/random/base.ts +66 -0
  129. package/src/random/index.ts +5 -1
  130. package/src/random/random-boolean.ts +40 -0
  131. package/src/random/random-integer.ts +60 -0
  132. package/src/random/random-number.ts +72 -0
  133. package/src/random/random-string.ts +66 -0
  134. package/src/request/README.md +108 -0
  135. package/src/request/fetch/base.ts +108 -0
  136. package/src/request/fetch/browser.ts +280 -0
  137. package/src/request/fetch/general.ts +20 -0
  138. package/src/request/fetch/index.ts +4 -0
  139. package/src/request/fetch/nodejs.ts +280 -0
  140. package/src/request/index.ts +2 -0
  141. package/src/request/request/base.ts +246 -0
  142. package/src/request/request/general.ts +63 -0
  143. package/src/request/request/index.ts +3 -0
  144. package/src/request/request/resource.ts +68 -0
  145. package/src/result/README.md +4 -0
  146. package/src/result/controller.ts +58 -0
  147. package/src/result/either.ts +363 -0
  148. package/src/result/generator.ts +168 -0
  149. package/src/result/index.ts +3 -0
  150. package/src/route/README.md +105 -0
  151. package/src/route/adapter/browser.ts +122 -0
  152. package/src/route/adapter/driver.ts +56 -0
  153. package/src/route/adapter/index.ts +2 -0
  154. package/src/route/index.ts +3 -0
  155. package/src/route/router/index.ts +2 -0
  156. package/src/route/router/route.ts +630 -0
  157. package/src/route/router/router.ts +1641 -0
  158. package/src/route/uri/hash.ts +307 -0
  159. package/src/route/uri/index.ts +7 -0
  160. package/src/route/uri/pathname.ts +376 -0
  161. package/src/route/uri/search.ts +412 -0
  162. package/src/service/README.md +1 -0
  163. package/src/service/index.ts +1 -0
  164. package/src/service/service.ts +110 -0
  165. package/src/socket/README.md +105 -0
  166. package/src/socket/client/index.ts +2 -0
  167. package/src/socket/client/socket-unit.ts +658 -0
  168. package/src/socket/client/socket.ts +203 -0
  169. package/src/socket/common/index.ts +2 -0
  170. package/src/socket/common/socket-unit-common.ts +23 -0
  171. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  172. package/src/socket/index.ts +3 -0
  173. package/src/socket/server/index.ts +3 -0
  174. package/src/socket/server/server.ts +183 -0
  175. package/src/socket/server/socket-unit.ts +448 -0
  176. package/src/socket/server/socket.ts +264 -0
  177. package/src/storage/table.ts +3 -3
  178. package/src/timer/expiration/expiration-manager.ts +3 -3
  179. package/src/timer/expiration/remaining-manager.ts +3 -3
  180. package/src/tube/README.md +99 -0
  181. package/src/tube/helper.ts +137 -0
  182. package/src/tube/index.ts +2 -0
  183. package/src/tube/tube.ts +880 -0
  184. package/src/weixin/README.md +1 -0
  185. package/src/weixin/index.ts +2 -0
  186. package/src/weixin/official-account/authorization.ts +157 -0
  187. package/src/weixin/official-account/index.ts +2 -0
  188. package/src/weixin/official-account/js-api.ts +132 -0
  189. package/src/weixin/open/index.ts +1 -0
  190. package/src/weixin/open/oauth2.ts +131 -0
  191. package/tests/unit/ai/ai.spec.ts +85 -0
  192. package/tests/unit/aio/content.spec.ts +105 -0
  193. package/tests/unit/aio/json.spec.ts +146 -0
  194. package/tests/unit/aio/prompt.spec.ts +111 -0
  195. package/tests/unit/basic/error.spec.ts +16 -4
  196. package/tests/unit/basic/promise.spec.ts +158 -50
  197. package/tests/unit/basic/schedule.spec.ts +74 -0
  198. package/tests/unit/basic/stream.spec.ts +90 -37
  199. package/tests/unit/credential/api-key.spec.ts +36 -0
  200. package/tests/unit/credential/bearer.spec.ts +23 -0
  201. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  202. package/tests/unit/credential/password.spec.ts +40 -0
  203. package/tests/unit/cron/cron.spec.ts +84 -0
  204. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  205. package/tests/unit/event/event-manager.spec.ts +3 -3
  206. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  207. package/tests/unit/exception/error/error.spec.ts +83 -0
  208. package/tests/unit/exception/error/match.spec.ts +81 -0
  209. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  210. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  211. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  212. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  213. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  214. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  215. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  216. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  217. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  218. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  219. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  220. package/tests/unit/http/api/api-server.spec.ts +363 -0
  221. package/tests/unit/http/api/api-test.spec.ts +117 -0
  222. package/tests/unit/http/api/api.spec.ts +121 -0
  223. package/tests/unit/http/api-adapter/node-http.spec.ts +187 -0
  224. package/tests/unit/identifier/uuid.spec.ts +0 -1
  225. package/tests/unit/json/repair.spec.ts +11 -0
  226. package/tests/unit/log/logger.spec.ts +19 -4
  227. package/tests/unit/openai/openai.spec.ts +64 -0
  228. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  229. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  230. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  231. package/tests/unit/random/base.spec.ts +58 -0
  232. package/tests/unit/random/random-boolean.spec.ts +25 -0
  233. package/tests/unit/random/random-integer.spec.ts +32 -0
  234. package/tests/unit/random/random-number.spec.ts +33 -0
  235. package/tests/unit/random/random-string.spec.ts +22 -0
  236. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  237. package/tests/unit/request/fetch/general.spec.ts +43 -0
  238. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  239. package/tests/unit/request/request/base.spec.ts +382 -0
  240. package/tests/unit/request/request/general.spec.ts +160 -0
  241. package/tests/unit/result/controller.spec.ts +82 -0
  242. package/tests/unit/result/either.spec.ts +377 -0
  243. package/tests/unit/result/generator.spec.ts +273 -0
  244. package/tests/unit/route/router/route.spec.ts +430 -0
  245. package/tests/unit/route/router/router.spec.ts +407 -0
  246. package/tests/unit/route/uri/hash.spec.ts +72 -0
  247. package/tests/unit/route/uri/pathname.spec.ts +146 -0
  248. package/tests/unit/route/uri/search.spec.ts +107 -0
  249. package/tests/unit/socket/client.spec.ts +208 -0
  250. package/tests/unit/socket/server.spec.ts +133 -0
  251. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  252. package/tests/unit/tube/helper.spec.ts +139 -0
  253. package/tests/unit/tube/tube.spec.ts +501 -0
  254. package/vite.config.ts +2 -1
  255. package/dist/index.js +0 -50
  256. package/dist/index.js.map +0 -209
  257. package/src/random/string.ts +0 -35
  258. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,67 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ BooleanInputorController,
5
+ FileInputorController,
6
+ FormInputorController,
7
+ MultiSelectInputorController,
8
+ NumberInputorController,
9
+ SelectInputorController,
10
+ TextInputorController,
11
+ boolean as createBooleanInputor,
12
+ file as createFileInputor,
13
+ form as createFormInputor,
14
+ multiSelect as createMultiSelectInputor,
15
+ number as createNumberInputor,
16
+ select as createSelectInputor,
17
+ text as createTextInputor,
18
+ } from "#Source/form/index.ts"
19
+
20
+ test("boolean creates a BooleanInputorController", () => {
21
+ expect(createBooleanInputor({ value: false })).toBeInstanceOf(BooleanInputorController)
22
+ })
23
+
24
+ test("number creates a NumberInputorController", () => {
25
+ expect(createNumberInputor({ value: 1 })).toBeInstanceOf(NumberInputorController)
26
+ })
27
+
28
+ test("text creates a TextInputorController", () => {
29
+ expect(createTextInputor({ value: "alpha" })).toBeInstanceOf(TextInputorController)
30
+ })
31
+
32
+ test("file creates a FileInputorController", () => {
33
+ expect(createFileInputor({ value: undefined })).toBeInstanceOf(FileInputorController)
34
+ })
35
+
36
+ test("select creates a SelectInputorController", () => {
37
+ expect(createSelectInputor({ value: [] })).toBeInstanceOf(SelectInputorController)
38
+ })
39
+
40
+ test("multiSelect creates a MultiSelectInputorController", () => {
41
+ expect(createMultiSelectInputor({ value: [] })).toBeInstanceOf(MultiSelectInputorController)
42
+ })
43
+
44
+ test("form creates a FormInputorController", () => {
45
+ const nameController = createTextInputor({ value: "alpha" })
46
+ const ageController = createNumberInputor({ value: 1 })
47
+
48
+ expect(createFormInputor({
49
+ value: {
50
+ name: "alpha",
51
+ age: 1,
52
+ },
53
+ items: {
54
+ name: nameController,
55
+ age: ageController,
56
+ },
57
+ encodeValue: (schemaDict) => {
58
+ return {
59
+ name: schemaDict.name.value,
60
+ age: schemaDict.age.value,
61
+ }
62
+ },
63
+ decodeValue: (value) => {
64
+ return value
65
+ },
66
+ })).toBeInstanceOf(FormInputorController)
67
+ })
@@ -0,0 +1,34 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { MultiSelectInputorController } from "#Source/form/index.ts"
4
+
5
+ test("MultiSelectInputorController.select toggles options while respecting disabled items and maxSelectCount", async () => {
6
+ const controller = new MultiSelectInputorController<string>({
7
+ id: "multi-select-id",
8
+ value: [
9
+ { name: "a", label: "A", value: "a", isSelected: true },
10
+ { name: "b", label: "B", value: "b", isSelected: false },
11
+ { name: "c", label: "C", value: "c", isSelected: false, isDisabled: true },
12
+ ],
13
+ maxSelectCount: 1,
14
+ })
15
+
16
+ await controller.waitForReady()
17
+ await controller.select("b")
18
+
19
+ expect((await controller.getSchema()).value).toEqual([
20
+ { name: "a", label: "A", value: "a", isSelected: true },
21
+ { name: "b", label: "B", value: "b", isSelected: false },
22
+ { name: "c", label: "C", value: "c", isSelected: false, isDisabled: true },
23
+ ])
24
+
25
+ await controller.select("a")
26
+ await controller.select("b")
27
+ await controller.select("c")
28
+
29
+ expect((await controller.getSchema()).value).toEqual([
30
+ { name: "a", label: "A", value: "a", isSelected: false },
31
+ { name: "b", label: "B", value: "b", isSelected: true },
32
+ { name: "c", label: "C", value: "c", isSelected: false, isDisabled: true },
33
+ ])
34
+ })
@@ -0,0 +1,36 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ NUMBER_INPUTOR_TYPE_NAME,
5
+ NumberInputorController,
6
+ } from "#Source/form/index.ts"
7
+
8
+ test("NumberInputorController.getSchema exposes constraints and updateValue clamps with step rounding", async () => {
9
+ const controller = new NumberInputorController({
10
+ id: "number-id",
11
+ value: 2,
12
+ min: 0,
13
+ max: 10,
14
+ step: 3,
15
+ })
16
+
17
+ await controller.waitForReady()
18
+ await controller.updateValue(11)
19
+
20
+ expect(await controller.getSchema()).toEqual({
21
+ inputorTypeName: NUMBER_INPUTOR_TYPE_NAME,
22
+ id: "number-id",
23
+ name: `number-number-id`,
24
+ placeholder: "",
25
+ value: 9,
26
+ isVoid: false,
27
+ isFocused: false,
28
+ isTouched: false,
29
+ isDirty: true,
30
+ isDisabled: false,
31
+ isDebugMode: false,
32
+ min: 0,
33
+ max: 10,
34
+ step: 3,
35
+ })
36
+ })
@@ -0,0 +1,49 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { SelectInputorController } from "#Source/form/index.ts"
4
+
5
+ test("SelectInputorController.select keeps disabled selected options sticky and supports unselect", async () => {
6
+ const stickyController = new SelectInputorController<string>({
7
+ id: "select-sticky-id",
8
+ value: [
9
+ { name: "a", label: "A", value: "a", isSelected: false },
10
+ { name: "b", label: "B", value: "b", isSelected: true, isDisabled: true },
11
+ { name: "c", label: "C", value: "c", isSelected: false },
12
+ ],
13
+ })
14
+
15
+ await stickyController.waitForReady()
16
+ await stickyController.select("a")
17
+
18
+ expect((await stickyController.getSchema()).value).toEqual([
19
+ { name: "a", label: "A", value: "a", isSelected: false },
20
+ { name: "b", label: "B", value: "b", isSelected: true, isDisabled: true },
21
+ { name: "c", label: "C", value: "c", isSelected: false },
22
+ ])
23
+
24
+ const unselectController = new SelectInputorController<string>({
25
+ id: "select-unselect-id",
26
+ value: [
27
+ { name: "a", label: "A", value: "a", isSelected: false },
28
+ { name: "b", label: "B", value: "b", isSelected: false },
29
+ ],
30
+ enableUnselect: true,
31
+ })
32
+
33
+ await unselectController.waitForReady()
34
+ await unselectController.select("a")
35
+ await unselectController.select("a")
36
+
37
+ expect((await unselectController.getSchema()).value).toEqual([
38
+ { name: "a", label: "A", value: "a", isSelected: false },
39
+ { name: "b", label: "B", value: "b", isSelected: false },
40
+ ])
41
+
42
+ await unselectController.updateIsDisabled(true)
43
+ await unselectController.select("b")
44
+
45
+ expect((await unselectController.getSchema()).value).toEqual([
46
+ { name: "a", label: "A", value: "a", isSelected: false },
47
+ { name: "b", label: "B", value: "b", isSelected: false },
48
+ ])
49
+ })
@@ -0,0 +1,34 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ TEXT_INPUTOR_TYPE_NAME,
5
+ TextInputorController,
6
+ } from "#Source/form/index.ts"
7
+
8
+ test("TextInputorController.getSchema exposes length constraints and updateValue truncates by maxLength", async () => {
9
+ const controller = new TextInputorController({
10
+ id: "text-id",
11
+ value: "hello",
12
+ minLength: 2,
13
+ maxLength: 4,
14
+ })
15
+
16
+ await controller.waitForReady()
17
+ await controller.updateValue("planet")
18
+
19
+ expect(await controller.getSchema()).toEqual({
20
+ inputorTypeName: TEXT_INPUTOR_TYPE_NAME,
21
+ id: "text-id",
22
+ name: `text-text-id`,
23
+ placeholder: "",
24
+ value: "plan",
25
+ isVoid: false,
26
+ isFocused: false,
27
+ isTouched: false,
28
+ isDirty: true,
29
+ isDisabled: false,
30
+ isDebugMode: false,
31
+ minLength: 2,
32
+ maxLength: 4,
33
+ })
34
+ })
@@ -0,0 +1,207 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import type {
4
+ ApiCoreHandler,
5
+ ApiCoreHeaders,
6
+ ApiCoreRequestFiles,
7
+ ApiCoreRequestFormData,
8
+ ApiCoreStream,
9
+ ApiHostStartResult,
10
+ } from "#Source/http/index.ts"
11
+ import { ApiCore, ApiHost } from "#Source/http/index.ts"
12
+
13
+ interface TestApiCoreOptions {
14
+ url: string
15
+ method: string
16
+ headers?: ApiCoreHeaders | undefined
17
+ bodyText?: string | undefined
18
+ formData?: ApiCoreRequestFormData | undefined
19
+ }
20
+
21
+ class TestApiCore extends ApiCore {
22
+ readonly jsonPayloads: unknown[] = []
23
+ readonly textPayloads: unknown[] = []
24
+ errorCallCount = 0
25
+
26
+ private readonly url: string
27
+ private readonly method: string
28
+ private readonly headers: ApiCoreHeaders
29
+ private readonly bodyText: string
30
+ private readonly formData: ApiCoreRequestFormData
31
+
32
+ constructor(options: TestApiCoreOptions) {
33
+ super()
34
+
35
+ this.url = options.url
36
+ this.method = options.method
37
+ this.headers = options.headers ?? {}
38
+ this.bodyText = options.bodyText ?? ""
39
+ this.formData = options.formData ?? []
40
+ }
41
+
42
+ async getRequestHeaders(): Promise<ApiCoreHeaders> {
43
+ return await Promise.resolve(this.headers)
44
+ }
45
+
46
+ async getRequestMethod(): Promise<string> {
47
+ return await Promise.resolve(this.method)
48
+ }
49
+
50
+ getRequestUrl(): string {
51
+ return this.url
52
+ }
53
+
54
+ async getRequestBodyText(): Promise<string> {
55
+ return await Promise.resolve(this.bodyText)
56
+ }
57
+
58
+ async getRequestBodyFormData(): Promise<ApiCoreRequestFormData> {
59
+ return await Promise.resolve(this.formData)
60
+ }
61
+
62
+ async text(data: unknown): Promise<void> {
63
+ this.textPayloads.push(data)
64
+ await Promise.resolve()
65
+ }
66
+
67
+ async json(data: unknown): Promise<void> {
68
+ this.jsonPayloads.push(data)
69
+ await Promise.resolve()
70
+ }
71
+
72
+ async stream(): Promise<ApiCoreStream> {
73
+ return await Promise.resolve({
74
+ write: vi.fn(async () => {
75
+ await Promise.resolve()
76
+ }),
77
+ end: vi.fn(async () => {
78
+ await Promise.resolve()
79
+ }),
80
+ })
81
+ }
82
+
83
+ async error(): Promise<void> {
84
+ this.errorCallCount = this.errorCallCount + 1
85
+ await Promise.resolve()
86
+ }
87
+ }
88
+
89
+ class TestApiHost extends ApiHost<{ id: string }> {
90
+ apiCoreHandler: ApiCoreHandler | undefined
91
+ startCallCount = 0
92
+ closeCallCount = 0
93
+
94
+ constructor() {
95
+ super({ port: 0 })
96
+ }
97
+
98
+ attachApiCoreHandler(apiCoreHandler: ApiCoreHandler): void {
99
+ this.apiCoreHandler = apiCoreHandler
100
+ }
101
+
102
+ async start(): Promise<ApiHostStartResult> {
103
+ this.startCallCount = this.startCallCount + 1
104
+ this.server = { id: "server-1" }
105
+
106
+ return await Promise.resolve({
107
+ urlList: ["http://127.0.0.1:0"],
108
+ })
109
+ }
110
+
111
+ async close(): Promise<void> {
112
+ this.closeCallCount = this.closeCallCount + 1
113
+ this.server = undefined
114
+ await Promise.resolve()
115
+ }
116
+ }
117
+
118
+ test("ApiCore.getRequestHeader returns lowercase header values", async () => {
119
+ const apiCore = new TestApiCore({
120
+ url: "http://127.0.0.1/users?page=2",
121
+ method: "GET",
122
+ headers: {
123
+ "x-trace-id": ["trace-1"],
124
+ },
125
+ })
126
+
127
+ await expect(apiCore.getRequestHeader("X-Trace-Id")).resolves.toEqual(["trace-1"])
128
+ })
129
+
130
+ test("ApiCore.getRequestPath extracts the pathname from the request URL", async () => {
131
+ const apiCore = new TestApiCore({
132
+ url: "http://127.0.0.1/users?page=2",
133
+ method: "GET",
134
+ })
135
+
136
+ await expect(apiCore.getRequestPath()).resolves.toBe("/users")
137
+ })
138
+
139
+ test("ApiCore.getRequestSearch extracts the search string from the request URL", async () => {
140
+ const apiCore = new TestApiCore({
141
+ url: "http://127.0.0.1/users?page=2",
142
+ method: "GET",
143
+ })
144
+
145
+ await expect(apiCore.getRequestSearch()).resolves.toBe("?page=2")
146
+ })
147
+
148
+ test("ApiCore.getRequestBodyJson parses JSON bodies and treats blank bodies as empty objects", async () => {
149
+ const jsonApiCore = new TestApiCore({
150
+ url: "http://127.0.0.1/users",
151
+ method: "POST",
152
+ bodyText: '{"name":"Mobius"}',
153
+ })
154
+ const emptyApiCore = new TestApiCore({
155
+ url: "http://127.0.0.1/users",
156
+ method: "POST",
157
+ bodyText: " ",
158
+ })
159
+
160
+ await expect(jsonApiCore.getRequestBodyJson()).resolves.toEqual({
161
+ name: "Mobius",
162
+ })
163
+ await expect(emptyApiCore.getRequestBodyJson()).resolves.toEqual({})
164
+ })
165
+
166
+ test("ApiCore.getRequestFiles returns only file entries from form data", async () => {
167
+ const file = new File(["avatar"], "avatar.txt", { type: "text/plain" })
168
+ const apiCore = new TestApiCore({
169
+ url: "http://127.0.0.1/upload",
170
+ method: "POST",
171
+ formData: [
172
+ { name: "name", value: "Mobius" },
173
+ { name: "avatar", value: file },
174
+ ],
175
+ })
176
+
177
+ const files = await apiCore.getRequestFiles()
178
+
179
+ expect(files).toEqual<ApiCoreRequestFiles>([
180
+ {
181
+ name: "avatar",
182
+ value: file,
183
+ },
184
+ ])
185
+ })
186
+
187
+ test("ApiHost.safeGetServer throws before start and returns the running server after start", async () => {
188
+ const apiHost = new TestApiHost()
189
+
190
+ expect(() => apiHost.safeGetServer()).toThrow("Server is not running.")
191
+
192
+ await apiHost.start()
193
+
194
+ expect(apiHost.safeGetServer()).toEqual({ id: "server-1" })
195
+ })
196
+
197
+ test("ApiHost.isRunning reflects whether a server is attached", async () => {
198
+ const apiHost = new TestApiHost()
199
+
200
+ expect(apiHost.isRunning()).toBe(false)
201
+
202
+ await apiHost.start()
203
+ expect(apiHost.isRunning()).toBe(true)
204
+
205
+ await apiHost.close()
206
+ expect(apiHost.isRunning()).toBe(false)
207
+ })
@@ -0,0 +1,120 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
4
+ import { createApiSchema } from "#Source/http/index.ts"
5
+
6
+ const isRecord = (value: unknown): value is Record<string, unknown> => {
7
+ return typeof value === "object" && value !== null
8
+ }
9
+
10
+ const createSchema = <Output>(
11
+ validate: (value: unknown) => StandardSchemaV1.Result<Output>,
12
+ ): StandardSchemaV1<unknown, Output> => {
13
+ return {
14
+ "~standard": {
15
+ version: 1 as const,
16
+ vendor: "mobius-http-test",
17
+ validate,
18
+ },
19
+ }
20
+ }
21
+
22
+ const inputQuerySchema = createSchema<{ page: string }>((value) => {
23
+ if (isRecord(value) && typeof value["page"] === "string") {
24
+ return {
25
+ value: {
26
+ page: value["page"],
27
+ },
28
+ }
29
+ }
30
+
31
+ return {
32
+ issues: [{ message: "page is required" }],
33
+ }
34
+ })
35
+
36
+ const inputBodySchema = createSchema<{ name: string }>((value) => {
37
+ if (isRecord(value) && typeof value["name"] === "string") {
38
+ return {
39
+ value: {
40
+ name: value["name"],
41
+ },
42
+ }
43
+ }
44
+
45
+ return {
46
+ issues: [{ message: "name is required" }],
47
+ }
48
+ })
49
+
50
+ const outputSuccessSchema = createSchema<{ status: "success" }>((value) => {
51
+ if (typeof value === "object" && value !== null && (value as { status?: unknown }).status === "success") {
52
+ return {
53
+ value: {
54
+ status: "success",
55
+ },
56
+ }
57
+ }
58
+
59
+ return {
60
+ issues: [{ message: "status must be success" }],
61
+ }
62
+ })
63
+
64
+ const outputErrorSchema = createSchema<{ status: "error" }>((value) => {
65
+ if (typeof value === "object" && value !== null && (value as { status?: unknown }).status === "error") {
66
+ return {
67
+ value: {
68
+ status: "error",
69
+ },
70
+ }
71
+ }
72
+
73
+ return {
74
+ issues: [{ message: "status must be error" }],
75
+ }
76
+ })
77
+
78
+ const apiSchema = createApiSchema({
79
+ path: "/users",
80
+ method: "post",
81
+ inputQuerySchema,
82
+ inputBodySchema,
83
+ outputSuccessSchema,
84
+ outputErrorSchema,
85
+ })
86
+
87
+ test("ApiSchema.getApiSchemaOptions returns the configured schema options", () => {
88
+ expect(apiSchema.getApiSchemaOptions()).toEqual({
89
+ path: "/users",
90
+ method: "post",
91
+ inputQuerySchema,
92
+ inputBodySchema,
93
+ outputSuccessSchema,
94
+ outputErrorSchema,
95
+ })
96
+ })
97
+
98
+ test("ApiSchema.getPath returns the configured path", () => {
99
+ expect(apiSchema.getPath()).toBe("/users")
100
+ })
101
+
102
+ test("ApiSchema.getMethod returns the configured method", () => {
103
+ expect(apiSchema.getMethod()).toBe("post")
104
+ })
105
+
106
+ test("ApiSchema.getInputQuerySchema returns the configured query schema", () => {
107
+ expect(apiSchema.getInputQuerySchema()).toBe(inputQuerySchema)
108
+ })
109
+
110
+ test("ApiSchema.getInputBodySchema returns the configured body schema", () => {
111
+ expect(apiSchema.getInputBodySchema()).toBe(inputBodySchema)
112
+ })
113
+
114
+ test("ApiSchema.getOutputSuccessSchema returns the configured success schema", () => {
115
+ expect(apiSchema.getOutputSuccessSchema()).toBe(outputSuccessSchema)
116
+ })
117
+
118
+ test("ApiSchema.getOutputErrorSchema returns the configured error schema", () => {
119
+ expect(apiSchema.getOutputErrorSchema()).toBe(outputErrorSchema)
120
+ })