@shortlink-org/portolan 0.1.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 (826) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/catalog/enum_test.go +46 -0
  4. package/catalog/model.go +932 -0
  5. package/catalog/roundtrip_test.go +185 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/portolan.mjs +368 -0
  8. package/go.mod +14 -0
  9. package/go.sum +14 -0
  10. package/index.html +13 -0
  11. package/internal/commands/cargo.go +154 -0
  12. package/internal/commands/commands.go +143 -0
  13. package/internal/commands/commands_test.go +323 -0
  14. package/internal/commands/gradle.go +77 -0
  15. package/internal/commands/justfile.go +82 -0
  16. package/internal/commands/makefile.go +106 -0
  17. package/internal/commands/maven.go +93 -0
  18. package/internal/commands/packagejson.go +159 -0
  19. package/internal/commands/pyproject.go +169 -0
  20. package/internal/commands/taskfile.go +101 -0
  21. package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
  22. package/internal/commands/testdata/estate/Makefile +33 -0
  23. package/internal/commands/testdata/estate/Taskfile.yml +21 -0
  24. package/internal/commands/testdata/estate/build.gradle.kts +21 -0
  25. package/internal/commands/testdata/estate/justfile +20 -0
  26. package/internal/commands/testdata/estate/package.json +13 -0
  27. package/internal/commands/testdata/estate/pom.xml +29 -0
  28. package/internal/commands/testdata/estate/pyproject.toml +27 -0
  29. package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
  30. package/internal/commands/testdata/golden/commands.json +305 -0
  31. package/internal/gohttp/analyze.go +2500 -0
  32. package/internal/gohttp/endpoints.go +1067 -0
  33. package/internal/gohttp/roots.go +320 -0
  34. package/internal/gohttp/typed.go +143 -0
  35. package/internal/goscan/constants.go +85 -0
  36. package/internal/goscan/goscan_test.go +227 -0
  37. package/internal/goscan/names.go +52 -0
  38. package/internal/goscan/parse_test.go +11 -0
  39. package/internal/goscan/source.go +36 -0
  40. package/internal/goscan/tree.go +155 -0
  41. package/internal/goscan/types.go +99 -0
  42. package/internal/wsdl/ids.go +127 -0
  43. package/internal/wsdl/ids_test.go +21 -0
  44. package/internal/wsdl/model.go +62 -0
  45. package/internal/wsdl/parse.go +920 -0
  46. package/internal/wsdl/parse_test.go +133 -0
  47. package/package.json +117 -0
  48. package/plugin/describe.go +107 -0
  49. package/plugin/describe_test.go +114 -0
  50. package/plugin/protocol.go +117 -0
  51. package/plugin/schematest/schematest.go +126 -0
  52. package/plugins/README.md +1160 -0
  53. package/plugins/extract-adr/describe.go +19 -0
  54. package/plugins/extract-adr/describe_test.go +11 -0
  55. package/plugins/extract-adr/extract.go +153 -0
  56. package/plugins/extract-adr/extract_test.go +350 -0
  57. package/plugins/extract-adr/history.go +99 -0
  58. package/plugins/extract-adr/main.go +65 -0
  59. package/plugins/extract-adr/options.schema.json +26 -0
  60. package/plugins/extract-adr/parse.go +642 -0
  61. package/plugins/extract-adr/parse_test.go +405 -0
  62. package/plugins/extract-asyncapi/describe.go +19 -0
  63. package/plugins/extract-asyncapi/describe_test.go +11 -0
  64. package/plugins/extract-asyncapi/extract.go +315 -0
  65. package/plugins/extract-asyncapi/extract_test.go +197 -0
  66. package/plugins/extract-asyncapi/main.go +48 -0
  67. package/plugins/extract-asyncapi/options.schema.json +26 -0
  68. package/plugins/extract-asyncapi/spec.go +151 -0
  69. package/plugins/extract-celery/README.md +169 -0
  70. package/plugins/extract-celery/extract.py +346 -0
  71. package/plugins/extract-celery/extract_test.py +192 -0
  72. package/plugins/extract-celery/main.py +66 -0
  73. package/plugins/extract-celery/options.py +31 -0
  74. package/plugins/extract-celery/options.schema.json +31 -0
  75. package/plugins/extract-celery/producers.py +156 -0
  76. package/plugins/extract-celery/routes_test.py +106 -0
  77. package/plugins/extract-commands/describe.go +19 -0
  78. package/plugins/extract-commands/describe_test.go +11 -0
  79. package/plugins/extract-commands/extract.go +79 -0
  80. package/plugins/extract-commands/extract_test.go +92 -0
  81. package/plugins/extract-commands/main.go +55 -0
  82. package/plugins/extract-commands/options.schema.json +20 -0
  83. package/plugins/extract-csr/avro.go +258 -0
  84. package/plugins/extract-csr/describe.go +19 -0
  85. package/plugins/extract-csr/describe_test.go +11 -0
  86. package/plugins/extract-csr/extract.go +338 -0
  87. package/plugins/extract-csr/extract_test.go +374 -0
  88. package/plugins/extract-csr/jsonschema.go +343 -0
  89. package/plugins/extract-csr/lock.go +25 -0
  90. package/plugins/extract-csr/main.go +84 -0
  91. package/plugins/extract-csr/options.schema.json +43 -0
  92. package/plugins/extract-csr/subject.go +102 -0
  93. package/plugins/extract-django/README.md +386 -0
  94. package/plugins/extract-django/apps.py +100 -0
  95. package/plugins/extract-django/auth.py +302 -0
  96. package/plugins/extract-django/auth_test.py +188 -0
  97. package/plugins/extract-django/clients.py +121 -0
  98. package/plugins/extract-django/contracts.py +458 -0
  99. package/plugins/extract-django/database.py +119 -0
  100. package/plugins/extract-django/domain.py +318 -0
  101. package/plugins/extract-django/events.py +86 -0
  102. package/plugins/extract-django/extract.py +601 -0
  103. package/plugins/extract-django/extract_test.py +397 -0
  104. package/plugins/extract-django/flows.py +1278 -0
  105. package/plugins/extract-django/flows_test.py +338 -0
  106. package/plugins/extract-django/ids.py +42 -0
  107. package/plugins/extract-django/ids_test.py +30 -0
  108. package/plugins/extract-django/lifecycle.py +222 -0
  109. package/plugins/extract-django/lifecycle_test.py +76 -0
  110. package/plugins/extract-django/main.py +66 -0
  111. package/plugins/extract-django/openapi.py +229 -0
  112. package/plugins/extract-django/openapi_test.py +60 -0
  113. package/plugins/extract-django/operations.py +96 -0
  114. package/plugins/extract-django/options.py +61 -0
  115. package/plugins/extract-django/options.schema.json +126 -0
  116. package/plugins/extract-django/routing.py +214 -0
  117. package/plugins/extract-django/routing_test.py +198 -0
  118. package/plugins/extract-django/serializers.py +558 -0
  119. package/plugins/extract-django/store.py +434 -0
  120. package/plugins/extract-django/transport.py +242 -0
  121. package/plugins/extract-flows/describe.go +19 -0
  122. package/plugins/extract-flows/describe_test.go +11 -0
  123. package/plugins/extract-flows/extract.go +91 -0
  124. package/plugins/extract-flows/main.go +45 -0
  125. package/plugins/extract-flows/options.schema.json +16 -0
  126. package/plugins/extract-flows/parse.go +593 -0
  127. package/plugins/extract-flows/parse_test.go +204 -0
  128. package/plugins/extract-glossary/describe.go +19 -0
  129. package/plugins/extract-glossary/describe_test.go +11 -0
  130. package/plugins/extract-glossary/extract.go +115 -0
  131. package/plugins/extract-glossary/extract_test.go +220 -0
  132. package/plugins/extract-glossary/main.go +59 -0
  133. package/plugins/extract-glossary/options.schema.json +21 -0
  134. package/plugins/extract-glossary/parse.go +214 -0
  135. package/plugins/extract-glossary/parse_test.go +203 -0
  136. package/plugins/extract-go/aggregate.go +214 -0
  137. package/plugins/extract-go/client.go +409 -0
  138. package/plugins/extract-go/client_test.go +266 -0
  139. package/plugins/extract-go/describe.go +19 -0
  140. package/plugins/extract-go/describe_test.go +11 -0
  141. package/plugins/extract-go/enum.go +195 -0
  142. package/plugins/extract-go/enum_test.go +82 -0
  143. package/plugins/extract-go/event.go +99 -0
  144. package/plugins/extract-go/extract.go +191 -0
  145. package/plugins/extract-go/extract_test.go +261 -0
  146. package/plugins/extract-go/flow.go +1441 -0
  147. package/plugins/extract-go/flow_test.go +609 -0
  148. package/plugins/extract-go/httpclient.go +174 -0
  149. package/plugins/extract-go/httpclient_test.go +296 -0
  150. package/plugins/extract-go/ids.go +92 -0
  151. package/plugins/extract-go/layout.go +225 -0
  152. package/plugins/extract-go/layout_test.go +125 -0
  153. package/plugins/extract-go/lifecycle.go +324 -0
  154. package/plugins/extract-go/lifecycle_test.go +108 -0
  155. package/plugins/extract-go/main.go +79 -0
  156. package/plugins/extract-go/operation.go +157 -0
  157. package/plugins/extract-go/options.schema.json +66 -0
  158. package/plugins/extract-go/source.go +316 -0
  159. package/plugins/extract-go/transport.go +321 -0
  160. package/plugins/extract-go/transport_test.go +145 -0
  161. package/plugins/extract-go/wiring.go +434 -0
  162. package/plugins/extract-go-nats/describe.go +19 -0
  163. package/plugins/extract-go-nats/describe_test.go +11 -0
  164. package/plugins/extract-go-nats/extract.go +177 -0
  165. package/plugins/extract-go-nats/extract_test.go +389 -0
  166. package/plugins/extract-go-nats/index.go +379 -0
  167. package/plugins/extract-go-nats/main.go +42 -0
  168. package/plugins/extract-go-nats/options.schema.json +21 -0
  169. package/plugins/extract-go-nats/resolve.go +161 -0
  170. package/plugins/extract-go-nats/sites.go +224 -0
  171. package/plugins/extract-graphql/describe.go +19 -0
  172. package/plugins/extract-graphql/describe_test.go +11 -0
  173. package/plugins/extract-graphql/extract.go +433 -0
  174. package/plugins/extract-graphql/extract_test.go +256 -0
  175. package/plugins/extract-graphql/ids.go +49 -0
  176. package/plugins/extract-graphql/lex.go +237 -0
  177. package/plugins/extract-graphql/main.go +51 -0
  178. package/plugins/extract-graphql/options.schema.json +31 -0
  179. package/plugins/extract-graphql/parse.go +621 -0
  180. package/plugins/extract-graphql/parse_test.go +122 -0
  181. package/plugins/extract-http-clients/describe.go +19 -0
  182. package/plugins/extract-http-clients/describe_test.go +11 -0
  183. package/plugins/extract-http-clients/extract.go +705 -0
  184. package/plugins/extract-http-clients/extract_test.go +1263 -0
  185. package/plugins/extract-http-clients/main.go +42 -0
  186. package/plugins/extract-http-clients/options.schema.json +33 -0
  187. package/plugins/extract-java/README.md +190 -0
  188. package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
  189. package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
  190. package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
  191. package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
  192. package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
  193. package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
  194. package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
  195. package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
  196. package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
  197. package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
  198. package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
  199. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  200. package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
  201. package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
  202. package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
  203. package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
  204. package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
  205. package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
  206. package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
  207. package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
  208. package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
  209. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
  210. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
  211. package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
  212. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  213. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
  214. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
  215. package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
  216. package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
  217. package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
  218. package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
  219. package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
  220. package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
  221. package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
  222. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  223. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  224. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  225. package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
  226. package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
  227. package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
  228. package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
  229. package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
  230. package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
  231. package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
  232. package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
  233. package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
  234. package/plugins/extract-java/options.schema.json +70 -0
  235. package/plugins/extract-openapi/describe.go +19 -0
  236. package/plugins/extract-openapi/describe_test.go +11 -0
  237. package/plugins/extract-openapi/discover.go +243 -0
  238. package/plugins/extract-openapi/extract.go +526 -0
  239. package/plugins/extract-openapi/extract_test.go +545 -0
  240. package/plugins/extract-openapi/main.go +75 -0
  241. package/plugins/extract-openapi/options.schema.json +62 -0
  242. package/plugins/extract-openapi/spec.go +350 -0
  243. package/plugins/extract-project/README.md +22 -0
  244. package/plugins/extract-project/describe.go +19 -0
  245. package/plugins/extract-project/describe_test.go +11 -0
  246. package/plugins/extract-project/extract.go +221 -0
  247. package/plugins/extract-project/extract_test.go +109 -0
  248. package/plugins/extract-project/main.go +41 -0
  249. package/plugins/extract-project/options.schema.json +48 -0
  250. package/plugins/extract-proto/ast.go +125 -0
  251. package/plugins/extract-proto/consumes.go +77 -0
  252. package/plugins/extract-proto/describe.go +19 -0
  253. package/plugins/extract-proto/extract.go +293 -0
  254. package/plugins/extract-proto/extract_test.go +459 -0
  255. package/plugins/extract-proto/ids.go +89 -0
  256. package/plugins/extract-proto/ids_test.go +57 -0
  257. package/plugins/extract-proto/lex.go +285 -0
  258. package/plugins/extract-proto/main.go +100 -0
  259. package/plugins/extract-proto/module.go +120 -0
  260. package/plugins/extract-proto/options.schema.json +51 -0
  261. package/plugins/extract-proto/parse.go +720 -0
  262. package/plugins/extract-proto/parse_test.go +307 -0
  263. package/plugins/extract-proto/provides.go +236 -0
  264. package/plugins/extract-proto/resolve.go +222 -0
  265. package/plugins/extract-proto/resolve_test.go +119 -0
  266. package/plugins/extract-python-kafka/README.md +36 -0
  267. package/plugins/extract-python-kafka/extract.py +211 -0
  268. package/plugins/extract-python-kafka/extract_test.py +68 -0
  269. package/plugins/extract-python-kafka/main.py +59 -0
  270. package/plugins/extract-python-kafka/options.py +29 -0
  271. package/plugins/extract-python-kafka/options.schema.json +31 -0
  272. package/plugins/extract-redis/README.md +11 -0
  273. package/plugins/extract-redis/describe.go +19 -0
  274. package/plugins/extract-redis/describe_test.go +11 -0
  275. package/plugins/extract-redis/extract.go +183 -0
  276. package/plugins/extract-redis/extract_test.go +168 -0
  277. package/plugins/extract-redis/keyspaces.go +469 -0
  278. package/plugins/extract-redis/main.go +44 -0
  279. package/plugins/extract-redis/options.schema.json +30 -0
  280. package/plugins/extract-river/describe.go +19 -0
  281. package/plugins/extract-river/describe_test.go +11 -0
  282. package/plugins/extract-river/extract.go +507 -0
  283. package/plugins/extract-river/extract_test.go +132 -0
  284. package/plugins/extract-river/main.go +42 -0
  285. package/plugins/extract-river/options.schema.json +21 -0
  286. package/plugins/extract-rust/Cargo.lock +133 -0
  287. package/plugins/extract-rust/Cargo.toml +23 -0
  288. package/plugins/extract-rust/README.md +204 -0
  289. package/plugins/extract-rust/options.schema.json +65 -0
  290. package/plugins/extract-rust/rustfmt.toml +1 -0
  291. package/plugins/extract-sql/ddl.go +893 -0
  292. package/plugins/extract-sql/ddl_test.go +401 -0
  293. package/plugins/extract-sql/describe.go +19 -0
  294. package/plugins/extract-sql/describe_test.go +11 -0
  295. package/plugins/extract-sql/layout.go +221 -0
  296. package/plugins/extract-sql/layout_test.go +55 -0
  297. package/plugins/extract-sql/lineage.go +107 -0
  298. package/plugins/extract-sql/main.go +141 -0
  299. package/plugins/extract-sql/maps.go +564 -0
  300. package/plugins/extract-sql/maps_java.go +117 -0
  301. package/plugins/extract-sql/maps_rust.go +333 -0
  302. package/plugins/extract-sql/maps_rust_test.go +70 -0
  303. package/plugins/extract-sql/maps_test.go +204 -0
  304. package/plugins/extract-sql/maps_ts.go +398 -0
  305. package/plugins/extract-sql/maps_ts_test.go +136 -0
  306. package/plugins/extract-sql/options.schema.json +53 -0
  307. package/plugins/extract-sql/projection.go +70 -0
  308. package/plugins/extract-sql/projection_test.go +50 -0
  309. package/plugins/extract-sql/store.go +420 -0
  310. package/plugins/extract-sql/store_test.go +233 -0
  311. package/plugins/extract-sql/view.go +295 -0
  312. package/plugins/extract-ts/README.md +306 -0
  313. package/plugins/extract-ts/ast.test.ts +178 -0
  314. package/plugins/extract-ts/ast.ts +648 -0
  315. package/plugins/extract-ts/clients.ts +185 -0
  316. package/plugins/extract-ts/domain.ts +164 -0
  317. package/plugins/extract-ts/extract.test.ts +208 -0
  318. package/plugins/extract-ts/extract.ts +200 -0
  319. package/plugins/extract-ts/flows.ts +783 -0
  320. package/plugins/extract-ts/graphql.test.ts +73 -0
  321. package/plugins/extract-ts/graphql.ts +181 -0
  322. package/plugins/extract-ts/ids.test.ts +21 -0
  323. package/plugins/extract-ts/ids.ts +52 -0
  324. package/plugins/extract-ts/lifecycle.ts +138 -0
  325. package/plugins/extract-ts/main.ts +48 -0
  326. package/plugins/extract-ts/openapi.test.ts +64 -0
  327. package/plugins/extract-ts/openapi.ts +99 -0
  328. package/plugins/extract-ts/operations.ts +93 -0
  329. package/plugins/extract-ts/options.schema.json +96 -0
  330. package/plugins/extract-ts/source.test.ts +174 -0
  331. package/plugins/extract-ts/source.ts +465 -0
  332. package/plugins/extract-ts/transport.ts +147 -0
  333. package/plugins/extract-ts/tsconfig.json +13 -0
  334. package/plugins/extract-ts/wiring.ts +190 -0
  335. package/plugins/extract-watermill/describe.go +19 -0
  336. package/plugins/extract-watermill/describe_test.go +11 -0
  337. package/plugins/extract-watermill/extract.go +1228 -0
  338. package/plugins/extract-watermill/extract_test.go +234 -0
  339. package/plugins/extract-watermill/main.go +41 -0
  340. package/plugins/extract-watermill/options.schema.json +21 -0
  341. package/plugins/extract-wsdl/describe.go +19 -0
  342. package/plugins/extract-wsdl/describe_test.go +11 -0
  343. package/plugins/extract-wsdl/extract.go +148 -0
  344. package/plugins/extract-wsdl/extract_test.go +45 -0
  345. package/plugins/extract-wsdl/main.go +53 -0
  346. package/plugins/extract-wsdl/options.schema.json +59 -0
  347. package/plugins/fetch-bsr/auth.go +114 -0
  348. package/plugins/fetch-bsr/bsr.go +240 -0
  349. package/plugins/fetch-bsr/cache.go +66 -0
  350. package/plugins/fetch-bsr/describe.go +19 -0
  351. package/plugins/fetch-bsr/fetch.go +192 -0
  352. package/plugins/fetch-bsr/fetch_test.go +465 -0
  353. package/plugins/fetch-bsr/lock.go +71 -0
  354. package/plugins/fetch-bsr/main.go +117 -0
  355. package/plugins/fetch-bsr/options.schema.json +42 -0
  356. package/plugins/fetch-csr/auth.go +95 -0
  357. package/plugins/fetch-csr/cache.go +69 -0
  358. package/plugins/fetch-csr/describe.go +19 -0
  359. package/plugins/fetch-csr/describe_test.go +11 -0
  360. package/plugins/fetch-csr/fetch.go +254 -0
  361. package/plugins/fetch-csr/fetch_test.go +494 -0
  362. package/plugins/fetch-csr/lock.go +95 -0
  363. package/plugins/fetch-csr/main.go +122 -0
  364. package/plugins/fetch-csr/options.schema.json +42 -0
  365. package/plugins/fetch-csr/registry.go +209 -0
  366. package/plugins/fetch-git/cache.go +65 -0
  367. package/plugins/fetch-git/describe.go +19 -0
  368. package/plugins/fetch-git/describe_test.go +11 -0
  369. package/plugins/fetch-git/fetch.go +181 -0
  370. package/plugins/fetch-git/fetch_test.go +332 -0
  371. package/plugins/fetch-git/git.go +169 -0
  372. package/plugins/fetch-git/lock.go +72 -0
  373. package/plugins/fetch-git/main.go +127 -0
  374. package/plugins/fetch-git/offline.go +39 -0
  375. package/plugins/fetch-git/options.schema.json +42 -0
  376. package/plugins/fetch-git/pin.go +90 -0
  377. package/plugins/fetch-git/pin_test.go +74 -0
  378. package/plugins/gen-backstage/describe.go +17 -0
  379. package/plugins/gen-backstage/main.go +22 -0
  380. package/plugins/gen-backstage/options.schema.json +30 -0
  381. package/plugins/gen-backstage/plugin.go +473 -0
  382. package/plugins/gen-backstage/plugin_test.go +145 -0
  383. package/plugins/gen-backstage.wasm +0 -0
  384. package/plugins/gen-markdown/adr.go +162 -0
  385. package/plugins/gen-markdown/aggregate.go +362 -0
  386. package/plugins/gen-markdown/canonical.go +93 -0
  387. package/plugins/gen-markdown/context.go +90 -0
  388. package/plugins/gen-markdown/coverage_test.go +89 -0
  389. package/plugins/gen-markdown/describe.go +19 -0
  390. package/plugins/gen-markdown/describe_test.go +11 -0
  391. package/plugins/gen-markdown/external.go +71 -0
  392. package/plugins/gen-markdown/flow.go +277 -0
  393. package/plugins/gen-markdown/glossary.go +76 -0
  394. package/plugins/gen-markdown/glossary_test.go +148 -0
  395. package/plugins/gen-markdown/llms.go +302 -0
  396. package/plugins/gen-markdown/main.go +22 -0
  397. package/plugins/gen-markdown/markdown.go +254 -0
  398. package/plugins/gen-markdown/markdown_test.go +100 -0
  399. package/plugins/gen-markdown/module.go +107 -0
  400. package/plugins/gen-markdown/options.schema.json +16 -0
  401. package/plugins/gen-markdown/plugin.go +39 -0
  402. package/plugins/gen-markdown/quality_test.go +176 -0
  403. package/plugins/gen-markdown/redis_test.go +38 -0
  404. package/plugins/gen-markdown/render.go +344 -0
  405. package/plugins/gen-markdown/render_test.go +186 -0
  406. package/plugins/gen-markdown/service.go +456 -0
  407. package/plugins/gen-markdown/source.go +177 -0
  408. package/plugins/gen-markdown/store.go +201 -0
  409. package/plugins/gen-markdown.wasm +0 -0
  410. package/plugins/gen-mermaid/describe.go +17 -0
  411. package/plugins/gen-mermaid/main.go +22 -0
  412. package/plugins/gen-mermaid/options.schema.json +10 -0
  413. package/plugins/gen-mermaid/plugin.go +104 -0
  414. package/plugins/gen-mermaid/plugin_test.go +33 -0
  415. package/plugins/gen-mermaid.wasm +0 -0
  416. package/plugins/openapi/ids.go +261 -0
  417. package/plugins/openapi/ids_test.go +98 -0
  418. package/plugins/pyplugin/catalog.py +209 -0
  419. package/plugins/pyplugin/celery_conf.py +436 -0
  420. package/plugins/pyplugin/celery_tasks.py +76 -0
  421. package/plugins/pyplugin/kafka.py +570 -0
  422. package/plugins/pyplugin/names.py +38 -0
  423. package/plugins/pyplugin/protocol.py +79 -0
  424. package/plugins/pyplugin/source.py +285 -0
  425. package/plugins/verify-codeowners/describe.go +19 -0
  426. package/plugins/verify-codeowners/describe_test.go +11 -0
  427. package/plugins/verify-codeowners/main.go +75 -0
  428. package/plugins/verify-codeowners/match.go +85 -0
  429. package/plugins/verify-codeowners/match_test.go +47 -0
  430. package/plugins/verify-codeowners/options.schema.json +15 -0
  431. package/plugins/verify-codeowners/owners.go +164 -0
  432. package/plugins/verify-codeowners/owners_test.go +225 -0
  433. package/plugins/verify-codeowners/parse.go +90 -0
  434. package/plugins/verify-codeowners/parse_test.go +62 -0
  435. package/plugins/verify-otel/describe.go +19 -0
  436. package/plugins/verify-otel/describe_test.go +11 -0
  437. package/plugins/verify-otel/main.go +53 -0
  438. package/plugins/verify-otel/match.go +336 -0
  439. package/plugins/verify-otel/options.schema.json +30 -0
  440. package/plugins/verify-otel/otlp.go +200 -0
  441. package/plugins/verify-otel/verify.go +734 -0
  442. package/plugins/verify-otel/verify_test.go +460 -0
  443. package/portolan.json +1073 -0
  444. package/public/favicon.svg +12 -0
  445. package/schema/portolan.schema.json +2558 -0
  446. package/scripts/README.md +53 -0
  447. package/scripts/build-report.mjs +60 -0
  448. package/scripts/build-report.test.mjs +44 -0
  449. package/scripts/builtin-plugins.mjs +49 -0
  450. package/scripts/catalog-sources.mjs +67 -0
  451. package/scripts/diff.mjs +315 -0
  452. package/scripts/diff.test.mjs +151 -0
  453. package/scripts/forge-comment.mjs +144 -0
  454. package/scripts/forge-comment.test.mjs +151 -0
  455. package/scripts/forge-release.mjs +138 -0
  456. package/scripts/forge-release.test.mjs +106 -0
  457. package/scripts/forge.mjs +61 -0
  458. package/scripts/gen-likec4.mjs +854 -0
  459. package/scripts/gen-likec4.test.mjs +258 -0
  460. package/scripts/gen.mjs +603 -0
  461. package/scripts/generated-docs.test.mjs +66 -0
  462. package/scripts/local-api.mjs +1192 -0
  463. package/scripts/local-api.test.mjs +387 -0
  464. package/scripts/manifest.mjs +214 -0
  465. package/scripts/manifest.test.mjs +148 -0
  466. package/scripts/output-path.mjs +79 -0
  467. package/scripts/output-path.test.mjs +45 -0
  468. package/scripts/package-smoke.mjs +37 -0
  469. package/scripts/plugin-host.mjs +419 -0
  470. package/scripts/plugin-host.test.mjs +109 -0
  471. package/scripts/plugin-wasm-worker.mjs +34 -0
  472. package/scripts/run-builtin.mjs +81 -0
  473. package/scripts/schema.mjs +357 -0
  474. package/scripts/site-docs.mjs +295 -0
  475. package/scripts/site-docs.test.mjs +181 -0
  476. package/scripts/vendor-lock.mjs +58 -0
  477. package/scripts/vendor-lock.test.mjs +69 -0
  478. package/src/README.md +38 -0
  479. package/src/app/App.tsx +448 -0
  480. package/src/app/BranchPicker.test.ts +46 -0
  481. package/src/app/BranchPicker.tsx +153 -0
  482. package/src/app/Breadcrumbs.test.ts +38 -0
  483. package/src/app/Breadcrumbs.tsx +223 -0
  484. package/src/app/BuildStamp.tsx +146 -0
  485. package/src/app/CommandPalette.tsx +292 -0
  486. package/src/app/HashScroll.tsx +45 -0
  487. package/src/app/Sidebar.tsx +1742 -0
  488. package/src/app/TopBar.tsx +393 -0
  489. package/src/app/density.tsx +64 -0
  490. package/src/app/forge-access.test.ts +27 -0
  491. package/src/app/forge-access.tsx +80 -0
  492. package/src/app/panels.tsx +141 -0
  493. package/src/app/pins.tsx +242 -0
  494. package/src/app/query-client.ts +24 -0
  495. package/src/app/responsive.ts +55 -0
  496. package/src/app/shortcuts.test.ts +39 -0
  497. package/src/app/shortcuts.tsx +282 -0
  498. package/src/app/theme.tsx +51 -0
  499. package/src/app/title.test.ts +14 -0
  500. package/src/app/title.ts +30 -0
  501. package/src/app/toast.tsx +59 -0
  502. package/src/app/tree-keys.test.ts +80 -0
  503. package/src/app/tree-keys.ts +87 -0
  504. package/src/app/ui-store.ts +52 -0
  505. package/src/catalog-error.test.ts +105 -0
  506. package/src/catalog-profile.test.ts +59 -0
  507. package/src/catalog-profile.ts +101 -0
  508. package/src/catalog-stores.test.ts +431 -0
  509. package/src/catalog.test.ts +1215 -0
  510. package/src/catalog.ts +3125 -0
  511. package/src/chat/Cards.tsx +408 -0
  512. package/src/chat/ChatPanel.tsx +79 -0
  513. package/src/chat/ChatSettings.tsx +219 -0
  514. package/src/chat/Composer.tsx +99 -0
  515. package/src/chat/Conversation.tsx +281 -0
  516. package/src/chat/Header.tsx +46 -0
  517. package/src/chat/Message.tsx +49 -0
  518. package/src/chat/Notice.tsx +195 -0
  519. package/src/chat/Starter.tsx +67 -0
  520. package/src/chat/Steps.tsx +45 -0
  521. package/src/chat/Waiting.tsx +87 -0
  522. package/src/chat/flags.test.ts +59 -0
  523. package/src/chat/flags.ts +155 -0
  524. package/src/chat/linkify.test.ts +30 -0
  525. package/src/chat/linkify.ts +45 -0
  526. package/src/chat/prefs.ts +56 -0
  527. package/src/chat/prompt.test.ts +43 -0
  528. package/src/chat/prompt.ts +157 -0
  529. package/src/chat/store.ts +35 -0
  530. package/src/chat/tools.ts +58 -0
  531. package/src/chat/transport.ts +70 -0
  532. package/src/components/AdrRow.tsx +29 -0
  533. package/src/components/AnchorLink.tsx +67 -0
  534. package/src/components/ApiReference.tsx +134 -0
  535. package/src/components/AsyncApiReference.tsx +120 -0
  536. package/src/components/CatalogStamp.tsx +155 -0
  537. package/src/components/ChannelRows.tsx +132 -0
  538. package/src/components/CommandRows.tsx +139 -0
  539. package/src/components/DiagramSkeleton.tsx +71 -0
  540. package/src/components/EditorLink.tsx +85 -0
  541. package/src/components/FieldTree.tsx +305 -0
  542. package/src/components/FlowTrigger.tsx +34 -0
  543. package/src/components/Ident.tsx +89 -0
  544. package/src/components/Integrations.tsx +243 -0
  545. package/src/components/LifecycleDiagram.tsx +110 -0
  546. package/src/components/MachineDocs.tsx +42 -0
  547. package/src/components/Markdown.tsx +173 -0
  548. package/src/components/MarkdownTable.tsx +206 -0
  549. package/src/components/Mermaid.tsx +211 -0
  550. package/src/components/MethodRows.tsx +309 -0
  551. package/src/components/Overlay.tsx +125 -0
  552. package/src/components/PageHeader.tsx +189 -0
  553. package/src/components/RowActions.tsx +97 -0
  554. package/src/components/SchemaDocument.tsx +89 -0
  555. package/src/components/Select.tsx +126 -0
  556. package/src/components/ShapeRows.tsx +118 -0
  557. package/src/components/SourceDoc.tsx +82 -0
  558. package/src/components/SourcePreview.tsx +441 -0
  559. package/src/components/TabRow.tsx +182 -0
  560. package/src/components/TechIcon.tsx +34 -0
  561. package/src/components/Toc.tsx +132 -0
  562. package/src/components/WhatLinksHere.tsx +273 -0
  563. package/src/components/WsdlReference.tsx +84 -0
  564. package/src/components/ddd-icons.tsx +107 -0
  565. package/src/components/kind.tsx +157 -0
  566. package/src/components/logo.tsx +61 -0
  567. package/src/components/primitives.tsx +264 -0
  568. package/src/data.test.ts +18 -0
  569. package/src/data.ts +202 -0
  570. package/src/enrich.test.ts +1150 -0
  571. package/src/enrich.ts +672 -0
  572. package/src/er/ErCanvas.tsx +533 -0
  573. package/src/er/RedisSchema.tsx +74 -0
  574. package/src/er/RelationCard.tsx +267 -0
  575. package/src/er/StoreHeader.tsx +155 -0
  576. package/src/er/TableNode.tsx +84 -0
  577. package/src/er/ViewNode.tsx +83 -0
  578. package/src/er/layout.test.ts +115 -0
  579. package/src/er/layout.ts +77 -0
  580. package/src/er/lineage.test.ts +140 -0
  581. package/src/er/lineage.ts +107 -0
  582. package/src/er/markers.tsx +79 -0
  583. package/src/er/spec.test.ts +344 -0
  584. package/src/er/spec.ts +426 -0
  585. package/src/flow/ChainList.tsx +162 -0
  586. package/src/flow/FlowTable.tsx +123 -0
  587. package/src/flow/FlowToolbar.tsx +372 -0
  588. package/src/flow/StepDetail.tsx +380 -0
  589. package/src/flow/StepRail.tsx +428 -0
  590. package/src/flow/answers.test.ts +50 -0
  591. package/src/flow/answers.ts +58 -0
  592. package/src/flow/chain.test.ts +383 -0
  593. package/src/flow/chain.ts +353 -0
  594. package/src/flow/chapters.test.ts +185 -0
  595. package/src/flow/chapters.ts +225 -0
  596. package/src/flow/continues.test.ts +102 -0
  597. package/src/flow/continues.ts +65 -0
  598. package/src/flow/cross-context.test.ts +97 -0
  599. package/src/flow/cross-context.ts +35 -0
  600. package/src/flow/mermaid.test.ts +22 -0
  601. package/src/flow/mermaid.ts +141 -0
  602. package/src/flow/outline.test.ts +212 -0
  603. package/src/flow/outline.ts +170 -0
  604. package/src/flow/paths.test.ts +129 -0
  605. package/src/flow/paths.ts +147 -0
  606. package/src/flow/prefs.test.ts +80 -0
  607. package/src/flow/prefs.ts +104 -0
  608. package/src/graph/DependencyGraph.tsx +283 -0
  609. package/src/graph/DependencyNodes.tsx +162 -0
  610. package/src/graph/EdgeLabel.tsx +61 -0
  611. package/src/graph/FocusedEventGraph.tsx +211 -0
  612. package/src/graph/GraphToolbar.tsx +187 -0
  613. package/src/graph/RoutedEdge.tsx +80 -0
  614. package/src/graph/dependency-layout.test.ts +360 -0
  615. package/src/graph/dependency-layout.ts +444 -0
  616. package/src/graph/elk.test.ts +70 -0
  617. package/src/graph/elk.ts +287 -0
  618. package/src/graph/nodes.tsx +97 -0
  619. package/src/graph/theme.ts +81 -0
  620. package/src/graph/useElkFlow.ts +125 -0
  621. package/src/index.css +1547 -0
  622. package/src/language/InLanguage.tsx +40 -0
  623. package/src/language/TermCard.tsx +132 -0
  624. package/src/language/cards.test.ts +114 -0
  625. package/src/language/cards.ts +111 -0
  626. package/src/lib/admonition.test.ts +49 -0
  627. package/src/lib/admonition.ts +48 -0
  628. package/src/lib/adr.test.ts +131 -0
  629. package/src/lib/adr.ts +68 -0
  630. package/src/lib/api.test.ts +122 -0
  631. package/src/lib/api.ts +135 -0
  632. package/src/lib/backlinks.test.ts +330 -0
  633. package/src/lib/backlinks.ts +567 -0
  634. package/src/lib/branch-compare.ts +16 -0
  635. package/src/lib/build-info.test.ts +76 -0
  636. package/src/lib/build-info.ts +80 -0
  637. package/src/lib/catalog-diff.test.ts +275 -0
  638. package/src/lib/catalog-diff.ts +655 -0
  639. package/src/lib/clipboard.ts +43 -0
  640. package/src/lib/comparison-memory.test.ts +40 -0
  641. package/src/lib/comparison-memory.ts +39 -0
  642. package/src/lib/context-color.ts +27 -0
  643. package/src/lib/context-map.test.ts +148 -0
  644. package/src/lib/context-map.ts +399 -0
  645. package/src/lib/data-model.test.ts +245 -0
  646. package/src/lib/data-model.ts +399 -0
  647. package/src/lib/data-problems.test.ts +178 -0
  648. package/src/lib/data-problems.ts +314 -0
  649. package/src/lib/derive.test.ts +251 -0
  650. package/src/lib/derive.ts +515 -0
  651. package/src/lib/editor-link.test.ts +125 -0
  652. package/src/lib/editor-link.ts +96 -0
  653. package/src/lib/editor-prefs.ts +27 -0
  654. package/src/lib/event-graph.test.ts +237 -0
  655. package/src/lib/event-graph.ts +327 -0
  656. package/src/lib/export-canvas.ts +37 -0
  657. package/src/lib/export-file.test.ts +30 -0
  658. package/src/lib/export-file.ts +20 -0
  659. package/src/lib/flow-tree.test.ts +177 -0
  660. package/src/lib/flow-tree.ts +160 -0
  661. package/src/lib/forge-refs.test.ts +50 -0
  662. package/src/lib/forge-refs.ts +60 -0
  663. package/src/lib/format.test.ts +35 -0
  664. package/src/lib/format.ts +64 -0
  665. package/src/lib/github-catalog.test.ts +301 -0
  666. package/src/lib/github-catalog.ts +610 -0
  667. package/src/lib/integrations.test.ts +119 -0
  668. package/src/lib/integrations.ts +133 -0
  669. package/src/lib/kinds.test.ts +91 -0
  670. package/src/lib/kinds.ts +204 -0
  671. package/src/lib/lifecycle.test.ts +93 -0
  672. package/src/lib/lifecycle.ts +165 -0
  673. package/src/lib/local-api.ts +194 -0
  674. package/src/lib/motion-features.ts +11 -0
  675. package/src/lib/motion.test.ts +120 -0
  676. package/src/lib/motion.tsx +269 -0
  677. package/src/lib/palette-recent.test.ts +80 -0
  678. package/src/lib/palette-recent.ts +90 -0
  679. package/src/lib/palette.test.ts +365 -0
  680. package/src/lib/palette.ts +593 -0
  681. package/src/lib/pins.test.ts +139 -0
  682. package/src/lib/pins.ts +130 -0
  683. package/src/lib/problems.test.ts +112 -0
  684. package/src/lib/project-preview.ts +5 -0
  685. package/src/lib/proto-problems.test.ts +121 -0
  686. package/src/lib/proto-problems.ts +65 -0
  687. package/src/lib/queries.test.ts +113 -0
  688. package/src/lib/queries.ts +131 -0
  689. package/src/lib/registry.test.ts +219 -0
  690. package/src/lib/registry.ts +208 -0
  691. package/src/lib/scenarios.ts +547 -0
  692. package/src/lib/setup-info.test.ts +199 -0
  693. package/src/lib/setup-info.ts +415 -0
  694. package/src/lib/shape.test.ts +289 -0
  695. package/src/lib/shape.ts +454 -0
  696. package/src/lib/sidebar-prefs.test.ts +32 -0
  697. package/src/lib/sidebar-prefs.ts +75 -0
  698. package/src/lib/source-code.test.ts +159 -0
  699. package/src/lib/source-code.ts +286 -0
  700. package/src/lib/source-doc.test.ts +131 -0
  701. package/src/lib/source-doc.ts +103 -0
  702. package/src/lib/source-highlight.test.ts +29 -0
  703. package/src/lib/source-highlight.ts +24 -0
  704. package/src/lib/source-link.test.ts +153 -0
  705. package/src/lib/source-link.ts +201 -0
  706. package/src/lib/spec-files.test.ts +24 -0
  707. package/src/lib/spec-files.ts +23 -0
  708. package/src/lib/table-directive.test.ts +139 -0
  709. package/src/lib/table-directive.ts +106 -0
  710. package/src/lib/tech.test.ts +120 -0
  711. package/src/lib/tech.ts +421 -0
  712. package/src/lib/terms.test.ts +67 -0
  713. package/src/lib/terms.ts +179 -0
  714. package/src/lib/wire-problems.test.ts +263 -0
  715. package/src/lib/wire-problems.ts +281 -0
  716. package/src/likec4/C4View.tsx +55 -0
  717. package/src/likec4/CanvasBridge.tsx +187 -0
  718. package/src/likec4/FlowView.tsx +235 -0
  719. package/src/likec4/InteractiveView.tsx +144 -0
  720. package/src/likec4/canvas-viewport.test.ts +149 -0
  721. package/src/likec4/canvas-viewport.ts +135 -0
  722. package/src/likec4/flow-edges.test.ts +51 -0
  723. package/src/likec4/flow-edges.ts +89 -0
  724. package/src/likec4/frame-theme.test.ts +76 -0
  725. package/src/likec4/frame-theme.ts +101 -0
  726. package/src/likec4/highlight-css.test.ts +49 -0
  727. package/src/likec4/highlight-css.ts +52 -0
  728. package/src/likec4/ids.test.ts +151 -0
  729. package/src/likec4/ids.ts +115 -0
  730. package/src/likec4/levels.tsx +70 -0
  731. package/src/likec4/mapping.test.ts +66 -0
  732. package/src/likec4/mapping.ts +54 -0
  733. package/src/likec4/reserved.json +141 -0
  734. package/src/likec4/view-index.ts +78 -0
  735. package/src/main.tsx +13 -0
  736. package/src/map/ContextMapGraph.tsx +274 -0
  737. package/src/map/ContextNode.tsx +70 -0
  738. package/src/map/FloatingEdge.tsx +86 -0
  739. package/src/merge.test.ts +1038 -0
  740. package/src/merge.ts +1004 -0
  741. package/src/pages/AdrDetail.tsx +242 -0
  742. package/src/pages/AdrIndex.tsx +114 -0
  743. package/src/pages/AggregatePage.tsx +727 -0
  744. package/src/pages/BlockPage.tsx +478 -0
  745. package/src/pages/CatalogFailure.tsx +111 -0
  746. package/src/pages/Changes.tsx +313 -0
  747. package/src/pages/ContextMap.tsx +534 -0
  748. package/src/pages/ContextPage.tsx +346 -0
  749. package/src/pages/EnumPage.tsx +282 -0
  750. package/src/pages/EventPage.tsx +686 -0
  751. package/src/pages/ExternalPage.tsx +204 -0
  752. package/src/pages/FlowDetail.tsx +663 -0
  753. package/src/pages/FlowIndex.tsx +303 -0
  754. package/src/pages/GraphPage.tsx +217 -0
  755. package/src/pages/Language.tsx +257 -0
  756. package/src/pages/ModulePage.tsx +366 -0
  757. package/src/pages/NotFound.tsx +54 -0
  758. package/src/pages/Overview.tsx +344 -0
  759. package/src/pages/Problems.tsx +360 -0
  760. package/src/pages/RegistryIndex.tsx +118 -0
  761. package/src/pages/ServicePage.tsx +746 -0
  762. package/src/pages/Settings.tsx +975 -0
  763. package/src/pages/StorePage.tsx +114 -0
  764. package/src/peek/PeekCard.test.ts +35 -0
  765. package/src/peek/PeekCard.tsx +118 -0
  766. package/src/peek/PeekLayer.tsx +147 -0
  767. package/src/peek/model.test.ts +156 -0
  768. package/src/peek/model.ts +253 -0
  769. package/src/routes.test.ts +222 -0
  770. package/src/routes.ts +449 -0
  771. package/src/selection/DetailPanel.tsx +1407 -0
  772. package/src/selection/hash.test.ts +73 -0
  773. package/src/selection/hash.ts +58 -0
  774. package/src/selection/model.test.ts +199 -0
  775. package/src/selection/model.ts +473 -0
  776. package/src/selection/pages.test.ts +115 -0
  777. package/src/selection/pages.ts +268 -0
  778. package/src/selection/store.test.ts +62 -0
  779. package/src/selection/store.ts +46 -0
  780. package/src/selection/sync.tsx +99 -0
  781. package/src/table/ColumnsMenu.tsx +72 -0
  782. package/src/table/DataTable.tsx +780 -0
  783. package/src/table/Facets.tsx +54 -0
  784. package/src/table/GroupMenu.tsx +69 -0
  785. package/src/table/Toolbar.tsx +190 -0
  786. package/src/table/cells.tsx +117 -0
  787. package/src/table/compare.test.ts +117 -0
  788. package/src/table/compare.ts +196 -0
  789. package/src/table/export.test.ts +88 -0
  790. package/src/table/export.ts +66 -0
  791. package/src/table/facet-groups.test.ts +74 -0
  792. package/src/table/facet-groups.ts +56 -0
  793. package/src/table/grouping.test.ts +84 -0
  794. package/src/table/grouping.ts +82 -0
  795. package/src/table/infer.test.ts +57 -0
  796. package/src/table/infer.ts +50 -0
  797. package/src/table/persist.test.ts +76 -0
  798. package/src/table/persist.ts +112 -0
  799. package/src/table/sort-url.test.ts +75 -0
  800. package/src/table/sort-url.ts +52 -0
  801. package/src/table/types.ts +80 -0
  802. package/src/table/useTableState.ts +220 -0
  803. package/src/test-catalog.ts +7 -0
  804. package/src/testing/estate/data__catalog.json +2847 -0
  805. package/src/testing/estate/data__flows.json +1724 -0
  806. package/src/testing/estate/examples__auth__portolan__api.json +212 -0
  807. package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
  808. package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
  809. package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
  810. package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
  811. package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
  812. package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
  813. package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
  814. package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
  815. package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
  816. package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
  817. package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
  818. package/src/testing/estate.ts +49 -0
  819. package/src/trail/Trail.tsx +81 -0
  820. package/src/trail/model.test.ts +157 -0
  821. package/src/trail/model.ts +189 -0
  822. package/src/trail/record.tsx +32 -0
  823. package/src/trail/store.test.ts +66 -0
  824. package/src/trail/store.ts +102 -0
  825. package/tsconfig.json +35 -0
  826. package/vite.config.ts +171 -0
@@ -0,0 +1,434 @@
1
+ """The schema, read off the models.
2
+
3
+ A Django model is both the domain object and the table, so the two facts the
4
+ SQL extractor has to pair up - a column, and the field it carries - are one
5
+ declaration here. That is why the store comes out of this plugin rather than
6
+ out of `extract-sql`: nothing has to be guessed, and `maps` is exact.
7
+
8
+ The types are the ones Django's PostgreSQL backend emits. `storeKind` picks the
9
+ mapping; a kind with no mapping is reported and the column keeps the field's
10
+ own name, which is honest about not knowing rather than plausible and wrong.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import ast
16
+ from dataclasses import dataclass
17
+ from typing import Dict, List, Optional, Tuple
18
+
19
+ import catalog
20
+ from apps import App, label_of
21
+ from domain import Aggregate, FieldDef, ModelDef
22
+ from ids import slug
23
+ from source import assigned, const_str, dotted, inner_class, keyword, keyword_bool, keyword_int, keyword_str
24
+
25
+ POSTGRES = {
26
+ "AutoField": "serial",
27
+ "BigAutoField": "bigserial",
28
+ "SmallAutoField": "smallserial",
29
+ "BigIntegerField": "bigint",
30
+ "BinaryField": "bytea",
31
+ "BooleanField": "boolean",
32
+ "CharField": "varchar",
33
+ "DateField": "date",
34
+ "DateTimeField": "timestamptz",
35
+ "DecimalField": "numeric",
36
+ "DurationField": "interval",
37
+ "EmailField": "varchar(254)",
38
+ "FileField": "varchar(100)",
39
+ "FilePathField": "varchar(100)",
40
+ "FloatField": "double precision",
41
+ "GenericIPAddressField": "inet",
42
+ "ImageField": "varchar(100)",
43
+ "IntegerField": "integer",
44
+ "JSONField": "jsonb",
45
+ "PositiveBigIntegerField": "bigint",
46
+ "PositiveIntegerField": "integer",
47
+ "PositiveSmallIntegerField": "smallint",
48
+ "SlugField": "varchar(50)",
49
+ "SmallIntegerField": "smallint",
50
+ "TextField": "text",
51
+ "TimeField": "time",
52
+ "URLField": "varchar(200)",
53
+ "UUIDField": "uuid",
54
+ # django-multiselectfield stores its comma-separated value in a varchar.
55
+ "MultiSelectField": "varchar",
56
+ }
57
+
58
+ ON_DELETE = {
59
+ "CASCADE": "cascade",
60
+ "SET_NULL": "set null",
61
+ "SET_DEFAULT": "set default",
62
+ "PROTECT": "restrict",
63
+ "RESTRICT": "restrict",
64
+ "DO_NOTHING": "no action",
65
+ }
66
+
67
+ def meta_value(model: ModelDef, name: str):
68
+ meta = inner_class(model.node, "Meta")
69
+ if meta is None:
70
+ return None
71
+ for attr, value, _ in assigned(meta):
72
+ if attr == name:
73
+ return value
74
+ return None
75
+
76
+
77
+ def meta_str(model: ModelDef, name: str) -> str:
78
+ return const_str(meta_value(model, name))
79
+
80
+
81
+ def table_name(model: ModelDef, label: str) -> str:
82
+ """`Meta.db_table`, or the name Django composes: `<app label>_<model>`."""
83
+ return meta_str(model, "db_table") or "%s_%s" % (label, model.name.lower())
84
+
85
+
86
+ def column_name(f: FieldDef) -> str:
87
+ declared = keyword_str(f.call, "db_column")
88
+ if declared:
89
+ return declared
90
+ return f.name + "_id" if f.kind in ("ForeignKey", "OneToOneField") else f.name
91
+
92
+
93
+ def field_call_type(field_kind: str, call: ast.Call, kind: str, b, ref: str, integers: Optional[Dict[str, int]] = None) -> str:
94
+ integers = integers or {}
95
+ if kind != "postgres":
96
+ return field_kind
97
+ if field_kind == "ArrayField":
98
+ base_field = call.args[0] if call.args else keyword(call, "base_field")
99
+ if not isinstance(base_field, ast.Call):
100
+ b.warn(ref, "ArrayField has no statically readable base field; the column keeps the field's own name")
101
+ return field_kind
102
+ item_kind = dotted(base_field.func).split(".")[-1]
103
+ item_type = field_call_type(item_kind, base_field, kind, b, ref)
104
+ size = integers.get("size", keyword_int(call, "size"))
105
+ return "%s[%s]" % (item_type, size if size is not None else "")
106
+ base = POSTGRES.get(field_kind)
107
+ if base is None:
108
+ b.warn(ref, "%s is not a field this reader has a column type for; the column keeps the field's own name" % field_kind)
109
+ return field_kind
110
+ if field_kind in ("CharField", "MultiSelectField"):
111
+ length = integers.get("max_length", keyword_int(call, "max_length"))
112
+ return "varchar(%d)" % length if length else "varchar"
113
+ if field_kind == "DecimalField":
114
+ digits = integers.get("max_digits", keyword_int(call, "max_digits"))
115
+ places = integers.get("decimal_places", keyword_int(call, "decimal_places"))
116
+ if digits is not None and places is not None:
117
+ return "numeric(%d,%d)" % (digits, places)
118
+ return "numeric"
119
+ return base
120
+
121
+
122
+ def column_type(f: FieldDef, kind: str, b, ref: str) -> str:
123
+ return field_call_type(f.kind, f.call, kind, b, ref, f.integers)
124
+
125
+
126
+ SERIAL = {"serial": "integer", "bigserial": "bigint", "smallserial": "smallint"}
127
+
128
+
129
+ @dataclass
130
+ class ModelIndex:
131
+ """Models addressable the same ways Django relation fields spell them."""
132
+
133
+ qualified: Dict[str, Tuple[str, ModelDef]]
134
+ by_name: Dict[str, List[Tuple[str, ModelDef]]]
135
+
136
+ def find(self, relation: str, current: ModelDef) -> Optional[Tuple[str, ModelDef]]:
137
+ name = relation.strip("'\"")
138
+ if name in ("self", ""):
139
+ name = current.name
140
+ if "." in name:
141
+ hit = self.qualified.get(name.lower())
142
+ if hit is not None:
143
+ return hit
144
+ local = self.qualified.get((current.app.label + "." + name).lower())
145
+ if local is not None:
146
+ return local
147
+ candidates = self.by_name.get(name.lower(), [])
148
+ return candidates[0] if len(candidates) == 1 else None
149
+
150
+
151
+ def target_of(f: FieldDef, model: ModelDef, tables: ModelIndex) -> Optional[Tuple[str, ModelDef]]:
152
+ """The model a relational field points at, wherever in the service it is."""
153
+ if f.kind not in ("ForeignKey", "OneToOneField", "ManyToManyField"):
154
+ return None
155
+ return tables.find(f.relation, model)
156
+
157
+
158
+ def auto_column_type(auto_field: str, kind: str) -> str:
159
+ return POSTGRES.get(auto_field, auto_field) if kind == "postgres" else auto_field
160
+
161
+
162
+ def referenced_field(model: ModelDef, relation: Optional[FieldDef] = None) -> Optional[FieldDef]:
163
+ to_field = keyword_str(relation.call, "to_field") if relation is not None else ""
164
+ return model.field(to_field) if to_field else pk_field(model)
165
+
166
+
167
+ def referenced_column(model: ModelDef, relation: Optional[FieldDef] = None) -> str:
168
+ field = referenced_field(model, relation)
169
+ return column_name(field) if field is not None else "id"
170
+
171
+
172
+ def referenced_name(model: ModelDef, relation: Optional[FieldDef] = None) -> str:
173
+ field = referenced_field(model, relation)
174
+ return field.name if field is not None else "id"
175
+
176
+
177
+ def key_type(model: ModelDef, kind: str, auto_field: str, b, ref: str, relation: Optional[FieldDef] = None) -> str:
178
+ """What a foreign key pointing at this model holds. A key into a bigserial
179
+ column is a bigint: the sequence belongs to the row, not to the reference."""
180
+ f = referenced_field(model, relation)
181
+ if f is None:
182
+ return SERIAL.get(auto_column_type(auto_field, kind), auto_column_type(auto_field, kind))
183
+ declared = column_type(f, kind, b, ref)
184
+ return SERIAL.get(declared, declared)
185
+
186
+
187
+ def fk_of(f: FieldDef, model: ModelDef, tables: ModelIndex, store_id: str) -> Optional[Dict[str, str]]:
188
+ constrained = keyword(f.call, "db_constraint")
189
+ if isinstance(constrained, ast.Constant) and constrained.value is False:
190
+ return None
191
+ hit = target_of(f, model, tables)
192
+ if hit is None:
193
+ return None
194
+ name, target_model = hit
195
+ on_delete = dotted(keyword(f.call, "on_delete")).split(".")[-1]
196
+ out = {"table": "%s.%s" % (store_id, name), "column": referenced_column(target_model, f)}
197
+ if on_delete in ON_DELETE:
198
+ out["onDelete"] = ON_DELETE[on_delete]
199
+ return out
200
+
201
+
202
+ def pk_field(model: ModelDef) -> Optional[FieldDef]:
203
+ for f in model.fields:
204
+ if keyword_bool(f.call, "primary_key"):
205
+ return f
206
+ return None
207
+
208
+
209
+ def pk_name(model: ModelDef) -> str:
210
+ f = pk_field(model)
211
+ return column_name(f) if f is not None else "id"
212
+
213
+
214
+ def indexes(model: ModelDef, table: str, columns: Dict[str, str]) -> List[Dict[str, object]]:
215
+ """What the database is told to keep, from the fields and from Meta.
216
+
217
+ Django appends a hash of its own to an index it names itself; inventing one
218
+ would be a name no database has, so an index without a declared name is
219
+ called after its table and its columns.
220
+ """
221
+ out: List[Dict[str, object]] = []
222
+ for f in model.fields:
223
+ column = columns.get(f.name)
224
+ if column is None:
225
+ continue
226
+ if keyword_bool(f.call, "unique"):
227
+ out.append({"name": "%s_%s_key" % (table, column), "columns": [column], "unique": True})
228
+ elif keyword_bool(f.call, "db_index"):
229
+ out.append({"name": "%s_%s" % (table, column), "columns": [column], "unique": False})
230
+ listed = meta_value(model, "indexes")
231
+ if isinstance(listed, (ast.List, ast.Tuple)):
232
+ for item in listed.elts:
233
+ if not isinstance(item, ast.Call):
234
+ continue
235
+ fields = keyword(item, "fields")
236
+ names = [columns.get(const_str(e).lstrip("-"), const_str(e).lstrip("-")) for e in fields.elts] if isinstance(fields, (ast.List, ast.Tuple)) else []
237
+ if not names:
238
+ continue
239
+ out.append({"name": keyword_str(item, "name") or "%s_%s" % (table, "_".join(names)), "columns": names, "unique": False})
240
+ for name in ("unique_together", "constraints"):
241
+ value = meta_value(model, name)
242
+ if not isinstance(value, (ast.List, ast.Tuple, ast.Set)):
243
+ continue
244
+ for item in value.elts:
245
+ names: List[str] = []
246
+ declared = ""
247
+ if isinstance(item, (ast.Tuple, ast.List)):
248
+ names = [columns.get(const_str(e), const_str(e)) for e in item.elts if const_str(e)]
249
+ elif isinstance(item, ast.Call):
250
+ fields = keyword(item, "fields")
251
+ if isinstance(fields, (ast.List, ast.Tuple)):
252
+ names = [columns.get(const_str(e), const_str(e)) for e in fields.elts if const_str(e)]
253
+ declared = keyword_str(item, "name")
254
+ if names:
255
+ out.append({"name": declared or "%s_%s_key" % (table, "_".join(names)), "columns": names, "unique": True})
256
+ return out
257
+
258
+
259
+ def index_models(models_by_app: Dict[str, List[ModelDef]]) -> ModelIndex:
260
+ """Every concrete model, including applications with no aggregate root.
261
+
262
+ Qualified names always resolve. A bare model name resolves only when it is
263
+ unique across the service, matching Django's preference for a local app
264
+ before a globally imported model.
265
+ """
266
+ qualified: Dict[str, Tuple[str, ModelDef]] = {}
267
+ by_name: Dict[str, List[Tuple[str, ModelDef]]] = {}
268
+ for models in models_by_app.values():
269
+ for model in models:
270
+ if not model.concrete:
271
+ continue
272
+ hit = (table_name(model, label_of(model.app)), model)
273
+ qualified[(model.app.label + "." + model.name).lower()] = hit
274
+ qualified[(model.app.dotted + "." + model.name).lower()] = hit
275
+ by_name.setdefault(model.name.lower(), []).append(hit)
276
+ return ModelIndex(qualified=qualified, by_name=by_name)
277
+
278
+
279
+ def aggregate_models(aggregates: List[Aggregate]) -> Dict[Tuple[str, str], Aggregate]:
280
+ out: Dict[Tuple[str, str], Aggregate] = {}
281
+ for aggregate in aggregates:
282
+ for model in aggregate.models:
283
+ out[(model.module.dotted, model.name)] = aggregate
284
+ return out
285
+
286
+
287
+ def model_table(
288
+ model: ModelDef,
289
+ names: ModelIndex,
290
+ aggregate: Optional[Aggregate],
291
+ domains: Dict[Tuple[str, str], Aggregate],
292
+ svc_id: str,
293
+ store: str,
294
+ kind: str,
295
+ auto_field: str,
296
+ b,
297
+ ) -> Dict[str, object]:
298
+ store_id = "%s.%s" % (svc_id, store)
299
+ found = names.find(model.app.label + "." + model.name, model)
300
+ name = found[0] if found is not None else table_name(model, label_of(model.app))
301
+ table_id = "%s.%s" % (store_id, name)
302
+ columns: List[Dict[str, object]] = []
303
+ by_field: Dict[str, str] = {}
304
+ if pk_field(model) is None:
305
+ columns.append(catalog.column("id", auto_column_type(auto_field, kind), False, pk=True))
306
+ by_field["id"] = "id"
307
+ for f in model.fields:
308
+ if f.kind == "ManyToManyField":
309
+ continue
310
+ column = column_name(f)
311
+ by_field[f.name] = column
312
+ fk = fk_of(f, model, names, store_id)
313
+ target = target_of(f, model, names)
314
+ if target is not None:
315
+ declared = key_type(target[1], kind, auto_field, b, table_id, f)
316
+ elif f.kind in ("ForeignKey", "OneToOneField"):
317
+ implicit = auto_column_type(auto_field, kind)
318
+ declared = SERIAL.get(implicit, implicit)
319
+ b.warn(
320
+ model.module.where(f.node),
321
+ "%s.%s points at %s, which is not a model read here; the column uses the default primary-key type and has no FK link"
322
+ % (model.name, f.name, f.relation or "a model"),
323
+ )
324
+ else:
325
+ declared = column_type(f, kind, b, table_id)
326
+ if target is not None and (target[1].module.dotted, target[1].name) in domains:
327
+ maps = "%s.%s" % (target[1].name, referenced_name(target[1], f))
328
+ else:
329
+ maps = "%s.%s" % (model.name, f.name) if aggregate is not None else ""
330
+ columns.append(
331
+ catalog.column(
332
+ column,
333
+ declared,
334
+ keyword_bool(f.call, "null"),
335
+ pk=keyword_bool(f.call, "primary_key"),
336
+ fk=fk,
337
+ maps=maps,
338
+ doc=f.help(),
339
+ )
340
+ )
341
+ block = "%s.%s" % (aggregate.id, slug(model.name)) if aggregate is not None else ""
342
+ return catalog.table(
343
+ table_id,
344
+ name,
345
+ columns,
346
+ indexes(model, name, by_field),
347
+ {"aggregate": aggregate.id, "block": block} if aggregate is not None else None,
348
+ "aggregate-root" if aggregate is not None and model is aggregate.root else "child" if aggregate is not None else "",
349
+ meta_str(model, "verbose_name"),
350
+ )
351
+
352
+
353
+ def many_to_many_tables(
354
+ model: ModelDef,
355
+ names: ModelIndex,
356
+ svc_id: str,
357
+ store: str,
358
+ kind: str,
359
+ auto_field: str,
360
+ b,
361
+ ) -> List[Dict[str, object]]:
362
+ """Implicit join tables created by Django for ManyToManyField values."""
363
+ store_id = "%s.%s" % (svc_id, store)
364
+ source_hit = names.find(model.app.label + "." + model.name, model)
365
+ if source_hit is None:
366
+ return []
367
+ source_table, _ = source_hit
368
+ out: List[Dict[str, object]] = []
369
+ for field in model.fields:
370
+ if field.kind != "ManyToManyField" or keyword(field.call, "through") is not None:
371
+ continue
372
+ target = target_of(field, model, names)
373
+ if target is None:
374
+ b.warn(model.module.where(field.node), "%s.%s is a many-to-many whose target %s cannot be resolved" % (model.name, field.name, field.relation or "model"))
375
+ continue
376
+ target_table, target_model = target
377
+ relation_table = keyword_str(field.call, "db_table") or "%s_%s" % (source_table, field.name)
378
+ table_id = "%s.%s" % (store_id, relation_table)
379
+ if target_model is model:
380
+ source_column = "from_%s_id" % model.name.lower()
381
+ target_column = "to_%s_id" % model.name.lower()
382
+ else:
383
+ source_column = "%s_id" % model.name.lower()
384
+ target_column = "%s_id" % target_model.name.lower()
385
+ columns = [
386
+ catalog.column("id", auto_column_type(auto_field, kind), False, pk=True),
387
+ catalog.column(
388
+ source_column,
389
+ key_type(model, kind, auto_field, b, table_id),
390
+ False,
391
+ fk={"table": "%s.%s" % (store_id, source_table), "column": pk_name(model), "onDelete": "cascade"},
392
+ ),
393
+ catalog.column(
394
+ target_column,
395
+ key_type(target_model, kind, auto_field, b, table_id),
396
+ False,
397
+ fk={"table": "%s.%s" % (store_id, target_table), "column": pk_name(target_model), "onDelete": "cascade"},
398
+ ),
399
+ ]
400
+ out.append(
401
+ catalog.table(
402
+ table_id,
403
+ relation_table,
404
+ columns,
405
+ [{"name": "%s_%s_%s_key" % (relation_table, source_column, target_column), "columns": [source_column, target_column], "unique": True}],
406
+ None,
407
+ "",
408
+ "Django-generated many-to-many table for %s.%s." % (model.name, field.name),
409
+ )
410
+ )
411
+ return out
412
+
413
+
414
+ def read_all(
415
+ models_by_app: Dict[str, List[ModelDef]],
416
+ aggregates: List[Aggregate],
417
+ svc_id: str,
418
+ store: str,
419
+ kind: str,
420
+ auto_field: str,
421
+ b,
422
+ ) -> List[Dict[str, object]]:
423
+ """The complete ORM schema, whether or not domain roots were inferred."""
424
+ names = index_models(models_by_app)
425
+ domains = aggregate_models(aggregates)
426
+ out: List[Dict[str, object]] = []
427
+ for models in models_by_app.values():
428
+ for model in models:
429
+ if not model.concrete:
430
+ continue
431
+ aggregate = domains.get((model.module.dotted, model.name))
432
+ out.append(model_table(model, names, aggregate, domains, svc_id, store, kind, auto_field, b))
433
+ out.extend(many_to_many_tables(model, names, svc_id, store, kind, auto_field, b))
434
+ return out
@@ -0,0 +1,242 @@
1
+ """The way in.
2
+
3
+ A DRF view is the handler: local HTTP methods, concrete generic actions and a
4
+ ViewSet's inherited or decorated actions. ``routing`` supplies the URLConf
5
+ mount, so the same endpoint opens a code flow and contributes its verb/path to
6
+ the service's inferred HTTP contract.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ import re
13
+ from dataclasses import dataclass, field as dc_field
14
+ from typing import Dict, List, Optional
15
+
16
+ from apps import App
17
+ from ids import slug
18
+ from routing import Route, Routes
19
+ from source import Module, const_str, doc, dotted, keyword, methods
20
+
21
+ # The actions a ViewSet has without writing one.
22
+ ACTIONS = {
23
+ "list": "GET",
24
+ "create": "POST",
25
+ "retrieve": "GET",
26
+ "update": "PUT",
27
+ "partial_update": "PATCH",
28
+ "destroy": "DELETE",
29
+ }
30
+
31
+ VIEW_BASES = ("ViewSet", "ModelViewSet", "ReadOnlyModelViewSet", "GenericViewSet", "APIView", "View", "GenericAPIView")
32
+
33
+ # Methods supplied by DRF's concrete generic views even when the project does
34
+ # not override them. These are framework declarations, not guesses about the
35
+ # application's code.
36
+ GENERIC_ACTIONS = {
37
+ "ListAPIView": [("list", "GET")],
38
+ "CreateAPIView": [("create", "POST")],
39
+ "ListCreateAPIView": [("list", "GET"), ("create", "POST")],
40
+ "RetrieveAPIView": [("retrieve", "GET")],
41
+ "UpdateAPIView": [("update", "PUT"), ("partial_update", "PATCH")],
42
+ "DestroyAPIView": [("destroy", "DELETE")],
43
+ "RetrieveUpdateAPIView": [("retrieve", "GET"), ("update", "PUT"), ("partial_update", "PATCH")],
44
+ "RetrieveDestroyAPIView": [("retrieve", "GET"), ("destroy", "DELETE")],
45
+ "RetrieveUpdateDestroyAPIView": [("retrieve", "GET"), ("update", "PUT"), ("partial_update", "PATCH"), ("destroy", "DELETE")],
46
+ "ModelViewSet": list(ACTIONS.items()),
47
+ "ReadOnlyModelViewSet": [("list", "GET"), ("retrieve", "GET")],
48
+ }
49
+
50
+
51
+ @dataclass
52
+ class Endpoint:
53
+ id: str # invoice_issue
54
+ action: str # issue
55
+ view: str # InvoiceViewSet
56
+ verb: str
57
+ node: ast.AST
58
+ module: Module
59
+ path: str = ""
60
+ route_source: str = ""
61
+ doc: str = ""
62
+ use_cases: List[str] = dc_field(default_factory=list)
63
+ path_parameters: Dict[str, str] = dc_field(default_factory=dict)
64
+
65
+
66
+ def basenames(app: App) -> Dict[str, str]:
67
+ """`router.register("invoices", InvoiceViewSet, basename="invoice")`, as the
68
+ view class to the name its endpoints go by."""
69
+ out = {}
70
+ for module in app.package("urls"):
71
+ for node in ast.walk(module.tree):
72
+ if not isinstance(node, ast.Call) or dotted(node.func).split(".")[-1] != "register":
73
+ continue
74
+ view = ""
75
+ for arg in node.args[1:]:
76
+ name = dotted(arg)
77
+ if name:
78
+ view = name.split(".")[-1]
79
+ break
80
+ if not view:
81
+ continue
82
+ base = const_str(keyword(node, "basename"))
83
+ if not base and len(node.args) > 0:
84
+ base = const_str(node.args[0]).strip("^/$").replace("/", "_")
85
+ out[view] = base or view_name(view)
86
+ return out
87
+
88
+
89
+ def view_name(name: str) -> str:
90
+ """InvoiceViewSet -> invoice: what is left when the framework's suffix is."""
91
+ for suffix in ("ViewSet", "APIView", "GenericAPIView", "View"):
92
+ if name.endswith(suffix) and len(name) > len(suffix):
93
+ name = name[: -len(suffix)]
94
+ break
95
+ return slug(name).replace("-", "_")
96
+
97
+
98
+ def verb_of(node: ast.AST, action: str) -> str:
99
+ decorator = None
100
+ for dec in getattr(node, "decorator_list", []):
101
+ if isinstance(dec, ast.Call) and dotted(dec.func).split(".")[-1] in ("action", "api_view"):
102
+ decorator = dec
103
+ if decorator is not None:
104
+ methods_arg = keyword(decorator, "methods")
105
+ if methods_arg is None and decorator.args:
106
+ methods_arg = decorator.args[0]
107
+ if isinstance(methods_arg, (ast.List, ast.Tuple, ast.Set)) and methods_arg.elts:
108
+ return const_str(methods_arg.elts[0]).upper()
109
+ return ACTIONS.get(action, "" if action not in ("get", "post", "put", "patch", "delete") else action.upper())
110
+
111
+
112
+ def route_base(route: Route, fallback: str) -> str:
113
+ if route.router:
114
+ return route.basename or fallback
115
+ named = slug(route.name).replace("-", "_") if route.name else ""
116
+ path = re.sub(r"[^a-z0-9]+", "_", route.path.lower()).strip("_")
117
+ return named or path or fallback
118
+
119
+
120
+ def action_path(route: Route, node: ast.AST, action: str) -> str:
121
+ if not route.router:
122
+ return route.path
123
+ if action in ("list", "create"):
124
+ return route.path
125
+ detail = action in ("retrieve", "update", "partial_update", "destroy")
126
+ suffix = action.replace("_", "-")
127
+ for dec in getattr(node, "decorator_list", []):
128
+ if not isinstance(dec, ast.Call) or dotted(dec.func).split(".")[-1] != "action":
129
+ continue
130
+ detail_node = keyword(dec, "detail")
131
+ detail = isinstance(detail_node, ast.Constant) and detail_node.value is True
132
+ suffix = const_str(keyword(dec, "url_path")) or suffix
133
+ base = route.path.rstrip("/")
134
+ return base + ("/{id}" if detail else "") + ("/" + suffix if action not in ACTIONS else "") + "/"
135
+
136
+
137
+ def endpoint(node: ast.AST, module: Module, view: str, action: str, verb: str, base: str, route: Optional[Route], description: str = "") -> Endpoint:
138
+ ident = base if base == action or base.endswith("_" + action) else "%s_%s" % (base, action)
139
+ return Endpoint(
140
+ id=ident,
141
+ action=action,
142
+ view=view,
143
+ verb=verb,
144
+ node=node,
145
+ module=module,
146
+ path=action_path(route, node, action) if route else "",
147
+ route_source=route.source if route else "",
148
+ doc=description,
149
+ path_parameters=dict(route.parameters) if route else {},
150
+ )
151
+
152
+
153
+ def read_endpoints(app: App, b, routes: Optional[Routes] = None) -> List[Endpoint]:
154
+ registered = basenames(app)
155
+ out: List[Endpoint] = []
156
+ for module in app.package("views"):
157
+ for node in module.classes():
158
+ bases = [base.split(".")[-1] for base in map(dotted, node.bases)]
159
+ mounted = routes.for_view(module.dotted, node.name) if routes else []
160
+ mounted += [route for route in (routes.entries if routes else []) if route.module == module.dotted and route.view.startswith(node.name + ".")]
161
+ if not any(base in VIEW_BASES or base in GENERIC_ACTIONS for base in bases) and not mounted:
162
+ continue
163
+ base = registered.get(node.name)
164
+ if base is None and not mounted:
165
+ base = view_name(node.name)
166
+ b.warn(module.rel, "%s is registered by no router in %s/urls.py; its endpoints are named after the class" % (node.name, app.rel))
167
+ handlers = {handler.name: handler for handler in methods(node)}
168
+ declared = []
169
+ for handler in handlers.values():
170
+ if handler.name.startswith("_"):
171
+ continue
172
+ is_action = any(
173
+ isinstance(dec, ast.Call) and dotted(dec.func).split(".")[-1] == "action" for dec in getattr(handler, "decorator_list", [])
174
+ )
175
+ if handler.name not in ACTIONS and not is_action and handler.name not in ("get", "post", "put", "patch", "delete"):
176
+ # A plain Django class may expose an arbitrarily named
177
+ # method directly in URLConf (`Planet.fetch`). The route
178
+ # proves the HTTP entrypoint but, without a method
179
+ # decorator or request-method branch, not one particular
180
+ # verb. Keep it as a flow root and leave it out of the
181
+ # inferred OpenAPI contract.
182
+ if handler.name not in {
183
+ route.view.split(".", 1)[1]
184
+ for route in mounted
185
+ if "." in route.view
186
+ }:
187
+ continue
188
+ declared.append((handler.name, verb_of(handler, handler.name), handler, doc(handler)))
189
+ inherited = []
190
+ for inherited_base in bases:
191
+ inherited += GENERIC_ACTIONS.get(inherited_base, [])
192
+ for action, verb in inherited:
193
+ same_direct_handler = mounted and all(not route.router for route in mounted) and any(item[1] == verb for item in declared)
194
+ if action not in handlers and not any(item[0] == action for item in declared) and not same_direct_handler:
195
+ declared.append((action, verb, node, doc(node)))
196
+ class_routes = routes.for_view(module.dotted, node.name) if routes else []
197
+ method_routes = {route.view.split(".", 1)[1]: route for route in mounted if "." in route.view}
198
+ if not class_routes and not method_routes:
199
+ for action, verb, handler, description in declared:
200
+ out.append(endpoint(handler, module, node.name, action, verb, base or view_name(node.name), None, description))
201
+ continue
202
+ for action, verb, handler, description in declared:
203
+ targets = class_routes or ([method_routes[action]] if action in method_routes else [])
204
+ for route in targets:
205
+ if not verb:
206
+ b.warn(
207
+ route.source,
208
+ "%s is mounted as an HTTP view, but no HTTP verb is declared; its flow is extracted and the route is omitted from inferred OpenAPI"
209
+ % route.view,
210
+ )
211
+ out.append(endpoint(handler, module, node.name, action, verb, route_base(route, base or view_name(node.name)), route, description))
212
+ for node in module.functions():
213
+ mounted = routes.for_view(module.dotted, node.name) if routes else []
214
+ if not mounted and not any(isinstance(dec, ast.Call) and dotted(dec.func).split(".")[-1] == "api_view" for dec in getattr(node, "decorator_list", [])):
215
+ continue
216
+ verb = verb_of(node, node.name)
217
+ if not verb:
218
+ continue
219
+ if mounted:
220
+ for route in mounted:
221
+ out.append(endpoint(node, module, "", verb.lower(), verb, route_base(route, node.name), route, doc(node)))
222
+ else:
223
+ out.append(Endpoint(id=node.name, action=node.name, view="", verb=verb, node=node, module=module, doc=doc(node)))
224
+ unique = {}
225
+ for found in out:
226
+ unique[(found.id, found.verb, found.path, found.module.rel)] = found
227
+ return sorted(unique.values(), key=lambda e: (e.id, e.verb, e.path))
228
+
229
+
230
+ def receivers(app: App) -> List[ast.AST]:
231
+ """`@receiver(signal)` functions: what runs when somebody else's event
232
+ arrives. Django keeps them in `handlers.py`, `signals.py` or `receivers.py`,
233
+ and all three are read."""
234
+ out = []
235
+ for name in ("handlers", "signals", "receivers"):
236
+ for module in app.package(name):
237
+ for node in module.functions():
238
+ for dec in getattr(node, "decorator_list", []):
239
+ if isinstance(dec, ast.Call) and dotted(dec.func).split(".")[-1] == "receiver":
240
+ out.append((module, node, dec))
241
+ break
242
+ return out