-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add FillID method for Service, Route and Consumer (#299)
- Loading branch information
Showing
3 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package kong | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
|
||
"github.com/google/uuid" | ||
) | ||
|
||
// FillID fills the ID of an entity. It is a no-op if the entity already has an ID. | ||
// ID is generated in a deterministic way using UUIDv5. The UUIDv5 namespace is different for each entity type. | ||
// The name used to generate the ID for Service is Service.Name. | ||
func (s *Service) FillID() error { | ||
if s == nil { | ||
return fmt.Errorf("service is nil") | ||
} | ||
if s.ID != nil { | ||
// ID already set, do nothing. | ||
return nil | ||
} | ||
if s.Name == nil || *s.Name == "" { | ||
return fmt.Errorf("service name is required") | ||
} | ||
|
||
gen, err := idGeneratorFor(s) | ||
if err != nil { | ||
return fmt.Errorf("could not get id generator: %w", err) | ||
} | ||
|
||
s.ID = gen.buildIDFor(*s.Name) | ||
return nil | ||
} | ||
|
||
// FillID fills the ID of an entity. It is a no-op if the entity already has an ID. | ||
// ID is generated in a deterministic way using UUIDv5. The UUIDv5 namespace is different for each entity type. | ||
// The name used to generate the ID for Route is Route.Name. | ||
func (r *Route) FillID() error { | ||
if r == nil { | ||
return fmt.Errorf("route is nil") | ||
} | ||
if r.ID != nil { | ||
// ID already set, do nothing. | ||
return nil | ||
} | ||
if r.Name == nil || *r.Name == "" { | ||
return fmt.Errorf("route name is required") | ||
} | ||
|
||
gen, err := idGeneratorFor(r) | ||
if err != nil { | ||
return fmt.Errorf("could not get id generator: %w", err) | ||
} | ||
|
||
r.ID = gen.buildIDFor(*r.Name) | ||
return nil | ||
} | ||
|
||
// FillID fills the ID of an entity. It is a no-op if the entity already has an ID. | ||
// ID is generated in a deterministic way using UUIDv5. The UUIDv5 namespace is different for each entity type. | ||
// The name used to generate the ID for Consumer is Consumer.Username. | ||
func (c *Consumer) FillID() error { | ||
if c == nil { | ||
return fmt.Errorf("consumer is nil") | ||
} | ||
if c.ID != nil { | ||
// ID already set, do nothing. | ||
return nil | ||
} | ||
if c.Username == nil || *c.Username == "" { | ||
return fmt.Errorf("consumer username is required") | ||
} | ||
|
||
gen, err := idGeneratorFor(c) | ||
if err != nil { | ||
return fmt.Errorf("could not get id generator: %w", err) | ||
} | ||
|
||
c.ID = gen.buildIDFor(*c.Username) | ||
return nil | ||
} | ||
|
||
var ( | ||
// _kongEntitiesNamespace is the UUIDv5 namespace used to generate IDs for Kong entities. | ||
_kongEntitiesNamespace = uuid.MustParse("fd02801f-0957-4a15-a55a-c8d9606f30b5") | ||
|
||
// _idGenerators is a map of entity type to ID generator. | ||
// Plural names of entities are used as names for UUIDv5 namespaces to match Kong's behavior which uses schemas | ||
// names for that purpose. | ||
// See https://github.com/Kong/kong/blob/master/kong/db/schema/others/declarative_config.lua for reference. | ||
_idGenerators = map[reflect.Type]idGenerator{ | ||
reflect.TypeOf(Service{}): newIDGeneratorFor("services"), | ||
reflect.TypeOf(Route{}): newIDGeneratorFor("routes"), | ||
reflect.TypeOf(Consumer{}): newIDGeneratorFor("consumers"), | ||
} | ||
) | ||
|
||
type idGenerator struct { | ||
namespace uuid.UUID | ||
} | ||
|
||
func (g idGenerator) buildIDFor(entityKey string) *string { | ||
id := uuid.NewSHA1(g.namespace, []byte(entityKey)).String() | ||
return &id | ||
} | ||
|
||
// newIDGeneratorFor returns a new ID generator for the given entity type. Should be used only to initialize _idGenerators. | ||
func newIDGeneratorFor(entityPluralName string) idGenerator { | ||
entityTypeNamespace := uuid.NewSHA1(_kongEntitiesNamespace, []byte(entityPluralName)) | ||
return idGenerator{namespace: entityTypeNamespace} | ||
} | ||
|
||
// IDFillable is a type constraint for entities that can be filled with an ID. | ||
type IDFillable interface { | ||
FillID() error | ||
} | ||
|
||
// idGeneratorFor returns the ID generator for the given entity type. | ||
func idGeneratorFor(entity IDFillable) (idGenerator, error) { | ||
generator, ok := _idGenerators[reflect.TypeOf(entity).Elem()] | ||
if !ok { | ||
// This should never happen, as the map is initialized with all supported entity types. | ||
// If it does happen, it is a bug in the code. | ||
return idGenerator{}, fmt.Errorf("unsupported entity type: '%T'", entity) | ||
} | ||
return generator, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package kong_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/kong/go-kong/kong" | ||
) | ||
|
||
type fillEntityIDTestCase struct { | ||
name string | ||
entity kong.IDFillable | ||
|
||
assertEntity func(t *testing.T, entity kong.IDFillable) | ||
expectErr bool | ||
} | ||
|
||
func TestFillEntityID(t *testing.T) { | ||
testCases := []fillEntityIDTestCase{ | ||
// Service | ||
{ | ||
name: "service nil pointer", | ||
entity: (*kong.Service)(nil), | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "service with nil name", | ||
entity: &kong.Service{}, | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "service with empty name", | ||
entity: &kong.Service{Name: kong.String("")}, | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "service with name", | ||
entity: &kong.Service{ | ||
Name: kong.String("some.service.name"), | ||
}, | ||
assertEntity: func(t *testing.T, e kong.IDFillable) { | ||
svc := e.(*kong.Service) | ||
require.NotNil(t, svc.ID) | ||
const expectedID = "d9bee1f8-db6e-5a37-9281-fd4aca16dc00" | ||
require.Equal(t, expectedID, *svc.ID, "ID should be deterministic") | ||
}, | ||
}, | ||
// Route | ||
{ | ||
name: "route nil pointer", | ||
entity: (*kong.Route)(nil), | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "route with nil name", | ||
entity: &kong.Route{}, | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "route with empty name", | ||
entity: &kong.Route{Name: kong.String("")}, | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "route with name", | ||
entity: &kong.Route{ | ||
Name: kong.String("some.service.name"), | ||
}, | ||
assertEntity: func(t *testing.T, e kong.IDFillable) { | ||
route := e.(*kong.Route) | ||
require.NotNil(t, route.ID) | ||
|
||
const expectedID = "7f0753cd-bff2-5f74-85b0-e8e2057d9500" | ||
require.Equal(t, expectedID, *route.ID, "ID should be deterministic") | ||
}, | ||
}, | ||
// Consumer | ||
{ | ||
name: "consumer nil pointer", | ||
entity: (*kong.Consumer)(nil), | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "consumer with nil username", | ||
entity: &kong.Consumer{}, | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "consumer with empty username", | ||
entity: &kong.Consumer{Username: kong.String("")}, | ||
expectErr: true, | ||
}, | ||
{ | ||
name: "consumer with username", | ||
entity: &kong.Consumer{ | ||
Username: kong.String("some.username"), | ||
}, | ||
assertEntity: func(t *testing.T, e kong.IDFillable) { | ||
consumer := e.(*kong.Consumer) | ||
require.NotNil(t, consumer.ID) | ||
|
||
const expectedID = "782780c4-d345-5682-bdf6-722dccaf93e0" | ||
require.Equal(t, expectedID, *consumer.ID, "ID should be deterministic") | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
tc := tc | ||
|
||
t.Run(tc.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
err := tc.entity.FillID() | ||
if tc.expectErr { | ||
require.Error(t, err) | ||
return | ||
} | ||
|
||
require.NoError(t, err) | ||
tc.assertEntity(t, tc.entity) | ||
}) | ||
} | ||
} |