@shortlink-org/portolan 0.1.0 → 0.2.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 +69 -205
- package/cli/init.mjs +285 -0
- package/cli/init.test.mjs +158 -0
- package/cli/portolan.mjs +37 -73
- package/index.html +19 -0
- package/package.json +5 -10
- package/plugins/README.md +66 -27
- package/plugins/portolan-go.wasm +0 -0
- package/portolan.json +52 -73
- package/public/landing/cats/bug-hunter.webp +0 -0
- package/public/landing/cats/compass-nap.webp +0 -0
- package/public/landing/cats/diagrammer.webp +0 -0
- package/public/landing/cats/docs-reader.webp +0 -0
- package/public/landing/cats/laptop.webp +0 -0
- package/public/landing/cats/map-inspector.webp +0 -0
- package/public/landing/cats/server-break.webp +0 -0
- package/public/landing/cats/star-mapper.webp +0 -0
- package/public/landing/cats/system-builder.webp +0 -0
- package/public/landing/cats/thread-tangle.webp +0 -0
- package/public/og.png +0 -0
- package/public/readme-header.webp +0 -0
- package/schema/portolan.schema.json +180 -0
- package/scripts/builtin-plugins.mjs +14 -0
- package/scripts/delivery-presets.mjs +353 -0
- package/scripts/gen.mjs +37 -4
- package/scripts/history.mjs +126 -0
- package/scripts/history.test.mjs +110 -0
- package/scripts/host-plugins/fetch-bsr.mjs +351 -0
- package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
- package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
- package/scripts/host-plugins/fetch-csr.mjs +386 -0
- package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
- package/scripts/host-plugins/fetch-git.mjs +379 -0
- package/scripts/host-plugins/fetch-git.test.mjs +210 -0
- package/scripts/local-api.mjs +22 -4
- package/scripts/local-api.test.mjs +84 -2
- package/scripts/manifest.mjs +3 -2
- package/scripts/package-smoke.mjs +6 -2
- package/scripts/plugin-host.mjs +77 -18
- package/scripts/plugin-host.test.mjs +50 -1
- package/scripts/plugin-wasm-worker.mjs +6 -3
- package/scripts/run-builtin.mjs +6 -26
- package/scripts/schema.mjs +6 -1
- package/src/app/App.tsx +24 -429
- package/src/app/Breadcrumbs.test.ts +7 -0
- package/src/app/Breadcrumbs.tsx +15 -2
- package/src/app/CatalogApp.tsx +436 -0
- package/src/app/Sidebar.tsx +0 -1
- package/src/chat/ChatPanel.tsx +98 -41
- package/src/chat/Composer.tsx +4 -2
- package/src/chat/Conversation.tsx +79 -15
- package/src/chat/Header.tsx +43 -9
- package/src/chat/Starter.tsx +64 -3
- package/src/chat/Steps.tsx +1 -1
- package/src/chat/flags.test.ts +51 -14
- package/src/chat/flags.ts +8 -2
- package/src/chat/page-context.test.ts +51 -0
- package/src/chat/page-context.ts +128 -0
- package/src/chat/prompt.test.ts +49 -1
- package/src/chat/prompt.ts +42 -1
- package/src/chat/tools.ts +7 -2
- package/src/chat/transport.ts +16 -4
- package/src/components/ProblemRow.tsx +182 -0
- package/src/data.ts +20 -5
- package/src/er/ErCanvas.tsx +1 -1
- package/src/graph/DependencyGraph.tsx +10 -1
- package/src/graph/FocusedEventGraph.tsx +1 -1
- package/src/graph/GraphToolbar.tsx +2 -4
- package/src/index.css +220 -2
- package/src/landing/DraggableReveal.tsx +168 -0
- package/src/landing/EstateGraph.tsx +28 -0
- package/src/landing/FlowPlayback.tsx +282 -0
- package/src/landing/HeroMap.tsx +128 -0
- package/src/landing/LandingPage.tsx +757 -0
- package/src/landing/ProductFrame.tsx +68 -0
- package/src/landing/ProductTour.tsx +371 -0
- package/src/landing/catalog.ts +25 -0
- package/src/landing/motion.tsx +52 -0
- package/src/lib/all-problems.ts +28 -0
- package/src/lib/local-api.ts +35 -1
- package/src/lib/setup-info.test.ts +8 -0
- package/src/lib/setup-info.ts +3 -2
- package/src/lib/source-code.ts +2 -1
- package/src/likec4/C4View.tsx +5 -0
- package/src/likec4/InteractiveView.tsx +5 -1
- package/src/map/ContextMapGraph.tsx +47 -20
- package/src/pages/ContextMap.tsx +5 -1
- package/src/pages/FlowDetail.tsx +24 -7
- package/src/pages/Overview.tsx +68 -12
- package/src/pages/Problems.tsx +6 -193
- package/src/pages/Settings.tsx +133 -108
- package/src/pages/settings/DeliverySettings.tsx +229 -0
- package/src/pages/settings/PreferencesSettings.tsx +101 -0
- package/src/routes.test.ts +20 -0
- package/src/routes.ts +13 -1
- package/vite.config.ts +12 -8
- package/catalog/enum_test.go +0 -46
- package/catalog/model.go +0 -932
- package/catalog/roundtrip_test.go +0 -185
- package/catalog/via_test.go +0 -38
- package/go.mod +0 -14
- package/go.sum +0 -14
- package/internal/commands/cargo.go +0 -154
- package/internal/commands/commands.go +0 -143
- package/internal/commands/commands_test.go +0 -323
- package/internal/commands/gradle.go +0 -77
- package/internal/commands/justfile.go +0 -82
- package/internal/commands/makefile.go +0 -106
- package/internal/commands/maven.go +0 -93
- package/internal/commands/packagejson.go +0 -159
- package/internal/commands/pyproject.go +0 -169
- package/internal/commands/taskfile.go +0 -101
- package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
- package/internal/commands/testdata/estate/Makefile +0 -33
- package/internal/commands/testdata/estate/Taskfile.yml +0 -21
- package/internal/commands/testdata/estate/build.gradle.kts +0 -21
- package/internal/commands/testdata/estate/justfile +0 -20
- package/internal/commands/testdata/estate/package.json +0 -13
- package/internal/commands/testdata/estate/pom.xml +0 -29
- package/internal/commands/testdata/estate/pyproject.toml +0 -27
- package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
- package/internal/commands/testdata/golden/commands.json +0 -305
- package/internal/gohttp/analyze.go +0 -2500
- package/internal/gohttp/endpoints.go +0 -1067
- package/internal/gohttp/roots.go +0 -320
- package/internal/gohttp/typed.go +0 -143
- package/internal/goscan/constants.go +0 -85
- package/internal/goscan/goscan_test.go +0 -227
- package/internal/goscan/names.go +0 -52
- package/internal/goscan/parse_test.go +0 -11
- package/internal/goscan/source.go +0 -36
- package/internal/goscan/tree.go +0 -155
- package/internal/goscan/types.go +0 -99
- package/internal/wsdl/ids.go +0 -127
- package/internal/wsdl/ids_test.go +0 -21
- package/internal/wsdl/model.go +0 -62
- package/internal/wsdl/parse.go +0 -920
- package/internal/wsdl/parse_test.go +0 -133
- package/plugin/describe.go +0 -107
- package/plugin/describe_test.go +0 -114
- package/plugin/protocol.go +0 -117
- package/plugin/schematest/schematest.go +0 -126
- package/plugins/extract-adr/describe.go +0 -19
- package/plugins/extract-adr/describe_test.go +0 -11
- package/plugins/extract-adr/extract.go +0 -153
- package/plugins/extract-adr/extract_test.go +0 -350
- package/plugins/extract-adr/history.go +0 -99
- package/plugins/extract-adr/main.go +0 -65
- package/plugins/extract-adr/parse.go +0 -642
- package/plugins/extract-adr/parse_test.go +0 -405
- package/plugins/extract-asyncapi/describe.go +0 -19
- package/plugins/extract-asyncapi/describe_test.go +0 -11
- package/plugins/extract-asyncapi/extract.go +0 -315
- package/plugins/extract-asyncapi/extract_test.go +0 -197
- package/plugins/extract-asyncapi/main.go +0 -48
- package/plugins/extract-asyncapi/spec.go +0 -151
- package/plugins/extract-commands/describe.go +0 -19
- package/plugins/extract-commands/describe_test.go +0 -11
- package/plugins/extract-commands/extract.go +0 -79
- package/plugins/extract-commands/extract_test.go +0 -92
- package/plugins/extract-commands/main.go +0 -55
- package/plugins/extract-csr/avro.go +0 -258
- package/plugins/extract-csr/describe.go +0 -19
- package/plugins/extract-csr/describe_test.go +0 -11
- package/plugins/extract-csr/extract.go +0 -338
- package/plugins/extract-csr/extract_test.go +0 -374
- package/plugins/extract-csr/jsonschema.go +0 -343
- package/plugins/extract-csr/lock.go +0 -25
- package/plugins/extract-csr/main.go +0 -84
- package/plugins/extract-csr/subject.go +0 -102
- package/plugins/extract-flows/describe.go +0 -19
- package/plugins/extract-flows/describe_test.go +0 -11
- package/plugins/extract-flows/extract.go +0 -91
- package/plugins/extract-flows/main.go +0 -45
- package/plugins/extract-flows/parse.go +0 -593
- package/plugins/extract-flows/parse_test.go +0 -204
- package/plugins/extract-glossary/describe.go +0 -19
- package/plugins/extract-glossary/describe_test.go +0 -11
- package/plugins/extract-glossary/extract.go +0 -115
- package/plugins/extract-glossary/extract_test.go +0 -220
- package/plugins/extract-glossary/main.go +0 -59
- package/plugins/extract-glossary/parse.go +0 -214
- package/plugins/extract-glossary/parse_test.go +0 -203
- package/plugins/extract-go/aggregate.go +0 -214
- package/plugins/extract-go/client.go +0 -409
- package/plugins/extract-go/client_test.go +0 -266
- package/plugins/extract-go/describe.go +0 -19
- package/plugins/extract-go/describe_test.go +0 -11
- package/plugins/extract-go/enum.go +0 -195
- package/plugins/extract-go/enum_test.go +0 -82
- package/plugins/extract-go/event.go +0 -99
- package/plugins/extract-go/extract.go +0 -191
- package/plugins/extract-go/extract_test.go +0 -261
- package/plugins/extract-go/flow.go +0 -1441
- package/plugins/extract-go/flow_test.go +0 -609
- package/plugins/extract-go/httpclient.go +0 -174
- package/plugins/extract-go/httpclient_test.go +0 -296
- package/plugins/extract-go/ids.go +0 -92
- package/plugins/extract-go/layout.go +0 -225
- package/plugins/extract-go/layout_test.go +0 -125
- package/plugins/extract-go/lifecycle.go +0 -324
- package/plugins/extract-go/lifecycle_test.go +0 -108
- package/plugins/extract-go/main.go +0 -79
- package/plugins/extract-go/operation.go +0 -157
- package/plugins/extract-go/source.go +0 -316
- package/plugins/extract-go/transport.go +0 -321
- package/plugins/extract-go/transport_test.go +0 -145
- package/plugins/extract-go/wiring.go +0 -434
- package/plugins/extract-go-nats/describe.go +0 -19
- package/plugins/extract-go-nats/describe_test.go +0 -11
- package/plugins/extract-go-nats/extract.go +0 -177
- package/plugins/extract-go-nats/extract_test.go +0 -389
- package/plugins/extract-go-nats/index.go +0 -379
- package/plugins/extract-go-nats/main.go +0 -42
- package/plugins/extract-go-nats/resolve.go +0 -161
- package/plugins/extract-go-nats/sites.go +0 -224
- package/plugins/extract-graphql/describe.go +0 -19
- package/plugins/extract-graphql/describe_test.go +0 -11
- package/plugins/extract-graphql/extract.go +0 -433
- package/plugins/extract-graphql/extract_test.go +0 -256
- package/plugins/extract-graphql/ids.go +0 -49
- package/plugins/extract-graphql/lex.go +0 -237
- package/plugins/extract-graphql/main.go +0 -51
- package/plugins/extract-graphql/parse.go +0 -621
- package/plugins/extract-graphql/parse_test.go +0 -122
- package/plugins/extract-http-clients/describe.go +0 -19
- package/plugins/extract-http-clients/describe_test.go +0 -11
- package/plugins/extract-http-clients/extract.go +0 -705
- package/plugins/extract-http-clients/extract_test.go +0 -1263
- package/plugins/extract-http-clients/main.go +0 -42
- package/plugins/extract-openapi/describe.go +0 -19
- package/plugins/extract-openapi/describe_test.go +0 -11
- package/plugins/extract-openapi/discover.go +0 -243
- package/plugins/extract-openapi/extract.go +0 -526
- package/plugins/extract-openapi/extract_test.go +0 -545
- package/plugins/extract-openapi/main.go +0 -75
- package/plugins/extract-openapi/spec.go +0 -350
- package/plugins/extract-project/describe.go +0 -19
- package/plugins/extract-project/describe_test.go +0 -11
- package/plugins/extract-project/extract.go +0 -221
- package/plugins/extract-project/extract_test.go +0 -109
- package/plugins/extract-project/main.go +0 -41
- package/plugins/extract-proto/ast.go +0 -125
- package/plugins/extract-proto/consumes.go +0 -77
- package/plugins/extract-proto/describe.go +0 -19
- package/plugins/extract-proto/extract.go +0 -293
- package/plugins/extract-proto/extract_test.go +0 -459
- package/plugins/extract-proto/ids.go +0 -89
- package/plugins/extract-proto/ids_test.go +0 -57
- package/plugins/extract-proto/lex.go +0 -285
- package/plugins/extract-proto/main.go +0 -100
- package/plugins/extract-proto/module.go +0 -120
- package/plugins/extract-proto/parse.go +0 -720
- package/plugins/extract-proto/parse_test.go +0 -307
- package/plugins/extract-proto/provides.go +0 -236
- package/plugins/extract-proto/resolve.go +0 -222
- package/plugins/extract-proto/resolve_test.go +0 -119
- package/plugins/extract-redis/describe.go +0 -19
- package/plugins/extract-redis/describe_test.go +0 -11
- package/plugins/extract-redis/extract.go +0 -183
- package/plugins/extract-redis/extract_test.go +0 -168
- package/plugins/extract-redis/keyspaces.go +0 -469
- package/plugins/extract-redis/main.go +0 -44
- package/plugins/extract-river/describe.go +0 -19
- package/plugins/extract-river/describe_test.go +0 -11
- package/plugins/extract-river/extract.go +0 -507
- package/plugins/extract-river/extract_test.go +0 -132
- package/plugins/extract-river/main.go +0 -42
- package/plugins/extract-sql/ddl.go +0 -893
- package/plugins/extract-sql/ddl_test.go +0 -401
- package/plugins/extract-sql/describe.go +0 -19
- package/plugins/extract-sql/describe_test.go +0 -11
- package/plugins/extract-sql/layout.go +0 -221
- package/plugins/extract-sql/layout_test.go +0 -55
- package/plugins/extract-sql/lineage.go +0 -107
- package/plugins/extract-sql/main.go +0 -141
- package/plugins/extract-sql/maps.go +0 -564
- package/plugins/extract-sql/maps_java.go +0 -117
- package/plugins/extract-sql/maps_rust.go +0 -333
- package/plugins/extract-sql/maps_rust_test.go +0 -70
- package/plugins/extract-sql/maps_test.go +0 -204
- package/plugins/extract-sql/maps_ts.go +0 -398
- package/plugins/extract-sql/maps_ts_test.go +0 -136
- package/plugins/extract-sql/projection.go +0 -70
- package/plugins/extract-sql/projection_test.go +0 -50
- package/plugins/extract-sql/store.go +0 -420
- package/plugins/extract-sql/store_test.go +0 -233
- package/plugins/extract-sql/view.go +0 -295
- package/plugins/extract-watermill/describe.go +0 -19
- package/plugins/extract-watermill/describe_test.go +0 -11
- package/plugins/extract-watermill/extract.go +0 -1228
- package/plugins/extract-watermill/extract_test.go +0 -234
- package/plugins/extract-watermill/main.go +0 -41
- package/plugins/extract-wsdl/describe.go +0 -19
- package/plugins/extract-wsdl/describe_test.go +0 -11
- package/plugins/extract-wsdl/extract.go +0 -148
- package/plugins/extract-wsdl/extract_test.go +0 -45
- package/plugins/extract-wsdl/main.go +0 -53
- package/plugins/fetch-bsr/auth.go +0 -114
- package/plugins/fetch-bsr/bsr.go +0 -240
- package/plugins/fetch-bsr/cache.go +0 -66
- package/plugins/fetch-bsr/describe.go +0 -19
- package/plugins/fetch-bsr/fetch.go +0 -192
- package/plugins/fetch-bsr/fetch_test.go +0 -465
- package/plugins/fetch-bsr/lock.go +0 -71
- package/plugins/fetch-bsr/main.go +0 -117
- package/plugins/fetch-csr/auth.go +0 -95
- package/plugins/fetch-csr/cache.go +0 -69
- package/plugins/fetch-csr/describe.go +0 -19
- package/plugins/fetch-csr/describe_test.go +0 -11
- package/plugins/fetch-csr/fetch.go +0 -254
- package/plugins/fetch-csr/fetch_test.go +0 -494
- package/plugins/fetch-csr/lock.go +0 -95
- package/plugins/fetch-csr/main.go +0 -122
- package/plugins/fetch-csr/registry.go +0 -209
- package/plugins/fetch-git/cache.go +0 -65
- package/plugins/fetch-git/describe.go +0 -19
- package/plugins/fetch-git/describe_test.go +0 -11
- package/plugins/fetch-git/fetch.go +0 -181
- package/plugins/fetch-git/fetch_test.go +0 -332
- package/plugins/fetch-git/git.go +0 -169
- package/plugins/fetch-git/lock.go +0 -72
- package/plugins/fetch-git/main.go +0 -127
- package/plugins/fetch-git/offline.go +0 -39
- package/plugins/fetch-git/pin.go +0 -90
- package/plugins/fetch-git/pin_test.go +0 -74
- package/plugins/gen-backstage/describe.go +0 -17
- package/plugins/gen-backstage/main.go +0 -22
- package/plugins/gen-backstage/plugin.go +0 -473
- package/plugins/gen-backstage/plugin_test.go +0 -145
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +0 -162
- package/plugins/gen-markdown/aggregate.go +0 -362
- package/plugins/gen-markdown/canonical.go +0 -93
- package/plugins/gen-markdown/context.go +0 -90
- package/plugins/gen-markdown/coverage_test.go +0 -89
- package/plugins/gen-markdown/describe.go +0 -19
- package/plugins/gen-markdown/describe_test.go +0 -11
- package/plugins/gen-markdown/external.go +0 -71
- package/plugins/gen-markdown/flow.go +0 -277
- package/plugins/gen-markdown/glossary.go +0 -76
- package/plugins/gen-markdown/glossary_test.go +0 -148
- package/plugins/gen-markdown/llms.go +0 -302
- package/plugins/gen-markdown/main.go +0 -22
- package/plugins/gen-markdown/markdown.go +0 -254
- package/plugins/gen-markdown/markdown_test.go +0 -100
- package/plugins/gen-markdown/module.go +0 -107
- package/plugins/gen-markdown/plugin.go +0 -39
- package/plugins/gen-markdown/quality_test.go +0 -176
- package/plugins/gen-markdown/redis_test.go +0 -38
- package/plugins/gen-markdown/render.go +0 -344
- package/plugins/gen-markdown/render_test.go +0 -186
- package/plugins/gen-markdown/service.go +0 -456
- package/plugins/gen-markdown/source.go +0 -177
- package/plugins/gen-markdown/store.go +0 -201
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +0 -17
- package/plugins/gen-mermaid/main.go +0 -22
- package/plugins/gen-mermaid/plugin.go +0 -104
- package/plugins/gen-mermaid/plugin_test.go +0 -33
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +0 -261
- package/plugins/openapi/ids_test.go +0 -98
- package/plugins/verify-codeowners/describe.go +0 -19
- package/plugins/verify-codeowners/describe_test.go +0 -11
- package/plugins/verify-codeowners/main.go +0 -75
- package/plugins/verify-codeowners/match.go +0 -85
- package/plugins/verify-codeowners/match_test.go +0 -47
- package/plugins/verify-codeowners/owners.go +0 -164
- package/plugins/verify-codeowners/owners_test.go +0 -225
- package/plugins/verify-codeowners/parse.go +0 -90
- package/plugins/verify-codeowners/parse_test.go +0 -62
- package/plugins/verify-otel/describe.go +0 -19
- package/plugins/verify-otel/describe_test.go +0 -11
- package/plugins/verify-otel/main.go +0 -53
- package/plugins/verify-otel/match.go +0 -336
- package/plugins/verify-otel/otlp.go +0 -200
- package/plugins/verify-otel/verify.go +0 -734
- package/plugins/verify-otel/verify_test.go +0 -460
- /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
- /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"fmt"
|
|
6
|
-
"os"
|
|
7
|
-
"path/filepath"
|
|
8
|
-
"strings"
|
|
9
|
-
|
|
10
|
-
"github.com/shortlink-org/portolan/catalog"
|
|
11
|
-
"github.com/shortlink-org/portolan/plugin"
|
|
12
|
-
"gopkg.in/yaml.v3"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
func extract(in plugin.Input, opts Options) (plugin.Response, error) {
|
|
16
|
-
b := &plugin.Builder{}
|
|
17
|
-
root := in.Root
|
|
18
|
-
|
|
19
|
-
if opts.Context == "" {
|
|
20
|
-
opts.Context = filepath.Base(root)
|
|
21
|
-
}
|
|
22
|
-
if opts.Service == "" {
|
|
23
|
-
opts.Service = filepath.Base(root)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
specPath, err := findSpec(root, opts.Spec, b)
|
|
27
|
-
if err != nil {
|
|
28
|
-
return plugin.Response{}, err
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
doc, err := load(specPath)
|
|
32
|
-
if err != nil {
|
|
33
|
-
return plugin.Response{}, fmt.Errorf("%s: %w", specPath, err)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
source := filepath.ToSlash(specPath)
|
|
37
|
-
service := opts.Context + "." + opts.Service
|
|
38
|
-
|
|
39
|
-
channels := readChannels(doc, source, service, b)
|
|
40
|
-
if len(channels) == 0 {
|
|
41
|
-
b.Warn(service, source+" declares no channels")
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
fragment := catalog.Catalog{
|
|
45
|
-
GeneratedAt: in.GeneratedAt,
|
|
46
|
-
Commit: in.Commit,
|
|
47
|
-
Contexts: []catalog.BoundedContext{{
|
|
48
|
-
ID: opts.Context,
|
|
49
|
-
Slug: opts.Context,
|
|
50
|
-
// Named by whichever source knows the name, as the OpenAPI
|
|
51
|
-
// extractor's fragment says beside the same gap: this document
|
|
52
|
-
// describes what the service says on the bus, not what it is
|
|
53
|
-
// called.
|
|
54
|
-
Services: []catalog.Service{{
|
|
55
|
-
ID: service,
|
|
56
|
-
Slug: opts.Service,
|
|
57
|
-
Provides: []catalog.RpcService{},
|
|
58
|
-
Consumes: []catalog.RpcCall{},
|
|
59
|
-
Aggregates: []catalog.Aggregate{},
|
|
60
|
-
Channels: channels,
|
|
61
|
-
}},
|
|
62
|
-
}},
|
|
63
|
-
Defs: map[string]catalog.TypeDef{},
|
|
64
|
-
Flows: []catalog.Flow{},
|
|
65
|
-
Adrs: []catalog.Adr{},
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
encoded, err := json.MarshalIndent(fragment, "", " ")
|
|
69
|
-
if err != nil {
|
|
70
|
-
return plugin.Response{}, err
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
b.File(firstNonEmpty(opts.Out, "bus.json"), string(encoded)+"\n")
|
|
74
|
-
|
|
75
|
-
return b.Response(), nil
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// draft is a channel while it is being read: the messages arrive from the
|
|
79
|
-
// operations that name them, one direction at a time, and the same message may
|
|
80
|
-
// be named twice.
|
|
81
|
-
type draft struct {
|
|
82
|
-
channel catalog.Channel
|
|
83
|
-
seen map[string]bool
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
func (d *draft) add(message catalog.ChannelMessage) {
|
|
87
|
-
if message.Name == "" {
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
key := string(message.Direction) + " " + message.Name
|
|
91
|
-
if d.seen[key] {
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
d.seen[key] = true
|
|
95
|
-
d.channel.Messages = append(d.channel.Messages, message)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
func readChannels(doc *document, source, service string, b *plugin.Builder) []catalog.Channel {
|
|
99
|
-
if doc.major == 2 {
|
|
100
|
-
return channelsV2(doc, source, service, b)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return channelsV3(doc, source, service, b)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// channelsV3 reads 3.x: channels declare addresses and messages, and operations
|
|
107
|
-
// say which way each message travels.
|
|
108
|
-
func channelsV3(doc *document, source, service string, b *plugin.Builder) []catalog.Channel {
|
|
109
|
-
drafts := map[string]*draft{}
|
|
110
|
-
var order []string
|
|
111
|
-
|
|
112
|
-
for _, c := range entries(child(doc.root, "channels")) {
|
|
113
|
-
address := firstNonEmpty(text(child(c.value, "address")), c.key)
|
|
114
|
-
drafts[c.key] = &draft{
|
|
115
|
-
channel: catalog.Channel{
|
|
116
|
-
Address: address,
|
|
117
|
-
Title: text(child(c.value, "title")),
|
|
118
|
-
Doc: text(child(c.value, "description")),
|
|
119
|
-
Messages: []catalog.ChannelMessage{},
|
|
120
|
-
Source: source,
|
|
121
|
-
},
|
|
122
|
-
seen: map[string]bool{},
|
|
123
|
-
}
|
|
124
|
-
order = append(order, c.key)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
for _, op := range entries(child(doc.root, "operations")) {
|
|
128
|
-
direction, ok := directionOf(text(child(op.value, "action")))
|
|
129
|
-
if !ok {
|
|
130
|
-
b.Warn(service, source+": operation "+op.key+" has no action, so nothing says which way its messages travel")
|
|
131
|
-
|
|
132
|
-
continue
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
_, ref := doc.deref(child(op.value, "channel"))
|
|
136
|
-
key := refKey(ref)
|
|
137
|
-
d, found := drafts[key]
|
|
138
|
-
if !found {
|
|
139
|
-
b.Warn(service, source+": operation "+op.key+" names channel "+key+", which the document does not declare")
|
|
140
|
-
|
|
141
|
-
continue
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
named := items(child(op.value, "messages"))
|
|
145
|
-
if len(named) == 0 {
|
|
146
|
-
// An operation that names no message carries everything the
|
|
147
|
-
// channel declares, which is how a document says "this whole
|
|
148
|
-
// channel goes this way".
|
|
149
|
-
for _, m := range entries(child(channelNode(doc, key), "messages")) {
|
|
150
|
-
d.add(doc.message(m.value, m.key, direction))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
for _, message := range named {
|
|
157
|
-
node, ref := doc.deref(message)
|
|
158
|
-
if node == nil {
|
|
159
|
-
b.Warn(service, source+": operation "+op.key+" names message "+ref+", which is not in this document")
|
|
160
|
-
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
d.add(doc.message(node, refKey(ref), direction))
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
out := make([]catalog.Channel, 0, len(order))
|
|
168
|
-
for _, key := range order {
|
|
169
|
-
d := drafts[key]
|
|
170
|
-
if len(d.channel.Messages) == 0 {
|
|
171
|
-
b.Warn(service, source+": channel "+d.channel.Address+" has no operation, so nothing says which way it travels")
|
|
172
|
-
}
|
|
173
|
-
out = append(out, d.channel)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return out
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// channelsV2 reads 2.x, where the channel key is the address and the two
|
|
180
|
-
// operations are named from the client's side: `subscribe` is what the
|
|
181
|
-
// application produces, `publish` is what it consumes.
|
|
182
|
-
func channelsV2(doc *document, source, service string, b *plugin.Builder) []catalog.Channel {
|
|
183
|
-
var out []catalog.Channel
|
|
184
|
-
|
|
185
|
-
for _, c := range entries(child(doc.root, "channels")) {
|
|
186
|
-
d := &draft{
|
|
187
|
-
channel: catalog.Channel{
|
|
188
|
-
Address: c.key,
|
|
189
|
-
Doc: text(child(c.value, "description")),
|
|
190
|
-
Messages: []catalog.ChannelMessage{},
|
|
191
|
-
Source: source,
|
|
192
|
-
},
|
|
193
|
-
seen: map[string]bool{},
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
for _, op := range []struct {
|
|
197
|
-
key string
|
|
198
|
-
direction catalog.ChannelDirection
|
|
199
|
-
}{
|
|
200
|
-
{"subscribe", catalog.ChannelSend},
|
|
201
|
-
{"publish", catalog.ChannelReceive},
|
|
202
|
-
} {
|
|
203
|
-
node := child(c.value, op.key)
|
|
204
|
-
if node == nil {
|
|
205
|
-
continue
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
message := child(node, "message")
|
|
209
|
-
candidates := items(child(message, "oneOf"))
|
|
210
|
-
if len(candidates) == 0 && message != nil {
|
|
211
|
-
candidates = []*yaml.Node{message}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
for _, candidate := range candidates {
|
|
215
|
-
resolved, ref := doc.deref(candidate)
|
|
216
|
-
if resolved == nil {
|
|
217
|
-
b.Warn(service, source+": channel "+c.key+" names message "+ref+", which is not in this document")
|
|
218
|
-
|
|
219
|
-
continue
|
|
220
|
-
}
|
|
221
|
-
d.add(doc.message(resolved, refKey(ref), op.direction))
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if len(d.channel.Messages) == 0 {
|
|
226
|
-
b.Warn(service, source+": channel "+c.key+" declares no message")
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
out = append(out, d.channel)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return out
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// message reads one message object into the fact the catalog keeps: the name it
|
|
236
|
-
// goes by on the wire, and enough prose to read it without opening the
|
|
237
|
-
// document.
|
|
238
|
-
//
|
|
239
|
-
// The name is the message's own `name` when it has one, and the key it is filed
|
|
240
|
-
// under when it does not. Those are different things - the key is the
|
|
241
|
-
// document's, the name is the bus's - and an event's wire name is compared
|
|
242
|
-
// against this, so a document that leaves `name` out is saying the two are the
|
|
243
|
-
// same.
|
|
244
|
-
func (d *document) message(node *yaml.Node, key string, direction catalog.ChannelDirection) catalog.ChannelMessage {
|
|
245
|
-
resolved, _ := d.deref(node)
|
|
246
|
-
if resolved == nil {
|
|
247
|
-
resolved = node
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return catalog.ChannelMessage{
|
|
251
|
-
Name: firstNonEmpty(text(child(resolved, "name")), key),
|
|
252
|
-
Title: text(child(resolved, "title")),
|
|
253
|
-
Doc: firstNonEmpty(text(child(resolved, "summary")), text(child(resolved, "description"))),
|
|
254
|
-
Direction: direction,
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
func channelNode(doc *document, key string) *yaml.Node {
|
|
259
|
-
return child(doc.root, "channels", key)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
func directionOf(action string) (catalog.ChannelDirection, bool) {
|
|
263
|
-
switch action {
|
|
264
|
-
case "send":
|
|
265
|
-
return catalog.ChannelSend, true
|
|
266
|
-
case "receive":
|
|
267
|
-
return catalog.ChannelReceive, true
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return "", false
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// findSpec locates the document. Told where it is, it looks there; otherwise it
|
|
274
|
-
// searches, and says what it found so a service with two documents does not get
|
|
275
|
-
// described from whichever one sorted first without anybody noticing.
|
|
276
|
-
func findSpec(root, declared string, b *plugin.Builder) (string, error) {
|
|
277
|
-
if declared != "" {
|
|
278
|
-
return filepath.Join(root, filepath.FromSlash(declared)), nil
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
var found []string
|
|
282
|
-
err := filepath.WalkDir(root, func(p string, entry os.DirEntry, err error) error {
|
|
283
|
-
if err != nil || entry.IsDir() {
|
|
284
|
-
return err
|
|
285
|
-
}
|
|
286
|
-
switch entry.Name() {
|
|
287
|
-
case "asyncapi.yaml", "asyncapi.yml":
|
|
288
|
-
found = append(found, p)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return nil
|
|
292
|
-
})
|
|
293
|
-
if err != nil {
|
|
294
|
-
return "", err
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if len(found) == 0 {
|
|
298
|
-
return "", fmt.Errorf("no asyncapi document under %s, and none named in the options", root)
|
|
299
|
-
}
|
|
300
|
-
if len(found) > 1 {
|
|
301
|
-
b.Warn("", "found "+strings.Join(found, ", ")+"; reading the first and ignoring the rest")
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return found[0], nil
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
func firstNonEmpty(values ...string) string {
|
|
308
|
-
for _, value := range values {
|
|
309
|
-
if strings.TrimSpace(value) != "" {
|
|
310
|
-
return value
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return ""
|
|
315
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"strings"
|
|
6
|
-
"testing"
|
|
7
|
-
|
|
8
|
-
"github.com/shortlink-org/portolan/catalog"
|
|
9
|
-
"github.com/shortlink-org/portolan/plugin"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
func run3(t *testing.T, root string, opts Options) plugin.Response {
|
|
13
|
-
t.Helper()
|
|
14
|
-
|
|
15
|
-
resp, err := extract(
|
|
16
|
-
plugin.Input{Root: root, Commit: "abc1234", GeneratedAt: "2026-01-01T00:00:00Z"},
|
|
17
|
-
opts,
|
|
18
|
-
)
|
|
19
|
-
if err != nil {
|
|
20
|
-
t.Fatal(err)
|
|
21
|
-
}
|
|
22
|
-
if len(resp.Files) != 1 {
|
|
23
|
-
t.Fatalf("expected one fragment, got %d files", len(resp.Files))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return resp
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
func channelsOf(t *testing.T, resp plugin.Response) []catalog.Channel {
|
|
30
|
-
t.Helper()
|
|
31
|
-
|
|
32
|
-
var out catalog.Catalog
|
|
33
|
-
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
|
|
34
|
-
t.Fatal(err)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return out.Contexts[0].Services[0].Channels
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func byAddress(channels []catalog.Channel, address string) *catalog.Channel {
|
|
41
|
-
for i := range channels {
|
|
42
|
-
if channels[i].Address == address {
|
|
43
|
-
return &channels[i]
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return nil
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
func spell(messages []catalog.ChannelMessage) []string {
|
|
51
|
-
out := make([]string, 0, len(messages))
|
|
52
|
-
for _, m := range messages {
|
|
53
|
-
out = append(out, string(m.Direction)+" "+m.Name)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return out
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// An operation that names no message carries everything the channel declares,
|
|
60
|
-
// which is how a document says "this whole channel goes one way".
|
|
61
|
-
func TestSendTakesTheWholeChannel(t *testing.T) {
|
|
62
|
-
channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
|
|
63
|
-
|
|
64
|
-
basket := byAddress(channels, "shop.cart.basket")
|
|
65
|
-
if basket == nil {
|
|
66
|
-
t.Fatalf("no basket channel in %v", channels)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
want := "send cart.BasketCreated,send cart.BasketCheckedOut"
|
|
70
|
-
if got := strings.Join(spell(basket.Messages), ","); got != want {
|
|
71
|
-
t.Errorf("messages = %s, want %s", got, want)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// The address is the channel's own when it names one, not the key it is filed
|
|
76
|
-
// under: the key is the document's word, the address is the broker's, and an
|
|
77
|
-
// event's wire is compared against the second.
|
|
78
|
-
func TestAddressBeatsTheKey(t *testing.T) {
|
|
79
|
-
channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
|
|
80
|
-
|
|
81
|
-
if byAddress(channels, "basket") != nil {
|
|
82
|
-
t.Error("the channel key was taken for an address")
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// What the service listens for is the half no publisher's source could say.
|
|
87
|
-
func TestReceiveIsReadFromTheOperation(t *testing.T) {
|
|
88
|
-
channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
|
|
89
|
-
|
|
90
|
-
sessions := byAddress(channels, "auth_session")
|
|
91
|
-
if sessions == nil {
|
|
92
|
-
t.Fatalf("no session channel in %v", channels)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if got := strings.Join(spell(sessions.Messages), ","); got != "receive auth.SessionEnded" {
|
|
96
|
-
t.Errorf("messages = %s", got)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// The name on the wire and the prose beside it, so a reader does not have to
|
|
101
|
-
// open the document to know what arrived.
|
|
102
|
-
func TestMessageCarriesItsNameAndSummary(t *testing.T) {
|
|
103
|
-
channels := channelsOf(t, run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"}))
|
|
104
|
-
|
|
105
|
-
basket := byAddress(channels, "shop.cart.basket")
|
|
106
|
-
created := basket.Messages[0]
|
|
107
|
-
|
|
108
|
-
if created.Name != "cart.BasketCreated" {
|
|
109
|
-
t.Errorf("name = %q", created.Name)
|
|
110
|
-
}
|
|
111
|
-
if created.Title != "Basket created" {
|
|
112
|
-
t.Errorf("title = %q", created.Title)
|
|
113
|
-
}
|
|
114
|
-
if created.Doc != "A shopper has a basket." {
|
|
115
|
-
t.Errorf("doc = %q", created.Doc)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// A channel nothing operates on is still a channel - the address is a fact -
|
|
120
|
-
// but which way it travels is not known, and silence about that is what the
|
|
121
|
-
// diagnostic is for.
|
|
122
|
-
func TestChannelWithNoOperationIsReportedNotDropped(t *testing.T) {
|
|
123
|
-
resp := run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"})
|
|
124
|
-
|
|
125
|
-
telemetry := byAddress(channelsOf(t, resp), "shop.cart.telemetry")
|
|
126
|
-
if telemetry == nil {
|
|
127
|
-
t.Fatal("a channel with no operation was dropped")
|
|
128
|
-
}
|
|
129
|
-
if len(telemetry.Messages) != 0 {
|
|
130
|
-
t.Errorf("messages = %v", telemetry.Messages)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
var said bool
|
|
134
|
-
for _, d := range resp.Warnings() {
|
|
135
|
-
if strings.Contains(d.Message, "shop.cart.telemetry") {
|
|
136
|
-
said = true
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if !said {
|
|
140
|
-
t.Errorf("nothing was said about it: %v", resp.Warnings())
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 2.x names its operations from the client's side: `subscribe` is what the
|
|
145
|
-
// application produces and `publish` is what it consumes. Reading them the
|
|
146
|
-
// obvious way gets every arrow backwards.
|
|
147
|
-
func TestVersionTwoOperationsAreReadFromTheApplicationsSide(t *testing.T) {
|
|
148
|
-
channels := channelsOf(t, run3(t, "testdata/v2", Options{Context: "shop", Service: "billing"}))
|
|
149
|
-
|
|
150
|
-
raised := byAddress(channels, "billing_invoice")
|
|
151
|
-
if raised == nil {
|
|
152
|
-
t.Fatalf("no invoice channel in %v", channels)
|
|
153
|
-
}
|
|
154
|
-
want := "send billing.InvoiceRaised,send billing.InvoiceSettled"
|
|
155
|
-
if got := strings.Join(spell(raised.Messages), ","); got != want {
|
|
156
|
-
t.Errorf("subscribe means the application sends; got %s", got)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
consumed := byAddress(channels, "auth_user")
|
|
160
|
-
if consumed == nil {
|
|
161
|
-
t.Fatalf("no auth_user channel in %v", channels)
|
|
162
|
-
}
|
|
163
|
-
if got := strings.Join(spell(consumed.Messages), ","); got != "receive auth.UserRegistered" {
|
|
164
|
-
t.Errorf("publish means the application receives; got %s", got)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// The fragment describes what the service says on the bus and nothing else.
|
|
169
|
-
func TestFragmentClaimsNothingItDoesNotKnow(t *testing.T) {
|
|
170
|
-
var out catalog.Catalog
|
|
171
|
-
resp := run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"})
|
|
172
|
-
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &out); err != nil {
|
|
173
|
-
t.Fatal(err)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
service := out.Contexts[0].Services[0]
|
|
177
|
-
if service.Name != "" || service.Readme != "" || service.Repo != "" {
|
|
178
|
-
t.Errorf("the bus fragment should not name the service: %+v", service)
|
|
179
|
-
}
|
|
180
|
-
if len(service.Aggregates) != 0 || len(service.Provides) != 0 {
|
|
181
|
-
t.Error("the bus fragment should carry no aggregates and no interfaces")
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// The fragment's name is the manifest's to choose, and `bus.json` when it does
|
|
186
|
-
// not.
|
|
187
|
-
func TestFragmentName(t *testing.T) {
|
|
188
|
-
resp := run3(t, "testdata/v3", Options{Context: "shop", Service: "cart"})
|
|
189
|
-
if resp.Files[0].Name != "bus.json" {
|
|
190
|
-
t.Errorf("name = %q", resp.Files[0].Name)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
resp = run3(t, "testdata/v3", Options{Context: "shop", Service: "cart", Out: "channels.json"})
|
|
194
|
-
if resp.Files[0].Name != "channels.json" {
|
|
195
|
-
t.Errorf("name = %q", resp.Files[0].Name)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Package main is portolan-extract-asyncapi: an AsyncAPI document in, a catalog
|
|
2
|
-
// fragment out.
|
|
3
|
-
//
|
|
4
|
-
// It describes one aspect of a service - what it puts on the bus and what it
|
|
5
|
-
// listens for - and nothing else. The events themselves come from a different
|
|
6
|
-
// extractor reading the domain, and the two meet in the merge: that one says an
|
|
7
|
-
// aggregate raises BasketCreated and calls it `cart.BasketCreated` on the wire,
|
|
8
|
-
// this one says the service declares a channel carrying that name. Neither
|
|
9
|
-
// knows the other exists, and the pages hold the two against each other.
|
|
10
|
-
package main
|
|
11
|
-
|
|
12
|
-
import (
|
|
13
|
-
"fmt"
|
|
14
|
-
"io"
|
|
15
|
-
"os"
|
|
16
|
-
|
|
17
|
-
"github.com/shortlink-org/portolan/plugin"
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
// Options are what the manifest tells the extractor: the things an AsyncAPI
|
|
21
|
-
// document does not say about the estate it belongs to.
|
|
22
|
-
type Options struct {
|
|
23
|
-
Context string `json:"context"`
|
|
24
|
-
Service string `json:"service"`
|
|
25
|
-
|
|
26
|
-
// Spec is the document, relative to the input root. Left out, the
|
|
27
|
-
// extractor looks for one.
|
|
28
|
-
Spec string `json:"spec,omitempty"`
|
|
29
|
-
|
|
30
|
-
Out string `json:"out,omitempty"`
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
func main() {
|
|
34
|
-
if err := run(os.Stdin, os.Stdout); err != nil {
|
|
35
|
-
fmt.Fprintln(os.Stderr, "portolan-extract-asyncapi:", err)
|
|
36
|
-
os.Exit(1)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func run(stdin io.Reader, stdout io.Writer) error {
|
|
41
|
-
return plugin.Serve(stdin, stdout, descriptor(), func(req plugin.Request, opts Options) (plugin.Response, error) {
|
|
42
|
-
if req.Input.Root == "" {
|
|
43
|
-
return plugin.Response{}, fmt.Errorf("no input root: an extractor has nothing to read")
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return extract(req.Input, opts)
|
|
47
|
-
})
|
|
48
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"os"
|
|
5
|
-
"strings"
|
|
6
|
-
|
|
7
|
-
"gopkg.in/yaml.v3"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
// The document is read as a node tree rather than into structs, for the reason
|
|
11
|
-
// the OpenAPI extractor gives beside its own reader: a mapping unmarshalled
|
|
12
|
-
// into a Go map loses the order its author wrote, and a channel object holds
|
|
13
|
-
// keys this does not model, which walking the nodes ignores rather than fails
|
|
14
|
-
// on.
|
|
15
|
-
//
|
|
16
|
-
// Two versions have to be read here, and they disagree about a word. In 3.x an
|
|
17
|
-
// operation says `action: send` or `action: receive`, from the application's
|
|
18
|
-
// side, and that is the end of it. In 2.x a channel has `publish` and
|
|
19
|
-
// `subscribe`, and both are written from the *client's* side: `publish` is what
|
|
20
|
-
// somebody else publishes to the application, and `subscribe` is what the
|
|
21
|
-
// application produces for somebody else to subscribe to. Reading 2.x the
|
|
22
|
-
// obvious way gets every arrow backwards, which is the confusion 3.x was
|
|
23
|
-
// changed to end.
|
|
24
|
-
|
|
25
|
-
type document struct {
|
|
26
|
-
root *yaml.Node
|
|
27
|
-
path string
|
|
28
|
-
// major is the major version of the document: 2 or 3.
|
|
29
|
-
major int
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
func load(path string) (*document, error) {
|
|
33
|
-
contents, err := os.ReadFile(path)
|
|
34
|
-
if err != nil {
|
|
35
|
-
return nil, err
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
var root yaml.Node
|
|
39
|
-
if err := yaml.Unmarshal(contents, &root); err != nil {
|
|
40
|
-
return nil, err
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
node := &root
|
|
44
|
-
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
|
|
45
|
-
node = node.Content[0]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
doc := &document{root: node, path: path, major: 3}
|
|
49
|
-
if version := text(child(node, "asyncapi")); strings.HasPrefix(version, "2.") {
|
|
50
|
-
doc.major = 2
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return doc, nil
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// entry is one key/value pair of a mapping, in document order.
|
|
57
|
-
type entry struct {
|
|
58
|
-
key string
|
|
59
|
-
value *yaml.Node
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
func entries(node *yaml.Node) []entry {
|
|
63
|
-
if node == nil || node.Kind != yaml.MappingNode {
|
|
64
|
-
return nil
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
out := make([]entry, 0, len(node.Content)/2)
|
|
68
|
-
for i := 0; i+1 < len(node.Content); i += 2 {
|
|
69
|
-
out = append(out, entry{key: node.Content[i].Value, value: node.Content[i+1]})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return out
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// child follows a path of mapping keys, and answers nil for anything missing.
|
|
76
|
-
func child(node *yaml.Node, keys ...string) *yaml.Node {
|
|
77
|
-
for _, key := range keys {
|
|
78
|
-
found := false
|
|
79
|
-
for _, e := range entries(node) {
|
|
80
|
-
if e.key == key {
|
|
81
|
-
node = e.value
|
|
82
|
-
found = true
|
|
83
|
-
|
|
84
|
-
break
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if !found {
|
|
88
|
-
return nil
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return node
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
func text(node *yaml.Node) string {
|
|
96
|
-
if node == nil {
|
|
97
|
-
return ""
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return strings.TrimSpace(node.Value)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// items reads a sequence, or the single node written where a sequence was
|
|
104
|
-
// allowed. A document that names one message where it could have named several
|
|
105
|
-
// is the common case, and both spellings mean the same thing.
|
|
106
|
-
func items(node *yaml.Node) []*yaml.Node {
|
|
107
|
-
if node == nil {
|
|
108
|
-
return nil
|
|
109
|
-
}
|
|
110
|
-
if node.Kind != yaml.SequenceNode {
|
|
111
|
-
return []*yaml.Node{node}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return node.Content
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// deref follows a local $ref, and answers the node itself when there is none.
|
|
118
|
-
//
|
|
119
|
-
// Only local refs are followed. A document that reaches into another file is
|
|
120
|
-
// describing something this extractor cannot see, and guessing at a name from
|
|
121
|
-
// the last segment of a path would be inventing one.
|
|
122
|
-
func (d *document) deref(node *yaml.Node) (*yaml.Node, string) {
|
|
123
|
-
ref := text(child(node, "$ref"))
|
|
124
|
-
if ref == "" {
|
|
125
|
-
return node, ""
|
|
126
|
-
}
|
|
127
|
-
if !strings.HasPrefix(ref, "#/") {
|
|
128
|
-
return nil, ref
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
var keys []string
|
|
132
|
-
for _, segment := range strings.Split(strings.TrimPrefix(ref, "#/"), "/") {
|
|
133
|
-
// JSON Pointer escapes, which a channel address containing a slash
|
|
134
|
-
// needs: `shop~1cart` is one key, not two.
|
|
135
|
-
segment = strings.ReplaceAll(segment, "~1", "/")
|
|
136
|
-
keys = append(keys, strings.ReplaceAll(segment, "~0", "~"))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return child(d.root, keys...), ref
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// refKey is the last segment of a ref, which is the key the thing it points at
|
|
143
|
-
// is filed under.
|
|
144
|
-
func refKey(ref string) string {
|
|
145
|
-
at := strings.LastIndex(ref, "/")
|
|
146
|
-
if at < 0 {
|
|
147
|
-
return ref
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return strings.ReplaceAll(strings.ReplaceAll(ref[at+1:], "~1", "/"), "~0", "~")
|
|
151
|
-
}
|