@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,302 @@
1
+ """Who may call an endpoint, read off DRF's permission and authentication
2
+ classes and written into the inferred OpenAPI document as `security`.
3
+
4
+ DRF decides both per view, and the reader follows the same order the
5
+ framework does: the handler's own `@action(permission_classes=...)` or the
6
+ function's `@permission_classes(...)`, then a `get_permissions()` that a
7
+ `self.action` branch can decide, then the class attribute - its own or one a
8
+ local base class declares - and past all of those `REST_FRAMEWORK` in the
9
+ settings module, and past that DRF's own defaults, which are session or
10
+ basic authentication and `AllowAny`. `authentication_classes` is read the
11
+ same way.
12
+
13
+ What is written is OpenAPI's, not DRF's. An authentication class becomes a
14
+ security scheme - `SessionAuthentication` a cookie, `TokenAuthentication` an
15
+ `Authorization` header, `JWTAuthentication` a bearer token - and the
16
+ permission classes decide whether an operation requires one of them, accepts
17
+ one optionally, or takes nobody's name. A permission this reader does not
18
+ know - a project's own `IsOwner` - is not guessed at: the operation gets no
19
+ `security` of its own, its classes are still written under
20
+ `x-portolan-permissions`, and the diagnostic says which class it was.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import ast
26
+ from dataclasses import dataclass, field
27
+ from typing import Any, Dict, List, Optional, Tuple
28
+
29
+ from celery_conf import follow, settings_module_name
30
+ from serializers import resolve_symbol, selected_return
31
+ from source import Module, Project, assigned, const_str, dotted, keyword
32
+
33
+ # DRF's authentication classes and the common third-party ones, to the
34
+ # security scheme OpenAPI writes them as. The id is what the operations refer
35
+ # to, and it is the one drf-spectacular gives the same class, so a document a
36
+ # project generates at runtime and this one agree on the name.
37
+ AUTHENTICATION: Dict[str, Tuple[str, Dict[str, Any]]] = {
38
+ "SessionAuthentication": ("cookieAuth", {"type": "apiKey", "in": "cookie", "name": "sessionid"}),
39
+ "BasicAuthentication": ("basicAuth", {"type": "http", "scheme": "basic"}),
40
+ "TokenAuthentication": (
41
+ "tokenAuth",
42
+ {"type": "apiKey", "in": "header", "name": "Authorization", "description": "Token-based authentication with required prefix \"Token\"."},
43
+ ),
44
+ "JWTAuthentication": ("jwtAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}),
45
+ "JWTStatelessUserAuthentication": ("jwtAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}),
46
+ "JSONWebTokenAuthentication": ("jwtAuth", {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}),
47
+ "OAuth2Authentication": (
48
+ "oauth2",
49
+ {"type": "oauth2", "flows": {}, "description": "OAuth 2.0 bearer token; the flows are configured at runtime and are not recoverable from source."},
50
+ ),
51
+ }
52
+
53
+ # DRF's own permissions, by what they say about who may call.
54
+ PUBLIC = {"AllowAny"}
55
+ REQUIRED = {"IsAuthenticated", "IsAdminUser", "DjangoModelPermissions", "DjangoObjectPermissions"}
56
+ READ_OPTIONAL = {"IsAuthenticatedOrReadOnly", "DjangoModelPermissionsOrAnonReadOnly"}
57
+ SAFE_VERBS = {"GET", "HEAD", "OPTIONS"}
58
+
59
+ # What DRF applies when the settings say nothing.
60
+ DEFAULT_AUTHENTICATION = ["SessionAuthentication", "BasicAuthentication"]
61
+ DEFAULT_PERMISSION = ["AllowAny"]
62
+
63
+
64
+ @dataclass
65
+ class Security:
66
+ """What one operation says about its caller."""
67
+
68
+ # The OpenAPI `security` requirement, or None when it could not be decided.
69
+ requirement: Optional[List[Dict[str, List[str]]]]
70
+ # The permission classes as the code names them, for `x-portolan-permissions`.
71
+ permissions: List[str] = field(default_factory=list)
72
+ # The schemes the requirement refers to, for `components.securitySchemes`.
73
+ schemes: Dict[str, Dict[str, Any]] = field(default_factory=dict)
74
+
75
+
76
+ class Registry:
77
+ def __init__(self, project: Project, settings: str, b) -> None:
78
+ self.project = project
79
+ self.b = b
80
+ self.default_authentication, self.default_permission = read_defaults(project, settings)
81
+ self.schemes: Dict[str, Dict[str, Any]] = {}
82
+ self._warned: set = set()
83
+
84
+ # --- what the document says at the top ---------------------------------
85
+
86
+ def default_security(self) -> Optional[List[Dict[str, List[str]]]]:
87
+ """The document-level requirement: what an operation the settings
88
+ alone decide would carry, for a POST."""
89
+ decided = self.decide(self.default_authentication, self.default_permission, "POST", "")
90
+ return decided.requirement
91
+
92
+ # --- what one operation says -------------------------------------------
93
+
94
+ def security_for(self, endpoint) -> Security:
95
+ declared = declared_security(endpoint)
96
+ if declared is not None:
97
+ return self.schemes_of(declared)
98
+ authentication = self.classes_for(endpoint, "authentication_classes", "get_authenticators")
99
+ if authentication is None:
100
+ authentication = list(self.default_authentication)
101
+ permission = self.classes_for(endpoint, "permission_classes", "get_permissions")
102
+ if permission is None:
103
+ permission = list(self.default_permission)
104
+ if UNRESOLVED in authentication or UNRESOLVED in permission:
105
+ names = [name for name in permission if name is not UNRESOLVED]
106
+ self.warn(endpoint.route_source or endpoint.module.rel, "%s builds its permission or authentication classes in a way this reader cannot follow; the operation carries no security of its own" % endpoint.id)
107
+ return Security(None, names)
108
+ return self.decide(authentication, permission, endpoint.verb.upper(), endpoint.route_source or endpoint.module.rel)
109
+
110
+ def decide(self, authentication: List[str], permission: List[str], verb: str, where: str) -> Security:
111
+ schemes: Dict[str, Dict[str, Any]] = {}
112
+ for name in authentication:
113
+ scheme_id, scheme = self.scheme_of(name, where)
114
+ schemes[scheme_id] = scheme
115
+ self.schemes.update(schemes)
116
+ unknown = [name for name in permission if name not in PUBLIC and name not in REQUIRED and name not in READ_OPTIONAL]
117
+ if unknown:
118
+ for name in unknown:
119
+ self.warn(where, "%s is not a DRF permission this reader knows, so whether the endpoint needs a caller is not said; the class is kept under x-portolan-permissions" % name)
120
+ return Security(None, list(permission), schemes)
121
+ # DRF's permission list is a conjunction, and its authentication list
122
+ # is tried in order until one answers, so the requirement is one
123
+ # alternative per scheme - and an empty one when nobody is needed.
124
+ required = any(name in REQUIRED for name in permission) or (verb not in SAFE_VERBS and any(name in READ_OPTIONAL for name in permission))
125
+ requirement = [{scheme_id: []} for scheme_id in schemes]
126
+ if required and not requirement:
127
+ self.warn(where, "the endpoint requires an authenticated caller but declares no authentication class: nobody can call it")
128
+ if not required and requirement:
129
+ requirement.append({})
130
+ return Security(requirement, list(permission), schemes)
131
+
132
+ def schemes_of(self, declared: List[Dict[str, List[str]]]) -> Security:
133
+ """A requirement the schema decorator spelled out, in OpenAPI's own
134
+ words; the schemes it names are the document's to declare."""
135
+ known = {scheme_id: scheme for _, (scheme_id, scheme) in AUTHENTICATION.items()}
136
+ schemes = {}
137
+ for alternative in declared:
138
+ for scheme_id in alternative:
139
+ if scheme_id in known:
140
+ schemes[scheme_id] = known[scheme_id]
141
+ self.schemes.update(schemes)
142
+ return Security(declared, [], schemes)
143
+
144
+ def scheme_of(self, name: str, where: str) -> Tuple[str, Dict[str, Any]]:
145
+ if name in AUTHENTICATION:
146
+ scheme_id, scheme = AUTHENTICATION[name]
147
+ return scheme_id, dict(scheme)
148
+ self.warn(where, "%s is not a DRF authentication class this reader knows; it is written as a security scheme of its own, and how the credential travels is not said" % name)
149
+ scheme_id = name[0].lower() + name[1:] if name else "customAuth"
150
+ return scheme_id, {
151
+ "type": "http",
152
+ "scheme": "unknown",
153
+ "description": "%s: a custom authentication class. How the credential travels is not recoverable from source." % name,
154
+ "x-portolan-inferred": True,
155
+ }
156
+
157
+ # --- where the classes come from ---------------------------------------
158
+
159
+ def classes_for(self, endpoint, attribute: str, selector: str) -> Optional[List[Any]]:
160
+ """The classes in force for this endpoint, in DRF's order of
161
+ precedence, or None when nothing but the settings says."""
162
+ node = endpoint.node
163
+ module = endpoint.module
164
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
165
+ for decorator in node.decorator_list:
166
+ if not isinstance(decorator, ast.Call):
167
+ continue
168
+ last = dotted(decorator.func).split(".")[-1]
169
+ if last == attribute and decorator.args:
170
+ return class_names(decorator.args[0], module)
171
+ if last == "action" and keyword(decorator, attribute) is not None:
172
+ return class_names(keyword(decorator, attribute), module)
173
+ view = view_class(endpoint)
174
+ if view is None:
175
+ return None
176
+ chosen = next((m for m in view.body if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef)) and m.name == selector), None)
177
+ if chosen is not None:
178
+ selected = selected_return(chosen.body, endpoint.action)
179
+ if selected is None:
180
+ return [UNRESOLVED]
181
+ return class_names(selected, module)
182
+ found = self.class_attribute(module, view, attribute, 0)
183
+ if found is None:
184
+ return None
185
+ value, owner = found
186
+ return class_names(value, owner)
187
+
188
+ def class_attribute(self, module: Module, node: ast.ClassDef, attribute: str, depth: int) -> Optional[Tuple[ast.AST, Module]]:
189
+ """The attribute as this class or a local base of it assigns it."""
190
+ for name, value, _ in assigned(node):
191
+ if name == attribute:
192
+ return value, module
193
+ if depth > 5:
194
+ return None
195
+ for base in node.bases:
196
+ name = dotted(base)
197
+ if not name:
198
+ continue
199
+ target_module, target_name = resolve_symbol(self.project, module, name)
200
+ target = self.project.module(target_module) if target_module else None
201
+ if target is None:
202
+ continue
203
+ base_node = next((c for c in target.classes() if c.name == target_name.split(".")[-1]), None)
204
+ if base_node is None:
205
+ continue
206
+ found = self.class_attribute(target, base_node, attribute, depth + 1)
207
+ if found is not None:
208
+ return found
209
+ return None
210
+
211
+ def warn(self, where: str, message: str) -> None:
212
+ if (where, message) in self._warned:
213
+ return
214
+ self._warned.add((where, message))
215
+ self.b.warn(where, message)
216
+
217
+
218
+ # A class expression syntax does not name, kept apart from every real name.
219
+ UNRESOLVED = object()
220
+
221
+
222
+ def view_class(endpoint) -> Optional[ast.ClassDef]:
223
+ if not endpoint.view:
224
+ return None
225
+ name = endpoint.view.split(".", 1)[0]
226
+ return next((node for node in endpoint.module.classes() if node.name == name), None)
227
+
228
+
229
+ def class_names(value: Optional[ast.AST], module: Module) -> List[Any]:
230
+ """`[IsAuthenticated, perms.IsOwner()]` as the class names it lists; an
231
+ element that is not a name - `IsAuthenticated | ReadOnly` - is UNRESOLVED."""
232
+ value = follow(value, module)
233
+ if not isinstance(value, (ast.List, ast.Tuple)):
234
+ return [UNRESOLVED]
235
+ out: List[Any] = []
236
+ for element in value.elts:
237
+ target = element.func if isinstance(element, ast.Call) else element
238
+ name = dotted(target).split(".")[-1]
239
+ out.append(name if name else UNRESOLVED)
240
+ return out
241
+
242
+
243
+ def declared_security(endpoint) -> Optional[List[Dict[str, List[str]]]]:
244
+ """`extend_schema(auth=[...])` or `swagger_auto_schema(security=[...])`
245
+ on the handler: the requirement as the author wrote it, when it is
246
+ written as literals."""
247
+ node = endpoint.node
248
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
249
+ return None
250
+ for decorator in node.decorator_list:
251
+ if not isinstance(decorator, ast.Call):
252
+ continue
253
+ last = dotted(decorator.func).split(".")[-1]
254
+ value = keyword(decorator, "auth") if last == "extend_schema" else keyword(decorator, "security") if last == "swagger_auto_schema" else None
255
+ if value is None:
256
+ continue
257
+ if not isinstance(value, (ast.List, ast.Tuple)):
258
+ return None
259
+ out: List[Dict[str, List[str]]] = []
260
+ for element in value.elts:
261
+ if isinstance(element, ast.Dict):
262
+ alternative: Dict[str, List[str]] = {}
263
+ for key, scopes in zip(element.keys, element.values):
264
+ scheme_id = const_str(key)
265
+ if not scheme_id or not isinstance(scopes, (ast.List, ast.Tuple)):
266
+ return None
267
+ alternative[scheme_id] = [const_str(scope) for scope in scopes.elts]
268
+ out.append(alternative)
269
+ elif const_str(element):
270
+ out.append({const_str(element): []})
271
+ else:
272
+ return None
273
+ return out
274
+ return None
275
+
276
+
277
+ def read_defaults(project: Project, settings: str) -> Tuple[List[str], List[str]]:
278
+ """`REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]` and
279
+ `["DEFAULT_PERMISSION_CLASSES"]` off the settings module, as class names;
280
+ DRF's own defaults where the settings say nothing."""
281
+ authentication = list(DEFAULT_AUTHENTICATION)
282
+ permission = list(DEFAULT_PERMISSION)
283
+ module = project.module(settings or settings_module_name(project))
284
+ if module is None:
285
+ return authentication, permission
286
+ config: Optional[ast.AST] = None
287
+ for name, value, _ in assigned(module.tree):
288
+ if name == "REST_FRAMEWORK":
289
+ config = follow(value, module)
290
+ if not isinstance(config, ast.Dict):
291
+ return authentication, permission
292
+ for key, value in zip(config.keys, config.values):
293
+ setting = const_str(key) if key is not None else ""
294
+ value = follow(value, module)
295
+ if setting not in ("DEFAULT_AUTHENTICATION_CLASSES", "DEFAULT_PERMISSION_CLASSES") or not isinstance(value, (ast.List, ast.Tuple)):
296
+ continue
297
+ names = [const_str(element).split(".")[-1] for element in value.elts if const_str(element)]
298
+ if setting == "DEFAULT_AUTHENTICATION_CLASSES":
299
+ authentication = names
300
+ else:
301
+ permission = names
302
+ return authentication, permission
@@ -0,0 +1,188 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+
7
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
8
+ sys.path.insert(1, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "pyplugin"))
9
+
10
+ import apps # noqa: E402
11
+ import auth # noqa: E402
12
+ import routing # noqa: E402
13
+ import serializers # noqa: E402
14
+ import transport # noqa: E402
15
+ from extract import openapi_document, routed_applications # noqa: E402
16
+ from protocol import Builder # noqa: E402
17
+ from source import Project # noqa: E402
18
+
19
+
20
+ class WhoMayCall(unittest.TestCase):
21
+ """Each way DRF says who may call, once, and what OpenAPI it becomes."""
22
+
23
+ def setUp(self):
24
+ self.holder = tempfile.TemporaryDirectory(prefix="portolan-django-auth-")
25
+ root = self.holder.name
26
+ files = {
27
+ "manage.py": 'os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")\n',
28
+ "config/settings.py": '''
29
+ ROOT_URLCONF = "config.urls"
30
+ REST_FRAMEWORK = {
31
+ "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.TokenAuthentication",),
32
+ "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"],
33
+ }
34
+ ''',
35
+ "config/urls.py": '''
36
+ from django.urls import include, path
37
+ urlpatterns = [path("", include("api.urls"))]
38
+ ''',
39
+ "api/base.py": '''
40
+ from rest_framework.permissions import IsAuthenticated
41
+ from rest_framework.views import APIView
42
+ class Protected(APIView):
43
+ permission_classes = [IsAuthenticated]
44
+ ''',
45
+ "api/views.py": '''
46
+ from drf_spectacular.utils import extend_schema
47
+ from rest_framework import viewsets
48
+ from rest_framework.decorators import api_view, authentication_classes, permission_classes
49
+ from rest_framework.permissions import AllowAny, IsAuthenticated
50
+ from rest_framework.views import APIView
51
+ from .base import Protected
52
+ from .perms import IsOwner
53
+
54
+ class Public(APIView):
55
+ def get(self, request):
56
+ return None
57
+
58
+ class Admin(Protected):
59
+ def post(self, request):
60
+ return None
61
+
62
+ class Mixed(viewsets.ViewSet):
63
+ def get_permissions(self):
64
+ if self.action == "list":
65
+ return [AllowAny()]
66
+ return [IsAuthenticated()]
67
+ def list(self, request):
68
+ return None
69
+ def create(self, request):
70
+ return None
71
+
72
+ class Owned(APIView):
73
+ permission_classes = [IsOwner]
74
+ def get(self, request):
75
+ return None
76
+
77
+ class Spelled(APIView):
78
+ @extend_schema(auth=[{"basicAuth": []}])
79
+ def get(self, request):
80
+ return None
81
+
82
+ @api_view(["POST"])
83
+ @authentication_classes([])
84
+ @permission_classes([IsAuthenticated])
85
+ def hook(request):
86
+ return None
87
+ ''',
88
+ "api/perms.py": '''
89
+ from rest_framework.permissions import BasePermission
90
+ class IsOwner(BasePermission):
91
+ pass
92
+ ''',
93
+ "api/urls.py": '''
94
+ from django.urls import path
95
+ from rest_framework.routers import DefaultRouter
96
+ from .views import Admin, Owned, Public, Spelled, hook, Mixed
97
+
98
+ router = DefaultRouter()
99
+ router.register("mixed", Mixed, basename="mixed")
100
+ urlpatterns = [
101
+ path("public/", Public.as_view()),
102
+ path("admin/", Admin.as_view()),
103
+ path("owned/", Owned.as_view()),
104
+ path("spelled/", Spelled.as_view()),
105
+ path("hook/", hook),
106
+ ] + router.urls
107
+ ''',
108
+ }
109
+ for name, contents in files.items():
110
+ path = os.path.join(root, name)
111
+ os.makedirs(os.path.dirname(path), exist_ok=True)
112
+ with open(path, "w", encoding="utf-8") as handle:
113
+ handle.write(contents)
114
+ self.project = Project(root, root, lambda path: os.path.relpath(path, root).replace(os.sep, "/"))
115
+ self.b = Builder()
116
+ routes = routing.read(self.project)
117
+ endpoint_apps = routed_applications(self.project, apps.discover(self.project, []), routes)
118
+ endpoints = []
119
+ for app in endpoint_apps:
120
+ endpoints += [(app, endpoint) for endpoint in transport.read_endpoints(app, self.b, routes)]
121
+ registry = serializers.read(self.project, endpoint_apps)
122
+ self.registry = auth.Registry(self.project, "", self.b)
123
+ self.spec = openapi_document(endpoints, "Api", self.b, registry, "", self.registry)
124
+
125
+ def tearDown(self):
126
+ self.holder.cleanup()
127
+
128
+ def operation(self, path, verb):
129
+ return self.spec["paths"][path][verb]
130
+
131
+ def test_the_settings_decide_the_document_and_a_view_that_says_nothing(self):
132
+ self.assertEqual(self.registry.default_authentication, ["TokenAuthentication"])
133
+ self.assertEqual(self.spec["security"], [{"tokenAuth": []}, {}])
134
+ self.assertEqual(self.operation("/public/", "get")["security"], [{"tokenAuth": []}, {}])
135
+ self.assertEqual(self.operation("/public/", "get")["x-portolan-permissions"], ["AllowAny"])
136
+ self.assertEqual(self.spec["components"]["securitySchemes"]["tokenAuth"]["in"], "header")
137
+
138
+ def test_a_local_base_class_lends_its_permissions(self):
139
+ self.assertEqual(self.operation("/admin/", "post")["security"], [{"tokenAuth": []}])
140
+ self.assertEqual(self.operation("/admin/", "post")["x-portolan-permissions"], ["IsAuthenticated"])
141
+
142
+ def test_get_permissions_is_decided_per_action(self):
143
+ self.assertEqual(self.operation("/mixed/", "get")["security"], [{"tokenAuth": []}, {}])
144
+ self.assertEqual(self.operation("/mixed/", "post")["security"], [{"tokenAuth": []}])
145
+
146
+ def test_a_permission_the_reader_does_not_know_is_named_and_not_guessed(self):
147
+ owned = self.operation("/owned/", "get")
148
+ self.assertNotIn("security", owned)
149
+ self.assertEqual(owned["x-portolan-permissions"], ["IsOwner"])
150
+ self.assertTrue(any("IsOwner is not a DRF permission" in w.message for w in self.b.warnings))
151
+
152
+ def test_the_schema_decorator_spells_the_requirement_itself(self):
153
+ self.assertEqual(self.operation("/spelled/", "get")["security"], [{"basicAuth": []}])
154
+ self.assertEqual(self.spec["components"]["securitySchemes"]["basicAuth"], {"type": "http", "scheme": "basic"})
155
+
156
+ def test_a_function_view_reads_its_decorators_and_an_empty_list_is_a_fact(self):
157
+ hook = self.operation("/hook/", "post")
158
+ self.assertEqual(hook["security"], [])
159
+ self.assertEqual(hook["x-portolan-permissions"], ["IsAuthenticated"])
160
+ self.assertTrue(any("nobody can call it" in w.message for w in self.b.warnings))
161
+
162
+ def test_the_document_is_still_json(self):
163
+ json.dumps(self.spec)
164
+
165
+
166
+ class ReadOnly(unittest.TestCase):
167
+ def test_read_only_permissions_relax_safe_verbs_only(self):
168
+ registry = auth.Registry.__new__(auth.Registry)
169
+ registry.schemes = {}
170
+ registry._warned = set()
171
+ registry.b = Builder()
172
+ for verb, expected in (("GET", [{"cookieAuth": []}, {}]), ("POST", [{"cookieAuth": []}])):
173
+ decided = registry.decide(["SessionAuthentication"], ["IsAuthenticatedOrReadOnly"], verb, "")
174
+ self.assertEqual(decided.requirement, expected)
175
+
176
+ def test_a_custom_authentication_class_is_a_scheme_whose_transport_is_unknown(self):
177
+ registry = auth.Registry.__new__(auth.Registry)
178
+ registry.schemes = {}
179
+ registry._warned = set()
180
+ registry.b = Builder()
181
+ decided = registry.decide(["HmacAuthentication"], ["IsAuthenticated"], "POST", "api/views.py")
182
+ self.assertEqual(decided.requirement, [{"hmacAuthentication": []}])
183
+ self.assertEqual(decided.schemes["hmacAuthentication"]["scheme"], "unknown")
184
+ self.assertEqual(len(registry.b.warnings), 1)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ unittest.main()
@@ -0,0 +1,121 @@
1
+ """A call to another service, read off the client that makes it.
2
+
3
+ `<app>/clients/<peer>/` holds the vendored document and the class that calls
4
+ it. The code names the verb and the route - `self._http.post("/v1/quotes")` -
5
+ and the document beside it says which operation answers there, so the call is
6
+ recorded under the id the callee's own extractor would give it. Without a
7
+ document there is no id to share: the call is still recorded, against the route
8
+ it names, and left unresolved.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ast
14
+ import os
15
+ from dataclasses import dataclass, field as dc_field
16
+ from typing import Dict, List, Optional, Tuple
17
+
18
+ import openapi
19
+ from apps import App
20
+ from source import Module, const_str, dotted
21
+
22
+ VERBS = {"get", "post", "put", "patch", "delete", "head", "options", "request"}
23
+
24
+
25
+ @dataclass
26
+ class Call:
27
+ """One method of a client, and what it reaches."""
28
+
29
+ id: str # "shop.v1.Pricing/GetQuote", or "POST /v1/quotes" when nothing names it
30
+ pkg: str # what the manifest's peers map is keyed by: the api id
31
+ source: str # the document, or the client module
32
+ label: str # what the arrow says: the operation, or the route
33
+ resolved: bool
34
+
35
+
36
+ @dataclass
37
+ class Client:
38
+ name: str # the class
39
+ module: Module
40
+ directory: str
41
+ spec: Optional[openapi.Spec] = None
42
+ calls: Dict[str, Call] = dc_field(default_factory=dict) # method -> call
43
+
44
+
45
+ def path_of(node: ast.AST) -> str:
46
+ """The route as the code writes it, with every hole spelled the same."""
47
+ literal = const_str(node)
48
+ if literal:
49
+ return literal
50
+ if isinstance(node, ast.JoinedStr):
51
+ out = ""
52
+ for part in node.values:
53
+ if isinstance(part, ast.Constant) and isinstance(part.value, str):
54
+ out += part.value
55
+ else:
56
+ out += "{}"
57
+ return out
58
+ return ""
59
+
60
+
61
+ def http_call(node: ast.Call) -> Optional[Tuple[str, str]]:
62
+ """(verb, route) for a call that goes out over HTTP.
63
+
64
+ The route has to look like one. `get` is the name of an HTTP verb and also
65
+ the name of the method every mapping in Python has, and what tells the two
66
+ apart is that only one of them is handed a path."""
67
+ name = dotted(node.func)
68
+ verb = name.split(".")[-1]
69
+ if verb not in VERBS or not node.args:
70
+ return None
71
+ if verb == "request":
72
+ if len(node.args) < 2:
73
+ return None
74
+ route = path_of(node.args[1])
75
+ return (const_str(node.args[0]).upper(), route) if is_route(route) else None
76
+ route = path_of(node.args[0])
77
+ return (verb.upper(), route) if is_route(route) else None
78
+
79
+
80
+ def is_route(route: str) -> bool:
81
+ return route.startswith("/") or route.startswith("http://") or route.startswith("https://")
82
+
83
+
84
+ def read_clients(app: App, peers: Dict[str, str], rel, b) -> List[Client]:
85
+ out: List[Client] = []
86
+ for module in app.package("clients"):
87
+ directory = os.path.dirname(module.path)
88
+ spec_path = openapi.beside(directory)
89
+ spec = openapi.read(spec_path) if spec_path else None
90
+ for node in module.classes():
91
+ client = Client(name=node.name, module=module, directory=directory, spec=spec)
92
+ for method in node.body:
93
+ if not isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
94
+ continue
95
+ for child in ast.walk(method):
96
+ if not isinstance(child, ast.Call):
97
+ continue
98
+ found = http_call(child)
99
+ if found is None:
100
+ continue
101
+ verb, route = found
102
+ client.calls[method.name] = resolve(verb, route, spec, module, os.path.basename(directory), rel, b)
103
+ break
104
+ if client.calls:
105
+ out.append(client)
106
+ if spec is None and any(module.classes()):
107
+ b.warn(module.rel, "no OpenAPI document beside this client: its calls name a route, and nothing says which operation answers there")
108
+ for client in out:
109
+ for call in client.calls.values():
110
+ if call.resolved and call.pkg not in peers:
111
+ b.warn(call.id, "%s answers this call and the manifest names no peer for it: the step stays unresolved" % call.pkg)
112
+ return out
113
+
114
+
115
+ def resolve(verb: str, route: str, spec: Optional[openapi.Spec], module: Module, fallback: str, rel, b) -> Call:
116
+ if spec is not None:
117
+ operation = spec.find(verb, route)
118
+ if operation is not None:
119
+ return Call(id=operation.call_id(spec.api), pkg=spec.api, source=rel(spec.path), label=operation.id, resolved=True)
120
+ b.warn(module.rel, "%s %s is not a route %s declares" % (verb, route, rel(spec.path)))
121
+ return Call(id="%s %s" % (verb, route), pkg=fallback, source=module.rel, label="%s %s" % (verb, route), resolved=False)