@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
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
// Where a call and the interface it names disagree.
|
|
2
|
-
//
|
|
3
|
-
// docs/adr/org.0001.md promises this: producers publish their schema, consumers
|
|
4
|
-
// keep a narrowed copy, and "a field, method or enum value that differs between
|
|
5
|
-
// the two is reported against the consuming service". The extractor retains
|
|
6
|
-
// those copies on the consumer; this reader compares them after every source
|
|
7
|
-
// has met in the merged catalog.
|
|
8
|
-
//
|
|
9
|
-
// It is deliberately NOT done in the merge. src/merge.ts is documented as
|
|
10
|
-
// union-by-id plus first-non-empty and nothing else, and teaching it to compare
|
|
11
|
-
// shapes would put a semantic judgement in the one place that has none. The
|
|
12
|
-
// comparison belongs here, over the merged catalog, on the page where the rest
|
|
13
|
-
// of that judgement already lives.
|
|
14
|
-
//
|
|
15
|
-
// This is also NOT the same finding as derive.ts's `rpc` problem. That one is
|
|
16
|
-
// about the PEER: a call whose other end is nobody the catalog knows. This one
|
|
17
|
-
// is about a call whose peer is known and whose METHOD is not - the copy is
|
|
18
|
-
// stale, or the producer removed something, and the two are different repairs.
|
|
19
|
-
|
|
20
|
-
import type {
|
|
21
|
-
Catalog,
|
|
22
|
-
CatalogIndex,
|
|
23
|
-
RpcEnum,
|
|
24
|
-
RpcMessage,
|
|
25
|
-
RpcMethod,
|
|
26
|
-
RpcService,
|
|
27
|
-
Service,
|
|
28
|
-
} from "../catalog";
|
|
29
|
-
import type { Problem } from "./derive";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Every missing method and every retained consumer descriptor that disagrees
|
|
33
|
-
* with the provider's method, field, or enum value.
|
|
34
|
-
*
|
|
35
|
-
* An unresolved call is skipped: it already has a problem of its own from
|
|
36
|
-
* `problems()`, and reporting the same edge twice under two headings would make
|
|
37
|
-
* the page look worse than the estate is.
|
|
38
|
-
*/
|
|
39
|
-
export function protoProblems(
|
|
40
|
-
catalog: Catalog,
|
|
41
|
-
index: CatalogIndex,
|
|
42
|
-
): Problem[] {
|
|
43
|
-
const out: Problem[] = [];
|
|
44
|
-
|
|
45
|
-
for (const context of catalog.contexts) {
|
|
46
|
-
for (const service of context.services) {
|
|
47
|
-
for (const call of service.consumes) {
|
|
48
|
-
if (call.status === "unresolved") continue;
|
|
49
|
-
|
|
50
|
-
// The peer has to be a service in the catalog before the method is
|
|
51
|
-
// worth asking about. A call to something outside the estate is a fact,
|
|
52
|
-
// not a defect.
|
|
53
|
-
if (!index.serviceById.has(call.peer)) continue;
|
|
54
|
-
|
|
55
|
-
if (index.rpcProviderByMethod.has(call.id)) continue;
|
|
56
|
-
|
|
57
|
-
out.push({
|
|
58
|
-
kind: "proto-missing",
|
|
59
|
-
// An error, not a staleness warning: the peer is in the catalog and
|
|
60
|
-
// does not answer on this. Either the copy is behind or the method is
|
|
61
|
-
// gone, and both are a call that will fail.
|
|
62
|
-
severity: "error",
|
|
63
|
-
context: context.id,
|
|
64
|
-
service: service.id,
|
|
65
|
-
id: call.id,
|
|
66
|
-
peer: call.peer,
|
|
67
|
-
note: call.note,
|
|
68
|
-
source: call.source,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const copy of service.copies ?? []) {
|
|
73
|
-
const drift = driftAgainstProvider(service, copy, index);
|
|
74
|
-
if (drift) out.push(drift);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Compare one consumer copy only when one of its calls resolves to a provider. */
|
|
83
|
-
function driftAgainstProvider(
|
|
84
|
-
consumer: Service,
|
|
85
|
-
copy: RpcService,
|
|
86
|
-
index: CatalogIndex,
|
|
87
|
-
): Problem | undefined {
|
|
88
|
-
const calls = new Map(
|
|
89
|
-
consumer.consumes
|
|
90
|
-
.filter((call) => call.id.startsWith(`${copy.id}/`))
|
|
91
|
-
.map((call) => [call.id, call]),
|
|
92
|
-
);
|
|
93
|
-
const resolved = copy.methods
|
|
94
|
-
.map((method) => calls.get(`${copy.id}/${method.name}`))
|
|
95
|
-
.find(
|
|
96
|
-
(call) =>
|
|
97
|
-
call?.status !== "unresolved" && index.serviceById.has(call?.peer ?? ""),
|
|
98
|
-
);
|
|
99
|
-
if (!resolved) return undefined;
|
|
100
|
-
|
|
101
|
-
const providerService = index.serviceById.get(resolved.peer);
|
|
102
|
-
const provider = providerService?.provides.find(
|
|
103
|
-
(candidate) => candidate.id === copy.id,
|
|
104
|
-
);
|
|
105
|
-
if (!providerService || !provider) return undefined;
|
|
106
|
-
|
|
107
|
-
const differences = compareInterfaces(copy, provider);
|
|
108
|
-
if (differences.length === 0) return undefined;
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
kind: "proto-drift",
|
|
112
|
-
severity: "warning",
|
|
113
|
-
context: index.serviceContext.get(consumer.id)?.id ?? "",
|
|
114
|
-
service: consumer.id,
|
|
115
|
-
id: copy.id,
|
|
116
|
-
peer: providerService.id,
|
|
117
|
-
note: `Vendored copy differs from ${providerService.id}: ${differences.join("; ")}.`,
|
|
118
|
-
source: copy.source,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function compareInterfaces(copy: RpcService, provider: RpcService): string[] {
|
|
123
|
-
const out: string[] = [];
|
|
124
|
-
const methods = new Map(provider.methods.map((method) => [method.name, method]));
|
|
125
|
-
|
|
126
|
-
for (const method of copy.methods) {
|
|
127
|
-
const actual = methods.get(method.name);
|
|
128
|
-
// A missing method already has the stronger proto-missing error.
|
|
129
|
-
if (!actual) continue;
|
|
130
|
-
compareMethod(method, actual, out);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const messages = new Map(
|
|
134
|
-
(provider.messages ?? []).map((message) => [message.name, message]),
|
|
135
|
-
);
|
|
136
|
-
for (const message of copy.messages ?? []) {
|
|
137
|
-
compareMessage(message, messages.get(message.name), out);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const enums = new Map(
|
|
141
|
-
(provider.enums ?? []).map((item) => [item.name, item]),
|
|
142
|
-
);
|
|
143
|
-
for (const item of copy.enums ?? []) {
|
|
144
|
-
compareEnum(item, enums.get(item.name), out);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return out;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function compareMethod(
|
|
151
|
-
copy: RpcMethod,
|
|
152
|
-
provider: RpcMethod,
|
|
153
|
-
out: string[],
|
|
154
|
-
): void {
|
|
155
|
-
for (const side of ["request", "response"] as const) {
|
|
156
|
-
if (copy[side] !== provider[side]) {
|
|
157
|
-
out.push(
|
|
158
|
-
`${copy.name} ${side} is ${copy[side] || "unnamed"}, provider has ${provider[side] || "unnamed"}`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (copy.streaming !== provider.streaming) {
|
|
163
|
-
out.push(
|
|
164
|
-
`${copy.name} streaming is ${copy.streaming ?? "unary"}, provider has ${provider.streaming ?? "unary"}`,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* A narrowed copy may omit provider fields it does not read. Every field it
|
|
171
|
-
* does carry must still have the provider's type and protobuf number.
|
|
172
|
-
*/
|
|
173
|
-
function compareMessage(
|
|
174
|
-
copy: RpcMessage,
|
|
175
|
-
provider: RpcMessage | undefined,
|
|
176
|
-
out: string[],
|
|
177
|
-
): void {
|
|
178
|
-
if (!provider) {
|
|
179
|
-
out.push(`message ${copy.name} is absent from the provider`);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const fields = new Map(provider.fields.map((field) => [field.name, field]));
|
|
184
|
-
for (const field of copy.fields) {
|
|
185
|
-
const actual = fields.get(field.name);
|
|
186
|
-
if (!actual) {
|
|
187
|
-
out.push(`${copy.name}.${field.name} is absent from the provider`);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (field.type !== actual.type) {
|
|
191
|
-
out.push(
|
|
192
|
-
`${copy.name}.${field.name} is ${field.type}, provider has ${actual.type}`,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
if (
|
|
196
|
-
field.number !== undefined &&
|
|
197
|
-
actual.number !== undefined &&
|
|
198
|
-
field.number !== actual.number
|
|
199
|
-
) {
|
|
200
|
-
out.push(
|
|
201
|
-
`${copy.name}.${field.name} is field ${field.number}, provider has ${actual.number}`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Enum names and numbers are both wire claims, so the sets must match whole. */
|
|
208
|
-
function compareEnum(
|
|
209
|
-
copy: RpcEnum,
|
|
210
|
-
provider: RpcEnum | undefined,
|
|
211
|
-
out: string[],
|
|
212
|
-
): void {
|
|
213
|
-
if (!provider) {
|
|
214
|
-
out.push(`enum ${copy.name} is absent from the provider`);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const copyByName = new Map(copy.values.map((value) => [value.name, value]));
|
|
219
|
-
const providerByName = new Map(
|
|
220
|
-
provider.values.map((value) => [value.name, value]),
|
|
221
|
-
);
|
|
222
|
-
for (const value of copy.values) {
|
|
223
|
-
const actual = providerByName.get(value.name);
|
|
224
|
-
if (!actual) {
|
|
225
|
-
out.push(`${copy.name}.${value.name} is absent from the provider`);
|
|
226
|
-
} else if (value.number !== actual.number) {
|
|
227
|
-
out.push(
|
|
228
|
-
`${copy.name}.${value.name} is ${value.number}, provider has ${actual.number}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
for (const value of provider.values) {
|
|
233
|
-
if (!copyByName.has(value.name)) {
|
|
234
|
-
out.push(`${copy.name}.${value.name} is missing from the copy`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
package/src/lib/wire-problems.ts
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
// Where two services meet on one channel, and where a service's document and
|
|
2
|
-
// its code do not tell the same story about the bus.
|
|
3
|
-
//
|
|
4
|
-
// A channel belongs to the service that publishes on it, the way a store
|
|
5
|
-
// belongs to the service that writes it: a subscriber to `shop.cart.basket`
|
|
6
|
-
// is told, by the name, whose events it is reading and whose schema to vendor.
|
|
7
|
-
// A second publisher on the same channel breaks that promise silently - the
|
|
8
|
-
// subscriber dispatches on the event's name and never notices the message
|
|
9
|
-
// came from somewhere else, until one arrives that it cannot parse.
|
|
10
|
-
//
|
|
11
|
-
// Two sources say what a service puts on the bus, and they are read out of
|
|
12
|
-
// different files by different extractors. The domain says an aggregate raises
|
|
13
|
-
// an event and how it leaves, in `wire`. An AsyncAPI document says the service
|
|
14
|
-
// declares a channel and what travels on it, in `channels`. Neither is the
|
|
15
|
-
// truth on its own - a document is a promise and a wire is what the code does -
|
|
16
|
-
// and where they disagree is a fact worth a row, because one of them is stale.
|
|
17
|
-
//
|
|
18
|
-
// This is the messaging half of `shared-store`. It is done here, over the
|
|
19
|
-
// merged catalog, for the same reason the store rule is: each extractor sees
|
|
20
|
-
// one repository and cannot know what another declares.
|
|
21
|
-
|
|
22
|
-
import type { Catalog, CatalogIndex, ChannelMessage, Event, Service } from "../catalog";
|
|
23
|
-
import type { Problem } from "./derive";
|
|
24
|
-
|
|
25
|
-
interface Publisher {
|
|
26
|
-
service: Service;
|
|
27
|
-
context: string;
|
|
28
|
-
/** The events of this service whose wire names the channel. */
|
|
29
|
-
events: Event[];
|
|
30
|
-
/** Whether the service's own document declares it sends here. */
|
|
31
|
-
declared: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function wireProblems(
|
|
35
|
-
catalog: Catalog,
|
|
36
|
-
index: CatalogIndex,
|
|
37
|
-
): Problem[] {
|
|
38
|
-
return [
|
|
39
|
-
...sharedChannels(catalog),
|
|
40
|
-
...documentAgainstCode(catalog),
|
|
41
|
-
...messageEncodingMismatches(catalog),
|
|
42
|
-
...unresolvedSubscriptions(catalog, index),
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface MessageEndpoint {
|
|
47
|
-
context: string;
|
|
48
|
-
service: Service;
|
|
49
|
-
address: string;
|
|
50
|
-
source?: string;
|
|
51
|
-
message: ChannelMessage;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Producer and subscriber have both named a format, and those formats differ. */
|
|
55
|
-
function messageEncodingMismatches(catalog: Catalog): Problem[] {
|
|
56
|
-
const sends = new Map<string, MessageEndpoint[]>();
|
|
57
|
-
const receives: MessageEndpoint[] = [];
|
|
58
|
-
for (const context of catalog.contexts) {
|
|
59
|
-
for (const service of context.services) {
|
|
60
|
-
for (const channel of service.channels ?? []) {
|
|
61
|
-
for (const message of channel.messages) {
|
|
62
|
-
const endpoint = { context: context.id, service, address: channel.address, source: channel.source, message };
|
|
63
|
-
const key = `${channel.address}\u0000${message.name}`;
|
|
64
|
-
if (message.direction === "send") {
|
|
65
|
-
const publishers = sends.get(key) ?? [];
|
|
66
|
-
publishers.push(endpoint);
|
|
67
|
-
sends.set(key, publishers);
|
|
68
|
-
} else {
|
|
69
|
-
receives.push(endpoint);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const out: Problem[] = [];
|
|
77
|
-
for (const receiver of receives) {
|
|
78
|
-
const expected = wireFormat(receiver.message);
|
|
79
|
-
if (!expected) continue;
|
|
80
|
-
const key = `${receiver.address}\u0000${receiver.message.name}`;
|
|
81
|
-
for (const publisher of sends.get(key) ?? []) {
|
|
82
|
-
const actual = wireFormat(publisher.message);
|
|
83
|
-
if (!actual || actual === expected) continue;
|
|
84
|
-
out.push({
|
|
85
|
-
kind: "message-encoding",
|
|
86
|
-
severity: "error",
|
|
87
|
-
context: receiver.context,
|
|
88
|
-
service: receiver.service.id,
|
|
89
|
-
id: receiver.service.id,
|
|
90
|
-
peer: publisher.service.id,
|
|
91
|
-
note: `${receiver.service.id} expects ${receiver.message.name} on ${receiver.address} as ${expected}, but ${publisher.service.id} sends it as ${actual}.`,
|
|
92
|
-
source: receiver.source,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function wireFormat(message: ChannelMessage): string {
|
|
100
|
-
if (message.encoding) return message.encoding.trim().toLowerCase();
|
|
101
|
-
const contentType = message.contentType?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
102
|
-
if (contentType.includes("msgpack") || contentType.includes("messagepack")) return "msgpack";
|
|
103
|
-
return contentType;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Every service that publishes on a channel another service publishes on
|
|
108
|
-
* too: one row per service per such channel, pointing at the other side.
|
|
109
|
-
*
|
|
110
|
-
* Publishing is either of the two claims - an event whose wire names the
|
|
111
|
-
* channel, or a document declaring a send on it - because both are the service
|
|
112
|
-
* saying it owns the channel, and a document that says so while the code has
|
|
113
|
-
* not caught up is the same collision one release earlier.
|
|
114
|
-
*
|
|
115
|
-
* A service with several aggregates on one channel is not this: the channel
|
|
116
|
-
* is still one service's. An event without a wire is skipped, because a
|
|
117
|
-
* channel nobody named cannot be shared.
|
|
118
|
-
*/
|
|
119
|
-
function sharedChannels(catalog: Catalog): Problem[] {
|
|
120
|
-
const byChannel = new Map<string, Map<string, Publisher>>();
|
|
121
|
-
|
|
122
|
-
const publisher = (
|
|
123
|
-
channel: string,
|
|
124
|
-
service: Service,
|
|
125
|
-
context: string,
|
|
126
|
-
): Publisher => {
|
|
127
|
-
const publishers = byChannel.get(channel) ?? new Map<string, Publisher>();
|
|
128
|
-
byChannel.set(channel, publishers);
|
|
129
|
-
const mine = publishers.get(service.id) ?? {
|
|
130
|
-
service,
|
|
131
|
-
context,
|
|
132
|
-
events: [],
|
|
133
|
-
declared: false,
|
|
134
|
-
};
|
|
135
|
-
publishers.set(service.id, mine);
|
|
136
|
-
|
|
137
|
-
return mine;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
for (const context of catalog.contexts) {
|
|
141
|
-
for (const service of context.services) {
|
|
142
|
-
for (const aggregate of service.aggregates) {
|
|
143
|
-
for (const event of aggregate.events) {
|
|
144
|
-
const channel = event.wire?.channel;
|
|
145
|
-
if (!channel) continue;
|
|
146
|
-
publisher(channel, service, context.id).events.push(event);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
for (const channel of service.channels ?? []) {
|
|
151
|
-
if (channel.kind && channel.kind !== "event") continue;
|
|
152
|
-
if (!sends(channel.messages)) continue;
|
|
153
|
-
publisher(channel.address, service, context.id).declared = true;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const out: Problem[] = [];
|
|
159
|
-
for (const [channel, publishers] of byChannel) {
|
|
160
|
-
if (publishers.size < 2) continue;
|
|
161
|
-
for (const mine of publishers.values()) {
|
|
162
|
-
for (const theirs of publishers.values()) {
|
|
163
|
-
if (theirs === mine) continue;
|
|
164
|
-
const [first] = mine.events;
|
|
165
|
-
out.push({
|
|
166
|
-
kind: "shared-channel",
|
|
167
|
-
// An error, like a second writer in a database: a subscriber vendors
|
|
168
|
-
// one publisher's schema, and a message from the other is one it
|
|
169
|
-
// will fail on, not one it will merely misfile.
|
|
170
|
-
severity: "error",
|
|
171
|
-
context: mine.context,
|
|
172
|
-
service: mine.service.id,
|
|
173
|
-
// The near end is one event when this side has one, so the row leads
|
|
174
|
-
// to a page; a side known only from its document leads to the
|
|
175
|
-
// service, which is the only page there is.
|
|
176
|
-
id: first?.id ?? mine.service.id,
|
|
177
|
-
peer: theirs.service.id,
|
|
178
|
-
note: `${mine.service.id} publishes on ${channel}, and so does ${theirs.service.id}: ${claim(theirs, channel)}. A subscriber reads both without being told.`,
|
|
179
|
-
source: first?.versions[0]?.source ?? sourceOf(mine.service, channel),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return out;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** What a publisher is putting on a channel, as the catalog knows it. */
|
|
189
|
-
function claim(publisher: Publisher, channel: string): string {
|
|
190
|
-
if (publisher.events.length > 0) {
|
|
191
|
-
return publisher.events.map((e) => e.name).join(", ");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const declared = publisher.service.channels?.find(
|
|
195
|
-
(c) => c.address === channel,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
return (
|
|
199
|
-
declared?.messages
|
|
200
|
-
.filter((m) => m.direction === "send")
|
|
201
|
-
.map((m) => m.name)
|
|
202
|
-
.join(", ") ?? "nothing the catalog can name"
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Where a service's document and its own code disagree about the bus.
|
|
208
|
-
*
|
|
209
|
-
* Both directions are a warning rather than an error, and for the same reason:
|
|
210
|
-
* neither claim is wrong on its own. A channel the code publishes on and the
|
|
211
|
-
* document does not declare is a document that has fallen behind - or an event
|
|
212
|
-
* going out where nobody agreed it would. A channel the document declares and
|
|
213
|
-
* no event names is a promise nothing keeps yet.
|
|
214
|
-
*
|
|
215
|
-
* A service with no document says nothing here. Silence is not a disagreement,
|
|
216
|
-
* and a rule that fired on every service without an AsyncAPI file would be a
|
|
217
|
-
* page of rows about work nobody has started.
|
|
218
|
-
*/
|
|
219
|
-
function documentAgainstCode(catalog: Catalog): Problem[] {
|
|
220
|
-
const out: Problem[] = [];
|
|
221
|
-
|
|
222
|
-
for (const context of catalog.contexts) {
|
|
223
|
-
for (const service of context.services) {
|
|
224
|
-
const channels = service.channels ?? [];
|
|
225
|
-
const declared = new Set(
|
|
226
|
-
channels
|
|
227
|
-
.filter((c) => (!c.kind || c.kind === "event") && sends(c.messages))
|
|
228
|
-
.map((c) => c.address),
|
|
229
|
-
);
|
|
230
|
-
// Channels that only receive say nothing about what the service
|
|
231
|
-
// sends: a subscription read out of the code is not a document of
|
|
232
|
-
// the service's publications, and holding the events against it
|
|
233
|
-
// would report every one of them.
|
|
234
|
-
if (declared.size === 0) continue;
|
|
235
|
-
const published = new Map<string, Event>();
|
|
236
|
-
|
|
237
|
-
for (const aggregate of service.aggregates) {
|
|
238
|
-
for (const event of aggregate.events) {
|
|
239
|
-
const channel = event.wire?.channel;
|
|
240
|
-
if (!channel) continue;
|
|
241
|
-
if (!published.has(channel)) published.set(channel, event);
|
|
242
|
-
if (declared.has(channel)) continue;
|
|
243
|
-
|
|
244
|
-
out.push({
|
|
245
|
-
kind: "channel-undeclared",
|
|
246
|
-
severity: "warning",
|
|
247
|
-
context: context.id,
|
|
248
|
-
service: service.id,
|
|
249
|
-
id: event.id,
|
|
250
|
-
peer: channel,
|
|
251
|
-
note: `${event.name} goes out on ${channel}, which ${service.id} does not declare it sends on. A subscriber reading the document does not know this message exists.`,
|
|
252
|
-
source: event.versions[0]?.source,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
for (const channel of channels) {
|
|
258
|
-
if (channel.kind && channel.kind !== "event") continue;
|
|
259
|
-
if (!sends(channel.messages)) continue;
|
|
260
|
-
if (published.has(channel.address)) continue;
|
|
261
|
-
|
|
262
|
-
out.push({
|
|
263
|
-
kind: "channel-unpublished",
|
|
264
|
-
severity: "warning",
|
|
265
|
-
context: context.id,
|
|
266
|
-
service: service.id,
|
|
267
|
-
id: service.id,
|
|
268
|
-
peer: channel.address,
|
|
269
|
-
note: `${service.id} declares it sends on ${channel.address}, and no event of it names that channel: ${channel.messages
|
|
270
|
-
.filter((m) => m.direction === "send")
|
|
271
|
-
.map((m) => m.name)
|
|
272
|
-
.join(", ")}.`,
|
|
273
|
-
source: channel.source,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return out;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* A message a service listens for that nothing in the estate publishes.
|
|
284
|
-
*
|
|
285
|
-
* This is the only edge in the catalog that runs the other way. Everywhere else
|
|
286
|
-
* the publisher names its consumers and a name that resolves to nobody is the
|
|
287
|
-
* problem; here the subscriber names a message, and the estate is searched for
|
|
288
|
-
* whoever puts it on the wire. A subscription that resolves is how two
|
|
289
|
-
* repositories that never mention each other are found to be joined.
|
|
290
|
-
*/
|
|
291
|
-
function unresolvedSubscriptions(
|
|
292
|
-
catalog: Catalog,
|
|
293
|
-
index: CatalogIndex,
|
|
294
|
-
): Problem[] {
|
|
295
|
-
// Every name anything in the estate puts on the wire. The index knows the
|
|
296
|
-
// events; a document declaring a send is the other half, and it counts for
|
|
297
|
-
// the same reason it counts as publishing above - the code may simply not
|
|
298
|
-
// have caught up with it yet.
|
|
299
|
-
const published = new Set(index.eventByWireName.keys());
|
|
300
|
-
for (const context of catalog.contexts) {
|
|
301
|
-
for (const service of context.services) {
|
|
302
|
-
for (const channel of service.channels ?? []) {
|
|
303
|
-
for (const message of channel.messages) {
|
|
304
|
-
if (message.direction === "send") published.add(message.name);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const out: Problem[] = [];
|
|
311
|
-
for (const context of catalog.contexts) {
|
|
312
|
-
for (const service of context.services) {
|
|
313
|
-
for (const channel of service.channels ?? []) {
|
|
314
|
-
for (const message of channel.messages) {
|
|
315
|
-
if (message.direction !== "receive") continue;
|
|
316
|
-
if (published.has(message.name)) continue;
|
|
317
|
-
|
|
318
|
-
out.push({
|
|
319
|
-
kind: "subscription-unresolved",
|
|
320
|
-
severity: "warning",
|
|
321
|
-
context: context.id,
|
|
322
|
-
service: service.id,
|
|
323
|
-
id: service.id,
|
|
324
|
-
peer: message.name,
|
|
325
|
-
note: `${service.id} listens on ${channel.address} for ${message.name}, and nothing in the catalog publishes it. Either the publisher is outside the estate, or the name has drifted.`,
|
|
326
|
-
source: channel.source,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return out;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function sends(messages: { direction: string }[]): boolean {
|
|
337
|
-
return messages.some((m) => m.direction === "send");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function sourceOf(service: Service, channel: string): string | undefined {
|
|
341
|
-
return service.channels?.find((c) => c.address === channel)?.source;
|
|
342
|
-
}
|