@shortlink-org/portolan 0.2.4 → 0.3.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 (151) hide show
  1. package/README.md +23 -0
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1066 -0
  5. package/catalog/roundtrip_test.go +203 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +8 -0
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +14 -0
  11. package/go.sum +20 -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 +22 -10
  35. package/plugin/describe.go +118 -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 +109 -46
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-celery/extract.py +0 -2
  42. package/plugins/extract-celery/extract_test.py +1 -1
  43. package/plugins/extract-django/README.md +39 -17
  44. package/plugins/extract-django/domain.py +28 -17
  45. package/plugins/extract-django/extract.py +21 -7
  46. package/plugins/extract-django/extract_test.py +55 -2
  47. package/plugins/extract-django/lifecycle.py +2 -0
  48. package/plugins/extract-django/operations.py +1 -1
  49. package/plugins/extract-django/routing_test.py +109 -1
  50. package/plugins/extract-django/store.py +1 -1
  51. package/plugins/extract-django/transport.py +101 -55
  52. package/plugins/extract-django/verbs.py +241 -0
  53. package/plugins/extract-go/README.md +47 -0
  54. package/plugins/extract-http-clients/describe.go +19 -0
  55. package/plugins/extract-http-clients/describe_test.go +11 -0
  56. package/plugins/extract-http-clients/extract.go +740 -0
  57. package/plugins/extract-http-clients/extract_test.go +1561 -0
  58. package/plugins/extract-http-clients/main.go +41 -0
  59. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  60. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  61. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  62. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  63. package/plugins/extract-python-kafka/extract.py +0 -2
  64. package/plugins/extract-python-kafka/extract_test.py +1 -1
  65. package/plugins/extract-ts/extract.test.ts +2 -2
  66. package/plugins/extract-ts/extract.ts +4 -5
  67. package/plugins/extract-ts/graphql.test.ts +1 -1
  68. package/plugins/openapi/ids.go +261 -0
  69. package/plugins/openapi/ids_test.go +98 -0
  70. package/plugins/portolan-go.wasm +0 -0
  71. package/plugins/pyplugin/protocol.py +1 -5
  72. package/portolan.json +3 -2
  73. package/schema/portolan.schema.json +34 -0
  74. package/scripts/README.md +18 -12
  75. package/scripts/catalog-sources.mjs +6 -0
  76. package/scripts/delivery-presets.mjs +21 -11
  77. package/scripts/diff.mjs +5 -1
  78. package/scripts/django-aggregates.test.mjs +58 -0
  79. package/scripts/gen-likec4.mjs +1 -1
  80. package/scripts/gen.mjs +118 -115
  81. package/scripts/go-discovery.test.mjs +30 -0
  82. package/scripts/history.mjs +186 -3
  83. package/scripts/history.test.mjs +1 -1
  84. package/scripts/host-plugins/fetch-git.mjs +77 -21
  85. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  86. package/scripts/local-api.mjs +71 -4
  87. package/scripts/local-api.test.mjs +63 -4
  88. package/scripts/local-discovery.mjs +82 -9
  89. package/scripts/manifest.mjs +5 -3
  90. package/scripts/manifest.test.mjs +24 -0
  91. package/scripts/output-diff.mjs +94 -0
  92. package/scripts/output-diff.test.mjs +36 -0
  93. package/scripts/package-smoke.mjs +62 -4
  94. package/scripts/plugin-host.mjs +22 -2
  95. package/scripts/plugin-host.test.mjs +9 -0
  96. package/scripts/plugin-wasm-worker.mjs +4 -1
  97. package/scripts/provenance.mjs +72 -0
  98. package/scripts/provenance.test.mjs +149 -0
  99. package/scripts/run-builtin.mjs +39 -5
  100. package/scripts/schema.mjs +29 -0
  101. package/scripts/warning-policy.mjs +161 -0
  102. package/scripts/warning-policy.test.mjs +56 -0
  103. package/src/app/Sidebar.tsx +3 -3
  104. package/src/catalog-docs.test.ts +64 -0
  105. package/src/catalog-docs.ts +35 -0
  106. package/src/catalog-error.test.ts +15 -0
  107. package/src/catalog-model.ts +48 -5
  108. package/src/catalog-validation.ts +9 -0
  109. package/src/chat/Starter.tsx +5 -11
  110. package/src/chat/tools.test.ts +27 -0
  111. package/src/chat/tools.ts +5 -9
  112. package/src/components/CatalogStamp.tsx +10 -8
  113. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  114. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  115. package/src/components/Integrations.tsx +1 -1
  116. package/src/components/MachineDocs.tsx +6 -5
  117. package/src/components/MethodRows.tsx +9 -2
  118. package/src/components/RelationEvidence.test.tsx +14 -0
  119. package/src/components/RelationEvidence.tsx +53 -0
  120. package/src/data.ts +25 -7
  121. package/src/enrich.test.ts +332 -1
  122. package/src/enrich.ts +206 -3
  123. package/src/flow/StepDetail.tsx +6 -0
  124. package/src/flow/evidence.test.ts +16 -0
  125. package/src/flow/evidence.ts +34 -0
  126. package/src/index.css +44 -0
  127. package/src/landing/DraggableReveal.tsx +3 -2
  128. package/src/landing/EvidencePipeline.tsx +105 -0
  129. package/src/landing/LandingPage.tsx +2 -59
  130. package/src/lib/catalog-diff.ts +1 -1
  131. package/src/lib/django-aggregates.d.mts +9 -0
  132. package/src/lib/django-aggregates.mjs +36 -0
  133. package/src/lib/django-aggregates.test.ts +29 -0
  134. package/src/lib/django-aggregates.ts +5 -0
  135. package/src/lib/local-api.ts +20 -2
  136. package/src/lib/setup-info.test.ts +17 -0
  137. package/src/lib/setup-info.ts +58 -0
  138. package/src/lib/warnings.test.ts +54 -0
  139. package/src/lib/warnings.ts +260 -0
  140. package/src/map/ContextMapGraph.tsx +76 -32
  141. package/src/merge.ts +16 -9
  142. package/src/pages/AggregatePage.tsx +8 -7
  143. package/src/pages/ContextPage.tsx +6 -5
  144. package/src/pages/ServicePage.tsx +4 -3
  145. package/src/pages/Settings.tsx +189 -41
  146. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  147. package/src/selection/DetailPanel.tsx +15 -0
  148. package/src/virtual-provenance.d.ts +11 -0
  149. package/vite.config.ts +5 -0
  150. package/scripts/vendor-lock.mjs +0 -58
  151. package/scripts/vendor-lock.test.mjs +0 -69
@@ -0,0 +1,949 @@
1
+ package wsdl
2
+
3
+ import (
4
+ "encoding/xml"
5
+ "fmt"
6
+ "io"
7
+ "io/fs"
8
+ "net/url"
9
+ "os"
10
+ "path/filepath"
11
+ "sort"
12
+ "strings"
13
+ )
14
+
15
+ const (
16
+ soap11WSDL = "http://schemas.xmlsoap.org/wsdl/soap/"
17
+ soap12WSDL = "http://schemas.xmlsoap.org/wsdl/soap12/"
18
+ xsdNS = "http://www.w3.org/2001/XMLSchema"
19
+ )
20
+
21
+ type qname struct {
22
+ ns string
23
+ local string
24
+ }
25
+
26
+ type node struct {
27
+ name xml.Name
28
+ attrs []xml.Attr
29
+ ns map[string]string
30
+ text string
31
+ children []*node
32
+ }
33
+
34
+ func (n *node) attr(name string) string {
35
+ for _, attr := range n.attrs {
36
+ if attr.Name.Local == name && attr.Name.Space != "xmlns" {
37
+ return strings.TrimSpace(attr.Value)
38
+ }
39
+ }
40
+ return ""
41
+ }
42
+
43
+ func (n *node) child(name string) *node {
44
+ for _, child := range n.children {
45
+ if child.name.Local == name {
46
+ return child
47
+ }
48
+ }
49
+ return nil
50
+ }
51
+
52
+ func (n *node) childrenNamed(name string) []*node {
53
+ var out []*node
54
+ for _, child := range n.children {
55
+ if child.name.Local == name {
56
+ out = append(out, child)
57
+ }
58
+ }
59
+ return out
60
+ }
61
+
62
+ func (n *node) documentation() string {
63
+ doc := n.child("documentation")
64
+ if doc == nil {
65
+ annotation := n.child("annotation")
66
+ if annotation != nil {
67
+ doc = annotation.child("documentation")
68
+ }
69
+ }
70
+ if doc == nil {
71
+ return ""
72
+ }
73
+ return strings.Join(strings.Fields(doc.deepText()), " ")
74
+ }
75
+
76
+ func (n *node) deepText() string {
77
+ parts := []string{n.text}
78
+ for _, child := range n.children {
79
+ parts = append(parts, child.deepText())
80
+ }
81
+ return strings.Join(parts, " ")
82
+ }
83
+
84
+ func parseXML(reader io.Reader) (*node, error) {
85
+ decoder := xml.NewDecoder(reader)
86
+ rootNS := map[string]string{"xml": "http://www.w3.org/XML/1998/namespace"}
87
+ var root *node
88
+ var stack []*node
89
+ for {
90
+ token, err := decoder.Token()
91
+ if err == io.EOF {
92
+ break
93
+ }
94
+ if err != nil {
95
+ return nil, err
96
+ }
97
+ switch value := token.(type) {
98
+ case xml.StartElement:
99
+ inScope := rootNS
100
+ if len(stack) > 0 {
101
+ inScope = stack[len(stack)-1].ns
102
+ }
103
+ namespaces := make(map[string]string, len(inScope)+2)
104
+ for prefix, namespace := range inScope {
105
+ namespaces[prefix] = namespace
106
+ }
107
+ for _, attr := range value.Attr {
108
+ switch {
109
+ case attr.Name.Space == "xmlns":
110
+ namespaces[attr.Name.Local] = attr.Value
111
+ case attr.Name.Space == "" && attr.Name.Local == "xmlns":
112
+ namespaces[""] = attr.Value
113
+ }
114
+ }
115
+ current := &node{name: value.Name, attrs: value.Attr, ns: namespaces}
116
+ if len(stack) == 0 {
117
+ root = current
118
+ } else {
119
+ parent := stack[len(stack)-1]
120
+ parent.children = append(parent.children, current)
121
+ }
122
+ stack = append(stack, current)
123
+ case xml.CharData:
124
+ if len(stack) > 0 {
125
+ stack[len(stack)-1].text += string(value)
126
+ }
127
+ case xml.EndElement:
128
+ if len(stack) > 0 {
129
+ stack = stack[:len(stack)-1]
130
+ }
131
+ }
132
+ }
133
+ if root == nil {
134
+ return nil, fmt.Errorf("empty XML document")
135
+ }
136
+ return root, nil
137
+ }
138
+
139
+ type document struct {
140
+ path string
141
+ root *node
142
+ }
143
+
144
+ type loader struct {
145
+ root string
146
+ docs map[string]*document
147
+ loaded map[string]bool
148
+ adopted map[string]map[string]bool
149
+ warnings []string
150
+ duplicates []duplicate
151
+ }
152
+
153
+ // duplicate is one name declared again in a namespace: where the second
154
+ // declaration was found, and where the one the index kept came from.
155
+ type duplicate struct {
156
+ path string
157
+ name string
158
+ namespace string
159
+ origin string
160
+ }
161
+
162
+ // Read loads one WSDL document and every local WSDL/XSD import or include it
163
+ // reaches. spec is relative to root.
164
+ func Read(root, spec string) (Result, error) {
165
+ absRoot, err := filepath.Abs(root)
166
+ if err != nil {
167
+ return Result{}, err
168
+ }
169
+ l := newLoader(absRoot)
170
+ path, err := l.localPath(absRoot, spec)
171
+ if err != nil {
172
+ return Result{}, err
173
+ }
174
+ if err := l.load(path, ""); err != nil {
175
+ return Result{}, err
176
+ }
177
+ result := l.result([]string{path}, true)
178
+ return result, nil
179
+ }
180
+
181
+ // Discover reads every WSDL in root. Imported interface-only documents are
182
+ // used to complete their parent and are not emitted a second time.
183
+ func Discover(root string) (Result, error) {
184
+ absRoot, err := filepath.Abs(root)
185
+ if err != nil {
186
+ return Result{}, err
187
+ }
188
+ var candidates []string
189
+ err = filepath.WalkDir(absRoot, func(path string, entry fs.DirEntry, walkErr error) error {
190
+ if walkErr != nil {
191
+ return walkErr
192
+ }
193
+ if entry.IsDir() {
194
+ if path != absRoot && (strings.HasPrefix(entry.Name(), ".") || entry.Name() == "node_modules") {
195
+ return filepath.SkipDir
196
+ }
197
+ return nil
198
+ }
199
+ if strings.EqualFold(filepath.Ext(entry.Name()), ".wsdl") {
200
+ candidates = append(candidates, path)
201
+ }
202
+ return nil
203
+ })
204
+ if err != nil {
205
+ return Result{}, err
206
+ }
207
+ sort.Strings(candidates)
208
+ var roots []string
209
+ for _, path := range candidates {
210
+ file, openErr := os.Open(path)
211
+ if openErr != nil {
212
+ return Result{}, openErr
213
+ }
214
+ node, parseErr := parseXML(file)
215
+ _ = file.Close()
216
+ if parseErr != nil {
217
+ return Result{}, fmt.Errorf("%s: %w", path, parseErr)
218
+ }
219
+ if node.name.Local == "definitions" && len(node.childrenNamed("service")) > 0 {
220
+ roots = append(roots, path)
221
+ }
222
+ }
223
+ standalone := false
224
+ if len(roots) == 0 {
225
+ // Some repositories keep only the reusable portType document. It is
226
+ // still a useful contract when no service document exists to own it.
227
+ roots = candidates
228
+ standalone = true
229
+ }
230
+ combined := Result{}
231
+ for _, path := range roots {
232
+ l := newLoader(absRoot)
233
+ if err := l.load(path, ""); err != nil {
234
+ return Result{}, err
235
+ }
236
+ part := l.result([]string{path}, standalone)
237
+ combined.Contracts = append(combined.Contracts, part.Contracts...)
238
+ combined.Warnings = append(combined.Warnings, part.Warnings...)
239
+ }
240
+ combined.Contracts = uniqueContracts(combined.Contracts)
241
+ combined.Warnings = unique(combined.Warnings)
242
+ sort.Slice(combined.Contracts, func(i, j int) bool {
243
+ if combined.Contracts[i].Name != combined.Contracts[j].Name {
244
+ return combined.Contracts[i].Name < combined.Contracts[j].Name
245
+ }
246
+ return combined.Contracts[i].Source < combined.Contracts[j].Source
247
+ })
248
+ sort.Strings(combined.Warnings)
249
+ return combined, nil
250
+ }
251
+
252
+ func newLoader(root string) *loader {
253
+ return &loader{
254
+ root: root, docs: map[string]*document{}, loaded: map[string]bool{},
255
+ adopted: map[string]map[string]bool{},
256
+ }
257
+ }
258
+
259
+ func (l *loader) localPath(from, location string) (string, error) {
260
+ if strings.TrimSpace(location) == "" {
261
+ return "", fmt.Errorf("empty WSDL path")
262
+ }
263
+ if parsed, err := url.Parse(location); err == nil && parsed.Scheme != "" {
264
+ return "", fmt.Errorf("remote import %q is not read", location)
265
+ }
266
+ path := location
267
+ if !filepath.IsAbs(path) {
268
+ path = filepath.Join(from, filepath.FromSlash(path))
269
+ }
270
+ path, err := filepath.Abs(filepath.Clean(path))
271
+ if err != nil {
272
+ return "", err
273
+ }
274
+ rel, err := filepath.Rel(l.root, path)
275
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
276
+ return "", fmt.Errorf("import %q leaves the input root", location)
277
+ }
278
+ return path, nil
279
+ }
280
+
281
+ func (l *loader) load(path, adoptedNS string) error {
282
+ key := path + "\x00" + adoptedNS
283
+ if l.loaded[key] {
284
+ return nil
285
+ }
286
+ l.loaded[key] = true
287
+ doc := l.docs[path]
288
+ if doc == nil {
289
+ file, err := os.Open(path)
290
+ if err != nil {
291
+ return err
292
+ }
293
+ root, parseErr := parseXML(file)
294
+ closeErr := file.Close()
295
+ if parseErr != nil {
296
+ return fmt.Errorf("%s: %w", path, parseErr)
297
+ }
298
+ if closeErr != nil {
299
+ return closeErr
300
+ }
301
+ doc = &document{path: path, root: root}
302
+ l.docs[path] = doc
303
+ }
304
+ if doc.root.name.Local == "schema" && doc.root.attr("targetNamespace") == "" && adoptedNS != "" {
305
+ if l.adopted[path] == nil {
306
+ l.adopted[path] = map[string]bool{}
307
+ }
308
+ l.adopted[path][adoptedNS] = true
309
+ }
310
+
311
+ for _, ref := range importReferences(doc.root, adoptedNS) {
312
+ imported, err := l.localPath(filepath.Dir(path), ref.location)
313
+ if err != nil {
314
+ rel, _ := filepath.Rel(l.root, path)
315
+ l.warnings = append(l.warnings, filepath.ToSlash(rel)+": "+err.Error())
316
+ continue
317
+ }
318
+ if err := l.load(imported, ref.adoptedNS); err != nil {
319
+ rel, _ := filepath.Rel(l.root, path)
320
+ l.warnings = append(l.warnings, filepath.ToSlash(rel)+": import "+ref.location+": "+err.Error())
321
+ }
322
+ }
323
+ return nil
324
+ }
325
+
326
+ type importReference struct {
327
+ location string
328
+ adoptedNS string
329
+ }
330
+
331
+ func importReferences(root *node, inheritedNS string) []importReference {
332
+ var out []importReference
333
+ var walk func(*node, string)
334
+ walk = func(n *node, schemaNS string) {
335
+ if n.name.Local == "schema" {
336
+ schemaNS = first(n.attr("targetNamespace"), schemaNS)
337
+ }
338
+ if n.name.Local == "import" || n.name.Local == "include" || n.name.Local == "redefine" {
339
+ if location := first(n.attr("location"), n.attr("schemaLocation")); location != "" {
340
+ adopted := ""
341
+ if n.name.Local == "include" || n.name.Local == "redefine" {
342
+ adopted = schemaNS
343
+ }
344
+ out = append(out, importReference{location: location, adoptedNS: adopted})
345
+ }
346
+ }
347
+ for _, child := range n.children {
348
+ walk(child, schemaNS)
349
+ }
350
+ }
351
+ walk(root, inheritedNS)
352
+ seen := map[string]bool{}
353
+ var uniqueRefs []importReference
354
+ for _, ref := range out {
355
+ key := ref.location + "\x00" + ref.adoptedNS
356
+ if !seen[key] {
357
+ uniqueRefs = append(uniqueRefs, ref)
358
+ seen[key] = true
359
+ }
360
+ }
361
+ return uniqueRefs
362
+ }
363
+
364
+ type index struct {
365
+ messages map[qname]*node
366
+ portTypes map[qname]*node
367
+ bindings map[qname]*node
368
+ elements map[qname]*node
369
+ complexTypes map[qname]*node
370
+ namespace map[*node]string
371
+ // origin is the document each indexed node was read from, so a duplicate
372
+ // can say which declaration won.
373
+ origin map[*node]string
374
+ }
375
+
376
+ func (l *loader) buildIndex() index {
377
+ ix := index{
378
+ messages: map[qname]*node{}, portTypes: map[qname]*node{}, bindings: map[qname]*node{},
379
+ elements: map[qname]*node{}, complexTypes: map[qname]*node{}, namespace: map[*node]string{},
380
+ origin: map[*node]string{},
381
+ }
382
+ paths := make([]string, 0, len(l.docs))
383
+ for path := range l.docs {
384
+ paths = append(paths, path)
385
+ }
386
+ sort.Strings(paths)
387
+ for _, path := range paths {
388
+ root := l.docs[path].root
389
+ source := l.relative(path)
390
+ switch root.name.Local {
391
+ case "definitions":
392
+ ns := root.attr("targetNamespace")
393
+ ix.namespace[root] = ns
394
+ for _, child := range root.children {
395
+ switch child.name.Local {
396
+ case "message":
397
+ ix.put(ix.messages, qname{ns, child.attr("name")}, child, &l.duplicates, source)
398
+ case "portType", "interface":
399
+ ix.put(ix.portTypes, qname{ns, child.attr("name")}, child, &l.duplicates, source)
400
+ case "binding":
401
+ ix.put(ix.bindings, qname{ns, child.attr("name")}, child, &l.duplicates, source)
402
+ case "types":
403
+ for _, schema := range child.childrenNamed("schema") {
404
+ ix.addSchema(schema, source, "", &l.duplicates)
405
+ }
406
+ }
407
+ }
408
+ case "schema":
409
+ namespaces := []string{root.attr("targetNamespace")}
410
+ if namespaces[0] == "" && len(l.adopted[path]) > 0 {
411
+ namespaces = namespaces[:0]
412
+ for namespace := range l.adopted[path] {
413
+ namespaces = append(namespaces, namespace)
414
+ }
415
+ sort.Strings(namespaces)
416
+ }
417
+ for _, namespace := range namespaces {
418
+ ix.addSchema(root, source, namespace, &l.duplicates)
419
+ }
420
+ }
421
+ }
422
+ return ix
423
+ }
424
+
425
+ func (ix *index) addSchema(schema *node, path, adoptedNS string, duplicates *[]duplicate) {
426
+ ns := first(schema.attr("targetNamespace"), adoptedNS)
427
+ ix.namespace[schema] = ns
428
+ for _, child := range schema.children {
429
+ switch child.name.Local {
430
+ case "element":
431
+ ix.put(ix.elements, qname{ns, child.attr("name")}, child, duplicates, path)
432
+ case "complexType":
433
+ ix.put(ix.complexTypes, qname{ns, child.attr("name")}, child, duplicates, path)
434
+ }
435
+ }
436
+ }
437
+
438
+ // put keeps the first declaration of a name and records every later one.
439
+ // Documents are indexed in path order, so "first" is the alphabetically
440
+ // earliest file, and the same file read under two adopted namespaces is
441
+ // the same node and not a duplicate of itself.
442
+ func (ix *index) put(target map[qname]*node, key qname, value *node, duplicates *[]duplicate, path string) {
443
+ if key.local == "" {
444
+ return
445
+ }
446
+ if previous, exists := target[key]; exists && previous != value {
447
+ *duplicates = append(*duplicates, duplicate{path: path, name: key.local, namespace: key.ns, origin: ix.origin[previous]})
448
+ return
449
+ }
450
+ target[key] = value
451
+ ix.origin[value] = path
452
+ }
453
+
454
+ func (l *loader) result(rootPaths []string, includeStandalone bool) Result {
455
+ ix := l.buildIndex()
456
+ var contracts []Contract
457
+ for _, path := range rootPaths {
458
+ doc := l.docs[path]
459
+ if doc == nil || doc.root.name.Local != "definitions" {
460
+ continue
461
+ }
462
+ definitions := doc.root
463
+ namespace := definitions.attr("targetNamespace")
464
+ services := definitions.childrenNamed("service")
465
+ if len(services) == 0 && includeStandalone {
466
+ contract := Contract{Name: first(definitions.attr("name"), strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))), Namespace: namespace, Summary: definitions.documentation(), Source: l.relative(path)}
467
+ for _, portType := range definitions.childrenNamed("portType") {
468
+ contract.Interfaces = append(contract.Interfaces, ix.abstractInterface(portType))
469
+ }
470
+ if len(contract.Interfaces) > 0 {
471
+ contracts = append(contracts, contract)
472
+ }
473
+ continue
474
+ }
475
+ for _, service := range services {
476
+ contract := Contract{Name: first(service.attr("name"), definitions.attr("name")), Namespace: namespace, Summary: first(service.documentation(), definitions.documentation()), Source: l.relative(path)}
477
+ for _, port := range service.childrenNamed("port") {
478
+ if iface, ok := ix.boundInterface(port); ok {
479
+ contract.Interfaces = append(contract.Interfaces, iface)
480
+ } else {
481
+ l.warnings = append(l.warnings, l.relative(path)+": port "+first(port.attr("name"), "<unnamed>")+" refers to a binding or portType that is not available locally")
482
+ }
483
+ }
484
+ if len(contract.Interfaces) > 0 {
485
+ contracts = append(contracts, contract)
486
+ }
487
+ }
488
+ }
489
+ contracts = uniqueContracts(contracts)
490
+ for i := range contracts {
491
+ sort.Slice(contracts[i].Interfaces, func(a, b int) bool { return contracts[i].Interfaces[a].Name < contracts[i].Interfaces[b].Name })
492
+ }
493
+ sort.Slice(contracts, func(i, j int) bool {
494
+ if contracts[i].Name != contracts[j].Name {
495
+ return contracts[i].Name < contracts[j].Name
496
+ }
497
+ return contracts[i].Source < contracts[j].Source
498
+ })
499
+ sort.Strings(l.warnings)
500
+ return Result{Contracts: contracts, Warnings: unique(l.warnings), SchemaWarnings: summarizeDuplicates(l.duplicates)}
501
+ }
502
+
503
+ // summarizeDuplicates says once per namespace and winning document which
504
+ // names were declared again and where. A schema copied beside every WSDL that
505
+ // uses it is one finding, not one per type; the reader fixing it wants the
506
+ // file that is kept, the files that repeat it, and enough names to recognise
507
+ // the copy.
508
+ func summarizeDuplicates(duplicates []duplicate) []string {
509
+ type group struct {
510
+ namespace string
511
+ origin string
512
+ names []string
513
+ paths []string
514
+ }
515
+ groups := map[string]*group{}
516
+ var keys []string
517
+ for _, d := range duplicates {
518
+ key := d.namespace + "\x00" + d.origin
519
+ found := groups[key]
520
+ if found == nil {
521
+ found = &group{namespace: d.namespace, origin: d.origin}
522
+ groups[key] = found
523
+ keys = append(keys, key)
524
+ }
525
+ found.names = append(found.names, d.name)
526
+ found.paths = append(found.paths, d.path)
527
+ }
528
+ sort.Strings(keys)
529
+ var out []string
530
+ for _, key := range keys {
531
+ found := groups[key]
532
+ names := unique(found.names)
533
+ paths := unique(found.paths)
534
+ sort.Strings(paths)
535
+ var b strings.Builder
536
+ b.WriteString(paths[0] + ": duplicate declaration " + names[0])
537
+ if len(names) > 1 {
538
+ fmt.Fprintf(&b, " and %d more", len(names)-1)
539
+ }
540
+ b.WriteString(" in namespace " + found.namespace)
541
+ if len(paths) > 1 {
542
+ b.WriteString(" (also in " + strings.Join(paths[1:], ", ") + ")")
543
+ }
544
+ if len(names) > 1 {
545
+ b.WriteString("; the declarations in " + found.origin + " are used")
546
+ } else {
547
+ b.WriteString("; the declaration in " + found.origin + " is used")
548
+ }
549
+ out = append(out, b.String())
550
+ }
551
+ return out
552
+ }
553
+
554
+ func (l *loader) relative(path string) string {
555
+ rel, err := filepath.Rel(l.root, path)
556
+ if err != nil {
557
+ return filepath.ToSlash(path)
558
+ }
559
+ return filepath.ToSlash(rel)
560
+ }
561
+
562
+ func (ix index) abstractInterface(portType *node) Interface {
563
+ iface := Interface{Name: portType.attr("name"), PortType: portType.attr("name")}
564
+ iface.Operations, iface.Messages = ix.operations(portType, nil)
565
+ return iface
566
+ }
567
+
568
+ func (ix index) boundInterface(port *node) (Interface, bool) {
569
+ bindingRef := resolveQName(port, port.attr("binding"), "")
570
+ binding := ix.lookup(ix.bindings, bindingRef)
571
+ if binding == nil {
572
+ return Interface{}, false
573
+ }
574
+ portTypeRef := resolveQName(binding, binding.attr("type"), bindingRef.ns)
575
+ portType := ix.lookup(ix.portTypes, portTypeRef)
576
+ if portType == nil {
577
+ return Interface{}, false
578
+ }
579
+ version, style := bindingSOAP(binding)
580
+ endpoint := ""
581
+ for _, child := range port.children {
582
+ if child.name.Local == "address" && (child.name.Space == soap11WSDL || child.name.Space == soap12WSDL || strings.Contains(child.name.Space, "soap")) {
583
+ endpoint = child.attr("location")
584
+ if child.name.Space == soap12WSDL {
585
+ version = "1.2"
586
+ } else if version == "" {
587
+ version = "1.1"
588
+ }
589
+ }
590
+ }
591
+ iface := Interface{Name: first(port.attr("name"), binding.attr("name")), Binding: binding.attr("name"), PortType: portType.attr("name"), Endpoint: endpoint, Version: version, Style: style}
592
+ iface.Operations, iface.Messages = ix.operations(portType, binding)
593
+ return iface, true
594
+ }
595
+
596
+ func bindingSOAP(binding *node) (version, style string) {
597
+ for _, child := range binding.children {
598
+ if child.name.Local != "binding" {
599
+ continue
600
+ }
601
+ if child.name.Space == soap12WSDL {
602
+ version = "1.2"
603
+ } else if child.name.Space == soap11WSDL || strings.Contains(child.name.Space, "soap") {
604
+ version = "1.1"
605
+ }
606
+ style = child.attr("style")
607
+ }
608
+ return version, style
609
+ }
610
+
611
+ func (ix index) operations(portType, binding *node) ([]Operation, []Message) {
612
+ bindingOps := map[string]*node{}
613
+ if binding != nil {
614
+ for _, operation := range binding.childrenNamed("operation") {
615
+ bindingOps[operation.attr("name")] = operation
616
+ }
617
+ }
618
+ var operations []Operation
619
+ messageRefs := map[qname]bool{}
620
+ for _, abstract := range portType.childrenNamed("operation") {
621
+ name := abstract.attr("name")
622
+ bound := bindingOps[name]
623
+ operation := Operation{Name: name, Doc: abstract.documentation()}
624
+ if input := abstract.child("input"); input != nil {
625
+ ref := resolveQName(input, input.attr("message"), "")
626
+ operation.Request = ref.local
627
+ messageRefs[ref] = ref.local != ""
628
+ }
629
+ if output := abstract.child("output"); output != nil {
630
+ ref := resolveQName(output, output.attr("message"), "")
631
+ operation.Response = ref.local
632
+ messageRefs[ref] = ref.local != ""
633
+ }
634
+ for _, fault := range abstract.childrenNamed("fault") {
635
+ ref := resolveQName(fault, fault.attr("message"), "")
636
+ faultName := first(fault.attr("name"), ref.local)
637
+ if faultName != "" {
638
+ operation.Faults = append(operation.Faults, faultName)
639
+ }
640
+ messageRefs[ref] = ref.local != ""
641
+ }
642
+ if bound != nil {
643
+ for _, child := range bound.children {
644
+ if child.name.Local == "operation" && (child.name.Space == soap11WSDL || child.name.Space == soap12WSDL || strings.Contains(child.name.Space, "soap")) {
645
+ operation.Action = first(child.attr("soapAction"), child.attr("soapActionURI"))
646
+ }
647
+ }
648
+ operation.Headers = bindingHeaders(bound)
649
+ if operation.Doc == "" {
650
+ operation.Doc = bound.documentation()
651
+ }
652
+ }
653
+ operation.Faults = unique(operation.Faults)
654
+ operations = append(operations, operation)
655
+ }
656
+ var messages []Message
657
+ seen := map[string]bool{}
658
+ var typeQueue []qname
659
+ refs := make([]qname, 0, len(messageRefs))
660
+ for ref, present := range messageRefs {
661
+ if present {
662
+ refs = append(refs, ref)
663
+ }
664
+ }
665
+ sort.Slice(refs, func(i, j int) bool { return qnameLess(refs[i], refs[j]) })
666
+ for _, ref := range refs {
667
+ message, reached := ix.message(ref)
668
+ if message.Name != "" && !seen[message.Name] {
669
+ messages = append(messages, message)
670
+ seen[message.Name] = true
671
+ }
672
+ typeQueue = append(typeQueue, reached...)
673
+ }
674
+ for len(typeQueue) > 0 {
675
+ ref := typeQueue[0]
676
+ typeQueue = typeQueue[1:]
677
+ if ref.local == "" || seen[ref.local] || builtin(ref) {
678
+ continue
679
+ }
680
+ complex := ix.lookup(ix.complexTypes, ref)
681
+ if complex == nil {
682
+ continue
683
+ }
684
+ fields, reached := ix.shape(complex, map[qname]bool{})
685
+ messages = append(messages, Message{Name: ref.local, Fields: fields})
686
+ seen[ref.local] = true
687
+ typeQueue = append(typeQueue, reached...)
688
+ }
689
+ sort.Slice(operations, func(i, j int) bool { return operations[i].Name < operations[j].Name })
690
+ sort.Slice(messages, func(i, j int) bool { return messages[i].Name < messages[j].Name })
691
+ return operations, messages
692
+ }
693
+
694
+ func bindingHeaders(operation *node) []string {
695
+ var headers []string
696
+ for _, direction := range []string{"input", "output"} {
697
+ container := operation.child(direction)
698
+ if container == nil {
699
+ continue
700
+ }
701
+ for _, child := range container.children {
702
+ if child.name.Local != "header" || !(child.name.Space == soap11WSDL || child.name.Space == soap12WSDL || strings.Contains(child.name.Space, "soap")) {
703
+ continue
704
+ }
705
+ message := resolveQName(child, child.attr("message"), "").local
706
+ header := first(child.attr("part"), message)
707
+ if message != "" && child.attr("part") != "" {
708
+ header = message + "." + child.attr("part")
709
+ }
710
+ if header != "" {
711
+ headers = append(headers, header)
712
+ }
713
+ }
714
+ }
715
+ return unique(headers)
716
+ }
717
+
718
+ func (ix index) message(ref qname) (Message, []qname) {
719
+ messageNode := ix.lookup(ix.messages, ref)
720
+ if messageNode == nil {
721
+ return Message{Name: ref.local}, nil
722
+ }
723
+ message := Message{Name: messageNode.attr("name")}
724
+ var reached []qname
725
+ parts := messageNode.childrenNamed("part")
726
+ for _, part := range parts {
727
+ if elementValue := part.attr("element"); elementValue != "" {
728
+ elementRef := resolveQName(part, elementValue, ref.ns)
729
+ element := ix.lookup(ix.elements, elementRef)
730
+ if element != nil && len(parts) == 1 {
731
+ fields, refs := ix.elementShape(element, elementRef.ns)
732
+ if len(fields) > 0 {
733
+ message.Fields = append(message.Fields, fields...)
734
+ reached = append(reached, refs...)
735
+ continue
736
+ }
737
+ }
738
+ typeName := elementRef.local
739
+ if element != nil && element.attr("type") != "" {
740
+ typeRef := resolveQName(element, element.attr("type"), elementRef.ns)
741
+ typeName = typeLabel(typeRef)
742
+ reached = append(reached, typeRef)
743
+ }
744
+ message.Fields = append(message.Fields, Field{Name: first(part.attr("name"), elementRef.local), Type: typeName, Doc: documentationOf(element)})
745
+ continue
746
+ }
747
+ typeRef := resolveQName(part, part.attr("type"), ref.ns)
748
+ message.Fields = append(message.Fields, Field{Name: first(part.attr("name"), "value"), Type: typeLabel(typeRef), Doc: part.documentation()})
749
+ reached = append(reached, typeRef)
750
+ }
751
+ return message, reached
752
+ }
753
+
754
+ func (ix index) elementShape(element *node, fallbackNS string) ([]Field, []qname) {
755
+ if inline := element.child("complexType"); inline != nil {
756
+ return ix.shape(inline, map[qname]bool{})
757
+ }
758
+ typeRef := resolveQName(element, element.attr("type"), fallbackNS)
759
+ if complex := ix.lookup(ix.complexTypes, typeRef); complex != nil {
760
+ fields, refs := ix.shape(complex, map[qname]bool{typeRef: true})
761
+ return fields, append(refs, typeRef)
762
+ }
763
+ return nil, []qname{typeRef}
764
+ }
765
+
766
+ func (ix index) shape(complex *node, resolving map[qname]bool) ([]Field, []qname) {
767
+ var fields []Field
768
+ var refs []qname
769
+ var walk func(*node)
770
+ walk = func(current *node) {
771
+ for _, child := range current.children {
772
+ switch child.name.Local {
773
+ case "extension", "restriction":
774
+ base := resolveQName(child, child.attr("base"), "")
775
+ if !builtin(base) && !resolving[base] {
776
+ if parent := ix.lookup(ix.complexTypes, base); parent != nil {
777
+ resolving[base] = true
778
+ parentFields, parentRefs := ix.shape(parent, resolving)
779
+ delete(resolving, base)
780
+ fields = append(fields, parentFields...)
781
+ refs = append(refs, parentRefs...)
782
+ }
783
+ }
784
+ walk(child)
785
+ case "sequence", "all", "choice", "complexContent", "simpleContent", "group":
786
+ walk(child)
787
+ case "element":
788
+ name := child.attr("name")
789
+ ref := resolveQName(child, child.attr("ref"), "")
790
+ if name == "" {
791
+ name = ref.local
792
+ }
793
+ typeRef := resolveQName(child, child.attr("type"), "")
794
+ if typeRef.local == "" && ref.local != "" {
795
+ if declared := ix.lookup(ix.elements, ref); declared != nil {
796
+ typeRef = resolveQName(declared, declared.attr("type"), ref.ns)
797
+ if name == "" {
798
+ name = declared.attr("name")
799
+ }
800
+ }
801
+ }
802
+ typeName := typeLabel(typeRef)
803
+ if inline := child.child("complexType"); inline != nil {
804
+ typeName = first(name, "object")
805
+ }
806
+ if typeName == "" {
807
+ typeName = "any"
808
+ }
809
+ if max := child.attr("maxOccurs"); max == "unbounded" || (max != "" && max != "1") {
810
+ typeName += "[]"
811
+ }
812
+ fields = append(fields, Field{Name: name, Type: typeName, Doc: child.documentation()})
813
+ refs = append(refs, typeRef)
814
+ case "attribute":
815
+ name := first(child.attr("name"), resolveQName(child, child.attr("ref"), "").local)
816
+ typeRef := resolveQName(child, child.attr("type"), "")
817
+ fields = append(fields, Field{Name: "@" + name, Type: first(typeLabel(typeRef), "string"), Doc: child.documentation()})
818
+ refs = append(refs, typeRef)
819
+ }
820
+ }
821
+ }
822
+ walk(complex)
823
+ return uniqueFields(fields), refs
824
+ }
825
+
826
+ func (ix index) lookup(values map[qname]*node, ref qname) *node {
827
+ if found := values[ref]; found != nil {
828
+ return found
829
+ }
830
+ if ref.local == "" {
831
+ return nil
832
+ }
833
+ var match *node
834
+ for key, value := range values {
835
+ if key.local != ref.local {
836
+ continue
837
+ }
838
+ if match != nil {
839
+ return nil
840
+ }
841
+ match = value
842
+ }
843
+ return match
844
+ }
845
+
846
+ func resolveQName(n *node, value, fallbackNS string) qname {
847
+ value = strings.TrimSpace(value)
848
+ if value == "" {
849
+ return qname{}
850
+ }
851
+ if prefix, local, ok := strings.Cut(value, ":"); ok {
852
+ return qname{ns: n.ns[prefix], local: local}
853
+ }
854
+ namespace := n.ns[""]
855
+ if namespace == "" || namespace == "http://schemas.xmlsoap.org/wsdl/" {
856
+ namespace = fallbackNS
857
+ }
858
+ return qname{ns: namespace, local: value}
859
+ }
860
+
861
+ func typeLabel(ref qname) string {
862
+ if ref.local == "" {
863
+ return ""
864
+ }
865
+ if ref.ns == xsdNS || strings.Contains(strings.ToLower(ref.ns), "xmlschema") {
866
+ switch strings.ToLower(ref.local) {
867
+ case "integer", "int", "long", "short", "byte", "nonnegativeinteger", "positiveinteger":
868
+ return "int"
869
+ case "decimal", "double", "float":
870
+ return "number"
871
+ case "boolean":
872
+ return "boolean"
873
+ case "datetime", "date", "time", "duration":
874
+ return strings.ToLower(ref.local)
875
+ case "base64binary", "hexbinary":
876
+ return "bytes"
877
+ case "anytype", "anysimpletype":
878
+ return "any"
879
+ default:
880
+ return "string"
881
+ }
882
+ }
883
+ return ref.local
884
+ }
885
+
886
+ func builtin(ref qname) bool {
887
+ return ref.local == "" || ref.ns == xsdNS || strings.Contains(strings.ToLower(ref.ns), "xmlschema")
888
+ }
889
+
890
+ func documentationOf(n *node) string {
891
+ if n == nil {
892
+ return ""
893
+ }
894
+ return n.documentation()
895
+ }
896
+
897
+ func uniqueContracts(in []Contract) []Contract {
898
+ seen := map[string]bool{}
899
+ var out []Contract
900
+ for _, contract := range in {
901
+ key := contract.Source + "\x00" + contract.Name
902
+ if !seen[key] {
903
+ out = append(out, contract)
904
+ seen[key] = true
905
+ }
906
+ }
907
+ return out
908
+ }
909
+
910
+ func uniqueFields(in []Field) []Field {
911
+ seen := map[string]bool{}
912
+ var out []Field
913
+ for _, field := range in {
914
+ if field.Name == "" || seen[field.Name] {
915
+ continue
916
+ }
917
+ seen[field.Name] = true
918
+ out = append(out, field)
919
+ }
920
+ return out
921
+ }
922
+
923
+ func qnameLess(left, right qname) bool {
924
+ if left.ns != right.ns {
925
+ return left.ns < right.ns
926
+ }
927
+ return left.local < right.local
928
+ }
929
+
930
+ func unique(in []string) []string {
931
+ seen := map[string]bool{}
932
+ var out []string
933
+ for _, value := range in {
934
+ if value != "" && !seen[value] {
935
+ seen[value] = true
936
+ out = append(out, value)
937
+ }
938
+ }
939
+ return out
940
+ }
941
+
942
+ func first(values ...string) string {
943
+ for _, value := range values {
944
+ if strings.TrimSpace(value) != "" {
945
+ return strings.TrimSpace(value)
946
+ }
947
+ }
948
+ return ""
949
+ }