@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,507 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "fmt"
6
- "go/ast"
7
- "go/token"
8
- "path"
9
- "sort"
10
- "strings"
11
-
12
- "github.com/shortlink-org/portolan/catalog"
13
- "github.com/shortlink-org/portolan/internal/goscan"
14
- "github.com/shortlink-org/portolan/plugin"
15
- )
16
-
17
- const riverImport = "github.com/riverqueue/river"
18
-
19
- // riverForeign is what River's own names are worth: QueueDefault is the
20
- // queue called "default", and nothing in the service's tree says so.
21
- func riverForeign(importPath, name string) (string, bool) {
22
- if importPath == riverImport && name == "QueueDefault" {
23
- return "default", true
24
- }
25
- return "", false
26
- }
27
-
28
- type argType struct {
29
- key string
30
- name string
31
- kind string
32
- doc string
33
- fields []string
34
- at goscan.Source
35
- }
36
-
37
- type worker struct {
38
- key string
39
- name string
40
- args string
41
- entrypoint string
42
- at goscan.Source
43
- registered bool
44
- }
45
-
46
- type producer struct {
47
- args string
48
- queue string
49
- entrypoint string
50
- at goscan.Source
51
- }
52
-
53
- type queueJob struct {
54
- args *argType
55
- worker *worker
56
- producers []producer
57
- }
58
-
59
- // scanner is the tree, and what River leaves in it: the job argument types,
60
- // the workers over them, and every Insert that names one.
61
- type scanner struct {
62
- *goscan.Tree
63
- args map[string]*argType
64
- workers map[string]*worker
65
- registered map[string]bool
66
- producers []producer
67
- }
68
-
69
- func extract(in plugin.Input, opts Options) (plugin.Response, error) {
70
- b := &plugin.Builder{}
71
- tree, err := goscan.Read(in.Root)
72
- if err != nil {
73
- return plugin.Response{}, err
74
- }
75
- tree.Foreign = riverForeign
76
- s := &scanner{
77
- Tree: tree,
78
- args: map[string]*argType{},
79
- workers: map[string]*worker{},
80
- registered: map[string]bool{},
81
- }
82
- s.index()
83
-
84
- serviceID := opts.Context + "." + opts.Service
85
- channels, flows := s.catalog(serviceID, opts.Context, b)
86
- if len(channels) == 0 && len(flows) == 0 {
87
- b.Warn(in.Root, "no River jobs joined a JobArgs.Kind declaration to an Insert call or registered Worker")
88
- }
89
-
90
- fragment := catalog.Catalog{
91
- GeneratedAt: in.GeneratedAt,
92
- Commit: in.Commit,
93
- Contexts: []catalog.BoundedContext{{
94
- ID: opts.Context,
95
- Slug: opts.Context,
96
- Services: []catalog.Service{{
97
- ID: serviceID,
98
- Slug: opts.Service,
99
- Provides: []catalog.RpcService{},
100
- Consumes: []catalog.RpcCall{},
101
- Aggregates: []catalog.Aggregate{},
102
- Channels: channels,
103
- }},
104
- }},
105
- Defs: map[string]catalog.TypeDef{},
106
- Flows: flows,
107
- Adrs: []catalog.Adr{},
108
- }
109
- encoded, err := json.MarshalIndent(fragment, "", " ")
110
- if err != nil {
111
- return plugin.Response{}, err
112
- }
113
- b.File(goscan.FirstNonEmpty(opts.Out, "river.json"), string(encoded)+"\n")
114
-
115
- return b.Response(), nil
116
- }
117
-
118
- func (s *scanner) index() {
119
- for _, file := range s.Files {
120
- s.indexTypes(file)
121
- }
122
- for _, file := range s.Files {
123
- s.indexKindMethods(file)
124
- s.indexWorkers(file)
125
- s.indexRegistrations(file)
126
- }
127
- for key := range s.registered {
128
- if found := s.workers[key]; found != nil {
129
- found.registered = true
130
- }
131
- }
132
- for _, file := range s.Files {
133
- s.indexProducers(file)
134
- }
135
- }
136
-
137
- func (s *scanner) indexTypes(file *goscan.File) {
138
- for _, decl := range file.Node.Decls {
139
- gen, ok := decl.(*ast.GenDecl)
140
- if !ok || gen.Tok != token.TYPE {
141
- continue
142
- }
143
- for _, raw := range gen.Specs {
144
- spec := raw.(*ast.TypeSpec)
145
- body, ok := spec.Type.(*ast.StructType)
146
- if !ok {
147
- continue
148
- }
149
- key := file.Pkg + "." + spec.Name.Name
150
- doc := ""
151
- if spec.Doc != nil {
152
- doc = strings.TrimSpace(spec.Doc.Text())
153
- } else if gen.Doc != nil {
154
- doc = strings.TrimSpace(gen.Doc.Text())
155
- }
156
- s.args[key] = &argType{key: key, name: spec.Name.Name, doc: doc, fields: s.FieldsOf(body), at: s.At(spec.Pos())}
157
- }
158
- }
159
- }
160
-
161
- func (s *scanner) indexKindMethods(file *goscan.File) {
162
- for _, decl := range file.Node.Decls {
163
- fn, ok := decl.(*ast.FuncDecl)
164
- if !ok || fn.Recv == nil || fn.Name.Name != "Kind" || fn.Body == nil {
165
- continue
166
- }
167
- key := s.TypeKey(fn.Recv.List[0].Type, file)
168
- arg := s.args[key]
169
- if arg == nil {
170
- continue
171
- }
172
- ast.Inspect(fn.Body, func(node ast.Node) bool {
173
- ret, ok := node.(*ast.ReturnStmt)
174
- if !ok || len(ret.Results) != 1 || arg.kind != "" {
175
- return true
176
- }
177
- arg.kind = s.StringOf(ret.Results[0], file, map[string]bool{})
178
- return false
179
- })
180
- if arg.kind != "" {
181
- arg.at = s.At(fn.Pos())
182
- }
183
- }
184
- }
185
-
186
- func (s *scanner) indexWorkers(file *goscan.File) {
187
- for _, decl := range file.Node.Decls {
188
- fn, ok := decl.(*ast.FuncDecl)
189
- if !ok || fn.Recv == nil || fn.Name.Name != "Work" || fn.Type.Params == nil {
190
- continue
191
- }
192
- var args string
193
- for _, field := range fn.Type.Params.List {
194
- if key := s.riverJobArg(field.Type, file); key != "" {
195
- args = key
196
- break
197
- }
198
- }
199
- if args == "" {
200
- continue
201
- }
202
- key := s.TypeKey(fn.Recv.List[0].Type, file)
203
- s.workers[key] = &worker{
204
- key: key, name: goscan.LastSegment(key), args: args,
205
- entrypoint: s.riverFunctionKey(file, fn),
206
- at: s.At(fn.Pos()),
207
- }
208
- }
209
- }
210
-
211
- func (s *scanner) indexRegistrations(file *goscan.File) {
212
- ast.Inspect(file.Node, func(node ast.Node) bool {
213
- call, ok := node.(*ast.CallExpr)
214
- if !ok || len(call.Args) < 2 {
215
- return true
216
- }
217
- sel, ok := call.Fun.(*ast.SelectorExpr)
218
- if !ok || (sel.Sel.Name != "AddWorker" && sel.Sel.Name != "AddWorkerSafely") {
219
- return true
220
- }
221
- pkg, ok := sel.X.(*ast.Ident)
222
- if !ok || file.Imports[pkg.Name] != riverImport {
223
- return true
224
- }
225
- if key := s.compositeType(call.Args[1], file, nil); key != "" {
226
- s.registered[key] = true
227
- }
228
- return true
229
- })
230
- }
231
-
232
- func (s *scanner) indexProducers(file *goscan.File) {
233
- usesRiver := false
234
- for _, imported := range file.Imports {
235
- if imported == riverImport || strings.HasPrefix(imported, riverImport+"/") {
236
- usesRiver = true
237
- }
238
- }
239
- if !usesRiver {
240
- return
241
- }
242
- for _, decl := range file.Node.Decls {
243
- fn, ok := decl.(*ast.FuncDecl)
244
- if !ok || fn.Body == nil {
245
- continue
246
- }
247
- locals := map[string]string{}
248
- ast.Inspect(fn.Body, func(node ast.Node) bool {
249
- switch item := node.(type) {
250
- case *ast.AssignStmt:
251
- for i, lhs := range item.Lhs {
252
- name, ok := lhs.(*ast.Ident)
253
- if !ok || len(item.Rhs) == 0 {
254
- continue
255
- }
256
- rhs := item.Rhs[min(i, len(item.Rhs)-1)]
257
- if key := s.compositeType(rhs, file, locals); key != "" {
258
- locals[name.Name] = key
259
- }
260
- }
261
- case *ast.DeclStmt:
262
- gen, ok := item.Decl.(*ast.GenDecl)
263
- if !ok || gen.Tok != token.VAR {
264
- break
265
- }
266
- for _, raw := range gen.Specs {
267
- spec := raw.(*ast.ValueSpec)
268
- for i, name := range spec.Names {
269
- key := s.TypeKey(spec.Type, file)
270
- if key == "" && len(spec.Values) > 0 {
271
- key = s.compositeType(spec.Values[min(i, len(spec.Values)-1)], file, locals)
272
- }
273
- if key != "" {
274
- locals[name.Name] = key
275
- }
276
- }
277
- }
278
- case *ast.CallExpr:
279
- sel, ok := item.Fun.(*ast.SelectorExpr)
280
- if !ok || (sel.Sel.Name != "Insert" && sel.Sel.Name != "InsertTx") {
281
- break
282
- }
283
- argIndex := 1
284
- if sel.Sel.Name == "InsertTx" {
285
- argIndex = 2
286
- }
287
- if len(item.Args) <= argIndex {
288
- break
289
- }
290
- key := s.compositeType(item.Args[argIndex], file, locals)
291
- if s.args[key] == nil || s.args[key].kind == "" {
292
- break
293
- }
294
- queue := "default"
295
- if len(item.Args) > argIndex+1 {
296
- if named := s.queueFrom(item.Args[argIndex+1], file); named != "" {
297
- queue = named
298
- }
299
- }
300
- s.producers = append(s.producers, producer{args: key, queue: queue, entrypoint: s.riverFunctionKey(file, fn), at: s.At(item.Pos())})
301
- }
302
- return true
303
- })
304
- }
305
- }
306
-
307
- func (s *scanner) catalog(serviceID, owner string, b *plugin.Builder) ([]catalog.Channel, []catalog.Flow) {
308
- workerByArgs := map[string]*worker{}
309
- for _, found := range s.workers {
310
- if found.registered {
311
- workerByArgs[found.args] = found
312
- }
313
- }
314
- queues := map[string]map[string]*queueJob{}
315
- for _, p := range s.producers {
316
- arg := s.args[p.args]
317
- if arg == nil || arg.kind == "" {
318
- continue
319
- }
320
- if queues[p.queue] == nil {
321
- queues[p.queue] = map[string]*queueJob{}
322
- }
323
- job := queues[p.queue][p.args]
324
- if job == nil {
325
- job = &queueJob{args: arg, worker: workerByArgs[p.args]}
326
- queues[p.queue][p.args] = job
327
- }
328
- job.producers = append(job.producers, p)
329
- }
330
- for args, found := range workerByArgs {
331
- seen := false
332
- for _, jobs := range queues {
333
- if jobs[args] != nil {
334
- seen = true
335
- }
336
- }
337
- if !seen {
338
- b.Warn(found.at.String(), "registered River worker "+found.name+" handles "+goscan.LastSegment(args)+", but no local Insert call reveals which queue feeds it")
339
- }
340
- }
341
-
342
- queueNames := make([]string, 0, len(queues))
343
- for queue := range queues {
344
- queueNames = append(queueNames, queue)
345
- }
346
- sort.Strings(queueNames)
347
- var channels []catalog.Channel
348
- var flows []catalog.Flow
349
- for _, queue := range queueNames {
350
- jobs := queues[queue]
351
- keys := make([]string, 0, len(jobs))
352
- for key := range jobs {
353
- keys = append(keys, key)
354
- }
355
- sort.Slice(keys, func(i, j int) bool { return jobs[keys[i]].args.kind < jobs[keys[j]].args.kind })
356
- channel := catalog.Channel{
357
- Address: queue,
358
- Kind: catalog.ChannelKindJob,
359
- Title: "River work queue",
360
- Doc: "Jobs inserted and handled through github.com/riverqueue/river.",
361
- Messages: []catalog.ChannelMessage{},
362
- }
363
- for _, key := range keys {
364
- job := jobs[key]
365
- doc := jobDescription(job.args)
366
- channel.Messages = append(channel.Messages, catalog.ChannelMessage{Name: job.args.kind, Title: job.args.name, Doc: doc, Direction: catalog.ChannelSend})
367
- if channel.Source == "" && len(job.producers) > 0 {
368
- channel.Source = job.producers[0].at.String()
369
- }
370
- if job.worker == nil {
371
- b.Warn(job.producers[0].at.String(), "River job "+job.args.kind+" is inserted, but no registered Worker["+job.args.name+"] was found in this component")
372
- continue
373
- }
374
- channel.Messages = append(channel.Messages, catalog.ChannelMessage{Name: job.args.kind, Title: job.worker.name, Doc: "Handled by " + job.worker.name + ".Work. " + doc, Direction: catalog.ChannelReceive})
375
- flows = append(flows, riverFlow(serviceID, owner, queue, job))
376
- }
377
- channels = append(channels, channel)
378
- }
379
-
380
- return channels, flows
381
- }
382
-
383
- func riverFlow(serviceID, owner, queue string, job *queueJob) catalog.Flow {
384
- broker := "river." + goscan.Slug(queue)
385
- slugged := goscan.Slug(goscan.LastSegment(serviceID) + "-river-" + job.args.kind)
386
- if queue != "default" {
387
- slugged += "-" + goscan.Slug(queue)
388
- }
389
- doc := jobDescription(job.args)
390
- handoff := func(direction string) *catalog.FlowHandoff {
391
- return &catalog.FlowHandoff{Kind: "job", Transport: "river", Channel: queue, Message: job.args.kind, Direction: direction}
392
- }
393
- return catalog.Flow{
394
- ID: "flow." + slugged,
395
- Slug: slugged,
396
- Name: goscan.Title(job.args.kind) + " job",
397
- Summary: "River job `" + job.args.kind + "` is inserted on `" + queue + "` and handled by `" + job.worker.name + ".Work`.",
398
- Source: job.producers[0].at.String(),
399
- Trigger: &catalog.FlowTrigger{Kind: "job", Label: "River · " + queue, Confidence: "high"},
400
- EntryPoint: producerEntrypoint(job.producers),
401
- Owner: owner,
402
- Participants: []catalog.Participant{
403
- {ID: serviceID, Kind: catalog.ParticipantService, Context: stringPtr(owner)},
404
- {ID: broker, Kind: catalog.ParticipantBroker, Label: "River · " + queue},
405
- },
406
- Steps: catalog.FlowNodes{
407
- &catalog.Step{Type: "step", ID: "enqueue", From: serviceID, To: broker, Kind: catalog.StepCall, Label: "enqueue " + job.args.kind, Status: catalog.StatusDeclared, Note: doc, Line: job.producers[0].at.String(), Handoff: handoff("send")},
408
- &catalog.Step{Type: "step", ID: "work", From: broker, To: serviceID, Kind: catalog.StepCall, Label: job.worker.name + ".Work", Status: catalog.StatusDeclared, Note: "River dispatches `" + job.args.kind + "` to the registered worker.", Line: job.worker.at.String(), ContinuesAt: job.worker.entrypoint, Handoff: handoff("receive")},
409
- },
410
- }
411
- }
412
-
413
- func producerEntrypoint(producers []producer) string {
414
- seen := map[string]bool{}
415
- entrypoint := ""
416
- for _, producer := range producers {
417
- if producer.entrypoint == "" || seen[producer.entrypoint] {
418
- continue
419
- }
420
- seen[producer.entrypoint] = true
421
- if entrypoint != "" {
422
- return ""
423
- }
424
- entrypoint = producer.entrypoint
425
- }
426
- return entrypoint
427
- }
428
-
429
- func (s *scanner) riverFunctionKey(file *goscan.File, fn *ast.FuncDecl) string {
430
- name := fn.Name.Name
431
- if fn.Recv != nil && len(fn.Recv.List) > 0 {
432
- if receiver := s.TypeKey(fn.Recv.List[0].Type, file); receiver != "" {
433
- name = goscan.LastSegment(receiver) + "." + name
434
- }
435
- }
436
- return sourceFunctionKey(file, name)
437
- }
438
-
439
- func sourceFunctionKey(file *goscan.File, name string) string {
440
- dir := path.Dir(file.Name)
441
- if dir == "." || dir == "" {
442
- return name
443
- }
444
- return dir + ":" + name
445
- }
446
-
447
- func (s *scanner) riverJobArg(expr ast.Expr, file *goscan.File) string {
448
- expr = goscan.Unwrap(expr)
449
- index, ok := expr.(*ast.IndexExpr)
450
- if !ok {
451
- return ""
452
- }
453
- sel, ok := index.X.(*ast.SelectorExpr)
454
- if !ok || sel.Sel.Name != "Job" {
455
- return ""
456
- }
457
- pkg, ok := sel.X.(*ast.Ident)
458
- if !ok || file.Imports[pkg.Name] != riverImport {
459
- return ""
460
- }
461
- return s.TypeKey(index.Index, file)
462
- }
463
-
464
- func (s *scanner) compositeType(expr ast.Expr, file *goscan.File, locals map[string]string) string {
465
- expr = goscan.Unwrap(expr)
466
- switch value := expr.(type) {
467
- case *ast.CompositeLit:
468
- return s.TypeKey(value.Type, file)
469
- case *ast.Ident:
470
- return locals[value.Name]
471
- }
472
- return ""
473
- }
474
-
475
- func (s *scanner) queueFrom(expr ast.Expr, file *goscan.File) string {
476
- expr = goscan.Unwrap(expr)
477
- lit, ok := expr.(*ast.CompositeLit)
478
- if !ok {
479
- return ""
480
- }
481
- for _, raw := range lit.Elts {
482
- field, ok := raw.(*ast.KeyValueExpr)
483
- if !ok {
484
- continue
485
- }
486
- name, ok := field.Key.(*ast.Ident)
487
- if ok && name.Name == "Queue" {
488
- return s.StringOf(field.Value, file, map[string]bool{})
489
- }
490
- }
491
- return ""
492
- }
493
-
494
- func jobDescription(args *argType) string {
495
- if len(args.fields) == 0 {
496
- return "Arguments: `" + args.name + "`."
497
- }
498
- fields := args.fields
499
- suffix := ""
500
- if len(fields) > 10 {
501
- suffix = fmt.Sprintf(", and %d more", len(fields)-10)
502
- fields = fields[:10]
503
- }
504
- return "Arguments `" + args.name + "`: `" + strings.Join(fields, "`, `") + "`" + suffix + "."
505
- }
506
-
507
- func stringPtr(value string) *string { return &value }
@@ -1,132 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "os"
6
- "path/filepath"
7
- "strings"
8
- "testing"
9
-
10
- "github.com/shortlink-org/portolan/catalog"
11
- "github.com/shortlink-org/portolan/plugin"
12
- )
13
-
14
- func write(t *testing.T, root, name, contents string) {
15
- t.Helper()
16
- path := filepath.Join(root, name)
17
- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
18
- t.Fatal(err)
19
- }
20
- if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
21
- t.Fatal(err)
22
- }
23
- }
24
-
25
- func extracted(t *testing.T, root string) (catalog.Catalog, plugin.Response) {
26
- t.Helper()
27
- resp, err := extract(plugin.Input{Root: root, Commit: "abc", GeneratedAt: "2026-01-01T00:00:00Z"}, Options{Context: "ops", Service: "mailer"})
28
- if err != nil {
29
- t.Fatal(err)
30
- }
31
- var out catalog.Catalog
32
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
33
- t.Fatal(err)
34
- }
35
- return out, resp
36
- }
37
-
38
- func TestExtractsQueuePayloadAndLifecycle(t *testing.T) {
39
- root := t.TempDir()
40
- write(t, root, "go.mod", "module example.com/mailer\n")
41
- write(t, root, "queue/names.go", `package queue
42
- const Critical = "critical_mail"
43
- `)
44
- write(t, root, "jobs/send.go", `package jobs
45
- type SendArgs struct {
46
- MessageID string `+"`json:\"message_id\"`"+`
47
- Attempts int `+"`json:\"attempts,omitempty\"`"+`
48
- }
49
- func (SendArgs) Kind() string { return "send_mail" }
50
- `)
51
- write(t, root, "jobs/worker.go", `package jobs
52
- import (
53
- "context"
54
- "github.com/riverqueue/river"
55
- )
56
- type SendWorker struct { river.WorkerDefaults[SendArgs] }
57
- func (*SendWorker) Work(context.Context, *river.Job[SendArgs]) error { return nil }
58
- `)
59
- write(t, root, "app/run.go", `package app
60
- import (
61
- "context"
62
- "github.com/riverqueue/river"
63
- "example.com/mailer/jobs"
64
- "example.com/mailer/queue"
65
- )
66
- func setup(workers *river.Workers, client interface { Insert(context.Context, river.JobArgs, *river.InsertOpts) (any, error) }, ctx context.Context) {
67
- river.AddWorker(workers, &jobs.SendWorker{})
68
- args := jobs.SendArgs{MessageID: "1"}
69
- _, _ = client.Insert(ctx, args, &river.InsertOpts{Queue: queue.Critical})
70
- }
71
- `)
72
-
73
- out, _ := extracted(t, root)
74
- service := out.Contexts[0].Services[0]
75
- if len(service.Channels) != 1 {
76
- t.Fatalf("channels = %+v", service.Channels)
77
- }
78
- channel := service.Channels[0]
79
- if channel.Address != "critical_mail" || channel.Kind != catalog.ChannelKindJob {
80
- t.Fatalf("channel = %+v", channel)
81
- }
82
- if len(channel.Messages) != 2 || channel.Messages[0].Direction != catalog.ChannelSend || channel.Messages[1].Direction != catalog.ChannelReceive {
83
- t.Fatalf("messages = %+v", channel.Messages)
84
- }
85
- if !strings.Contains(channel.Messages[0].Doc, "message_id string") {
86
- t.Errorf("payload doc = %q", channel.Messages[0].Doc)
87
- }
88
- if len(out.Flows) != 1 || len(out.Flows[0].Steps) != 2 {
89
- t.Fatalf("flows = %+v", out.Flows)
90
- }
91
- if out.Flows[0].Steps[0].(*catalog.Step).Label != "enqueue send_mail" || out.Flows[0].Steps[1].(*catalog.Step).Label != "SendWorker.Work" {
92
- t.Fatalf("steps = %+v", out.Flows[0].Steps)
93
- }
94
- if got := out.Flows[0].Steps[1].(*catalog.Step).ContinuesAt; got != "jobs:SendWorker.Work" {
95
- t.Fatalf("worker continuation = %q", got)
96
- }
97
- if got := out.Flows[0].EntryPoint; got != "app:setup" {
98
- t.Fatalf("producer entrypoint = %q", got)
99
- }
100
- enqueue := out.Flows[0].Steps[0].(*catalog.Step).Handoff
101
- work := out.Flows[0].Steps[1].(*catalog.Step).Handoff
102
- if enqueue == nil || work == nil || enqueue.Direction != "send" || work.Direction != "receive" || enqueue.Transport != "river" || enqueue.Channel != "critical_mail" || enqueue.Message != "send_mail" {
103
- t.Fatalf("job handoffs = enqueue %+v, work %+v", enqueue, work)
104
- }
105
- }
106
-
107
- func TestProducerWithoutRegisteredWorkerStaysVisible(t *testing.T) {
108
- root := t.TempDir()
109
- write(t, root, "go.mod", "module example.com/jobs\n")
110
- write(t, root, "job.go", `package jobs
111
- import (
112
- "context"
113
- "github.com/riverqueue/river"
114
- )
115
- type CleanupArgs struct{}
116
- func (CleanupArgs) Kind() string { return "cleanup" }
117
- func enqueue(client interface { Insert(context.Context, river.JobArgs, *river.InsertOpts) (any, error) }, ctx context.Context) {
118
- _, _ = client.Insert(ctx, CleanupArgs{}, nil)
119
- }
120
- `)
121
-
122
- out, resp := extracted(t, root)
123
- if got := out.Contexts[0].Services[0].Channels[0].Address; got != "default" {
124
- t.Fatalf("queue = %q", got)
125
- }
126
- if len(out.Flows) != 0 {
127
- t.Fatalf("unexpected flows: %+v", out.Flows)
128
- }
129
- if len(resp.Warnings()) != 1 || !strings.Contains(resp.Warnings()[0].Message, "no registered Worker") {
130
- t.Fatalf("warnings = %+v", resp.Warnings())
131
- }
132
- }
@@ -1,42 +0,0 @@
1
- // Package main is portolan-extract-river: River job declarations, producers
2
- // and registered workers in a Go repository in, work queues and job flows out.
3
- package main
4
-
5
- import (
6
- "fmt"
7
- "io"
8
- "os"
9
- "path/filepath"
10
-
11
- "github.com/shortlink-org/portolan/internal/goscan"
12
- "github.com/shortlink-org/portolan/plugin"
13
- )
14
-
15
- type Options struct {
16
- Context string `json:"context"`
17
- Service string `json:"service"`
18
- Out string `json:"out,omitempty"`
19
- }
20
-
21
- func main() {
22
- if err := run(os.Stdin, os.Stdout); err != nil {
23
- fmt.Fprintln(os.Stderr, "portolan-extract-river:", err)
24
- os.Exit(1)
25
- }
26
- }
27
-
28
- func run(stdin io.Reader, stdout io.Writer) error {
29
- return plugin.Serve(stdin, stdout, descriptor(), func(req plugin.Request, opts Options) (plugin.Response, error) {
30
- if req.Input.Root == "" {
31
- return plugin.Response{}, fmt.Errorf("no input root: an extractor has nothing to read")
32
- }
33
- if opts.Context == "" {
34
- opts.Context = goscan.Slug(filepath.Base(req.Input.Root))
35
- }
36
- if opts.Service == "" {
37
- opts.Service = goscan.Slug(filepath.Base(req.Input.Root))
38
- }
39
-
40
- return extract(req.Input, opts)
41
- })
42
- }