@shortlink-org/portolan 0.1.0 → 0.2.1
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 +27 -10
- package/scripts/local-api.test.mjs +85 -2
- package/scripts/manifest.mjs +3 -2
- package/scripts/package-smoke.mjs +24 -3
- 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 +17 -9
- 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,350 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"os"
|
|
5
|
-
"path/filepath"
|
|
6
|
-
"strconv"
|
|
7
|
-
"strings"
|
|
8
|
-
|
|
9
|
-
"gopkg.in/yaml.v3"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
// The document is read as a node tree rather than into structs.
|
|
13
|
-
//
|
|
14
|
-
// Two reasons, and both are about the reader of the output. A YAML mapping
|
|
15
|
-
// unmarshalled into a Go map loses its order, and the order paths and schemas
|
|
16
|
-
// are written in is the author's - alphabetising it would be a worse document
|
|
17
|
-
// than the one they wrote. And a path item holds keys that are not verbs, and a
|
|
18
|
-
// schema holds keywords this does not model; walking the nodes means ignoring
|
|
19
|
-
// them rather than failing on them.
|
|
20
|
-
|
|
21
|
-
// verbs, in the order a reader expects to meet them rather than the order they
|
|
22
|
-
// happen to appear.
|
|
23
|
-
var verbs = []string{"get", "post", "put", "patch", "delete", "head", "options", "trace"}
|
|
24
|
-
|
|
25
|
-
type document struct {
|
|
26
|
-
root *yaml.Node
|
|
27
|
-
path string
|
|
28
|
-
cache map[string]*document
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
func load(path string) (*document, error) {
|
|
32
|
-
cache := map[string]*document{}
|
|
33
|
-
return loadCached(path, cache)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func loadCached(path string, cache map[string]*document) (*document, error) {
|
|
37
|
-
path = filepath.Clean(path)
|
|
38
|
-
if held := cache[path]; held != nil {
|
|
39
|
-
return held, nil
|
|
40
|
-
}
|
|
41
|
-
contents, err := os.ReadFile(path)
|
|
42
|
-
if err != nil {
|
|
43
|
-
return nil, err
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
var root yaml.Node
|
|
47
|
-
if err := yaml.Unmarshal(contents, &root); err != nil {
|
|
48
|
-
return nil, err
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// A document node wraps the mapping.
|
|
52
|
-
node := &root
|
|
53
|
-
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
|
|
54
|
-
node = node.Content[0]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
doc := &document{root: node, path: path, cache: cache}
|
|
58
|
-
cache[path] = doc
|
|
59
|
-
return doc, nil
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// entry is one key/value pair of a mapping, in document order.
|
|
63
|
-
type entry struct {
|
|
64
|
-
key string
|
|
65
|
-
value *yaml.Node
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
func entries(node *yaml.Node) []entry {
|
|
69
|
-
if node == nil || node.Kind != yaml.MappingNode {
|
|
70
|
-
return nil
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
out := make([]entry, 0, len(node.Content)/2)
|
|
74
|
-
for i := 0; i+1 < len(node.Content); i += 2 {
|
|
75
|
-
out = append(out, entry{key: node.Content[i].Value, value: node.Content[i+1]})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return out
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// child follows a path of mapping keys, and answers nil for anything missing.
|
|
82
|
-
func child(node *yaml.Node, keys ...string) *yaml.Node {
|
|
83
|
-
for _, key := range keys {
|
|
84
|
-
found := false
|
|
85
|
-
for _, e := range entries(node) {
|
|
86
|
-
if e.key == key {
|
|
87
|
-
node = e.value
|
|
88
|
-
found = true
|
|
89
|
-
|
|
90
|
-
break
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if !found {
|
|
94
|
-
return nil
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return node
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
func text(node *yaml.Node) string {
|
|
102
|
-
if node == nil {
|
|
103
|
-
return ""
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return strings.TrimSpace(node.Value)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
func list(node *yaml.Node) []string {
|
|
110
|
-
if node == nil || node.Kind != yaml.SequenceNode {
|
|
111
|
-
return nil
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
out := make([]string, 0, len(node.Content))
|
|
115
|
-
for _, item := range node.Content {
|
|
116
|
-
out = append(out, item.Value)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return out
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Where a document keeps its named schemas: under components in OpenAPI 3, and
|
|
123
|
-
// under definitions in Swagger 2.0. A reference is a pointer to one of these
|
|
124
|
-
// two places, and the name is what follows.
|
|
125
|
-
var schemaHomes = []string{"components/schemas/", "definitions/"}
|
|
126
|
-
|
|
127
|
-
type schemaRef struct {
|
|
128
|
-
doc *document
|
|
129
|
-
node *yaml.Node
|
|
130
|
-
name string
|
|
131
|
-
key string
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// schemaRefs collects every component schema a subtree names, in the order they
|
|
135
|
-
// are met.
|
|
136
|
-
//
|
|
137
|
-
// A $ref that points at another component - a shared response, a parameter - is
|
|
138
|
-
// followed rather than ignored, because that is how a spec says "the same error
|
|
139
|
-
// body as everywhere else", and a reader who is not shown it is being told this
|
|
140
|
-
// endpoint returns nothing on failure.
|
|
141
|
-
func (d *document) schemaRefs(node *yaml.Node, into *[]schemaRef, seen, visited map[string]bool) {
|
|
142
|
-
if node == nil {
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
switch node.Kind {
|
|
147
|
-
case yaml.MappingNode:
|
|
148
|
-
for _, e := range entries(node) {
|
|
149
|
-
if e.key != "$ref" {
|
|
150
|
-
d.schemaRefs(e.value, into, seen, visited)
|
|
151
|
-
|
|
152
|
-
continue
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
ref := text(e.value)
|
|
156
|
-
targetDoc, target, pointer, ok := d.resolve(ref)
|
|
157
|
-
if !ok {
|
|
158
|
-
continue
|
|
159
|
-
}
|
|
160
|
-
key := targetDoc.path + "#" + pointer
|
|
161
|
-
if name, schema := schemaName(pointer); schema {
|
|
162
|
-
if !seen[key] {
|
|
163
|
-
seen[key] = true
|
|
164
|
-
*into = append(*into, schemaRef{doc: targetDoc, node: target, name: name, key: key})
|
|
165
|
-
}
|
|
166
|
-
continue
|
|
167
|
-
}
|
|
168
|
-
if visited[key] {
|
|
169
|
-
continue
|
|
170
|
-
}
|
|
171
|
-
visited[key] = true
|
|
172
|
-
targetDoc.schemaRefs(target, into, seen, visited)
|
|
173
|
-
}
|
|
174
|
-
case yaml.SequenceNode:
|
|
175
|
-
for _, item := range node.Content {
|
|
176
|
-
d.schemaRefs(item, into, seen, visited)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// shapes are what an operation sends and what it answers with, by the names the
|
|
182
|
-
// document gives them.
|
|
183
|
-
//
|
|
184
|
-
// A body behind a $ref has a name a reader can look up in Messages; an inline
|
|
185
|
-
// schema has none, and inventing one would put a shape in the catalog the
|
|
186
|
-
// document does not have. A success with no body at all is answered by its
|
|
187
|
-
// status: 204 is a real answer, and saying nothing for it reads as a call that
|
|
188
|
-
// never came back.
|
|
189
|
-
func (d *document) shapes(item, operation *yaml.Node) (string, string) {
|
|
190
|
-
request := d.bodyName(child(operation, "requestBody"))
|
|
191
|
-
if request == "" {
|
|
192
|
-
request = d.bodyParameter(operation, item)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
code, body := "", (*yaml.Node)(nil)
|
|
196
|
-
for _, e := range entries(child(operation, "responses")) {
|
|
197
|
-
status, err := strconv.Atoi(e.key)
|
|
198
|
-
if err != nil || status < 200 || status > 299 {
|
|
199
|
-
continue
|
|
200
|
-
}
|
|
201
|
-
if code == "" || e.key < code {
|
|
202
|
-
code, body = e.key, e.value
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if body == nil {
|
|
206
|
-
return request, ""
|
|
207
|
-
}
|
|
208
|
-
if name := d.bodyName(body); name != "" {
|
|
209
|
-
return request, name
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return request, code
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// bodyName is the component schema a request or response body names, following
|
|
216
|
-
// one $ref to a shared component on the way. A list of a schema is that schema
|
|
217
|
-
// with brackets: the message is the item, and the reader wants to see there are
|
|
218
|
-
// several.
|
|
219
|
-
//
|
|
220
|
-
// OpenAPI 3 puts the schema under a media type; Swagger 2.0 puts it straight on
|
|
221
|
-
// the response. Both are read, so a document generated by a tool that still
|
|
222
|
-
// writes 2.0 - swag, for one - names its shapes like any other.
|
|
223
|
-
func (d *document) bodyName(node *yaml.Node) string {
|
|
224
|
-
bodyDoc, body := d.follow(node)
|
|
225
|
-
for _, e := range entries(child(body, "content")) {
|
|
226
|
-
mediaDoc, media := bodyDoc.follow(e.value)
|
|
227
|
-
schema := child(media, "schema")
|
|
228
|
-
if name := mediaDoc.schemaTypeName(schema); name != "" {
|
|
229
|
-
return name
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if schema := child(body, "schema"); schema != nil {
|
|
233
|
-
return bodyDoc.schemaTypeName(schema)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return ""
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// bodyParameter is the Swagger 2.0 request body: a parameter with `in: body`,
|
|
240
|
-
// on the operation or inherited from its path item, whose schema is the shape.
|
|
241
|
-
// The operation's own list is read first, because it is the one that wins when
|
|
242
|
-
// both declare one.
|
|
243
|
-
func (d *document) bodyParameter(operation, item *yaml.Node) string {
|
|
244
|
-
for _, owner := range []*yaml.Node{operation, item} {
|
|
245
|
-
for _, param := range itemsOf(child(owner, "parameters")) {
|
|
246
|
-
paramDoc, param := d.follow(param)
|
|
247
|
-
if text(child(param, "in")) != "body" {
|
|
248
|
-
continue
|
|
249
|
-
}
|
|
250
|
-
return paramDoc.schemaTypeName(child(param, "schema"))
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return ""
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
func (d *document) schemaTypeName(node *yaml.Node) string {
|
|
258
|
-
if _, _, pointer, ok := d.resolve(text(child(node, "$ref"))); ok {
|
|
259
|
-
if name, schema := schemaName(pointer); schema {
|
|
260
|
-
return name
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if text(child(node, "type")) == "array" {
|
|
264
|
-
if name := d.schemaTypeName(child(node, "items")); name != "" {
|
|
265
|
-
return name + "[]"
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
for _, keyword := range []string{"oneOf", "anyOf"} {
|
|
269
|
-
var names []string
|
|
270
|
-
for _, branch := range itemsOf(child(node, keyword)) {
|
|
271
|
-
if name := d.schemaTypeName(branch); name != "" {
|
|
272
|
-
names = append(names, name)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
if len(names) > 0 {
|
|
276
|
-
return strings.Join(unique(names), " | ")
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return ""
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// follow resolves chains of local or relative-file component references.
|
|
283
|
-
func (d *document) follow(node *yaml.Node) (*document, *yaml.Node) {
|
|
284
|
-
seen := map[string]bool{}
|
|
285
|
-
for {
|
|
286
|
-
ref := text(child(node, "$ref"))
|
|
287
|
-
nextDoc, next, pointer, ok := d.resolve(ref)
|
|
288
|
-
if !ok {
|
|
289
|
-
return d, node
|
|
290
|
-
}
|
|
291
|
-
key := nextDoc.path + "#" + pointer
|
|
292
|
-
if seen[key] {
|
|
293
|
-
return d, node
|
|
294
|
-
}
|
|
295
|
-
seen[key] = true
|
|
296
|
-
d, node = nextDoc, next
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// resolve follows JSON Pointer refs in this document or in a relative YAML/
|
|
301
|
-
// JSON file. Remote refs are intentionally not fetched by an extractor.
|
|
302
|
-
func (d *document) resolve(ref string) (*document, *yaml.Node, string, bool) {
|
|
303
|
-
if ref == "" || strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") {
|
|
304
|
-
return nil, nil, "", false
|
|
305
|
-
}
|
|
306
|
-
file, fragment, hasFragment := strings.Cut(ref, "#")
|
|
307
|
-
if !hasFragment {
|
|
308
|
-
return nil, nil, "", false
|
|
309
|
-
}
|
|
310
|
-
target := d
|
|
311
|
-
if file != "" {
|
|
312
|
-
loaded, err := loadCached(filepath.Join(filepath.Dir(d.path), filepath.FromSlash(file)), d.cache)
|
|
313
|
-
if err != nil {
|
|
314
|
-
return nil, nil, "", false
|
|
315
|
-
}
|
|
316
|
-
target = loaded
|
|
317
|
-
}
|
|
318
|
-
pointer := strings.TrimPrefix(fragment, "/")
|
|
319
|
-
if pointer == "" {
|
|
320
|
-
return target, target.root, pointer, true
|
|
321
|
-
}
|
|
322
|
-
parts := strings.Split(pointer, "/")
|
|
323
|
-
for i := range parts {
|
|
324
|
-
parts[i] = strings.ReplaceAll(strings.ReplaceAll(parts[i], "~1", "/"), "~0", "~")
|
|
325
|
-
}
|
|
326
|
-
node := child(target.root, parts...)
|
|
327
|
-
return target, node, pointer, node != nil
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
func schemaName(pointer string) (string, bool) {
|
|
331
|
-
for _, home := range schemaHomes {
|
|
332
|
-
name, ok := strings.CutPrefix(pointer, home)
|
|
333
|
-
if !ok || name == "" || strings.Contains(name, "/") {
|
|
334
|
-
continue
|
|
335
|
-
}
|
|
336
|
-
return strings.ReplaceAll(strings.ReplaceAll(name, "~1", "/"), "~0", "~"), true
|
|
337
|
-
}
|
|
338
|
-
return "", false
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// refName is the name a reference spells even when it points at nothing: a
|
|
342
|
-
// document that names a schema it never defines still tells the reader what the
|
|
343
|
-
// author called it.
|
|
344
|
-
func refName(ref string) (string, bool) {
|
|
345
|
-
pointer, ok := strings.CutPrefix(ref, "#/")
|
|
346
|
-
if !ok {
|
|
347
|
-
return "", false
|
|
348
|
-
}
|
|
349
|
-
return schemaName(pointer)
|
|
350
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
_ "embed"
|
|
5
|
-
|
|
6
|
-
"github.com/shortlink-org/portolan/plugin"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
//go:embed options.schema.json
|
|
10
|
-
var optionsSchema []byte
|
|
11
|
-
|
|
12
|
-
func descriptor() plugin.Descriptor {
|
|
13
|
-
return plugin.Descriptor{
|
|
14
|
-
Name: "extract-project",
|
|
15
|
-
Summary: "Reads repository metadata and describes a group and component without assuming DDD or a particular language.",
|
|
16
|
-
Phases: []string{plugin.PhaseExtract},
|
|
17
|
-
Options: optionsSchema,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"os"
|
|
6
|
-
"path/filepath"
|
|
7
|
-
"regexp"
|
|
8
|
-
"strings"
|
|
9
|
-
|
|
10
|
-
"github.com/shortlink-org/portolan/catalog"
|
|
11
|
-
"github.com/shortlink-org/portolan/internal/commands"
|
|
12
|
-
"github.com/shortlink-org/portolan/plugin"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
var packageRepository = regexp.MustCompile(`(?m)"repository"\s*:\s*(?:"([^"]+)"|\{[^}]*"url"\s*:\s*"([^"]+)")`)
|
|
16
|
-
var cargoName = regexp.MustCompile(`(?m)^name\s*=\s*"([^"]+)"`)
|
|
17
|
-
|
|
18
|
-
func extract(in plugin.Input, opts Options) (plugin.Response, error) {
|
|
19
|
-
root := in.Root
|
|
20
|
-
base := slug(filepath.Base(root))
|
|
21
|
-
group := firstNonEmpty(slug(opts.Group), base, "project")
|
|
22
|
-
component := firstNonEmpty(slug(opts.Component), base, "component")
|
|
23
|
-
readme := read(filepath.Join(root, "README.md"))
|
|
24
|
-
b := &plugin.Builder{}
|
|
25
|
-
cmds, warnings := commands.Read(root)
|
|
26
|
-
for _, warning := range warnings {
|
|
27
|
-
b.Warn(root, warning)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
fragment := catalog.Catalog{
|
|
31
|
-
GeneratedAt: in.GeneratedAt,
|
|
32
|
-
Commit: in.Commit,
|
|
33
|
-
Contexts: []catalog.BoundedContext{{
|
|
34
|
-
ID: group,
|
|
35
|
-
Slug: group,
|
|
36
|
-
Name: firstNonEmpty(opts.GroupName, title(group)),
|
|
37
|
-
Summary: opts.GroupSummary,
|
|
38
|
-
Kind: catalog.GroupKind(firstNonEmpty(opts.GroupKind, string(catalog.GroupKindSystem))),
|
|
39
|
-
Services: []catalog.Service{{
|
|
40
|
-
ID: group + "." + component,
|
|
41
|
-
Slug: component,
|
|
42
|
-
Name: firstNonEmpty(opts.ComponentName, markdownTitle(readme), title(component)),
|
|
43
|
-
Repo: firstNonEmpty(opts.Repo, repository(root)),
|
|
44
|
-
Path: filepath.ToSlash(root),
|
|
45
|
-
Readme: readme,
|
|
46
|
-
Kind: catalog.ComponentKind(firstNonEmpty(opts.ComponentKind, inferredKind(root))),
|
|
47
|
-
Technologies: stated(opts.Technologies, technologies(root)),
|
|
48
|
-
Provides: []catalog.RpcService{},
|
|
49
|
-
Consumes: []catalog.RpcCall{},
|
|
50
|
-
Aggregates: []catalog.Aggregate{},
|
|
51
|
-
Commands: cmds,
|
|
52
|
-
}},
|
|
53
|
-
}},
|
|
54
|
-
Defs: map[string]catalog.TypeDef{},
|
|
55
|
-
Flows: []catalog.Flow{},
|
|
56
|
-
Adrs: []catalog.Adr{},
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
encoded, err := json.MarshalIndent(fragment, "", " ")
|
|
60
|
-
if err != nil {
|
|
61
|
-
return plugin.Response{}, err
|
|
62
|
-
}
|
|
63
|
-
b.File(firstNonEmpty(opts.Out, "project.json"), string(encoded)+"\n")
|
|
64
|
-
return b.Response(), nil
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
func repository(root string) string {
|
|
68
|
-
for _, line := range strings.Split(read(filepath.Join(root, "go.mod")), "\n") {
|
|
69
|
-
if value, ok := strings.CutPrefix(strings.TrimSpace(line), "module "); ok {
|
|
70
|
-
return strings.TrimSpace(value)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if match := packageRepository.FindStringSubmatch(read(filepath.Join(root, "package.json"))); len(match) > 0 {
|
|
74
|
-
return strings.TrimSuffix(firstNonEmpty(match[1], match[2]), ".git")
|
|
75
|
-
}
|
|
76
|
-
if match := cargoName.FindStringSubmatch(read(filepath.Join(root, "Cargo.toml"))); len(match) > 0 {
|
|
77
|
-
return match[1]
|
|
78
|
-
}
|
|
79
|
-
return ""
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
func inferredKind(root string) string {
|
|
83
|
-
if exists(filepath.Join(root, "cmd")) || exists(filepath.Join(root, "Dockerfile")) || exists(filepath.Join(root, "docker-compose.yml")) || exists(filepath.Join(root, "docker-compose.yaml")) {
|
|
84
|
-
return string(catalog.ComponentKindApplication)
|
|
85
|
-
}
|
|
86
|
-
return string(catalog.ComponentKindLibrary)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func technologies(root string) []string {
|
|
90
|
-
goMod := read(filepath.Join(root, "go.mod"))
|
|
91
|
-
packageJSON := read(filepath.Join(root, "package.json"))
|
|
92
|
-
cargo := read(filepath.Join(root, "Cargo.toml"))
|
|
93
|
-
markers := []struct {
|
|
94
|
-
name string
|
|
95
|
-
seen bool
|
|
96
|
-
}{
|
|
97
|
-
{"Go", goMod != ""},
|
|
98
|
-
{"Node.js", packageJSON != ""},
|
|
99
|
-
{"Rust", cargo != ""},
|
|
100
|
-
{"Java", exists(filepath.Join(root, "pom.xml")) || exists(filepath.Join(root, "build.gradle")) || exists(filepath.Join(root, "build.gradle.kts"))},
|
|
101
|
-
{"Python", exists(filepath.Join(root, "pyproject.toml")) || exists(filepath.Join(root, "requirements.txt")) || exists(filepath.Join(root, "manage.py"))},
|
|
102
|
-
{"Gin", strings.Contains(goMod, "github.com/gin-gonic/gin")},
|
|
103
|
-
{"Kafka", strings.Contains(goMod, "segmentio/kafka-go") || strings.Contains(goMod, "Shopify/sarama") || strings.Contains(goMod, "IBM/sarama") || strings.Contains(goMod, "ThreeDotsLabs/watermill-kafka")},
|
|
104
|
-
{"PostgreSQL", strings.Contains(goMod, "jackc/pgx") || strings.Contains(goMod, "lib/pq")},
|
|
105
|
-
{"Redis", strings.Contains(goMod, "go-redis/redis") || strings.Contains(goMod, "redis/go-redis")},
|
|
106
|
-
{"OpenTelemetry", strings.Contains(goMod, "go.opentelemetry.io/otel")},
|
|
107
|
-
{"River", strings.Contains(goMod, "riverqueue/river")},
|
|
108
|
-
{"Watermill", strings.Contains(goMod, "github.com/ThreeDotsLabs/watermill")},
|
|
109
|
-
{"Docker", exists(filepath.Join(root, "Dockerfile")) || exists(filepath.Join(root, "docker-compose.yml")) || exists(filepath.Join(root, "docker-compose.yaml"))},
|
|
110
|
-
{"Helm", hasHelm(root)},
|
|
111
|
-
}
|
|
112
|
-
out := make([]string, 0, len(markers))
|
|
113
|
-
for _, marker := range markers {
|
|
114
|
-
if marker.seen {
|
|
115
|
-
out = append(out, marker.name)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return out
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
func hasHelm(root string) bool {
|
|
122
|
-
if exists(filepath.Join(root, "Chart.yaml")) || exists(filepath.Join(root, "charts")) || exists(filepath.Join(root, "helm")) {
|
|
123
|
-
return true
|
|
124
|
-
}
|
|
125
|
-
entries, err := os.ReadDir(root)
|
|
126
|
-
if err != nil {
|
|
127
|
-
return false
|
|
128
|
-
}
|
|
129
|
-
for _, entry := range entries {
|
|
130
|
-
if entry.IsDir() && strings.HasPrefix(strings.ToLower(entry.Name()), ".helm") {
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return false
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
func exists(path string) bool {
|
|
138
|
-
_, err := os.Stat(path)
|
|
139
|
-
return err == nil
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
func read(path string) string {
|
|
143
|
-
contents, err := os.ReadFile(path)
|
|
144
|
-
if err != nil {
|
|
145
|
-
return ""
|
|
146
|
-
}
|
|
147
|
-
return strings.TrimSpace(string(contents))
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
func markdownTitle(md string) string {
|
|
151
|
-
fence := ""
|
|
152
|
-
for _, line := range strings.Split(md, "\n") {
|
|
153
|
-
trimmed := strings.TrimSpace(line)
|
|
154
|
-
marker := ""
|
|
155
|
-
if strings.HasPrefix(trimmed, "```") {
|
|
156
|
-
marker = "```"
|
|
157
|
-
} else if strings.HasPrefix(trimmed, "~~~") {
|
|
158
|
-
marker = "~~~"
|
|
159
|
-
}
|
|
160
|
-
if marker != "" {
|
|
161
|
-
if fence == "" {
|
|
162
|
-
fence = marker
|
|
163
|
-
} else if fence == marker {
|
|
164
|
-
fence = ""
|
|
165
|
-
}
|
|
166
|
-
continue
|
|
167
|
-
}
|
|
168
|
-
if fence != "" {
|
|
169
|
-
continue
|
|
170
|
-
}
|
|
171
|
-
if value, ok := strings.CutPrefix(trimmed, "# "); ok {
|
|
172
|
-
return strings.TrimSpace(value)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return ""
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
func slug(value string) string {
|
|
179
|
-
value = strings.ToLower(strings.TrimSpace(value))
|
|
180
|
-
var out strings.Builder
|
|
181
|
-
dash := false
|
|
182
|
-
for _, r := range value {
|
|
183
|
-
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
|
|
184
|
-
out.WriteRune(r)
|
|
185
|
-
dash = false
|
|
186
|
-
} else if out.Len() > 0 && !dash {
|
|
187
|
-
out.WriteByte('-')
|
|
188
|
-
dash = true
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return strings.TrimSuffix(out.String(), "-")
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
func title(value string) string {
|
|
195
|
-
parts := strings.FieldsFunc(value, func(r rune) bool { return r == '-' || r == '_' })
|
|
196
|
-
for i := range parts {
|
|
197
|
-
if parts[i] != "" {
|
|
198
|
-
parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return strings.Join(parts, " ")
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
func firstNonEmpty(values ...string) string {
|
|
205
|
-
for _, value := range values {
|
|
206
|
-
if strings.TrimSpace(value) != "" {
|
|
207
|
-
return value
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return ""
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// stated prefers what the manifest says to what the tree implies: a component
|
|
214
|
-
// whose manifests sit above its root, one of several in a repository, has
|
|
215
|
-
// nothing at its root for technologies to read.
|
|
216
|
-
func stated(told, inferred []string) []string {
|
|
217
|
-
if len(told) > 0 {
|
|
218
|
-
return told
|
|
219
|
-
}
|
|
220
|
-
return inferred
|
|
221
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"os"
|
|
6
|
-
"path/filepath"
|
|
7
|
-
"testing"
|
|
8
|
-
|
|
9
|
-
"github.com/shortlink-org/portolan/catalog"
|
|
10
|
-
"github.com/shortlink-org/portolan/plugin"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
func TestExtractsANeutralApplicationWithoutDDD(t *testing.T) {
|
|
14
|
-
root := t.TempDir()
|
|
15
|
-
mustWrite(t, filepath.Join(root, "README.md"), "# Flight support\n\nCoordinates providers.\n")
|
|
16
|
-
mustWrite(t, filepath.Join(root, "go.mod"), "module example.com/flight/support\n")
|
|
17
|
-
mustWrite(t, filepath.Join(root, "cmd", "support", "main.go"), "package main\n")
|
|
18
|
-
mustWrite(t, filepath.Join(root, "Dockerfile"), "FROM scratch\n")
|
|
19
|
-
mustWrite(t, filepath.Join(root, "Makefile"), "build: ## Compile the binary\n\tgo build ./cmd/support\n")
|
|
20
|
-
|
|
21
|
-
response, err := extract(plugin.Input{Root: root, Commit: "abc", GeneratedAt: "2026-09-06T00:00:00Z"}, Options{Group: "avia", Component: "support"})
|
|
22
|
-
if err != nil {
|
|
23
|
-
t.Fatal(err)
|
|
24
|
-
}
|
|
25
|
-
var got catalog.Catalog
|
|
26
|
-
if err := json.Unmarshal([]byte(response.Files[0].Contents), &got); err != nil {
|
|
27
|
-
t.Fatal(err)
|
|
28
|
-
}
|
|
29
|
-
group := got.Contexts[0]
|
|
30
|
-
component := group.Services[0]
|
|
31
|
-
if group.Kind != catalog.GroupKindSystem {
|
|
32
|
-
t.Errorf("group kind = %q", group.Kind)
|
|
33
|
-
}
|
|
34
|
-
if component.Kind != catalog.ComponentKindApplication {
|
|
35
|
-
t.Errorf("component kind = %q", component.Kind)
|
|
36
|
-
}
|
|
37
|
-
if component.Name != "Flight support" || component.Repo != "example.com/flight/support" {
|
|
38
|
-
t.Errorf("component = %+v", component)
|
|
39
|
-
}
|
|
40
|
-
if len(component.Technologies) != 2 || component.Technologies[0] != "Go" || component.Technologies[1] != "Docker" {
|
|
41
|
-
t.Errorf("technologies = %v", component.Technologies)
|
|
42
|
-
}
|
|
43
|
-
if len(component.Aggregates) != 0 {
|
|
44
|
-
t.Fatalf("invented aggregates: %+v", component.Aggregates)
|
|
45
|
-
}
|
|
46
|
-
if len(component.Commands) != 1 || component.Commands[0].Run != "make build" || component.Commands[0].Doc != "Compile the binary" {
|
|
47
|
-
t.Errorf("commands = %+v", component.Commands)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
func TestExplicitKindsWin(t *testing.T) {
|
|
52
|
-
root := t.TempDir()
|
|
53
|
-
response, err := extract(plugin.Input{Root: root}, Options{Group: "platform", GroupKind: "team", Component: "tools", ComponentKind: "cli"})
|
|
54
|
-
if err != nil {
|
|
55
|
-
t.Fatal(err)
|
|
56
|
-
}
|
|
57
|
-
var got catalog.Catalog
|
|
58
|
-
if err := json.Unmarshal([]byte(response.Files[0].Contents), &got); err != nil {
|
|
59
|
-
t.Fatal(err)
|
|
60
|
-
}
|
|
61
|
-
if got.Contexts[0].Kind != catalog.GroupKindTeam || got.Contexts[0].Services[0].Kind != catalog.ComponentKindCLI {
|
|
62
|
-
t.Fatalf("explicit kinds were lost: %+v", got.Contexts[0])
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
func TestMarkdownTitleIgnoresShellCommentsInsideFences(t *testing.T) {
|
|
67
|
-
md := "## Getting started\n\n```sh\nmake migrate\n# add superuser\n```\n\n# Actual service\n"
|
|
68
|
-
if got := markdownTitle(md); got != "Actual service" {
|
|
69
|
-
t.Fatalf("markdownTitle = %q", got)
|
|
70
|
-
}
|
|
71
|
-
md = "~~~sh\n# also not a title\n~~~\n"
|
|
72
|
-
if got := markdownTitle(md); got != "" {
|
|
73
|
-
t.Fatalf("markdownTitle without a real H1 = %q", got)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
func TestRecognizesMessagingLibrariesAsTechnologies(t *testing.T) {
|
|
78
|
-
root := t.TempDir()
|
|
79
|
-
mustWrite(t, filepath.Join(root, "go.mod"), `module example.com/worker
|
|
80
|
-
require (
|
|
81
|
-
github.com/riverqueue/river v0.26.0
|
|
82
|
-
github.com/ThreeDotsLabs/watermill v1.5.1
|
|
83
|
-
github.com/ThreeDotsLabs/watermill-kafka/v2 v2.5.0
|
|
84
|
-
)
|
|
85
|
-
`)
|
|
86
|
-
|
|
87
|
-
got := technologies(root)
|
|
88
|
-
for _, want := range []string{"Kafka", "River", "Watermill"} {
|
|
89
|
-
found := false
|
|
90
|
-
for _, value := range got {
|
|
91
|
-
if value == want {
|
|
92
|
-
found = true
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if !found {
|
|
96
|
-
t.Errorf("technologies %v do not contain %q", got, want)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
func mustWrite(t *testing.T, path, contents string) {
|
|
102
|
-
t.Helper()
|
|
103
|
-
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
104
|
-
t.Fatal(err)
|
|
105
|
-
}
|
|
106
|
-
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
|
107
|
-
t.Fatal(err)
|
|
108
|
-
}
|
|
109
|
-
}
|