Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
<!-- Ideally, this should get auto-generated via tools like [auto-changelog](https://github.com/CookPete/auto-changelog). Eventually, this will get set up as part of the repository template. -->

## Unreleased

### Features

- **Management API:** `CreateIdentityResponseIdentity` now also decodes the `identity_id` field the Kinde API returns when creating an identity for an existing enterprise connection (previously only `id` was recognized). Added `EffectiveIdentityID()` helper to read whichever field is populated.
18 changes: 18 additions & 0 deletions kinde/management_api/create_identity_response_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package management_api

// EffectiveIdentityID returns the identity ID from the CreateIdentity response.
// The API returns "id" (normal case) or "identity_id" (when creating with an
// existing enterprise identity). This helper returns whichever is set so
// callers get a single value regardless of which key the API returned.
func (s *CreateIdentityResponseIdentity) EffectiveIdentityID() (string, bool) {
if s == nil {
return "", false
}
if id, ok := s.ID.Get(); ok && id != "" {
return id, true
}
if id, ok := s.IdentityID.Get(); ok && id != "" {
return id, true
}
return "", false
}
99 changes: 99 additions & 0 deletions kinde/management_api/create_identity_response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package management_api

import (
"testing"

"github.com/go-faster/jx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestCreateIdentityResponse_Decode_WithId verifies that a response with "id" field decodes correctly.
func TestCreateIdentityResponse_Decode_WithId(t *testing.T) {
json := `{
"message": "Identity created",
"code": "IDENTITY_CREATED",
"identity": {
"id": "idl_abc123"
}
}`

d := jx.DecodeBytes([]byte(json))
var response CreateIdentityResponse

err := response.Decode(d)
require.NoError(t, err)

require.True(t, response.Identity.IsSet(), "Identity should be set")
identity, _ := response.Identity.Get()
assert.True(t, identity.ID.IsSet(), "Identity ID should be set")
id, ok := identity.ID.Get()
require.True(t, ok)
assert.Equal(t, "idl_abc123", id)

effectiveID, ok := identity.EffectiveIdentityID()
require.True(t, ok)
assert.Equal(t, "idl_abc123", effectiveID)
}

// TestCreateIdentityResponse_Decode_WithIdentityId verifies the bug fix: when the API returns
// "identity_id" (e.g. for existing enterprise identity), the identity is decoded and EffectiveIdentityID works.
func TestCreateIdentityResponse_Decode_WithIdentityId(t *testing.T) {
json := `{
"message": "Identity created",
"code": "IDENTITY_CREATED",
"identity": {
"identity_id": "idl_existing_enterprise_123"
}
}`

d := jx.DecodeBytes([]byte(json))
var response CreateIdentityResponse

err := response.Decode(d)
require.NoError(t, err)

require.True(t, response.Identity.IsSet(), "Identity should be set")
identity, _ := response.Identity.Get()
assert.True(t, identity.IdentityID.IsSet(), "IdentityID should be set (from identity_id field)")
id, ok := identity.IdentityID.Get()
require.True(t, ok)
assert.Equal(t, "idl_existing_enterprise_123", id)

effectiveID, ok := identity.EffectiveIdentityID()
require.True(t, ok, "EffectiveIdentityID should return the identity_id value")
assert.Equal(t, "idl_existing_enterprise_123", effectiveID)
}

// TestCreateIdentityResponseIdentity_Decode_IdentityIdField verifies the identity object
// decoder accepts "identity_id" and populates IdentityID.
func TestCreateIdentityResponseIdentity_Decode_IdentityIdField(t *testing.T) {
json := `{"identity_id": "idl_xyz789"}`

d := jx.DecodeBytes([]byte(json))
var identity CreateIdentityResponseIdentity

err := identity.Decode(d)
require.NoError(t, err)

assert.True(t, identity.IdentityID.IsSet(), "IdentityID should be set from identity_id field")
id, ok := identity.IdentityID.Get()
require.True(t, ok)
assert.Equal(t, "idl_xyz789", id)

effectiveID, ok := identity.EffectiveIdentityID()
require.True(t, ok)
assert.Equal(t, "idl_xyz789", effectiveID)
}

// TestCreateIdentityResponseIdentity_EffectiveIdentityID_prefers_id verifies EffectiveIdentityID
// returns ID when both ID and IdentityID are set (e.g. spec allows both).
func TestCreateIdentityResponseIdentity_EffectiveIdentityID_prefers_id(t *testing.T) {
identity := CreateIdentityResponseIdentity{}
identity.SetID(NewOptString("idl_primary"))
identity.SetIdentityID(NewOptString("idl_secondary"))

effectiveID, ok := identity.EffectiveIdentityID()
require.True(t, ok)
assert.Equal(t, "idl_primary", effectiveID)
}
189 changes: 189 additions & 0 deletions kinde/management_api/fix_identity_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//go:build ignore
// +build ignore

package main

import (
"bytes"
"fmt"
"os"
)

// This tool patches the generated CreateIdentityResponseIdentity type to also
// accept "identity_id". The Kinde API returns "id" when creating a new
// identity but returns "identity_id" instead when the identity already
// exists on an enterprise connection. The published OpenAPI spec only
// documents "id", so ogen never generates the "identity_id" field on its own.
// See EffectiveIdentityID() in create_identity_response_helpers.go for the
// helper that reads whichever field is populated.

const (
schemasFile = "oas_schemas_gen.go"
jsonFile = "oas_json_gen.go"
)

var (
oldSchema = `type CreateIdentityResponseIdentity struct {
// The identity's ID.
ID OptString ` + "`json:\"id\"`" + `
}

// GetID returns the value of ID.
func (s *CreateIdentityResponseIdentity) GetID() OptString {
return s.ID
}

// SetID sets the value of ID.
func (s *CreateIdentityResponseIdentity) SetID(val OptString) {
s.ID = val
}`

newSchema = `type CreateIdentityResponseIdentity struct {
// The identity's ID.
ID OptString ` + "`json:\"id\"`" + `
// The identity's ID, returned instead of "id" when the identity already
// exists on an enterprise connection.
IdentityID OptString ` + "`json:\"identity_id\"`" + `
}

// GetID returns the value of ID.
func (s *CreateIdentityResponseIdentity) GetID() OptString {
return s.ID
}

// GetIdentityID returns the value of IdentityID.
func (s *CreateIdentityResponseIdentity) GetIdentityID() OptString {
return s.IdentityID
}

// SetID sets the value of ID.
func (s *CreateIdentityResponseIdentity) SetID(val OptString) {
s.ID = val
}

// SetIdentityID sets the value of IdentityID.
func (s *CreateIdentityResponseIdentity) SetIdentityID(val OptString) {
s.IdentityID = val
}`

oldJSON = `func (s *CreateIdentityResponseIdentity) encodeFields(e *jx.Encoder) {
{
if s.ID.Set {
e.FieldStart("id")
s.ID.Encode(e)
}
}
}

var jsonFieldsNameOfCreateIdentityResponseIdentity = [1]string{
0: "id",
}`

newJSON = `func (s *CreateIdentityResponseIdentity) encodeFields(e *jx.Encoder) {
{
if s.ID.Set {
e.FieldStart("id")
s.ID.Encode(e)
}
}
{
if s.IdentityID.Set {
e.FieldStart("identity_id")
s.IdentityID.Encode(e)
}
}
}

var jsonFieldsNameOfCreateIdentityResponseIdentity = [2]string{
0: "id",
1: "identity_id",
}`

oldDecode = ` case "id":
if err := func() error {
s.ID.Reset()
if err := s.ID.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"id\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode CreateIdentityResponseIdentity")
}`

newDecode = ` case "id":
if err := func() error {
s.ID.Reset()
if err := s.ID.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"id\"")
}
case "identity_id":
if err := func() error {
s.IdentityID.Reset()
if err := s.IdentityID.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"identity_id\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode CreateIdentityResponseIdentity")
}`
)

func patch(file string, replacements [][2]string) error {
content, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("reading %s: %w", file, err)
}

newContent := content
for _, r := range replacements {
old, want := r[0], r[1]
if bytes.Contains(newContent, []byte(want)) {
// Already patched.
continue
}
if !bytes.Contains(newContent, []byte(old)) {
return fmt.Errorf("%s: expected pattern not found - the generated shape has likely changed (ogen upgrade?), update fix_identity_id.go:\n%s", file, old)
}
newContent = bytes.Replace(newContent, []byte(old), []byte(want), 1)
}

if bytes.Equal(content, newContent) {
fmt.Printf("%s already patched, skipping\n", file)
return nil
}

if err := os.WriteFile(file, newContent, 0o644); err != nil {
return fmt.Errorf("writing %s: %w", file, err)
}
fmt.Printf("✅ Patched %s for CreateIdentityResponseIdentity.identity_id\n", file)
return nil
}

func main() {
if err := patch(schemasFile, [][2]string{{oldSchema, newSchema}}); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := patch(jsonFile, [][2]string{{oldJSON, newJSON}, {oldDecode, newDecode}}); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
1 change: 1 addition & 0 deletions kinde/management_api/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ package management_api
//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target . -package management_api --clean https://api-spec.kinde.com/kinde-management-api-spec.yaml
//go:generate go run fix_optstring.go
//go:generate go run fix_oneof.go
//go:generate go run fix_identity_id.go
19 changes: 18 additions & 1 deletion kinde/management_api/oas_json_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions kinde/management_api/oas_schemas_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading