@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
package/plugins/openapi/ids.go
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
// Package openapi is what two extractors agree on about an OpenAPI document:
|
|
2
|
-
// how its interfaces are named in the catalog, and which operation answers on
|
|
3
|
-
// which route. The server side reads the document to say what a service
|
|
4
|
-
// provides; the client side reads the copy vendored beside a generated client
|
|
5
|
-
// to say what a service calls. Both have to spell `auth.v1.Sessions/login` the
|
|
6
|
-
// same way, or the call would never resolve to the method, and one place is
|
|
7
|
-
// how they do.
|
|
8
|
-
package openapi
|
|
9
|
-
|
|
10
|
-
import (
|
|
11
|
-
"fmt"
|
|
12
|
-
"os"
|
|
13
|
-
"strings"
|
|
14
|
-
|
|
15
|
-
"gopkg.in/yaml.v3"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
// APIID is the document's title and major version: `auth` 1.0.0 gives
|
|
19
|
-
// `auth.v1`. It prefixes every interface the document declares.
|
|
20
|
-
func APIID(title, version string) string {
|
|
21
|
-
name := title
|
|
22
|
-
if name == "" {
|
|
23
|
-
name = "api"
|
|
24
|
-
}
|
|
25
|
-
name = strings.ReplaceAll(strings.ToLower(name), " ", "-")
|
|
26
|
-
|
|
27
|
-
if major, _, ok := strings.Cut(version, "."); ok && major != "" {
|
|
28
|
-
return name + ".v" + major
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return name
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// DocumentAPIID is the id a document says it goes by in the estate, or the one
|
|
35
|
-
// built from its title and version when it says nothing.
|
|
36
|
-
//
|
|
37
|
-
// `x-portolan-api` in `info` is for a copy vendored from outside the estate:
|
|
38
|
-
// Stripe's document is titled "Stripe API" and versioned "2026-08-26.dahlia",
|
|
39
|
-
// and `stripe-api.v2026-08-26` on every arrow would be the document's words
|
|
40
|
-
// where the estate wants its own. The copy is the consumer's translation
|
|
41
|
-
// boundary already, so it is the one place the estate's name may be written,
|
|
42
|
-
// and both sides of a call read it from there rather than from two manifests.
|
|
43
|
-
func DocumentAPIID(declared, title, version string) string {
|
|
44
|
-
if declared = strings.TrimSpace(declared); declared != "" {
|
|
45
|
-
return declared
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return APIID(title, version)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ExternalID is the id a system outside the estate goes by when nothing but
|
|
52
|
-
// its own document names it: the document's title as a slug, with a trailing
|
|
53
|
-
// "API" dropped because the system is the thing, not its interface. "Gordian
|
|
54
|
-
// Flights & Ancillaries API" is gordian-flights-ancillaries; "Stripe API" is
|
|
55
|
-
// stripe. Both sides of a call - the extractor that reads the vendored copy
|
|
56
|
-
// for what the system answers on, and the one that reads the client beside it
|
|
57
|
-
// for what this service calls - derive it here, so neither needs the manifest
|
|
58
|
-
// to say it.
|
|
59
|
-
func ExternalID(title string) string {
|
|
60
|
-
var b strings.Builder
|
|
61
|
-
dash := false
|
|
62
|
-
for _, r := range strings.ToLower(strings.TrimSpace(title)) {
|
|
63
|
-
switch {
|
|
64
|
-
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
|
65
|
-
b.WriteRune(r)
|
|
66
|
-
dash = false
|
|
67
|
-
case !dash && b.Len() > 0:
|
|
68
|
-
b.WriteByte('-')
|
|
69
|
-
dash = true
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
id := strings.TrimRight(b.String(), "-")
|
|
73
|
-
if trimmed := strings.TrimSuffix(id, "-api"); trimmed != "" {
|
|
74
|
-
id = trimmed
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return id
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Title is the human form of a tag: users becomes Users, price_list becomes
|
|
81
|
-
// PriceList, because it sits in an id beside a proto-shaped service name.
|
|
82
|
-
func Title(name string) string {
|
|
83
|
-
var b strings.Builder
|
|
84
|
-
for _, word := range strings.FieldsFunc(name, func(r rune) bool { return r == '_' || r == '-' || r == ' ' }) {
|
|
85
|
-
runes := []rune(word)
|
|
86
|
-
if runes[0] >= 'a' && runes[0] <= 'z' {
|
|
87
|
-
runes[0] = runes[0] - 'a' + 'A'
|
|
88
|
-
}
|
|
89
|
-
b.WriteString(string(runes))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return b.String()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// InterfaceID is the contract declared by the OpenAPI document. Tags organise
|
|
96
|
-
// operations inside that contract; they do not create separate interfaces.
|
|
97
|
-
func InterfaceID(api, _ string) string { return api }
|
|
98
|
-
|
|
99
|
-
// Verbs, in the order the paths object lists them by convention.
|
|
100
|
-
var Verbs = []string{"get", "put", "post", "delete", "options", "head", "patch", "trace"}
|
|
101
|
-
|
|
102
|
-
// Operation is one route of the document and the name it goes by.
|
|
103
|
-
type Operation struct {
|
|
104
|
-
// ID is the operationId, or `VERB /path` when the document has none.
|
|
105
|
-
ID string
|
|
106
|
-
// Tag is the first tag, retained as document metadata. It does not change
|
|
107
|
-
// the identity of the contract.
|
|
108
|
-
Tag string
|
|
109
|
-
// Verb is upper case: POST.
|
|
110
|
-
Verb string
|
|
111
|
-
// Path is the template as written: /v1/users/{userId}.
|
|
112
|
-
Path string
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Interface is the id of the interface the operation belongs to.
|
|
116
|
-
func (o Operation) Interface(api string) string { return InterfaceID(api, o.Tag) }
|
|
117
|
-
|
|
118
|
-
// CallID is what a call to this operation is known by in the catalog.
|
|
119
|
-
func (o Operation) CallID(api string) string { return o.Interface(api) + "/" + o.ID }
|
|
120
|
-
|
|
121
|
-
// Spec is the part of a document these extractors read.
|
|
122
|
-
type Spec struct {
|
|
123
|
-
API string
|
|
124
|
-
Operations []Operation
|
|
125
|
-
|
|
126
|
-
// Title, Description and DocsURL are what the document says about the
|
|
127
|
-
// system it describes, for naming that system when the manifest does not.
|
|
128
|
-
Title string
|
|
129
|
-
Description string
|
|
130
|
-
DocsURL string
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Read loads a document and lists its operations. The api id may be
|
|
134
|
-
// overridden by whoever calls, since the manifest may name one.
|
|
135
|
-
func Read(path string) (*Spec, error) {
|
|
136
|
-
contents, err := os.ReadFile(path)
|
|
137
|
-
if err != nil {
|
|
138
|
-
return nil, err
|
|
139
|
-
}
|
|
140
|
-
var root yaml.Node
|
|
141
|
-
if err := yaml.Unmarshal(contents, &root); err != nil {
|
|
142
|
-
return nil, fmt.Errorf("%s: %w", path, err)
|
|
143
|
-
}
|
|
144
|
-
node := &root
|
|
145
|
-
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
|
|
146
|
-
node = node.Content[0]
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
info := child(node, "info")
|
|
150
|
-
spec := &Spec{
|
|
151
|
-
API: DocumentAPIID(text(child(info, "x-portolan-api")), text(child(info, "title")), text(child(info, "version"))),
|
|
152
|
-
Title: text(child(info, "title")),
|
|
153
|
-
Description: text(child(info, "description")),
|
|
154
|
-
DocsURL: text(child(node, "externalDocs", "url")),
|
|
155
|
-
}
|
|
156
|
-
for _, p := range entries(child(node, "paths")) {
|
|
157
|
-
for _, verb := range Verbs {
|
|
158
|
-
operation := child(p.value, verb)
|
|
159
|
-
if operation == nil {
|
|
160
|
-
continue
|
|
161
|
-
}
|
|
162
|
-
id := text(child(operation, "operationId"))
|
|
163
|
-
if id == "" {
|
|
164
|
-
id = strings.ToUpper(verb) + " " + p.key
|
|
165
|
-
}
|
|
166
|
-
tags := list(child(operation, "tags"))
|
|
167
|
-
tag := ""
|
|
168
|
-
if len(tags) > 0 {
|
|
169
|
-
tag = tags[0]
|
|
170
|
-
}
|
|
171
|
-
spec.Operations = append(spec.Operations, Operation{ID: id, Tag: tag, Verb: strings.ToUpper(verb), Path: p.key})
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return spec, nil
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Find looks up an operation by verb and route. Both sides may spell a path
|
|
179
|
-
// parameter differently - `{userId}` in the document, `%s` in a generated
|
|
180
|
-
// client - so parameters are compared by position, not by name.
|
|
181
|
-
func (s *Spec) Find(verb, path string) (Operation, bool) {
|
|
182
|
-
want := strings.ToUpper(verb) + " " + shape(path)
|
|
183
|
-
for _, op := range s.Operations {
|
|
184
|
-
if op.Verb+" "+shape(op.Path) == want {
|
|
185
|
-
return op, true
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return Operation{}, false
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// shape replaces every parameter, however spelled, with one marker.
|
|
193
|
-
func shape(path string) string {
|
|
194
|
-
var b strings.Builder
|
|
195
|
-
for _, segment := range strings.Split(strings.TrimSuffix(path, "/"), "/") {
|
|
196
|
-
if strings.HasPrefix(segment, "{") || segment == "%s" || segment == "%v" || segment == "%d" {
|
|
197
|
-
b.WriteString("/*")
|
|
198
|
-
|
|
199
|
-
continue
|
|
200
|
-
}
|
|
201
|
-
b.WriteString("/" + segment)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return b.String()
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
type entry struct {
|
|
208
|
-
key string
|
|
209
|
-
value *yaml.Node
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
func entries(node *yaml.Node) []entry {
|
|
213
|
-
if node == nil || node.Kind != yaml.MappingNode {
|
|
214
|
-
return nil
|
|
215
|
-
}
|
|
216
|
-
var out []entry
|
|
217
|
-
for i := 0; i+1 < len(node.Content); i += 2 {
|
|
218
|
-
out = append(out, entry{key: node.Content[i].Value, value: node.Content[i+1]})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return out
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
func child(node *yaml.Node, keys ...string) *yaml.Node {
|
|
225
|
-
for _, key := range keys {
|
|
226
|
-
if node == nil || node.Kind != yaml.MappingNode {
|
|
227
|
-
return nil
|
|
228
|
-
}
|
|
229
|
-
var next *yaml.Node
|
|
230
|
-
for i := 0; i+1 < len(node.Content); i += 2 {
|
|
231
|
-
if node.Content[i].Value == key {
|
|
232
|
-
next = node.Content[i+1]
|
|
233
|
-
|
|
234
|
-
break
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
node = next
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return node
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
func text(node *yaml.Node) string {
|
|
244
|
-
if node == nil || node.Kind != yaml.ScalarNode {
|
|
245
|
-
return ""
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return strings.TrimSpace(node.Value)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
func list(node *yaml.Node) []string {
|
|
252
|
-
if node == nil || node.Kind != yaml.SequenceNode {
|
|
253
|
-
return nil
|
|
254
|
-
}
|
|
255
|
-
var out []string
|
|
256
|
-
for _, item := range node.Content {
|
|
257
|
-
out = append(out, text(item))
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return out
|
|
261
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
package openapi
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"os"
|
|
5
|
-
"path/filepath"
|
|
6
|
-
"testing"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
func TestIDs(t *testing.T) {
|
|
10
|
-
if got := APIID("Auth", "1.2.3"); got != "auth.v1" {
|
|
11
|
-
t.Errorf("APIID = %q", got)
|
|
12
|
-
}
|
|
13
|
-
if got := APIID("", ""); got != "api" {
|
|
14
|
-
t.Errorf("APIID of nothing = %q", got)
|
|
15
|
-
}
|
|
16
|
-
if got := Title("price_list"); got != "PriceList" {
|
|
17
|
-
t.Errorf("Title = %q", got)
|
|
18
|
-
}
|
|
19
|
-
if got := InterfaceID("auth.v1", "sessions"); got != "auth.v1" {
|
|
20
|
-
t.Errorf("InterfaceID = %q", got)
|
|
21
|
-
}
|
|
22
|
-
if got := InterfaceID("auth.v1", ""); got != "auth.v1" {
|
|
23
|
-
t.Errorf("InterfaceID without a tag = %q", got)
|
|
24
|
-
}
|
|
25
|
-
// A vendored copy may say what the estate calls it; a document that says
|
|
26
|
-
// nothing is named the way it always was.
|
|
27
|
-
if got := DocumentAPIID(" stripe.v1 ", "Stripe API", "2026-08-26.dahlia"); got != "stripe.v1" {
|
|
28
|
-
t.Errorf("DocumentAPIID with x-portolan-api = %q", got)
|
|
29
|
-
}
|
|
30
|
-
if got := DocumentAPIID("", "Stripe API", "2026-08-26.dahlia"); got != "stripe-api.v2026-08-26" {
|
|
31
|
-
t.Errorf("DocumentAPIID without = %q", got)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// A system outside the estate is named by its document's title, and the
|
|
36
|
-
// title's "API" is the interface, not the system.
|
|
37
|
-
func TestExternalID(t *testing.T) {
|
|
38
|
-
for title, want := range map[string]string{
|
|
39
|
-
"Gordian Flights & Ancillaries API": "gordian-flights-ancillaries",
|
|
40
|
-
"Stripe API": "stripe",
|
|
41
|
-
" Partner REST API ": "partner-rest",
|
|
42
|
-
"API": "api",
|
|
43
|
-
"": "",
|
|
44
|
-
"Amadeus (Self-Service) v2": "amadeus-self-service-v2",
|
|
45
|
-
} {
|
|
46
|
-
if got := ExternalID(title); got != want {
|
|
47
|
-
t.Errorf("ExternalID(%q) = %q, want %q", title, got, want)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const doc = `openapi: 3.0.3
|
|
53
|
-
info:
|
|
54
|
-
title: auth
|
|
55
|
-
version: 1.0.0
|
|
56
|
-
paths:
|
|
57
|
-
/v1/sessions:
|
|
58
|
-
post:
|
|
59
|
-
operationId: login
|
|
60
|
-
tags: [sessions]
|
|
61
|
-
/v1/users/{userId}:
|
|
62
|
-
get:
|
|
63
|
-
operationId: getUser
|
|
64
|
-
tags: [users]
|
|
65
|
-
/v1/health:
|
|
66
|
-
get: {}
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
func TestReadAndFind(t *testing.T) {
|
|
70
|
-
path := filepath.Join(t.TempDir(), "openapi.yaml")
|
|
71
|
-
if err := os.WriteFile(path, []byte(doc), 0o644); err != nil {
|
|
72
|
-
t.Fatal(err)
|
|
73
|
-
}
|
|
74
|
-
spec, err := Read(path)
|
|
75
|
-
if err != nil {
|
|
76
|
-
t.Fatal(err)
|
|
77
|
-
}
|
|
78
|
-
if spec.API != "auth.v1" || len(spec.Operations) != 3 {
|
|
79
|
-
t.Fatalf("spec = %+v", spec)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
login, ok := spec.Find("post", "/v1/sessions")
|
|
83
|
-
if !ok || login.CallID(spec.API) != "auth.v1/login" {
|
|
84
|
-
t.Errorf("login = %+v %v", login, ok)
|
|
85
|
-
}
|
|
86
|
-
// A generated client spells the parameter as a format verb.
|
|
87
|
-
user, ok := spec.Find("GET", "/v1/users/%s")
|
|
88
|
-
if !ok || user.CallID(spec.API) != "auth.v1/getUser" {
|
|
89
|
-
t.Errorf("getUser = %+v %v", user, ok)
|
|
90
|
-
}
|
|
91
|
-
health, ok := spec.Find("GET", "/v1/health")
|
|
92
|
-
if !ok || health.CallID(spec.API) != "auth.v1/GET /v1/health" {
|
|
93
|
-
t.Errorf("an operation without an id or tag = %+v %v", health, ok)
|
|
94
|
-
}
|
|
95
|
-
if _, ok := spec.Find("DELETE", "/v1/sessions"); ok {
|
|
96
|
-
t.Error("a verb the document does not declare was found")
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -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: "verify-codeowners",
|
|
15
|
-
Summary: "Reads the repository's CODEOWNERS against the catalog: who to ask about each service, which services nobody owns, and which rules own nothing.",
|
|
16
|
-
Phases: []string{plugin.PhaseVerify},
|
|
17
|
-
Options: optionsSchema,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Command verify-codeowners says who to ask about each service, by reading the
|
|
2
|
-
// CODEOWNERS file the repository already keeps.
|
|
3
|
-
//
|
|
4
|
-
// Ownership is the question a catalog is opened for and the one it answered
|
|
5
|
-
// worst: `owner` on a flow or a store means the bounded context, not a team,
|
|
6
|
-
// so "who do I ask about shop.oms" had no answer anywhere on the page. The
|
|
7
|
-
// answer was already written down - every repository with review rules has it,
|
|
8
|
-
// in a file the forge itself enforces - and typing it a second time into the
|
|
9
|
-
// manifest would make the copy the thing that goes stale.
|
|
10
|
-
//
|
|
11
|
-
// So it is read, never written, and never resolved. A handle is what the file
|
|
12
|
-
// says and what a reviewer types; turning `@acme/oms-team` into the people in
|
|
13
|
-
// it is a call to a forge's API, which needs a credential this deliberately
|
|
14
|
-
// does not have and would answer differently tomorrow.
|
|
15
|
-
//
|
|
16
|
-
// It is a verifier rather than an extractor because a rule is a path and only
|
|
17
|
-
// the merged catalog knows where each service is. It earns the name twice
|
|
18
|
-
// over: a service no rule matches, and a rule that matches no service, are
|
|
19
|
-
// both reported - the second being the failure a CODEOWNERS file has and can
|
|
20
|
-
// never report about itself, a team believing it owns something it does not.
|
|
21
|
-
//
|
|
22
|
-
// Nothing here is forge-specific. GitHub, GitLab and Gitea read the same file
|
|
23
|
-
// from the same three places; where GitLab's sections change whose rule wins,
|
|
24
|
-
// the file is read the flatter way and said so out loud.
|
|
25
|
-
//
|
|
26
|
-
// {
|
|
27
|
-
// "plugins": [
|
|
28
|
-
// { "name": "codeowners", "process": { "command": "go", "args": ["run", "./plugins/verify-codeowners"] } }
|
|
29
|
-
// ],
|
|
30
|
-
// "verify": [
|
|
31
|
-
// { "plugin": "codeowners", "in": ".github", "out": "data", "options": { "out": "owners.json" } }
|
|
32
|
-
// ]
|
|
33
|
-
// }
|
|
34
|
-
//
|
|
35
|
-
// Point `in` at the directory the file is in rather than at the repository
|
|
36
|
-
// root. The host dates a fragment from the last commit to touch the step's
|
|
37
|
-
// input, and the subject of this one is the CODEOWNERS file: rooted at the
|
|
38
|
-
// repository, it would be restamped by every commit ever made.
|
|
39
|
-
package main
|
|
40
|
-
|
|
41
|
-
import (
|
|
42
|
-
"fmt"
|
|
43
|
-
"io"
|
|
44
|
-
"os"
|
|
45
|
-
|
|
46
|
-
"github.com/shortlink-org/portolan/plugin"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
// Options are what the manifest tells the verifier.
|
|
50
|
-
type Options struct {
|
|
51
|
-
// File is the CODEOWNERS to read, relative to the input root. Left out,
|
|
52
|
-
// the three places a forge looks: CODEOWNERS, .github/CODEOWNERS and
|
|
53
|
-
// docs/CODEOWNERS, in that order.
|
|
54
|
-
File string `json:"file,omitempty"`
|
|
55
|
-
|
|
56
|
-
// Out names the fragment file.
|
|
57
|
-
Out string `json:"out,omitempty"`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
func main() {
|
|
61
|
-
if err := run(os.Stdin, os.Stdout); err != nil {
|
|
62
|
-
fmt.Fprintln(os.Stderr, "portolan-verify-codeowners:", err)
|
|
63
|
-
os.Exit(1)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
func run(stdin io.Reader, stdout io.Writer) error {
|
|
68
|
-
return plugin.Serve(stdin, stdout, descriptor(), func(req plugin.Request, opts Options) (plugin.Response, error) {
|
|
69
|
-
if req.Input.Root == "" {
|
|
70
|
-
return plugin.Response{}, fmt.Errorf("no input root: there is nowhere to look for a CODEOWNERS")
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return verify(req, opts)
|
|
74
|
-
})
|
|
75
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
// Whether a rule owns a directory.
|
|
4
|
-
//
|
|
5
|
-
// CODEOWNERS patterns are gitignore patterns, and the question asked here is
|
|
6
|
-
// narrower than the one gitignore answers: not "does this pattern match this
|
|
7
|
-
// file" but "does this pattern own this SERVICE", where a service is a
|
|
8
|
-
// directory. A rule owns a directory when it matches the directory itself or
|
|
9
|
-
// anything above it - `services/` owns `services/oms` - and does not when it
|
|
10
|
-
// matches only something inside it: `services/oms/internal` is a rule about
|
|
11
|
-
// part of the service, and reading it as ownership of the whole would hand a
|
|
12
|
-
// team a page they never asked for.
|
|
13
|
-
//
|
|
14
|
-
// The two shapes a pattern comes in are the whole of the rule:
|
|
15
|
-
//
|
|
16
|
-
// - anchored, when it has a `/` anywhere but the end - `services/oms`,
|
|
17
|
-
// `/docs` - and is matched against the path from the repository root;
|
|
18
|
-
// - floating, when it is a single segment - `*.go`, `internal` - and matches
|
|
19
|
-
// that segment at any depth, which is how gitignore has always read one.
|
|
20
|
-
//
|
|
21
|
-
// `*` and `?` stay inside a segment, `**` spans any number of them.
|
|
22
|
-
|
|
23
|
-
import (
|
|
24
|
-
"path"
|
|
25
|
-
"strings"
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
// owns reports whether pattern owns the directory at target, both written
|
|
29
|
-
// repository-relative and with forward slashes.
|
|
30
|
-
func owns(pattern, target string) bool {
|
|
31
|
-
pattern = strings.TrimSpace(pattern)
|
|
32
|
-
target = strings.Trim(target, "/")
|
|
33
|
-
if pattern == "" || target == "" {
|
|
34
|
-
return false
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// A pattern that is only a `/*` or `*` owns everything at that level.
|
|
38
|
-
anchored := strings.HasPrefix(pattern, "/") || strings.Contains(strings.TrimSuffix(pattern, "/"), "/")
|
|
39
|
-
pattern = strings.Trim(pattern, "/")
|
|
40
|
-
if pattern == "" {
|
|
41
|
-
return true // "/" - the whole repository.
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
segments := strings.Split(target, "/")
|
|
45
|
-
if !anchored {
|
|
46
|
-
// Any segment matching means the directory it names is owned, and so
|
|
47
|
-
// is everything under it - including this one.
|
|
48
|
-
for _, segment := range segments {
|
|
49
|
-
if ok, _ := path.Match(pattern, segment); ok {
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return false
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return matchPrefix(strings.Split(pattern, "/"), segments)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// matchPrefix reports whether the pattern matches the target or a prefix of
|
|
61
|
-
// it. A pattern longer than the target names something inside the directory
|
|
62
|
-
// and owns no more of it than that.
|
|
63
|
-
func matchPrefix(pattern, target []string) bool {
|
|
64
|
-
if len(pattern) == 0 {
|
|
65
|
-
return true // Every segment of the pattern matched; the rest is inside it.
|
|
66
|
-
}
|
|
67
|
-
if pattern[0] == "**" {
|
|
68
|
-
// Zero or more segments: try here, then one deeper.
|
|
69
|
-
for i := 0; i <= len(target); i++ {
|
|
70
|
-
if matchPrefix(pattern[1:], target[i:]) {
|
|
71
|
-
return true
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return false
|
|
76
|
-
}
|
|
77
|
-
if len(target) == 0 {
|
|
78
|
-
return false
|
|
79
|
-
}
|
|
80
|
-
if ok, _ := path.Match(pattern[0], target[0]); !ok {
|
|
81
|
-
return false
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return matchPrefix(pattern[1:], target[1:])
|
|
85
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import "testing"
|
|
4
|
-
|
|
5
|
-
// The rule the whole plugin turns on: a pattern owns a directory when it names
|
|
6
|
-
// the directory or anything above it, and does not when it names only
|
|
7
|
-
// something inside it.
|
|
8
|
-
func TestOwns(t *testing.T) {
|
|
9
|
-
cases := []struct {
|
|
10
|
-
pattern string
|
|
11
|
-
target string
|
|
12
|
-
want bool
|
|
13
|
-
why string
|
|
14
|
-
}{
|
|
15
|
-
{"examples/shop/oms", "examples/shop/oms", true, "the directory itself"},
|
|
16
|
-
{"examples/shop", "examples/shop/oms", true, "a directory above it"},
|
|
17
|
-
{"examples/", "examples/shop/oms", true, "a trailing slash is still a directory"},
|
|
18
|
-
{"/examples", "examples/shop/oms", true, "anchored at the root"},
|
|
19
|
-
{"*", "examples/shop/oms", true, "everything"},
|
|
20
|
-
{"/", "examples/shop/oms", true, "the whole repository"},
|
|
21
|
-
|
|
22
|
-
{"examples/shop/oms/internal", "examples/shop/oms", false, "a rule about part of the service is not a rule about the service"},
|
|
23
|
-
{"examples/auth", "examples/shop/oms", false, "a sibling"},
|
|
24
|
-
{"docs", "examples/shop/oms", false, "somewhere else entirely"},
|
|
25
|
-
|
|
26
|
-
// Floating patterns match at any depth, which is what gitignore has
|
|
27
|
-
// always meant by a pattern with no slash in it.
|
|
28
|
-
{"oms", "examples/shop/oms", true, "a single segment matches at any depth"},
|
|
29
|
-
{"shop", "examples/shop/oms", true, "including a segment above"},
|
|
30
|
-
{"om", "examples/shop/oms", false, "and matches whole segments, not prefixes"},
|
|
31
|
-
|
|
32
|
-
{"examples/*/oms", "examples/shop/oms", true, "a star stays inside its segment"},
|
|
33
|
-
{"examples/*", "examples/shop/oms", true, "and matches the segment above"},
|
|
34
|
-
{"examples/*/oms", "examples/shop/cart/oms", false, "so it does not cross a slash"},
|
|
35
|
-
{"examples/**/oms", "examples/shop/cart/oms", true, "two stars do"},
|
|
36
|
-
{"examples/**", "examples/shop/oms", true, "two stars also match nothing at all"},
|
|
37
|
-
|
|
38
|
-
{"", "examples/shop/oms", false, "an empty pattern owns nothing"},
|
|
39
|
-
{"examples", "", false, "and nothing is owned by no pattern"},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for _, c := range cases {
|
|
43
|
-
if got := owns(c.pattern, c.target); got != c.want {
|
|
44
|
-
t.Errorf("owns(%q, %q) = %v, want %v - %s", c.pattern, c.target, got, c.want, c.why)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|