go icon indicating copy to clipboard operation
go copied to clipboard

x/exp/cmd/apidiff: reports bogus changes between identical export data

Open pohly opened this issue 9 months ago • 1 comments

Go version

go version go1.22.0 linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/pohly/.cache/go-build'
GOENV='/home/pohly/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/nvme/gopath/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/nvme/gopath'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/nvme/gopath/go-1.22.0'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/nvme/gopath/go-1.22.0/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.22.0'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/nvme/gopath/src/k8s.io/kubernetes/go.mod'
GOWORK='/nvme/gopath/src/k8s.io/kubernetes/go.work'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3365691786=/tmp/go-build -gno-record-gcc-switches'

What did you do?

I am running apidiff -w twice, then compare the exported data. Specific steps:

  • Check out https://github.com/kubernetes/kubernetes.
  • git checkout 17854f0e0a153b06f9d0db096e2cd8ab2fa89c11 (or later - needs to include https://github.com/kubernetes/kubernetes/commit/0c7370bb851c15825d30a516722139ccccca0cfc).
  • go install golang.org/x/exp/cmd/[email protected] (current latest)
  • apidiff -m -w /tmp/after.out ./staging/src/k8s.io/client-go
  • apidiff -m /tmp/after.out /tmp/after.out (same file!)

What did you see happen?

Ignoring internal package k8s.io/client-go/applyconfigurations/internal
Ignoring internal package k8s.io/client-go/tools/internal/events
Ignoring internal package k8s.io/client-go/applyconfigurations/internal
Ignoring internal package k8s.io/client-go/tools/internal/events
Incompatible changes:
- ./util/workqueue.BucketRateLimiter: changed from TypedBucketRateLimiter[any] to TypedBucketRateLimiter[T comparable]
- ./util/workqueue.DelayingQueueConfig: changed from TypedDelayingQueueConfig[any] to TypedDelayingQueueConfig[T comparable]
- ./util/workqueue.ItemExponentialFailureRateLimiter: changed from TypedItemExponentialFailureRateLimiter[any] to TypedItemExponentialFailureRateLimiter[T comparable]
- ./util/workqueue.ItemFastSlowRateLimiter: changed from TypedItemFastSlowRateLimiter[any] to TypedItemFastSlowRateLimiter[T comparable]
- ./util/workqueue.MaxOfRateLimiter: changed from TypedMaxOfRateLimiter[any] to TypedMaxOfRateLimiter[T comparable]
- ./util/workqueue.QueueConfig: changed from TypedQueueConfig[any] to TypedQueueConfig[T comparable]
- ./util/workqueue.RateLimitingQueueConfig: changed from TypedRateLimitingQueueConfig[any] to TypedRateLimitingQueueConfig[T comparable]
- ./util/workqueue.Type: changed from Typed[any] to Typed[t comparable]
- ./util/workqueue.WithMaxWaitRateLimiter: changed from TypedWithMaxWaitRateLimiter[any] to TypedWithMaxWaitRateLimiter[T comparable]

Note that e.g. type BucketRateLimiter = TypedBucketRateLimiter[any]. There is no TypedBucketRateLimiter[T comparable].

What did you expect to see?

No changes because the exported data is identical.

pohly avatar Apr 26 '24 08:04 pohly

I like option B. It’s not breaking (which in the current alpha phase of the package doesn’t really matter, but still…). Also, there’s probably more info contained within response than just the status code, so this would be a handy way to have the “meta” stuff separated from the actual data/error.

edit: I’ll use the library in a react-router/Remix project, so having more than just the status code available in an explicit manner would certainly be super helpful.

Just curious: Why do you not want to use generics?

HerrBertling avatar Apr 08 '23 09:04 HerrBertling

True—breaking changes don’t matter much in pre-1.0. But I think the API / usability is everything, and I do think sweating the details matter! I should have put more emphasis on “easy to use” more than “breaking changes.” I also would rather err on the side of “happy path is easy/terse; complex path is mildly annoying“ rather than “everything is equally mildly annoying.” 😄

Why do you not want to use generics?

The main README has a note on this, but that’s a path to errors. Imagine the lowest-common-denominator in a codebase: not yourself, but either someone that doesn’t know better, or someone who is trying to shortcut everything. Imagine having the wrong generic, or simply providing <any> everywhere. That’s effectively as good as not using TypeScript! And further, I believe generics are a last-resort when introspection fails. And within the realm of your OpenAPI schema, I think it’s more helpful to assume everything can be introspected. No errors, no shortcuts, and even better usability!

drwpow avatar Apr 11 '23 16:04 drwpow

So v0.1.0 does somewhat solve this in that it creates unions of “good” and “bad” types. Theoretically, now, if you have different schemas for different error codes (like different shapes entirely), then that will now be reflected when you check { error }. Likewise for { data } (though that is much less-common to have multiple different responses for the same method that have wildly-varying shapes).

I’m leaving this issue open for now, because I want to investigate this further. But I think for various reasons, we’ll be going forward with Option B mostly because that’s lighter-weight and easier to get type inference working for.

Currently the only thing missing in 0.1.0 from making Option B possible is the response.status discriminator—currently it doesn’t associate specific statuses with specific response codes yet (but is very possible to do in an upcoming release)

drwpow avatar May 01 '23 05:05 drwpow

Update on this: the response.status discriminator support still hasn’t been added, and this issue will be complete once that’s in.

drwpow avatar May 22 '23 16:05 drwpow

I am very interested in this, and just wanted to add my support for going forward with Option B!

In the meantime, we use something of the sort to get the right types based on the status code:

if (response.error) {
  let error;

  switch (response.response.status) {
    case 400:
      error = response.error as paths["/rtm/notes/create"]["post"]["responses"]["400"]["content"]["application/ld+json"];
      break;
    case 422:
      error = response.error as paths["/rtm/notes/create"]["post"]["responses"]["422"]["content"]["application/ld+json"];
      break;
    // etc...
  }

It's not nearly as neat, but it kind of "works". 🤷‍♂️

EmilePerron avatar Aug 08 '23 20:08 EmilePerron

I also support option B, I think checking status codes is a good practice anyway. I'm working with this api:

get_region: {
  responses: {
    200: {
      content: {
        "application/json": components["schemas"]["Region"];
      };
    }; 
    202: {
      content: {
        "application/json": components["schemas"]["Region"];
      };
    };
    204: {
      content: never;
    };
  };
};

~Currently, data isn't type safe for this API, typescript will let me access properties of Region even though in the 204 case, there is no response body.~ I misspoke here! It appears that now data is a union of all possible response shapes, which is at least type safe. However, depending on what the shapes in question are, it can be awkward to narrow the type.

RobinClowers avatar Apr 26 '24 21:04 RobinClowers

This issue is stale because it has been open for 90 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

github-actions[bot] avatar Aug 06 '24 12:08 github-actions[bot]

This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem.

github-actions[bot] avatar Aug 15 '24 02:08 github-actions[bot]