@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,315 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "fmt"
6
- "os"
7
- "path/filepath"
8
- "strings"
9
-
10
- "github.com/shortlink-org/portolan/catalog"
11
- "github.com/shortlink-org/portolan/plugin"
12
- "gopkg.in/yaml.v3"
13
- )
14
-
15
- func extract(in plugin.Input, opts Options) (plugin.Response, error) {
16
- b := &plugin.Builder{}
17
- root := in.Root
18
-
19
- if opts.Context == "" {
20
- opts.Context = filepath.Base(root)
21
- }
22
- if opts.Service == "" {
23
- opts.Service = filepath.Base(root)
24
- }
25
-
26
- specPath, err := findSpec(root, opts.Spec, b)
27
- if err != nil {
28
- return plugin.Response{}, err
29
- }
30
-
31
- doc, err := load(specPath)
32
- if err != nil {
33
- return plugin.Response{}, fmt.Errorf("%s: %w", specPath, err)
34
- }
35
-
36
- source := filepath.ToSlash(specPath)
37
- service := opts.Context + "." + opts.Service
38
-
39
- channels := readChannels(doc, source, service, b)
40
- if len(channels) == 0 {
41
- b.Warn(service, source+" declares no channels")
42
- }
43
-
44
- fragment := catalog.Catalog{
45
- GeneratedAt: in.GeneratedAt,
46
- Commit: in.Commit,
47
- Contexts: []catalog.BoundedContext{{
48
- ID: opts.Context,
49
- Slug: opts.Context,
50
- // Named by whichever source knows the name, as the OpenAPI
51
- // extractor's fragment says beside the same gap: this document
52
- // describes what the service says on the bus, not what it is
53
- // called.
54
- Services: []catalog.Service{{
55
- ID: service,
56
- Slug: opts.Service,
57
- Provides: []catalog.RpcService{},
58
- Consumes: []catalog.RpcCall{},
59
- Aggregates: []catalog.Aggregate{},
60
- Channels: channels,
61
- }},
62
- }},
63
- Defs: map[string]catalog.TypeDef{},
64
- Flows: []catalog.Flow{},
65
- Adrs: []catalog.Adr{},
66
- }
67
-
68
- encoded, err := json.MarshalIndent(fragment, "", " ")
69
- if err != nil {
70
- return plugin.Response{}, err
71
- }
72
-
73
- b.File(firstNonEmpty(opts.Out, "bus.json"), string(encoded)+"\n")
74
-
75
- return b.Response(), nil
76
- }
77
-
78
- // draft is a channel while it is being read: the messages arrive from the
79
- // operations that name them, one direction at a time, and the same message may
80
- // be named twice.
81
- type draft struct {
82
- channel catalog.Channel
83
- seen map[string]bool
84
- }
85
-
86
- func (d *draft) add(message catalog.ChannelMessage) {
87
- if message.Name == "" {
88
- return
89
- }
90
- key := string(message.Direction) + " " + message.Name
91
- if d.seen[key] {
92
- return
93
- }
94
- d.seen[key] = true
95
- d.channel.Messages = append(d.channel.Messages, message)
96
- }
97
-
98
- func readChannels(doc *document, source, service string, b *plugin.Builder) []catalog.Channel {
99
- if doc.major == 2 {
100
- return channelsV2(doc, source, service, b)
101
- }
102
-
103
- return channelsV3(doc, source, service, b)
104
- }
105
-
106
- // channelsV3 reads 3.x: channels declare addresses and messages, and operations
107
- // say which way each message travels.
108
- func channelsV3(doc *document, source, service string, b *plugin.Builder) []catalog.Channel {
109
- drafts := map[string]*draft{}
110
- var order []string
111
-
112
- for _, c := range entries(child(doc.root, "channels")) {
113
- address := firstNonEmpty(text(child(c.value, "address")), c.key)
114
- drafts[c.key] = &draft{
115
- channel: catalog.Channel{
116
- Address: address,
117
- Title: text(child(c.value, "title")),
118
- Doc: text(child(c.value, "description")),
119
- Messages: []catalog.ChannelMessage{},
120
- Source: source,
121
- },
122
- seen: map[string]bool{},
123
- }
124
- order = append(order, c.key)
125
- }
126
-
127
- for _, op := range entries(child(doc.root, "operations")) {
128
- direction, ok := directionOf(text(child(op.value, "action")))
129
- if !ok {
130
- b.Warn(service, source+": operation "+op.key+" has no action, so nothing says which way its messages travel")
131
-
132
- continue
133
- }
134
-
135
- _, ref := doc.deref(child(op.value, "channel"))
136
- key := refKey(ref)
137
- d, found := drafts[key]
138
- if !found {
139
- b.Warn(service, source+": operation "+op.key+" names channel "+key+", which the document does not declare")
140
-
141
- continue
142
- }
143
-
144
- named := items(child(op.value, "messages"))
145
- if len(named) == 0 {
146
- // An operation that names no message carries everything the
147
- // channel declares, which is how a document says "this whole
148
- // channel goes this way".
149
- for _, m := range entries(child(channelNode(doc, key), "messages")) {
150
- d.add(doc.message(m.value, m.key, direction))
151
- }
152
-
153
- continue
154
- }
155
-
156
- for _, message := range named {
157
- node, ref := doc.deref(message)
158
- if node == nil {
159
- b.Warn(service, source+": operation "+op.key+" names message "+ref+", which is not in this document")
160
-
161
- continue
162
- }
163
- d.add(doc.message(node, refKey(ref), direction))
164
- }
165
- }
166
-
167
- out := make([]catalog.Channel, 0, len(order))
168
- for _, key := range order {
169
- d := drafts[key]
170
- if len(d.channel.Messages) == 0 {
171
- b.Warn(service, source+": channel "+d.channel.Address+" has no operation, so nothing says which way it travels")
172
- }
173
- out = append(out, d.channel)
174
- }
175
-
176
- return out
177
- }
178
-
179
- // channelsV2 reads 2.x, where the channel key is the address and the two
180
- // operations are named from the client's side: `subscribe` is what the
181
- // application produces, `publish` is what it consumes.
182
- func channelsV2(doc *document, source, service string, b *plugin.Builder) []catalog.Channel {
183
- var out []catalog.Channel
184
-
185
- for _, c := range entries(child(doc.root, "channels")) {
186
- d := &draft{
187
- channel: catalog.Channel{
188
- Address: c.key,
189
- Doc: text(child(c.value, "description")),
190
- Messages: []catalog.ChannelMessage{},
191
- Source: source,
192
- },
193
- seen: map[string]bool{},
194
- }
195
-
196
- for _, op := range []struct {
197
- key string
198
- direction catalog.ChannelDirection
199
- }{
200
- {"subscribe", catalog.ChannelSend},
201
- {"publish", catalog.ChannelReceive},
202
- } {
203
- node := child(c.value, op.key)
204
- if node == nil {
205
- continue
206
- }
207
-
208
- message := child(node, "message")
209
- candidates := items(child(message, "oneOf"))
210
- if len(candidates) == 0 && message != nil {
211
- candidates = []*yaml.Node{message}
212
- }
213
-
214
- for _, candidate := range candidates {
215
- resolved, ref := doc.deref(candidate)
216
- if resolved == nil {
217
- b.Warn(service, source+": channel "+c.key+" names message "+ref+", which is not in this document")
218
-
219
- continue
220
- }
221
- d.add(doc.message(resolved, refKey(ref), op.direction))
222
- }
223
- }
224
-
225
- if len(d.channel.Messages) == 0 {
226
- b.Warn(service, source+": channel "+c.key+" declares no message")
227
- }
228
-
229
- out = append(out, d.channel)
230
- }
231
-
232
- return out
233
- }
234
-
235
- // message reads one message object into the fact the catalog keeps: the name it
236
- // goes by on the wire, and enough prose to read it without opening the
237
- // document.
238
- //
239
- // The name is the message's own `name` when it has one, and the key it is filed
240
- // under when it does not. Those are different things - the key is the
241
- // document's, the name is the bus's - and an event's wire name is compared
242
- // against this, so a document that leaves `name` out is saying the two are the
243
- // same.
244
- func (d *document) message(node *yaml.Node, key string, direction catalog.ChannelDirection) catalog.ChannelMessage {
245
- resolved, _ := d.deref(node)
246
- if resolved == nil {
247
- resolved = node
248
- }
249
-
250
- return catalog.ChannelMessage{
251
- Name: firstNonEmpty(text(child(resolved, "name")), key),
252
- Title: text(child(resolved, "title")),
253
- Doc: firstNonEmpty(text(child(resolved, "summary")), text(child(resolved, "description"))),
254
- Direction: direction,
255
- }
256
- }
257
-
258
- func channelNode(doc *document, key string) *yaml.Node {
259
- return child(doc.root, "channels", key)
260
- }
261
-
262
- func directionOf(action string) (catalog.ChannelDirection, bool) {
263
- switch action {
264
- case "send":
265
- return catalog.ChannelSend, true
266
- case "receive":
267
- return catalog.ChannelReceive, true
268
- }
269
-
270
- return "", false
271
- }
272
-
273
- // findSpec locates the document. Told where it is, it looks there; otherwise it
274
- // searches, and says what it found so a service with two documents does not get
275
- // described from whichever one sorted first without anybody noticing.
276
- func findSpec(root, declared string, b *plugin.Builder) (string, error) {
277
- if declared != "" {
278
- return filepath.Join(root, filepath.FromSlash(declared)), nil
279
- }
280
-
281
- var found []string
282
- err := filepath.WalkDir(root, func(p string, entry os.DirEntry, err error) error {
283
- if err != nil || entry.IsDir() {
284
- return err
285
- }
286
- switch entry.Name() {
287
- case "asyncapi.yaml", "asyncapi.yml":
288
- found = append(found, p)
289
- }
290
-
291
- return nil
292
- })
293
- if err != nil {
294
- return "", err
295
- }
296
-
297
- if len(found) == 0 {
298
- return "", fmt.Errorf("no asyncapi document under %s, and none named in the options", root)
299
- }
300
- if len(found) > 1 {
301
- b.Warn("", "found "+strings.Join(found, ", ")+"; reading the first and ignoring the rest")
302
- }
303
-
304
- return found[0], nil
305
- }
306
-
307
- func firstNonEmpty(values ...string) string {
308
- for _, value := range values {
309
- if strings.TrimSpace(value) != "" {
310
- return value
311
- }
312
- }
313
-
314
- return ""
315
- }
@@ -1,197 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "strings"
6
- "testing"
7
-
8
- "github.com/shortlink-org/portolan/catalog"
9
- "github.com/shortlink-org/portolan/plugin"
10
- )
11
-
12
- func run3(t *testing.T, root string, opts Options) plugin.Response {
13
- t.Helper()
14
-
15
- resp, err := extract(
16
- plugin.Input{Root: root, Commit: "abc1234", GeneratedAt: "2026-01-01T00:00:00Z"},
17
- opts,
18
- )
19
- if err != nil {
20
- t.Fatal(err)
21
- }
22
- if len(resp.Files) != 1 {
23
- t.Fatalf("expected one fragment, got %d files", len(resp.Files))
24
- }
25
-
26
- return resp
27
- }
28
-
29
- func channelsOf(t *testing.T, resp plugin.Response) []catalog.Channel {
30
- t.Helper()
31
-
32
- var out catalog.Catalog
33
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
34
- t.Fatal(err)
35
- }
36
-
37
- return out.Contexts[0].Services[0].Channels
38
- }
39
-
40
- func byAddress(channels []catalog.Channel, address string) *catalog.Channel {
41
- for i := range channels {
42
- if channels[i].Address == address {
43
- return &channels[i]
44
- }
45
- }
46
-
47
- return nil
48
- }
49
-
50
- func spell(messages []catalog.ChannelMessage) []string {
51
- out := make([]string, 0, len(messages))
52
- for _, m := range messages {
53
- out = append(out, string(m.Direction)+" "+m.Name)
54
- }
55
-
56
- return out
57
- }
58
-
59
- // An operation that names no message carries everything the channel declares,
60
- // which is how a document says "this whole channel goes one way".
61
- func TestSendTakesTheWholeChannel(t *testing.T) {
62
- channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
63
-
64
- basket := byAddress(channels, "shop.cart.basket")
65
- if basket == nil {
66
- t.Fatalf("no basket channel in %v", channels)
67
- }
68
-
69
- want := "send cart.BasketCreated,send cart.BasketCheckedOut"
70
- if got := strings.Join(spell(basket.Messages), ","); got != want {
71
- t.Errorf("messages = %s, want %s", got, want)
72
- }
73
- }
74
-
75
- // The address is the channel's own when it names one, not the key it is filed
76
- // under: the key is the document's word, the address is the broker's, and an
77
- // event's wire is compared against the second.
78
- func TestAddressBeatsTheKey(t *testing.T) {
79
- channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
80
-
81
- if byAddress(channels, "basket") != nil {
82
- t.Error("the channel key was taken for an address")
83
- }
84
- }
85
-
86
- // What the service listens for is the half no publisher's source could say.
87
- func TestReceiveIsReadFromTheOperation(t *testing.T) {
88
- channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
89
-
90
- sessions := byAddress(channels, "auth_session")
91
- if sessions == nil {
92
- t.Fatalf("no session channel in %v", channels)
93
- }
94
-
95
- if got := strings.Join(spell(sessions.Messages), ","); got != "receive auth.SessionEnded" {
96
- t.Errorf("messages = %s", got)
97
- }
98
- }
99
-
100
- // The name on the wire and the prose beside it, so a reader does not have to
101
- // open the document to know what arrived.
102
- func TestMessageCarriesItsNameAndSummary(t *testing.T) {
103
- channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
104
-
105
- basket := byAddress(channels, "shop.cart.basket")
106
- created := basket.Messages[0]
107
-
108
- if created.Name != "cart.BasketCreated" {
109
- t.Errorf("name = %q", created.Name)
110
- }
111
- if created.Title != "Basket created" {
112
- t.Errorf("title = %q", created.Title)
113
- }
114
- if created.Doc != "A shopper has a basket." {
115
- t.Errorf("doc = %q", created.Doc)
116
- }
117
- }
118
-
119
- // A channel nothing operates on is still a channel - the address is a fact -
120
- // but which way it travels is not known, and silence about that is what the
121
- // diagnostic is for.
122
- func TestChannelWithNoOperationIsReportedNotDropped(t *testing.T) {
123
- resp := run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"})
124
-
125
- telemetry := byAddress(channelsOf(t, resp), "shop.cart.telemetry")
126
- if telemetry == nil {
127
- t.Fatal("a channel with no operation was dropped")
128
- }
129
- if len(telemetry.Messages) != 0 {
130
- t.Errorf("messages = %v", telemetry.Messages)
131
- }
132
-
133
- var said bool
134
- for _, d := range resp.Warnings() {
135
- if strings.Contains(d.Message, "shop.cart.telemetry") {
136
- said = true
137
- }
138
- }
139
- if !said {
140
- t.Errorf("nothing was said about it: %v", resp.Warnings())
141
- }
142
- }
143
-
144
- // 2.x names its operations from the client's side: `subscribe` is what the
145
- // application produces and `publish` is what it consumes. Reading them the
146
- // obvious way gets every arrow backwards.
147
- func TestVersionTwoOperationsAreReadFromTheApplicationsSide(t *testing.T) {
148
- channels := channelsOf(t, run3(t, "testdata/v2", Options{Context: "shop", Service: "billing"}))
149
-
150
- raised := byAddress(channels, "billing_invoice")
151
- if raised == nil {
152
- t.Fatalf("no invoice channel in %v", channels)
153
- }
154
- want := "send billing.InvoiceRaised,send billing.InvoiceSettled"
155
- if got := strings.Join(spell(raised.Messages), ","); got != want {
156
- t.Errorf("subscribe means the application sends; got %s", got)
157
- }
158
-
159
- consumed := byAddress(channels, "auth_user")
160
- if consumed == nil {
161
- t.Fatalf("no auth_user channel in %v", channels)
162
- }
163
- if got := strings.Join(spell(consumed.Messages), ","); got != "receive auth.UserRegistered" {
164
- t.Errorf("publish means the application receives; got %s", got)
165
- }
166
- }
167
-
168
- // The fragment describes what the service says on the bus and nothing else.
169
- func TestFragmentClaimsNothingItDoesNotKnow(t *testing.T) {
170
- var out catalog.Catalog
171
- resp := run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"})
172
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
173
- t.Fatal(err)
174
- }
175
-
176
- service := out.Contexts[0].Services[0]
177
- if service.Name != "" || service.Readme != "" || service.Repo != "" {
178
- t.Errorf("the bus fragment should not name the service: %+v", service)
179
- }
180
- if len(service.Aggregates) != 0 || len(service.Provides) != 0 {
181
- t.Error("the bus fragment should carry no aggregates and no interfaces")
182
- }
183
- }
184
-
185
- // The fragment's name is the manifest's to choose, and `bus.json` when it does
186
- // not.
187
- func TestFragmentName(t *testing.T) {
188
- resp := run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"})
189
- if resp.Files[0].Name != "bus.json" {
190
- t.Errorf("name = %q", resp.Files[0].Name)
191
- }
192
-
193
- resp = run3(t, "testdata/v3", Options{Context: "shop", Service: "cart", Out: "channels.json"})
194
- if resp.Files[0].Name != "channels.json" {
195
- t.Errorf("name = %q", resp.Files[0].Name)
196
- }
197
- }
@@ -1,48 +0,0 @@
1
- // Package main is portolan-extract-asyncapi: an AsyncAPI document in, a catalog
2
- // fragment out.
3
- //
4
- // It describes one aspect of a service - what it puts on the bus and what it
5
- // listens for - and nothing else. The events themselves come from a different
6
- // extractor reading the domain, and the two meet in the merge: that one says an
7
- // aggregate raises BasketCreated and calls it `cart.BasketCreated` on the wire,
8
- // this one says the service declares a channel carrying that name. Neither
9
- // knows the other exists, and the pages hold the two against each other.
10
- package main
11
-
12
- import (
13
- "fmt"
14
- "io"
15
- "os"
16
-
17
- "github.com/shortlink-org/portolan/plugin"
18
- )
19
-
20
- // Options are what the manifest tells the extractor: the things an AsyncAPI
21
- // document does not say about the estate it belongs to.
22
- type Options struct {
23
- Context string `json:"context"`
24
- Service string `json:"service"`
25
-
26
- // Spec is the document, relative to the input root. Left out, the
27
- // extractor looks for one.
28
- Spec string `json:"spec,omitempty"`
29
-
30
- Out string `json:"out,omitempty"`
31
- }
32
-
33
- func main() {
34
- if err := run(os.Stdin, os.Stdout); err != nil {
35
- fmt.Fprintln(os.Stderr, "portolan-extract-asyncapi:", err)
36
- os.Exit(1)
37
- }
38
- }
39
-
40
- func run(stdin io.Reader, stdout io.Writer) error {
41
- return plugin.Serve(stdin, stdout, descriptor(), func(req plugin.Request, opts Options) (plugin.Response, error) {
42
- if req.Input.Root == "" {
43
- return plugin.Response{}, fmt.Errorf("no input root: an extractor has nothing to read")
44
- }
45
-
46
- return extract(req.Input, opts)
47
- })
48
- }
@@ -1,151 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "strings"
6
-
7
- "gopkg.in/yaml.v3"
8
- )
9
-
10
- // The document is read as a node tree rather than into structs, for the reason
11
- // the OpenAPI extractor gives beside its own reader: a mapping unmarshalled
12
- // into a Go map loses the order its author wrote, and a channel object holds
13
- // keys this does not model, which walking the nodes ignores rather than fails
14
- // on.
15
- //
16
- // Two versions have to be read here, and they disagree about a word. In 3.x an
17
- // operation says `action: send` or `action: receive`, from the application's
18
- // side, and that is the end of it. In 2.x a channel has `publish` and
19
- // `subscribe`, and both are written from the *client's* side: `publish` is what
20
- // somebody else publishes to the application, and `subscribe` is what the
21
- // application produces for somebody else to subscribe to. Reading 2.x the
22
- // obvious way gets every arrow backwards, which is the confusion 3.x was
23
- // changed to end.
24
-
25
- type document struct {
26
- root *yaml.Node
27
- path string
28
- // major is the major version of the document: 2 or 3.
29
- major int
30
- }
31
-
32
- func load(path string) (*document, error) {
33
- contents, err := os.ReadFile(path)
34
- if err != nil {
35
- return nil, err
36
- }
37
-
38
- var root yaml.Node
39
- if err := yaml.Unmarshal(contents, &root); err != nil {
40
- return nil, err
41
- }
42
-
43
- node := &root
44
- if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
45
- node = node.Content[0]
46
- }
47
-
48
- doc := &document{root: node, path: path, major: 3}
49
- if version := text(child(node, "asyncapi")); strings.HasPrefix(version, "2.") {
50
- doc.major = 2
51
- }
52
-
53
- return doc, nil
54
- }
55
-
56
- // entry is one key/value pair of a mapping, in document order.
57
- type entry struct {
58
- key string
59
- value *yaml.Node
60
- }
61
-
62
- func entries(node *yaml.Node) []entry {
63
- if node == nil || node.Kind != yaml.MappingNode {
64
- return nil
65
- }
66
-
67
- out := make([]entry, 0, len(node.Content)/2)
68
- for i := 0; i+1 < len(node.Content); i += 2 {
69
- out = append(out, entry{key: node.Content[i].Value, value: node.Content[i+1]})
70
- }
71
-
72
- return out
73
- }
74
-
75
- // child follows a path of mapping keys, and answers nil for anything missing.
76
- func child(node *yaml.Node, keys ...string) *yaml.Node {
77
- for _, key := range keys {
78
- found := false
79
- for _, e := range entries(node) {
80
- if e.key == key {
81
- node = e.value
82
- found = true
83
-
84
- break
85
- }
86
- }
87
- if !found {
88
- return nil
89
- }
90
- }
91
-
92
- return node
93
- }
94
-
95
- func text(node *yaml.Node) string {
96
- if node == nil {
97
- return ""
98
- }
99
-
100
- return strings.TrimSpace(node.Value)
101
- }
102
-
103
- // items reads a sequence, or the single node written where a sequence was
104
- // allowed. A document that names one message where it could have named several
105
- // is the common case, and both spellings mean the same thing.
106
- func items(node *yaml.Node) []*yaml.Node {
107
- if node == nil {
108
- return nil
109
- }
110
- if node.Kind != yaml.SequenceNode {
111
- return []*yaml.Node{node}
112
- }
113
-
114
- return node.Content
115
- }
116
-
117
- // deref follows a local $ref, and answers the node itself when there is none.
118
- //
119
- // Only local refs are followed. A document that reaches into another file is
120
- // describing something this extractor cannot see, and guessing at a name from
121
- // the last segment of a path would be inventing one.
122
- func (d *document) deref(node *yaml.Node) (*yaml.Node, string) {
123
- ref := text(child(node, "$ref"))
124
- if ref == "" {
125
- return node, ""
126
- }
127
- if !strings.HasPrefix(ref, "#/") {
128
- return nil, ref
129
- }
130
-
131
- var keys []string
132
- for _, segment := range strings.Split(strings.TrimPrefix(ref, "#/"), "/") {
133
- // JSON Pointer escapes, which a channel address containing a slash
134
- // needs: `shop~1cart` is one key, not two.
135
- segment = strings.ReplaceAll(segment, "~1", "/")
136
- keys = append(keys, strings.ReplaceAll(segment, "~0", "~"))
137
- }
138
-
139
- return child(d.root, keys...), ref
140
- }
141
-
142
- // refKey is the last segment of a ref, which is the key the thing it points at
143
- // is filed under.
144
- func refKey(ref string) string {
145
- at := strings.LastIndex(ref, "/")
146
- if at < 0 {
147
- return ref
148
- }
149
-
150
- return strings.ReplaceAll(strings.ReplaceAll(ref[at+1:], "~1", "/"), "~0", "~")
151
- }