@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,333 +0,0 @@
1
- package main
2
-
3
- // The same fact read out of a repository written in Rust, where sqlx puts the
4
- // statement first and the values after it, one `bind` each:
5
- //
6
- // sqlx::query("INSERT INTO orders (id, customer_id, placed_at) VALUES ($1, $2, $3)")
7
- // .bind(order.id)
8
- // .bind(&order.customer_id)
9
- // .bind(order.placed_at)
10
- //
11
- // or, in the checked macros, as arguments after the literal:
12
- //
13
- // sqlx::query!("INSERT INTO orders (id, customer_id) VALUES ($1, $2)", order.id, order.customer_id)
14
- //
15
- // Read off the text like the TypeScript one, for the same reason: the
16
- // statement is a string literal, the values are expressions right after it,
17
- // and each is a field chain from the aggregate, possibly borrowed, possibly
18
- // wrapped in one conversion. Anything else is left unmapped.
19
-
20
- import (
21
- "os"
22
- "path/filepath"
23
- "regexp"
24
- "strings"
25
-
26
- "github.com/pgplex/pgparser/nodes"
27
- "github.com/shortlink-org/portolan/plugin"
28
- )
29
-
30
- func readMapsRust(root, repositoryDir, aggregate string, b *plugin.Builder) map[string]map[string]string {
31
- out := map[string]map[string]string{}
32
- dir := mapSourceDir(root, repositoryDir, aggregate)
33
- entries, err := os.ReadDir(filepath.Join(root, filepath.FromSlash(dir)))
34
- if err != nil {
35
- return out
36
- }
37
- root_ := title(aggregate)
38
- for _, entry := range entries {
39
- name := entry.Name()
40
- if entry.IsDir() || !strings.HasSuffix(name, ".rs") {
41
- continue
42
- }
43
- source, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(dir), name))
44
- if err != nil {
45
- continue
46
- }
47
- for _, stmt := range rustInsertCalls(string(source)) {
48
- table, columns, values := parseInsert(stmt.sql)
49
- if table == "" {
50
- continue
51
- }
52
- mapped := out[table]
53
- if mapped == nil {
54
- mapped = map[string]string{}
55
- out[table] = mapped
56
- }
57
- numbers := placeholderNumbers(stmt.sql)
58
- if numbers == nil || countParams(values) != len(numbers) {
59
- if countParams(values) > 0 {
60
- b.Warn(table, "the insert into "+table+" has placeholders this reader cannot number; its columns are left unmapped")
61
- }
62
- continue
63
- }
64
- seen := 0
65
- for i, column := range columns {
66
- if i >= len(values) {
67
- break
68
- }
69
- if _, ok := values[i].(*nodes.ParamRef); !ok {
70
- continue
71
- }
72
- number := numbers[seen]
73
- seen++
74
- if number < 1 || number > len(stmt.args) {
75
- b.Warn(table, "the insert refers to $"+itoa(number)+" but the call binds "+itoa(len(stmt.args))+" values; "+column+" is left unmapped")
76
- continue
77
- }
78
- field := fieldOfRust(stmt.args[number-1])
79
- if field == "" {
80
- continue
81
- }
82
- mapped[column] = root_ + "." + field
83
- }
84
- }
85
- }
86
-
87
- return out
88
- }
89
-
90
- // rustInsertCalls walks the source the way insertCalls does, with what Rust
91
- // adds to the reading: raw strings, `r"…"` and `r#"…"#`, which is how a long
92
- // statement is usually written; char literals, `'x'` and `'\n'`; and a
93
- // lifetime, `'a`, which opens no literal at all and must not be read as one.
94
- // A literal that holds an INSERT is a statement, and its values are the
95
- // `.bind(…)` chain after the call that holds it, or the arguments after it
96
- // inside a macro.
97
- func rustInsertCalls(source string) []tsInsert {
98
- var out []tsInsert
99
- for i := 0; i < len(source); i++ {
100
- c := source[i]
101
- switch {
102
- case c == '/' && i+1 < len(source) && source[i+1] == '/':
103
- for i < len(source) && source[i] != '\n' {
104
- i++
105
- }
106
- case c == '/' && i+1 < len(source) && source[i+1] == '*':
107
- end := strings.Index(source[i+2:], "*/")
108
- if end < 0 {
109
- return out
110
- }
111
- i += 2 + end + 1
112
- case c == '\'':
113
- if end := charLiteralEnd(source, i); end > 0 {
114
- i = end
115
- }
116
- case c == '"', c == 'r' && rawStringAt(source, i):
117
- start, end, hashes := stringLiteral(source, i)
118
- if end < 0 {
119
- return out
120
- }
121
- sql := source[start:end]
122
- i = end + hashes
123
- if !strings.Contains(strings.ToUpper(sql), "INSERT INTO") {
124
- continue
125
- }
126
- j := skipSpace(source, i+1)
127
- if j >= len(source) {
128
- continue
129
- }
130
- if source[j] == ',' {
131
- // A trailing comma inside the call is not the start of the
132
- // macro's arguments: the next thing decides.
133
- j = skipSpace(source, j+1)
134
- }
135
- if j >= len(source) {
136
- continue
137
- }
138
- if source[j] == ')' {
139
- // The builder shape: the values are bound one at a time after
140
- // the call closes.
141
- args, last := bindChain(source, j+1)
142
- out = append(out, tsInsert{sql: sql, args: args})
143
- i = last
144
- } else {
145
- // The macro shape: the values are the rest of the call.
146
- args, close := argsUntilClose(source, j)
147
- out = append(out, tsInsert{sql: sql, args: args})
148
- i = close
149
- }
150
- }
151
- }
152
-
153
- return out
154
- }
155
-
156
- // rawStringAt says whether the `r` at i opens a raw string: `r"` or `r#…"`.
157
- // An `r` inside a longer word, `order`, is not one, and the caller only asks
158
- // about an `r` that is not preceded by an identifier character.
159
- func rawStringAt(source string, i int) bool {
160
- if i > 0 && isIdent(source[i-1]) {
161
- return false
162
- }
163
- j := i + 1
164
- for j < len(source) && source[j] == '#' {
165
- j++
166
- }
167
-
168
- return j < len(source) && source[j] == '"'
169
- }
170
-
171
- // stringLiteral reads the literal opening at i and answers with the bounds of
172
- // its content, the index of its closing quote, and how many `#` follow that
173
- // quote for a raw string.
174
- func stringLiteral(source string, i int) (int, int, int) {
175
- if source[i] == '"' {
176
- end := closingQuote(source, i)
177
-
178
- return i + 1, end, 0
179
- }
180
- hashes := 0
181
- j := i + 1
182
- for source[j] == '#' {
183
- hashes++
184
- j++
185
- }
186
- closer := `"` + strings.Repeat("#", hashes)
187
- end := strings.Index(source[j+1:], closer)
188
- if end < 0 {
189
- return 0, -1, 0
190
- }
191
-
192
- return j + 1, j + 1 + end, hashes
193
- }
194
-
195
- // charLiteralEnd is the index of the quote closing a char literal at i, or
196
- // -1 when the quote at i is a lifetime's and closes nothing.
197
- func charLiteralEnd(source string, i int) int {
198
- if i+2 < len(source) && source[i+1] == '\\' {
199
- end := strings.IndexByte(source[i+2:], '\'')
200
- if end < 0 {
201
- return -1
202
- }
203
-
204
- return i + 2 + end
205
- }
206
- if i+2 < len(source) && source[i+2] == '\'' {
207
- return i + 2
208
- }
209
-
210
- return -1
211
- }
212
-
213
- // argsUntilClose splits the arguments from i to the `)` that closes the call
214
- // they are in, and answers with the index of that paren.
215
- func argsUntilClose(source string, i int) ([]string, int) {
216
- depth := 0
217
- var quote byte
218
- for j := i; j < len(source); j++ {
219
- c := source[j]
220
- if quote != 0 {
221
- if c == '\\' {
222
- j++
223
- } else if c == quote {
224
- quote = 0
225
- }
226
- continue
227
- }
228
- switch c {
229
- case '"':
230
- quote = c
231
- case '[', '(', '{':
232
- depth++
233
- case ']', '}':
234
- depth--
235
- case ')':
236
- if depth == 0 {
237
- return splitTopLevel(source[i:j]), j
238
- }
239
- depth--
240
- }
241
- }
242
-
243
- return nil, len(source) - 1
244
- }
245
-
246
- // bindChain reads `.bind(x)` calls one after another from i, stepping over
247
- // whitespace and comments between them, and answers with what each was
248
- // handed and the index it stopped at.
249
- func bindChain(source string, i int) ([]string, int) {
250
- var args []string
251
- last := i - 1
252
- for {
253
- j := skipSpaceAndComments(source, i)
254
- if !strings.HasPrefix(source[j:], ".bind(") {
255
- return args, last
256
- }
257
- open := j + len(".bind")
258
- close := matchingBracket(source, open)
259
- if close < 0 {
260
- return args, last
261
- }
262
- args = append(args, strings.TrimSpace(source[open+1:close]))
263
- last = close
264
- i = close + 1
265
- }
266
- }
267
-
268
- func skipSpaceAndComments(source string, i int) int {
269
- for {
270
- i = skipSpace(source, i)
271
- if strings.HasPrefix(source[i:], "//") {
272
- for i < len(source) && source[i] != '\n' {
273
- i++
274
- }
275
- continue
276
- }
277
- if strings.HasPrefix(source[i:], "/*") {
278
- end := strings.Index(source[i+2:], "*/")
279
- if end < 0 {
280
- return len(source)
281
- }
282
- i += 2 + end + 2
283
- continue
284
- }
285
-
286
- return i
287
- }
288
- }
289
-
290
- func isIdent(c byte) bool {
291
- return c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9'
292
- }
293
-
294
- var rustChain = regexp.MustCompile(`^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+$`)
295
-
296
- // fieldOfRust follows a bound value back to the field it carries, the way
297
- // fieldOfTS does: `order.customer_id` is customer_id, `&order.customer_id`
298
- // and `*order.version` the same, `order.status.as_str()` is status,
299
- // `Some(order.placed_at)` and `i64::from(order.version)` are placed_at and
300
- // version. A bare name, a literal, a call that makes its own value -
301
- // `Uuid::new_v4()` - and a call over several values carry no field.
302
- func fieldOfRust(expr string) string {
303
- expr = strings.TrimSpace(expr)
304
- expr = strings.TrimSuffix(expr, "?")
305
- for strings.HasPrefix(expr, "&") || strings.HasPrefix(expr, "*") {
306
- expr = strings.TrimSpace(strings.TrimLeft(expr, "&*"))
307
- expr = strings.TrimSpace(strings.TrimPrefix(expr, "mut "))
308
- }
309
- for strings.HasSuffix(expr, ")") {
310
- open := openingParen(expr)
311
- if open <= 0 {
312
- return ""
313
- }
314
- callee, inner := expr[:open], expr[open+1:len(expr)-1]
315
- args := splitTopLevel(inner)
316
- switch {
317
- case rustChain.MatchString(callee) && len(args) == 0:
318
- // A method on the value: the value is the chain before it.
319
- expr = callee[:strings.LastIndex(callee, ".")]
320
- case len(args) == 1:
321
- // A conversion wrapped round the value.
322
- return fieldOfRust(args[0])
323
- default:
324
- return ""
325
- }
326
- }
327
- if !rustChain.MatchString(expr) {
328
- return ""
329
- }
330
- parts := strings.Split(expr, ".")
331
-
332
- return strings.Join(parts[1:], ".")
333
- }
@@ -1,70 +0,0 @@
1
- package main
2
-
3
- import (
4
- "testing"
5
- )
6
-
7
- // The same rule as for Go and TypeScript, read off text: the chain from the
8
- // aggregate, a borrow or a dereference in front of it, one conversion round
9
- // it, a method on the value stripped.
10
- func TestFieldOfRustFollowsTheChainBorrowsAndOneWrapper(t *testing.T) {
11
- cases := map[string]string{
12
- "order.id": "id",
13
- "&order.customer_id": "customer_id",
14
- "&mut order.lines": "lines",
15
- "*order.version": "version",
16
- "order.status.as_str()": "status",
17
- "order.total.amount_minor": "total.amount_minor",
18
- "order.total.currency.code()": "total.currency",
19
- "Some(order.placed_at)": "placed_at",
20
- "i64::from(order.version)": "version",
21
- "order.basket_id.to_string()": "basket_id",
22
- "&order.customer_id.as_deref()?": "customer_id",
23
- "Uuid::new_v4()": "",
24
- "serde_json::to_value(&event)?": "",
25
- "combine(order.a, order.b)": "",
26
- "TOPIC": "",
27
- "1": "",
28
- "\"literal\"": "",
29
- "Utc::now()": "",
30
- }
31
- for in, want := range cases {
32
- if got := fieldOfRust(in); got != want {
33
- t.Errorf("fieldOfRust(%q) = %q, want %q", in, got, want)
34
- }
35
- }
36
- }
37
-
38
- func TestRustInsertCallsReadBothShapesAndStepOverLifetimes(t *testing.T) {
39
- source := `
40
- impl<'a> PostgresOrders<'a> {
41
- async fn save(&self, tx: &mut Transaction<'_, Postgres>, order: &Order) -> Result<(), Error> {
42
- sqlx::query("BEGIN").execute(&mut **tx).await?; // a literal before, which must not swallow what follows
43
- let sep = ',';
44
- sqlx::query(
45
- r#"INSERT INTO orders (id, customer_id, version) VALUES ($1, $2, 1)"#,
46
- )
47
- .bind(order.id)
48
- // the customer auth vouched for
49
- .bind(&order.customer_id)
50
- .execute(&mut **tx)
51
- .await?;
52
- let rows = sqlx::query("SELECT * FROM orders WHERE id = $1").bind(id).fetch_all(pool).await?;
53
- sqlx::query!("INSERT INTO outbox (uuid, payload) VALUES ($1, $2)", Uuid::new_v4(), serde_json::to_value(&event)?)
54
- .execute(&mut **tx)
55
- .await?;
56
- Ok(())
57
- }
58
- }
59
- `
60
- calls := rustInsertCalls(source)
61
- if len(calls) != 2 {
62
- t.Fatalf("found %d inserts, want 2", len(calls))
63
- }
64
- if len(calls[0].args) != 2 || calls[0].args[1] != "&order.customer_id" {
65
- t.Fatalf("first insert args = %q", calls[0].args)
66
- }
67
- if len(calls[1].args) != 2 || calls[1].args[1] != "serde_json::to_value(&event)?" {
68
- t.Fatalf("second insert args = %q", calls[1].args)
69
- }
70
- }
@@ -1,204 +0,0 @@
1
- package main
2
-
3
- import (
4
- "go/ast"
5
- "go/parser"
6
- "strings"
7
- "testing"
8
-
9
- "github.com/shortlink-org/portolan/plugin"
10
- )
11
-
12
- func builderFor(t *testing.T) *plugin.Builder {
13
- t.Helper()
14
-
15
- return &plugin.Builder{}
16
- }
17
-
18
- func expr(t *testing.T, src string) ast.Expr {
19
- t.Helper()
20
-
21
- parsed, err := parser.ParseExpr(src)
22
- if err != nil {
23
- t.Fatal(err)
24
- }
25
-
26
- return parsed
27
- }
28
-
29
- // The column carries the value object, not the conversion applied to it on the
30
- // way out - and the conversion is written both ways round.
31
- func TestFieldOfFollowsBothCallShapes(t *testing.T) {
32
- s := scope{packages: map[string]bool{"time": true, "pgtype": true, "uuid": true}}
33
- cases := map[string]string{
34
- "u.ID": "ID",
35
- "u.Email.String()": "Email",
36
- "nullable(s.RevokedAt)": "RevokedAt",
37
- "pgtype.Text(u.Email)": "Email",
38
- "time.Now()": "",
39
- "uuid.NewString()": "",
40
- "combine(u.A, u.B)": "",
41
- "localVariable": "",
42
- `"literal"`: "",
43
- "q.ID()": "ID",
44
- "q.Total().AmountMinor()": "Total",
45
- "q.Total().Currency()": "Total",
46
- "q.IssuedAt().Format(\"x\")": "IssuedAt",
47
- }
48
-
49
- for src, want := range cases {
50
- if got := fieldOf(expr(t, src), s); got != want {
51
- t.Errorf("fieldOf(%s) = %q, want %q", src, got, want)
52
- }
53
- }
54
- }
55
-
56
- // Inside Save the aggregate is the parameter typed as the root, and nothing
57
- // else: the loop variable over its lines carries the line's fields, and a
58
- // mapping from those to the aggregate would be a claim about the wrong type.
59
- func TestFieldOfReadsOnlyTheRootWhenTheFunctionNamesIt(t *testing.T) {
60
- s := scope{packages: map[string]bool{}, roots: map[string]bool{"q": true}}
61
- cases := map[string]string{
62
- "q.ID()": "ID",
63
- "q.BasketID()": "BasketID",
64
- "l.SKU()": "",
65
- "l.Quantity": "",
66
- "raised.Name()": "",
67
- }
68
-
69
- for src, want := range cases {
70
- if got := fieldOf(expr(t, src), s); got != want {
71
- t.Errorf("fieldOf(%s) = %q, want %q", src, got, want)
72
- }
73
- }
74
- }
75
-
76
- // The variable of a range loop stands for one element of what it walks, so a
77
- // column written from it carries a field of the elements of a field.
78
- func TestFieldOfFollowsARangeVariable(t *testing.T) {
79
- s := scope{
80
- packages: map[string]bool{},
81
- roots: map[string]bool{"q": true},
82
- aliases: map[string]ast.Expr{"l": expr(t, "q.Lines()"), "raised": expr(t, "events"), "l2": expr(t, "l2.Items")},
83
- }
84
- cases := map[string]string{
85
- "l.SKU()": "Lines.SKU",
86
- "l.UnitPrice().Currency()": "Lines.UnitPrice",
87
- "raised.Name()": "",
88
- "l2.Name": "",
89
- }
90
-
91
- for src, want := range cases {
92
- if got := fieldOf(expr(t, src), s); got != want {
93
- t.Errorf("fieldOf(%s) = %q, want %q", src, got, want)
94
- }
95
- }
96
- }
97
-
98
- // Pricing keeps its fields unexported and writes rows off getters, and its
99
- // lines are written from a loop variable over them.
100
- func TestMapsThroughGetters(t *testing.T) {
101
- b := builderFor(t)
102
- mapped := readMaps("../../examples/shop/pricing", "internal/infrastructure/repository/quote", "quote", b)
103
-
104
- want := map[string]map[string]string{
105
- "quotes": {
106
- "id": "Quote.ID",
107
- "basket_id": "Quote.BasketID",
108
- "total_minor": "Quote.Total",
109
- "currency": "Quote.Total",
110
- "state": "Quote.State",
111
- "issued_at": "Quote.IssuedAt",
112
- "expires_at": "Quote.ExpiresAt",
113
- },
114
- "quote_lines": {
115
- "quote_id": "Quote.ID",
116
- "sku": "Quote.Lines.SKU",
117
- "quantity": "Quote.Lines.Quantity",
118
- "unit_price_minor": "Quote.Lines.UnitPrice",
119
- "currency": "Quote.Lines.UnitPrice",
120
- },
121
- }
122
- for table, columns := range want {
123
- for column, field := range columns {
124
- if got := mapped[table][column]; got != field {
125
- t.Errorf("%s.%s -> %q, want %q", table, column, got, field)
126
- }
127
- }
128
- if len(mapped[table]) != len(columns) {
129
- t.Errorf("%s: mapped %v, want exactly %v", table, mapped[table], columns)
130
- }
131
- }
132
- if len(mapped["outbox"]) != 0 {
133
- t.Errorf("outbox: mapped %v, want nothing", mapped["outbox"])
134
- }
135
- }
136
-
137
- // The grammar port keeps a placeholder as a node with no number on it, so the
138
- // numbers are read off the text. Taking the order of the nodes instead would
139
- // be right for every statement written in ascending order and silently wrong
140
- // for the one that is not.
141
- func TestPlaceholderNumbersComeFromTheText(t *testing.T) {
142
- got := placeholderNumbers("INSERT INTO t (a, b, c) VALUES ($2, $1, 1)")
143
- if len(got) != 2 || got[0] != 2 || got[1] != 1 {
144
- t.Errorf("numbers = %v, want [2 1]", got)
145
- }
146
-
147
- // A RETURNING clause is not a value being written.
148
- got = placeholderNumbers("INSERT INTO t (a) VALUES ($1) RETURNING id")
149
- if len(got) != 1 || got[0] != 1 {
150
- t.Errorf("numbers = %v, want [1]", got)
151
- }
152
- }
153
-
154
- // A statement is routinely assembled from a shared column list rather than
155
- // written whole.
156
- func TestStringValueFoldsConstantsAndConcatenation(t *testing.T) {
157
- constants := map[string]string{"columns": "id, name"}
158
-
159
- got, ok := stringValue(expr(t, "`INSERT INTO t (`+columns+`) VALUES ($1, $2)`"), constants)
160
- if !ok || !strings.Contains(got, "id, name") {
161
- t.Errorf("folded = %q ok=%v", got, ok)
162
- }
163
-
164
- // Anything that cannot be folded is left alone rather than read half
165
- // resolved.
166
- if _, ok := stringValue(expr(t, "prefix + buildRest()"), constants); ok {
167
- t.Error("a call is not a constant")
168
- }
169
- }
170
-
171
- // The whole point of the field: no rule over column names produces
172
- // password_hash -> Password, and the statement that writes the row does.
173
- func TestMapsAgainstTheRealService(t *testing.T) {
174
- b := builderFor(t)
175
- mapped := readMaps("../../examples/auth", "internal/user/infrastructure/repository", "user", b)
176
-
177
- users := mapped["users"]
178
- if users["password_hash"] != "User.Password" {
179
- t.Errorf("password_hash -> %q", users["password_hash"])
180
- }
181
- if users["email"] != "User.Email" {
182
- t.Errorf("email -> %q", users["email"])
183
- }
184
- // `version` is written as a literal 1, so nothing carries it from the
185
- // aggregate and the column stays unmapped rather than guessed at.
186
- if got, ok := users["version"]; ok {
187
- t.Errorf("version should be unmapped, got %q", got)
188
- }
189
- }
190
-
191
- // The session repository builds its statement from a constant column list and
192
- // wraps one value in a conversion, so it exercises both readings at once.
193
- func TestMapsThroughAConstantColumnList(t *testing.T) {
194
- b := builderFor(t)
195
- mapped := readMaps("../../examples/auth", "internal/session/infrastructure/repository", "session", b)
196
-
197
- sessions := mapped["sessions"]
198
- if sessions["revoked_at"] != "Session.RevokedAt" {
199
- t.Errorf("revoked_at -> %q", sessions["revoked_at"])
200
- }
201
- if sessions["user_id"] != "Session.UserID" {
202
- t.Errorf("user_id -> %q", sessions["user_id"])
203
- }
204
- }