@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.
Files changed (291) hide show
  1. package/README.md +174 -6
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1213 -0
  5. package/catalog/roundtrip_test.go +210 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +14 -1
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +21 -0
  11. package/go.sum +34 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +28 -15
  35. package/plugin/describe.go +156 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +404 -50
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-argocd/options.schema.json +44 -0
  42. package/plugins/extract-celery/extract.py +0 -2
  43. package/plugins/extract-celery/extract_test.py +1 -1
  44. package/plugins/extract-celery/main.py +1 -0
  45. package/plugins/extract-csharp-ddd/README.md +213 -0
  46. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  47. package/plugins/extract-django/README.md +52 -19
  48. package/plugins/extract-django/choices.py +47 -0
  49. package/plugins/extract-django/domain.py +30 -18
  50. package/plugins/extract-django/extract.py +21 -7
  51. package/plugins/extract-django/extract_test.py +68 -2
  52. package/plugins/extract-django/lifecycle.py +4 -28
  53. package/plugins/extract-django/main.py +1 -0
  54. package/plugins/extract-django/operations.py +1 -1
  55. package/plugins/extract-django/routing_test.py +109 -1
  56. package/plugins/extract-django/rules.py +154 -0
  57. package/plugins/extract-django/rules_test.py +158 -0
  58. package/plugins/extract-django/store.py +1 -1
  59. package/plugins/extract-django/transport.py +101 -55
  60. package/plugins/extract-django/verbs.py +241 -0
  61. package/plugins/extract-go/README.md +47 -0
  62. package/plugins/extract-go-sqs/options.schema.json +21 -0
  63. package/plugins/extract-http-clients/describe.go +20 -0
  64. package/plugins/extract-http-clients/describe_test.go +11 -0
  65. package/plugins/extract-http-clients/extract.go +740 -0
  66. package/plugins/extract-http-clients/extract_test.go +1561 -0
  67. package/plugins/extract-http-clients/main.go +41 -0
  68. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  69. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  70. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  71. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  72. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  73. package/plugins/extract-k8s/options.schema.json +32 -0
  74. package/plugins/extract-laravel/Cargo.lock +962 -0
  75. package/plugins/extract-laravel/Cargo.toml +20 -0
  76. package/plugins/extract-laravel/README.md +200 -0
  77. package/plugins/extract-laravel/options.schema.json +68 -0
  78. package/plugins/extract-laravel/rustfmt.toml +1 -0
  79. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  80. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  81. package/plugins/extract-php-ddd/README.md +141 -0
  82. package/plugins/extract-php-ddd/options.schema.json +50 -0
  83. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  84. package/plugins/extract-python-kafka/extract.py +0 -2
  85. package/plugins/extract-python-kafka/extract_test.py +1 -1
  86. package/plugins/extract-python-kafka/main.py +1 -0
  87. package/plugins/extract-terraform/options.schema.json +26 -0
  88. package/plugins/extract-ts/extract.test.ts +2 -2
  89. package/plugins/extract-ts/extract.ts +4 -5
  90. package/plugins/extract-ts/graphql.test.ts +1 -1
  91. package/plugins/extract-ts/main.ts +1 -0
  92. package/plugins/openapi/ids.go +261 -0
  93. package/plugins/openapi/ids_test.go +98 -0
  94. package/plugins/phpscan/Cargo.lock +953 -0
  95. package/plugins/phpscan/Cargo.toml +19 -0
  96. package/plugins/phpscan/rustfmt.toml +1 -0
  97. package/plugins/portolan-go.wasm +0 -0
  98. package/plugins/pyplugin/catalog.py +24 -1
  99. package/plugins/pyplugin/protocol.py +1 -5
  100. package/plugins/verify-otel/options.schema.json +12 -0
  101. package/portolan.json +132 -4
  102. package/rules/builtin.json +218 -0
  103. package/schema/portolan.schema.json +905 -4
  104. package/scripts/README.md +21 -13
  105. package/scripts/catalog-sources.mjs +6 -0
  106. package/scripts/delivery-presets.mjs +57 -14
  107. package/scripts/diff.mjs +5 -1
  108. package/scripts/django-aggregates.test.mjs +58 -0
  109. package/scripts/gen-likec4.mjs +150 -17
  110. package/scripts/gen-likec4.test.mjs +96 -0
  111. package/scripts/gen.mjs +148 -118
  112. package/scripts/gitops-example.test.mjs +108 -0
  113. package/scripts/go-discovery.test.mjs +30 -0
  114. package/scripts/history.mjs +186 -3
  115. package/scripts/history.test.mjs +1 -1
  116. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  117. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  118. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  119. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  120. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  121. package/scripts/host-plugins/fetch-git.mjs +78 -21
  122. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  123. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  124. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  125. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  126. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  127. package/scripts/json-format.mjs +192 -0
  128. package/scripts/json-format.test.mjs +97 -0
  129. package/scripts/local-api.mjs +287 -12
  130. package/scripts/local-api.test.mjs +150 -5
  131. package/scripts/local-discovery.mjs +94 -9
  132. package/scripts/manifest.mjs +14 -3
  133. package/scripts/manifest.test.mjs +24 -0
  134. package/scripts/output-diff.mjs +94 -0
  135. package/scripts/output-diff.test.mjs +36 -0
  136. package/scripts/package-smoke.mjs +62 -4
  137. package/scripts/plugin-host.mjs +45 -3
  138. package/scripts/plugin-host.test.mjs +9 -0
  139. package/scripts/plugin-wasm-worker.mjs +4 -1
  140. package/scripts/plugins-fresh.mjs +97 -0
  141. package/scripts/plugins-fresh.test.mjs +64 -0
  142. package/scripts/provenance.mjs +72 -0
  143. package/scripts/provenance.test.mjs +149 -0
  144. package/scripts/run-builtin.mjs +62 -7
  145. package/scripts/schema.mjs +167 -19
  146. package/scripts/trace-trials.mjs +176 -0
  147. package/scripts/trace-trials.test.mjs +142 -0
  148. package/scripts/warning-policy.mjs +167 -0
  149. package/scripts/warning-policy.test.mjs +93 -0
  150. package/src/app/Breadcrumbs.test.ts +3 -0
  151. package/src/app/Breadcrumbs.tsx +3 -0
  152. package/src/app/CatalogApp.tsx +2 -0
  153. package/src/app/Sidebar.tsx +3 -3
  154. package/src/app/SidebarFooter.tsx +20 -4
  155. package/src/catalog-docs.test.ts +64 -0
  156. package/src/catalog-docs.ts +35 -0
  157. package/src/catalog-error.test.ts +15 -0
  158. package/src/catalog-index.ts +25 -0
  159. package/src/catalog-model.ts +293 -5
  160. package/src/catalog-validation.ts +113 -2
  161. package/src/catalog.test.ts +40 -1
  162. package/src/chat/Starter.tsx +5 -11
  163. package/src/chat/tools.test.ts +27 -0
  164. package/src/chat/tools.ts +5 -9
  165. package/src/components/CatalogStamp.tsx +10 -8
  166. package/src/components/ChannelRows.test.tsx +76 -1
  167. package/src/components/ChannelRows.tsx +223 -60
  168. package/src/components/DeploymentRows.tsx +131 -0
  169. package/src/components/DocsLinks.test.tsx +27 -0
  170. package/src/components/DocsLinks.tsx +56 -0
  171. package/src/components/FieldTree.tsx +5 -2
  172. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  173. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  174. package/src/components/Integrations.tsx +1 -1
  175. package/src/components/MachineDocs.tsx +6 -5
  176. package/src/components/MethodRows.tsx +9 -2
  177. package/src/components/PluginIcon.tsx +77 -0
  178. package/src/components/ProblemRow.tsx +173 -153
  179. package/src/components/RelationEvidence.test.tsx +14 -0
  180. package/src/components/RelationEvidence.tsx +53 -0
  181. package/src/components/RuleMarks.tsx +22 -0
  182. package/src/components/ShapeRows.tsx +24 -20
  183. package/src/data.ts +46 -7
  184. package/src/enrich.test.ts +459 -4
  185. package/src/enrich.ts +308 -7
  186. package/src/er/ErCanvas.tsx +218 -12
  187. package/src/er/GroupNode.tsx +57 -0
  188. package/src/er/StoreHeader.tsx +1 -0
  189. package/src/er/layout.test.ts +85 -2
  190. package/src/er/layout.ts +140 -5
  191. package/src/er/spec.test.ts +17 -0
  192. package/src/er/spec.ts +23 -10
  193. package/src/flow/Recordings.test.tsx +52 -0
  194. package/src/flow/Recordings.tsx +236 -0
  195. package/src/flow/StepDetail.tsx +59 -0
  196. package/src/flow/TraceTrial.tsx +419 -0
  197. package/src/flow/evidence.test.ts +16 -0
  198. package/src/flow/evidence.ts +34 -0
  199. package/src/flow/examples.test.ts +33 -0
  200. package/src/flow/examples.ts +37 -0
  201. package/src/flow/outline.test.ts +28 -0
  202. package/src/flow/outline.ts +7 -2
  203. package/src/flow/trace-trial-resume.test.ts +42 -0
  204. package/src/flow/trace-trial-resume.ts +74 -0
  205. package/src/graph/elk.ts +78 -0
  206. package/src/index.css +68 -0
  207. package/src/landing/DraggableReveal.tsx +3 -2
  208. package/src/landing/EvidencePipeline.tsx +105 -0
  209. package/src/landing/LandingPage.tsx +17 -70
  210. package/src/landing/ProductTour.tsx +6 -6
  211. package/src/lib/all-problems.ts +27 -17
  212. package/src/lib/catalog-diff.ts +1 -1
  213. package/src/lib/centrality.test.ts +251 -0
  214. package/src/lib/centrality.ts +232 -0
  215. package/src/lib/confluence.test.ts +37 -0
  216. package/src/lib/confluence.ts +41 -0
  217. package/src/lib/context-color.ts +1 -1
  218. package/src/lib/deployment-drift.ts +22 -0
  219. package/src/lib/derive.ts +43 -69
  220. package/src/lib/django-aggregates.d.mts +9 -0
  221. package/src/lib/django-aggregates.mjs +36 -0
  222. package/src/lib/django-aggregates.test.ts +29 -0
  223. package/src/lib/django-aggregates.ts +5 -0
  224. package/src/lib/environments.test.ts +83 -0
  225. package/src/lib/environments.ts +57 -0
  226. package/src/lib/integration-url.test.ts +30 -0
  227. package/src/lib/integration-url.ts +63 -0
  228. package/src/lib/kafka-ui.ts +3 -45
  229. package/src/lib/local-api.ts +116 -4
  230. package/src/lib/notion.ts +13 -0
  231. package/src/lib/plugin-index.json +2765 -0
  232. package/src/lib/plugins.test.ts +68 -0
  233. package/src/lib/plugins.ts +264 -0
  234. package/src/lib/problem-flows.test.ts +61 -0
  235. package/src/lib/problem-flows.ts +78 -0
  236. package/src/lib/problem-rules-cel.d.mts +43 -0
  237. package/src/lib/problem-rules-cel.mjs +407 -0
  238. package/src/lib/problem-rules.test.ts +287 -0
  239. package/src/lib/problem-rules.ts +271 -0
  240. package/src/lib/problem-subjects.ts +737 -0
  241. package/src/lib/rule-entries.ts +39 -0
  242. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  243. package/src/lib/rules-deploy.test.ts +133 -0
  244. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  245. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  246. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  247. package/src/lib/rules.test.ts +51 -0
  248. package/src/lib/rules.ts +86 -0
  249. package/src/lib/setup-info.test.ts +17 -0
  250. package/src/lib/setup-info.ts +58 -0
  251. package/src/lib/shape.test.ts +32 -0
  252. package/src/lib/shape.ts +30 -6
  253. package/src/lib/tech.ts +16 -0
  254. package/src/lib/trace-project.test.ts +34 -0
  255. package/src/lib/trace-project.ts +50 -0
  256. package/src/lib/use-problems.ts +23 -0
  257. package/src/lib/warnings.test.ts +63 -0
  258. package/src/lib/warnings.ts +260 -0
  259. package/src/likec4/ids.test.ts +6 -2
  260. package/src/likec4/ids.ts +43 -0
  261. package/src/main.tsx +23 -0
  262. package/src/map/ContextMapGraph.tsx +76 -32
  263. package/src/merge-deployments.test.ts +127 -0
  264. package/src/merge.test.ts +82 -0
  265. package/src/merge.ts +189 -18
  266. package/src/pages/AggregatePage.tsx +65 -14
  267. package/src/pages/ContextMap.tsx +45 -3
  268. package/src/pages/ContextPage.tsx +8 -5
  269. package/src/pages/EventPage.tsx +15 -5
  270. package/src/pages/FlowDetail.tsx +23 -2
  271. package/src/pages/GraphPage.tsx +40 -3
  272. package/src/pages/Overview.tsx +152 -12
  273. package/src/pages/PluginIndex.tsx +190 -0
  274. package/src/pages/Problems.tsx +396 -128
  275. package/src/pages/ServicePage.tsx +62 -5
  276. package/src/pages/Settings.tsx +217 -43
  277. package/src/pages/settings/AboutSettings.tsx +8 -1
  278. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  279. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  280. package/src/pages/settings/RecordingSettings.tsx +138 -0
  281. package/src/pages/settings/RulesSettings.tsx +825 -0
  282. package/src/routes.test.ts +9 -0
  283. package/src/routes.ts +22 -1
  284. package/src/selection/DetailPanel.tsx +15 -0
  285. package/src/virtual-provenance.d.ts +11 -0
  286. package/vite.config.ts +5 -0
  287. package/scripts/vendor-lock.mjs +0 -58
  288. package/scripts/vendor-lock.test.mjs +0 -69
  289. package/src/lib/data-problems.ts +0 -314
  290. package/src/lib/proto-problems.ts +0 -237
  291. 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
+ }