@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,705 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "net/url"
6
- "path/filepath"
7
- "sort"
8
- "strconv"
9
- "strings"
10
- "unicode"
11
-
12
- "github.com/shortlink-org/portolan/catalog"
13
- "github.com/shortlink-org/portolan/internal/gohttp"
14
- "github.com/shortlink-org/portolan/plugin"
15
- )
16
-
17
- func extract(in plugin.Input, opts Options) (plugin.Response, error) {
18
- result, err := gohttp.Analyze(in.Root)
19
- if err != nil {
20
- return plugin.Response{}, err
21
- }
22
- b := &plugin.Builder{}
23
- for _, warning := range result.Warnings {
24
- b.Warn(in.Root, warning)
25
- }
26
- if result.TypedCallGraphError != "" {
27
- b.Warn(in.Root, "typed call graph unavailable; using syntax fallback: "+result.TypedCallGraphError)
28
- }
29
-
30
- serviceID := opts.Context + "." + opts.Service
31
- externals := externalCatalog(result.Contracts, result.Calls, opts)
32
- consumes := make([]catalog.RpcCall, 0, len(result.Calls))
33
- seenCalls := map[string]bool{}
34
- for _, call := range result.Calls {
35
- peer, status := peerOf(call, opts)
36
- if seenCalls[call.ID] {
37
- continue
38
- }
39
- seenCalls[call.ID] = true
40
- source := call.Source.String()
41
- if call.Contract != "" {
42
- source = call.Contract
43
- }
44
- consumes = append(consumes, catalog.RpcCall{
45
- ID: call.ID, Peer: peer, Status: status, Source: source,
46
- Note: callNote(call),
47
- })
48
- }
49
- sort.Slice(consumes, func(i, j int) bool { return consumes[i].ID < consumes[j].ID })
50
-
51
- endpointFlows, covered := flowsOfEndpoints(serviceID, opts.Context, result.EndpointFlows, opts)
52
- coverEndpointDescendants(covered, result.EndpointFlows, result.Flows)
53
- rootFlows := flowsOfRoots(serviceID, opts.Context, result.RootFlows, opts, covered)
54
- coverRootDescendants(covered, result.RootFlows, result.Flows)
55
- flows := append(endpointFlows, rootFlows...)
56
- flows = append(flows, flowsOfGroupsExcept(serviceID, opts.Context, result.Flows, opts, covered)...)
57
- if len(result.Calls) == 0 {
58
- b.Warn(in.Root, "no outbound net/http, oapi-codegen, or SOAP calls were found")
59
- }
60
- fragment := catalog.Catalog{
61
- GeneratedAt: in.GeneratedAt,
62
- Commit: in.Commit,
63
- Contexts: []catalog.BoundedContext{{
64
- ID: opts.Context, Slug: opts.Context,
65
- Services: []catalog.Service{{
66
- ID: serviceID, Slug: opts.Service,
67
- Provides: []catalog.RpcService{}, Consumes: consumes, Aggregates: []catalog.Aggregate{},
68
- }},
69
- }},
70
- Defs: map[string]catalog.TypeDef{}, Flows: flows, Adrs: []catalog.Adr{}, Externals: externals,
71
- }
72
- encoded, err := json.MarshalIndent(fragment, "", " ")
73
- if err != nil {
74
- return plugin.Response{}, err
75
- }
76
- b.File(firstNonEmpty(opts.Out, "http-clients.json"), string(encoded)+"\n")
77
- return b.Response(), nil
78
- }
79
-
80
- func externalCatalog(contracts []gohttp.Contract, calls []gohttp.Call, opts Options) []catalog.External {
81
- used := map[string]bool{}
82
- called := map[string]bool{}
83
- for _, call := range calls {
84
- used[call.API] = call.API != ""
85
- called[call.ID] = true
86
- }
87
- byExternal := map[string]*catalog.External{}
88
- for _, contract := range contracts {
89
- if !used[contract.API] || opts.Peers[contract.API] != "" {
90
- continue
91
- }
92
- external := firstNonEmpty(opts.Externals[contract.API], contract.External)
93
- if external == "" {
94
- continue
95
- }
96
- target := byExternal[external]
97
- if target == nil {
98
- target = &catalog.External{
99
- ID: external, Slug: external, Name: firstNonEmpty(contract.Name, title(external)),
100
- Summary: contract.Summary, URL: contract.URL, Provides: []catalog.RpcService{},
101
- }
102
- byExternal[external] = target
103
- }
104
- byInterface := map[string][]catalog.RpcMethod{}
105
- for _, op := range contract.Operations {
106
- if !called[op.CallID(contract.API)] {
107
- continue
108
- }
109
- iface := op.Interface(contract.API)
110
- byInterface[iface] = append(byInterface[iface], catalog.RpcMethod{
111
- Name: op.ID, HTTP: &catalog.HttpRoute{Method: op.Verb, Path: op.Path},
112
- })
113
- }
114
- for _, op := range contract.SOAP {
115
- if !called[op.Interface+"/"+op.ID] {
116
- continue
117
- }
118
- byInterface[op.Interface] = append(byInterface[op.Interface], catalog.RpcMethod{
119
- Name: op.ID, Request: op.Request, Response: op.Response,
120
- SOAP: &catalog.SoapRoute{
121
- Action: op.Action, Version: op.Version, Style: op.Style,
122
- Endpoint: op.Endpoint, Binding: op.Binding,
123
- Faults: append([]string(nil), op.Faults...), Headers: append([]string(nil), op.Headers...),
124
- },
125
- })
126
- }
127
- var provides []catalog.RpcService
128
- for id, methods := range byInterface {
129
- sort.Slice(methods, func(i, j int) bool { return methods[i].Name < methods[j].Name })
130
- provides = append(provides, catalog.RpcService{ID: id, Source: contract.Source, Methods: methods})
131
- }
132
- sort.Slice(provides, func(i, j int) bool { return provides[i].ID < provides[j].ID })
133
- for _, provided := range provides {
134
- at := -1
135
- for i := range target.Provides {
136
- if target.Provides[i].ID == provided.ID {
137
- at = i
138
- break
139
- }
140
- }
141
- if at < 0 {
142
- target.Provides = append(target.Provides, provided)
143
- continue
144
- }
145
- seenMethod := map[string]bool{}
146
- for _, method := range target.Provides[at].Methods {
147
- seenMethod[method.Name] = true
148
- }
149
- for _, method := range provided.Methods {
150
- if !seenMethod[method.Name] {
151
- target.Provides[at].Methods = append(target.Provides[at].Methods, method)
152
- seenMethod[method.Name] = true
153
- }
154
- }
155
- sort.Slice(target.Provides[at].Methods, func(i, j int) bool {
156
- return target.Provides[at].Methods[i].Name < target.Provides[at].Methods[j].Name
157
- })
158
- }
159
- }
160
- var out []catalog.External
161
- for _, external := range byExternal {
162
- sort.Slice(external.Provides, func(i, j int) bool { return external.Provides[i].ID < external.Provides[j].ID })
163
- out = append(out, *external)
164
- }
165
- sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
166
- return out
167
- }
168
-
169
- func flowsOf(serviceID, context string, calls []gohttp.Call, opts Options) []catalog.Flow {
170
- byFunction := map[string][]gohttp.Call{}
171
- for _, call := range calls {
172
- byFunction[call.Function] = append(byFunction[call.Function], call)
173
- }
174
- functions := make([]string, 0, len(byFunction))
175
- for function := range byFunction {
176
- functions = append(functions, function)
177
- }
178
- sort.Strings(functions)
179
- groups := make([]gohttp.FlowGroup, 0, len(functions))
180
- for _, function := range functions {
181
- groups = append(groups, gohttp.FlowGroup{Function: function, Calls: byFunction[function]})
182
- }
183
- return flowsOfGroups(serviceID, context, groups, opts)
184
- }
185
-
186
- func flowsOfGroups(serviceID, context string, groups []gohttp.FlowGroup, opts Options) []catalog.Flow {
187
- return flowsOfGroupsExcept(serviceID, context, groups, opts, nil)
188
- }
189
-
190
- func flowsOfGroupsExcept(serviceID, context string, groups []gohttp.FlowGroup, opts Options, excluded map[string]bool) []catalog.Flow {
191
- var flows []catalog.Flow
192
- for _, flowGroup := range groups {
193
- if excluded[flowGroup.Function] {
194
- continue
195
- }
196
- function := flowGroup.Function
197
- group := flowGroup.Calls
198
- if len(group) == 0 {
199
- continue
200
- }
201
- participants := []catalog.Participant{{ID: serviceID, Kind: catalog.ParticipantService, Context: &context}}
202
- participantSeen := map[string]bool{serviceID: true}
203
- callSteps := make([]*catalog.Step, 0, len(group))
204
- for index, call := range group {
205
- peer, status := peerOf(call, opts)
206
- participant := participantOf(peer, call, opts)
207
- if !participantSeen[participant.ID] {
208
- participants = append(participants, participant)
209
- participantSeen[participant.ID] = true
210
- }
211
- note := callNote(call)
212
- if len(call.Conditions) > 0 {
213
- condition := "when " + strings.Join(uniqueStrings(call.Conditions), " and ")
214
- if note == "" {
215
- note = condition
216
- } else {
217
- note += "; " + condition
218
- }
219
- }
220
- if len(call.Chain) > 1 {
221
- chain := "via " + strings.Join(call.Chain, " → ")
222
- if note == "" {
223
- note = chain
224
- } else {
225
- note += "; " + chain
226
- }
227
- }
228
- callSteps = append(callSteps, &catalog.Step{
229
- Type: "step", ID: "s" + strconv.Itoa(index+1), From: serviceID, To: participant.ID,
230
- Kind: catalog.StepRPC, Ref: call.ID, Label: callLabel(call), Status: status,
231
- Note: note, Line: call.Source.String(),
232
- })
233
- }
234
- steps := catalog.FlowNodes{}
235
- if left, right, ok := complementaryCalls(group); ok && len(group) == 2 {
236
- steps = append(steps, &catalog.Alt{
237
- Type: "alt", ID: "alt1",
238
- Branches: []catalog.AltBranch{
239
- {Title: left, Steps: catalog.FlowNodes{callSteps[0]}},
240
- {Title: right, Steps: catalog.FlowNodes{callSteps[1]}},
241
- },
242
- })
243
- } else {
244
- for _, step := range callSteps {
245
- steps = append(steps, step)
246
- }
247
- }
248
- name := flowName(function)
249
- flowSource := flowGroup.Source.String()
250
- if flowSource == "" {
251
- flowSource = group[0].Source.String()
252
- }
253
- flows = append(flows, catalog.Flow{
254
- ID: "flow." + serviceID + ".http-client." + slug(function),
255
- Slug: serviceID + "-http-client-" + slug(function),
256
- Name: name + " → outbound APIs",
257
- Summary: standaloneFlowSummary(function, flowGroup.Callers),
258
- Source: flowSource,
259
- Trigger: &catalog.FlowTrigger{
260
- Kind: "unproven", Label: "No execution root proven", Confidence: "low",
261
- },
262
- EntryPoint: function, Owner: context,
263
- Participants: participants, Steps: steps,
264
- })
265
- }
266
- return flows
267
- }
268
-
269
- func flowsOfRoots(serviceID, context string, roots []gohttp.RootFlow, opts Options, covered map[string]bool) []catalog.Flow {
270
- flows := make([]catalog.Flow, 0, len(roots))
271
- for _, root := range roots {
272
- for _, function := range root.Covered {
273
- covered[function] = true
274
- }
275
- actorID := serviceID + ".api-client"
276
- actorLabel := "API client"
277
- firstKind := catalog.StepRPC
278
- firstLabel := strings.TrimSpace(root.Method + " " + root.Path)
279
- if root.Kind == gohttp.RootCallback {
280
- actorID = serviceID + ".callback-sender"
281
- actorLabel = "Callback sender"
282
- }
283
- if root.Kind == gohttp.RootStartup {
284
- actorID = serviceID + ".process"
285
- actorLabel = "Process startup"
286
- firstKind = catalog.StepCall
287
- firstLabel = root.Label
288
- }
289
- if root.Kind == gohttp.RootScheduled {
290
- actorID = serviceID + ".scheduler"
291
- actorLabel = "Scheduler"
292
- firstKind = catalog.StepCall
293
- firstLabel = root.Label
294
- }
295
- participants := []catalog.Participant{
296
- {ID: actorID, Kind: catalog.ParticipantActor, Label: actorLabel},
297
- {ID: serviceID, Kind: catalog.ParticipantService, Context: &context},
298
- }
299
- participantSeen := map[string]bool{actorID: true, serviceID: true}
300
- steps := catalog.FlowNodes{&catalog.Step{
301
- Type: "step", ID: "s1", From: actorID, To: serviceID,
302
- Kind: firstKind, Label: firstLabel, Status: catalog.StatusDeclared,
303
- Line: root.Source.String(),
304
- }}
305
- for index, call := range root.Calls {
306
- peer, status := peerOf(call, opts)
307
- participant := participantOf(peer, call, opts)
308
- if !participantSeen[participant.ID] {
309
- participants = append(participants, participant)
310
- participantSeen[participant.ID] = true
311
- }
312
- note := callNote(call)
313
- if len(call.Conditions) > 0 {
314
- condition := "when " + strings.Join(uniqueStrings(call.Conditions), " and ")
315
- if note == "" {
316
- note = condition
317
- } else {
318
- note += "; " + condition
319
- }
320
- }
321
- if len(call.Chain) > 1 {
322
- chain := "via " + strings.Join(call.Chain, " → ")
323
- if note == "" {
324
- note = chain
325
- } else {
326
- note += "; " + chain
327
- }
328
- }
329
- steps = append(steps, &catalog.Step{
330
- Type: "step", ID: "s" + strconv.Itoa(index+2), From: serviceID, To: participant.ID,
331
- Kind: catalog.StepRPC, Ref: call.ID, Label: callLabel(call), Status: status,
332
- Note: note, Line: call.Source.String(),
333
- })
334
- }
335
- reached := append([]string{}, root.Covered...)
336
- if root.Handler != "" {
337
- reached = append(reached, root.Handler)
338
- }
339
- markLastStepReaches(steps, reached)
340
- summary := "Source-backed execution path from a concrete root to outbound APIs."
341
- switch root.Kind {
342
- case gohttp.RootCallback:
343
- summary = "Source-backed callback path from the inbound webhook to outbound APIs."
344
- case gohttp.RootStartup:
345
- summary = "Source-backed startup path executed while the process is assembled."
346
- case gohttp.RootScheduled:
347
- summary = "Source-backed scheduled path from timer registration to outbound APIs."
348
- case gohttp.RootHTTP:
349
- summary = "Source-backed request path from the inbound endpoint to outbound APIs."
350
- }
351
- rootIdentity := root.Label
352
- if root.Kind == gohttp.RootStartup {
353
- rootIdentity = strings.TrimPrefix(rootIdentity, "Startup → ")
354
- }
355
- rootSlug := slug(string(root.Kind) + "-" + rootIdentity)
356
- flows = append(flows, catalog.Flow{
357
- ID: "flow." + serviceID + ".root." + rootSlug,
358
- Slug: serviceID + "-root-" + rootSlug,
359
- Name: root.Label + " → outbound APIs", Summary: summary,
360
- Source: root.Source.String(),
361
- Trigger: &catalog.FlowTrigger{
362
- Kind: string(root.Kind), Label: root.Label, Confidence: root.Confidence,
363
- },
364
- Owner: context, Participants: participants, Steps: steps,
365
- })
366
- }
367
- return flows
368
- }
369
-
370
- func standaloneFlowSummary(function string, callers []string) string {
371
- base := "Source-backed outbound calls made by '" + function + "'."
372
- if len(callers) == 0 {
373
- return base + " No source caller was found; this may be a public entrypoint, a background task, or unreachable code."
374
- }
375
- displays := make([]string, 0, len(callers))
376
- for _, caller := range callers {
377
- displays = append(displays, flowName(caller))
378
- }
379
- return base + " Source callers were found (" + strings.Join(uniqueStrings(displays), ", ") + "), but no inbound or asynchronous root was proven."
380
- }
381
-
382
- func flowsOfEndpoints(serviceID, context string, endpoints []gohttp.EndpointFlow, opts Options) ([]catalog.Flow, map[string]bool) {
383
- covered := map[string]bool{}
384
- flows := make([]catalog.Flow, 0, len(endpoints))
385
- for _, endpoint := range endpoints {
386
- actorID := serviceID + ".api-client"
387
- participants := []catalog.Participant{
388
- {ID: actorID, Kind: catalog.ParticipantActor, Label: "API client"},
389
- {ID: serviceID, Kind: catalog.ParticipantService, Context: &context},
390
- }
391
- participantSeen := map[string]bool{actorID: true, serviceID: true}
392
- branches := make([]catalog.AltBranch, 0, len(endpoint.Branches))
393
- stepIndex := 2
394
- for _, branch := range endpoint.Branches {
395
- covered[branch.Function] = true
396
- steps := catalog.FlowNodes{}
397
- for _, call := range branch.Calls {
398
- peer, status := peerOf(call, opts)
399
- participant := participantOf(peer, call, opts)
400
- if !participantSeen[participant.ID] {
401
- participants = append(participants, participant)
402
- participantSeen[participant.ID] = true
403
- }
404
- note := callNote(call)
405
- if len(call.Chain) > 1 {
406
- chain := "via " + strings.Join(call.Chain, " → ")
407
- if note == "" {
408
- note = chain
409
- } else {
410
- note += "; " + chain
411
- }
412
- }
413
- steps = append(steps, &catalog.Step{
414
- Type: "step", ID: "s" + strconv.Itoa(stepIndex), From: serviceID, To: participant.ID,
415
- Kind: catalog.StepRPC, Ref: call.ID, Label: callLabel(call), Status: status,
416
- Note: note, Line: call.Source.String(),
417
- })
418
- stepIndex++
419
- }
420
- if len(steps) == 0 {
421
- provider := filepath.Base(branch.Provider)
422
- steps = append(steps, &catalog.Step{
423
- Type: "step", ID: "s" + strconv.Itoa(stepIndex), From: serviceID, To: serviceID,
424
- Kind: catalog.StepCall, Label: title(provider) + " " + title(branch.Operation),
425
- Status: catalog.StatusDeclared,
426
- Note: "provider implementation is source-backed; its outbound transport was not resolved in the analyzed source",
427
- Line: branch.Source.String(),
428
- })
429
- stepIndex++
430
- }
431
- markLastStepReaches(steps, []string{branch.Function})
432
- branches = append(branches, catalog.AltBranch{
433
- Title: "connector = \"" + branch.Condition + "\"", Steps: steps,
434
- })
435
- }
436
- steps := catalog.FlowNodes{&catalog.Step{
437
- Type: "step", ID: "s1", From: actorID, To: serviceID,
438
- Kind: catalog.StepRPC, Label: strings.TrimSpace(endpoint.Method + " " + endpoint.Path),
439
- Status: catalog.StatusDeclared, Line: endpoint.Source.String(),
440
- }}
441
- if len(branches) == 1 {
442
- steps = append(steps, branches[0].Steps...)
443
- } else {
444
- steps = append(steps, &catalog.Alt{Type: "alt", ID: "providers", Branches: branches})
445
- }
446
- name := strings.TrimSpace(endpoint.Method + " " + endpoint.Path)
447
- flows = append(flows, catalog.Flow{
448
- ID: "flow." + serviceID + ".endpoint." + slug(name),
449
- Slug: serviceID + "-endpoint-" + slug(name),
450
- Name: name + " → provider APIs",
451
- Summary: "Source-backed request path from the inbound endpoint through provider selection to outbound APIs.",
452
- Source: endpoint.Source.String(),
453
- Trigger: &catalog.FlowTrigger{
454
- Kind: "http", Label: name, Confidence: "high",
455
- },
456
- Owner: context, Participants: participants, Steps: steps,
457
- })
458
- }
459
- return flows, covered
460
- }
461
-
462
- func markLastStepReaches(nodes catalog.FlowNodes, reaches []string) {
463
- for index := len(nodes) - 1; index >= 0; index-- {
464
- switch node := nodes[index].(type) {
465
- case *catalog.Step:
466
- node.Reaches = uniqueStrings(reaches)
467
- return
468
- case *catalog.Alt:
469
- for branch := range node.Branches {
470
- markLastStepReaches(node.Branches[branch].Steps, reaches)
471
- }
472
- return
473
- case *catalog.Parallel:
474
- for branch := range node.Branches {
475
- markLastStepReaches(node.Branches[branch], reaches)
476
- }
477
- return
478
- case *catalog.Loop:
479
- markLastStepReaches(node.Steps, reaches)
480
- return
481
- }
482
- }
483
- }
484
-
485
- func coverRootDescendants(covered map[string]bool, roots []gohttp.RootFlow, groups []gohttp.FlowGroup) {
486
- for _, root := range roots {
487
- callKeys := map[string]bool{}
488
- for _, call := range root.Calls {
489
- callKeys[endpointCallKey(call)] = true
490
- }
491
- for _, group := range groups {
492
- if covered[group.Function] || len(group.Calls) == 0 {
493
- continue
494
- }
495
- allCallsCovered := true
496
- for _, call := range group.Calls {
497
- if !callKeys[endpointCallKey(call)] {
498
- allCallsCovered = false
499
- break
500
- }
501
- }
502
- if allCallsCovered {
503
- covered[group.Function] = true
504
- }
505
- }
506
- }
507
- }
508
-
509
- func coverEndpointDescendants(covered map[string]bool, endpoints []gohttp.EndpointFlow, groups []gohttp.FlowGroup) {
510
- for _, endpoint := range endpoints {
511
- // Typed dispatch can lift a union of calls from every provider branch
512
- // into a shared coordinator such as ActionFlow. Compare those callers
513
- // with the whole endpoint, not with one branch at a time, or the same
514
- // source-backed calls reappear as a misleading standalone flow.
515
- callKeys := map[string]bool{}
516
- for _, branch := range endpoint.Branches {
517
- for _, call := range branch.Calls {
518
- callKeys[endpointCallKey(call)] = true
519
- }
520
- }
521
- for _, group := range groups {
522
- if covered[group.Function] || len(group.Calls) == 0 {
523
- continue
524
- }
525
- // Exact source location plus operation id is stronger evidence than
526
- // a display name in the reconstructed call chain. Wrappers reached
527
- // through interfaces and promoted methods can omit that name while
528
- // still contributing precisely the same source-backed calls.
529
- allCallsCovered := true
530
- for _, call := range group.Calls {
531
- if !callKeys[endpointCallKey(call)] {
532
- allCallsCovered = false
533
- break
534
- }
535
- }
536
- if allCallsCovered {
537
- covered[group.Function] = true
538
- }
539
- }
540
- }
541
- }
542
-
543
- func endpointCallKey(call gohttp.Call) string {
544
- return call.Source.String() + "\x00" + call.ID
545
- }
546
-
547
- func flowName(function string) string {
548
- directory, display, qualified := strings.Cut(function, ":")
549
- if !qualified {
550
- display = function
551
- }
552
- receiver, method, methodCall := strings.Cut(display, ".")
553
- if !methodCall || (receiver != "Client" && receiver != "Connector") {
554
- return title(strings.ReplaceAll(display, ".", " "))
555
- }
556
- parts := strings.Split(filepath.ToSlash(directory), "/")
557
- for index := len(parts) - 1; index >= 0; index-- {
558
- part := parts[index]
559
- if part == "" || part == "client" || part == "connector" || part == "adapter" {
560
- continue
561
- }
562
- return title(part) + " " + title(method)
563
- }
564
- return title(strings.ReplaceAll(display, ".", " "))
565
- }
566
-
567
- func complementaryCalls(calls []gohttp.Call) (string, string, bool) {
568
- if len(calls) != 2 {
569
- return "", "", false
570
- }
571
- for _, left := range calls[0].Conditions {
572
- for _, right := range calls[1].Conditions {
573
- if right == "not ("+left+")" || left == "not ("+right+")" {
574
- return left, right, true
575
- }
576
- }
577
- }
578
- return "", "", false
579
- }
580
-
581
- func participantOf(peer string, call gohttp.Call, opts Options) catalog.Participant {
582
- if call.API != "" {
583
- if service := opts.Peers[call.API]; service != "" {
584
- context, _, _ := strings.Cut(service, ".")
585
- return catalog.Participant{ID: service, Kind: catalog.ParticipantService, Context: &context}
586
- }
587
- if external := firstNonEmpty(opts.Externals[call.API], call.External); external != "" {
588
- return catalog.Participant{ID: external, Kind: catalog.ParticipantExternal}
589
- }
590
- }
591
- return catalog.Participant{ID: peer, Kind: catalog.ParticipantUnknown, Label: rawPeerLabel(call)}
592
- }
593
-
594
- func peerOf(call gohttp.Call, opts Options) (string, catalog.Status) {
595
- if call.API != "" {
596
- if peer := opts.Peers[call.API]; peer != "" {
597
- return peer, catalog.StatusDeclared
598
- }
599
- if peer := firstNonEmpty(opts.Externals[call.API], call.External); peer != "" {
600
- return peer, catalog.StatusDeclared
601
- }
602
- }
603
- return rawPeer(call), catalog.StatusUnresolved
604
- }
605
-
606
- func rawPeer(call gohttp.Call) string {
607
- if parsed, err := url.Parse(call.Endpoint); err == nil && parsed.Hostname() != "" {
608
- return slug(parsed.Hostname())
609
- }
610
- if call.Protocol == "SOAP" {
611
- if parsed, err := url.Parse(call.Action); err == nil && parsed.Hostname() != "" {
612
- return slug(parsed.Hostname())
613
- }
614
- return "soap-peer"
615
- }
616
- return "http-peer"
617
- }
618
-
619
- func rawPeerLabel(call gohttp.Call) string {
620
- if call.Endpoint != "" {
621
- return call.Endpoint
622
- }
623
- if call.Protocol == "SOAP" {
624
- if call.Action != "" {
625
- if parsed, err := url.Parse(call.Action); err == nil && parsed.Hostname() != "" {
626
- return parsed.Hostname()
627
- }
628
- return call.Action
629
- }
630
- return "SOAP endpoint"
631
- }
632
- return "HTTP endpoint"
633
- }
634
-
635
- func callLabel(call gohttp.Call) string {
636
- if call.Protocol == "SOAP" {
637
- return "SOAP " + call.Action
638
- }
639
- return strings.TrimSpace(call.Method + " " + call.Path)
640
- }
641
-
642
- func callNote(call gohttp.Call) string {
643
- var parts []string
644
- if call.Endpoint != "" && call.Endpoint != call.Path {
645
- parts = append(parts, "endpoint '"+call.Endpoint+"'")
646
- }
647
- if call.Request != "" {
648
- parts = append(parts, "request '"+call.Request+"'")
649
- }
650
- if call.Response != "" {
651
- parts = append(parts, "response '"+call.Response+"'")
652
- }
653
- if len(call.URLTrace) > 1 {
654
- parts = append(parts, "URL source "+strings.Join(call.URLTrace, " → "))
655
- }
656
- return strings.Join(parts, "; ")
657
- }
658
-
659
- func slug(value string) string {
660
- var out strings.Builder
661
- dash := false
662
- for _, r := range strings.ToLower(value) {
663
- if unicode.IsLetter(r) || unicode.IsDigit(r) {
664
- out.WriteRune(r)
665
- dash = false
666
- } else if out.Len() > 0 && !dash {
667
- out.WriteByte('-')
668
- dash = true
669
- }
670
- }
671
- return strings.Trim(out.String(), "-")
672
- }
673
-
674
- func title(value string) string {
675
- var words []string
676
- for _, word := range strings.FieldsFunc(value, func(r rune) bool { return r == '-' || r == '_' || unicode.IsSpace(r) }) {
677
- runes := []rune(word)
678
- if len(runes) > 0 {
679
- runes[0] = unicode.ToUpper(runes[0])
680
- }
681
- words = append(words, string(runes))
682
- }
683
- return strings.Join(words, " ")
684
- }
685
-
686
- func firstNonEmpty(values ...string) string {
687
- for _, value := range values {
688
- if value != "" {
689
- return value
690
- }
691
- }
692
- return ""
693
- }
694
-
695
- func uniqueStrings(values []string) []string {
696
- seen := map[string]bool{}
697
- out := make([]string, 0, len(values))
698
- for _, value := range values {
699
- if value != "" && !seen[value] {
700
- seen[value] = true
701
- out = append(out, value)
702
- }
703
- }
704
- return out
705
- }