@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,141 @@
|
|
|
1
|
+
// Package plugin is the wire contract between the host and a generator.
|
|
2
|
+
//
|
|
3
|
+
// One JSON message in, one JSON message out, and the same shape whichever
|
|
4
|
+
// direction the generator runs in: an extractor reads source and answers with
|
|
5
|
+
// a catalog fragment, a renderer reads the catalog and answers with pages.
|
|
6
|
+
// Both name files; neither writes them. The host does that, which is what lets
|
|
7
|
+
// a generator run as a wasm module with nothing preopened at all.
|
|
8
|
+
package plugin
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"encoding/json"
|
|
12
|
+
"fmt"
|
|
13
|
+
"os"
|
|
14
|
+
|
|
15
|
+
"github.com/shortlink-org/portolan/catalog"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const Version = "0.1.0"
|
|
19
|
+
|
|
20
|
+
// Request is everything a plugin is allowed to know. There is no ambient state
|
|
21
|
+
// to read, no environment, and - for a wasm plugin - no filesystem either.
|
|
22
|
+
type Request struct {
|
|
23
|
+
PortolanVersion string `json:"portolanVersion"`
|
|
24
|
+
|
|
25
|
+
// Kind is what the host is asking for. Empty - the only thing anyone sent
|
|
26
|
+
// before this field existed - means do the work. KindDescribe means answer
|
|
27
|
+
// with a Descriptor and read nothing: it is how the manifest schema is
|
|
28
|
+
// built, and it has to be cheap enough to run over every declared plugin.
|
|
29
|
+
Kind string `json:"kind,omitempty"`
|
|
30
|
+
|
|
31
|
+
// Catalog is the merged catalog, for a plugin that renders from it. An
|
|
32
|
+
// extractor is handed the zero value: it runs before there is one.
|
|
33
|
+
Catalog catalog.Catalog `json:"catalog"`
|
|
34
|
+
|
|
35
|
+
// Input is where a plugin that reads source should look. Empty for a
|
|
36
|
+
// renderer, which has no business touching the tree.
|
|
37
|
+
Input Input `json:"input"`
|
|
38
|
+
|
|
39
|
+
// Options belong to the plugin and are passed through from the manifest
|
|
40
|
+
// unread, so the host never has to know what any plugin can be told.
|
|
41
|
+
Options json.RawMessage `json:"options"`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Input struct {
|
|
45
|
+
// Root is the directory to read, relative to the repository.
|
|
46
|
+
Root string `json:"root"`
|
|
47
|
+
|
|
48
|
+
// Output is the directory where the host will place the files returned by
|
|
49
|
+
// this plugin, relative to the repository. Extractors use it only when a
|
|
50
|
+
// catalog entry needs to point at a generated companion document.
|
|
51
|
+
Output string `json:"output,omitempty"`
|
|
52
|
+
|
|
53
|
+
// Provenance is not part of the request (portolan.0010). A fragment used
|
|
54
|
+
// to carry a commit and a date that the host worked out from the last
|
|
55
|
+
// commit touching Root and every plugin copied to the top of its output.
|
|
56
|
+
// The host now reads both from the git history of the fragment it wrote,
|
|
57
|
+
// so a plugin has nothing to stamp and no clock to read: its output
|
|
58
|
+
// changes exactly when the source it describes changes, and when that was
|
|
59
|
+
// is a fact about the file, not a field in it.
|
|
60
|
+
|
|
61
|
+
// History is when each file under Root was first committed and last
|
|
62
|
+
// changed, keyed by the file's path as the plugin would name it - the same
|
|
63
|
+
// relative form as Root. Present only for a plugin whose descriptor asks
|
|
64
|
+
// (NeedHistory) and only when Root lies inside a git checkout; a plugin
|
|
65
|
+
// that asked and finds nil knows there was no history to read. The host
|
|
66
|
+
// reads it (portolan.0007) so that no plugin has to run git, which a wasm
|
|
67
|
+
// module cannot.
|
|
68
|
+
History map[string]FileHistory `json:"history,omitempty"`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// FileHistory is one file's first and last commit. Revised is nil when the
|
|
72
|
+
// file has one commit.
|
|
73
|
+
type FileHistory struct {
|
|
74
|
+
Created Commit `json:"created"`
|
|
75
|
+
Revised *Commit `json:"revised,omitempty"`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Commit is one commit as the host read it: full sha, author name, and the
|
|
79
|
+
// committer date in strict ISO 8601.
|
|
80
|
+
type Commit struct {
|
|
81
|
+
Commit string `json:"commit"`
|
|
82
|
+
Author string `json:"author"`
|
|
83
|
+
Date string `json:"date"`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Response is what comes back.
|
|
87
|
+
//
|
|
88
|
+
// Files are named, not written (portolan.0001). The host rejects a name that climbs out of the
|
|
89
|
+
// output directory, and that rejection is the whole of a plugin's authority
|
|
90
|
+
// over the tree.
|
|
91
|
+
type Response struct {
|
|
92
|
+
Files []File `json:"files"`
|
|
93
|
+
warnings []Warning
|
|
94
|
+
|
|
95
|
+
// Describe answers KindDescribe and is absent otherwise. A plugin written
|
|
96
|
+
// against an older version of this protocol answers without it, which the
|
|
97
|
+
// host reports rather than treating as a plugin with no options.
|
|
98
|
+
Describe *Descriptor `json:"describe,omitempty"`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type File struct {
|
|
102
|
+
Name string `json:"name"`
|
|
103
|
+
Contents string `json:"contents"`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Warning is an in-process extraction note. It is intentionally not serialized
|
|
107
|
+
// into Response: the wire contract has one decisive outcome, files or error.
|
|
108
|
+
type Warning struct {
|
|
109
|
+
Severity string `json:"severity"` // "warning" | "error"
|
|
110
|
+
Message string `json:"message"`
|
|
111
|
+
Ref string `json:"ref,omitempty"`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Builder accumulates a response. Every plugin needs exactly this and nothing
|
|
115
|
+
// more, so it lives here rather than being written twice.
|
|
116
|
+
type Builder struct {
|
|
117
|
+
Files []File
|
|
118
|
+
Warnings []Warning
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func (b *Builder) File(name, contents string) {
|
|
122
|
+
b.Files = append(b.Files, File{Name: name, Contents: contents})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func (b *Builder) Warn(ref, message string) {
|
|
126
|
+
b.Warnings = append(b.Warnings, Warning{Severity: "warning", Message: message, Ref: ref})
|
|
127
|
+
where := ""
|
|
128
|
+
if ref != "" {
|
|
129
|
+
where = ref + ": "
|
|
130
|
+
}
|
|
131
|
+
fmt.Fprintln(os.Stderr, "warning:", where+message)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func (b *Builder) Response() Response {
|
|
135
|
+
return Response{Files: b.Files, warnings: b.Warnings}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Warnings exposes extraction notes to in-process tests and embedders. They
|
|
139
|
+
// are deliberately absent from the JSON protocol: a caller's result is files
|
|
140
|
+
// or an error, not files plus an advisory property nobody is required to act on.
|
|
141
|
+
func (r Response) Warnings() []Warning { return r.warnings }
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Package schematest checks that a plugin's options schema still describes its
|
|
2
|
+
// options struct.
|
|
3
|
+
//
|
|
4
|
+
// The schema is written by hand rather than reflected out of the struct,
|
|
5
|
+
// because what makes it worth having is the prose: which slug a store expects,
|
|
6
|
+
// whose proto package a peer answers for. None of that survives reflection.
|
|
7
|
+
// What reflection is good for is the half nobody would notice going stale - a
|
|
8
|
+
// field renamed, a field added, a schema left behind - so that is all this
|
|
9
|
+
// does.
|
|
10
|
+
package schematest
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"encoding/json"
|
|
14
|
+
"reflect"
|
|
15
|
+
"sort"
|
|
16
|
+
"strings"
|
|
17
|
+
"testing"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
type document struct {
|
|
21
|
+
Type string `json:"type"`
|
|
22
|
+
AdditionalProperties *bool `json:"additionalProperties"`
|
|
23
|
+
Properties map[string]json.RawMessage `json:"properties"`
|
|
24
|
+
Required []string `json:"required"`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check reports every way the schema and the struct have come apart.
|
|
28
|
+
//
|
|
29
|
+
// options is a zero value of the plugin's options type; schema is the embedded
|
|
30
|
+
// document it hands back when asked to describe itself.
|
|
31
|
+
func Check(t *testing.T, schema []byte, options any) {
|
|
32
|
+
t.Helper()
|
|
33
|
+
|
|
34
|
+
var doc document
|
|
35
|
+
if err := json.Unmarshal(schema, &doc); err != nil {
|
|
36
|
+
t.Fatalf("options schema is not JSON: %v", err)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if doc.Type != "object" {
|
|
40
|
+
t.Errorf("options schema has type %q, want object", doc.Type)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Without this an unknown key validates, and the misspelled option - the
|
|
44
|
+
// one error this whole mechanism exists to catch - goes through silently.
|
|
45
|
+
if doc.AdditionalProperties == nil || *doc.AdditionalProperties {
|
|
46
|
+
t.Error("options schema must set additionalProperties: false")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
described := keys(doc.Properties)
|
|
50
|
+
declared := fields(reflect.TypeOf(options))
|
|
51
|
+
|
|
52
|
+
for _, name := range missing(declared, described) {
|
|
53
|
+
t.Errorf("option %q is in the struct but not in the schema", name)
|
|
54
|
+
}
|
|
55
|
+
for _, name := range missing(described, declared) {
|
|
56
|
+
t.Errorf("option %q is in the schema but not in the struct", name)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for _, name := range doc.Required {
|
|
60
|
+
if _, ok := doc.Properties[name]; !ok {
|
|
61
|
+
t.Errorf("option %q is required but not described", name)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A property with nothing to say is a property the manifest schema will
|
|
66
|
+
// offer with no hint of what it means, which is where this started.
|
|
67
|
+
for name, raw := range doc.Properties {
|
|
68
|
+
var property struct {
|
|
69
|
+
Description string `json:"description"`
|
|
70
|
+
}
|
|
71
|
+
if err := json.Unmarshal(raw, &property); err != nil {
|
|
72
|
+
t.Errorf("option %q: %v", name, err)
|
|
73
|
+
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if strings.TrimSpace(property.Description) == "" {
|
|
77
|
+
t.Errorf("option %q has no description", name)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// fields are the json names of an options struct, which is the only shape this
|
|
83
|
+
// takes: a plugin's options are a flat object read out of the manifest.
|
|
84
|
+
func fields(t reflect.Type) []string {
|
|
85
|
+
var names []string
|
|
86
|
+
|
|
87
|
+
for i := 0; i < t.NumField(); i++ {
|
|
88
|
+
tag := t.Field(i).Tag.Get("json")
|
|
89
|
+
name, _, _ := strings.Cut(tag, ",")
|
|
90
|
+
if name == "" || name == "-" {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
names = append(names, name)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sort.Strings(names)
|
|
97
|
+
|
|
98
|
+
return names
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func keys(m map[string]json.RawMessage) []string {
|
|
102
|
+
names := make([]string, 0, len(m))
|
|
103
|
+
for name := range m {
|
|
104
|
+
names = append(names, name)
|
|
105
|
+
}
|
|
106
|
+
sort.Strings(names)
|
|
107
|
+
|
|
108
|
+
return names
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// missing is everything in a that is not in b.
|
|
112
|
+
func missing(a, b []string) []string {
|
|
113
|
+
have := make(map[string]bool, len(b))
|
|
114
|
+
for _, name := range b {
|
|
115
|
+
have[name] = true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var gone []string
|
|
119
|
+
for _, name := range a {
|
|
120
|
+
if !have[name] {
|
|
121
|
+
gone = append(gone, name)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return gone
|
|
126
|
+
}
|