@shortlink-org/portolan 0.2.4 → 0.4.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/README.md +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -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 +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +28 -15
- package/plugin/describe.go +156 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.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-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +116 -4
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/selection/DetailPanel.tsx +15 -0
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"properties": {
|
|
5
|
+
"repo": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "The repository this tree is, spelled the way `Service.repo` spells it. A git generator pointing at another repository cannot be walked and is passed over with a warning; left out, every git generator is taken to mean this tree.",
|
|
8
|
+
"examples": ["github.com/acme/gitops"]
|
|
9
|
+
},
|
|
10
|
+
"paths": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": { "type": "string" },
|
|
13
|
+
"description": "Directories under the input root that hold the Applications and ApplicationSets, so overlays and charts beside them are not read for Argo CD objects. Left out, the whole root is walked.",
|
|
14
|
+
"examples": [["bootstrap", "appsets"]]
|
|
15
|
+
},
|
|
16
|
+
"environmentLabel": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"default": "env",
|
|
19
|
+
"description": "The Application label that names the environment. An Application without it is placed by the cluster it deploys to."
|
|
20
|
+
},
|
|
21
|
+
"labels": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"description": "Which Application labels name the service it deploys. The defaults are the Kubernetes recommended labels, the same ones fetch-argocd and fetch-k8s read.",
|
|
25
|
+
"properties": {
|
|
26
|
+
"context": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "The label whose value is the bounded context's slug.",
|
|
29
|
+
"default": "app.kubernetes.io/part-of"
|
|
30
|
+
},
|
|
31
|
+
"service": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "The label whose value is the service's slug.",
|
|
34
|
+
"default": "app.kubernetes.io/name"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"out": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Name of the fragment file, inside the step's output directory.",
|
|
41
|
+
"default": "argocd.json"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -155,8 +155,6 @@ def extract(input_: Input, opts: Options, b: Builder, cwd: str = "") -> None:
|
|
|
155
155
|
flows.sort(key=lambda f: f["slug"])
|
|
156
156
|
|
|
157
157
|
fragment: Dict[str, Any] = {
|
|
158
|
-
"generatedAt": input_.generated_at,
|
|
159
|
-
"commit": input_.commit,
|
|
160
158
|
"contexts": [
|
|
161
159
|
{
|
|
162
160
|
"id": context,
|
|
@@ -27,7 +27,7 @@ ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
|
|
27
27
|
def run(fixture, options):
|
|
28
28
|
b = Builder()
|
|
29
29
|
extract(
|
|
30
|
-
Input(root=os.path.relpath(os.path.join(HERE, "testdata", fixture), ROOT)
|
|
30
|
+
Input(root=os.path.relpath(os.path.join(HERE, "testdata", fixture), ROOT)),
|
|
31
31
|
Options.of(options),
|
|
32
32
|
b,
|
|
33
33
|
cwd=ROOT,
|
|
@@ -25,6 +25,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
25
25
|
DESCRIPTOR = {
|
|
26
26
|
"name": "extract-celery",
|
|
27
27
|
"summary": "Reads Celery task declarations, the calls that enqueue them and the routes that place them into work queues and source-backed job flows.",
|
|
28
|
+
"category": "messaging",
|
|
28
29
|
"phases": ["extract"],
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# extract-csharp-ddd
|
|
2
|
+
|
|
3
|
+
A C# tree laid out by module and layer in, a catalog fragment out - three
|
|
4
|
+
kinds, when the API host proves an HTTP contract and the database project a
|
|
5
|
+
schema. The .NET twin of `extract-php-ddd`: like it, this reads a layout
|
|
6
|
+
rather than a framework, and the layout is the claim. Nothing is annotated
|
|
7
|
+
for the catalog; the directory a class sits in and the base it extends say
|
|
8
|
+
what it is.
|
|
9
|
+
|
|
10
|
+
The layout is the one [kgrzybek's modular-monolith-with-ddd](https://github.com/kgrzybek/modular-monolith-with-ddd)
|
|
11
|
+
made common in .NET - `src/Modules/<Module>/{Domain,Application,Infrastructure,IntegrationEvents}`
|
|
12
|
+
with one API host under `src/API` and one SQL Server database project under
|
|
13
|
+
`src/Database` - and it is that project the site ships as a profile, read by
|
|
14
|
+
this extractor alone.
|
|
15
|
+
|
|
16
|
+
Written in C# and run as a process plugin, `dotnet plugins/extract-csharp-ddd/bin/portolan-extract-csharp-ddd.dll`,
|
|
17
|
+
built by `npm run plugins:build` with the .NET SDK (8 or later). The parser
|
|
18
|
+
is Roslyn, the compiler's own, and the one dependency; every `.cs` under the
|
|
19
|
+
root goes into one `Compilation`, so a partial class is one type, a base
|
|
20
|
+
chain is followed across files and modules, and the type of `new X(...)` or
|
|
21
|
+
of a variable is a fact rather than a guess. **The application is never
|
|
22
|
+
built or run, and none of its packages is restored.** What comes from NuGet
|
|
23
|
+
- MediatR's `INotificationHandler`, EF's `IEntityTypeConfiguration`,
|
|
24
|
+
ASP.NET's `ControllerBase` - is an error type with a name, and every rule
|
|
25
|
+
that touches one goes by the name as written on the base list. Only the
|
|
26
|
+
runtime's own assemblies are referenced, so that `Guid`, `string` and
|
|
27
|
+
`List<T>` read as themselves.
|
|
28
|
+
|
|
29
|
+
## The layout it reads
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
src/Modules/<Module>/Domain/<Aggregate>/ the model: root, entities, value objects, events, ports
|
|
33
|
+
src/Modules/<Module>/Application/<Group>/ the use cases: commands, queries, their handlers, notification handlers
|
|
34
|
+
src/Modules/<Module>/Infrastructure/ the adapters: repositories, EF configurations, the bus subscriptions
|
|
35
|
+
src/Modules/<Module>/IntegrationEvents/ what the module tells the other modules
|
|
36
|
+
src/API/**/Modules/<Module>/*Controller.cs the HTTP edge of that module
|
|
37
|
+
src/Database/**/Structure/<schema>/Tables/ the tables, one file each, T-SQL
|
|
38
|
+
src/Database/**/Structure/<schema>/Views/ the views over them
|
|
39
|
+
src/BuildingBlocks/ the bases: Entity, ValueObject, DomainEventBase, IntegrationEvent
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`modules`, `api` and `database` in the options rename the three roots. A
|
|
43
|
+
directory named `Tests`, or ending in `Tests`, is not read; neither is `bin/`
|
|
44
|
+
or `obj/`.
|
|
45
|
+
|
|
46
|
+
## What becomes what
|
|
47
|
+
|
|
48
|
+
**Context.** A directory under `src/Modules` with `Domain` or `Application`
|
|
49
|
+
under it, with its slug as id - `user-access` for `UserAccess`.
|
|
50
|
+
`classification` in the options applies to every module the tree declares;
|
|
51
|
+
nothing in the tree says which is core.
|
|
52
|
+
|
|
53
|
+
**Service.** One per module, `<module>.module` - `meetings.module` - because
|
|
54
|
+
a module here has its own schema, its own outbox, its own composition root
|
|
55
|
+
and its own slice of the API. There is one deployable, and the catalog says
|
|
56
|
+
so where it matters: a call from one module to another is drawn as a `call`
|
|
57
|
+
step with a note that it is in process. The controllers filed under the
|
|
58
|
+
module's directory of the API host are its HTTP interfaces, one per
|
|
59
|
+
controller, `meetings.module.meetings`.
|
|
60
|
+
|
|
61
|
+
**Aggregate.** A directory under `Domain/` whose own files declare a root: a
|
|
62
|
+
class implementing `IAggregateRoot` or extending a class named
|
|
63
|
+
`AggregateRoot` (the event-sourced base Payments keeps in its `SeedWork`).
|
|
64
|
+
Subdirectories with no root of their own fold in - `Events/`, `Rules/` - and
|
|
65
|
+
one with a root is an aggregate in its own right, `Members/MemberSubscriptions`.
|
|
66
|
+
`SharedKernel`, `SeedWork`, `Shared`, `Rules`, `Events` and `Exceptions` at
|
|
67
|
+
the top of `Domain/` are not aggregates. A directory with types and no root
|
|
68
|
+
is a group the application layer may claim as a `model-group`; unclaimed, it
|
|
69
|
+
is reported.
|
|
70
|
+
|
|
71
|
+
The root and every class extending `Entity` are the entities, the root first.
|
|
72
|
+
Their fields are the public properties inherited from a base that is not the
|
|
73
|
+
framework's, then their own members in source order, a private `_title`
|
|
74
|
+
read as `title`, the `_domainEvents` list left out. A value object is a
|
|
75
|
+
class extending `ValueObject`, `TypedIdValueBase` or `AggregateId<T>`, with
|
|
76
|
+
its public properties read up the base chain, so `MeetingId` has the base's
|
|
77
|
+
`value: Guid`; one with no fields is reported and not written. A C# `enum` in
|
|
78
|
+
the directory is a closed set with its members, `[Obsolete]` carried as
|
|
79
|
+
deprecated. Rules, exceptions, domain services and `I*Context` interfaces are
|
|
80
|
+
not shape and are skipped.
|
|
81
|
+
|
|
82
|
+
**Domain event.** A class extending `DomainEventBase` or implementing
|
|
83
|
+
`IDomainEvent`, in the aggregate's directory. Its fields are its own public
|
|
84
|
+
properties - `Id` and `OccurredOn` are the envelope. It has no wire: a
|
|
85
|
+
domain event here is a MediatR notification in the same transaction, and a
|
|
86
|
+
notification replayed from the outbox after it. Its consumers are the
|
|
87
|
+
module's `INotificationHandler<TheEvent>` classes, and its
|
|
88
|
+
`INotificationHandler<TheNotification>` classes where the notification
|
|
89
|
+
extends `DomainNotificationBase<TheEvent>`, each `declared` with the
|
|
90
|
+
handler's name. The id keeps the class name whole,
|
|
91
|
+
`meetings.module.meetings.MeetingCreatedDomainEvent`, because the
|
|
92
|
+
integration event that follows it is a different fact with the same stem.
|
|
93
|
+
|
|
94
|
+
**Integration event.** A class extending `IntegrationEvent` under the
|
|
95
|
+
module's `IntegrationEvents/`. Its publisher is the module whose application
|
|
96
|
+
layer calls `IEventsBus.Publish(new X(...))`, and its aggregate is the one
|
|
97
|
+
whose domain event the publishing handler's notification wraps; an event
|
|
98
|
+
nobody publishes is reported and filed under the owning module's
|
|
99
|
+
`integration-events` group. Its wire is the class name on its own channel,
|
|
100
|
+
`<bus>.<Event>` with `bus` from the options, `integration-events` by
|
|
101
|
+
default, because the code names the class `InMemoryEventBus` and nothing
|
|
102
|
+
else. Its consumers are the modules whose
|
|
103
|
+
infrastructure says `SubscribeToIntegrationEvent<X>` or
|
|
104
|
+
`new IntegrationEventGenericHandler<X>()`, each with the
|
|
105
|
+
`INotificationHandler<X>` that hears it - `declared` when both are there,
|
|
106
|
+
`unresolved` and reported when a module subscribes without a handler or
|
|
107
|
+
handles without subscribing.
|
|
108
|
+
|
|
109
|
+
**Channel.** The bus has no named channels - `InMemoryEventBus` routes by
|
|
110
|
+
type and a module subscribes to a type - so each integration event is a
|
|
111
|
+
channel of its own, `<bus>.<Event>`, of kind `event`: the module that
|
|
112
|
+
publishes it sends on it, the modules that subscribe receive. Every module
|
|
113
|
+
whose application layer calls `ICommandsScheduler.EnqueueAsync(new C(...))`
|
|
114
|
+
has a `job` channel, `<module>.internal-commands`: the command written to
|
|
115
|
+
its `InternalCommands` table in the same transaction, read by the
|
|
116
|
+
ProcessInternalCommands job and handed to its handler. A command enqueued
|
|
117
|
+
that no handler in the module takes is reported.
|
|
118
|
+
|
|
119
|
+
**Operation.** A class under `Application/` whose base list says
|
|
120
|
+
`ICommandHandler<C>` or `ICommandHandler<C, R>` or `IQueryHandler<Q, R>`; the
|
|
121
|
+
message names the operation, `create-meeting` for `CreateMeetingCommand`.
|
|
122
|
+
Its fields are the message's widest constructor's parameters, or its
|
|
123
|
+
settable properties when it has none; its doc the handler's `<summary>`, or
|
|
124
|
+
the message's; its source the handler's `Handle`. The aggregate is the
|
|
125
|
+
domain directory named like the first directory under `Application/`
|
|
126
|
+
(`Application/Meetings/CreateMeeting/` is `Domain/Meetings`), and a group
|
|
127
|
+
with no such directory - `Countries`, `Authentication` - is a model-group
|
|
128
|
+
named after it. A message extending `InternalCommandBase` is reached from
|
|
129
|
+
the job channel; one implementing `IRecurringCommand` runs on a Quartz
|
|
130
|
+
schedule. A handler under `Infrastructure/` is plumbing - `ProcessOutboxCommandHandler`
|
|
131
|
+
- and is not read. `exposedBy` names the actions of the module's own
|
|
132
|
+
controllers that new the message up; an action in another module's
|
|
133
|
+
controller is a call across contexts and lives in the flow.
|
|
134
|
+
|
|
135
|
+
**Store.** One per schema of the database project, owned by the module
|
|
136
|
+
whose EF configurations `ToTable("X", "schema")` into it, else the module
|
|
137
|
+
named like it, else the module that writes it; a read owns nothing, and a
|
|
138
|
+
schema with no owner - `app`, written by the building blocks - is reported
|
|
139
|
+
and not read. The module's home schema is its `db`, `meetings.module.db`;
|
|
140
|
+
a second schema keeps its name. `storeKind` in the options says what the
|
|
141
|
+
schemas live in, `other` for SQL Server. A table is read from its
|
|
142
|
+
`CREATE TABLE`: columns with type and nullability, the primary key from the
|
|
143
|
+
constraint or the column, foreign keys, and the indexes created beside it.
|
|
144
|
+
A view is read from its `CREATE VIEW`: the select list's names, each typed
|
|
145
|
+
from the column it selects when the alias resolves, the tables it reads,
|
|
146
|
+
and the SELECT itself as the definition.
|
|
147
|
+
|
|
148
|
+
An `IEntityTypeConfiguration<T>` in the module's infrastructure says which
|
|
149
|
+
table holds T: the `ToTable` in `Configure` is the table, `Property<X>("_f").HasColumnName("C")`
|
|
150
|
+
and `Property(x => x.P)` map its columns to T's fields, an `OwnsOne` maps
|
|
151
|
+
the value object's parts to the owner's field, and an `OwnsMany` with a
|
|
152
|
+
`ToTable` of its own is the child's table. The table `persists` the block
|
|
153
|
+
and takes the role `aggregate-root` or `child`; a table nobody maps is
|
|
154
|
+
`outbox` when it is the outbox, `projection` when the application
|
|
155
|
+
layer writes it with SQL, `lookup` when it is only ever read, `other`
|
|
156
|
+
otherwise. A `Messages` or `Streams` table is where an event-sourced
|
|
157
|
+
module's `IAggregateStore` keeps its streams.
|
|
158
|
+
|
|
159
|
+
Accesses come from two places. A class in the infrastructure implementing an
|
|
160
|
+
`I*Repository` port from the domain, or `IAggregateStore`, reads and writes
|
|
161
|
+
the table that holds the port's root - `write MeetingRepository.AddAsync` -
|
|
162
|
+
by the method's name: `Add`, `Save`, `Update`, `Append` write, `Remove` and
|
|
163
|
+
`Delete` delete, the rest read. Every string in a method of the application
|
|
164
|
+
or infrastructure layer is read as SQL: `FROM`, `JOIN`, `INSERT INTO`,
|
|
165
|
+
`UPDATE` and `DELETE FROM` against a schema-qualified name are accesses by
|
|
166
|
+
that method; a view read is an access on the tables the view reads; a name
|
|
167
|
+
the database project does not declare is reported - which is how the
|
|
168
|
+
project's `v_Countriess` was found.
|
|
169
|
+
|
|
170
|
+
**Flow.** One per controller action: the request in, the command or query
|
|
171
|
+
the action news up, then what the handler does, followed through the
|
|
172
|
+
classes it holds and the domain methods it calls, by symbol. A call on a
|
|
173
|
+
port is a step to the store; a Dapper query a step to the tables its SQL
|
|
174
|
+
names; `AddDomainEvent(...)` an event raised in process, drawn as a
|
|
175
|
+
self-message with the event's id; `IEventsBus.Publish(...)` an event on the
|
|
176
|
+
bus with a `message` handoff; `ICommandsScheduler.EnqueueAsync(...)` a `job`
|
|
177
|
+
handoff to the module's internal queue; a command handed to another module's
|
|
178
|
+
facade a `call` across the boundary, followed there. One flow per
|
|
179
|
+
notification handler of a domain event, from the event in; one per
|
|
180
|
+
subscription a module handles, from the bus in; one per internal command,
|
|
181
|
+
from the queue in; one per recurring command, on its schedule. Each flow
|
|
182
|
+
carries the trigger it was read from: `http`, `event`, `message`, `job` or
|
|
183
|
+
`scheduled`.
|
|
184
|
+
|
|
185
|
+
**HTTP.** One OpenAPI 3.1 document per module with controllers,
|
|
186
|
+
`openapi.<module>.yaml` (`openapiOut` in the options), one operation per
|
|
187
|
+
action: the path from `[Route]` and `[HttpGet("...")]` with `[controller]`
|
|
188
|
+
filled in, the `{parameters}` typed from the action's, the `[FromBody]`
|
|
189
|
+
class as the request schema one level deep, `[ProducesResponseType]` as the
|
|
190
|
+
responses, `[HasPermission]` as `x-portolan-permission`, the action's
|
|
191
|
+
`<summary>` or its name as the summary.
|
|
192
|
+
|
|
193
|
+
## Options
|
|
194
|
+
|
|
195
|
+
See `options.schema.json`. `modules`, `api` and `database` default to
|
|
196
|
+
`src/Modules`, `src/API` and `src/Database`; `bus` to `integration-events`;
|
|
197
|
+
`storeKind` to `other`; `repo` and `classification` to nothing.
|
|
198
|
+
|
|
199
|
+
## Tests
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
dotnet run --project plugins/extract-csharp-ddd/test
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`testdata/mymeetings` is a small tree in the layout: three modules, one of
|
|
206
|
+
them event-sourced, a module publishing an integration event two others
|
|
207
|
+
hear, an internal command enqueued from a subscription, a recurring command,
|
|
208
|
+
an EF configuration with owned types and a child table, views, and a
|
|
209
|
+
controller filed under a module the tree does not have. `expected.json`,
|
|
210
|
+
`expected-stores.json` and the `openapi.*.yaml` beside it are what the
|
|
211
|
+
extractor writes for it, and `expected-warnings.txt` what it says on the
|
|
212
|
+
way; `UPDATE_GOLDEN=1` writes them again after a deliberate change, and the
|
|
213
|
+
diff is the review.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"properties": {
|
|
5
|
+
"modules": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "The directory the modules are in, relative to the input root: one directory per module, `Domain`, `Application`, `Infrastructure` and `IntegrationEvents` under it. Each module is a bounded context with one service.",
|
|
8
|
+
"default": "src/Modules"
|
|
9
|
+
},
|
|
10
|
+
"api": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The directory the HTTP host is in, relative to the input root. Controllers under a `Modules/<Module>` directory beneath it are that module's HTTP edge.",
|
|
13
|
+
"default": "src/API"
|
|
14
|
+
},
|
|
15
|
+
"database": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "The directory the database project is in, relative to the input root: `Structure/<schema>/Tables/*.sql` and `Structure/<schema>/Views/*.sql` beneath it, one schema per module.",
|
|
18
|
+
"default": "src/Database"
|
|
19
|
+
},
|
|
20
|
+
"repo": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Where the source lives, as `host/owner/name`. Nothing in a .NET tree says, so the manifest does.",
|
|
23
|
+
"examples": ["github.com/kgrzybek/modular-monolith-with-ddd"]
|
|
24
|
+
},
|
|
25
|
+
"bus": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "The address of the bus integration events cross modules on. The code names the class, `InMemoryEventBus`, and nothing else, so the manifest says.",
|
|
28
|
+
"default": "integration-events"
|
|
29
|
+
},
|
|
30
|
+
"storeKind": {
|
|
31
|
+
"enum": ["postgres", "mysql", "sqlite", "other"],
|
|
32
|
+
"description": "What the schemas live in. SQL Server is `other`.",
|
|
33
|
+
"default": "other"
|
|
34
|
+
},
|
|
35
|
+
"classification": {
|
|
36
|
+
"enum": ["core", "supporting", "generic"],
|
|
37
|
+
"description": "Whether the modules are core to the business, support it, or are generic and could be bought. One value for every module the tree declares."
|
|
38
|
+
},
|
|
39
|
+
"out": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Name of the fragment file, inside the step's output directory.",
|
|
42
|
+
"default": "domain.json"
|
|
43
|
+
},
|
|
44
|
+
"openapiOut": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Name of the inferred OpenAPI 3.1 document, inside the step's output directory, one per module with controllers; `{service}` is replaced with the module's slug.",
|
|
47
|
+
"default": "openapi.{service}.yaml"
|
|
48
|
+
},
|
|
49
|
+
"storesOut": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Name of the store fragment, inside the step's output directory. Written when the database project declares a schema a module maps to.",
|
|
52
|
+
"default": "stores.json"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -39,21 +39,41 @@ resolves them without a type checker.
|
|
|
39
39
|
|
|
40
40
|
## What becomes what
|
|
41
41
|
|
|
42
|
-
**
|
|
43
|
-
application
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
**Models and aggregates.** Every concrete, non-proxy model is visible. When
|
|
43
|
+
an application has an explicitly configured root, a model named after the
|
|
44
|
+
application (`invoices` → `Invoice`), or just one concrete model, it retains
|
|
45
|
+
its existing aggregate representation and stable root-based id.
|
|
46
|
+
|
|
47
|
+
Otherwise the application is emitted as a `kind: "model-group"` entry with
|
|
48
|
+
an empty `root` and an id ending in `models-<application-package>`. All of its
|
|
49
|
+
concrete models appear once, alongside the application's value objects,
|
|
50
|
+
operations and events. This is a source grouping, not a claim that the models
|
|
51
|
+
share a transactional boundary. Its tables link to the model blocks without
|
|
52
|
+
being labelled aggregate roots or children. No root-selection warning is
|
|
53
|
+
emitted, and no configuration is required to browse the models.
|
|
54
|
+
|
|
55
|
+
`options.aggregates` remains an optional refinement: selecting a root replaces
|
|
56
|
+
the source group with an aggregate. A configured root that no longer exists
|
|
57
|
+
produces a warning with concrete candidates, while retaining every model in
|
|
58
|
+
the group. Settings can help repair that configuration and save the choice to
|
|
59
|
+
the matching extraction step. Regenerate to apply it; other options are kept.
|
|
51
60
|
|
|
52
61
|
**Field.** Each model attribute assigned a field, with the type as written:
|
|
53
62
|
`CharField`, `DateTimeField`, and a relation as `ForeignKey[Invoice]`. The doc
|
|
54
63
|
is the field's own `help_text`, which is the one place a Django model already
|
|
55
|
-
writes down what a column means.
|
|
56
|
-
|
|
64
|
+
writes down what a column means. What the field call says a value must
|
|
65
|
+
satisfy is read into the catalog's own words (portolan.0015): `max_length`
|
|
66
|
+
is `max_len`, `unique=True` is `unique`, `choices` is `in` with the stored
|
|
67
|
+
values — read off a `Choices` class, a literal list of pairs or a module-level
|
|
68
|
+
constant, and left out when they come from a call — an `EmailField`,
|
|
69
|
+
`URLField` or `UUIDField` is `format`, a `PositiveIntegerField` is `gte 0`,
|
|
70
|
+
and `MinValueValidator`, `MaxValueValidator`, `MinLengthValidator`,
|
|
71
|
+
`MaxLengthValidator` and `RegexValidator` are the bounds they were given. The
|
|
72
|
+
model is the source of truth for `required`: a field must be given unless
|
|
73
|
+
the model fills it (`default`, `auto_now`, an auto id) or excuses it (`blank`,
|
|
74
|
+
`null`); what a serializer adds is that layer's word and is not read here.
|
|
75
|
+
Value objects are the frozen dataclasses in `values.py`, their fields the
|
|
76
|
+
annotations as written.
|
|
57
77
|
|
|
58
78
|
**Event.** `events.py`, in either of the two ways a Django project says it. A
|
|
59
79
|
dataclass with `name = "billing.InvoiceIssued"` is an event, its payload the
|
|
@@ -162,9 +182,24 @@ observable effects. Traversal is bounded and cycles are cut by symbol, so a
|
|
|
162
182
|
recursive helper cannot make extraction recursive. ORM models from routed
|
|
163
183
|
applications remain visible here even when the application has no unambiguous
|
|
164
184
|
aggregate root. A URLConf may directly mount an arbitrarily named method of a
|
|
165
|
-
plain class, such as `Planet.fetch
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
plain class, such as `Planet.fetch`, or a plain function; that is still an
|
|
186
|
+
HTTP flow root, and its verb is read off what the code declares, in the order
|
|
187
|
+
a reviewer would trust it: a decorator on the handler (`@action(methods=…)`,
|
|
188
|
+
`@api_view`, `@require_http_methods([…])`, `@require_GET`, `@require_POST`,
|
|
189
|
+
`@require_safe`, also through `method_decorator`), the same decorators on the
|
|
190
|
+
class (`@method_decorator(…, name="dispatch")`), the class's
|
|
191
|
+
`http_method_names` (less `HEAD`, `OPTIONS` and `TRACE`, which every route
|
|
192
|
+
answers), a branch on `request.method` in the handler body, and last a project
|
|
193
|
+
wrapper — a decorator or a function the handler hands `request` to — whose own
|
|
194
|
+
body does one of those, followed a bounded number of levels deep. The first
|
|
195
|
+
tier that speaks decides; a declaration listing several verbs makes one
|
|
196
|
+
endpoint per verb, `planet_status` and `planet_status_patch`. When none of
|
|
197
|
+
them speaks, the verb is not guessed: the route stays in `provides` with an
|
|
198
|
+
empty `http.method`, the inferred OpenAPI document keeps the path as an item
|
|
199
|
+
with no operations and `x-portolan-verb: unknown`, the flow is retained, and
|
|
200
|
+
a diagnostic names the route. The merge never matches an outbound call
|
|
201
|
+
against a route whose verb is unknown, so the link waits for a declaration
|
|
202
|
+
rather than being confirmed by the path alone. An
|
|
168
203
|
inherited DRF generic action has no local handler body, so
|
|
169
204
|
its framework behaviour is reconstructed instead: list/retrieve read the
|
|
170
205
|
model, create/update validate through the selected serializer and persist it,
|
|
@@ -267,8 +302,7 @@ The primary store is inferred from `DATABASES["default"]` when that setting is
|
|
|
267
302
|
statically readable; `store` remains the override for settings assembled at
|
|
268
303
|
runtime or for a catalog that needs a different stable slug. Every concrete,
|
|
269
304
|
non-proxy model becomes a table even when its application has several possible
|
|
270
|
-
aggregate roots:
|
|
271
|
-
schema. Fields from abstract model bases are copied into those tables
|
|
305
|
+
aggregate roots: both the model group and persistence schema stay visible. Fields from abstract model bases are copied into those tables
|
|
272
306
|
regardless of which model module sorts first. `DEFAULT_AUTO_FIELD` supplies
|
|
273
307
|
implicit primary-key types. PostgreSQL `ArrayField` nesting and
|
|
274
308
|
`MultiSelectField` storage are rendered as their database types; `to_field`
|
|
@@ -323,9 +357,8 @@ Everything else means what it means for `extract-ts`.
|
|
|
323
357
|
|
|
324
358
|
These cases do not become facts in the fragment:
|
|
325
359
|
|
|
326
|
-
- a domain aggregate for an application with no models
|
|
327
|
-
|
|
328
|
-
still included);
|
|
360
|
+
- a domain aggregate for an application with no models (its statically
|
|
361
|
+
resolvable HTTP routes are still included);
|
|
329
362
|
- a model that declares no fields;
|
|
330
363
|
- an events module holding a class with no wire name, and a signal with no
|
|
331
364
|
payload;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""The `Choices` classes a model keeps, read as the values the column holds.
|
|
2
|
+
|
|
3
|
+
`class Status(models.TextChoices): DRAFT = "draft", "Draft"` says two things:
|
|
4
|
+
the states a lifecycle moves between, and the values a `status` field may
|
|
5
|
+
take. Both readers want the same table, member name to stored value, and
|
|
6
|
+
neither wants the label.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from typing import Dict
|
|
13
|
+
|
|
14
|
+
from source import assigned, bases, inner_class
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_choices(node: ast.AST) -> bool:
|
|
18
|
+
return isinstance(node, ast.ClassDef) and any(b.split(".")[-1].endswith("Choices") for b in bases(node))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def choices_of(model, name: str) -> Dict[str, str]:
|
|
22
|
+
"""A Choices class, as member name to the value stored in the column. Looked
|
|
23
|
+
up inside the model first, then among the module's own classes."""
|
|
24
|
+
node = inner_class(model.node, name)
|
|
25
|
+
if node is None:
|
|
26
|
+
for other in model.module.classes():
|
|
27
|
+
if other.name == name:
|
|
28
|
+
node = other
|
|
29
|
+
break
|
|
30
|
+
if node is None or not is_choices(node):
|
|
31
|
+
return {}
|
|
32
|
+
out = {}
|
|
33
|
+
for member, value, _ in assigned(node):
|
|
34
|
+
if isinstance(value, ast.Constant) and not isinstance(value.value, bool):
|
|
35
|
+
out[member] = str(value.value)
|
|
36
|
+
elif isinstance(value, ast.Tuple) and value.elts and isinstance(value.elts[0], ast.Constant):
|
|
37
|
+
out[member] = str(value.elts[0].value)
|
|
38
|
+
return out
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def choice_tables(model) -> Dict[str, Dict[str, str]]:
|
|
42
|
+
"""Every Choices class in reach of the model, by class name."""
|
|
43
|
+
out = {}
|
|
44
|
+
for node in list(model.node.body) + list(model.module.tree.body):
|
|
45
|
+
if is_choices(node):
|
|
46
|
+
out[node.name] = choices_of(model, node.name)
|
|
47
|
+
return out
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
"""`models.py` read as the model.
|
|
2
2
|
|
|
3
|
-
A Django application
|
|
4
|
-
|
|
5
|
-
dataclasses in `values.py` are its value objects.
|
|
6
|
-
|
|
7
|
-
in `extract-ts`.
|
|
3
|
+
A Django application groups its models. When a root is known, the group is
|
|
4
|
+
an aggregate; otherwise it remains a model-group with no asserted boundary.
|
|
5
|
+
Frozen dataclasses in `values.py` are its value objects. Ambiguity never hides
|
|
6
|
+
the concrete models or requires configuration to browse them.
|
|
8
7
|
"""
|
|
9
8
|
|
|
10
9
|
from __future__ import annotations
|
|
11
10
|
|
|
12
11
|
import ast
|
|
12
|
+
import json
|
|
13
13
|
from dataclasses import dataclass, field as dc_field
|
|
14
14
|
from typing import Dict, List, Optional
|
|
15
15
|
|
|
16
16
|
import catalog
|
|
17
17
|
from apps import App
|
|
18
18
|
from ids import aggregate_id, block_id, pascal, singular, slug
|
|
19
|
+
from rules import rules_of
|
|
19
20
|
from source import (
|
|
20
21
|
Module,
|
|
21
22
|
Project,
|
|
@@ -90,7 +91,7 @@ class Aggregate:
|
|
|
90
91
|
"""One application, and the catalog object being built from it."""
|
|
91
92
|
|
|
92
93
|
app: App
|
|
93
|
-
root: ModelDef
|
|
94
|
+
root: Optional[ModelDef]
|
|
94
95
|
models: List[ModelDef]
|
|
95
96
|
aggregate: Dict[str, object]
|
|
96
97
|
|
|
@@ -257,7 +258,7 @@ def root_of(app: App, models: List[ModelDef], named: Dict[str, str], b) -> Optio
|
|
|
257
258
|
for m in concrete:
|
|
258
259
|
if m.name == want:
|
|
259
260
|
return m
|
|
260
|
-
b.warn(app.rel, "aggregates names %s for %s, and no model there is called that" % (want, app.label))
|
|
261
|
+
b.warn(app.rel, "aggregates names %s for %s, and no model there is called that%s" % (want, app.label, aggregate_candidates(app, concrete)))
|
|
261
262
|
return None
|
|
262
263
|
for candidate in (pascal(singular(app.label)), pascal(app.label)):
|
|
263
264
|
for m in concrete:
|
|
@@ -268,13 +269,20 @@ def root_of(app: App, models: List[ModelDef], named: Dict[str, str], b) -> Optio
|
|
|
268
269
|
if not concrete:
|
|
269
270
|
b.warn(app.rel, "no models in this application: nothing here to be an aggregate")
|
|
270
271
|
return None
|
|
271
|
-
b.warn(
|
|
272
|
-
app.rel,
|
|
273
|
-
"no model called %s, and %d models to choose from: name the root in the aggregates option" % (pascal(singular(app.label)), len(concrete)),
|
|
274
|
-
)
|
|
275
272
|
return None
|
|
276
273
|
|
|
277
274
|
|
|
275
|
+
def aggregate_candidates(app: App, models: List[ModelDef]) -> str:
|
|
276
|
+
"""Keep candidate evidence in the warning-only plugin protocol."""
|
|
277
|
+
return "; aggregate candidates: " + json.dumps({
|
|
278
|
+
"app": app.dotted,
|
|
279
|
+
"models": [
|
|
280
|
+
{"name": model.name, "path": model.module.rel, "line": model.node.lineno}
|
|
281
|
+
for model in sorted(models, key=lambda model: (model.name, model.module.rel))
|
|
282
|
+
],
|
|
283
|
+
}, ensure_ascii=True)
|
|
284
|
+
|
|
285
|
+
|
|
278
286
|
def read_aggregates(
|
|
279
287
|
project: Project,
|
|
280
288
|
applications: List[App],
|
|
@@ -289,14 +297,18 @@ def read_aggregates(
|
|
|
289
297
|
if models is None:
|
|
290
298
|
models = read_models(app)
|
|
291
299
|
root = root_of(app, models, named, b)
|
|
292
|
-
if root is None:
|
|
293
|
-
continue
|
|
294
300
|
concrete = [m for m in models if m.concrete]
|
|
295
|
-
|
|
296
|
-
|
|
301
|
+
if not concrete:
|
|
302
|
+
continue
|
|
303
|
+
ordered = [root] + [m for m in concrete if m is not root] if root else concrete
|
|
304
|
+
# An application is a source grouping, not proof of a transactional
|
|
305
|
+
# boundary. Keep every model visible without inventing such a boundary.
|
|
306
|
+
agg_slug = slug(root.name) if root else "models-" + slug(app.dotted.replace(".", "-"))
|
|
297
307
|
agg_id = aggregate_id(svc_id, agg_slug)
|
|
298
|
-
readme = app.readme or (ast.get_docstring(root.node, clean=True) or "").strip()
|
|
299
|
-
obj = catalog.aggregate(agg_id, agg_slug, root.name, readme, root.name)
|
|
308
|
+
readme = app.readme or ((ast.get_docstring(root.node, clean=True) or "").strip() if root else "")
|
|
309
|
+
obj = catalog.aggregate(agg_id, agg_slug, root.name if root else app.dotted, readme, root.name if root else "")
|
|
310
|
+
if root is None:
|
|
311
|
+
obj["kind"] = "model-group"
|
|
300
312
|
for model in ordered:
|
|
301
313
|
obj["entities"].append(
|
|
302
314
|
catalog.block(
|
|
@@ -304,7 +316,7 @@ def read_aggregates(
|
|
|
304
316
|
slug(model.name),
|
|
305
317
|
model.name,
|
|
306
318
|
doc(model.node),
|
|
307
|
-
[catalog.field(f.name, f.type(), f.help()) for f in model.fields],
|
|
319
|
+
[catalog.field(f.name, f.type(), f.help(), **rules_of(f, model)) for f in model.fields],
|
|
308
320
|
)
|
|
309
321
|
)
|
|
310
322
|
for module, node in value_objects(app):
|