feat: edge gateway service
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// Package downstream defines the verified internal command contract used by the
|
||||
// gateway after the authenticated edge pipeline succeeds.
|
||||
package downstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRouteNotFound reports that Router does not have an exact-match handler
|
||||
// for the supplied authenticated message type.
|
||||
ErrRouteNotFound = errors.New("downstream route not found")
|
||||
|
||||
// ErrDownstreamUnavailable reports that the resolved downstream dependency is
|
||||
// temporarily unavailable.
|
||||
ErrDownstreamUnavailable = errors.New("downstream service is unavailable")
|
||||
)
|
||||
|
||||
// AuthenticatedCommand is the minimum verified unary command context the
|
||||
// gateway may forward to downstream business services.
|
||||
type AuthenticatedCommand struct {
|
||||
// ProtocolVersion is the authenticated transport protocol version accepted
|
||||
// by the gateway.
|
||||
ProtocolVersion string
|
||||
|
||||
// UserID is the authenticated user identity resolved from SessionCache.
|
||||
UserID string
|
||||
|
||||
// DeviceSessionID is the authenticated device session that originated the
|
||||
// command.
|
||||
DeviceSessionID string
|
||||
|
||||
// MessageType is the stable exact-match downstream routing key.
|
||||
MessageType string
|
||||
|
||||
// TimestampMS is the client-supplied request timestamp that already passed
|
||||
// freshness verification.
|
||||
TimestampMS int64
|
||||
|
||||
// RequestID is the transport correlation and anti-replay identifier.
|
||||
RequestID string
|
||||
|
||||
// TraceID is the optional client-supplied correlation identifier.
|
||||
TraceID string
|
||||
|
||||
// PayloadBytes carries the verified opaque business payload bytes.
|
||||
PayloadBytes []byte
|
||||
}
|
||||
|
||||
// UnaryResult is the minimum downstream unary result the gateway needs in
|
||||
// order to build a signed authenticated client response.
|
||||
type UnaryResult struct {
|
||||
// ResultCode is the stable opaque downstream result code returned to the
|
||||
// client without business reinterpretation by the gateway.
|
||||
ResultCode string
|
||||
|
||||
// PayloadBytes carries the opaque downstream response payload bytes.
|
||||
PayloadBytes []byte
|
||||
}
|
||||
|
||||
// Client executes a verified authenticated unary command against one concrete
|
||||
// downstream service or adapter.
|
||||
type Client interface {
|
||||
// ExecuteCommand executes command and returns the downstream unary result.
|
||||
ExecuteCommand(ctx context.Context, command AuthenticatedCommand) (UnaryResult, error)
|
||||
}
|
||||
|
||||
// Router resolves the downstream unary client for one exact authenticated
|
||||
// message_type value.
|
||||
type Router interface {
|
||||
// Route returns the downstream client for messageType. Implementations must
|
||||
// wrap ErrRouteNotFound when the route table does not contain messageType.
|
||||
Route(messageType string) (Client, error)
|
||||
}
|
||||
|
||||
// StaticRouter resolves exact message_type literals from an immutable route
|
||||
// map supplied at construction time.
|
||||
type StaticRouter struct {
|
||||
routes map[string]Client
|
||||
}
|
||||
|
||||
// NewStaticRouter constructs a StaticRouter with a defensive copy of routes.
|
||||
func NewStaticRouter(routes map[string]Client) *StaticRouter {
|
||||
clonedRoutes := make(map[string]Client, len(routes))
|
||||
for messageType, client := range routes {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
clonedRoutes[messageType] = client
|
||||
}
|
||||
|
||||
return &StaticRouter{routes: clonedRoutes}
|
||||
}
|
||||
|
||||
// Route returns the exact-match client for messageType.
|
||||
func (r *StaticRouter) Route(messageType string) (Client, error) {
|
||||
if r == nil {
|
||||
return nil, ErrRouteNotFound
|
||||
}
|
||||
|
||||
client, ok := r.routes[messageType]
|
||||
if !ok || client == nil {
|
||||
return nil, ErrRouteNotFound
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package downstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStaticRouterRoutesExactMessageType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := &stubClient{}
|
||||
router := NewStaticRouter(map[string]Client{
|
||||
"fleet.move": want,
|
||||
})
|
||||
|
||||
got, err := router.Route("fleet.move")
|
||||
require.NoError(t, err)
|
||||
assert.Same(t, want, got)
|
||||
}
|
||||
|
||||
func TestStaticRouterRejectsUnknownMessageType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
router := NewStaticRouter(map[string]Client{
|
||||
"fleet.move": &stubClient{},
|
||||
})
|
||||
|
||||
_, err := router.Route("fleet.rename")
|
||||
require.ErrorIs(t, err, ErrRouteNotFound)
|
||||
}
|
||||
|
||||
type stubClient struct{}
|
||||
|
||||
func (*stubClient) ExecuteCommand(context.Context, AuthenticatedCommand) (UnaryResult, error) {
|
||||
return UnaryResult{}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user