|
| 1 | +# osapifix |
| 2 | + |
| 3 | +`osapifix` migrates a Go module across opensearch-go major versions. The source major is detected from the module's imports and the target defaults to the newest supported version, so the common invocation is: |
| 4 | + |
| 5 | +```sh |
| 6 | +osapifix rewrite -w ./... |
| 7 | +``` |
| 8 | + |
| 9 | +Today it supports the v4 -> v5 hop. Additional hops (v3 -> v4, v2 -> v3) are added as data, without engine changes. |
| 10 | + |
| 11 | +## Install |
| 12 | + |
| 13 | +`osapifix` is a separate Go module. Build the binary from an `opensearch-go` checkout: |
| 14 | + |
| 15 | +```sh |
| 16 | +git clone https://github.com/opensearch-project/opensearch-go |
| 17 | +(cd opensearch-go/cmd/osapifix && go build -o "$(go env GOPATH)/bin/osapifix" .) |
| 18 | +``` |
| 19 | + |
| 20 | +The examples below assume the resulting `osapifix` binary is on your `PATH`. |
| 21 | + |
| 22 | +## Subcommands |
| 23 | + |
| 24 | +### `rewrite` - API-shape migration (pre-compile) |
| 25 | + |
| 26 | +Rewrites source that uses the old major's API shapes (type names, method paths, field spellings) into the target's. It is purely syntactic (go/parser + astutil + go/printer) so it runs before the code compiles against the target. |
| 27 | + |
| 28 | +```sh |
| 29 | +osapifix rewrite [-src=auto] [-dst=vN] [-w] [dir] |
| 30 | +``` |
| 31 | + |
| 32 | +- `-src` - source major (`v4`), or `auto` (default) to detect from imports. |
| 33 | +- `-dst` - target major (`v5`), defaults to the newest supported version. |
| 34 | +- `-w` - apply changes. Omitted, `rewrite` is a dry run that prints the edits. |
| 35 | +- `dir` - module directory (default `.`). A `./...` pattern is accepted and resolved to its base directory. |
| 36 | + |
| 37 | +Writes are sandboxed to the target module directory via `os.Root`. |
| 38 | + |
| 39 | +### `vet` - runtime-hazard cleanup (post-compile) |
| 40 | + |
| 41 | +The target's precise types (`*int64`, `*string`, ...) flow into `any` sinks such as testify's `Equal`/`Greater`, compiling cleanly but failing at run time with "Elements should be the same type". `vet` runs go/analysis analyzers (`typedassert.go`) that catch these; `-fix` applies the safe rewrites. Run it after `rewrite` and a successful build. |
| 42 | + |
| 43 | +```sh |
| 44 | +osapifix vet [-fix] ./... |
| 45 | +``` |
| 46 | + |
| 47 | +## Typical flow (v4 -> v5) |
| 48 | + |
| 49 | +```sh |
| 50 | +osapifix rewrite -w ./... |
| 51 | +go get github.com/opensearch-project/opensearch-go/v5 && go build ./... |
| 52 | +osapifix vet -fix ./... |
| 53 | +``` |
| 54 | + |
| 55 | +## How it works |
| 56 | + |
| 57 | +Each adjacent transition (vN -> vN+1) is a `Hop`: hand-authored tables of type renames, field dispositions, method regroups, removed helpers, and semantic followups, keyed against two committed API surfaces (`surface_vN.json`). A migration request resolves to the ordered list of hops between source and target, applied one at a time - rewrite, rebuild against the intermediate version so the type-aware pass can load, then the next hop. Intermediate versions are not surfaced to the operator. |
| 58 | + |
| 59 | +| File | Responsibility | |
| 60 | +| ------------------ | ----------------------------------------------------------------------------- | |
| 61 | +| `transitions.go` | Version-neutral types (`Major`, `Hop`) and the `surfaces` / `hops` registries | |
| 62 | +| `hop_vN_to_vN1.go` | Hand-authored migration data for one hop | |
| 63 | +| `plan.go` | `planChain(src, dst)` -> the ordered per-hop plans | |
| 64 | +| `detect.go` | Source major from the module's imports | |
| 65 | +| `applydelta.go` | Type-aware AST rewriter | |
| 66 | +| `internal/surface` | Surface model and `DeriveDelta` between two surfaces | |
| 67 | +| `cmd/gensurface` | Generates a version's committed surface JSON | |
| 68 | + |
| 69 | +### Field dispositions |
| 70 | + |
| 71 | +A field that vanishes on the target is governed by an explicit `FieldDisposition`, matched by (source pkg + type + field): |
| 72 | + |
| 73 | +- **rename** - rewrite to the target field. The target type is stated explicitly, so a field may move across a type rename (e.g. `DocumentGetReq#DocumentID` -> `GetReq#ID`). |
| 74 | +- **remove** - drop the composite-literal key. |
| 75 | +- **manual** - the field's data relocated (e.g. a response collapsed to a raw `Body`); flagged for a human. |
| 76 | + |
| 77 | +A vanished field with no disposition fails the run with an `osapifix bug` error; the tool does not infer rename-versus-remove. Dispositions are verified against the surfaces by `TestHopFieldDispositionsAgainstSurfaces` and are established from source: response-field renames by a shared JSON wire tag, request-field renames by the v4 code that assembles the field into the spec-named element. |
| 78 | + |
| 79 | +### Source detection |
| 80 | + |
| 81 | +The major version is read from import paths (`.../opensearch-go/v4/...`), not `go.mod`: a partially migrated module may `require` both majors, and `go.mod` may name the target while call sites are still source-shaped. A module importing multiple majors migrates from the lowest; the rest are reported. |
| 82 | + |
| 83 | +## Adding a hop (e.g. v3 -> v4) |
| 84 | + |
| 85 | +1. Generate both endpoint surfaces with `cmd/gensurface`: |
| 86 | + |
| 87 | + ```sh |
| 88 | + go run ./cmd/gensurface -dir <v3-module-dir> -version v3 \ |
| 89 | + -patterns ./opensearchapi,.,./opensearchtransport -out surface_v3.json |
| 90 | + ``` |
| 91 | + |
| 92 | +2. Embed each surface (`//go:embed`) in `main.go` and register it in the `surfaces` map (`transitions.go`). |
| 93 | + |
| 94 | +3. Author `hop_v3_to_v4.go`: diff the surfaces and rule on every changed type, field, and method. Follow `hop_v4_to_v5.go`. |
| 95 | + |
| 96 | +4. Register the hop in the `hops` map (`transitions.go`). |
| 97 | + |
| 98 | +5. Add `hop_v3_to_v4_test.go` for version-specific facts. The drift guards validate the tables against the surfaces automatically; a `rewrite` against real v3 code fails loudly on any unruled field. |
| 99 | + |
| 100 | +## Testing |
| 101 | + |
| 102 | +```sh |
| 103 | +go test ./... |
| 104 | +``` |
| 105 | + |
| 106 | +| File | Covers | |
| 107 | +| ----------------------------------------- | --------------------------------------------------------------------------------- | |
| 108 | +| `plan_test.go` | `planChain` and `DeriveDelta` field dispositions, via synthetic v7/v8/v9 surfaces | |
| 109 | +| `delta_test.go` | Drift guards over every hop's type renames and field dispositions | |
| 110 | +| `hop_v4_to_v5_test.go` | v4 -> v5 version-specific facts | |
| 111 | +| `detect_test.go` | Source detection, version parsing, directory resolution | |
| 112 | +| `internal/surface/delta_internal_test.go` | Surface diffing internals | |
| 113 | + |
| 114 | +## Limitations |
| 115 | + |
| 116 | +- `vet` analyzers are v5-specific (`TypedAssertAnalyzer`) and target a single version; they do not chain across hops. |
| 117 | +- A module importing multiple majors migrates from the lowest; per-import-site source selection is not implemented. |
0 commit comments