@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.
- package/README.md +23 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1066 -0
- package/catalog/roundtrip_test.go +203 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +8 -0
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +14 -0
- package/go.sum +20 -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 +22 -10
- package/plugin/describe.go +118 -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 +109 -46
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-django/README.md +39 -17
- package/plugins/extract-django/domain.py +28 -17
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +55 -2
- package/plugins/extract-django/lifecycle.py +2 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- 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-http-clients/describe.go +19 -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/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-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- 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/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/protocol.py +1 -5
- package/portolan.json +3 -2
- package/schema/portolan.schema.json +34 -0
- package/scripts/README.md +18 -12
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +21 -11
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +1 -1
- package/scripts/gen.mjs +118 -115
- 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-git.mjs +77 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/local-api.mjs +71 -4
- package/scripts/local-api.test.mjs +63 -4
- package/scripts/local-discovery.mjs +82 -9
- package/scripts/manifest.mjs +5 -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 +22 -2
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +39 -5
- package/scripts/schema.mjs +29 -0
- package/scripts/warning-policy.mjs +161 -0
- package/scripts/warning-policy.test.mjs +56 -0
- package/src/app/Sidebar.tsx +3 -3
- 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-model.ts +48 -5
- package/src/catalog-validation.ts +9 -0
- 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/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/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/data.ts +25 -7
- package/src/enrich.test.ts +332 -1
- package/src/enrich.ts +206 -3
- package/src/flow/StepDetail.tsx +6 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/index.css +44 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +2 -59
- package/src/lib/catalog-diff.ts +1 -1
- 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/local-api.ts +20 -2
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/warnings.test.ts +54 -0
- package/src/lib/warnings.ts +260 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge.ts +16 -9
- package/src/pages/AggregatePage.tsx +8 -7
- package/src/pages/ContextPage.tsx +6 -5
- package/src/pages/ServicePage.tsx +4 -3
- package/src/pages/Settings.tsx +189 -41
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- 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
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
package extracthttpclients
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"net/url"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"sort"
|
|
8
|
+
"strconv"
|
|
9
|
+
"strings"
|
|
10
|
+
"unicode"
|
|
11
|
+
|
|
12
|
+
"github.com/shortlink-org/portolan/catalog"
|
|
13
|
+
"github.com/shortlink-org/portolan/internal/gohttp"
|
|
14
|
+
"github.com/shortlink-org/portolan/plugin"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func extract(in plugin.Input, opts Options) (plugin.Response, error) {
|
|
18
|
+
result, err := gohttp.Analyze(in.Root)
|
|
19
|
+
if err != nil {
|
|
20
|
+
return plugin.Response{}, err
|
|
21
|
+
}
|
|
22
|
+
b := &plugin.Builder{}
|
|
23
|
+
for _, warning := range result.Warnings {
|
|
24
|
+
b.Warn(in.Root, warning)
|
|
25
|
+
}
|
|
26
|
+
if result.TypedCallGraphError != "" {
|
|
27
|
+
b.Warn(in.Root, "typed call graph unavailable; using syntax fallback: "+result.TypedCallGraphError)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
serviceID := opts.Context + "." + opts.Service
|
|
31
|
+
externals := externalCatalog(result.Contracts, result.Calls, opts)
|
|
32
|
+
// One consumer entry per call identity. A raw call's ID already carries its
|
|
33
|
+
// destination (see gohttp.withCallID), so two `POST /foo` against different
|
|
34
|
+
// hosts land as two entries; a generated client's ID is its contract
|
|
35
|
+
// operation, whose peer is known. Several call sites that share an identity
|
|
36
|
+
// are one dependency: the entry keeps the first site in source order (the
|
|
37
|
+
// analyzer sorts by file and line, so traversal order cannot change it) and
|
|
38
|
+
// carries every site as evidence.
|
|
39
|
+
consumes := make([]catalog.RpcCall, 0, len(result.Calls))
|
|
40
|
+
consumeAt := map[string]int{}
|
|
41
|
+
for _, call := range result.Calls {
|
|
42
|
+
peer, status := peerOf(call, opts)
|
|
43
|
+
if at, seen := consumeAt[call.ID]; seen {
|
|
44
|
+
consumes[at].Evidence = appendEvidence(consumes[at].Evidence, httpCallEvidence(call))
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
consumeAt[call.ID] = len(consumes)
|
|
48
|
+
source := call.Source.String()
|
|
49
|
+
if call.Contract != "" {
|
|
50
|
+
source = call.Contract
|
|
51
|
+
}
|
|
52
|
+
consumes = append(consumes, catalog.RpcCall{
|
|
53
|
+
ID: call.ID, Peer: peer, Status: status, Source: source,
|
|
54
|
+
Note: callNote(call), Destination: call.Destination, Evidence: httpCallEvidence(call),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
sort.Slice(consumes, func(i, j int) bool { return consumes[i].ID < consumes[j].ID })
|
|
58
|
+
|
|
59
|
+
endpointFlows, covered := flowsOfEndpoints(serviceID, opts.Context, result.EndpointFlows, opts)
|
|
60
|
+
coverEndpointDescendants(covered, result.EndpointFlows, result.Flows)
|
|
61
|
+
rootFlows := flowsOfRoots(serviceID, opts.Context, result.RootFlows, opts, covered)
|
|
62
|
+
coverRootDescendants(covered, result.RootFlows, result.Flows)
|
|
63
|
+
flows := append(endpointFlows, rootFlows...)
|
|
64
|
+
flows = append(flows, flowsOfGroupsExcept(serviceID, opts.Context, result.Flows, opts, covered)...)
|
|
65
|
+
if len(result.Calls) == 0 {
|
|
66
|
+
b.Warn(in.Root, "no outbound net/http, oapi-codegen, or SOAP calls were found")
|
|
67
|
+
}
|
|
68
|
+
fragment := catalog.Catalog{
|
|
69
|
+
Contexts: []catalog.BoundedContext{{
|
|
70
|
+
ID: opts.Context, Slug: opts.Context,
|
|
71
|
+
Services: []catalog.Service{{
|
|
72
|
+
ID: serviceID, Slug: opts.Service,
|
|
73
|
+
Provides: []catalog.RpcService{}, Consumes: consumes, Aggregates: []catalog.Aggregate{},
|
|
74
|
+
}},
|
|
75
|
+
}},
|
|
76
|
+
Defs: map[string]catalog.TypeDef{}, Flows: flows, Adrs: []catalog.Adr{}, Externals: externals,
|
|
77
|
+
}
|
|
78
|
+
encoded, err := json.MarshalIndent(fragment, "", " ")
|
|
79
|
+
if err != nil {
|
|
80
|
+
return plugin.Response{}, err
|
|
81
|
+
}
|
|
82
|
+
b.File(firstNonEmpty(opts.Out, "http-clients.json"), string(encoded)+"\n")
|
|
83
|
+
return b.Response(), nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func externalCatalog(contracts []gohttp.Contract, calls []gohttp.Call, opts Options) []catalog.External {
|
|
87
|
+
used := map[string]bool{}
|
|
88
|
+
called := map[string]bool{}
|
|
89
|
+
for _, call := range calls {
|
|
90
|
+
used[call.API] = call.API != ""
|
|
91
|
+
called[call.ID] = true
|
|
92
|
+
}
|
|
93
|
+
byExternal := map[string]*catalog.External{}
|
|
94
|
+
for _, contract := range contracts {
|
|
95
|
+
if !used[contract.API] || opts.Peers[contract.API] != "" {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
external := firstNonEmpty(opts.Externals[contract.API], contract.External)
|
|
99
|
+
if external == "" {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
target := byExternal[external]
|
|
103
|
+
if target == nil {
|
|
104
|
+
target = &catalog.External{
|
|
105
|
+
ID: external, Slug: external, Name: firstNonEmpty(contract.Name, title(external)),
|
|
106
|
+
Summary: contract.Summary, URL: contract.URL, Provides: []catalog.RpcService{},
|
|
107
|
+
}
|
|
108
|
+
byExternal[external] = target
|
|
109
|
+
}
|
|
110
|
+
byInterface := map[string][]catalog.RpcMethod{}
|
|
111
|
+
for _, op := range contract.Operations {
|
|
112
|
+
if !called[op.CallID(contract.API)] {
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
iface := op.Interface(contract.API)
|
|
116
|
+
byInterface[iface] = append(byInterface[iface], catalog.RpcMethod{
|
|
117
|
+
Name: op.ID, HTTP: &catalog.HttpRoute{Method: op.Verb, Path: op.Path},
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
for _, op := range contract.SOAP {
|
|
121
|
+
if !called[op.Interface+"/"+op.ID] {
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
byInterface[op.Interface] = append(byInterface[op.Interface], catalog.RpcMethod{
|
|
125
|
+
Name: op.ID, Request: op.Request, Response: op.Response,
|
|
126
|
+
SOAP: &catalog.SoapRoute{
|
|
127
|
+
Action: op.Action, Version: op.Version, Style: op.Style,
|
|
128
|
+
Endpoint: op.Endpoint, Binding: op.Binding,
|
|
129
|
+
Faults: append([]string(nil), op.Faults...), Headers: append([]string(nil), op.Headers...),
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
var provides []catalog.RpcService
|
|
134
|
+
for id, methods := range byInterface {
|
|
135
|
+
sort.Slice(methods, func(i, j int) bool { return methods[i].Name < methods[j].Name })
|
|
136
|
+
provides = append(provides, catalog.RpcService{ID: id, Source: contract.Source, Methods: methods})
|
|
137
|
+
}
|
|
138
|
+
sort.Slice(provides, func(i, j int) bool { return provides[i].ID < provides[j].ID })
|
|
139
|
+
for _, provided := range provides {
|
|
140
|
+
at := -1
|
|
141
|
+
for i := range target.Provides {
|
|
142
|
+
if target.Provides[i].ID == provided.ID {
|
|
143
|
+
at = i
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if at < 0 {
|
|
148
|
+
target.Provides = append(target.Provides, provided)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
seenMethod := map[string]bool{}
|
|
152
|
+
for _, method := range target.Provides[at].Methods {
|
|
153
|
+
seenMethod[method.Name] = true
|
|
154
|
+
}
|
|
155
|
+
for _, method := range provided.Methods {
|
|
156
|
+
if !seenMethod[method.Name] {
|
|
157
|
+
target.Provides[at].Methods = append(target.Provides[at].Methods, method)
|
|
158
|
+
seenMethod[method.Name] = true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
sort.Slice(target.Provides[at].Methods, func(i, j int) bool {
|
|
162
|
+
return target.Provides[at].Methods[i].Name < target.Provides[at].Methods[j].Name
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
var out []catalog.External
|
|
167
|
+
for _, external := range byExternal {
|
|
168
|
+
sort.Slice(external.Provides, func(i, j int) bool { return external.Provides[i].ID < external.Provides[j].ID })
|
|
169
|
+
out = append(out, *external)
|
|
170
|
+
}
|
|
171
|
+
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
|
172
|
+
return out
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func flowsOf(serviceID, context string, calls []gohttp.Call, opts Options) []catalog.Flow {
|
|
176
|
+
byFunction := map[string][]gohttp.Call{}
|
|
177
|
+
for _, call := range calls {
|
|
178
|
+
byFunction[call.Function] = append(byFunction[call.Function], call)
|
|
179
|
+
}
|
|
180
|
+
functions := make([]string, 0, len(byFunction))
|
|
181
|
+
for function := range byFunction {
|
|
182
|
+
functions = append(functions, function)
|
|
183
|
+
}
|
|
184
|
+
sort.Strings(functions)
|
|
185
|
+
groups := make([]gohttp.FlowGroup, 0, len(functions))
|
|
186
|
+
for _, function := range functions {
|
|
187
|
+
groups = append(groups, gohttp.FlowGroup{Function: function, Calls: byFunction[function]})
|
|
188
|
+
}
|
|
189
|
+
return flowsOfGroups(serviceID, context, groups, opts)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func flowsOfGroups(serviceID, context string, groups []gohttp.FlowGroup, opts Options) []catalog.Flow {
|
|
193
|
+
return flowsOfGroupsExcept(serviceID, context, groups, opts, nil)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func flowsOfGroupsExcept(serviceID, context string, groups []gohttp.FlowGroup, opts Options, excluded map[string]bool) []catalog.Flow {
|
|
197
|
+
var flows []catalog.Flow
|
|
198
|
+
for _, flowGroup := range groups {
|
|
199
|
+
if excluded[flowGroup.Function] {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
function := flowGroup.Function
|
|
203
|
+
group := flowGroup.Calls
|
|
204
|
+
if len(group) == 0 {
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
participants := []catalog.Participant{{ID: serviceID, Kind: catalog.ParticipantService, Context: &context}}
|
|
208
|
+
participantSeen := map[string]bool{serviceID: true}
|
|
209
|
+
callSteps := make([]*catalog.Step, 0, len(group))
|
|
210
|
+
for index, call := range group {
|
|
211
|
+
peer, status := peerOf(call, opts)
|
|
212
|
+
participant := participantOf(peer, call, opts)
|
|
213
|
+
if !participantSeen[participant.ID] {
|
|
214
|
+
participants = append(participants, participant)
|
|
215
|
+
participantSeen[participant.ID] = true
|
|
216
|
+
}
|
|
217
|
+
note := callNote(call)
|
|
218
|
+
if len(call.Conditions) > 0 {
|
|
219
|
+
condition := "when " + strings.Join(uniqueStrings(call.Conditions), " and ")
|
|
220
|
+
if note == "" {
|
|
221
|
+
note = condition
|
|
222
|
+
} else {
|
|
223
|
+
note += "; " + condition
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if len(call.Chain) > 1 {
|
|
227
|
+
chain := "via " + strings.Join(call.Chain, " → ")
|
|
228
|
+
if note == "" {
|
|
229
|
+
note = chain
|
|
230
|
+
} else {
|
|
231
|
+
note += "; " + chain
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
callSteps = append(callSteps, &catalog.Step{
|
|
235
|
+
Type: "step", ID: "s" + strconv.Itoa(index+1), From: serviceID, To: participant.ID,
|
|
236
|
+
Kind: catalog.StepRPC, Ref: call.ID, Label: callLabel(call), Status: status,
|
|
237
|
+
Note: note, Line: call.Source.String(), Destination: call.Destination, Evidence: httpCallEvidence(call),
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
steps := catalog.FlowNodes{}
|
|
241
|
+
if left, right, ok := complementaryCalls(group); ok && len(group) == 2 {
|
|
242
|
+
steps = append(steps, &catalog.Alt{
|
|
243
|
+
Type: "alt", ID: "alt1",
|
|
244
|
+
Branches: []catalog.AltBranch{
|
|
245
|
+
{Title: left, Steps: catalog.FlowNodes{callSteps[0]}},
|
|
246
|
+
{Title: right, Steps: catalog.FlowNodes{callSteps[1]}},
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
} else {
|
|
250
|
+
for _, step := range callSteps {
|
|
251
|
+
steps = append(steps, step)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
name := flowName(function)
|
|
255
|
+
flowSource := flowGroup.Source.String()
|
|
256
|
+
if flowSource == "" {
|
|
257
|
+
flowSource = group[0].Source.String()
|
|
258
|
+
}
|
|
259
|
+
flows = append(flows, catalog.Flow{
|
|
260
|
+
ID: "flow." + serviceID + ".http-client." + slug(function),
|
|
261
|
+
Slug: serviceID + "-http-client-" + slug(function),
|
|
262
|
+
Name: name + " → outbound APIs",
|
|
263
|
+
Summary: standaloneFlowSummary(function, flowGroup.Callers),
|
|
264
|
+
Source: flowSource,
|
|
265
|
+
Trigger: &catalog.FlowTrigger{
|
|
266
|
+
Kind: "unproven", Label: "No execution root proven", Confidence: "low",
|
|
267
|
+
},
|
|
268
|
+
EntryPoint: function, Owner: context,
|
|
269
|
+
Participants: participants, Steps: steps,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
return flows
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
func flowsOfRoots(serviceID, context string, roots []gohttp.RootFlow, opts Options, covered map[string]bool) []catalog.Flow {
|
|
276
|
+
flows := make([]catalog.Flow, 0, len(roots))
|
|
277
|
+
for _, root := range roots {
|
|
278
|
+
for _, function := range root.Covered {
|
|
279
|
+
covered[function] = true
|
|
280
|
+
}
|
|
281
|
+
actorID := serviceID + ".api-client"
|
|
282
|
+
actorLabel := "API client"
|
|
283
|
+
firstKind := catalog.StepRPC
|
|
284
|
+
firstLabel := strings.TrimSpace(root.Method + " " + root.Path)
|
|
285
|
+
if root.Kind == gohttp.RootCallback {
|
|
286
|
+
actorID = serviceID + ".callback-sender"
|
|
287
|
+
actorLabel = "Callback sender"
|
|
288
|
+
}
|
|
289
|
+
if root.Kind == gohttp.RootStartup {
|
|
290
|
+
actorID = serviceID + ".process"
|
|
291
|
+
actorLabel = "Process startup"
|
|
292
|
+
firstKind = catalog.StepCall
|
|
293
|
+
firstLabel = root.Label
|
|
294
|
+
}
|
|
295
|
+
if root.Kind == gohttp.RootScheduled {
|
|
296
|
+
actorID = serviceID + ".scheduler"
|
|
297
|
+
actorLabel = "Scheduler"
|
|
298
|
+
firstKind = catalog.StepCall
|
|
299
|
+
firstLabel = root.Label
|
|
300
|
+
}
|
|
301
|
+
participants := []catalog.Participant{
|
|
302
|
+
{ID: actorID, Kind: catalog.ParticipantActor, Label: actorLabel},
|
|
303
|
+
{ID: serviceID, Kind: catalog.ParticipantService, Context: &context},
|
|
304
|
+
}
|
|
305
|
+
participantSeen := map[string]bool{actorID: true, serviceID: true}
|
|
306
|
+
steps := catalog.FlowNodes{&catalog.Step{
|
|
307
|
+
Type: "step", ID: "s1", From: actorID, To: serviceID,
|
|
308
|
+
Kind: firstKind, Label: firstLabel, Status: catalog.StatusDeclared,
|
|
309
|
+
Line: root.Source.String(),
|
|
310
|
+
}}
|
|
311
|
+
for index, call := range root.Calls {
|
|
312
|
+
peer, status := peerOf(call, opts)
|
|
313
|
+
participant := participantOf(peer, call, opts)
|
|
314
|
+
if !participantSeen[participant.ID] {
|
|
315
|
+
participants = append(participants, participant)
|
|
316
|
+
participantSeen[participant.ID] = true
|
|
317
|
+
}
|
|
318
|
+
note := callNote(call)
|
|
319
|
+
if len(call.Conditions) > 0 {
|
|
320
|
+
condition := "when " + strings.Join(uniqueStrings(call.Conditions), " and ")
|
|
321
|
+
if note == "" {
|
|
322
|
+
note = condition
|
|
323
|
+
} else {
|
|
324
|
+
note += "; " + condition
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if len(call.Chain) > 1 {
|
|
328
|
+
chain := "via " + strings.Join(call.Chain, " → ")
|
|
329
|
+
if note == "" {
|
|
330
|
+
note = chain
|
|
331
|
+
} else {
|
|
332
|
+
note += "; " + chain
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
steps = append(steps, &catalog.Step{
|
|
336
|
+
Type: "step", ID: "s" + strconv.Itoa(index+2), From: serviceID, To: participant.ID,
|
|
337
|
+
Kind: catalog.StepRPC, Ref: call.ID, Label: callLabel(call), Status: status,
|
|
338
|
+
Note: note, Line: call.Source.String(), Destination: call.Destination, Evidence: httpCallEvidence(call),
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
reached := append([]string{}, root.Covered...)
|
|
342
|
+
if root.Handler != "" {
|
|
343
|
+
reached = append(reached, root.Handler)
|
|
344
|
+
}
|
|
345
|
+
markLastStepReaches(steps, reached)
|
|
346
|
+
summary := "Source-backed execution path from a concrete root to outbound APIs."
|
|
347
|
+
switch root.Kind {
|
|
348
|
+
case gohttp.RootCallback:
|
|
349
|
+
summary = "Source-backed callback path from the inbound webhook to outbound APIs."
|
|
350
|
+
case gohttp.RootStartup:
|
|
351
|
+
summary = "Source-backed startup path executed while the process is assembled."
|
|
352
|
+
case gohttp.RootScheduled:
|
|
353
|
+
summary = "Source-backed scheduled path from timer registration to outbound APIs."
|
|
354
|
+
case gohttp.RootHTTP:
|
|
355
|
+
summary = "Source-backed request path from the inbound endpoint to outbound APIs."
|
|
356
|
+
}
|
|
357
|
+
rootIdentity := root.Label
|
|
358
|
+
if root.Kind == gohttp.RootStartup {
|
|
359
|
+
rootIdentity = strings.TrimPrefix(rootIdentity, "Startup → ")
|
|
360
|
+
}
|
|
361
|
+
rootSlug := slug(string(root.Kind) + "-" + rootIdentity)
|
|
362
|
+
flows = append(flows, catalog.Flow{
|
|
363
|
+
ID: "flow." + serviceID + ".root." + rootSlug,
|
|
364
|
+
Slug: serviceID + "-root-" + rootSlug,
|
|
365
|
+
Name: root.Label + " → outbound APIs", Summary: summary,
|
|
366
|
+
Source: root.Source.String(),
|
|
367
|
+
Trigger: &catalog.FlowTrigger{
|
|
368
|
+
Kind: string(root.Kind), Label: root.Label, Confidence: root.Confidence,
|
|
369
|
+
},
|
|
370
|
+
Owner: context, Participants: participants, Steps: steps,
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
return flows
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func standaloneFlowSummary(function string, callers []string) string {
|
|
377
|
+
base := "Source-backed outbound calls made by '" + function + "'."
|
|
378
|
+
if len(callers) == 0 {
|
|
379
|
+
return base + " No source caller was found; this may be a public entrypoint, a background task, or unreachable code."
|
|
380
|
+
}
|
|
381
|
+
displays := make([]string, 0, len(callers))
|
|
382
|
+
for _, caller := range callers {
|
|
383
|
+
displays = append(displays, flowName(caller))
|
|
384
|
+
}
|
|
385
|
+
return base + " Source callers were found (" + strings.Join(uniqueStrings(displays), ", ") + "), but no inbound or asynchronous root was proven."
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
func flowsOfEndpoints(serviceID, context string, endpoints []gohttp.EndpointFlow, opts Options) ([]catalog.Flow, map[string]bool) {
|
|
389
|
+
covered := map[string]bool{}
|
|
390
|
+
flows := make([]catalog.Flow, 0, len(endpoints))
|
|
391
|
+
for _, endpoint := range endpoints {
|
|
392
|
+
actorID := serviceID + ".api-client"
|
|
393
|
+
participants := []catalog.Participant{
|
|
394
|
+
{ID: actorID, Kind: catalog.ParticipantActor, Label: "API client"},
|
|
395
|
+
{ID: serviceID, Kind: catalog.ParticipantService, Context: &context},
|
|
396
|
+
}
|
|
397
|
+
participantSeen := map[string]bool{actorID: true, serviceID: true}
|
|
398
|
+
branches := make([]catalog.AltBranch, 0, len(endpoint.Branches))
|
|
399
|
+
stepIndex := 2
|
|
400
|
+
for _, branch := range endpoint.Branches {
|
|
401
|
+
covered[branch.Function] = true
|
|
402
|
+
steps := catalog.FlowNodes{}
|
|
403
|
+
for _, call := range branch.Calls {
|
|
404
|
+
peer, status := peerOf(call, opts)
|
|
405
|
+
participant := participantOf(peer, call, opts)
|
|
406
|
+
if !participantSeen[participant.ID] {
|
|
407
|
+
participants = append(participants, participant)
|
|
408
|
+
participantSeen[participant.ID] = true
|
|
409
|
+
}
|
|
410
|
+
note := callNote(call)
|
|
411
|
+
if len(call.Chain) > 1 {
|
|
412
|
+
chain := "via " + strings.Join(call.Chain, " → ")
|
|
413
|
+
if note == "" {
|
|
414
|
+
note = chain
|
|
415
|
+
} else {
|
|
416
|
+
note += "; " + chain
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
steps = append(steps, &catalog.Step{
|
|
420
|
+
Type: "step", ID: "s" + strconv.Itoa(stepIndex), From: serviceID, To: participant.ID,
|
|
421
|
+
Kind: catalog.StepRPC, Ref: call.ID, Label: callLabel(call), Status: status,
|
|
422
|
+
Note: note, Line: call.Source.String(), Destination: call.Destination, Evidence: httpCallEvidence(call),
|
|
423
|
+
})
|
|
424
|
+
stepIndex++
|
|
425
|
+
}
|
|
426
|
+
if len(steps) == 0 {
|
|
427
|
+
provider := filepath.Base(branch.Provider)
|
|
428
|
+
steps = append(steps, &catalog.Step{
|
|
429
|
+
Type: "step", ID: "s" + strconv.Itoa(stepIndex), From: serviceID, To: serviceID,
|
|
430
|
+
Kind: catalog.StepCall, Label: title(provider) + " " + title(branch.Operation),
|
|
431
|
+
Status: catalog.StatusDeclared,
|
|
432
|
+
Note: "provider implementation is source-backed; its outbound transport was not resolved in the analyzed source",
|
|
433
|
+
Line: branch.Source.String(),
|
|
434
|
+
})
|
|
435
|
+
stepIndex++
|
|
436
|
+
}
|
|
437
|
+
markLastStepReaches(steps, []string{branch.Function})
|
|
438
|
+
branches = append(branches, catalog.AltBranch{
|
|
439
|
+
Title: "connector = \"" + branch.Condition + "\"", Steps: steps,
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
steps := catalog.FlowNodes{&catalog.Step{
|
|
443
|
+
Type: "step", ID: "s1", From: actorID, To: serviceID,
|
|
444
|
+
Kind: catalog.StepRPC, Label: strings.TrimSpace(endpoint.Method + " " + endpoint.Path),
|
|
445
|
+
Status: catalog.StatusDeclared, Line: endpoint.Source.String(),
|
|
446
|
+
}}
|
|
447
|
+
if len(branches) == 1 {
|
|
448
|
+
steps = append(steps, branches[0].Steps...)
|
|
449
|
+
} else {
|
|
450
|
+
steps = append(steps, &catalog.Alt{Type: "alt", ID: "providers", Branches: branches})
|
|
451
|
+
}
|
|
452
|
+
name := strings.TrimSpace(endpoint.Method + " " + endpoint.Path)
|
|
453
|
+
flows = append(flows, catalog.Flow{
|
|
454
|
+
ID: "flow." + serviceID + ".endpoint." + slug(name),
|
|
455
|
+
Slug: serviceID + "-endpoint-" + slug(name),
|
|
456
|
+
Name: name + " → provider APIs",
|
|
457
|
+
Summary: "Source-backed request path from the inbound endpoint through provider selection to outbound APIs.",
|
|
458
|
+
Source: endpoint.Source.String(),
|
|
459
|
+
Trigger: &catalog.FlowTrigger{
|
|
460
|
+
Kind: "http", Label: name, Confidence: "high",
|
|
461
|
+
},
|
|
462
|
+
Owner: context, Participants: participants, Steps: steps,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
return flows, covered
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func markLastStepReaches(nodes catalog.FlowNodes, reaches []string) {
|
|
469
|
+
for index := len(nodes) - 1; index >= 0; index-- {
|
|
470
|
+
switch node := nodes[index].(type) {
|
|
471
|
+
case *catalog.Step:
|
|
472
|
+
node.Reaches = uniqueStrings(reaches)
|
|
473
|
+
return
|
|
474
|
+
case *catalog.Alt:
|
|
475
|
+
for branch := range node.Branches {
|
|
476
|
+
markLastStepReaches(node.Branches[branch].Steps, reaches)
|
|
477
|
+
}
|
|
478
|
+
return
|
|
479
|
+
case *catalog.Parallel:
|
|
480
|
+
for branch := range node.Branches {
|
|
481
|
+
markLastStepReaches(node.Branches[branch], reaches)
|
|
482
|
+
}
|
|
483
|
+
return
|
|
484
|
+
case *catalog.Loop:
|
|
485
|
+
markLastStepReaches(node.Steps, reaches)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
func coverRootDescendants(covered map[string]bool, roots []gohttp.RootFlow, groups []gohttp.FlowGroup) {
|
|
492
|
+
for _, root := range roots {
|
|
493
|
+
callKeys := map[string]bool{}
|
|
494
|
+
for _, call := range root.Calls {
|
|
495
|
+
callKeys[endpointCallKey(call)] = true
|
|
496
|
+
}
|
|
497
|
+
for _, group := range groups {
|
|
498
|
+
if covered[group.Function] || len(group.Calls) == 0 {
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
allCallsCovered := true
|
|
502
|
+
for _, call := range group.Calls {
|
|
503
|
+
if !callKeys[endpointCallKey(call)] {
|
|
504
|
+
allCallsCovered = false
|
|
505
|
+
break
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if allCallsCovered {
|
|
509
|
+
covered[group.Function] = true
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
func coverEndpointDescendants(covered map[string]bool, endpoints []gohttp.EndpointFlow, groups []gohttp.FlowGroup) {
|
|
516
|
+
for _, endpoint := range endpoints {
|
|
517
|
+
// Typed dispatch can lift a union of calls from every provider branch
|
|
518
|
+
// into a shared coordinator such as ActionFlow. Compare those callers
|
|
519
|
+
// with the whole endpoint, not with one branch at a time, or the same
|
|
520
|
+
// source-backed calls reappear as a misleading standalone flow.
|
|
521
|
+
callKeys := map[string]bool{}
|
|
522
|
+
for _, branch := range endpoint.Branches {
|
|
523
|
+
for _, call := range branch.Calls {
|
|
524
|
+
callKeys[endpointCallKey(call)] = true
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for _, group := range groups {
|
|
528
|
+
if covered[group.Function] || len(group.Calls) == 0 {
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
// Exact source location plus operation id is stronger evidence than
|
|
532
|
+
// a display name in the reconstructed call chain. Wrappers reached
|
|
533
|
+
// through interfaces and promoted methods can omit that name while
|
|
534
|
+
// still contributing precisely the same source-backed calls.
|
|
535
|
+
allCallsCovered := true
|
|
536
|
+
for _, call := range group.Calls {
|
|
537
|
+
if !callKeys[endpointCallKey(call)] {
|
|
538
|
+
allCallsCovered = false
|
|
539
|
+
break
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if allCallsCovered {
|
|
543
|
+
covered[group.Function] = true
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
func endpointCallKey(call gohttp.Call) string {
|
|
550
|
+
return call.Source.String() + "\x00" + call.ID
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
func flowName(function string) string {
|
|
554
|
+
directory, display, qualified := strings.Cut(function, ":")
|
|
555
|
+
if !qualified {
|
|
556
|
+
display = function
|
|
557
|
+
}
|
|
558
|
+
receiver, method, methodCall := strings.Cut(display, ".")
|
|
559
|
+
if !methodCall || (receiver != "Client" && receiver != "Connector") {
|
|
560
|
+
return title(strings.ReplaceAll(display, ".", " "))
|
|
561
|
+
}
|
|
562
|
+
parts := strings.Split(filepath.ToSlash(directory), "/")
|
|
563
|
+
for index := len(parts) - 1; index >= 0; index-- {
|
|
564
|
+
part := parts[index]
|
|
565
|
+
if part == "" || part == "client" || part == "connector" || part == "adapter" {
|
|
566
|
+
continue
|
|
567
|
+
}
|
|
568
|
+
return title(part) + " " + title(method)
|
|
569
|
+
}
|
|
570
|
+
return title(strings.ReplaceAll(display, ".", " "))
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
func complementaryCalls(calls []gohttp.Call) (string, string, bool) {
|
|
574
|
+
if len(calls) != 2 {
|
|
575
|
+
return "", "", false
|
|
576
|
+
}
|
|
577
|
+
for _, left := range calls[0].Conditions {
|
|
578
|
+
for _, right := range calls[1].Conditions {
|
|
579
|
+
if right == "not ("+left+")" || left == "not ("+right+")" {
|
|
580
|
+
return left, right, true
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return "", "", false
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func participantOf(peer string, call gohttp.Call, opts Options) catalog.Participant {
|
|
588
|
+
if call.API != "" {
|
|
589
|
+
if service := opts.Peers[call.API]; service != "" {
|
|
590
|
+
context, _, _ := strings.Cut(service, ".")
|
|
591
|
+
return catalog.Participant{ID: service, Kind: catalog.ParticipantService, Context: &context}
|
|
592
|
+
}
|
|
593
|
+
if external := firstNonEmpty(opts.Externals[call.API], call.External); external != "" {
|
|
594
|
+
return catalog.Participant{ID: external, Kind: catalog.ParticipantExternal}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return catalog.Participant{ID: peer, Kind: catalog.ParticipantUnknown, Label: rawPeerLabel(call)}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
func peerOf(call gohttp.Call, opts Options) (string, catalog.Status) {
|
|
601
|
+
if call.API != "" {
|
|
602
|
+
if peer := opts.Peers[call.API]; peer != "" {
|
|
603
|
+
return peer, catalog.StatusDeclared
|
|
604
|
+
}
|
|
605
|
+
if peer := firstNonEmpty(opts.Externals[call.API], call.External); peer != "" {
|
|
606
|
+
return peer, catalog.StatusDeclared
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return rawPeer(call), catalog.StatusUnresolved
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
func rawPeer(call gohttp.Call) string {
|
|
613
|
+
if parsed, err := url.Parse(call.Endpoint); err == nil && parsed.Hostname() != "" {
|
|
614
|
+
return slug(parsed.Hostname())
|
|
615
|
+
}
|
|
616
|
+
if call.Protocol == "SOAP" {
|
|
617
|
+
if parsed, err := url.Parse(call.Action); err == nil && parsed.Hostname() != "" {
|
|
618
|
+
return slug(parsed.Hostname())
|
|
619
|
+
}
|
|
620
|
+
return "soap-peer"
|
|
621
|
+
}
|
|
622
|
+
return "http-peer"
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
func rawPeerLabel(call gohttp.Call) string {
|
|
626
|
+
if call.Endpoint != "" {
|
|
627
|
+
return call.Endpoint
|
|
628
|
+
}
|
|
629
|
+
if call.Protocol == "SOAP" {
|
|
630
|
+
if call.Action != "" {
|
|
631
|
+
if parsed, err := url.Parse(call.Action); err == nil && parsed.Hostname() != "" {
|
|
632
|
+
return parsed.Hostname()
|
|
633
|
+
}
|
|
634
|
+
return call.Action
|
|
635
|
+
}
|
|
636
|
+
return "SOAP endpoint"
|
|
637
|
+
}
|
|
638
|
+
return "HTTP endpoint"
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
func callLabel(call gohttp.Call) string {
|
|
642
|
+
if call.Protocol == "SOAP" {
|
|
643
|
+
return "SOAP " + call.Action
|
|
644
|
+
}
|
|
645
|
+
return strings.TrimSpace(call.Method + " " + call.Path)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
func callNote(call gohttp.Call) string {
|
|
649
|
+
var parts []string
|
|
650
|
+
if call.Endpoint != "" && call.Endpoint != call.Path {
|
|
651
|
+
parts = append(parts, "endpoint '"+call.Endpoint+"'")
|
|
652
|
+
}
|
|
653
|
+
if call.Request != "" {
|
|
654
|
+
parts = append(parts, "request '"+call.Request+"'")
|
|
655
|
+
}
|
|
656
|
+
if call.Response != "" {
|
|
657
|
+
parts = append(parts, "response '"+call.Response+"'")
|
|
658
|
+
}
|
|
659
|
+
if len(call.URLTrace) > 1 {
|
|
660
|
+
parts = append(parts, "URL source "+strings.Join(call.URLTrace, " → "))
|
|
661
|
+
}
|
|
662
|
+
return strings.Join(parts, "; ")
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
func slug(value string) string {
|
|
666
|
+
var out strings.Builder
|
|
667
|
+
dash := false
|
|
668
|
+
for _, r := range strings.ToLower(value) {
|
|
669
|
+
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
|
670
|
+
out.WriteRune(r)
|
|
671
|
+
dash = false
|
|
672
|
+
} else if out.Len() > 0 && !dash {
|
|
673
|
+
out.WriteByte('-')
|
|
674
|
+
dash = true
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return strings.Trim(out.String(), "-")
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
func title(value string) string {
|
|
681
|
+
var words []string
|
|
682
|
+
for _, word := range strings.FieldsFunc(value, func(r rune) bool { return r == '-' || r == '_' || unicode.IsSpace(r) }) {
|
|
683
|
+
runes := []rune(word)
|
|
684
|
+
if len(runes) > 0 {
|
|
685
|
+
runes[0] = unicode.ToUpper(runes[0])
|
|
686
|
+
}
|
|
687
|
+
words = append(words, string(runes))
|
|
688
|
+
}
|
|
689
|
+
return strings.Join(words, " ")
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
func firstNonEmpty(values ...string) string {
|
|
693
|
+
for _, value := range values {
|
|
694
|
+
if value != "" {
|
|
695
|
+
return value
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return ""
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
func uniqueStrings(values []string) []string {
|
|
702
|
+
seen := map[string]bool{}
|
|
703
|
+
out := make([]string, 0, len(values))
|
|
704
|
+
for _, value := range values {
|
|
705
|
+
if value != "" && !seen[value] {
|
|
706
|
+
seen[value] = true
|
|
707
|
+
out = append(out, value)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return out
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// appendEvidence adds the evidence of another call site to an aggregated
|
|
714
|
+
// consumer entry, skipping items it already carries.
|
|
715
|
+
func appendEvidence(existing, more []catalog.RelationEvidence) []catalog.RelationEvidence {
|
|
716
|
+
for _, item := range more {
|
|
717
|
+
duplicate := false
|
|
718
|
+
for _, have := range existing {
|
|
719
|
+
if have.Kind == item.Kind && have.Rule == item.Rule && have.Source == item.Source && have.Symbol == item.Symbol {
|
|
720
|
+
duplicate = true
|
|
721
|
+
break
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if !duplicate {
|
|
725
|
+
existing = append(existing, item)
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return existing
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
func httpCallEvidence(call gohttp.Call) []catalog.RelationEvidence {
|
|
732
|
+
out := []catalog.RelationEvidence{{Kind: "call-site", Rule: "http-expression", Source: call.Source.String(), Symbol: call.Method + " " + call.Path}}
|
|
733
|
+
for _, function := range call.Chain {
|
|
734
|
+
out = append(out, catalog.RelationEvidence{Kind: "function", Rule: "analyzed-call-path", Symbol: function})
|
|
735
|
+
}
|
|
736
|
+
if call.Contract != "" {
|
|
737
|
+
out = append(out, catalog.RelationEvidence{Kind: "contract", Rule: "client-contract", Source: call.Contract, Symbol: call.ID})
|
|
738
|
+
}
|
|
739
|
+
return out
|
|
740
|
+
}
|