@shortlink-org/portolan 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +27 -10
  36. package/scripts/local-api.test.mjs +85 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +24 -3
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +17 -9
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -1,545 +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
- "gopkg.in/yaml.v3"
13
- )
14
-
15
- func fragment(t *testing.T) catalog.Catalog {
16
- t.Helper()
17
-
18
- resp, err := extract(
19
- plugin.Input{Root: "testdata", Commit: "abc1234", GeneratedAt: "2026-01-01T00:00:00Z"},
20
- Options{Context: "billing", Service: "invoices", Spec: "openapi.yaml"},
21
- )
22
- if err != nil {
23
- t.Fatal(err)
24
- }
25
- if len(resp.Files) != 1 {
26
- t.Fatalf("expected one fragment, got %d files", len(resp.Files))
27
- }
28
-
29
- var out catalog.Catalog
30
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
31
- t.Fatal(err)
32
- }
33
-
34
- return out
35
- }
36
-
37
- func testDocument(t *testing.T, source string) *document {
38
- t.Helper()
39
- var root yaml.Node
40
- if err := yaml.Unmarshal([]byte(source), &root); err != nil {
41
- t.Fatal(err)
42
- }
43
- node := root.Content[0]
44
- return &document{root: node, path: "test.yaml", cache: map[string]*document{}}
45
- }
46
-
47
- func provided(t *testing.T) []catalog.RpcService {
48
- t.Helper()
49
-
50
- return fragment(t).Contexts[0].Services[0].Provides
51
- }
52
-
53
- // A method is an object now, and every assertion below is about the names.
54
- func names(methods []catalog.RpcMethod) []string {
55
- out := make([]string, 0, len(methods))
56
- for _, method := range methods {
57
- out = append(out, method.Name)
58
- }
59
-
60
- return out
61
- }
62
-
63
- // Tags organise operations within one OpenAPI contract. They must not split a
64
- // document into duplicate interfaces with duplicate message inventories.
65
- func TestTagsStayWithinOneService(t *testing.T) {
66
- provides := provided(t)
67
- if len(provides) != 1 {
68
- t.Fatalf("provides = %d, want one contract", len(provides))
69
- }
70
- if provides[0].ID != "billing.v2" {
71
- t.Errorf("interface = %q", provides[0].ID)
72
- }
73
- if got := strings.Join(names(provides[0].Methods), ","); got != "raiseInvoice,GET /v2/health" {
74
- t.Errorf("methods = %q", got)
75
- }
76
- }
77
-
78
- func TestSharedSchemaIsNotRepeatedAcrossTags(t *testing.T) {
79
- doc := testDocument(t, `
80
- openapi: 3.0.3
81
- info: {title: shared, version: 1.0.0}
82
- paths:
83
- /orders:
84
- get:
85
- tags: [orders]
86
- operationId: listOrders
87
- responses:
88
- '200':
89
- content:
90
- application/json:
91
- schema: {$ref: '#/components/schemas/Result'}
92
- /refunds:
93
- get:
94
- tags: [refunds]
95
- operationId: listRefunds
96
- responses:
97
- '200':
98
- content:
99
- application/json:
100
- schema: {$ref: '#/components/schemas/Result'}
101
- components:
102
- schemas:
103
- Result: {type: object}
104
- `)
105
-
106
- services := rpcServices(doc, "shared.v1", "test.yaml", &plugin.Builder{})
107
- if len(services) != 1 || len(services[0].Methods) != 2 || len(services[0].Messages) != 1 {
108
- t.Fatalf("contract = %+v", services)
109
- }
110
- }
111
-
112
- // The id is the document's own title and major version, so it survives a patch
113
- // release without every id in the catalog changing.
114
- func TestApiIDFromTitleAndMajorVersion(t *testing.T) {
115
- for _, p := range provided(t) {
116
- if !strings.HasPrefix(p.ID, "billing.v2") {
117
- t.Errorf("unexpected id %q", p.ID)
118
- }
119
- }
120
- }
121
-
122
- // A shared response is how a spec says "the same error body as everywhere
123
- // else". Not following it would document the endpoint as returning nothing
124
- // when it fails.
125
- func TestFollowsRefsThroughSharedResponses(t *testing.T) {
126
- var names []string
127
- for _, p := range provided(t) {
128
- if p.ID != "billing.v2" {
129
- continue
130
- }
131
- for _, message := range p.Messages {
132
- names = append(names, message.Name)
133
- }
134
- }
135
-
136
- // Line arrives through RaiseRequest, Error through the shared BadRequest.
137
- want := "RaiseRequest,Invoice,Error,Line"
138
- if strings.Join(names, ",") != want {
139
- t.Errorf("messages = %v, want %s", names, want)
140
- }
141
- }
142
-
143
- func TestFieldTypesAndOptionality(t *testing.T) {
144
- var request *catalog.RpcMessage
145
- for _, p := range provided(t) {
146
- for i := range p.Messages {
147
- if p.Messages[i].Name == "RaiseRequest" {
148
- request = &p.Messages[i]
149
- }
150
- }
151
- }
152
- if request == nil {
153
- t.Fatal("no RaiseRequest message")
154
- }
155
-
156
- fields := map[string]catalog.Field{}
157
- for _, field := range request.Fields {
158
- fields[field.Name] = field
159
- }
160
-
161
- if got := fields["customerId"].Type; got != "string (uuid)" {
162
- t.Errorf("format belongs in the type, got %q", got)
163
- }
164
- if got := fields["customerId"].Doc; got != "Who is being billed." {
165
- t.Errorf("a required field carries its description alone, got %q", got)
166
- }
167
- if got := fields["lines"].Type; got != "[]Line" {
168
- t.Errorf("an array of refs, got %q", got)
169
- }
170
- // Which fields must be sent is the first thing a caller needs, and the
171
- // catalog has nowhere else to put it.
172
- if got := fields["lines"].Doc; !strings.HasPrefix(got, "Optional.") {
173
- t.Errorf("an optional field should say so, got %q", got)
174
- }
175
- }
176
-
177
- func TestComposedSchemasMapsEnumsAndNullableTypes(t *testing.T) {
178
- doc := testDocument(t, `
179
- components:
180
- schemas:
181
- Base:
182
- type: object
183
- required: [id]
184
- properties:
185
- id: {type: string, format: uuid}
186
- Account:
187
- allOf:
188
- - $ref: '#/components/schemas/Base'
189
- - type: object
190
- required: [labels]
191
- properties:
192
- labels:
193
- type: object
194
- additionalProperties: {type: string}
195
- state:
196
- type: string
197
- enum: [active, disabled]
198
- contact:
199
- nullable: true
200
- oneOf:
201
- - {type: string, format: email}
202
- - {type: string, format: phone}
203
- `)
204
- account := child(doc.root, "components", "schemas", "Account")
205
- fields := map[string]catalog.Field{}
206
- for _, field := range schemaFields(doc, account) {
207
- fields[field.Name] = field
208
- }
209
-
210
- if fields["id"].Type != "string (uuid)" || strings.HasPrefix(fields["id"].Doc, "Optional.") {
211
- t.Errorf("allOf required field = %+v", fields["id"])
212
- }
213
- if fields["labels"].Type != "map[string]string" || strings.HasPrefix(fields["labels"].Doc, "Optional.") {
214
- t.Errorf("map field = %+v", fields["labels"])
215
- }
216
- if fields["state"].Type != "string enum(active | disabled)" || !strings.HasPrefix(fields["state"].Doc, "Optional.") {
217
- t.Errorf("enum field = %+v", fields["state"])
218
- }
219
- if fields["contact"].Type != "string (email) | string (phone) | null" {
220
- t.Errorf("oneOf nullable field type = %q", fields["contact"].Type)
221
- }
222
- }
223
-
224
- func TestDiscriminatorKeepsPolymorphicMessagesVisible(t *testing.T) {
225
- doc := testDocument(t, `
226
- openapi: 3.1.0
227
- info: {title: animals, version: 1.0.0}
228
- paths:
229
- /pets/{id}:
230
- get:
231
- operationId: getPet
232
- responses:
233
- '200':
234
- content:
235
- application/json:
236
- schema: {$ref: '#/components/schemas/Pet'}
237
- components:
238
- schemas:
239
- Pet:
240
- oneOf:
241
- - {$ref: '#/components/schemas/Cat'}
242
- - {$ref: '#/components/schemas/Dog'}
243
- discriminator:
244
- propertyName: kind
245
- mapping:
246
- feline: '#/components/schemas/Cat'
247
- Cat:
248
- type: object
249
- required: [kind, lives]
250
- properties:
251
- kind: {type: string}
252
- lives: {type: integer}
253
- Dog:
254
- type: object
255
- required: [kind, good]
256
- properties:
257
- kind: {type: string}
258
- good: {type: boolean}
259
- `)
260
- services := rpcServices(doc, "animals.v1", "test.yaml", &plugin.Builder{})
261
- if len(services) != 1 || len(services[0].Methods) != 1 {
262
- t.Fatalf("services = %+v", services)
263
- }
264
- if got := services[0].Methods[0].Response; got != "Pet" {
265
- t.Fatalf("response = %q, want Pet", got)
266
- }
267
-
268
- byName := map[string]catalog.RpcMessage{}
269
- for _, message := range services[0].Messages {
270
- byName[message.Name] = message
271
- }
272
- pet, ok := byName["Pet"]
273
- if !ok || pet.Discriminator == nil {
274
- t.Fatalf("Pet discriminator is missing: %+v", pet)
275
- }
276
- if pet.Discriminator.Property != "kind" {
277
- t.Errorf("property = %q", pet.Discriminator.Property)
278
- }
279
- if len(pet.Discriminator.Variants) != 2 {
280
- t.Fatalf("variants = %+v", pet.Discriminator.Variants)
281
- }
282
- if got := pet.Discriminator.Variants[0]; got.Value != "feline" || got.Message != "Cat" {
283
- t.Errorf("mapped variant = %+v", got)
284
- }
285
- if got := pet.Discriminator.Variants[1]; got.Value != "Dog" || got.Message != "Dog" {
286
- t.Errorf("implicit variant = %+v", got)
287
- }
288
- if _, ok := byName["Cat"]; !ok {
289
- t.Error("Cat concrete message is missing")
290
- }
291
- if _, ok := byName["Dog"]; !ok {
292
- t.Error("Dog concrete message is missing")
293
- }
294
- }
295
-
296
- func TestRelativeExternalSchemaRefsAreLoaded(t *testing.T) {
297
- dir := t.TempDir()
298
- common := filepath.Join(dir, "common.yaml")
299
- root := filepath.Join(dir, "openapi.yaml")
300
- if err := os.WriteFile(common, []byte(`components:
301
- schemas:
302
- External:
303
- type: object
304
- required: [code]
305
- properties:
306
- code: {type: integer, format: int64}
307
- `), 0o600); err != nil {
308
- t.Fatal(err)
309
- }
310
- if err := os.WriteFile(root, []byte(`openapi: 3.1.0
311
- info: {title: external, version: 1.0.0}
312
- paths:
313
- /external:
314
- get:
315
- operationId: getExternal
316
- responses:
317
- '200':
318
- content:
319
- application/json:
320
- schema: {$ref: './common.yaml#/components/schemas/External'}
321
- `), 0o600); err != nil {
322
- t.Fatal(err)
323
- }
324
- doc, err := load(root)
325
- if err != nil {
326
- t.Fatal(err)
327
- }
328
- services := rpcServices(doc, "external.v1", root, &plugin.Builder{})
329
- if len(services) != 1 || len(services[0].Messages) != 1 {
330
- t.Fatalf("services = %+v", services)
331
- }
332
- message := services[0].Messages[0]
333
- if message.Name != "External" || len(message.Fields) != 1 || message.Fields[0].Type != "integer (int64)" {
334
- t.Errorf("external message = %+v", message)
335
- }
336
- }
337
-
338
- // The fragment describes what the service answers and nothing else; the name
339
- // and readme belong to whichever source knows them.
340
- func TestFragmentClaimsNothingItDoesNotKnow(t *testing.T) {
341
- service := fragment(t).Contexts[0].Services[0]
342
-
343
- if service.Name != "" || service.Readme != "" || service.Repo != "" {
344
- t.Errorf("the api fragment should not name the service: %+v", service)
345
- }
346
- if len(service.Aggregates) != 0 {
347
- t.Error("the api fragment should carry no aggregates")
348
- }
349
- }
350
-
351
- // What the operation sends and answers with. The document names both whenever
352
- // the body is a $ref; a success with no body at all is answered by its status,
353
- // because 204 is a real answer and an empty string reads as an unread one.
354
- func TestShapesOnEitherSide(t *testing.T) {
355
- byName := map[string]catalog.RpcMethod{}
356
- for _, p := range provided(t) {
357
- for _, method := range p.Methods {
358
- byName[method.Name] = method
359
- }
360
- }
361
-
362
- raise, ok := byName["raiseInvoice"]
363
- if !ok {
364
- t.Fatal("no raiseInvoice")
365
- }
366
- if raise.Request != "RaiseRequest" || raise.Response != "Invoice" {
367
- t.Errorf("raiseInvoice sends %q and returns %q", raise.Request, raise.Response)
368
- }
369
-
370
- health, ok := byName["GET /v2/health"]
371
- if !ok {
372
- t.Fatal("no health operation")
373
- }
374
- if health.Request != "" || health.Response != "200" {
375
- t.Errorf("health sends %q and returns %q", health.Request, health.Response)
376
- }
377
- }
378
-
379
- // The same document, read as a copy vendored from outside the estate: the
380
- // fragment carries no context and no service, only the external and what it
381
- // answers on, under the id the manifest gave it.
382
- func TestExternalCarriesNoService(t *testing.T) {
383
- resp, err := extract(
384
- plugin.Input{Root: "testdata", Commit: "abc1234", GeneratedAt: "2026-01-01T00:00:00Z"},
385
- Options{External: "psp", ExternalName: "PSP", ExternalURL: "https://psp.example/docs", API: "psp.v1", Spec: "openapi.yaml"},
386
- )
387
- if err != nil {
388
- t.Fatal(err)
389
- }
390
- var out catalog.Catalog
391
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
392
- t.Fatal(err)
393
- }
394
- if len(out.Contexts) != 0 {
395
- t.Errorf("an external's fragment names %d contexts", len(out.Contexts))
396
- }
397
- if len(out.Externals) != 1 {
398
- t.Fatalf("externals = %+v", out.Externals)
399
- }
400
- ext := out.Externals[0]
401
- if ext.ID != "psp" || ext.Slug != "psp" || ext.Name != "PSP" || ext.URL != "https://psp.example/docs" {
402
- t.Errorf("external = %+v", ext)
403
- }
404
- var ids []string
405
- for _, p := range ext.Provides {
406
- ids = append(ids, p.ID)
407
- }
408
- if strings.Join(ids, ",") != "psp.v1" {
409
- t.Errorf("interfaces = %v", ids)
410
- }
411
-
412
- // A dotted id would put the external inside a context nobody declared.
413
- if _, err := extract(plugin.Input{Root: "testdata"}, Options{External: "shop.psp"}); err == nil {
414
- t.Error("an external with a dot in its id was accepted")
415
- }
416
- }
417
-
418
- // A Swagger 2.0 document keeps its schemas under definitions, sends its body
419
- // as a parameter and puts the response schema straight on the response. A
420
- // generator that still writes 2.0 - swag, for one - documents a service
421
- // exactly as well as one that writes 3.
422
- func TestSwagger2DefinitionsAndBodyParameters(t *testing.T) {
423
- resp, err := extract(
424
- plugin.Input{Root: "testdata/swagger2", Commit: "abc1234", GeneratedAt: "2026-01-01T00:00:00Z"},
425
- Options{Context: "avia", Service: "aviasupp", Spec: "swagger.yaml"},
426
- )
427
- if err != nil {
428
- t.Fatal(err)
429
- }
430
- var out catalog.Catalog
431
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
432
- t.Fatal(err)
433
- }
434
-
435
- byName := map[string]catalog.RpcMethod{}
436
- shapes := map[string][]string{}
437
- for _, p := range out.Contexts[0].Services[0].Provides {
438
- for _, method := range p.Methods {
439
- byName[method.Name] = method
440
- }
441
- for _, message := range p.Messages {
442
- var fields []string
443
- for _, field := range message.Fields {
444
- fields = append(fields, field.Name+" "+field.Type)
445
- }
446
- shapes[message.Name] = fields
447
- }
448
- }
449
-
450
- book, ok := byName["POST /book"]
451
- if !ok {
452
- t.Fatalf("no POST /book among %v", byName)
453
- }
454
- if book.Request != "book.Request" || book.Response != "book.Response" {
455
- t.Errorf("book sends %q and returns %q", book.Request, book.Response)
456
- }
457
-
458
- // The body parameter is inherited from the path item, and a list of a
459
- // definition is that definition with brackets.
460
- search, ok := byName["search"]
461
- if !ok {
462
- t.Fatalf("no search among %v", byName)
463
- }
464
- if search.Request != "search.Request" || search.Response != "search.Offer[]" {
465
- t.Errorf("search sends %q and returns %q", search.Request, search.Response)
466
- }
467
-
468
- if got := strings.Join(shapes["book.Request"], ", "); got != "offer_id string, passengers []book.Passenger" {
469
- t.Errorf("book.Request = %q", got)
470
- }
471
- if _, ok := shapes["book.Passenger"]; !ok {
472
- t.Errorf("book.Passenger, reached through book.Request, is not among the messages: %v", shapes)
473
- }
474
- if _, ok := shapes["search.Offer"]; !ok {
475
- t.Errorf("search.Offer, the response item, is not among the messages: %v", shapes)
476
- }
477
- }
478
-
479
- // Nothing named: the tree says which documents this service implements and
480
- // which it calls. A server generated from a document means provides; a client
481
- // generated from one means a system outside the estate, named by the
482
- // document's own title, unless the manifest says the api is one of ours. A
483
- // document with neither beside it is reported and left alone.
484
- func TestATreeSaysWhatIsImplementedAndWhatIsCalled(t *testing.T) {
485
- read := func(opts Options) (catalog.Catalog, plugin.Response) {
486
- t.Helper()
487
- opts.Context, opts.Service = "avia", "aviasupp"
488
- resp, err := extract(plugin.Input{Root: "testdata/discover", Commit: "abc1234", GeneratedAt: "2026-01-01T00:00:00Z"}, opts)
489
- if err != nil {
490
- t.Fatal(err)
491
- }
492
- var out catalog.Catalog
493
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
494
- t.Fatal(err)
495
- }
496
- return out, resp
497
- }
498
-
499
- out, resp := read(Options{Peers: map[string]string{"partner-api.v2": "shop.partner"}})
500
-
501
- var provided []string
502
- for _, p := range out.Contexts[0].Services[0].Provides {
503
- provided = append(provided, p.ID+" from "+p.Source)
504
- }
505
- if strings.Join(provided, ", ") != "aviasupp.v1 from testdata/discover/docs/swagger.yaml" {
506
- t.Errorf("provides = %v", provided)
507
- }
508
-
509
- if len(out.Externals) != 1 {
510
- t.Fatalf("externals = %+v", out.Externals)
511
- }
512
- acme := out.Externals[0]
513
- if acme.ID != "acme-flights" || acme.Slug != "acme-flights" || acme.Name != "Acme Flights API" || acme.Summary != "Flights and ancillaries from Acme." || acme.URL != "https://developer.acme.example/flights" {
514
- t.Errorf("external = %+v", acme)
515
- }
516
- if len(acme.Provides) != 1 || acme.Provides[0].ID != "acme-flights-api.v1" {
517
- t.Errorf("acme provides %+v", acme.Provides)
518
- }
519
-
520
- warned := func(substring string) bool {
521
- for _, w := range resp.Warnings() {
522
- if strings.Contains(w.Message, substring) {
523
- return true
524
- }
525
- }
526
- return false
527
- }
528
- if !warned("found testdata/discover/internal/stray/openapi.yaml, and nothing beside it says") {
529
- t.Errorf("the stray document was not reported; warnings = %+v", resp.Warnings())
530
- }
531
- if !warned("nameless/client/openapi.yaml is called from here and its document has no title") {
532
- t.Errorf("the nameless document was not reported; warnings = %+v", resp.Warnings())
533
- }
534
-
535
- // The manifest may name the system itself; and a called document whose
536
- // api no peers line claims is a system outside the estate too.
537
- out, _ = read(Options{Externals: map[string]string{"acme-flights-api.v1": "acme"}})
538
- var ids []string
539
- for _, e := range out.Externals {
540
- ids = append(ids, e.ID)
541
- }
542
- if strings.Join(ids, ",") != "acme,partner" {
543
- t.Errorf("externals = %v", ids)
544
- }
545
- }
@@ -1,75 +0,0 @@
1
- // Package main is portolan-extract-openapi: an OpenAPI document in, a catalog
2
- // fragment out.
3
- //
4
- // It describes one aspect of a service - what it answers - and nothing else.
5
- // The aggregates come from a different extractor reading a different part of
6
- // the tree, and the two meet in the merge. Neither knows the other exists,
7
- // which is the whole reason each of them stays small.
8
- package main
9
-
10
- import (
11
- "fmt"
12
- "io"
13
- "os"
14
-
15
- "github.com/shortlink-org/portolan/plugin"
16
- )
17
-
18
- // Options are what the manifest tells the extractor: the things an OpenAPI
19
- // document does not say about the estate it belongs to.
20
- type Options struct {
21
- Context string `json:"context"`
22
- Service string `json:"service"`
23
-
24
- // Spec is the document, relative to the input root. Left out, the
25
- // extractor walks the tree for every document and reads what sits beside
26
- // each one: a server generated from it means this service implements it,
27
- // a client generated from it means this service calls it, and a called
28
- // document names a system outside the estate unless `peers` says the api
29
- // is one of ours.
30
- Spec string `json:"spec,omitempty"`
31
-
32
- // Peers says which called documents are ours, as the api id to the
33
- // service that implements it: {"auth.v1": "auth.auth"}. A called document
34
- // whose api is named here is that service's to describe, and is skipped.
35
- Peers map[string]string `json:"peers,omitempty"`
36
-
37
- // Externals names a called document's system when the name its title
38
- // would give is not the one wanted, as the api id to the external's bare
39
- // id: {"stripe.v1": "stripe"}.
40
- Externals map[string]string `json:"externals,omitempty"`
41
-
42
- // API prefixes the ids of the rpc services this produces. Left out, it is
43
- // built from the document's title and major version - `auth` 1.0.0 gives
44
- // `auth.v1`, and a tag called `users` gives `auth.v1.Users`.
45
- API string `json:"api,omitempty"`
46
-
47
- // External names a system outside the estate the document belongs to,
48
- // when it is not one of ours: the copy vendored beside an adapter, read
49
- // for what the third party answers on. Set, the fragment carries no
50
- // context and no service, only the external, and `context` and `service`
51
- // are not read.
52
- External string `json:"external,omitempty"`
53
- ExternalName string `json:"externalName,omitempty"`
54
- ExternalSummary string `json:"externalSummary,omitempty"`
55
- ExternalURL string `json:"externalUrl,omitempty"`
56
-
57
- Out string `json:"out,omitempty"`
58
- }
59
-
60
- func main() {
61
- if err := run(os.Stdin, os.Stdout); err != nil {
62
- fmt.Fprintln(os.Stderr, "portolan-extract-openapi:", err)
63
- os.Exit(1)
64
- }
65
- }
66
-
67
- func run(stdin io.Reader, stdout io.Writer) error {
68
- return plugin.Serve(stdin, stdout, descriptor(), func(req plugin.Request, opts Options) (plugin.Response, error) {
69
- if req.Input.Root == "" {
70
- return plugin.Response{}, fmt.Errorf("no input root: an extractor has nothing to read")
71
- }
72
-
73
- return extract(req.Input, opts)
74
- })
75
- }