@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.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- 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
|