refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
313
modules/pact/capabilities.go
Normal file
313
modules/pact/capabilities.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package pact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HasCommands is implemented by plugins that register console commands.
|
||||
type HasCommands interface {
|
||||
Commands() []bonfire.Command
|
||||
}
|
||||
|
||||
// HasConfig is implemented by plugins that ship default YAML configuration.
|
||||
// ConfigFS should contain files such as config/config.yaml, merged later at
|
||||
// the plugin ID path.
|
||||
type HasConfig interface {
|
||||
ConfigFS() fs.FS
|
||||
}
|
||||
|
||||
// HasMigrations is implemented by plugins that ship an ordered gormigrate
|
||||
// set. The kernel runs each set in party.Activate order with a separate
|
||||
// history table per plugin ID.
|
||||
type HasMigrations interface {
|
||||
Migrations() []*gormigrate.Migration
|
||||
}
|
||||
|
||||
// Middleware is a named HTTP wrapper registered by a plugin.
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
|
||||
// HasMiddleware is implemented by plugins that register named middleware.
|
||||
type HasMiddleware interface {
|
||||
Middlewares() map[string]Middleware
|
||||
}
|
||||
|
||||
// HasMiddlewareFactories is implemented by plugins that register
|
||||
// parameterized middleware ("name:param"), resolved at wrap time.
|
||||
type HasMiddlewareFactories interface {
|
||||
MiddlewareFactories() map[string]func(param string) Middleware
|
||||
}
|
||||
|
||||
// HasHouseMiddleware is implemented by plugins that register middleware
|
||||
// tagged as house-envelope/error handling -- refused inside a raw group
|
||||
// (D-16). This is the ONLY way a plugin declares a house-tagged name:
|
||||
// plugins never call a Router.Register* method directly (there is no such
|
||||
// call site anywhere in this codebase -- RegisterMiddleware/
|
||||
// RegisterMiddlewareFactory/RegisterHouseMiddleware are all called
|
||||
// exclusively from surf.Assemble/BuildRouter's plugin loop, the same way
|
||||
// HasMiddleware's Middlewares() map is today). A name present in both
|
||||
// Middlewares() and HouseMiddlewares() (from the same or a different
|
||||
// plugin) fails boot with the existing duplicate-name error, since both are
|
||||
// registered into the same underlying name table.
|
||||
type HasHouseMiddleware interface {
|
||||
HouseMiddlewares() map[string]Middleware
|
||||
}
|
||||
|
||||
// Router is the Laravel-like group builder implemented by surf.
|
||||
type Router interface {
|
||||
Group(prefix string, middleware []string, fn func(Router))
|
||||
GroupRaw(prefix string, middleware []string, fn func(Router))
|
||||
Get(path string, handler http.HandlerFunc, middleware ...string)
|
||||
Post(path string, handler http.HandlerFunc, middleware ...string)
|
||||
Put(path string, handler http.HandlerFunc, middleware ...string)
|
||||
Patch(path string, handler http.HandlerFunc, middleware ...string)
|
||||
Delete(path string, handler http.HandlerFunc, middleware ...string)
|
||||
Where(param, pattern string)
|
||||
WhereIn(param string, values ...string)
|
||||
}
|
||||
|
||||
// HasRoutes is implemented by plugins that declare HTTP routes.
|
||||
type HasRoutes interface {
|
||||
Routes(Router) error
|
||||
}
|
||||
|
||||
// HasModels is implemented by plugins that expose GORM models.
|
||||
type HasModels interface {
|
||||
Models() []any
|
||||
}
|
||||
|
||||
// JobArgs is the typed payload a job worker receives. Kind identifies the
|
||||
// job so a worker can reject unexpected argument types.
|
||||
type JobArgs interface {
|
||||
Kind() string
|
||||
}
|
||||
|
||||
// Job is a unit of background work. Phase 11 adapts this onto River; the
|
||||
// interface itself must not import River.
|
||||
type Job interface {
|
||||
Work(ctx context.Context, args JobArgs) error
|
||||
}
|
||||
|
||||
// HasJobs is implemented by plugins that register background jobs.
|
||||
type HasJobs interface {
|
||||
Jobs() []Job
|
||||
}
|
||||
|
||||
// AdminController is the compile-time admin controller contract. Phase 9
|
||||
// grows the schema pipeline; ID, model name and YAML config directory are
|
||||
// enough for generated stubs to compile.
|
||||
type AdminController interface {
|
||||
ID() string
|
||||
ModelName() string
|
||||
ConfigDir() string
|
||||
}
|
||||
|
||||
// HasAdminControllers is implemented by plugins that register admin controllers.
|
||||
type HasAdminControllers interface {
|
||||
AdminControllers() []AdminController
|
||||
}
|
||||
|
||||
// AdminAssets is the plugin-owned embedded tree of Winter admin YAML.
|
||||
// Paths are relative to the plugin root (controllers/..., models/...).
|
||||
type AdminAssets interface {
|
||||
AdminFS() fs.FS
|
||||
}
|
||||
|
||||
// AdminPermissioned is the D-03 permission list enforced before schema or SQL.
|
||||
type AdminPermissioned interface {
|
||||
RequiredPermissions() []string
|
||||
}
|
||||
|
||||
// AdminRecordSource supplies the GORM model the generic admin handlers query.
|
||||
// NewRecord returns a pointer to the model struct.
|
||||
type AdminRecordSource interface {
|
||||
NewRecord() any
|
||||
}
|
||||
|
||||
// Permission is one registerPermissions() entry.
|
||||
type Permission struct {
|
||||
Code string
|
||||
Tab string
|
||||
Label string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
// HasPermissions is implemented by plugins that declare backend permissions.
|
||||
type HasPermissions interface {
|
||||
Permissions() []Permission
|
||||
}
|
||||
|
||||
// NavigationItem is one registerNavigation() entry. Controller is the admin
|
||||
// controller ID; the SPA derives its route from that ID.
|
||||
type NavigationItem struct {
|
||||
Code string
|
||||
Label string
|
||||
Icon string
|
||||
Permissions []string
|
||||
Order int
|
||||
Controller string
|
||||
SideMenu []NavigationItem
|
||||
}
|
||||
|
||||
// HasNavigation is implemented by plugins that declare backend navigation.
|
||||
type HasNavigation interface {
|
||||
Navigation() []NavigationItem
|
||||
}
|
||||
|
||||
// SettingsItem is one registerSettings() entry.
|
||||
type SettingsItem struct {
|
||||
Code string
|
||||
Label string
|
||||
Description string
|
||||
Category string
|
||||
Icon string
|
||||
Model string
|
||||
Order int
|
||||
Keywords []string
|
||||
Permissions []string
|
||||
Form string `json:"-"`
|
||||
NewModel func() any `json:"-"`
|
||||
}
|
||||
|
||||
// HasSettings is implemented by plugins that declare settings screens.
|
||||
type HasSettings interface {
|
||||
Settings() []SettingsItem
|
||||
}
|
||||
|
||||
// Option is one dropdown choice. Label may be a phrase key until request time.
|
||||
type Option struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// DropdownOptionsProvider serves method-backed dropdown options.
|
||||
type DropdownOptionsProvider interface {
|
||||
DropdownOptions(field string) []Option
|
||||
}
|
||||
|
||||
// ListExtendQuery optionally narrows the admin list query.
|
||||
type ListExtendQuery interface {
|
||||
ListExtendQuery(ctx context.Context, db *gorm.DB) *gorm.DB
|
||||
}
|
||||
|
||||
// ListRelationColumnMapper maps a source-schema relation column onto the
|
||||
// physical column exposed by the related Go model. It supports legacy admin
|
||||
// schemas whose public field names no longer match the database schema.
|
||||
type ListRelationColumnMapper interface {
|
||||
ListRelationColumn(relation, column string) (string, bool)
|
||||
}
|
||||
|
||||
// FormExtendQuery optionally narrows admin form record lookup.
|
||||
type FormExtendQuery interface {
|
||||
FormExtendQuery(ctx context.Context, db *gorm.DB) *gorm.DB
|
||||
}
|
||||
|
||||
// FormBeforeCreate optionally rejects or stamps a record before insert.
|
||||
type FormBeforeCreate interface {
|
||||
FormBeforeCreate(ctx context.Context, model any) error
|
||||
}
|
||||
|
||||
// FormBeforeUpdate optionally rejects or stamps a record before update.
|
||||
type FormBeforeUpdate interface {
|
||||
FormBeforeUpdate(ctx context.Context, model any) error
|
||||
}
|
||||
|
||||
// FormAfterCreate optionally runs after insert, inside the same transaction.
|
||||
type FormAfterCreate interface {
|
||||
FormAfterCreate(ctx context.Context, model any) error
|
||||
}
|
||||
|
||||
// FormAfterUpdate optionally runs after update, inside the same transaction.
|
||||
type FormAfterUpdate interface {
|
||||
FormAfterUpdate(ctx context.Context, model any) error
|
||||
}
|
||||
|
||||
// FormBeforeDelete optionally rejects a record before delete.
|
||||
type FormBeforeDelete interface {
|
||||
FormBeforeDelete(ctx context.Context, model any) error
|
||||
}
|
||||
|
||||
// FormAfterDelete optionally runs after delete, inside the same transaction.
|
||||
type FormAfterDelete interface {
|
||||
FormAfterDelete(ctx context.Context, model any) error
|
||||
}
|
||||
|
||||
// RelationExtendManageQuery optionally narrows relation-manager candidates.
|
||||
type RelationExtendManageQuery interface {
|
||||
RelationExtendManageQuery(ctx context.Context, relation string, db *gorm.DB) *gorm.DB
|
||||
}
|
||||
|
||||
// RelationExtendOptionsQuery optionally narrows the rows a form relation
|
||||
// field offers (D-17). The same scoped query revalidates submitted ids on
|
||||
// save, so a row it does not return cannot be attached (D-18).
|
||||
type RelationExtendOptionsQuery interface {
|
||||
RelationExtendOptionsQuery(ctx context.Context, field string, db *gorm.DB) *gorm.DB
|
||||
}
|
||||
|
||||
// RelationBeforeLink optionally stamps pivot columns before a link insert.
|
||||
type RelationBeforeLink interface {
|
||||
RelationBeforeLink(ctx context.Context, relation string, parent, related any, pivot map[string]any) error
|
||||
}
|
||||
|
||||
// FilterScope is the model capability a config_filter scope name may call.
|
||||
// FilterScopes is the exact, case-sensitive set of names; request text never
|
||||
// selects a method outside that set.
|
||||
type FilterScope interface {
|
||||
FilterScopes() []string
|
||||
FilterScope(name string, db *gorm.DB, value any) *gorm.DB
|
||||
}
|
||||
|
||||
// FilterOptions serves the choices of a model-backed config_filter scope
|
||||
// (D-27). It is implemented by the same model as FilterScope and receives the
|
||||
// filter's scope method name; labels may be phrase keys.
|
||||
type FilterOptions interface {
|
||||
FilterOptions(scope string) []Option
|
||||
}
|
||||
|
||||
// HasLang is implemented by plugins that ship embedded translation YAML
|
||||
// under lang/<locale>/<group>.yaml.
|
||||
type HasLang interface {
|
||||
LangFS() fs.FS
|
||||
}
|
||||
|
||||
// HasLangOverrides is implemented by plugins (usually the app plugin) that
|
||||
// replace or add translations of any loaded namespace, including the
|
||||
// framework's backend::lang admin strings, and may add locales (D-20).
|
||||
// Layout: lang/<locale>/<namespace>/<group>.yaml, e.g.
|
||||
// lang/pl/backend/lang.yaml.
|
||||
type HasLangOverrides interface {
|
||||
LangOverridesFS() fs.FS
|
||||
}
|
||||
|
||||
// HasMailTemplates is implemented by plugins that ship Winter-shaped mail
|
||||
// templates and layout aliases. MailTemplatesFS contains views/mail assets;
|
||||
// MailTemplates lists dotted template names; MailLayouts maps a short layout
|
||||
// name to the full dotted layout name.
|
||||
type HasMailTemplates interface {
|
||||
MailTemplatesFS() fs.FS
|
||||
MailTemplates() []string
|
||||
MailLayouts() map[string]string
|
||||
}
|
||||
|
||||
// OptionalMessage is a service an optional plugin may publish so other
|
||||
// plugins can integrate without importing that plugin's package.
|
||||
type OptionalMessage interface {
|
||||
Message() string
|
||||
}
|
||||
|
||||
// Future capability families are type-asserted when their first consumer
|
||||
// packages exist:
|
||||
//
|
||||
// HasListeners
|
||||
// HasSchedule
|
||||
//
|
||||
// The kernel type-asserts HasConfig (party, before Register), HasCommands
|
||||
// (generated app main, after Boot), HasMigrations (lagoon migrate), and
|
||||
// HasMiddleware/HasMiddlewareFactories/HasHouseMiddleware/HasRoutes
|
||||
// (surf assemble). surf.BucketProvider is type-asserted in Assemble/
|
||||
// BuildRouter (not a pact interface: pact cannot import surf without a cycle).
|
||||
148
modules/pact/capabilities_test.go
Normal file
148
modules/pact/capabilities_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package pact
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
)
|
||||
|
||||
type configOnly struct{}
|
||||
|
||||
func (configOnly) ConfigFS() fs.FS {
|
||||
return fstest.MapFS{"config/config.yaml": {Data: []byte("k: 1\n")}}
|
||||
}
|
||||
|
||||
type commandsOnly struct{}
|
||||
|
||||
func (commandsOnly) Commands() []bonfire.Command {
|
||||
return []bonfire.Command{{Name: "demo:run"}}
|
||||
}
|
||||
|
||||
type bothCaps struct {
|
||||
configOnly
|
||||
commandsOnly
|
||||
}
|
||||
|
||||
type neither struct{}
|
||||
|
||||
type migrationsOnly struct{}
|
||||
|
||||
func (migrationsOnly) Migrations() []*gormigrate.Migration {
|
||||
return []*gormigrate.Migration{{ID: "202609170001_create_users"}}
|
||||
}
|
||||
|
||||
func TestOptionalCapabilitiesDiscoveredByTypeAssertion(t *testing.T) {
|
||||
plugins := []any{configOnly{}, commandsOnly{}, bothCaps{}, neither{}, extraMessage{}, migrationsOnly{}}
|
||||
|
||||
var configs, commands, messages, migrations, none int
|
||||
for _, p := range plugins {
|
||||
_, hasCfg := p.(HasConfig)
|
||||
_, hasCmd := p.(HasCommands)
|
||||
_, hasMsg := p.(OptionalMessage)
|
||||
_, hasMig := p.(HasMigrations)
|
||||
if hasCfg {
|
||||
configs++
|
||||
}
|
||||
if hasCmd {
|
||||
commands++
|
||||
}
|
||||
if hasMsg {
|
||||
messages++
|
||||
}
|
||||
if hasMig {
|
||||
migrations++
|
||||
}
|
||||
if !hasCfg && !hasCmd && !hasMsg && !hasMig {
|
||||
none++
|
||||
}
|
||||
}
|
||||
if configs != 2 {
|
||||
t.Fatalf("HasConfig count = %d, want 2", configs)
|
||||
}
|
||||
if commands != 2 {
|
||||
t.Fatalf("HasCommands count = %d, want 2", commands)
|
||||
}
|
||||
if messages != 1 {
|
||||
t.Fatalf("OptionalMessage count = %d, want 1", messages)
|
||||
}
|
||||
if migrations != 1 {
|
||||
t.Fatalf("HasMigrations count = %d, want 1", migrations)
|
||||
}
|
||||
if none != 1 {
|
||||
t.Fatalf("neither count = %d, want 1", none)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasMigrationsReturnsOrderedSet(t *testing.T) {
|
||||
var p HasMigrations = migrationsOnly{}
|
||||
set := p.Migrations()
|
||||
if len(set) != 1 || set[0].ID != "202609170001_create_users" {
|
||||
t.Fatalf("Migrations = %+v", set)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasConfigReturnsFilesystem(t *testing.T) {
|
||||
var p HasConfig = configOnly{}
|
||||
fsys := p.ConfigFS()
|
||||
if fsys == nil {
|
||||
t.Fatal("ConfigFS returned nil")
|
||||
}
|
||||
raw, err := fs.ReadFile(fsys, "config/config.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(raw) != "k: 1\n" {
|
||||
t.Fatalf("ConfigFS body = %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasCommandsReturnsNamespacedCommand(t *testing.T) {
|
||||
var p HasCommands = commandsOnly{}
|
||||
cmds := p.Commands()
|
||||
if len(cmds) != 1 || cmds[0].Name != "demo:run" {
|
||||
t.Fatalf("Commands = %+v", cmds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalMessageContract(t *testing.T) {
|
||||
var msg OptionalMessage = extraMessage{s: "hello-from-optional"}
|
||||
if msg.Message() != "hello-from-optional" {
|
||||
t.Fatalf("Message = %q", msg.Message())
|
||||
}
|
||||
}
|
||||
|
||||
type extraMessage struct{ s string }
|
||||
|
||||
func (e extraMessage) Message() string { return e.s }
|
||||
|
||||
type routesOnly struct{}
|
||||
|
||||
func (routesOnly) Routes(Router) error { return nil }
|
||||
|
||||
type middlewareOnly struct{}
|
||||
|
||||
func (middlewareOnly) Middlewares() map[string]Middleware {
|
||||
return map[string]Middleware{"jwt.auth": nil}
|
||||
}
|
||||
|
||||
type modelsOnly struct{}
|
||||
|
||||
func (modelsOnly) Models() []any { return []any{struct{}{}} }
|
||||
|
||||
func TestHTTPCapabilitiesDiscoveredByTypeAssertion(t *testing.T) {
|
||||
var r HasRoutes = routesOnly{}
|
||||
if err := r.Routes(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m HasMiddleware = middlewareOnly{}
|
||||
if _, ok := m.Middlewares()["jwt.auth"]; !ok {
|
||||
t.Fatal("missing jwt.auth")
|
||||
}
|
||||
var models HasModels = modelsOnly{}
|
||||
if len(models.Models()) != 1 {
|
||||
t.Fatalf("Models = %+v", models.Models())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user