Skip to content

Commit 8d60da6

Browse files
authored
jwk: RegisterKeyImporter takes KeyImporter, not a typed function (#2152)
* jwk: RegisterKeyImporter takes KeyImporter, not a typed function * jwk: collapse KeyImportFunc to a single generic name * jwk: KeyImporter generic interface, T inferable at registration * jwk: cover struct-shaped KeyImporter[T] dispatch path
1 parent f68d9ee commit 8d60da6

7 files changed

Lines changed: 205 additions & 130 deletions

File tree

MIGRATION.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ import _ "github.com/jwx-go/asmbase64/v4" // registration only
399399
### Recipe 10: Custom Key Importer
400400

401401
```go
402-
// Before
402+
// Before (v3)
403403
jwk.RegisterKeyImporter(&myKeyType{}, jwk.KeyImportFunc(func(raw any) (jwk.Key, error) {
404404
src, ok := raw.(*myKeyType)
405405
if !ok {
@@ -408,10 +408,14 @@ jwk.RegisterKeyImporter(&myKeyType{}, jwk.KeyImportFunc(func(raw any) (jwk.Key,
408408
// ... convert
409409
}))
410410

411-
// After
412-
jwk.RegisterKeyImporter(func(src *myKeyType) (jwk.Key, error) {
413-
// ... convert — type parameter inferred, no assertion needed
414-
})
411+
// After (v4): RegisterKeyImporter takes a jwk.KeyImporter[T]; use
412+
// jwk.KeyImportFunc[T] to adapt a typed function. The outer type
413+
// parameter is inferred from the adapter's typed Import method.
414+
jwk.RegisterKeyImporter(jwk.KeyImportFunc[*myKeyType](func(src *myKeyType) (jwk.Key, error) {
415+
// The interface is typed, so src is *myKeyType in the body —
416+
// no manual assertion.
417+
// ... convert
418+
}))
415419
```
416420

417421
### Recipe 11: Iterating Over Sets and Tokens

docs/04-jwk.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ registration points:
759759
- `KeyParser` — converts a JSON payload into a `jwk.Key` using the
760760
probe's hints. Register with [`jwk.RegisterKeyParser`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v4/jwk#RegisterKeyParser).
761761
- `KeyImporter[T]` — converts a raw Go crypto value into a `jwk.Key`.
762-
Register with [`jwk.RegisterKeyImporter[T]`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v4/jwk#RegisterKeyImporter).
762+
Register with [`jwk.RegisterKeyImporter`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v4/jwk#RegisterKeyImporter).
763763
- `KeyExporter` — converts a `jwk.Key` back into a raw Go value.
764764
Register with [`jwk.RegisterKeyExporter`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v4/jwk#RegisterKeyExporter),
765765
keyed by `jwk.KeyKind` (case-insensitive — usually
@@ -821,25 +821,46 @@ func (MyKeyParser) ParseKey(probe *jwk.KeyProbe, unmarshaler jwk.KeyUnmarshaler,
821821

822822
### How import / export dispatch
823823

824-
`jwk.Import[T](raw)` looks up a `KeyImporter[T]` registered for the
824+
`jwk.Import[T](raw)` looks up the `KeyImporter[T]` registered for the
825825
exact Go type of `raw`. Pre-registered importers exist for the standard
826826
crypto types (`*rsa.PrivateKey`, `*ecdsa.PublicKey`, `ed25519.PrivateKey`,
827-
etc.); add your own:
827+
etc.); register your own with `jwk.RegisterKeyImporter(KeyImporter[T])`.
828+
The interface is generic — its `Import(raw T) (Key, error)` method is
829+
typed — so the registration's type parameter is inferred from whatever
830+
you pass in. Use `jwk.KeyImportFunc[T]` to adapt a typed function:
828831

829832
```go
830833
func init() {
831-
if err := jwk.RegisterKeyImporter[*mypkg.SuperSecretKey](
832-
func(raw *mypkg.SuperSecretKey) (jwk.Key, error) {
833-
// raw is already typed — no `any`-cast or ContinueError
834-
// needed. v4's import surface is one importer per Go type.
835-
return mypkg.NewSuperSecretJWK(raw), nil
836-
},
834+
if err := jwk.RegisterKeyImporter(
835+
jwk.KeyImportFunc[*mypkg.SuperSecretKey](
836+
func(raw *mypkg.SuperSecretKey) (jwk.Key, error) {
837+
// The interface is typed, so raw is *mypkg.SuperSecretKey
838+
// — no any-cast needed in the body.
839+
return mypkg.NewSuperSecretJWK(raw), nil
840+
},
841+
),
837842
); err != nil {
838843
panic(err)
839844
}
840845
}
841846
```
842847

848+
For an importer with non-trivial state, implement the `KeyImporter[T]`
849+
interface on your own type and pass an instance directly:
850+
851+
```go
852+
type mySuperSecretImporter struct { /* config, caches, ... */ }
853+
854+
// Typed Import method makes mySuperSecretImporter satisfy
855+
// jwk.KeyImporter[*mypkg.SuperSecretKey].
856+
func (i *mySuperSecretImporter) Import(raw *mypkg.SuperSecretKey) (jwk.Key, error) {
857+
return i.convert(raw)
858+
}
859+
860+
// Registration: T inferred from the Import method's parameter type.
861+
jwk.RegisterKeyImporter(&mySuperSecretImporter{...})
862+
```
863+
843864
`jwk.Export[T](key)` runs a `KeyExporter` keyed by `jwk.KeyKind`
844865
(matched case-insensitively against `key.KeyType()`):
845866

docs/design/v4-generics.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,17 @@ func myFunc(src any) (jwk.Key, error) {
2121

2222
**v4:**
2323
```go
24-
jwk.RegisterKeyImporter(myFunc)
25-
// myFunc accepts the concrete type — no type switch needed
24+
jwk.RegisterKeyImporter(jwk.KeyImportFunc[*rsa.PrivateKey](myFunc))
25+
// myFunc accepts the concrete type. The outer T is inferred from the
26+
// adapter's typed Import method.
2627
func myFunc(src *rsa.PrivateKey) (jwk.Key, error) {
2728
...
2829
}
2930
```
3031

31-
Implementation: `RegisterKeyImporter[T any](fn func(T) (Key, error))` uses `reflect.TypeFor[T]()` to derive the map key at registration time. The runtime dispatch in `Import()` still uses `reflect.TypeOf(raw)` — this is unavoidable since the input type is only known at call time.
32+
Implementation: `KeyImporter[T any]` is a generic interface whose `Import(raw T) (Key, error)` method is itself typed; `KeyImportFunc[T any]` is a `func(T) (Key, error)` that satisfies it. `RegisterKeyImporter[T any](ki KeyImporter[T]) error` uses `reflect.TypeFor[T]()` to derive the map key at registration time and stores a closure that re-types `any` → T at dispatch. The runtime lookup in `Import()` still keys by `reflect.TypeOf(raw)`.
3233

33-
Callers holding a full `KeyImporter` value can register it through the same entry point by passing `imp.Import` as the function: `RegisterKeyImporter[T](imp.Import)`.
34+
The earlier v4.0.0 release shipped `RegisterKeyImporter[T](fn func(T) (Key, error))` — accepting a typed function rather than a `KeyImporter`, with the public `KeyImporter` interface and `KeyImportFunc` adapter exposed but never reachable from registration. The current shape restores the interface as the canonical registration value, makes the interface generic so type-parameter inference covers the registration call, and makes `KeyImportFunc[T]` a generic typed-function adapter that satisfies it.
3435

3536
### Type-safe generic accessors
3637

@@ -66,10 +67,10 @@ jwk.RegisterKeyImporter(&myKey{}, jwk.KeyImportFunc(func(src any) (jwk.Key, erro
6667
return doImport(k)
6768
}))
6869

69-
// v4: type-safe, no zero-value discriminator needed
70-
jwk.RegisterKeyImporter(func(k *myKey) (jwk.Key, error) {
70+
// v4: pass a KeyImporter[T]; T inferred from the adapter's typed Import.
71+
jwk.RegisterKeyImporter(jwk.KeyImportFunc[*myKey](func(k *myKey) (jwk.Key, error) {
7172
return doImport(k)
72-
})
73+
}))
7374
```
7475

7576
```go

jwk/convert.go

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ import (
2222
// A converter that converts from a raw key to a `jwk.Key` is called a KeyImporter.
2323
// A converter that converts from a `jwk.Key` to a raw key is called a KeyExporter.
2424

25-
var keyImporters = make(map[reflect.Type]KeyImporter)
25+
// keyImporters stores per-Go-type closures that adapt a typed
26+
// [KeyImporter] (parameterized by the same Go type that keys this map)
27+
// down to a non-generic any-taking dispatch shape. The closure carries
28+
// T and performs the raw.(T) type-assertion before delegating to the
29+
// underlying typed Import method; the assertion is safe because the
30+
// dispatch in [convertRawKey] keys this entry by reflect.TypeOf(raw)
31+
// matching reflect.TypeFor[T]().
32+
var keyImporters = make(map[reflect.Type]func(any) (Key, error))
2633

2734
// builtinImporterTypes is the set of raw key types whose importers
2835
// are owned by the jwk package and cannot be overridden by callers.
@@ -64,12 +71,14 @@ var keyExporters = make(map[KeyKind][]KeyExporter)
6471
var muKeyImporters sync.RWMutex
6572
var muKeyExporters sync.RWMutex
6673

67-
// RegisterKeyImporter registers a typed import function for converting
68-
// raw keys of type T to jwk.Key. The type is derived from the type
69-
// parameter, eliminating the need to pass a zero value.
74+
// RegisterKeyImporter registers a [KeyImporter] for the raw key type T.
75+
// The type parameter is normally inferred from the importer's typed
76+
// Import method; pass it explicitly only when inference would fail
77+
// (e.g. when supplying a [KeyImportFunc] without arguments).
7078
//
71-
// When `jwk.Import()` is called, the library looks up the appropriate
72-
// importer for the given raw key type (via `reflect`) and executes it.
79+
// When [Import] is called, the library looks up the importer for the
80+
// raw value's runtime type (via reflect) and invokes its Import method
81+
// through a closure that handles the any → T re-typing.
7382
//
7483
// Importer dispatch is single-valued per Go type T: there is exactly
7584
// one importer per reflect.Type. Subsequent registrations for the same
@@ -93,7 +102,12 @@ var muKeyExporters sync.RWMutex
93102
//
94103
// Extension modules calling this from init() must check the returned
95104
// error and panic on failure.
96-
func RegisterKeyImporter[T any](fn func(T) (Key, error)) error {
105+
//
106+
// To register from a typed function (the common case for extensions),
107+
// wrap it with [KeyImportFunc]:
108+
//
109+
// jwk.RegisterKeyImporter(jwk.KeyImportFunc[*mypkg.Key](importMyKey))
110+
func RegisterKeyImporter[T any](ki KeyImporter[T]) error {
97111
muKeyImporters.Lock()
98112
defer muKeyImporters.Unlock()
99113
t := reflect.TypeFor[T]()
@@ -103,7 +117,13 @@ func RegisterKeyImporter[T any](fn func(T) (Key, error)) error {
103117
if _, exists := keyImporters[t]; exists {
104118
return fmt.Errorf(`jwk.RegisterKeyImporter: an importer for %s is already registered; call jwk.UnregisterKeyImporter[%s]() first if you need to replace it`, t, t)
105119
}
106-
keyImporters[t] = &keyImportAdapter[T]{fn: fn}
120+
keyImporters[t] = func(raw any) (Key, error) {
121+
// Safe: dispatch in convertRawKey keys this entry by
122+
// reflect.TypeOf(raw) matching reflect.TypeFor[T](), so the
123+
// assertion cannot fail in the lookup path.
124+
//nolint:forcetypeassert
125+
return ki.Import(raw.(T))
126+
}
107127
return nil
108128
}
109129

@@ -183,33 +203,36 @@ func RegisterKeyExporter(ident KeyKind, conv KeyExporter) error {
183203
return nil
184204
}
185205

186-
// KeyImporter is used to convert from a raw key to a `jwk.Key`. mneumonic: from the PoV of the `jwk.Key`,
187-
// we're _importing_ a raw key.
188-
type KeyImporter interface {
189-
// Import takes the raw key to be converted, and returns a `jwk.Key` or an error if the conversion fails.
190-
Import(any) (Key, error)
191-
}
192-
193-
// KeyImportFunc is a convenience type to implement KeyImporter as a function.
194-
type KeyImportFunc func(any) (Key, error)
206+
// KeyImporter converts a raw key of Go type T into a [jwk.Key]. The
207+
// type parameter T is the raw-key type the importer handles. From the
208+
// point of view of the `jwk.Key`, we're _importing_ a raw key.
209+
//
210+
// Implementations are registered with [RegisterKeyImporter]; T flows
211+
// through to [reflect.TypeFor] at registration time to key the import
212+
// dispatch table.
213+
type KeyImporter[T any] interface {
214+
// Import takes the raw key to be converted, and returns a
215+
// [jwk.Key] or an error if the conversion fails.
216+
Import(raw T) (Key, error)
217+
}
218+
219+
// KeyImportFunc is the typed-function adapter that satisfies
220+
// [KeyImporter] for type T. The Import method just calls f directly —
221+
// no internal type assertion is needed because [KeyImporter]'s Import
222+
// is itself typed.
223+
//
224+
// This is the canonical way to register a typed import function:
225+
//
226+
// jwk.RegisterKeyImporter(jwk.KeyImportFunc[*mypkg.Key](importMyKey))
227+
//
228+
// For an importer with non-trivial state, implement [KeyImporter] on
229+
// your own type and pass an instance of it directly.
230+
type KeyImportFunc[T any] func(T) (Key, error)
195231

196-
func (f KeyImportFunc) Import(raw any) (Key, error) {
232+
func (f KeyImportFunc[T]) Import(raw T) (Key, error) {
197233
return f(raw)
198234
}
199235

200-
// keyImportAdapter wraps a typed function into a KeyImporter.
201-
type keyImportAdapter[T any] struct {
202-
fn func(T) (Key, error)
203-
}
204-
205-
func (a *keyImportAdapter[T]) Import(raw any) (Key, error) {
206-
v, ok := raw.(T)
207-
if !ok {
208-
return nil, fmt.Errorf(`cannot convert key type '%T' to %T`, raw, *new(T))
209-
}
210-
return a.fn(v)
211-
}
212-
213236
// KeyExporter is used to convert from a `jwk.Key` to a raw key. From the PoV of the `jwk.Key`,
214237
// we're _exporting_ it to a raw key.
215238
type KeyExporter interface {
@@ -269,7 +292,13 @@ func init() {
269292
func registerBuiltinKeyImporter[T any](fn func(T) (Key, error)) {
270293
t := reflect.TypeFor[T]()
271294
builtinImporterTypes[t] = struct{}{}
272-
keyImporters[t] = &keyImportAdapter[T]{fn: fn}
295+
keyImporters[t] = func(raw any) (Key, error) {
296+
// Safe: dispatch keys this entry by reflect.TypeOf(raw)
297+
// matching reflect.TypeFor[T](); the assertion cannot fail
298+
// when the closure is invoked from convertRawKey.
299+
//nolint:forcetypeassert
300+
return fn(raw.(T))
301+
}
273302
}
274303

275304
// panicOnRegistrationError converts a non-nil error returned by a Register*

0 commit comments

Comments
 (0)