package crud import ( "context" "git.nspix.com/golang/micro/gateway/http" "git.nspix.com/golang/rest/orm/query" "git.nspix.com/golang/rest/orm/schema" "github.com/jinzhu/inflection" "github.com/russross/blackfriday" "gorm.io/gorm" "sort" "strings" "sync" ) var ( Default = New() enable int32 = 1 ) type ( EachQueryCallback func(val interface{}, field *schema.Schema, search *query.Query, ctx *http.Context) (err error) QueryCallback func(val interface{}, query *query.Query, ctx *http.Context) (err error) SaveCallback func(val interface{}, ctx *http.Context) (err error) DeleteCallback func(val interface{}, ctx *http.Context) (err error) Naming interface { DisplayName() string } CRUD struct { db *gorm.DB formatter *Formatter once sync.Once ctx context.Context entityLocker sync.RWMutex entities map[string]*Entity formatLocker sync.RWMutex httpSvr *http.Server formatCallbacks map[string]EachQueryCallback } ) func (crud *CRUD) getCallbackByFormat(format string) EachQueryCallback { crud.formatLocker.RLock() v, ok := crud.formatCallbacks[format] crud.formatLocker.RUnlock() if ok { return v } return nil } func (crud *CRUD) Register(module string, value interface{}, opts ...Option) { o := &Options{ctx: crud.ctx, DB: crud.db} for _, f := range opts { f(o) } if crud.httpSvr == nil { return } e := newEntity(crud.ctx, value, o.DB) e.instance = crud e.Callbacks = o.Callbacks e.SetModule(module) if o.enable != nil { e.enable = o.enable } else { e.enable = &enable } if o.Formatter == nil { e.formatter = crud.formatter } else { e.formatter = o.Formatter } crud.entityLocker.Lock() crud.entities[e.stmt.Table+"@"+e.Module] = e crud.entityLocker.Unlock() e.naming.plural = inflection.Plural(e.stmt.Table) e.naming.singular = inflection.Singular(e.stmt.Table) if o.Scenarios == nil || len(o.Scenarios) == 0 { e.Scenarios = []string{ScenarioList, ScenarioView, ScenarioCreate, ScenarioUpdate, ScenarioDelete, ScenarioExport} } else { e.Scenarios = o.Scenarios } if v, ok := value.(Naming); ok { e.naming.label = v.DisplayName() } else { e.naming.label = strings.Title(e.naming.singular) } e.prefix = o.Prefix if e.hasScenarios(ScenarioList) { e.Urls[ScenarioList] = o.Prefix + "/" + e.naming.plural crud.httpSvr.Handle("GET", e.Urls[ScenarioList], e.actionIndex, o.Middleware...) } if e.hasScenarios(ScenarioView) { e.Urls[ScenarioView] = o.Prefix + "/" + e.naming.singular + "/:id" crud.httpSvr.Handle("GET", e.Urls[ScenarioView], e.actionView, o.Middleware...) } if e.hasScenarios(ScenarioCreate) { e.Urls[ScenarioCreate] = o.Prefix + "/" + e.naming.singular crud.httpSvr.Handle("POST", e.Urls[ScenarioCreate], e.actionCreate, o.Middleware...) } if e.hasScenarios(ScenarioUpdate) { e.Urls[ScenarioUpdate] = o.Prefix + "/" + e.naming.singular + "/:id" crud.httpSvr.Handle("PUT", e.Urls[ScenarioUpdate], e.actionUpdate, o.Middleware...) } if e.hasScenarios(ScenarioDelete) { e.Urls[ScenarioDelete] = o.Prefix + "/" + e.naming.singular + "/:id" crud.httpSvr.Handle("DELETE", e.Urls[ScenarioDelete], e.actionDelete, o.Middleware...) } if e.hasScenarios(ScenarioExport) { e.Urls[ScenarioExport] = o.Prefix + "/" + e.naming.singular + "-export" crud.httpSvr.Handle("GET", e.Urls[ScenarioExport], e.actionExport, o.Middleware...) } } func (crud *CRUD) AttachFormatQueryHook(format string, cb EachQueryCallback) { crud.formatLocker.Lock() crud.formatCallbacks[format] = cb crud.formatLocker.Unlock() } func (crud *CRUD) SetDB(db *gorm.DB) { crud.db = db } func (crud *CRUD) GetDB() *gorm.DB { return crud.db } func (crud *CRUD) SetHttpServer(svr *http.Server) { crud.httpSvr = svr crud.once.Do(func() { crud.bindDocAction() }) } func (crud *CRUD) SetContext(ctx context.Context) { crud.ctx = ctx if crud.httpSvr == nil { crud.httpSvr = http.FromContext(ctx) } if crud.db == nil { crud.db = ctx.Value("db").(*gorm.DB) } crud.once.Do(func() { crud.bindDocAction() }) } func (crud *CRUD) readSnapshot() Entities { crud.entityLocker.RLock() i := 0 es := make(Entities, len(crud.entities)) for _, e := range crud.entities { es[i] = e i++ } crud.entityLocker.RUnlock() sort.Sort(es) return es } func (crud *CRUD) actionCatalog(c *http.Context) (err error) { var sb strings.Builder sb.WriteString("# 接口目录") sb.WriteByte('\n') sb.WriteByte('\n') es := crud.readSnapshot() for _, e := range es { sb.WriteString("* [" + e.naming.label + "接口](/crud/doc/" + e.stmt.Table + "@" + e.Module + ")") sb.WriteByte('\n') } c.Response().Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = c.Response().Write([]byte(`
`)) _, _ = c.Response().Write(blackfriday.Run([]byte(sb.String()))) _, _ = c.Response().Write([]byte(`
`)) return } func (crud *CRUD) actionDocs(c *http.Context) (err error) { crud.entityLocker.RLock() entity, ok := crud.entities[c.ParamValue("id")] crud.entityLocker.RUnlock() if !ok { return c.Error(4004, "record not found") } c.Response().Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = c.Response().Write([]byte(`` + entity.naming.label + `
`)) _, _ = c.Response().Write(blackfriday.Run(crud.generateDoc(entity))) _, _ = c.Response().Write([]byte(`
`)) return } func (crud *CRUD) actionEntities(c *http.Context) (err error) { es := crud.readSnapshot() names := make([]map[string]interface{}, 0) for _, e := range es { names = append(names, map[string]interface{}{ "module": e.Module, "table": e.stmt.Table, "label": e.naming.label, "url": e.prefix + "/" + e.naming.singular, "urls": e.Urls, "scenarios": e.Scenarios, }) } return c.Success(names) } func (crud *CRUD) bindDocAction() { if crud.httpSvr != nil { crud.httpSvr.Handle("GET", "/crud/entities", crud.actionEntities) crud.httpSvr.Handle("GET", "/crud/docs", crud.actionCatalog) crud.httpSvr.Handle("GET", "/crud/doc/:id", crud.actionDocs) } } func New() *CRUD { return &CRUD{ formatter: DefaultFormat, ctx: context.Background(), formatCallbacks: make(map[string]EachQueryCallback), entities: make(map[string]*Entity), } }