@shortlink-org/portolan 0.1.0 → 0.2.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 (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +27 -10
  36. package/scripts/local-api.test.mjs +85 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +24 -3
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +17 -9
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -1,609 +0,0 @@
1
- package main
2
-
3
- import (
4
- "go/ast"
5
- "os"
6
- "path/filepath"
7
- "strings"
8
- "testing"
9
-
10
- "github.com/shortlink-org/portolan/catalog"
11
- "github.com/shortlink-org/portolan/plugin"
12
- )
13
-
14
- // The flows themselves are checked where every other fragment is: the committed
15
- // examples/auth/portolan/domain.json, which `npm run gen:check` holds to the
16
- // source. What is worth pinning here is the reading that gets there - the order
17
- // calls come out in, what a signature says came back, and the two places where
18
- // a wrong answer would be a plausible one.
19
-
20
- const useCaseSource = `package end_after_credential_change
21
-
22
- import (
23
- "context"
24
-
25
- "github.com/example/auth/internal/domain/session"
26
- "github.com/example/auth/internal/domain/session/event"
27
- )
28
-
29
- type UseCase struct {
30
- repo session.Repository
31
- now func() time.Time
32
- }
33
-
34
- func (uc *UseCase) Handle(ctx context.Context, in dto.Input) error {
35
- sessions, err := uc.repo.ByUserID(ctx, in.UserID)
36
- if err != nil {
37
- return err
38
- }
39
-
40
- for _, doomed := range change.Ends(sessions, uc.now()) {
41
- if err := uc.end(ctx, doomed.ID); err != nil {
42
- return err
43
- }
44
- }
45
- return nil
46
- }
47
-
48
- func (uc *UseCase) end(ctx context.Context, id string) error {
49
- for attempt := range retries {
50
- current, err := uc.repo.ByID(ctx, id)
51
- if err != nil {
52
- return err
53
- }
54
-
55
- ev, ended := current.Revoke(event.ReasonPasswordChanged, uc.now())
56
- if !ended {
57
- return nil
58
- }
59
-
60
- return uc.repo.Save(ctx, current, ev)
61
- }
62
- return nil
63
- }
64
- `
65
-
66
- func handleOf(t *testing.T, source, name string) (*pkg, *ast.FuncDecl) {
67
- t.Helper()
68
-
69
- pkg, err := parseSource("usecase.go", source)
70
- if err != nil {
71
- t.Fatal(err)
72
- }
73
-
74
- fn := pkg.methods("UseCase")[name]
75
- if fn == nil {
76
- t.Fatalf("no method %s on UseCase", name)
77
- }
78
-
79
- return pkg, fn
80
- }
81
-
82
- // The order of the calls is the order of the flow, and it has to be the order
83
- // they are written in - including a call in an `if` initialiser, which runs
84
- // before the block it guards.
85
- func TestCallSitesAreInSourceOrder(t *testing.T) {
86
- _, handle := handleOf(t, useCaseSource, "Handle")
87
-
88
- var got []string
89
- for _, site := range callSites(handle) {
90
- if selector, ok := site.call.Fun.(*ast.SelectorExpr); ok {
91
- got = append(got, selector.Sel.Name)
92
- }
93
- }
94
-
95
- want := []string{"ByUserID", "Ends", "now", "end"}
96
- if len(got) != len(want) {
97
- t.Fatalf("calls = %v, want %v", got, want)
98
- }
99
- for i := range want {
100
- if got[i] != want[i] {
101
- t.Errorf("call %d = %q, want %q", i, got[i], want[i])
102
- }
103
- }
104
- }
105
-
106
- // A step inside a loop says so. The frame is deliberately not built - the
107
- // condition is written for a compiler - but a reader who is not told that Save
108
- // runs once per session has been told something false. The note travels from
109
- // the loop in Handle, through the helper it calls, onto the step.
110
- func TestAStepInsideALoopSaysSo(t *testing.T) {
111
- d, _ := walkSource(t, useCaseSource, nil)
112
-
113
- save := stepLabelled(t, d.steps, "Save")
114
- want := "inside a loop over `change.Ends(sessions, uc.now())`, inside a loop over `retries`."
115
- if save.Note != want {
116
- t.Fatalf("Save note = %q, want %q", save.Note, want)
117
- }
118
- }
119
-
120
- // walkSource reads Handle of an in-memory use case the way extractFlows would,
121
- // with the store on its own lane and the given peers.
122
- func walkSource(t *testing.T, source string, peers map[string]string) (*flowDraft, *flowReader) {
123
- t.Helper()
124
-
125
- pkg, handle := handleOf(t, source, "Handle")
126
- r := newFlowReader(t.TempDir(), flowOptions{context: "auth", svcID: "auth.auth", service: "auth", store: "pg", peers: peers}, &plugin.Builder{})
127
- r.module = "github.com/example/auth"
128
-
129
- d := newDraft()
130
- d.lane(r.serviceLane())
131
- r.walkBody(d, &scope{
132
- pkg: pkg,
133
- key: "session/login",
134
- fields: useCaseFields(pkg),
135
- imports: importsOf(pkg),
136
- vars: map[string]domainRef{},
137
- recv: receiverIdent(handle),
138
- recvType: "UseCase",
139
- }, handle, 0)
140
-
141
- return d, r
142
- }
143
-
144
- func stepLabelled(t *testing.T, nodes catalog.FlowNodes, label string) *catalog.Step {
145
- t.Helper()
146
- for _, node := range nodes {
147
- switch x := node.(type) {
148
- case *catalog.Step:
149
- if x.Label == label {
150
- return x
151
- }
152
- case *catalog.Alt:
153
- for _, branch := range x.Branches {
154
- for _, inner := range branch.Steps {
155
- if step, ok := inner.(*catalog.Step); ok && step.Label == label {
156
- return step
157
- }
158
- }
159
- }
160
- }
161
- }
162
- t.Fatalf("no step labelled %s", label)
163
-
164
- return nil
165
- }
166
-
167
- const branchingSource = `package login
168
-
169
- import (
170
- "context"
171
-
172
- "github.com/example/auth/internal/domain/session"
173
- )
174
-
175
- type UseCase struct {
176
- repo session.Repository
177
- }
178
-
179
- func (uc *UseCase) Handle(ctx context.Context, in dto.Input) error {
180
- u, err := uc.repo.ByID(ctx, in.ID)
181
- if err != nil {
182
- return err
183
- }
184
-
185
- if in.Force {
186
- if err := uc.repo.Save(ctx, u); err != nil {
187
- return err
188
- }
189
- return nil
190
- } else if in.Dry {
191
- uc.repo.ByToken(ctx, in.Token)
192
- }
193
-
194
- return uc.repo.Save(ctx, u)
195
- }
196
- `
197
-
198
- // An if with a hop inside it is a choice the reader has to see, and an arm
199
- // that returns is one the flow does not continue past. The if with nothing in
200
- // it - the error check - is not drawn at all.
201
- func TestAnIfWithAHopInsideItIsAnAlt(t *testing.T) {
202
- d, _ := walkSource(t, branchingSource, nil)
203
-
204
- if len(d.steps) != 3 {
205
- t.Fatalf("top-level nodes = %d, want 3 (ByID, alt, Save): %s", len(d.steps), dump(d.steps))
206
- }
207
- alt, ok := d.steps[1].(*catalog.Alt)
208
- if !ok {
209
- t.Fatalf("second node is %T, want alt", d.steps[1])
210
- }
211
- if alt.ID != "alt4" {
212
- t.Errorf("alt id = %q; ids number every node, frames included", alt.ID)
213
- }
214
-
215
- type arm struct {
216
- title string
217
- steps int
218
- terminal bool
219
- }
220
- var got []arm
221
- for _, b := range alt.Branches {
222
- got = append(got, arm{b.Title, len(b.Steps), b.Terminal})
223
- }
224
- want := []arm{{"in.Force", 1, true}, {"in.Dry", 1, false}, {"otherwise", 0, false}}
225
- if len(got) != len(want) {
226
- t.Fatalf("arms = %+v, want %+v", got, want)
227
- }
228
- for i := range want {
229
- if got[i] != want[i] {
230
- t.Errorf("arm %d = %+v, want %+v", i, got[i], want[i])
231
- }
232
- }
233
-
234
- last, ok := d.steps[2].(*catalog.Step)
235
- if !ok || last.Label != "Save" || last.ID != "s5" {
236
- t.Errorf("last node = %+v, want the Save after the alt as s5", d.steps[2])
237
- }
238
- }
239
-
240
- // Two arms that both end the flow are not a choice about what comes next, and
241
- // the catalog refuses an alt whose every arm leaves. The arms stay drawn; the
242
- // mark comes off.
243
- func TestArmsThatAllLeaveLoseTheMark(t *testing.T) {
244
- const source = `package login
245
-
246
- import "github.com/example/auth/internal/domain/session"
247
-
248
- type UseCase struct {
249
- repo session.Repository
250
- }
251
-
252
- func (uc *UseCase) Handle(ctx context.Context, in dto.Input) error {
253
- if in.A {
254
- return uc.repo.Save(ctx, nil)
255
- } else {
256
- return uc.repo.ByID(ctx, in.ID)
257
- }
258
- }
259
- `
260
- d, _ := walkSource(t, source, nil)
261
- alt, ok := d.steps[0].(*catalog.Alt)
262
- if !ok {
263
- t.Fatalf("node = %T", d.steps[0])
264
- }
265
- for _, branch := range alt.Branches {
266
- if branch.Terminal {
267
- t.Errorf("arm %q is terminal", branch.Title)
268
- }
269
- }
270
- }
271
-
272
- func dump(nodes catalog.FlowNodes) string {
273
- out := ""
274
- for _, node := range nodes {
275
- switch x := node.(type) {
276
- case *catalog.Step:
277
- out += x.ID + ":" + x.Label + " "
278
- case *catalog.Alt:
279
- out += x.ID + "{" + dump(altSteps(x)) + "} "
280
- }
281
- }
282
-
283
- return out
284
- }
285
-
286
- func altSteps(alt *catalog.Alt) catalog.FlowNodes {
287
- var out catalog.FlowNodes
288
- for _, b := range alt.Branches {
289
- out = append(out, b.Steps...)
290
- }
291
-
292
- return out
293
- }
294
-
295
- // The loops enclosing a call travel into the body it reaches: a use case run
296
- // once per session does everything it does once per session, and the fact
297
- // belongs to its steps rather than to the call that got there.
298
- func TestLoopsTravelIntoTheBodyTheyReach(t *testing.T) {
299
- d := &flowDraft{seen: map[string]bool{}}
300
- d.enter("inside a loop over `sessions`")
301
- d.enter("inside a loop over `retries`")
302
- d.add(catalog.Step{Label: "Save"})
303
- d.leave()
304
- d.leave()
305
-
306
- step, ok := d.steps[0].(*catalog.Step)
307
- if !ok {
308
- t.Fatalf("step is %T", d.steps[0])
309
- }
310
- want := "inside a loop over `sessions`, inside a loop over `retries`."
311
- if step.Note != want {
312
- t.Errorf("note = %q, want %q", step.Note, want)
313
- }
314
- }
315
-
316
- // What came back is read off the signature, by position: a name bound to the
317
- // second result of `Start` holds an event, and one bound to the first holds a
318
- // session. Reading the body instead would be reading an implementation to find
319
- // out what a declaration already says.
320
- func TestResultsAreReadOffTheSignature(t *testing.T) {
321
- const domainSource = `package session
322
-
323
- import "github.com/example/auth/internal/domain/session/event"
324
-
325
- func Start(id, userID string, now time.Time) (*Session, event.SessionStarted, error) {
326
- return nil, event.SessionStarted{}, nil
327
- }
328
- `
329
-
330
- pkg, err := parseSource("session.go", domainSource)
331
- if err != nil {
332
- t.Fatal(err)
333
- }
334
-
335
- var start *ast.FuncDecl
336
- for _, decl := range pkg.files[0].Decls {
337
- if fn, ok := decl.(*ast.FuncDecl); ok && fn.Name.Name == "Start" {
338
- start = fn
339
- }
340
- }
341
- if start == nil {
342
- t.Fatal("no Start")
343
- }
344
-
345
- r := &flowReader{}
346
- got := r.resultRefs(pkg, "session", start.Type)
347
-
348
- if len(got) != 3 {
349
- t.Fatalf("results = %d, want 3 - a result this cannot place has to keep its position", len(got))
350
- }
351
- if got[0].name != "Session" || got[0].event {
352
- t.Errorf("first result = %+v", got[0])
353
- }
354
- if got[1].name != "SessionStarted" || !got[1].event || got[1].aggregate != "session" {
355
- t.Errorf("second result = %+v", got[1])
356
- }
357
- if got[2].name != "" {
358
- t.Errorf("error was read as %+v", got[2])
359
- }
360
- }
361
-
362
- // The binding lives in assembly because neither side of it can: login declares
363
- // the port so that it does not import the package that satisfies it.
364
- func TestPortBindingsAreReadFromAssembly(t *testing.T) {
365
- root := t.TempDir()
366
- dir := filepath.Join(root, "internal/di/provider")
367
- if err := os.MkdirAll(dir, 0o755); err != nil {
368
- t.Fatal(err)
369
- }
370
-
371
- const provider = `package provider
372
-
373
- import (
374
- "github.com/example/auth/internal/application/session/usecases/login"
375
- "github.com/example/auth/internal/application/user/usecases/authenticate"
376
- )
377
-
378
- func ProvideAuthenticator(uc *authenticate.UseCase) login.Authenticator {
379
- return authenticator{uc: uc}
380
- }
381
-
382
- // Two results is not a binding: the port is not the whole answer.
383
- func ProvideSomethingElse(uc *authenticate.UseCase) (login.Authenticator, error) {
384
- return nil, nil
385
- }
386
- `
387
- if err := os.WriteFile(filepath.Join(dir, "authenticator.go"), []byte(provider), 0o644); err != nil {
388
- t.Fatal(err)
389
- }
390
-
391
- got := portBindings(root)
392
- if want := "user/authenticate"; got["session/login.Authenticator"] != want {
393
- t.Errorf("binding = %q, want %q", got["session/login.Authenticator"], want)
394
- }
395
- if len(got) != 1 {
396
- t.Errorf("bindings = %v, want exactly the one that binds two use cases", got)
397
- }
398
- }
399
-
400
- func TestDirectWireBindingsMarkConsumerOwnedAdapterPorts(t *testing.T) {
401
- root := t.TempDir()
402
- dir := filepath.Join(root, "internal/user/di")
403
- if err := os.MkdirAll(dir, 0o755); err != nil {
404
- t.Fatal(err)
405
- }
406
-
407
- const assembly = `package di
408
-
409
- import (
410
- "github.com/google/wire"
411
- "github.com/example/auth/internal/user/application/register"
412
- "github.com/example/auth/internal/user/infrastructure/password"
413
- )
414
-
415
- var Set = wire.NewSet(
416
- password.NewHasher,
417
- wire.Bind(new(register.PasswordHasher), new(*password.Hasher)),
418
- )
419
- `
420
- if err := os.WriteFile(filepath.Join(dir, "infrastructure.go"), []byte(assembly), 0o644); err != nil {
421
- t.Fatal(err)
422
- }
423
-
424
- got := wireBoundPorts(root)
425
- if !got["user/register.PasswordHasher"] {
426
- t.Errorf("bindings = %v, want user/register.PasswordHasher", got)
427
- }
428
- }
429
-
430
- // A port filled by several use cases, one per method, is bound per method:
431
- // the signature names the use cases, the adapter's methods say which is which.
432
- func TestAPortOverSeveralUseCasesIsBoundPerMethod(t *testing.T) {
433
- root := t.TempDir()
434
- dir := filepath.Join(root, "internal/di/provider")
435
- if err := os.MkdirAll(dir, 0o755); err != nil {
436
- t.Fatal(err)
437
- }
438
-
439
- const provider = `package provider
440
-
441
- import (
442
- "context"
443
-
444
- "github.com/example/auth/internal/application/lockout/usecases/check"
445
- checkdto "github.com/example/auth/internal/application/lockout/usecases/check/dto"
446
- "github.com/example/auth/internal/application/lockout/usecases/record_failure"
447
- failuredto "github.com/example/auth/internal/application/lockout/usecases/record_failure/dto"
448
- "github.com/example/auth/internal/application/user/usecases/authenticate"
449
- )
450
-
451
- func ProvideLockout(check *check.UseCase, failed *record_failure.UseCase) authenticate.Lockout {
452
- return lockoutAdapter{check: check, failed: failed}
453
- }
454
-
455
- type lockoutAdapter struct {
456
- check *check.UseCase
457
- failed *record_failure.UseCase
458
- }
459
-
460
- func (l lockoutAdapter) Allowed(ctx context.Context, id string) (bool, error) {
461
- out, err := l.check.Handle(ctx, checkdto.Input{UserID: id})
462
- if err != nil {
463
- return false, err
464
- }
465
- return out.Allowed, nil
466
- }
467
-
468
- func (l lockoutAdapter) Failed(ctx context.Context, id string) error {
469
- return l.failed.Handle(ctx, failuredto.Input{UserID: id})
470
- }
471
- `
472
- if err := os.WriteFile(filepath.Join(dir, "lockout.go"), []byte(provider), 0o644); err != nil {
473
- t.Fatal(err)
474
- }
475
-
476
- got := portBindings(root)
477
- want := map[string]string{
478
- "user/authenticate.Lockout.Allowed": "lockout/check",
479
- "user/authenticate.Lockout.Failed": "lockout/record_failure",
480
- }
481
- for key, useCase := range want {
482
- if got[key] != useCase {
483
- t.Errorf("%s = %q, want %q", key, got[key], useCase)
484
- }
485
- }
486
- if _, bound := got["user/authenticate.Lockout"]; bound {
487
- t.Errorf("bindings = %v, want no port-level binding when the methods say which use case is which", got)
488
- }
489
- }
490
-
491
- // An import of the domain is a lane; the event package under it is a fact.
492
- // Nothing else is either, and a reader that guessed would put a helper package
493
- // on a diagram as if it were a database.
494
- func TestPackagesAreToldApart(t *testing.T) {
495
- imports := map[string]string{
496
- "session": "github.com/example/auth/internal/domain/session",
497
- "event": "github.com/example/auth/internal/domain/session/event",
498
- "uc": "github.com/example/auth/internal/application/user/usecases/authenticate",
499
- "time": "time",
500
- }
501
-
502
- if aggregate, name, ok := domainSelector("session.Repository", imports); !ok || aggregate != "session" || name != "Repository" {
503
- t.Errorf("domainSelector = %q %q %v", aggregate, name, ok)
504
- }
505
- if _, _, ok := domainSelector("time.Time", imports); ok {
506
- t.Error("time.Time was read as a domain port")
507
- }
508
- if aggregate, ok := eventPackage(imports["event"]); !ok || aggregate != "session" {
509
- t.Errorf("eventPackage = %q %v", aggregate, ok)
510
- }
511
- if _, ok := eventPackage(imports["session"]); ok {
512
- t.Error("the aggregate package was read as its event package")
513
- }
514
- if key, ok := useCaseSelector("*uc.UseCase", imports); !ok || key != "user/authenticate" {
515
- t.Errorf("useCaseSelector = %q %v", key, ok)
516
- }
517
- }
518
-
519
- func TestSentence(t *testing.T) {
520
- if got := sentence("revoke-sessions-on-password-change"); got != "Revoke sessions on password change" {
521
- t.Errorf("sentence = %q", got)
522
- }
523
- }
524
-
525
- // A switch is a choice too. Its arms are titled by what the case says, with
526
- // the subject in front; default is otherwise; an arm that returns is
527
- // terminal; and, like an if, nothing is drawn unless some arm has a hop.
528
- func TestASwitchWithAHopIsAnAlt(t *testing.T) {
529
- const source = `package login
530
-
531
- import (
532
- "context"
533
-
534
- "github.com/example/auth/internal/domain/session"
535
- )
536
-
537
- type UseCase struct {
538
- repo session.Repository
539
- }
540
-
541
- func (uc *UseCase) Handle(ctx context.Context, in dto.Input) error {
542
- u, err := uc.repo.ByID(ctx, in.ID)
543
- if err != nil {
544
- return err
545
- }
546
- switch in.Mode {
547
- case "soft", "gentle":
548
- return uc.repo.Save(ctx, u)
549
- case "hard":
550
- uc.repo.ByToken(ctx, in.Token)
551
- default:
552
- return nil
553
- }
554
- switch {
555
- case in.A > 1:
556
- return nil
557
- case in.B:
558
- return nil
559
- }
560
- return nil
561
- }
562
- `
563
- d, _ := walkSource(t, source, nil)
564
- if got := dump(d.steps); got != "s1:ByID alt4{s2:Save s3:ByToken } " {
565
- t.Fatalf("nodes = %q; the second switch has no hop and is not drawn", got)
566
- }
567
- alt := d.steps[1].(*catalog.Alt)
568
- var titles []string
569
- for _, b := range alt.Branches {
570
- titles = append(titles, b.Title)
571
- }
572
- if strings.Join(titles, "|") != `in.Mode is "soft", "gentle"|in.Mode is "hard"|otherwise` {
573
- t.Errorf("titles = %q", titles)
574
- }
575
- if !alt.Branches[0].Terminal || alt.Branches[1].Terminal || !alt.Branches[2].Terminal {
576
- t.Errorf("terminal = %v %v %v", alt.Branches[0].Terminal, alt.Branches[1].Terminal, alt.Branches[2].Terminal)
577
- }
578
- }
579
-
580
- // A type switch asks about one value, and every arm says what it turned out
581
- // to be.
582
- func TestATypeSwitchNamesWhatItAsksAbout(t *testing.T) {
583
- const source = `package login
584
-
585
- import "github.com/example/auth/internal/domain/session"
586
-
587
- type UseCase struct {
588
- repo session.Repository
589
- }
590
-
591
- func (uc *UseCase) Handle(ctx context.Context, in dto.Input) error {
592
- switch e := in.Event.(type) {
593
- case *Started, *Renewed:
594
- return uc.repo.Save(ctx, nil)
595
- case *Ended:
596
- uc.repo.ByID(ctx, e.ID)
597
- }
598
- return nil
599
- }
600
- `
601
- d, _ := walkSource(t, source, nil)
602
- alt, ok := d.steps[0].(*catalog.Alt)
603
- if !ok {
604
- t.Fatalf("node = %T", d.steps[0])
605
- }
606
- if alt.Branches[0].Title != "in.Event is *Started, *Renewed" || alt.Branches[1].Title != "in.Event is *Ended" || alt.Branches[2].Title != "otherwise" {
607
- t.Errorf("titles = %q %q %q", alt.Branches[0].Title, alt.Branches[1].Title, alt.Branches[2].Title)
608
- }
609
- }