优化结构
This commit is contained in:
@@ -6,7 +6,6 @@ package module
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/pkg/database/migration"
|
||||
"kra/pkg/task"
|
||||
)
|
||||
|
||||
type Menu struct {
|
||||
@@ -33,7 +32,6 @@ type Definition struct {
|
||||
Migrations []migration.Step
|
||||
Surface Surface
|
||||
TimedTasks []TimedTask
|
||||
Tasks []task.Method
|
||||
}
|
||||
|
||||
type Catalog struct {
|
||||
@@ -48,15 +46,6 @@ func (c Catalog) MigrationSteps() []migration.Step {
|
||||
return steps
|
||||
}
|
||||
|
||||
func (c Catalog) Surface() Surface {
|
||||
var surface Surface
|
||||
for _, item := range c.Definitions {
|
||||
surface.Menus = append(surface.Menus, item.Surface.Menus...)
|
||||
surface.APIs = append(surface.APIs, item.Surface.APIs...)
|
||||
}
|
||||
return surface
|
||||
}
|
||||
|
||||
func (c Catalog) DefaultTimedTasks() []TimedTask {
|
||||
var tasks []TimedTask
|
||||
for _, item := range c.Definitions {
|
||||
@@ -65,41 +54,6 @@ func (c Catalog) DefaultTimedTasks() []TimedTask {
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (c Catalog) TaskMethods() []task.Method {
|
||||
var methods []task.Method
|
||||
for _, item := range c.Definitions {
|
||||
methods = append(methods, item.Tasks...)
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
type RouteRegistrar interface {
|
||||
RegisterRoutes(public, private *gin.RouterGroup, engine *gin.Engine)
|
||||
}
|
||||
|
||||
type Runtime struct {
|
||||
routes []RouteRegistrar
|
||||
}
|
||||
|
||||
func NewRuntime(routes ...RouteRegistrar) *Runtime {
|
||||
return &Runtime{routes: append([]RouteRegistrar(nil), routes...)}
|
||||
}
|
||||
|
||||
func (r *Runtime) Add(routes ...RouteRegistrar) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.routes = append(r.routes, routes...)
|
||||
}
|
||||
|
||||
func (r *Runtime) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.Engine) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
for _, registrar := range r.routes {
|
||||
if registrar == nil {
|
||||
continue
|
||||
}
|
||||
registrar.RegisterRoutes(public, private, engine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,34 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/pkg/database/migration"
|
||||
"kra/pkg/task"
|
||||
)
|
||||
|
||||
func TestCatalogCollectsContributions(t *testing.T) {
|
||||
run := func(context.Context, json.RawMessage) error { return nil }
|
||||
catalog := Catalog{Definitions: []Definition{
|
||||
{
|
||||
Name: "system",
|
||||
Migrations: []migration.Step{{ID: "system_schema"}},
|
||||
Surface: Surface{Menus: []Menu{{Name: "users"}}, APIs: []API{{Path: "/users"}}},
|
||||
TimedTasks: []TimedTask{{Name: "system.cleanup"}},
|
||||
Tasks: []task.Method{{Name: "system.cleanup", Run: run}},
|
||||
},
|
||||
{
|
||||
Name: "orders",
|
||||
Migrations: []migration.Step{{ID: "orders_schema"}},
|
||||
Surface: Surface{Menus: []Menu{{Name: "orders"}}, APIs: []API{{Path: "/orders"}}},
|
||||
TimedTasks: []TimedTask{{Name: "orders.expire"}},
|
||||
Tasks: []task.Method{{Name: "orders.expire", Run: run}},
|
||||
},
|
||||
}}
|
||||
|
||||
if got := catalog.MigrationSteps(); len(got) != 2 || got[0].ID != "system_schema" || got[1].ID != "orders_schema" {
|
||||
t.Fatalf("unexpected migrations: %#v", got)
|
||||
}
|
||||
if got := catalog.Surface(); len(got.Menus) != 2 || len(got.APIs) != 2 || got.Menus[1].Name != "orders" {
|
||||
if got := catalog.Definitions[1].Surface; len(got.Menus) != 1 || len(got.APIs) != 1 || got.Menus[0].Name != "orders" {
|
||||
t.Fatalf("unexpected surface: %#v", got)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 || got[1].Name != "orders.expire" {
|
||||
t.Fatalf("unexpected default tasks: %#v", got)
|
||||
}
|
||||
if got := catalog.TaskMethods(); len(got) != 2 || got[1].Name != "orders.expire" {
|
||||
t.Fatalf("unexpected task methods: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
type routeRegistrarStub struct{ called bool }
|
||||
|
||||
func (stub *routeRegistrarStub) RegisterRoutes(*gin.RouterGroup, *gin.RouterGroup, *gin.Engine) {
|
||||
stub.called = true
|
||||
}
|
||||
|
||||
func TestRuntimeSkipsNilRouteRegistrars(t *testing.T) {
|
||||
stub := &routeRegistrarStub{}
|
||||
runtime := NewRuntime(nil, stub)
|
||||
runtime.RegisterRoutes(nil, nil, nil)
|
||||
if !stub.called {
|
||||
t.Fatal("non-nil route registrar was not called")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// Package protoutil contains protobuf helpers that are independent of any
|
||||
// application module.
|
||||
package protoutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// MergeProtoJSON applies a JSON object as a partial protobuf update while
|
||||
// retaining fields omitted by the caller.
|
||||
func MergeProtoJSON(target proto.Message, patch json.RawMessage, options protojson.UnmarshalOptions) error {
|
||||
currentRaw, err := protojson.MarshalOptions{UseProtoNames: false}.Marshal(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var current map[string]any
|
||||
if err = json.Unmarshal(currentRaw, ¤t); err != nil {
|
||||
return err
|
||||
}
|
||||
var incoming map[string]any
|
||||
if err = json.Unmarshal(patch, &incoming); err != nil {
|
||||
return err
|
||||
}
|
||||
incoming = normalizeJSONKeys(incoming).(map[string]any)
|
||||
mergeJSONObjects(current, incoming)
|
||||
merged, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return options.Unmarshal(merged, target)
|
||||
}
|
||||
|
||||
func normalizeJSONKeys(value any) any {
|
||||
switch item := value.(type) {
|
||||
case map[string]any:
|
||||
result := make(map[string]any, len(item))
|
||||
for key, nested := range item {
|
||||
result[snakeToLowerCamel(key)] = normalizeJSONKeys(nested)
|
||||
}
|
||||
return result
|
||||
case []any:
|
||||
result := make([]any, len(item))
|
||||
for index, nested := range item {
|
||||
result[index] = normalizeJSONKeys(nested)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func snakeToLowerCamel(value string) string {
|
||||
if !strings.Contains(value, "_") {
|
||||
return value
|
||||
}
|
||||
parts := strings.Split(value, "_")
|
||||
result := parts[0]
|
||||
for _, part := range parts[1:] {
|
||||
if part != "" {
|
||||
result += strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeJSONObjects(target, patch map[string]any) {
|
||||
for key, value := range patch {
|
||||
if incoming, ok := value.(map[string]any); ok {
|
||||
if existing, exists := target[key].(map[string]any); exists {
|
||||
mergeJSONObjects(existing, incoming)
|
||||
continue
|
||||
}
|
||||
}
|
||||
target[key] = value
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,6 @@ type Method struct {
|
||||
Run MethodFunc
|
||||
}
|
||||
|
||||
// Contributor registers task methods whose handlers need constructed runtime
|
||||
// dependencies. Modules expose a contributor and the application composition
|
||||
// root activates it; the task runtime never imports business modules.
|
||||
type Contributor interface {
|
||||
RegisterTasks(*Registry)
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
methods map[string]Method
|
||||
@@ -31,17 +24,6 @@ type Registry struct {
|
||||
|
||||
func NewRegistry() *Registry { return &Registry{methods: make(map[string]Method)} }
|
||||
|
||||
func Apply(registry *Registry, contributors ...Contributor) {
|
||||
if registry == nil {
|
||||
return
|
||||
}
|
||||
for _, contributor := range contributors {
|
||||
if contributor != nil {
|
||||
contributor.RegisterTasks(registry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) Register(method Method) {
|
||||
if r == nil || method.Name == "" || method.Run == nil {
|
||||
return
|
||||
|
||||
@@ -15,18 +15,3 @@ func TestRegistryIsComposableAndIdempotent(t *testing.T) {
|
||||
t.Fatalf("unexpected methods: %#v", methods)
|
||||
}
|
||||
}
|
||||
|
||||
type testContributor struct{ name string }
|
||||
|
||||
func (contributor testContributor) RegisterTasks(registry *Registry) {
|
||||
registry.Register(Method{Name: contributor.name, Run: func(context.Context, json.RawMessage) error { return nil }})
|
||||
}
|
||||
|
||||
func TestApplyCollectsModuleContributors(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
Apply(registry, testContributor{name: "system.cleanup"}, testContributor{name: "orders.expire"})
|
||||
methods := registry.List()
|
||||
if len(methods) != 2 || methods[0].Name != "orders.expire" || methods[1].Name != "system.cleanup" {
|
||||
t.Fatalf("unexpected contributed methods: %#v", methods)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user