@shortlink-org/portolan 0.1.0 → 0.2.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 (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 +22 -4
  36. package/scripts/local-api.test.mjs +84 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +6 -2
  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 +12 -8
  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,1228 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "fmt"
6
- "go/ast"
7
- "go/token"
8
- "path"
9
- "reflect"
10
- "sort"
11
- "strconv"
12
- "strings"
13
-
14
- "github.com/shortlink-org/portolan/catalog"
15
- "github.com/shortlink-org/portolan/internal/goscan"
16
- "github.com/shortlink-org/portolan/plugin"
17
- )
18
-
19
- const watermillMessageImport = "github.com/ThreeDotsLabs/watermill/message"
20
-
21
- type goType struct {
22
- key string
23
- name string
24
- doc string
25
- fields []string
26
- at goscan.Source
27
- }
28
-
29
- type configField struct {
30
- value string
31
- env string
32
- }
33
-
34
- type function struct {
35
- key string
36
- name string
37
- receiver string
38
- file *goscan.File
39
- decl *ast.FuncDecl
40
- params []string
41
- types map[string]string
42
- results []string
43
- }
44
-
45
- type topic struct {
46
- address string
47
- env string
48
- expr string
49
- param int
50
- at goscan.Source
51
- }
52
-
53
- func (t topic) valid() bool { return t.address != "" || t.param >= 0 }
54
-
55
- type publication struct {
56
- topic topic
57
- payload string
58
- at goscan.Source
59
- conditions []string
60
- terminal bool
61
- }
62
-
63
- type handler struct {
64
- name string
65
- input topic
66
- inputPayload string
67
- consumerGroup string
68
- entrypoint string
69
- publications []publication
70
- at goscan.Source
71
- }
72
-
73
- type cqrsHandler struct {
74
- name string
75
- payload string
76
- at goscan.Source
77
- }
78
-
79
- type channelState struct {
80
- channel catalog.Channel
81
- seen map[string]bool
82
- }
83
-
84
- type analysisState struct {
85
- types map[string]string
86
- strings map[string]topic
87
- bytesPayload map[string]string
88
- messages map[string]string
89
- topicParams map[string]int
90
- }
91
-
92
- type analysis struct {
93
- input string
94
- publications []publication
95
- }
96
-
97
- type pathState struct {
98
- conditions []string
99
- publications []int
100
- }
101
-
102
- // scanner is the tree, and what Watermill leaves in it: the types and
103
- // functions, what each function publishes, and the handlers registered on a
104
- // router or a CQRS processor.
105
- type scanner struct {
106
- *goscan.Tree
107
- types map[string]*goType
108
- configFields map[string]configField
109
- functions map[string]*function
110
- methodResults map[string]string
111
- methodChoices map[string]map[string]bool
112
- summaries map[string][]publication
113
- handlers []handler
114
- transport string
115
- }
116
-
117
- func extract(in plugin.Input, opts Options) (plugin.Response, error) {
118
- b := &plugin.Builder{}
119
- tree, err := goscan.Read(in.Root)
120
- if err != nil {
121
- return plugin.Response{}, err
122
- }
123
- s := &scanner{
124
- Tree: tree,
125
- types: map[string]*goType{},
126
- configFields: map[string]configField{},
127
- functions: map[string]*function{},
128
- methodResults: map[string]string{},
129
- methodChoices: map[string]map[string]bool{},
130
- summaries: map[string][]publication{},
131
- }
132
- for _, file := range s.Files {
133
- for _, imported := range file.Imports {
134
- s.detectTransport(imported)
135
- }
136
- }
137
- s.index(b)
138
-
139
- serviceID := opts.Context + "." + opts.Service
140
- channels, flows := s.catalog(serviceID, opts.Context)
141
- if len(s.handlers) == 0 {
142
- b.Warn(in.Root, "no Watermill Router.AddHandler or AddNoPublisherHandler declaration was found")
143
- }
144
-
145
- fragment := catalog.Catalog{
146
- GeneratedAt: in.GeneratedAt,
147
- Commit: in.Commit,
148
- Contexts: []catalog.BoundedContext{{
149
- ID: opts.Context,
150
- Slug: opts.Context,
151
- Services: []catalog.Service{{
152
- ID: serviceID,
153
- Slug: opts.Service,
154
- Provides: []catalog.RpcService{},
155
- Consumes: []catalog.RpcCall{},
156
- Aggregates: []catalog.Aggregate{},
157
- Channels: channels,
158
- }},
159
- }},
160
- Defs: map[string]catalog.TypeDef{},
161
- Flows: flows,
162
- Adrs: []catalog.Adr{},
163
- }
164
- encoded, err := json.MarshalIndent(fragment, "", " ")
165
- if err != nil {
166
- return plugin.Response{}, err
167
- }
168
- b.File(goscan.FirstNonEmpty(opts.Out, "watermill.json"), string(encoded)+"\n")
169
- return b.Response(), nil
170
- }
171
-
172
- func (s *scanner) detectTransport(imported string) {
173
- switch {
174
- case strings.Contains(imported, "watermill-kafka"):
175
- s.transport = "Kafka"
176
- case strings.Contains(imported, "watermill-nats") && s.transport == "":
177
- s.transport = "NATS"
178
- case strings.Contains(imported, "watermill-amqp") && s.transport == "":
179
- s.transport = "AMQP"
180
- case strings.Contains(imported, "watermill-sql") && s.transport == "":
181
- s.transport = "SQL"
182
- }
183
- }
184
-
185
- func (s *scanner) index(b *plugin.Builder) {
186
- for _, file := range s.Files {
187
- s.indexDeclarations(file)
188
- }
189
- s.finalizeMethodResults()
190
- for _, fn := range s.functions {
191
- if fn.decl.Body == nil {
192
- continue
193
- }
194
- state := s.stateFor(fn)
195
- s.summaries[fn.key] = s.analyzeBlock(fn.decl.Body, fn.file, state, false).publications
196
- }
197
- for _, fn := range s.functions {
198
- if fn.decl.Body != nil {
199
- s.indexHandlers(fn, b)
200
- s.indexCQRS(fn, b)
201
- }
202
- }
203
- }
204
-
205
- func (s *scanner) indexDeclarations(file *goscan.File) {
206
- for _, decl := range file.Node.Decls {
207
- switch item := decl.(type) {
208
- case *ast.GenDecl:
209
- if item.Tok != token.TYPE {
210
- continue
211
- }
212
- for _, raw := range item.Specs {
213
- s.indexType(file, item, raw.(*ast.TypeSpec))
214
- }
215
- case *ast.FuncDecl:
216
- s.indexFunction(file, item)
217
- }
218
- }
219
- }
220
-
221
- func (s *scanner) indexType(file *goscan.File, gen *ast.GenDecl, spec *ast.TypeSpec) {
222
- key := file.Pkg + "." + spec.Name.Name
223
- switch body := spec.Type.(type) {
224
- case *ast.StructType:
225
- doc := ""
226
- if spec.Doc != nil {
227
- doc = strings.TrimSpace(spec.Doc.Text())
228
- } else if gen.Doc != nil {
229
- doc = strings.TrimSpace(gen.Doc.Text())
230
- }
231
- s.types[key] = &goType{key: key, name: spec.Name.Name, doc: doc, fields: s.FieldsOf(body), at: s.At(spec.Pos())}
232
- for _, field := range body.Fields.List {
233
- if field.Tag == nil {
234
- continue
235
- }
236
- tagText, _ := strconv.Unquote(field.Tag.Value)
237
- tag := reflect.StructTag(tagText)
238
- for _, name := range field.Names {
239
- s.configFields[key+"."+name.Name] = configField{value: tag.Get("default"), env: tag.Get("envconfig")}
240
- }
241
- }
242
- case *ast.InterfaceType:
243
- for _, field := range body.Methods.List {
244
- fnType, ok := field.Type.(*ast.FuncType)
245
- if !ok || len(field.Names) == 0 {
246
- continue
247
- }
248
- results := s.resultTypes(fnType.Results, file)
249
- if len(results) > 0 {
250
- s.addMethodResult(key, field.Names[0].Name, results[0])
251
- }
252
- }
253
- }
254
- }
255
-
256
- func (s *scanner) indexFunction(file *goscan.File, decl *ast.FuncDecl) {
257
- key := file.Pkg + "." + decl.Name.Name
258
- receiver := ""
259
- if decl.Recv != nil && len(decl.Recv.List) > 0 {
260
- receiver = s.TypeKey(decl.Recv.List[0].Type, file)
261
- key = receiver + "." + decl.Name.Name
262
- }
263
- fn := &function{key: key, name: decl.Name.Name, receiver: receiver, file: file, decl: decl, types: map[string]string{}, results: s.resultTypes(decl.Type.Results, file)}
264
- if decl.Type.Params != nil {
265
- for _, field := range decl.Type.Params.List {
266
- typeKey := s.TypeKey(field.Type, file)
267
- for _, name := range field.Names {
268
- fn.params = append(fn.params, name.Name)
269
- fn.types[name.Name] = typeKey
270
- }
271
- }
272
- }
273
- s.functions[key] = fn
274
- if receiver != "" && len(fn.results) > 0 {
275
- s.addMethodResult(receiver, fn.name, fn.results[0])
276
- }
277
- }
278
-
279
- func (s *scanner) resultTypes(fields *ast.FieldList, file *goscan.File) []string {
280
- if fields == nil {
281
- return nil
282
- }
283
- var out []string
284
- for _, field := range fields.List {
285
- count := max(1, len(field.Names))
286
- for range count {
287
- out = append(out, s.TypeKey(field.Type, file))
288
- }
289
- }
290
- return out
291
- }
292
-
293
- func (s *scanner) addMethodResult(receiver, name, result string) {
294
- if result == "" {
295
- return
296
- }
297
- for _, key := range []string{receiver + "." + name, name} {
298
- if s.methodChoices[key] == nil {
299
- s.methodChoices[key] = map[string]bool{}
300
- }
301
- s.methodChoices[key][result] = true
302
- }
303
- }
304
-
305
- func (s *scanner) finalizeMethodResults() {
306
- for key, choices := range s.methodChoices {
307
- if len(choices) == 1 {
308
- for result := range choices {
309
- s.methodResults[key] = result
310
- }
311
- }
312
- }
313
- }
314
-
315
- func (s *scanner) stateFor(fn *function) *analysisState {
316
- state := &analysisState{types: map[string]string{}, strings: map[string]topic{}, bytesPayload: map[string]string{}, messages: map[string]string{}, topicParams: map[string]int{}}
317
- for name, kind := range fn.types {
318
- state.types[name] = kind
319
- }
320
- for i, name := range fn.params {
321
- state.topicParams[name] = i
322
- }
323
- return state
324
- }
325
-
326
- func cloneState(parent *analysisState) *analysisState {
327
- state := &analysisState{types: map[string]string{}, strings: map[string]topic{}, bytesPayload: map[string]string{}, messages: map[string]string{}, topicParams: map[string]int{}}
328
- for key, value := range parent.types {
329
- state.types[key] = value
330
- }
331
- for key, value := range parent.strings {
332
- state.strings[key] = value
333
- }
334
- for key, value := range parent.bytesPayload {
335
- state.bytesPayload[key] = value
336
- }
337
- for key, value := range parent.messages {
338
- state.messages[key] = value
339
- }
340
- for key, value := range parent.topicParams {
341
- state.topicParams[key] = value
342
- }
343
- return state
344
- }
345
-
346
- func (s *scanner) collectAssignments(body *ast.BlockStmt, file *goscan.File, state *analysisState) {
347
- ast.Inspect(body, func(node ast.Node) bool {
348
- if _, ok := node.(*ast.FuncLit); ok {
349
- return false
350
- }
351
- switch item := node.(type) {
352
- case *ast.AssignStmt:
353
- s.assign(item.Lhs, item.Rhs, file, state)
354
- case *ast.DeclStmt:
355
- gen, ok := item.Decl.(*ast.GenDecl)
356
- if ok && gen.Tok == token.VAR {
357
- for _, raw := range gen.Specs {
358
- spec := raw.(*ast.ValueSpec)
359
- lhs := make([]ast.Expr, len(spec.Names))
360
- for i, name := range spec.Names {
361
- lhs[i] = name
362
- }
363
- s.assign(lhs, spec.Values, file, state)
364
- if spec.Type != nil {
365
- for _, name := range spec.Names {
366
- state.types[name.Name] = s.TypeKey(spec.Type, file)
367
- }
368
- }
369
- }
370
- }
371
- }
372
- return true
373
- })
374
- }
375
-
376
- func (s *scanner) assign(lhs, rhs []ast.Expr, file *goscan.File, state *analysisState) {
377
- if len(rhs) == 0 {
378
- return
379
- }
380
- for i, raw := range lhs {
381
- name, ok := raw.(*ast.Ident)
382
- if !ok || name.Name == "_" {
383
- continue
384
- }
385
- expr := rhs[min(i, len(rhs)-1)]
386
- if len(rhs) == 1 && i > 0 {
387
- continue
388
- }
389
- if value := s.topicValue(expr, file, state, map[string]bool{}); value.address != "" {
390
- state.strings[name.Name] = value
391
- }
392
- if kind := s.typeOf(expr, file, state); kind != "" {
393
- state.types[name.Name] = kind
394
- }
395
- if call, ok := goscan.Unwrap(expr).(*ast.CallExpr); ok {
396
- if s.isPackageCall(call, file, "encoding/json", "Marshal") && len(call.Args) > 0 {
397
- state.bytesPayload[name.Name] = s.typeOf(call.Args[0], file, state)
398
- }
399
- if s.isPackageCall(call, file, watermillMessageImport, "NewMessage") && len(call.Args) > 1 {
400
- state.messages[name.Name] = s.payloadOf(call.Args[1], file, state)
401
- }
402
- }
403
- }
404
- if len(rhs) == 1 {
405
- call, ok := goscan.Unwrap(rhs[0]).(*ast.CallExpr)
406
- if ok {
407
- results := s.callResults(call, file, state)
408
- for i, raw := range lhs {
409
- name, ok := raw.(*ast.Ident)
410
- if ok && i < len(results) && results[i] != "" {
411
- state.types[name.Name] = results[i]
412
- }
413
- }
414
- }
415
- }
416
- }
417
-
418
- func (s *scanner) analyzeBlock(body *ast.BlockStmt, file *goscan.File, state *analysisState, includeHelpers bool) analysis {
419
- s.collectAssignments(body, file, state)
420
- result := analysis{}
421
- ast.Inspect(body, func(node ast.Node) bool {
422
- call, ok := node.(*ast.CallExpr)
423
- if !ok {
424
- return true
425
- }
426
- if s.isPackageCall(call, file, "encoding/json", "Unmarshal") && len(call.Args) > 1 && result.input == "" {
427
- result.input = s.typeOf(call.Args[1], file, state)
428
- }
429
- return true
430
- })
431
- s.walkStatements(body.List, []pathState{{}}, file, state, includeHelpers, &result.publications)
432
- result.publications = uniquePublications(result.publications)
433
- return result
434
- }
435
-
436
- func (s *scanner) walkStatements(statements []ast.Stmt, paths []pathState, file *goscan.File, state *analysisState, includeHelpers bool, out *[]publication) []pathState {
437
- for _, statement := range statements {
438
- var next []pathState
439
- for _, current := range paths {
440
- if branch, ok := statement.(*ast.IfStmt); ok {
441
- condition := s.PrintNode(branch.Cond)
442
- thenPath := current
443
- thenPath.conditions = appendCopy(current.conditions, condition)
444
- thenFalls := s.walkStatements(branch.Body.List, []pathState{thenPath}, file, state, includeHelpers, out)
445
-
446
- elsePath := current
447
- elsePath.conditions = appendCopy(current.conditions, "not ("+condition+")")
448
- var elseFalls []pathState
449
- switch otherwise := branch.Else.(type) {
450
- case *ast.BlockStmt:
451
- elseFalls = s.walkStatements(otherwise.List, []pathState{elsePath}, file, state, includeHelpers, out)
452
- case *ast.IfStmt:
453
- elseFalls = s.walkStatements([]ast.Stmt{otherwise}, []pathState{elsePath}, file, state, includeHelpers, out)
454
- default:
455
- elseFalls = []pathState{elsePath}
456
- }
457
- if len(thenFalls) > 0 && len(elseFalls) > 0 {
458
- merged := current
459
- merged.publications = unionPublicationIndexes(thenFalls, elseFalls)
460
- next = append(next, merged)
461
- } else {
462
- next = append(next, thenFalls...)
463
- next = append(next, elseFalls...)
464
- }
465
- continue
466
- }
467
-
468
- publications := s.publicationsIn(statement, file, state, includeHelpers)
469
- for _, value := range publications {
470
- value.conditions = append(append([]string{}, current.conditions...), value.conditions...)
471
- *out = append(*out, value)
472
- current.publications = append(current.publications, len(*out)-1)
473
- }
474
- if _, returns := statement.(*ast.ReturnStmt); returns {
475
- for _, index := range current.publications {
476
- (*out)[index].terminal = true
477
- }
478
- continue
479
- }
480
- next = append(next, current)
481
- }
482
- paths = next
483
- if len(paths) == 0 {
484
- break
485
- }
486
- }
487
- return paths
488
- }
489
-
490
- func (s *scanner) publicationsIn(statement ast.Stmt, file *goscan.File, state *analysisState, includeHelpers bool) []publication {
491
- var out []publication
492
- ast.Inspect(statement, func(node ast.Node) bool {
493
- if _, ok := node.(*ast.FuncLit); ok {
494
- return false
495
- }
496
- call, ok := node.(*ast.CallExpr)
497
- if !ok {
498
- return true
499
- }
500
- if sel, ok := call.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Publish" && len(call.Args) >= 2 {
501
- resolved := s.topicValue(call.Args[0], file, state, map[string]bool{})
502
- if resolved.valid() {
503
- resolved.at = s.At(call.Args[0].Pos())
504
- out = append(out, publication{topic: resolved, payload: s.payloadOf(call.Args[1], file, state), at: s.At(call.Pos())})
505
- }
506
- }
507
- if includeHelpers {
508
- if key := s.functionKey(call.Fun, file); key != "" {
509
- for _, summary := range s.summaries[key] {
510
- pub := summary
511
- if pub.topic.param >= 0 && pub.topic.param < len(call.Args) {
512
- index := pub.topic.param
513
- pub.topic = s.topicValue(call.Args[index], file, state, map[string]bool{})
514
- pub.topic.at = s.At(call.Args[index].Pos())
515
- }
516
- if pub.topic.address != "" {
517
- pub.at = s.At(call.Pos())
518
- out = append(out, pub)
519
- }
520
- }
521
- }
522
- }
523
- return true
524
- })
525
- return out
526
- }
527
-
528
- func appendCopy(values []string, value string) []string {
529
- out := append([]string{}, values...)
530
- return append(out, value)
531
- }
532
-
533
- func unionPublicationIndexes(groups ...[]pathState) []int {
534
- seen := map[int]bool{}
535
- var out []int
536
- for _, group := range groups {
537
- for _, state := range group {
538
- for _, index := range state.publications {
539
- if !seen[index] {
540
- seen[index] = true
541
- out = append(out, index)
542
- }
543
- }
544
- }
545
- }
546
- return out
547
- }
548
-
549
- func (s *scanner) indexHandlers(fn *function, b *plugin.Builder) {
550
- outer := s.stateFor(fn)
551
- s.collectAssignments(fn.decl.Body, fn.file, outer)
552
- ast.Inspect(fn.decl.Body, func(node ast.Node) bool {
553
- if _, ok := node.(*ast.FuncLit); ok {
554
- return false
555
- }
556
- call, ok := node.(*ast.CallExpr)
557
- if !ok {
558
- return true
559
- }
560
- sel, ok := call.Fun.(*ast.SelectorExpr)
561
- if !ok || (sel.Sel.Name != "AddHandler" && sel.Sel.Name != "AddNoPublisherHandler") {
562
- return true
563
- }
564
- usesMessage := false
565
- for _, imported := range fn.file.Imports {
566
- if imported == watermillMessageImport {
567
- usesMessage = true
568
- }
569
- }
570
- if !usesMessage || len(call.Args) < 4 {
571
- return true
572
- }
573
- callbackIndex := len(call.Args) - 1
574
- input := s.topicValue(call.Args[1], fn.file, outer, map[string]bool{})
575
- if input.address == "" {
576
- b.Warn(s.At(call.Args[1].Pos()).String(), "Watermill handler topic could not be resolved to a literal, constant, or config default")
577
- return false
578
- }
579
- name := s.topicValue(call.Args[0], fn.file, outer, map[string]bool{}).address
580
- if name == "" {
581
- name = s.PrintNode(call.Args[0])
582
- }
583
- callbackState := cloneState(outer)
584
- var body *ast.BlockStmt
585
- entrypoint := ""
586
- switch cb := goscan.Unwrap(call.Args[callbackIndex]).(type) {
587
- case *ast.FuncLit:
588
- body = cb.Body
589
- if cb.Type.Params != nil {
590
- for _, field := range cb.Type.Params.List {
591
- for _, param := range field.Names {
592
- callbackState.types[param.Name] = s.TypeKey(field.Type, fn.file)
593
- }
594
- }
595
- }
596
- case *ast.Ident:
597
- if target := s.functions[fn.file.Pkg+"."+cb.Name]; target != nil {
598
- body = target.decl.Body
599
- callbackState = s.stateFor(target)
600
- entrypoint = watermillFunctionKey(target)
601
- }
602
- }
603
- if body == nil {
604
- b.Warn(s.At(call.Args[callbackIndex].Pos()).String(), "Watermill handler body could not be resolved")
605
- return false
606
- }
607
- found := s.analyzeBlock(body, fn.file, callbackState, true)
608
- if sel.Sel.Name == "AddHandler" && len(call.Args) >= 7 {
609
- output := s.topicValue(call.Args[3], fn.file, outer, map[string]bool{})
610
- if output.address != "" {
611
- found.publications = append(found.publications, publication{topic: output, at: s.At(call.Args[3].Pos())})
612
- }
613
- }
614
- group := ""
615
- if subscriber, ok := call.Args[2].(*ast.Ident); ok {
616
- group = s.subscriberGroup(fn.decl.Body, fn.file, outer, subscriber.Name)
617
- }
618
- s.handlers = append(s.handlers, handler{name: name, input: input, inputPayload: found.input, consumerGroup: group, entrypoint: entrypoint, publications: uniquePublications(found.publications), at: s.At(call.Pos())})
619
- return false
620
- })
621
- }
622
-
623
- func watermillFunctionKey(fn *function) string {
624
- name := fn.name
625
- if fn.receiver != "" {
626
- name = goscan.LastSegment(fn.receiver) + "." + name
627
- }
628
- dir := path.Dir(fn.file.Name)
629
- if dir == "." || dir == "" {
630
- return name
631
- }
632
- return dir + ":" + name
633
- }
634
-
635
- func (s *scanner) indexCQRS(fn *function, b *plugin.Builder) {
636
- usesCQRS := false
637
- for _, imported := range fn.file.Imports {
638
- if imported == "github.com/ThreeDotsLabs/watermill/components/cqrs" {
639
- usesCQRS = true
640
- }
641
- }
642
- if !usesCQRS {
643
- return
644
- }
645
- state := s.stateFor(fn)
646
- s.collectAssignments(fn.decl.Body, fn.file, state)
647
- processorTopics := map[string]topic{}
648
- handlers := map[string]cqrsHandler{}
649
-
650
- ast.Inspect(fn.decl.Body, func(node ast.Node) bool {
651
- assign, ok := node.(*ast.AssignStmt)
652
- if !ok || len(assign.Rhs) == 0 {
653
- return true
654
- }
655
- call, ok := goscan.Unwrap(assign.Rhs[0]).(*ast.CallExpr)
656
- if !ok {
657
- return true
658
- }
659
- for _, raw := range assign.Lhs {
660
- name, ok := raw.(*ast.Ident)
661
- if !ok || name.Name == "_" {
662
- continue
663
- }
664
- if processorKind := s.cqrsCallName(call, fn.file); processorKind == "NewEventProcessorWithConfig" || processorKind == "NewCommandProcessorWithConfig" || processorKind == "NewEventProcessor" || processorKind == "NewCommandProcessor" {
665
- if resolved := s.cqrsProcessorTopic(call, fn.file, state); resolved.valid() {
666
- processorTopics[name.Name] = resolved
667
- }
668
- }
669
- if built, ok := s.cqrsHandler(call, fn.file, state); ok {
670
- handlers[name.Name] = built
671
- }
672
- }
673
- return true
674
- })
675
-
676
- ast.Inspect(fn.decl.Body, func(node ast.Node) bool {
677
- call, ok := node.(*ast.CallExpr)
678
- if !ok {
679
- return true
680
- }
681
- sel, ok := call.Fun.(*ast.SelectorExpr)
682
- if !ok || (sel.Sel.Name != "AddHandler" && sel.Sel.Name != "AddHandlers" && sel.Sel.Name != "AddHandlersGroup") {
683
- return true
684
- }
685
- receiver, ok := sel.X.(*ast.Ident)
686
- if !ok {
687
- return true
688
- }
689
- baseTopic, ok := processorTopics[receiver.Name]
690
- if !ok {
691
- return true
692
- }
693
- start := 0
694
- group := ""
695
- if sel.Sel.Name == "AddHandlersGroup" && len(call.Args) > 0 {
696
- group = s.topicValue(call.Args[0], fn.file, state, map[string]bool{}).address
697
- start = 1
698
- }
699
- for _, arg := range call.Args[start:] {
700
- var built cqrsHandler
701
- var found bool
702
- switch value := goscan.Unwrap(arg).(type) {
703
- case *ast.Ident:
704
- built, found = handlers[value.Name]
705
- case *ast.CallExpr:
706
- built, found = s.cqrsHandler(value, fn.file, state)
707
- }
708
- if !found {
709
- continue
710
- }
711
- input := baseTopic
712
- if input.address == "$message" {
713
- input.address = goscan.LastSegment(built.payload)
714
- }
715
- if input.address == "" {
716
- b.Warn(s.At(arg.Pos()).String(), "Watermill CQRS handler topic generator could not be resolved")
717
- continue
718
- }
719
- consumerGroup := group
720
- if consumerGroup == "" {
721
- consumerGroup = built.name
722
- }
723
- s.handlers = append(s.handlers, handler{name: built.name, input: input, inputPayload: built.payload, consumerGroup: consumerGroup, at: s.At(call.Pos())})
724
- }
725
- return true
726
- })
727
- }
728
-
729
- func (s *scanner) cqrsCallName(call *ast.CallExpr, file *goscan.File) string {
730
- fun := call.Fun
731
- switch generic := fun.(type) {
732
- case *ast.IndexExpr:
733
- fun = generic.X
734
- case *ast.IndexListExpr:
735
- fun = generic.X
736
- }
737
- sel, ok := fun.(*ast.SelectorExpr)
738
- if !ok {
739
- return ""
740
- }
741
- base, ok := sel.X.(*ast.Ident)
742
- if !ok || file.Imports[base.Name] != "github.com/ThreeDotsLabs/watermill/components/cqrs" {
743
- return ""
744
- }
745
- return sel.Sel.Name
746
- }
747
-
748
- func (s *scanner) cqrsHandler(call *ast.CallExpr, file *goscan.File, state *analysisState) (cqrsHandler, bool) {
749
- name := s.cqrsCallName(call, file)
750
- if name != "NewEventHandler" && name != "NewCommandHandler" && name != "NewGroupEventHandler" {
751
- return cqrsHandler{}, false
752
- }
753
- handlerName := name
754
- callbackIndex := 0
755
- if name != "NewGroupEventHandler" {
756
- if len(call.Args) < 2 {
757
- return cqrsHandler{}, false
758
- }
759
- handlerName = s.topicValue(call.Args[0], file, state, map[string]bool{}).address
760
- callbackIndex = 1
761
- }
762
- if handlerName == "" {
763
- handlerName = strings.TrimSuffix(strings.TrimPrefix(name, "New"), "Handler")
764
- }
765
- payload := ""
766
- switch generic := call.Fun.(type) {
767
- case *ast.IndexExpr:
768
- payload = s.TypeKey(generic.Index, file)
769
- case *ast.IndexListExpr:
770
- if len(generic.Indices) > 0 {
771
- payload = s.TypeKey(generic.Indices[0], file)
772
- }
773
- }
774
- if payload == "" && callbackIndex < len(call.Args) {
775
- if callback, ok := goscan.Unwrap(call.Args[callbackIndex]).(*ast.FuncLit); ok && callback.Type.Params != nil && len(callback.Type.Params.List) > 1 {
776
- payload = s.TypeKey(callback.Type.Params.List[1].Type, file)
777
- }
778
- }
779
- return cqrsHandler{name: handlerName, payload: payload, at: s.At(call.Pos())}, payload != ""
780
- }
781
-
782
- func (s *scanner) cqrsProcessorTopic(call *ast.CallExpr, file *goscan.File, state *analysisState) topic {
783
- name := s.cqrsCallName(call, file)
784
- index := 1
785
- if name == "NewEventProcessor" || name == "NewCommandProcessor" {
786
- index = 1
787
- }
788
- if len(call.Args) <= index {
789
- return topic{param: -1}
790
- }
791
- var generator ast.Expr
792
- if name == "NewEventProcessorWithConfig" || name == "NewCommandProcessorWithConfig" {
793
- config, ok := goscan.Unwrap(call.Args[index]).(*ast.CompositeLit)
794
- if !ok {
795
- return topic{param: -1}
796
- }
797
- for _, raw := range config.Elts {
798
- field, ok := raw.(*ast.KeyValueExpr)
799
- if !ok {
800
- continue
801
- }
802
- key, named := field.Key.(*ast.Ident)
803
- if named && key.Name == "GenerateSubscribeTopic" {
804
- generator = field.Value
805
- }
806
- }
807
- } else {
808
- generator = call.Args[index]
809
- }
810
- callback, ok := goscan.Unwrap(generator).(*ast.FuncLit)
811
- if !ok || callback.Body == nil {
812
- return topic{param: -1}
813
- }
814
- resolved := topic{param: -1}
815
- ast.Inspect(callback.Body, func(node ast.Node) bool {
816
- ret, ok := node.(*ast.ReturnStmt)
817
- if !ok || len(ret.Results) == 0 || resolved.valid() {
818
- return true
819
- }
820
- if selector, ok := goscan.Unwrap(ret.Results[0]).(*ast.SelectorExpr); ok {
821
- if selector.Sel.Name == "EventName" || selector.Sel.Name == "CommandName" {
822
- resolved = topic{address: "$message", param: -1, at: s.At(ret.Results[0].Pos())}
823
- return false
824
- }
825
- }
826
- resolved = s.topicValue(ret.Results[0], file, state, map[string]bool{})
827
- return false
828
- })
829
- return resolved
830
- }
831
-
832
- func (s *scanner) subscriberGroup(body *ast.BlockStmt, file *goscan.File, state *analysisState, variable string) string {
833
- group := ""
834
- ast.Inspect(body, func(node ast.Node) bool {
835
- assign, ok := node.(*ast.AssignStmt)
836
- if !ok || len(assign.Rhs) == 0 {
837
- return true
838
- }
839
- for _, lhs := range assign.Lhs {
840
- ident, ok := lhs.(*ast.Ident)
841
- if !ok || ident.Name != variable {
842
- continue
843
- }
844
- call, ok := goscan.Unwrap(assign.Rhs[0]).(*ast.CallExpr)
845
- if !ok || len(call.Args) < 2 {
846
- continue
847
- }
848
- group = s.topicValue(call.Args[1], file, state, map[string]bool{}).address
849
- }
850
- return group == ""
851
- })
852
- return group
853
- }
854
-
855
- func (s *scanner) catalog(serviceID, owner string) ([]catalog.Channel, []catalog.Flow) {
856
- channels := map[string]*channelState{}
857
- ensure := func(value topic) *channelState {
858
- found := channels[value.address]
859
- if found != nil {
860
- return found
861
- }
862
- title := "Watermill topic"
863
- if s.transport != "" {
864
- title = "Watermill / " + s.transport + " topic"
865
- }
866
- doc := "Messages routed through github.com/ThreeDotsLabs/watermill."
867
- if value.env != "" {
868
- doc += " Configured by `" + value.env + "`; extracted address is its source default."
869
- }
870
- found = &channelState{channel: catalog.Channel{Address: value.address, Title: title, Doc: doc, Messages: []catalog.ChannelMessage{}, Source: value.at.String()}, seen: map[string]bool{}}
871
- channels[value.address] = found
872
- return found
873
- }
874
- var flows []catalog.Flow
875
- for _, found := range s.handlers {
876
- inputPayload := goscan.FirstNonEmpty(goscan.LastSegment(found.inputPayload), found.name)
877
- input := ensure(found.input)
878
- addMessage(input, catalog.ChannelMessage{Name: inputPayload, Title: inputPayload, Doc: s.payloadDescription(found.inputPayload, "Input"), Direction: catalog.ChannelReceive})
879
- if len(found.publications) == 0 {
880
- flows = append(flows, s.flow(serviceID, owner, found, nil))
881
- }
882
- for i := range found.publications {
883
- pub := found.publications[i]
884
- output := ensure(pub.topic)
885
- payload := goscan.FirstNonEmpty(goscan.LastSegment(pub.payload), "message")
886
- addMessage(output, catalog.ChannelMessage{Name: payload, Title: payload, Doc: s.payloadDescription(pub.payload, "Payload"), Direction: catalog.ChannelSend})
887
- }
888
- if structuredBranches(found.publications) {
889
- flows = append(flows, s.branchedFlow(serviceID, owner, found))
890
- } else {
891
- for i := range found.publications {
892
- pub := found.publications[i]
893
- flows = append(flows, s.flow(serviceID, owner, found, &pub))
894
- }
895
- }
896
- }
897
- addresses := make([]string, 0, len(channels))
898
- for address := range channels {
899
- addresses = append(addresses, address)
900
- }
901
- sort.Strings(addresses)
902
- out := make([]catalog.Channel, 0, len(addresses))
903
- for _, address := range addresses {
904
- out = append(out, channels[address].channel)
905
- }
906
- sort.Slice(flows, func(i, j int) bool { return flows[i].Slug < flows[j].Slug })
907
- return out, flows
908
- }
909
-
910
- func structuredBranches(publications []publication) bool {
911
- if len(publications) < 2 {
912
- return false
913
- }
914
- seen := map[string]bool{}
915
- for _, pub := range publications {
916
- condition := strings.Join(pub.conditions, " and ")
917
- if condition == "" || seen[condition] {
918
- return false
919
- }
920
- seen[condition] = true
921
- }
922
- return true
923
- }
924
-
925
- func branchConditions(publications []publication) []string {
926
- all := make([]map[string]bool, len(publications))
927
- common := map[string]int{}
928
- for i, pub := range publications {
929
- all[i] = map[string]bool{}
930
- for _, condition := range pub.conditions {
931
- all[i][condition] = true
932
- common[condition]++
933
- }
934
- }
935
- titles := make([]string, len(publications))
936
- for i, pub := range publications {
937
- for _, condition := range pub.conditions {
938
- opposite := oppositeCondition(condition)
939
- for other := range publications {
940
- if other != i && all[other][opposite] {
941
- titles[i] = condition
942
- break
943
- }
944
- }
945
- if titles[i] != "" {
946
- break
947
- }
948
- }
949
- if titles[i] != "" {
950
- continue
951
- }
952
- var distinguishing []string
953
- for _, condition := range pub.conditions {
954
- if common[condition] != len(publications) {
955
- distinguishing = append(distinguishing, condition)
956
- }
957
- }
958
- titles[i] = goscan.FirstNonEmpty(strings.Join(distinguishing, " and "), strings.Join(pub.conditions, " and "))
959
- }
960
- return titles
961
- }
962
-
963
- func oppositeCondition(condition string) string {
964
- if strings.HasPrefix(condition, "not (") && strings.HasSuffix(condition, ")") {
965
- return strings.TrimSuffix(strings.TrimPrefix(condition, "not ("), ")")
966
- }
967
- return "not (" + condition + ")"
968
- }
969
-
970
- func addMessage(channel *channelState, message catalog.ChannelMessage) {
971
- key := string(message.Direction) + "|" + message.Name
972
- if channel.seen[key] {
973
- return
974
- }
975
- channel.seen[key] = true
976
- channel.channel.Messages = append(channel.channel.Messages, message)
977
- }
978
-
979
- func (s *scanner) flow(serviceID, owner string, found handler, pub *publication) catalog.Flow {
980
- inputBroker := "watermill." + goscan.Slug(found.input.address)
981
- ending := "consume"
982
- name := goscan.Title(found.name)
983
- summary := "Watermill handler `" + found.name + "` consumes `" + found.input.address + "`"
984
- participants := []catalog.Participant{{ID: serviceID, Kind: catalog.ParticipantService, Context: stringPtr(owner)}, {ID: inputBroker, Kind: catalog.ParticipantBroker, Label: goscan.FirstNonEmpty(s.transport, "Watermill") + " · " + found.input.address}}
985
- note := s.payloadDescription(found.inputPayload, "Input")
986
- if found.consumerGroup != "" {
987
- note += " Consumer group `" + found.consumerGroup + "`."
988
- }
989
- steps := catalog.FlowNodes{&catalog.Step{Type: "step", ID: "receive", From: inputBroker, To: serviceID, Kind: catalog.StepEvent, Label: found.name, Status: catalog.StatusDeclared, Note: strings.TrimSpace(note), Line: found.at.String(), ContinuesAt: found.entrypoint, Handoff: s.messageHandoff(found.input.address, "receive")}}
990
- if pub != nil {
991
- ending = pub.topic.address
992
- outputBroker := "watermill." + goscan.Slug(pub.topic.address)
993
- if outputBroker != inputBroker {
994
- participants = append(participants, catalog.Participant{ID: outputBroker, Kind: catalog.ParticipantBroker, Label: goscan.FirstNonEmpty(s.transport, "Watermill") + " · " + pub.topic.address})
995
- }
996
- payload := goscan.FirstNonEmpty(goscan.LastSegment(pub.payload), "message")
997
- steps = append(steps, &catalog.Step{Type: "step", ID: "publish", From: serviceID, To: outputBroker, Kind: catalog.StepEvent, Label: "publish " + payload, Status: catalog.StatusDeclared, Note: s.payloadDescription(pub.payload, "Payload"), Line: pub.at.String(), Handoff: s.messageHandoff(pub.topic.address, "send")})
998
- name += " → " + pub.topic.address
999
- summary += " and may publish `" + pub.topic.address + "`"
1000
- }
1001
- summary += "."
1002
- slugged := goscan.Slug(goscan.LastSegment(serviceID) + "-watermill-" + found.name + "-" + ending)
1003
- return catalog.Flow{ID: "flow." + slugged, Slug: slugged, Name: name, Summary: summary, Source: found.at.String(), Trigger: &catalog.FlowTrigger{Kind: "event", Label: found.input.address, Confidence: "high"}, Owner: owner, Participants: participants, Steps: steps}
1004
- }
1005
-
1006
- func (s *scanner) branchedFlow(serviceID, owner string, found handler) catalog.Flow {
1007
- inputBroker := "watermill." + goscan.Slug(found.input.address)
1008
- participants := []catalog.Participant{
1009
- {ID: serviceID, Kind: catalog.ParticipantService, Context: stringPtr(owner)},
1010
- {ID: inputBroker, Kind: catalog.ParticipantBroker, Label: goscan.FirstNonEmpty(s.transport, "Watermill") + " · " + found.input.address},
1011
- }
1012
- participantSeen := map[string]bool{serviceID: true, inputBroker: true}
1013
- branches := make([]catalog.AltBranch, 0, len(found.publications))
1014
- addresses := make([]string, 0, len(found.publications))
1015
- conditions := branchConditions(found.publications)
1016
- for i, pub := range found.publications {
1017
- outputBroker := "watermill." + goscan.Slug(pub.topic.address)
1018
- if !participantSeen[outputBroker] {
1019
- participantSeen[outputBroker] = true
1020
- participants = append(participants, catalog.Participant{ID: outputBroker, Kind: catalog.ParticipantBroker, Label: goscan.FirstNonEmpty(s.transport, "Watermill") + " · " + pub.topic.address})
1021
- }
1022
- payload := goscan.FirstNonEmpty(goscan.LastSegment(pub.payload), "message")
1023
- branches = append(branches, catalog.AltBranch{
1024
- Title: conditions[i],
1025
- Terminal: pub.terminal,
1026
- Steps: catalog.FlowNodes{&catalog.Step{
1027
- Type: "step",
1028
- ID: fmt.Sprintf("publish-%d", i+1),
1029
- From: serviceID,
1030
- To: outputBroker,
1031
- Kind: catalog.StepEvent,
1032
- Label: "publish " + payload,
1033
- Status: catalog.StatusDeclared,
1034
- Note: s.payloadDescription(pub.payload, "Payload"),
1035
- Line: pub.at.String(),
1036
- Handoff: s.messageHandoff(pub.topic.address, "send"),
1037
- }},
1038
- })
1039
- addresses = append(addresses, "`"+pub.topic.address+"`")
1040
- }
1041
- note := s.payloadDescription(found.inputPayload, "Input")
1042
- if found.consumerGroup != "" {
1043
- note += " Consumer group `" + found.consumerGroup + "`."
1044
- }
1045
- steps := catalog.FlowNodes{
1046
- &catalog.Step{Type: "step", ID: "receive", From: inputBroker, To: serviceID, Kind: catalog.StepEvent, Label: found.name, Status: catalog.StatusDeclared, Note: strings.TrimSpace(note), Line: found.at.String(), ContinuesAt: found.entrypoint, Handoff: s.messageHandoff(found.input.address, "receive")},
1047
- &catalog.Alt{Type: "alt", ID: "outcome", Branches: branches},
1048
- }
1049
- slugged := goscan.Slug(goscan.LastSegment(serviceID) + "-watermill-" + found.name)
1050
- return catalog.Flow{
1051
- ID: "flow." + slugged,
1052
- Slug: slugged,
1053
- Name: goscan.Title(found.name),
1054
- Summary: "Watermill handler `" + found.name + "` consumes `" + found.input.address + "` and source control flow branches to " + strings.Join(addresses, " or ") + ".",
1055
- Source: found.at.String(),
1056
- Trigger: &catalog.FlowTrigger{Kind: "event", Label: found.input.address, Confidence: "high"},
1057
- Owner: owner,
1058
- Participants: participants,
1059
- Steps: steps,
1060
- }
1061
- }
1062
-
1063
- func (s *scanner) messageHandoff(channel, direction string) *catalog.FlowHandoff {
1064
- transport := strings.ToLower(goscan.FirstNonEmpty(s.transport, "watermill"))
1065
- return &catalog.FlowHandoff{Kind: "message", Transport: transport, Channel: channel, Direction: direction}
1066
- }
1067
-
1068
- func (s *scanner) payloadDescription(key, prefix string) string {
1069
- found := s.types[key]
1070
- if found == nil {
1071
- if key == "" {
1072
- return prefix + " payload type is not visible in source."
1073
- }
1074
- return prefix + " `" + goscan.LastSegment(key) + "`."
1075
- }
1076
- if len(found.fields) == 0 {
1077
- return prefix + " `" + found.name + "`."
1078
- }
1079
- fields := found.fields
1080
- suffix := ""
1081
- if len(fields) > 10 {
1082
- suffix = fmt.Sprintf(", and %d more", len(fields)-10)
1083
- fields = fields[:10]
1084
- }
1085
- return prefix + " `" + found.name + "`: `" + strings.Join(fields, "`, `") + "`" + suffix + "."
1086
- }
1087
-
1088
- func uniquePublications(values []publication) []publication {
1089
- seen := map[string]bool{}
1090
- var out []publication
1091
- for _, value := range values {
1092
- if !value.topic.valid() {
1093
- continue
1094
- }
1095
- topicKey := value.topic.address
1096
- if topicKey == "" {
1097
- topicKey = fmt.Sprintf("$param:%d", value.topic.param)
1098
- }
1099
- key := topicKey + "|" + value.payload + "|" + strings.Join(value.conditions, "&&")
1100
- if seen[key] {
1101
- continue
1102
- }
1103
- seen[key] = true
1104
- out = append(out, value)
1105
- }
1106
- return out
1107
- }
1108
-
1109
- func (s *scanner) topicValue(expr ast.Expr, file *goscan.File, state *analysisState, visiting map[string]bool) topic {
1110
- expr = goscan.Unwrap(expr)
1111
- switch value := expr.(type) {
1112
- case *ast.BasicLit:
1113
- if value.Kind == token.STRING {
1114
- text, _ := strconv.Unquote(value.Value)
1115
- return topic{address: text, expr: s.PrintNode(expr), param: -1, at: s.At(expr.Pos())}
1116
- }
1117
- case *ast.Ident:
1118
- if local, ok := state.strings[value.Name]; ok {
1119
- return local
1120
- }
1121
- if index, ok := state.topicParams[value.Name]; ok {
1122
- return topic{param: index, expr: value.Name}
1123
- }
1124
- return s.constantValue(file.Pkg+"."+value.Name, visiting)
1125
- case *ast.SelectorExpr:
1126
- base, ok := value.X.(*ast.Ident)
1127
- if !ok {
1128
- break
1129
- }
1130
- if imported := file.Imports[base.Name]; imported != "" {
1131
- return s.constantValue(imported+"."+value.Sel.Name, visiting)
1132
- }
1133
- if kind := state.types[base.Name]; kind != "" {
1134
- if field, ok := s.configFields[kind+"."+value.Sel.Name]; ok && field.value != "" {
1135
- return topic{address: field.value, env: field.env, expr: s.PrintNode(expr), param: -1, at: s.At(expr.Pos())}
1136
- }
1137
- }
1138
- }
1139
- return topic{param: -1, expr: s.PrintNode(expr)}
1140
- }
1141
-
1142
- func (s *scanner) constantValue(key string, visiting map[string]bool) topic {
1143
- if visiting[key] {
1144
- return topic{param: -1}
1145
- }
1146
- found, ok := s.Constants[key]
1147
- if !ok {
1148
- return topic{param: -1}
1149
- }
1150
- visiting[key] = true
1151
- value := s.topicValue(found.Expr, found.File, &analysisState{types: map[string]string{}, strings: map[string]topic{}, topicParams: map[string]int{}}, visiting)
1152
- delete(visiting, key)
1153
- return value
1154
- }
1155
-
1156
- func (s *scanner) payloadOf(expr ast.Expr, file *goscan.File, state *analysisState) string {
1157
- expr = goscan.Unwrap(expr)
1158
- if ident, ok := expr.(*ast.Ident); ok {
1159
- if payload := state.messages[ident.Name]; payload != "" {
1160
- return payload
1161
- }
1162
- if payload := state.bytesPayload[ident.Name]; payload != "" {
1163
- return payload
1164
- }
1165
- }
1166
- return s.typeOf(expr, file, state)
1167
- }
1168
-
1169
- func (s *scanner) typeOf(expr ast.Expr, file *goscan.File, state *analysisState) string {
1170
- expr = goscan.Unwrap(expr)
1171
- switch value := expr.(type) {
1172
- case *ast.CompositeLit:
1173
- return s.TypeKey(value.Type, file)
1174
- case *ast.Ident:
1175
- return state.types[value.Name]
1176
- case *ast.CallExpr:
1177
- results := s.callResults(value, file, state)
1178
- if len(results) > 0 {
1179
- return results[0]
1180
- }
1181
- }
1182
- return ""
1183
- }
1184
-
1185
- func (s *scanner) callResults(call *ast.CallExpr, file *goscan.File, state *analysisState) []string {
1186
- if key := s.functionKey(call.Fun, file); key != "" {
1187
- if fn := s.functions[key]; fn != nil {
1188
- return fn.results
1189
- }
1190
- }
1191
- if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
1192
- if base, ok := sel.X.(*ast.Ident); ok {
1193
- if receiver := state.types[base.Name]; receiver != "" {
1194
- if result := s.methodResults[receiver+"."+sel.Sel.Name]; result != "" {
1195
- return []string{result}
1196
- }
1197
- }
1198
- }
1199
- if result := s.methodResults[sel.Sel.Name]; result != "" {
1200
- return []string{result}
1201
- }
1202
- }
1203
- return nil
1204
- }
1205
-
1206
- func (s *scanner) functionKey(expr ast.Expr, file *goscan.File) string {
1207
- switch value := expr.(type) {
1208
- case *ast.Ident:
1209
- return file.Pkg + "." + value.Name
1210
- case *ast.SelectorExpr:
1211
- base, ok := value.X.(*ast.Ident)
1212
- if ok && file.Imports[base.Name] != "" {
1213
- return file.Imports[base.Name] + "." + value.Sel.Name
1214
- }
1215
- }
1216
- return ""
1217
- }
1218
-
1219
- func (s *scanner) isPackageCall(call *ast.CallExpr, file *goscan.File, pkg, name string) bool {
1220
- sel, ok := call.Fun.(*ast.SelectorExpr)
1221
- if !ok || sel.Sel.Name != name {
1222
- return false
1223
- }
1224
- base, ok := sel.X.(*ast.Ident)
1225
- return ok && file.Imports[base.Name] == pkg
1226
- }
1227
-
1228
- func stringPtr(value string) *string { return &value }