diff --git a/cmd/gf/README.MD b/cmd/gf/README.MD index fa88966b2..374ef4012 100644 --- a/cmd/gf/README.MD +++ b/cmd/gf/README.MD @@ -5,6 +5,7 @@ ## 1. Install +## 1) PreCompiled Binary You can also install `gf` tool using pre-built binaries: https://github.com/gogf/gf/releases 1. `Mac` & `Linux` @@ -19,6 +20,13 @@ You can also install `gf` tool using pre-built binaries: https://github.com/gogf 3. Database `sqlite` and `oracle` are not support in `gf gen` command in default as it needs `cgo` and `gcc`, you can manually make some changes to the source codes and do the building. +## 2) Manually Install + + ```shell +git clone https://github.com/gogf/gf && cd cmd/gf && go install + ``` + + ## 2. Commands ```html $ gf diff --git a/cmd/gf/go.mod b/cmd/gf/go.mod index 430fb4783..94ef8f12a 100644 --- a/cmd/gf/go.mod +++ b/cmd/gf/go.mod @@ -6,7 +6,7 @@ require ( github.com/gogf/gf/contrib/drivers/mssql/v2 v2.0.0-rc2 github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.0.0-rc2 github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.0.0-rc2 - github.com/gogf/gf/v2 v2.0.0-rc + github.com/gogf/gf/v2 v2.0.0 github.com/olekukonko/tablewriter v0.0.5 ) diff --git a/cmd/gf/internal/consts/consts_gen_dao_template_dao.go b/cmd/gf/internal/consts/consts_gen_dao_template_dao.go index f07fd1d55..4fedb480e 100644 --- a/cmd/gf/internal/consts/consts_gen_dao_template_dao.go +++ b/cmd/gf/internal/consts/consts_gen_dao_template_dao.go @@ -11,10 +11,13 @@ import ( "{TplImportPrefix}/internal" ) +// internal{TplTableNameCamelCase}Dao is internal type for wrapping internal DAO implements. +type internal{TplTableNameCamelCase}Dao = *internal.{TplTableNameCamelCase}Dao + // {TplTableNameCamelLowerCase}Dao is the data access object for table {TplTableName}. // You can define custom methods on it to extend its functionality as you wish. type {TplTableNameCamelLowerCase}Dao struct { - *internal.{TplTableNameCamelCase}Dao + internal{TplTableNameCamelCase}Dao } var ( diff --git a/contrib/drivers/mssql/mssql.go b/contrib/drivers/mssql/mssql.go index 908aba1a8..f566870ce 100644 --- a/contrib/drivers/mssql/mssql.go +++ b/contrib/drivers/mssql/mssql.go @@ -223,7 +223,7 @@ func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, return nil, err } - result, err = d.DoGetAll(ctx, link, `SELECT NAME FROM SYSOBJECTS WHERE XTYPE='U' AND STATUS >= 0 ORDER BY NAME`) + result, err = d.DoSelect(ctx, link, `SELECT NAME FROM SYSOBJECTS WHERE XTYPE='U' AND STATUS >= 0 ORDER BY NAME`) if err != nil { return } @@ -289,7 +289,7 @@ ORDER BY a.id,a.colorder`, table, ) structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.DoGetAll(ctx, link, structureSql) + result, err = d.DoSelect(ctx, link, structureSql) if err != nil { return nil } diff --git a/contrib/drivers/oracle/oracle.go b/contrib/drivers/oracle/oracle.go index 4ea25c96b..ccae47bd5 100644 --- a/contrib/drivers/oracle/oracle.go +++ b/contrib/drivers/oracle/oracle.go @@ -194,7 +194,7 @@ func (d *Driver) parseSql(sql string) string { // Note that it ignores the parameter `schema` in oracle database, as it is not necessary. func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { var result gdb.Result - result, err = d.DoGetAll(ctx, nil, "SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME") + result, err = d.DoSelect(ctx, nil, "SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME") if err != nil { return } @@ -245,7 +245,7 @@ FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID`, return nil } structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.DoGetAll(ctx, link, structureSql) + result, err = d.DoSelect(ctx, link, structureSql) if err != nil { return nil } diff --git a/contrib/drivers/pgsql/pgsql.go b/contrib/drivers/pgsql/pgsql.go index 4f87cdc01..31696134e 100644 --- a/contrib/drivers/pgsql/pgsql.go +++ b/contrib/drivers/pgsql/pgsql.go @@ -142,7 +142,7 @@ func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, schema[0], ) } - result, err = d.DoGetAll(ctx, link, query) + result, err = d.DoSelect(ctx, link, query) if err != nil { return } @@ -198,7 +198,7 @@ ORDER BY a.attnum`, return nil } structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.DoGetAll(ctx, link, structureSql) + result, err = d.DoSelect(ctx, link, structureSql) if err != nil { return nil } diff --git a/contrib/drivers/sqlite/sqlite.go b/contrib/drivers/sqlite/sqlite.go index bca87fb33..66bba094d 100644 --- a/contrib/drivers/sqlite/sqlite.go +++ b/contrib/drivers/sqlite/sqlite.go @@ -106,7 +106,7 @@ func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, return nil, err } - result, err = d.DoGetAll(ctx, link, `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME`) + result, err = d.DoSelect(ctx, link, `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME`) if err != nil { return } @@ -141,7 +141,7 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string if link, err = d.SlaveLink(useSchema); err != nil { return nil } - result, err = d.DoGetAll(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, table)) + result, err = d.DoSelect(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, table)) if err != nil { return nil } diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index 82594aa01..a112d16cd 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -20,6 +20,7 @@ import ( "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/grand" ) @@ -94,15 +95,18 @@ type DB interface { // Internal APIs for CURD, which can be overwritten by custom CURD implements. // =========================================================================== - DoGetAll(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) // See Core.DoGetAll. + DoSelect(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) // See Core.DoSelect. DoInsert(ctx context.Context, link Link, table string, data List, option DoInsertOption) (result sql.Result, err error) // See Core.DoInsert. DoUpdate(ctx context.Context, link Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) // See Core.DoUpdate. DoDelete(ctx context.Context, link Link, table string, condition string, args ...interface{}) (result sql.Result, err error) // See Core.DoDelete. - DoQuery(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) // See Core.DoQuery. - DoExec(ctx context.Context, link Link, sql string, args ...interface{}) (result sql.Result, err error) // See Core.DoExec. - DoFilter(ctx context.Context, link Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) // See Core.DoFilter. - DoCommit(ctx context.Context, in DoCommitInput) (out DoCommitOutput, err error) // See Core.DoCommit. - DoPrepare(ctx context.Context, link Link, sql string) (*Stmt, error) // See Core.DoPrepare. + + DoQuery(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) // See Core.DoQuery. + DoExec(ctx context.Context, link Link, sql string, args ...interface{}) (result sql.Result, err error) // See Core.DoExec. + + DoFilter(ctx context.Context, link Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) // See Core.DoFilter. + DoCommit(ctx context.Context, in DoCommitInput) (out DoCommitOutput, err error) // See Core.DoCommit. + + DoPrepare(ctx context.Context, link Link, sql string) (*Stmt, error) // See Core.DoPrepare. // =========================================================================== // Query APIs for convenience purpose. @@ -212,9 +216,6 @@ type Driver interface { // Link is a common database function wrapper interface. type Link interface { - Query(sql string, args ...interface{}) (*sql.Rows, error) - Exec(sql string, args ...interface{}) (sql.Result, error) - Prepare(sql string) (*sql.Stmt, error) QueryContext(ctx context.Context, sql string, args ...interface{}) (*sql.Rows, error) ExecContext(ctx context.Context, sql string, args ...interface{}) (sql.Result, error) PrepareContext(ctx context.Context, sql string) (*sql.Stmt, error) @@ -237,10 +238,10 @@ type Sql struct { // DoInsertOption is the input struct for function DoInsert. type DoInsertOption struct { - OnDuplicateStr string - OnDuplicateMap map[string]interface{} - InsertOption int // Insert operation. - BatchCount int // Batch count for batch inserting. + OnDuplicateStr string // Custom string for `on duplicated` statement. + OnDuplicateMap map[string]interface{} // Custom key-value map from `OnDuplicateEx` function for `on duplicated` statement. + InsertOption int // Insert operation in constant value. + BatchCount int // Batch count for batch inserting. } // TableField is the struct for table field. @@ -284,9 +285,10 @@ const ( ctxTimeoutTypeExec = iota ctxTimeoutTypeQuery ctxTimeoutTypePrepare - commandEnvKeyForDryRun = "gf.gdb.dryrun" - modelForDaoSuffix = `ForDao` - dbRoleSlave = `slave` + commandEnvKeyForDryRun = "gf.gdb.dryrun" + modelForDaoSuffix = `ForDao` + dbRoleSlave = `slave` + contextKeyForDB gctx.StrKey = `DBInContext` ) const ( diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index 0dd217829..1bb335d9f 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -45,7 +45,6 @@ func (c *Core) Ctx(ctx context.Context) DB { configNode = c.db.GetConfig() ) *newCore = *c - newCore.ctx = ctx // It creates a new DB object, which is commonly a wrapper for object `Core`. newCore.db, err = driverMap[configNode.Type].New(newCore, configNode) if err != nil { @@ -53,6 +52,7 @@ func (c *Core) Ctx(ctx context.Context) DB { // Do not let it continue. panic(err) } + newCore.ctx = WithDB(ctx, newCore.db) return newCore.db } @@ -145,11 +145,11 @@ func (c *Core) Slave(schema ...string) (*sql.DB, error) { // GetAll queries and returns data records from database. func (c *Core) GetAll(ctx context.Context, sql string, args ...interface{}) (Result, error) { - return c.db.DoGetAll(ctx, nil, sql, args...) + return c.db.DoSelect(ctx, nil, sql, args...) } -// DoGetAll queries and returns data records from database. -func (c *Core) DoGetAll(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) { +// DoSelect queries and returns data records from database. +func (c *Core) DoSelect(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) { return c.db.DoQuery(ctx, link, sql, args...) } @@ -168,7 +168,7 @@ func (c *Core) GetOne(ctx context.Context, sql string, args ...interface{}) (Rec // GetArray queries and returns data values as slice from database. // Note that if there are multiple columns in the result, it returns just one column values randomly. func (c *Core) GetArray(ctx context.Context, sql string, args ...interface{}) ([]Value, error) { - all, err := c.db.DoGetAll(ctx, nil, sql, args...) + all, err := c.db.DoSelect(ctx, nil, sql, args...) if err != nil { return nil, err } diff --git a/database/gdb/gdb_core_utility.go b/database/gdb/gdb_core_utility.go index 0e0ff4403..9dd3beedd 100644 --- a/database/gdb/gdb_core_utility.go +++ b/database/gdb/gdb_core_utility.go @@ -8,12 +8,39 @@ package gdb import ( + "context" + "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" ) +// WithDB injects given db object into context and returns a new context. +func WithDB(ctx context.Context, db DB) context.Context { + if db == nil { + return ctx + } + dbCtx := db.GetCtx() + if ctxDb := DBFromCtx(dbCtx); ctxDb != nil { + return dbCtx + } + ctx = context.WithValue(ctx, contextKeyForDB, db) + return ctx +} + +// DBFromCtx retrieves and returns DB object from context. +func DBFromCtx(ctx context.Context) DB { + if ctx == nil { + return nil + } + v := ctx.Value(contextKeyForDB) + if v != nil { + return v.(DB) + } + return nil +} + // MasterLink acts like function Master but with additional `schema` parameter specifying // the schema for the connection. It is defined for internal usage. // Also see Master. diff --git a/database/gdb/gdb_driver_mysql.go b/database/gdb/gdb_driver_mysql.go index cbfff2434..a7e161185 100644 --- a/database/gdb/gdb_driver_mysql.go +++ b/database/gdb/gdb_driver_mysql.go @@ -100,7 +100,7 @@ func (d *DriverMysql) Tables(ctx context.Context, schema ...string) (tables []st if err != nil { return nil, err } - result, err = d.DoGetAll(ctx, link, `SHOW TABLES`) + result, err = d.DoSelect(ctx, link, `SHOW TABLES`) if err != nil { return } @@ -147,7 +147,7 @@ func (d *DriverMysql) TableFields(ctx context.Context, table string, schema ...s if link, err = d.SlaveLink(useSchema); err != nil { return nil } - result, err = d.DoGetAll( + result, err = d.DoSelect( ctx, link, fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.QuoteWord(table)), ) diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index b1c1e8196..94b92a8d9 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -44,6 +44,7 @@ type Model struct { lockInfo string // Lock for update or in shared lock. cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage. cacheOption CacheOption // Cache option for query statement. + hook HookHandler // Hook functions for model hook feature. unscoped bool // Disables soft deleting features when select/delete operations. safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement. diff --git a/database/gdb/gdb_model_delete.go b/database/gdb/gdb_model_delete.go index 334338f6f..8b27cd945 100644 --- a/database/gdb/gdb_model_delete.go +++ b/database/gdb/gdb_model_delete.go @@ -34,18 +34,40 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { ) // Soft deleting. if !m.unscoped && fieldNameDelete != "" { - return m.db.DoUpdate( - m.GetCtx(), - m.getLink(true), - m.tables, - fmt.Sprintf(`%s=?`, m.db.GetCore().QuoteString(fieldNameDelete)), - conditionWhere+conditionExtra, - append([]interface{}{gtime.Now().String()}, conditionArgs...), - ) + in := &HookUpdateInput{ + internalParamHookUpdate: internalParamHookUpdate{ + internalParamHook: internalParamHook{ + db: m.db, + link: m.getLink(true), + }, + handler: m.hook.Update, + }, + Table: m.tables, + Data: fmt.Sprintf(`%s=?`, m.db.GetCore().QuoteString(fieldNameDelete)), + Condition: conditionWhere + conditionExtra, + Args: append([]interface{}{gtime.Now().String()}, conditionArgs...), + } + return in.Next(m.GetCtx()) } conditionStr := conditionWhere + conditionExtra if !gstr.ContainsI(conditionStr, " WHERE ") { - return nil, gerror.NewCode(gcode.CodeMissingParameter, "there should be WHERE condition statement for DELETE operation") + return nil, gerror.NewCode( + gcode.CodeMissingParameter, + "there should be WHERE condition statement for DELETE operation", + ) } - return m.db.DoDelete(m.GetCtx(), m.getLink(true), m.tables, conditionStr, conditionArgs...) + + in := &HookDeleteInput{ + internalParamHookDelete: internalParamHookDelete{ + internalParamHook: internalParamHook{ + db: m.db, + link: m.getLink(true), + }, + handler: m.hook.Delete, + }, + Table: m.tables, + Condition: conditionStr, + Args: conditionArgs, + } + return in.Next(m.GetCtx()) } diff --git a/database/gdb/gdb_model_hook.go b/database/gdb/gdb_model_hook.go new file mode 100644 index 000000000..ca2702c40 --- /dev/null +++ b/database/gdb/gdb_model_hook.go @@ -0,0 +1,148 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gdb + +import ( + "context" + "database/sql" + + "github.com/gogf/gf/v2/text/gstr" +) + +type ( + HookFuncSelect func(ctx context.Context, in *HookSelectInput) (result Result, err error) + HookFuncInsert func(ctx context.Context, in *HookInsertInput) (result sql.Result, err error) + HookFuncUpdate func(ctx context.Context, in *HookUpdateInput) (result sql.Result, err error) + HookFuncDelete func(ctx context.Context, in *HookDeleteInput) (result sql.Result, err error) +) + +// HookHandler manages all supported hook functions for Model. +type HookHandler struct { + Select HookFuncSelect + Insert HookFuncInsert + Update HookFuncUpdate + Delete HookFuncDelete +} + +// internalParamHook manages all internal parameters for hook operations. +// The `internal` obviously means you cannot access these parameters outside this package. +type internalParamHook struct { + db DB // Underlying DB object. + link Link // Connection object from third party sql driver. + handlerCalled bool // Simple mark for custom handler called, in case of recursive calling. + removedWhere bool // Removed mark for condition string that was removed `WHERE` prefix. +} + +type internalParamHookSelect struct { + internalParamHook + handler HookFuncSelect +} + +type internalParamHookInsert struct { + internalParamHook + handler HookFuncInsert +} + +type internalParamHookUpdate struct { + internalParamHook + handler HookFuncUpdate +} + +type internalParamHookDelete struct { + internalParamHook + handler HookFuncDelete +} + +// HookSelectInput holds the parameters for select hook operation. +type HookSelectInput struct { + internalParamHookSelect + Table string + Sql string + Args []interface{} +} + +// HookInsertInput holds the parameters for insert hook operation. +type HookInsertInput struct { + internalParamHookInsert + Table string + Data List + Option DoInsertOption +} + +// HookUpdateInput holds the parameters for update hook operation. +type HookUpdateInput struct { + internalParamHookUpdate + Table string + Data interface{} + Condition string + Args []interface{} +} + +// HookDeleteInput holds the parameters for delete hook operation. +type HookDeleteInput struct { + internalParamHookDelete + Table string + Condition string + Args []interface{} +} + +// Next calls the next hook handler. +func (h *HookSelectInput) Next(ctx context.Context) (result Result, err error) { + if h.handler != nil && !h.handlerCalled { + h.handlerCalled = true + return h.handler(ctx, h) + } + return h.db.DoSelect(ctx, h.link, h.Sql, h.Args...) +} + +// Next calls the next hook handler. +func (h *HookInsertInput) Next(ctx context.Context) (result sql.Result, err error) { + if h.handler != nil && !h.handlerCalled { + h.handlerCalled = true + return h.handler(ctx, h) + } + return h.db.DoInsert(ctx, h.link, h.Table, h.Data, h.Option) +} + +// Next calls the next hook handler. +func (h *HookUpdateInput) Next(ctx context.Context) (result sql.Result, err error) { + if h.handler != nil && !h.handlerCalled { + h.handlerCalled = true + if gstr.HasPrefix(h.Condition, " WHERE ") { + h.removedWhere = true + h.Condition = gstr.TrimLeftStr(h.Condition, " WHERE ") + } + return h.handler(ctx, h) + } + if h.removedWhere { + h.Condition = " WHERE " + h.Condition + } + return h.db.DoUpdate(ctx, h.link, h.Table, h.Data, h.Condition, h.Args...) +} + +// Next calls the next hook handler. +func (h *HookDeleteInput) Next(ctx context.Context) (result sql.Result, err error) { + if h.handler != nil && !h.handlerCalled { + h.handlerCalled = true + if gstr.HasPrefix(h.Condition, " WHERE ") { + h.removedWhere = true + h.Condition = gstr.TrimLeftStr(h.Condition, " WHERE ") + } + return h.handler(ctx, h) + } + if h.removedWhere { + h.Condition = " WHERE " + h.Condition + } + return h.db.DoDelete(ctx, h.link, h.Table, h.Condition, h.Args...) +} + +// Hook sets the hook functions for current model. +func (m *Model) Hook(hook HookHandler) *Model { + model := m.getModel() + model.hook = hook + return model +} diff --git a/database/gdb/gdb_model_insert.go b/database/gdb/gdb_model_insert.go index 7039bd38b..4013ca7f7 100644 --- a/database/gdb/gdb_model_insert.go +++ b/database/gdb/gdb_model_insert.go @@ -140,7 +140,7 @@ func (m *Model) OnDuplicate(onDuplicate ...interface{}) *Model { return model } -// OnDuplicateEx sets the excluding columns for operations when columns conflicts occurs. +// OnDuplicateEx sets the excluding columns for operations when columns conflict occurs. // In MySQL, this is used for "ON DUPLICATE KEY UPDATE" statement. // The parameter `onDuplicateEx` can be type of string/map/slice. // Example: @@ -310,7 +310,20 @@ func (m *Model) doInsertWithOption(insertOption int) (result sql.Result, err err if err != nil { return result, err } - return m.db.DoInsert(m.GetCtx(), m.getLink(true), m.tables, list, doInsertOption) + + in := &HookInsertInput{ + internalParamHookInsert: internalParamHookInsert{ + internalParamHook: internalParamHook{ + db: m.db, + link: m.getLink(true), + }, + handler: m.hook.Insert, + }, + Table: m.tables, + Data: list, + Option: doInsertOption, + } + return in.Next(m.GetCtx()) } func (m *Model) formatDoInsertOption(insertOption int, columnNames []string) (option DoInsertOption, err error) { diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 386f108de..05049639c 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -532,9 +532,21 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e } } } - result, err = m.db.DoGetAll( - m.GetCtx(), m.getLink(false), sql, m.mergeArguments(args)..., - ) + + in := &HookSelectInput{ + internalParamHookSelect: internalParamHookSelect{ + internalParamHook: internalParamHook{ + db: m.db, + link: m.getLink(false), + }, + handler: m.hook.Select, + }, + Table: m.tables, + Sql: sql, + Args: m.mergeArguments(args), + } + result, err = in.Next(m.GetCtx()) + // Cache the result. if cacheKey != "" && err == nil { if m.cacheOption.Duration < 0 { diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index 064fccceb..f1be33829 100644 --- a/database/gdb/gdb_model_update.go +++ b/database/gdb/gdb_model_update.go @@ -74,14 +74,21 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro if !gstr.ContainsI(conditionStr, " WHERE ") { return nil, gerror.NewCode(gcode.CodeMissingParameter, "there should be WHERE condition statement for UPDATE operation") } - return m.db.DoUpdate( - m.GetCtx(), - m.getLink(true), - m.tables, - newData, - conditionStr, - m.mergeArguments(conditionArgs)..., - ) + + in := &HookUpdateInput{ + internalParamHookUpdate: internalParamHookUpdate{ + internalParamHook: internalParamHook{ + db: m.db, + link: m.getLink(true), + }, + handler: m.hook.Update, + }, + Table: m.tables, + Data: newData, + Condition: conditionStr, + Args: m.mergeArguments(conditionArgs), + } + return in.Next(m.GetCtx()) } // Increment increments a column's value by a given amount. diff --git a/database/gdb/gdb_model_with.go b/database/gdb/gdb_model_with.go index e5846e883..3e953a034 100644 --- a/database/gdb/gdb_model_with.go +++ b/database/gdb/gdb_model_with.go @@ -143,7 +143,7 @@ func (m *Model) doWithScanStruct(pointer interface{}) error { } // Recursively with feature checks. - model = m.db.With(field.Value) + model = m.db.With(field.Value).Hook(m.hook) if m.withAll { model = model.WithAll() } else { @@ -258,7 +258,7 @@ func (m *Model) doWithScanStructs(pointer interface{}) error { fieldKeys = structType.FieldKeys() } // Recursively with feature checks. - model = m.db.With(field.Value) + model = m.db.With(field.Value).Hook(m.hook) if m.withAll { model = model.WithAll() } else { diff --git a/database/gdb/gdb_z_mysql_feature_hook_test.go b/database/gdb/gdb_z_mysql_feature_hook_test.go new file mode 100644 index 000000000..0183dfa1f --- /dev/null +++ b/database/gdb/gdb_z_mysql_feature_hook_test.go @@ -0,0 +1,136 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gdb_test + +import ( + "context" + "database/sql" + "fmt" + "testing" + + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/test/gtest" +) + +func Test_Model_Hook_Select(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + m := db.Model(table).Hook(gdb.HookHandler{ + Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { + result, err = in.Next(ctx) + if err != nil { + return + } + for i, record := range result { + record["test"] = gvar.New(100 + record["id"].Int()) + result[i] = record + } + return + }, + }) + all, err := m.Where(`id > 6`).OrderAsc(`id`).All() + t.AssertNil(err) + t.Assert(len(all), 4) + t.Assert(all[0]["id"].Int(), 7) + t.Assert(all[0]["test"].Int(), 107) + t.Assert(all[1]["test"].Int(), 108) + t.Assert(all[2]["test"].Int(), 109) + t.Assert(all[3]["test"].Int(), 110) + }) +} + +func Test_Model_Hook_Insert(t *testing.T) { + table := createTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + m := db.Model(table).Hook(gdb.HookHandler{ + Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) { + for i, item := range in.Data { + item["passport"] = fmt.Sprintf(`test_port_%d`, item["id"]) + item["nickname"] = fmt.Sprintf(`test_name_%d`, item["id"]) + in.Data[i] = item + } + return in.Next(ctx) + }, + }) + _, err := m.Insert(g.Map{ + "id": 1, + "nickname": "name_1", + }) + t.AssertNil(err) + one, err := m.One() + t.AssertNil(err) + t.Assert(one["id"].Int(), 1) + t.Assert(one["passport"], `test_port_1`) + t.Assert(one["nickname"], `test_name_1`) + }) +} + +func Test_Model_Hook_Update(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + m := db.Model(table).Hook(gdb.HookHandler{ + Update: func(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) { + switch value := in.Data.(type) { + case gdb.List: + for i, data := range value { + data["passport"] = `port` + data["nickname"] = `name` + value[i] = data + } + in.Data = value + + case gdb.Map: + value["passport"] = `port` + value["nickname"] = `name` + in.Data = value + } + return in.Next(ctx) + }, + }) + _, err := m.Data(g.Map{ + "nickname": "name_1", + }).WherePri(1).Update() + t.AssertNil(err) + + one, err := m.One() + t.AssertNil(err) + t.Assert(one["id"].Int(), 1) + t.Assert(one["passport"], `port`) + t.Assert(one["nickname"], `name`) + }) +} + +func Test_Model_Hook_Delete(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + m := db.Model(table).Hook(gdb.HookHandler{ + Delete: func(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) { + return db.Model(table).Data(g.Map{ + "nickname": `deleted`, + }).Where(in.Condition).Update() + }, + }) + _, err := m.Where(1).Delete() + t.AssertNil(err) + + all, err := m.All() + t.AssertNil(err) + for _, item := range all { + t.Assert(item["nickname"].String(), `deleted`) + } + }) +} diff --git a/database/gdb/gdb_z_mysql_feature_model_struct_test.go b/database/gdb/gdb_z_mysql_feature_model_struct_test.go index 78dda93d5..e2886e05a 100644 --- a/database/gdb/gdb_z_mysql_feature_model_struct_test.go +++ b/database/gdb/gdb_z_mysql_feature_model_struct_test.go @@ -550,7 +550,7 @@ func Test_Scan_JsonAttributes(t *testing.T) { } table := "jfy_gift" - array := gstr.SplitAndTrim(gtest.TestDataContent(`issue1380.sql`), ";") + array := gstr.SplitAndTrim(gtest.DataContent(`issue1380.sql`), ";") for _, v := range array { if _, err := db.Exec(ctx, v); err != nil { gtest.Error(err) diff --git a/database/gdb/gdb_z_mysql_feature_with_test.go b/database/gdb/gdb_z_mysql_feature_with_test.go index 3175cb2d9..a0eb72860 100644 --- a/database/gdb/gdb_z_mysql_feature_with_test.go +++ b/database/gdb/gdb_z_mysql_feature_with_test.go @@ -10,7 +10,6 @@ import ( "fmt" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/test/gtest" @@ -1644,7 +1643,7 @@ func Test_Table_Relation_With_MultipleDepends1(t *testing.T) { dropTable("table_b") dropTable("table_c") }() - for _, v := range gstr.SplitAndTrim(gfile.GetContents(gdebug.TestDataPath("with_multiple_depends.sql")), ";") { + for _, v := range gstr.SplitAndTrim(gfile.GetContents(gtest.DataPath("with_multiple_depends.sql")), ";") { if _, err := db.Exec(ctx, v); err != nil { gtest.Error(err) } @@ -1716,7 +1715,7 @@ func Test_Table_Relation_With_MultipleDepends2(t *testing.T) { dropTable("table_b") dropTable("table_c") }() - for _, v := range gstr.SplitAndTrim(gfile.GetContents(gdebug.TestDataPath("with_multiple_depends.sql")), ";") { + for _, v := range gstr.SplitAndTrim(gfile.GetContents(gtest.DataPath("with_multiple_depends.sql")), ";") { if _, err := db.Exec(ctx, v); err != nil { gtest.Error(err) } @@ -1803,7 +1802,7 @@ func Test_Table_Relation_With_MultipleDepends_Embedded(t *testing.T) { dropTable("table_b") dropTable("table_c") }() - for _, v := range gstr.SplitAndTrim(gfile.GetContents(gdebug.TestDataPath("with_multiple_depends.sql")), ";") { + for _, v := range gstr.SplitAndTrim(gfile.GetContents(gtest.DataPath("with_multiple_depends.sql")), ";") { if _, err := db.Exec(ctx, v); err != nil { gtest.Error(err) } @@ -1996,7 +1995,7 @@ func Test_With_Feature_Issue1401(t *testing.T) { table1 = "parcels" table2 = "parcel_items" ) - array := gstr.SplitAndTrim(gtest.TestDataContent(`issue1401.sql`), ";") + array := gstr.SplitAndTrim(gtest.DataContent(`issue1401.sql`), ";") for _, v := range array { if _, err := db.Exec(ctx, v); err != nil { gtest.Error(err) @@ -2038,7 +2037,7 @@ func Test_With_Feature_Issue1412(t *testing.T) { table1 = "parcels" table2 = "items" ) - array := gstr.SplitAndTrim(gtest.TestDataContent(`issue1412.sql`), ";") + array := gstr.SplitAndTrim(gtest.DataContent(`issue1412.sql`), ";") for _, v := range array { if _, err := db.Exec(ctx, v); err != nil { gtest.Error(err) diff --git a/database/gdb/gdb_z_mysql_model_test.go b/database/gdb/gdb_z_mysql_model_test.go index 4b941e223..01bb5499c 100644 --- a/database/gdb/gdb_z_mysql_model_test.go +++ b/database/gdb/gdb_z_mysql_model_test.go @@ -18,7 +18,6 @@ import ( "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" @@ -2195,7 +2194,7 @@ func Test_Model_FieldsEx_WithReservedWords(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( table = "fieldsex_test_table" - sqlTpcPath = gdebug.TestDataPath("reservedwords_table_tpl.sql") + sqlTpcPath = gtest.DataPath("reservedwords_table_tpl.sql") sqlContent = gfile.GetContents(sqlTpcPath) ) t.AssertNE(sqlContent, "") diff --git a/debug/gdebug/gdebug_testdata.go b/debug/gdebug/gdebug_testdata.go deleted file mode 100644 index c87f24b13..000000000 --- a/debug/gdebug/gdebug_testdata.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gdebug - -import ( - "io/ioutil" - "path/filepath" -) - -// TestDataPath retrieves and returns the testdata path of current package, -// which is used for unit testing cases only. -// The optional parameter `names` specifies the sub-folders/sub-files, -// which will be joined with current system separator and returned with the path. -func TestDataPath(names ...string) string { - path := CallerDirectory() + string(filepath.Separator) + "testdata" - for _, name := range names { - path += string(filepath.Separator) + name - } - return path -} - -// TestDataContent retrieves and returns the file content for specified testdata path of current package -func TestDataContent(names ...string) string { - path := TestDataPath(names...) - if path != "" { - data, err := ioutil.ReadFile(path) - if err == nil { - return string(data) - } - } - return "" -} diff --git a/encoding/gbase64/gbase64_z_unit_test.go b/encoding/gbase64/gbase64_z_unit_test.go index 4e02ceada..65cbc6561 100644 --- a/encoding/gbase64/gbase64_z_unit_test.go +++ b/encoding/gbase64/gbase64_z_unit_test.go @@ -9,7 +9,6 @@ package gbase64_test import ( "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/encoding/gbase64" "github.com/gogf/gf/v2/test/gtest" ) @@ -66,7 +65,7 @@ func Test_Basic(t *testing.T) { } func Test_File(t *testing.T) { - path := gdebug.TestDataPath("test") + path := gtest.DataPath("test") expect := "dGVzdA==" gtest.C(t, func(t *gtest.T) { b, err := gbase64.EncodeFile(path) diff --git a/encoding/gcompress/gcompress_z_unit_gzip_test.go b/encoding/gcompress/gcompress_z_unit_gzip_test.go index 4bc86a615..3239e2976 100644 --- a/encoding/gcompress/gcompress_z_unit_gzip_test.go +++ b/encoding/gcompress/gcompress_z_unit_gzip_test.go @@ -9,7 +9,6 @@ package gcompress_test import ( "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/encoding/gcompress" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gtime" @@ -43,7 +42,7 @@ func Test_Gzip_UnGzip(t *testing.T) { } func Test_Gzip_UnGzip_File(t *testing.T) { - srcPath := gdebug.TestDataPath("gzip", "file.txt") + srcPath := gtest.DataPath("gzip", "file.txt") dstPath1 := gfile.Temp(gtime.TimestampNanoStr(), "gzip.zip") dstPath2 := gfile.Temp(gtime.TimestampNanoStr(), "file.txt") diff --git a/encoding/gcompress/gcompress_z_unit_zip_test.go b/encoding/gcompress/gcompress_z_unit_zip_test.go index b44069d4b..3d8f51cc4 100644 --- a/encoding/gcompress/gcompress_z_unit_zip_test.go +++ b/encoding/gcompress/gcompress_z_unit_zip_test.go @@ -10,7 +10,6 @@ import ( "bytes" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/encoding/gcompress" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gtime" @@ -20,8 +19,8 @@ import ( func Test_ZipPath(t *testing.T) { // file gtest.C(t, func(t *gtest.T) { - srcPath := gdebug.TestDataPath("zip", "path1", "1.txt") - dstPath := gdebug.TestDataPath("zip", "zip.zip") + srcPath := gtest.DataPath("zip", "path1", "1.txt") + dstPath := gtest.DataPath("zip", "zip.zip") t.Assert(gfile.Exists(dstPath), false) t.Assert(gcompress.ZipPath(srcPath, dstPath), nil) @@ -42,8 +41,8 @@ func Test_ZipPath(t *testing.T) { // multiple files gtest.C(t, func(t *gtest.T) { var ( - srcPath1 = gdebug.TestDataPath("zip", "path1", "1.txt") - srcPath2 = gdebug.TestDataPath("zip", "path2", "2.txt") + srcPath1 = gtest.DataPath("zip", "path1", "1.txt") + srcPath2 = gtest.DataPath("zip", "path2", "2.txt") dstPath = gfile.Temp(gtime.TimestampNanoStr(), "zip.zip") ) if p := gfile.Dir(dstPath); !gfile.Exists(p) { @@ -75,8 +74,8 @@ func Test_ZipPath(t *testing.T) { // one dir and one file. gtest.C(t, func(t *gtest.T) { var ( - srcPath1 = gdebug.TestDataPath("zip", "path1") - srcPath2 = gdebug.TestDataPath("zip", "path2", "2.txt") + srcPath1 = gtest.DataPath("zip", "path1") + srcPath2 = gtest.DataPath("zip", "path2", "2.txt") dstPath = gfile.Temp(gtime.TimestampNanoStr(), "zip.zip") ) if p := gfile.Dir(dstPath); !gfile.Exists(p) { @@ -107,8 +106,8 @@ func Test_ZipPath(t *testing.T) { }) // directory. gtest.C(t, func(t *gtest.T) { - srcPath := gdebug.TestDataPath("zip") - dstPath := gdebug.TestDataPath("zip", "zip.zip") + srcPath := gtest.DataPath("zip") + dstPath := gtest.DataPath("zip", "zip.zip") pwd := gfile.Pwd() err := gfile.Chdir(srcPath) @@ -141,10 +140,10 @@ func Test_ZipPath(t *testing.T) { // multiple directory paths joined using char ','. gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("zip") - srcPath1 = gdebug.TestDataPath("zip", "path1") - srcPath2 = gdebug.TestDataPath("zip", "path2") - dstPath = gdebug.TestDataPath("zip", "zip.zip") + srcPath = gtest.DataPath("zip") + srcPath1 = gtest.DataPath("zip", "path1") + srcPath2 = gtest.DataPath("zip", "path2") + dstPath = gtest.DataPath("zip", "zip.zip") ) pwd := gfile.Pwd() err := gfile.Chdir(srcPath) @@ -181,9 +180,9 @@ func Test_ZipPath(t *testing.T) { func Test_ZipPathWriter(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("zip") - srcPath1 = gdebug.TestDataPath("zip", "path1") - srcPath2 = gdebug.TestDataPath("zip", "path2") + srcPath = gtest.DataPath("zip") + srcPath1 = gtest.DataPath("zip", "path1") + srcPath2 = gtest.DataPath("zip", "path2") ) pwd := gfile.Pwd() err := gfile.Chdir(srcPath) diff --git a/encoding/gjson/gjson_api.go b/encoding/gjson/gjson_api.go index 2985d7e28..e3773f121 100644 --- a/encoding/gjson/gjson_api.go +++ b/encoding/gjson/gjson_api.go @@ -41,7 +41,7 @@ func (j *Json) IsNil() bool { } // Get retrieves and returns value by specified `pattern`. -// It returns all values of current Json object if `pattern` is given empty or string ".". +// It returns all values of current Json object if `pattern` is given ".". // It returns nil if no value found by `pattern`. // // We can also access slice item by its index number in `pattern` like: @@ -71,7 +71,7 @@ func (j *Json) Get(pattern string, def ...interface{}) *gvar.Var { } // GetJson gets the value by specified `pattern`, -// and converts it to a un-concurrent-safe Json object. +// and converts it to an un-concurrent-safe Json object. func (j *Json) GetJson(pattern string, def ...interface{}) *Json { return New(j.Get(pattern, def...).Val()) } diff --git a/encoding/gjson/gjson_z_example_load_test.go b/encoding/gjson/gjson_z_example_load_test.go index efe92e712..41fe9189d 100644 --- a/encoding/gjson/gjson_z_example_load_test.go +++ b/encoding/gjson/gjson_z_example_load_test.go @@ -9,17 +9,17 @@ package gjson_test import ( "fmt" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/test/gtest" ) func ExampleLoad() { - jsonFilePath := gdebug.TestDataPath("json", "data1.json") + jsonFilePath := gtest.DataPath("json", "data1.json") j, _ := gjson.Load(jsonFilePath) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) - notExistFilePath := gdebug.TestDataPath("json", "data2.json") + notExistFilePath := gtest.DataPath("json", "data2.json") j2, _ := gjson.Load(notExistFilePath) fmt.Println(j2.Get("name")) @@ -195,7 +195,7 @@ func ExampleIsValidDataType() { } func ExampleLoad_Xml() { - jsonFilePath := gdebug.TestDataPath("xml", "data1.xml") + jsonFilePath := gtest.DataPath("xml", "data1.xml") j, _ := gjson.Load(jsonFilePath) fmt.Println(j.Get("doc.name")) fmt.Println(j.Get("doc.score")) diff --git a/frame/gins/gins_server.go b/frame/gins/gins_server.go index 2455d7925..08e2277c5 100644 --- a/frame/gins/gins_server.go +++ b/frame/gins/gins_server.go @@ -58,10 +58,10 @@ func Server(name ...interface{}) *ghttp.Server { } } } - // Server configuration. + // Automatically retrieve configuration by instance name. serverConfigMap = Config().MustGet( ctx, - fmt.Sprintf(`%s.%s`, configNodeName, server.GetName()), + fmt.Sprintf(`%s.%s`, configNodeName, instanceName), ).Map() if len(serverConfigMap) == 0 { serverConfigMap = Config().MustGet(ctx, configNodeName).Map() @@ -81,7 +81,7 @@ func Server(name ...interface{}) *ghttp.Server { // Server logger configuration checks. serverLoggerConfigMap = Config().MustGet( ctx, - fmt.Sprintf(`%s.%s.%s`, configNodeName, server.GetName(), configNodeNameLogger), + fmt.Sprintf(`%s.%s.%s`, configNodeName, instanceName, configNodeNameLogger), ).Map() if len(serverLoggerConfigMap) > 0 { if err = server.Logger().SetConfigWithMap(serverLoggerConfigMap); err != nil { diff --git a/frame/gins/gins_z_unit_config_test.go b/frame/gins/gins_z_unit_config_test.go index ddfc98915..d635ba0c5 100644 --- a/frame/gins/gins_z_unit_config_test.go +++ b/frame/gins/gins_z_unit_config_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/gins" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gfile" @@ -23,7 +22,7 @@ import ( var ( ctx = context.Background() configContent = gfile.GetContents( - gdebug.TestDataPath("config", "config.toml"), + gtest.DataPath("config", "config.toml"), ) ) diff --git a/frame/gins/gins_z_unit_database_test.go b/frame/gins/gins_z_unit_database_test.go index 5ec575660..3fc466de6 100644 --- a/frame/gins/gins_z_unit_database_test.go +++ b/frame/gins/gins_z_unit_database_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/gins" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gfile" @@ -20,7 +19,7 @@ import ( func Test_Database(t *testing.T) { databaseContent := gfile.GetContents( - gdebug.TestDataPath("database", "config.toml"), + gtest.DataPath("database", "config.toml"), ) gtest.C(t, func(t *gtest.T) { var err error diff --git a/frame/gins/gins_z_unit_redis_test.go b/frame/gins/gins_z_unit_redis_test.go index 237db241c..9ee97d8c1 100644 --- a/frame/gins/gins_z_unit_redis_test.go +++ b/frame/gins/gins_z_unit_redis_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/gins" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gfile" @@ -20,7 +19,7 @@ import ( func Test_Redis(t *testing.T) { redisContent := gfile.GetContents( - gdebug.TestDataPath("redis", "config.toml"), + gtest.DataPath("redis", "config.toml"), ) gtest.C(t, func(t *gtest.T) { diff --git a/frame/gins/gins_z_unit_server_test.go b/frame/gins/gins_z_unit_server_test.go new file mode 100644 index 000000000..e212ecce6 --- /dev/null +++ b/frame/gins/gins_z_unit_server_test.go @@ -0,0 +1,35 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gins + +//func Test_Server(t *testing.T) { +// gtest.C(t, func(t *gtest.T) { +// var ( +// path = gcfg.DefaultConfigFileName +// serverConfigContent = gtest.DataContent("server", "config.yaml") +// err = gfile.PutContents(path, serverConfigContent) +// ) +// t.AssertNil(err) +// defer gfile.Remove(path) +// +// time.Sleep(time.Second) +// +// localInstances.Clear() +// defer localInstances.Clear() +// +// s := Server("tempByInstanceName") +// s.BindHandler("/", func(r *ghttp.Request) { +// r.Response.Write("hello") +// }) +// s.SetDumpRouterMap(false) +// t.AssertNil(s.Start()) +// defer t.AssertNil(s.Shutdown()) +// +// content := HttpClient().GetContent(gctx.New(), `http://127.0.0.1:8003/`) +// t.Assert(content, `hello`) +// }) +//} diff --git a/frame/gins/gins_z_unit_view_test.go b/frame/gins/gins_z_unit_view_test.go index bc66d5376..aa8cae13f 100644 --- a/frame/gins/gins_z_unit_view_test.go +++ b/frame/gins/gins_z_unit_view_test.go @@ -11,7 +11,6 @@ import ( "fmt" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gtime" @@ -53,7 +52,7 @@ func Test_View(t *testing.T) { func Test_View_Config(t *testing.T) { // view1 test1 gtest.C(t, func(t *gtest.T) { - dirPath := gdebug.TestDataPath("view1") + dirPath := gtest.DataPath("view1") Config().GetAdapter().(*gcfg.AdapterFile).SetContent(gfile.GetContents(gfile.Join(dirPath, "config.toml"))) defer Config().GetAdapter().(*gcfg.AdapterFile).ClearContent() defer localInstances.Clear() @@ -75,7 +74,7 @@ func Test_View_Config(t *testing.T) { }) // view1 test2 gtest.C(t, func(t *gtest.T) { - dirPath := gdebug.TestDataPath("view1") + dirPath := gtest.DataPath("view1") Config().GetAdapter().(*gcfg.AdapterFile).SetContent(gfile.GetContents(gfile.Join(dirPath, "config.toml"))) defer Config().GetAdapter().(*gcfg.AdapterFile).ClearContent() defer localInstances.Clear() @@ -97,7 +96,7 @@ func Test_View_Config(t *testing.T) { }) // view2 gtest.C(t, func(t *gtest.T) { - dirPath := gdebug.TestDataPath("view2") + dirPath := gtest.DataPath("view2") Config().GetAdapter().(*gcfg.AdapterFile).SetContent(gfile.GetContents(gfile.Join(dirPath, "config.toml"))) defer Config().GetAdapter().(*gcfg.AdapterFile).ClearContent() defer localInstances.Clear() @@ -119,7 +118,7 @@ func Test_View_Config(t *testing.T) { }) // view2 gtest.C(t, func(t *gtest.T) { - dirPath := gdebug.TestDataPath("view2") + dirPath := gtest.DataPath("view2") Config().GetAdapter().(*gcfg.AdapterFile).SetContent(gfile.GetContents(gfile.Join(dirPath, "config.toml"))) defer Config().GetAdapter().(*gcfg.AdapterFile).ClearContent() defer localInstances.Clear() diff --git a/frame/gins/testdata/server/config.yaml b/frame/gins/testdata/server/config.yaml new file mode 100644 index 000000000..3be493af7 --- /dev/null +++ b/frame/gins/testdata/server/config.yaml @@ -0,0 +1,4 @@ +server: + address: ":8000" + tempByInstanceName: + address: ":8003" \ No newline at end of file diff --git a/i18n/gi18n/gi18n_z_unit_test.go b/i18n/gi18n/gi18n_z_unit_test.go index d1e56db2a..aec748aad 100644 --- a/i18n/gi18n/gi18n_z_unit_test.go +++ b/i18n/gi18n/gi18n_z_unit_test.go @@ -24,7 +24,7 @@ import ( func Test_Basic(t *testing.T) { gtest.C(t, func(t *gtest.T) { i18n := gi18n.New(gi18n.Options{ - Path: gdebug.TestDataPath("i18n"), + Path: gtest.DataPath("i18n"), }) i18n.SetLanguage("none") t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") @@ -41,7 +41,7 @@ func Test_Basic(t *testing.T) { gtest.C(t, func(t *gtest.T) { i18n := gi18n.New(gi18n.Options{ - Path: gdebug.TestDataPath("i18n-file"), + Path: gtest.DataPath("i18n-file"), }) i18n.SetLanguage("none") t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") @@ -72,7 +72,7 @@ func Test_TranslateFormat(t *testing.T) { // Tf gtest.C(t, func(t *gtest.T) { i18n := gi18n.New(gi18n.Options{ - Path: gdebug.TestDataPath("i18n"), + Path: gtest.DataPath("i18n"), }) i18n.SetLanguage("none") t.Assert(i18n.Tf(context.Background(), "{#hello}{#world} %d", 2020), "{#hello}{#world} 2020") @@ -84,7 +84,7 @@ func Test_TranslateFormat(t *testing.T) { func Test_DefaultManager(t *testing.T) { gtest.C(t, func(t *gtest.T) { - err := gi18n.SetPath(gdebug.TestDataPath("i18n")) + err := gi18n.SetPath(gtest.DataPath("i18n")) t.AssertNil(err) gi18n.SetLanguage("none") diff --git a/net/gclient/gclient_z_unit_test.go b/net/gclient/gclient_z_unit_test.go index e9328bca7..a1d8dc5c0 100644 --- a/net/gclient/gclient_z_unit_test.go +++ b/net/gclient/gclient_z_unit_test.go @@ -15,7 +15,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gclient" @@ -337,7 +336,7 @@ func Test_Client_File_And_Param(t *testing.T) { time.Sleep(100 * time.Millisecond) gtest.C(t, func(t *gtest.T) { - path := gdebug.TestDataPath("upload", "file1.txt") + path := gtest.DataPath("upload", "file1.txt") data := g.Map{ "file": "@file:" + path, "json": `{"uuid": "luijquiopm", "isRelative": false, "fileName": "test111.xls"}`, diff --git a/net/ghttp/ghttp_server_config.go b/net/ghttp/ghttp_server_config.go index fff0d387e..1aa63a744 100644 --- a/net/ghttp/ghttp_server_config.go +++ b/net/ghttp/ghttp_server_config.go @@ -329,14 +329,18 @@ func (s *Server) SetConfigWithMap(m map[string]interface{}) error { // SetConfig sets the configuration for the server. func (s *Server) SetConfig(c ServerConfig) error { s.config = c - // Address, check and use a random free port. + // Automatically add ':' prefix for address if it is missed. + if s.config.Address != "" && !gstr.HasPrefix(s.config.Address, ":") { + s.config.Address = ":" + s.config.Address + } + // It checks and uses a random free port. array := gstr.Split(s.config.Address, ":") if s.config.Address == "" || len(array) < 2 || array[1] == "0" { s.config.Address = gstr.Join([]string{ array[0], gconv.String(gtcp.MustGetFreePort()), }, ":") } - // Static. + // Static files root. if c.ServerRoot != "" { s.SetServerRoot(c.ServerRoot) } diff --git a/net/ghttp/ghttp_z_unit_feature_https_test.go b/net/ghttp/ghttp_z_unit_feature_https_test.go index f99db9b5d..f7a62426c 100644 --- a/net/ghttp/ghttp_z_unit_feature_https_test.go +++ b/net/ghttp/ghttp_z_unit_feature_https_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" _ "github.com/gogf/gf/v2/net/ghttp/testdata/https/packed" @@ -31,8 +30,8 @@ func Test_HTTPS_Basic(t *testing.T) { }) }) s.EnableHTTPS( - gdebug.TestDataPath("https", "files", "server.crt"), - gdebug.TestDataPath("https", "files", "server.key"), + gtest.DataPath("https", "files", "server.crt"), + gtest.DataPath("https", "files", "server.key"), ) s.SetDumpRouterMap(false) s.Start() @@ -101,8 +100,8 @@ func Test_HTTPS_HTTP_Basic(t *testing.T) { }) }) s.EnableHTTPS( - gdebug.TestDataPath("https", "files", "server.crt"), - gdebug.TestDataPath("https", "files", "server.key"), + gtest.DataPath("https", "files", "server.crt"), + gtest.DataPath("https", "files", "server.key"), ) s.SetPort(portHttp) s.SetHTTPSPort(portHttps) diff --git a/net/ghttp/ghttp_z_unit_feature_middleware_basic_test.go b/net/ghttp/ghttp_z_unit_feature_middleware_basic_test.go index bb3841daa..af81b245d 100644 --- a/net/ghttp/ghttp_z_unit_feature_middleware_basic_test.go +++ b/net/ghttp/ghttp_z_unit_feature_middleware_basic_test.go @@ -13,7 +13,6 @@ import ( "time" "github.com/gogf/gf/v2/container/garray" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/test/gtest" @@ -168,7 +167,7 @@ func Test_Middleware_With_Static(t *testing.T) { }) }) s.SetDumpRouterMap(false) - s.SetServerRoot(gdebug.TestDataPath("static1")) + s.SetServerRoot(gtest.DataPath("static1")) s.Start() defer s.Shutdown() time.Sleep(100 * time.Millisecond) @@ -236,7 +235,7 @@ func Test_Middleware_Hook_With_Static(t *testing.T) { }) }) s.SetDumpRouterMap(false) - s.SetServerRoot(gdebug.TestDataPath("static1")) + s.SetServerRoot(gtest.DataPath("static1")) s.Start() defer s.Shutdown() time.Sleep(100 * time.Millisecond) diff --git a/net/ghttp/ghttp_z_unit_feature_request_file_test.go b/net/ghttp/ghttp_z_unit_feature_request_file_test.go index 509a01897..53f020045 100644 --- a/net/ghttp/ghttp_z_unit_feature_request_file_test.go +++ b/net/ghttp/ghttp_z_unit_feature_request_file_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gfile" @@ -46,7 +45,7 @@ func Test_Params_File_Single(t *testing.T) { client := g.Client() client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())) - srcPath := gdebug.TestDataPath("upload", "file1.txt") + srcPath := gtest.DataPath("upload", "file1.txt") dstPath := gfile.Join(dstDirPath, "file1.txt") content := client.PostContent(ctx, "/upload/single", g.Map{ "file": "@file:" + srcPath, @@ -62,7 +61,7 @@ func Test_Params_File_Single(t *testing.T) { client := g.Client() client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())) - srcPath := gdebug.TestDataPath("upload", "file2.txt") + srcPath := gtest.DataPath("upload", "file2.txt") content := client.PostContent(ctx, "/upload/single", g.Map{ "file": "@file:" + srcPath, "randomlyRename": true, @@ -97,7 +96,7 @@ func Test_Params_File_CustomName(t *testing.T) { client := g.Client() client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())) - srcPath := gdebug.TestDataPath("upload", "file1.txt") + srcPath := gtest.DataPath("upload", "file1.txt") dstPath := gfile.Join(dstDirPath, "my.txt") content := client.PostContent(ctx, "/upload/single", g.Map{ "file": "@file:" + srcPath, @@ -132,8 +131,8 @@ func Test_Params_File_Batch(t *testing.T) { client := g.Client() client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())) - srcPath1 := gdebug.TestDataPath("upload", "file1.txt") - srcPath2 := gdebug.TestDataPath("upload", "file2.txt") + srcPath1 := gtest.DataPath("upload", "file1.txt") + srcPath2 := gtest.DataPath("upload", "file2.txt") dstPath1 := gfile.Join(dstDirPath, "file1.txt") dstPath2 := gfile.Join(dstDirPath, "file2.txt") content := client.PostContent(ctx, "/upload/batch", g.Map{ @@ -152,8 +151,8 @@ func Test_Params_File_Batch(t *testing.T) { client := g.Client() client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())) - srcPath1 := gdebug.TestDataPath("upload", "file1.txt") - srcPath2 := gdebug.TestDataPath("upload", "file2.txt") + srcPath1 := gtest.DataPath("upload", "file1.txt") + srcPath2 := gtest.DataPath("upload", "file2.txt") content := client.PostContent(ctx, "/upload/batch", g.Map{ "file[0]": "@file:" + srcPath1, "file[1]": "@file:" + srcPath2, @@ -205,7 +204,7 @@ func Test_Params_Strict_Route_File_Single(t *testing.T) { client := g.Client() client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())) - srcPath := gdebug.TestDataPath("upload", "file1.txt") + srcPath := gtest.DataPath("upload", "file1.txt") dstPath := gfile.Join(dstDirPath, "file1.txt") content := client.PostContent(ctx, "/upload/single", g.Map{ "file": "@file:" + srcPath, diff --git a/net/ghttp/ghttp_z_unit_feature_static_test.go b/net/ghttp/ghttp_z_unit_feature_static_test.go index da5e5df90..840a7e4d3 100644 --- a/net/ghttp/ghttp_z_unit_feature_static_test.go +++ b/net/ghttp/ghttp_z_unit_feature_static_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/test/gtest" @@ -60,7 +59,7 @@ func Test_Static_ServerRoot(t *testing.T) { func Test_Static_ServerRoot_Security(t *testing.T) { gtest.C(t, func(t *gtest.T) { s := g.Server(guid.S()) - s.SetServerRoot(gdebug.TestDataPath("static1")) + s.SetServerRoot(gtest.DataPath("static1")) s.Start() defer s.Shutdown() time.Sleep(100 * time.Millisecond) diff --git a/net/ghttp/ghttp_z_unit_feature_template_test.go b/net/ghttp/ghttp_z_unit_feature_template_test.go index 8c41f443f..ab64c506b 100644 --- a/net/ghttp/ghttp_z_unit_feature_template_test.go +++ b/net/ghttp/ghttp_z_unit_feature_template_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/encoding/ghtml" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" @@ -24,7 +23,7 @@ import ( func Test_Template_Basic(t *testing.T) { gtest.C(t, func(t *gtest.T) { - v := gview.New(gdebug.TestDataPath("template", "basic")) + v := gview.New(gtest.DataPath("template", "basic")) s := g.Server(guid.S()) s.SetView(v) s.BindHandler("/", func(r *ghttp.Request) { @@ -47,7 +46,7 @@ func Test_Template_Basic(t *testing.T) { func Test_Template_Encode(t *testing.T) { gtest.C(t, func(t *gtest.T) { - v := gview.New(gdebug.TestDataPath("template", "basic")) + v := gview.New(gtest.DataPath("template", "basic")) v.SetAutoEncode(true) s := g.Server(guid.S()) s.SetView(v) @@ -71,7 +70,7 @@ func Test_Template_Encode(t *testing.T) { func Test_Template_Layout1(t *testing.T) { gtest.C(t, func(t *gtest.T) { - v := gview.New(gdebug.TestDataPath("template", "layout1")) + v := gview.New(gtest.DataPath("template", "layout1")) s := g.Server(guid.S()) s.SetView(v) s.BindHandler("/layout", func(r *ghttp.Request) { @@ -99,7 +98,7 @@ func Test_Template_Layout1(t *testing.T) { func Test_Template_Layout2(t *testing.T) { gtest.C(t, func(t *gtest.T) { - v := gview.New(gdebug.TestDataPath("template", "layout2")) + v := gview.New(gtest.DataPath("template", "layout2")) s := g.Server(guid.S()) s.SetView(v) s.BindHandler("/main1", func(r *ghttp.Request) { diff --git a/net/ghttp/ghttp_z_unit_issue_test.go b/net/ghttp/ghttp_z_unit_issue_test.go index f8e3b437e..0e5a1ca5f 100644 --- a/net/ghttp/ghttp_z_unit_issue_test.go +++ b/net/ghttp/ghttp_z_unit_issue_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/test/gtest" @@ -45,7 +44,7 @@ func Test_Issue1611(t *testing.T) { s := g.Server(guid.S()) v := g.View(guid.S()) content := "This is header" - gtest.AssertNil(v.SetPath(gdebug.TestDataPath("issue1611"))) + gtest.AssertNil(v.SetPath(gtest.DataPath("issue1611"))) s.SetView(v) s.BindHandler("/", func(r *ghttp.Request) { gtest.AssertNil(r.Response.WriteTpl("index/layout.html", g.Map{ diff --git a/os/gcfg/gcfg_adapter_file.go b/os/gcfg/gcfg_adapter_file.go index c7d42ccbe..d98ac0fd8 100644 --- a/os/gcfg/gcfg_adapter_file.go +++ b/os/gcfg/gcfg_adapter_file.go @@ -146,6 +146,22 @@ func (c *AdapterFile) Get(ctx context.Context, pattern string) (value interface{ return nil, nil } +// Set sets value with specified `pattern`. +// It supports hierarchical data access by char separator, which is '.' in default. +// It is commonly used for updates certain configuration value in runtime. +// Note that, it is not recommended using `Set` configuration at runtime as the configuration would be +// automatically refreshed if underlying configuration file changed. +func (c *AdapterFile) Set(pattern string, value interface{}) error { + j, err := c.getJson() + if err != nil { + return err + } + if j != nil { + return j.Set(pattern, value) + } + return nil +} + // Data retrieves and returns all configuration data as map type. func (c *AdapterFile) Data(ctx context.Context) (data map[string]interface{}, err error) { j, err := c.getJson() diff --git a/os/gcfg/gcfg_adapter_file_path.go b/os/gcfg/gcfg_adapter_file_path.go index bd83a3fe5..1db5c0189 100644 --- a/os/gcfg/gcfg_adapter_file_path.go +++ b/os/gcfg/gcfg_adapter_file_path.go @@ -85,7 +85,17 @@ func (c *AdapterFile) SetPath(path string) (err error) { } // AddPath adds an absolute or relative path to the search paths. -func (c *AdapterFile) AddPath(path string) (err error) { +func (c *AdapterFile) AddPath(paths ...string) (err error) { + for _, path := range paths { + if err = c.doAddPath(path); err != nil { + return err + } + } + return nil +} + +// doAddPath adds an absolute or relative path to the search paths. +func (c *AdapterFile) doAddPath(path string) (err error) { var ( isDir = false realPath = "" @@ -139,6 +149,11 @@ func (c *AdapterFile) AddPath(path string) (err error) { return nil } +// GetPaths returns the searching path array of current configuration manager. +func (c *AdapterFile) GetPaths() []string { + return c.searchPaths.Slice() +} + // doGetFilePath returns the absolute configuration file path for the given filename by `file`. // If `file` is not passed, it returns the configuration file path of the default name. // It returns an empty `path` string and an error if the given `file` does not exist. diff --git a/os/gcfg/gcfg_z_unit_adapter_file_test.go b/os/gcfg/gcfg_z_unit_adapter_file_test.go index c0036a9f0..c1e896b0f 100644 --- a/os/gcfg/gcfg_z_unit_adapter_file_test.go +++ b/os/gcfg/gcfg_z_unit_adapter_file_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/gogf/gf/v2/os/gcfg" + "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/test/gtest" ) @@ -99,3 +100,22 @@ func TestAdapterFile_With_UTF8_BOM(t *testing.T) { t.Assert(c.MustGet(ctx, "test.testStr"), "test") }) } + +func TestAdapterFile_Set(t *testing.T) { + config := `log-path = "logs"` + gtest.C(t, func(t *gtest.T) { + var ( + path = gcfg.DefaultConfigFileName + err = gfile.PutContents(path, config) + ) + t.Assert(err, nil) + defer gfile.Remove(path) + + c, err := gcfg.New() + t.Assert(c.MustGet(ctx, "log-path").String(), "logs") + + err = c.GetAdapter().(*gcfg.AdapterFile).Set("log-path", "custom-logs") + t.Assert(err, nil) + t.Assert(c.MustGet(ctx, "log-path").String(), "custom-logs") + }) +} diff --git a/os/gcfg/gcfg_z_unit_instance_test.go b/os/gcfg/gcfg_z_unit_instance_test.go index 3cc5997b4..47bf3cbda 100644 --- a/os/gcfg/gcfg_z_unit_instance_test.go +++ b/os/gcfg/gcfg_z_unit_instance_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/gogf/gf/v2/container/gmap" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/os/genv" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/test/gtest" @@ -60,7 +59,7 @@ func Test_Instance_AutoLocateConfigFile(t *testing.T) { // Automatically locate the configuration file with supported file extensions. gtest.C(t, func(t *gtest.T) { pwd := gfile.Pwd() - t.AssertNil(gfile.Chdir(gdebug.TestDataPath())) + t.AssertNil(gfile.Chdir(gtest.DataPath())) defer gfile.Chdir(pwd) t.Assert(Instance("c1") != nil, true) t.Assert(Instance("c1").MustGet(ctx, "my-config"), "1") @@ -69,7 +68,7 @@ func Test_Instance_AutoLocateConfigFile(t *testing.T) { // Automatically locate the configuration file with supported file extensions. gtest.C(t, func(t *gtest.T) { pwd := gfile.Pwd() - t.AssertNil(gfile.Chdir(gdebug.TestDataPath("folder1"))) + t.AssertNil(gfile.Chdir(gtest.DataPath("folder1"))) defer gfile.Chdir(pwd) t.Assert(Instance("c2").MustGet(ctx, "my-config"), 2) }) @@ -77,7 +76,7 @@ func Test_Instance_AutoLocateConfigFile(t *testing.T) { gtest.C(t, func(t *gtest.T) { localInstances.Clear() pwd := gfile.Pwd() - t.AssertNil(gfile.Chdir(gdebug.TestDataPath("default"))) + t.AssertNil(gfile.Chdir(gtest.DataPath("default"))) defer gfile.Chdir(pwd) t.Assert(Instance().MustGet(ctx, "my-config"), 1) @@ -90,7 +89,7 @@ func Test_Instance_AutoLocateConfigFile(t *testing.T) { func Test_Instance_EnvPath(t *testing.T) { gtest.C(t, func(t *gtest.T) { - genv.Set("GF_GCFG_PATH", gdebug.TestDataPath("envpath")) + genv.Set("GF_GCFG_PATH", gtest.DataPath("envpath")) defer genv.Set("GF_GCFG_PATH", "") t.Assert(Instance("c3") != nil, true) t.Assert(Instance("c3").MustGet(ctx, "my-config"), "3") @@ -101,7 +100,7 @@ func Test_Instance_EnvPath(t *testing.T) { func Test_Instance_EnvFile(t *testing.T) { gtest.C(t, func(t *gtest.T) { - genv.Set("GF_GCFG_PATH", gdebug.TestDataPath("envfile")) + genv.Set("GF_GCFG_PATH", gtest.DataPath("envfile")) defer genv.Set("GF_GCFG_PATH", "") genv.Set("GF_GCFG_FILE", "c6.json") defer genv.Set("GF_GCFG_FILE", "") diff --git a/os/gfile/gfile_z_unit_scan_test.go b/os/gfile/gfile_z_unit_scan_test.go index 42e80faf5..151488313 100644 --- a/os/gfile/gfile_z_unit_scan_test.go +++ b/os/gfile/gfile_z_unit_scan_test.go @@ -10,13 +10,12 @@ import ( "testing" "github.com/gogf/gf/v2/container/garray" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/test/gtest" ) func Test_ScanDir(t *testing.T) { - teatPath := gdebug.TestDataPath() + teatPath := gtest.DataPath() gtest.C(t, func(t *gtest.T) { files, err := gfile.ScanDir(teatPath, "*", false) t.AssertNil(err) @@ -35,7 +34,7 @@ func Test_ScanDir(t *testing.T) { } func Test_ScanDirFunc(t *testing.T) { - teatPath := gdebug.TestDataPath() + teatPath := gtest.DataPath() gtest.C(t, func(t *gtest.T) { files, err := gfile.ScanDirFunc(teatPath, "*", true, func(path string) string { if gfile.Name(path) != "file1" { @@ -50,7 +49,7 @@ func Test_ScanDirFunc(t *testing.T) { } func Test_ScanDirFile(t *testing.T) { - teatPath := gdebug.TestDataPath() + teatPath := gtest.DataPath() gtest.C(t, func(t *gtest.T) { files, err := gfile.ScanDirFile(teatPath, "*", false) t.AssertNil(err) @@ -67,7 +66,7 @@ func Test_ScanDirFile(t *testing.T) { } func Test_ScanDirFileFunc(t *testing.T) { - teatPath := gdebug.TestDataPath() + teatPath := gtest.DataPath() gtest.C(t, func(t *gtest.T) { array := garray.New() files, err := gfile.ScanDirFileFunc(teatPath, "*", false, func(path string) string { diff --git a/os/glog/glog_logger.go b/os/glog/glog_logger.go index 8dd985759..97acbe557 100644 --- a/os/glog/glog_logger.go +++ b/os/glog/glog_logger.go @@ -117,7 +117,6 @@ func (l *Logger) print(ctx context.Context, level int, values ...interface{}) { input = &HandlerInput{ Logger: l, Buffer: bytes.NewBuffer(nil), - Ctx: ctx, Time: now, Color: defaultLevelColor[level], Level: level, @@ -221,13 +220,13 @@ func (l *Logger) print(ctx context.Context, level int, values ...interface{}) { if l.config.Flags&F_ASYNC > 0 { input.IsAsync = true err := asyncPool.Add(ctx, func(ctx context.Context) { - input.Next() + input.Next(ctx) }) if err != nil { intlog.Errorf(ctx, `%+v`, err) } } else { - input.Next() + input.Next(ctx) } } diff --git a/os/glog/glog_logger_handler.go b/os/glog/glog_logger_handler.go index bdeb4cdfc..5c7d9932c 100644 --- a/os/glog/glog_logger_handler.go +++ b/os/glog/glog_logger_handler.go @@ -17,30 +17,29 @@ type Handler func(ctx context.Context, in *HandlerInput) // HandlerInput is the input parameter struct for logging Handler. type HandlerInput struct { - Logger *Logger // Logger. - Ctx context.Context // Context. - Buffer *bytes.Buffer // Buffer for logging content outputs. - Time time.Time // Logging time, which is the time that logging triggers. - TimeFormat string // Formatted time string, like "2016-01-09 12:00:00". - Color int // Using color, like COLOR_RED, COLOR_BLUE, etc. - Level int // Using level, like LEVEL_INFO, LEVEL_ERRO, etc. - LevelFormat string // Formatted level string, like "DEBU", "ERRO", etc. - CallerFunc string // The source function name that calls logging. - CallerPath string // The source file path and its line number that calls logging. - CtxStr string // The retrieved context value string from context. - Prefix string // Custom prefix string for logging content. - Content string // Content is the main logging content that passed by you. - IsAsync bool // IsAsync marks it is in asynchronous logging. - handlerIndex int // Middleware handling index for internal usage. + Logger *Logger // Logger. + Buffer *bytes.Buffer // Buffer for logging content outputs. + Time time.Time // Logging time, which is the time that logging triggers. + TimeFormat string // Formatted time string, like "2016-01-09 12:00:00". + Color int // Using color, like COLOR_RED, COLOR_BLUE, etc. + Level int // Using level, like LEVEL_INFO, LEVEL_ERRO, etc. + LevelFormat string // Formatted level string, like "DEBU", "ERRO", etc. + CallerFunc string // The source function name that calls logging. + CallerPath string // The source file path and its line number that calls logging. + CtxStr string // The retrieved context value string from context. + Prefix string // Custom prefix string for logging content. + Content string // Content is the main logging content that passed by you. + IsAsync bool // IsAsync marks it is in asynchronous logging. + handlerIndex int // Middleware handling index for internal usage. } // Next calls the next logging handler in middleware way. -func (i *HandlerInput) Next() { +func (i *HandlerInput) Next(ctx context.Context) { if len(i.Logger.config.Handlers)-1 > i.handlerIndex { i.handlerIndex++ - i.Logger.config.Handlers[i.handlerIndex](i.Ctx, i) + i.Logger.config.Handlers[i.handlerIndex](ctx, i) } else { - defaultHandler(i.Ctx, i) + defaultHandler(ctx, i) } } diff --git a/os/glog/glog_z_unit_logger_handler_test.go b/os/glog/glog_z_unit_logger_handler_test.go index aeca8983e..b36999d1a 100644 --- a/os/glog/glog_z_unit_logger_handler_test.go +++ b/os/glog/glog_z_unit_logger_handler_test.go @@ -21,7 +21,7 @@ var arrayForHandlerTest1 = garray.NewStrArray() func customHandler1(ctx context.Context, input *glog.HandlerInput) { arrayForHandlerTest1.Append(input.String(false)) - input.Next() + input.Next(ctx) } func TestLogger_SetHandlers1(t *testing.T) { diff --git a/os/gres/gres_z_unit_test.go b/os/gres/gres_z_unit_test.go index 2a5b424c7..b1dc65d82 100644 --- a/os/gres/gres_z_unit_test.go +++ b/os/gres/gres_z_unit_test.go @@ -12,7 +12,6 @@ import ( _ "github.com/gogf/gf/v2/os/gres/testdata/data" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gres" @@ -23,7 +22,7 @@ import ( func Test_PackToGoFile(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("files") + srcPath = gtest.DataPath("files") goFilePath = gfile.Temp(gtime.TimestampNanoStr(), "testdata.go") pkgName = "testdata" err = gres.PackToGoFile(srcPath, goFilePath, pkgName) @@ -36,7 +35,7 @@ func Test_PackToGoFile(t *testing.T) { func Test_Pack(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("files") + srcPath = gtest.DataPath("files") data, err = gres.Pack(srcPath) ) t.AssertNil(err) @@ -51,7 +50,7 @@ func Test_Pack(t *testing.T) { func Test_PackToFile(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("files") + srcPath = gtest.DataPath("files") dstPath = gfile.Temp(gtime.TimestampNanoStr()) err = gres.PackToFile(srcPath, dstPath) ) @@ -69,7 +68,7 @@ func Test_PackToFile(t *testing.T) { func Test_PackMulti(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("files") + srcPath = gtest.DataPath("files") goFilePath = gfile.Temp(gtime.TimestampNanoStr(), "data.go") pkgName = "data" array, err = gfile.ScanDir(srcPath, "*", false) @@ -84,7 +83,7 @@ func Test_PackMulti(t *testing.T) { func Test_PackWithPrefix1(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("files") + srcPath = gtest.DataPath("files") goFilePath = gfile.Temp(gtime.TimestampNanoStr(), "testdata.go") pkgName = "testdata" err = gres.PackToGoFile(srcPath, goFilePath, pkgName, "www/gf-site/test") @@ -97,7 +96,7 @@ func Test_PackWithPrefix1(t *testing.T) { func Test_PackWithPrefix2(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - srcPath = gdebug.TestDataPath("files") + srcPath = gtest.DataPath("files") goFilePath = gfile.Temp(gtime.TimestampNanoStr(), "testdata.go") pkgName = "testdata" err = gres.PackToGoFile(srcPath, goFilePath, pkgName, "/var/www/gf-site/test") diff --git a/os/gview/gview_z_unit_config_test.go b/os/gview/gview_z_unit_config_test.go index f8c125b5e..f138203c0 100644 --- a/os/gview/gview_z_unit_config_test.go +++ b/os/gview/gview_z_unit_config_test.go @@ -10,7 +10,6 @@ import ( "context" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gview" "github.com/gogf/gf/v2/test/gtest" @@ -19,7 +18,7 @@ import ( func Test_Config(t *testing.T) { gtest.C(t, func(t *gtest.T) { config := gview.Config{ - Paths: []string{gdebug.TestDataPath("config")}, + Paths: []string{gtest.DataPath("config")}, Data: g.Map{ "name": "gf", }, @@ -46,7 +45,7 @@ func Test_ConfigWithMap(t *testing.T) { gtest.C(t, func(t *gtest.T) { view := gview.New() err := view.SetConfigWithMap(g.Map{ - "Paths": []string{gdebug.TestDataPath("config")}, + "Paths": []string{gtest.DataPath("config")}, "DefaultFile": "test.html", "Delimiters": []string{"${", "}"}, "Data": g.Map{ diff --git a/os/gview/gview_z_unit_feature_encode_test.go b/os/gview/gview_z_unit_feature_encode_test.go index 55a3665ba..786f98a4b 100644 --- a/os/gview/gview_z_unit_feature_encode_test.go +++ b/os/gview/gview_z_unit_feature_encode_test.go @@ -10,7 +10,6 @@ import ( "context" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gview" @@ -20,7 +19,7 @@ import ( func Test_Encode_Parse(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() - v.SetPath(gdebug.TestDataPath("tpl")) + v.SetPath(gtest.DataPath("tpl")) v.SetAutoEncode(true) result, err := v.Parse(context.TODO(), "encode.tpl", g.Map{ "title": "my title", @@ -33,7 +32,7 @@ func Test_Encode_Parse(t *testing.T) { func Test_Encode_ParseContent(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() - tplContent := gfile.GetContents(gdebug.TestDataPath("tpl", "encode.tpl")) + tplContent := gfile.GetContents(gtest.DataPath("tpl", "encode.tpl")) v.SetAutoEncode(true) result, err := v.ParseContent(context.TODO(), tplContent, g.Map{ "title": "my title", diff --git a/os/gview/gview_z_unit_i18n_test.go b/os/gview/gview_z_unit_i18n_test.go index 5b22b8250..dfd1e4026 100644 --- a/os/gview/gview_z_unit_i18n_test.go +++ b/os/gview/gview_z_unit_i18n_test.go @@ -23,7 +23,7 @@ func Test_I18n(t *testing.T) { expect2 := `john says "こんにちは世界!"` expect3 := `john says "{#hello}{#world}!"` - g.I18n().SetPath(gdebug.TestDataPath("i18n")) + g.I18n().SetPath(gtest.DataPath("i18n")) g.I18n().SetLanguage("zh-CN") result1, err := g.View().ParseContent(context.TODO(), content, g.Map{ diff --git a/os/gview/gview_z_unit_test.go b/os/gview/gview_z_unit_test.go index 10468557d..cfead2fc9 100644 --- a/os/gview/gview_z_unit_test.go +++ b/os/gview/gview_z_unit_test.go @@ -14,7 +14,6 @@ import ( "testing" "time" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/encoding/ghtml" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" @@ -534,7 +533,7 @@ func Test_BuildInFuncDivide(t *testing.T) { func Test_Issue1416(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() - err := v.SetPath(gdebug.TestDataPath("issue1416")) + err := v.SetPath(gtest.DataPath("issue1416")) t.AssertNil(err) r, err := v.ParseOption(context.TODO(), gview.Option{ File: "gview.tpl", diff --git a/test/gtest/gtest_util.go b/test/gtest/gtest_util.go index 6ecb4bd9a..8cc511770 100644 --- a/test/gtest/gtest_util.go +++ b/test/gtest/gtest_util.go @@ -345,11 +345,11 @@ func AssertNil(value interface{}) { AssertNE(value, nil) } -// TestDataPath retrieves and returns the testdata path of current package, +// DataPath retrieves and returns the testdata path of current package, // which is used for unit testing cases only. // The optional parameter `names` specifies the sub-folders/sub-files, // which will be joined with current system separator and returned with the path. -func TestDataPath(names ...string) string { +func DataPath(names ...string) string { _, path, _ := gdebug.CallerWithFilter([]string{pathFilterKey}) path = filepath.Dir(path) + string(filepath.Separator) + "testdata" for _, name := range names { @@ -358,9 +358,9 @@ func TestDataPath(names ...string) string { return path } -// TestDataContent retrieves and returns the file content for specified testdata path of current package -func TestDataContent(names ...string) string { - path := TestDataPath(names...) +// DataContent retrieves and returns the file content for specified testdata path of current package +func DataContent(names ...string) string { + path := DataPath(names...) if path != "" { data, err := ioutil.ReadFile(path) if err == nil { diff --git a/util/gconv/gconv_slice_any.go b/util/gconv/gconv_slice_any.go index 9041886d3..415f47634 100644 --- a/util/gconv/gconv_slice_any.go +++ b/util/gconv/gconv_slice_any.go @@ -9,6 +9,7 @@ package gconv import ( "reflect" + "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" ) @@ -62,9 +63,13 @@ func Interfaces(any interface{}) []interface{} { array[k] = v } case []uint8: - array = make([]interface{}, len(value)) - for k, v := range value { - array[k] = v + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]interface{}, len(value)) + for k, v := range value { + array[k] = v + } } case []uint16: array = make([]interface{}, len(value)) diff --git a/util/gconv/gconv_slice_float.go b/util/gconv/gconv_slice_float.go index 4816d7044..3d7b49949 100644 --- a/util/gconv/gconv_slice_float.go +++ b/util/gconv/gconv_slice_float.go @@ -9,6 +9,7 @@ package gconv import ( "reflect" + "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" ) @@ -81,9 +82,13 @@ func Float32s(any interface{}) []float32 { array = append(array, Float32(v)) } case []uint8: - array = make([]float32, len(value)) - for k, v := range value { - array[k] = Float32(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]float32, len(value)) + for k, v := range value { + array[k] = Float32(v) + } } case []uint16: array = make([]float32, len(value)) @@ -201,9 +206,13 @@ func Float64s(any interface{}) []float64 { array = append(array, Float64(v)) } case []uint8: - array = make([]float64, len(value)) - for k, v := range value { - array[k] = Float64(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]float64, len(value)) + for k, v := range value { + array[k] = Float64(v) + } } case []uint16: array = make([]float64, len(value)) diff --git a/util/gconv/gconv_slice_int.go b/util/gconv/gconv_slice_int.go index 1ca0ab4d6..f28e7fd18 100644 --- a/util/gconv/gconv_slice_int.go +++ b/util/gconv/gconv_slice_int.go @@ -9,6 +9,7 @@ package gconv import ( "reflect" + "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" ) @@ -69,9 +70,13 @@ func Ints(any interface{}) []int { array[k] = int(v) } case []uint8: - array = make([]int, len(value)) - for k, v := range value { - array[k] = int(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]int, len(value)) + for k, v := range value { + array[k] = int(v) + } } case []uint16: array = make([]int, len(value)) @@ -194,9 +199,13 @@ func Int32s(any interface{}) []int32 { array[k] = int32(v) } case []uint8: - array = make([]int32, len(value)) - for k, v := range value { - array[k] = int32(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]int32, len(value)) + for k, v := range value { + array[k] = int32(v) + } } case []uint16: array = make([]int32, len(value)) @@ -319,9 +328,13 @@ func Int64s(any interface{}) []int64 { array[k] = int64(v) } case []uint8: - array = make([]int64, len(value)) - for k, v := range value { - array[k] = int64(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]int64, len(value)) + for k, v := range value { + array[k] = int64(v) + } } case []uint16: array = make([]int64, len(value)) diff --git a/util/gconv/gconv_slice_str.go b/util/gconv/gconv_slice_str.go index 31e766f52..c085d271f 100644 --- a/util/gconv/gconv_slice_str.go +++ b/util/gconv/gconv_slice_str.go @@ -9,6 +9,7 @@ package gconv import ( "reflect" + "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" ) @@ -57,9 +58,13 @@ func Strings(any interface{}) []string { array[k] = String(v) } case []uint8: - array = make([]string, len(value)) - for k, v := range value { - array[k] = String(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]string, len(value)) + for k, v := range value { + array[k] = String(v) + } } case []uint16: array = make([]string, len(value)) diff --git a/util/gconv/gconv_slice_uint.go b/util/gconv/gconv_slice_uint.go index e77dfc2c0..a1ffa7617 100644 --- a/util/gconv/gconv_slice_uint.go +++ b/util/gconv/gconv_slice_uint.go @@ -10,6 +10,7 @@ import ( "reflect" "strings" + "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/internal/utils" ) @@ -76,9 +77,13 @@ func Uints(any interface{}) []uint { case []uint: array = value case []uint8: - array = make([]uint, len(value)) - for k, v := range value { - array[k] = uint(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]uint, len(value)) + for k, v := range value { + array[k] = uint(v) + } } case []uint16: array = make([]uint, len(value)) @@ -210,9 +215,13 @@ func Uint32s(any interface{}) []uint32 { array[k] = uint32(v) } case []uint8: - array = make([]uint32, len(value)) - for k, v := range value { - array[k] = uint32(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]uint32, len(value)) + for k, v := range value { + array[k] = uint32(v) + } } case []uint16: array = make([]uint32, len(value)) @@ -341,9 +350,13 @@ func Uint64s(any interface{}) []uint64 { array[k] = uint64(v) } case []uint8: - array = make([]uint64, len(value)) - for k, v := range value { - array[k] = uint64(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]uint64, len(value)) + for k, v := range value { + array[k] = uint64(v) + } } case []uint16: array = make([]uint64, len(value)) diff --git a/util/grand/grand.go b/util/grand/grand.go index 77478cee4..34554a2f5 100644 --- a/util/grand/grand.go +++ b/util/grand/grand.go @@ -60,16 +60,12 @@ func N(min, max int) int { return min } if min >= 0 { - // Because Intn dose not support negative number, - // so we should first shift the value to left, - // then call Intn to produce the random number, - // and finally shift the result back to right. - return Intn(max-(min-0)+1) + (min - 0) + return Intn(max-min+1) + min } if min < 0 { - // Because Intn dose not support negative number, + // As `Intn` dose not support negative number, // so we should first shift the value to right, - // then call Intn to produce the random number, + // then call `Intn` to produce the random number, // and finally shift the result back to left. return Intn(max+(0-min)+1) - (0 - min) } diff --git a/util/gvalid/gvalid_error.go b/util/gvalid/gvalid_error.go index ba252d8c6..c489f710e 100644 --- a/util/gvalid/gvalid_error.go +++ b/util/gvalid/gvalid_error.go @@ -9,6 +9,7 @@ package gvalid import ( "strings" + "github.com/gogf/gf/v2/container/gset" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gstr" @@ -32,7 +33,7 @@ type Error interface { // validationError is the validation error for validation result. type validationError struct { code gcode.Code // Error code. - rules []fieldRule // Rules by sequence, which is used for keeping error sequence. + rules []fieldRule // Rules by sequence, which is used for keeping error sequence only. errors map[string]map[string]error // Error map:map[field]map[rule]message firstKey string // The first error rule key(empty in default). firstItem map[string]error // The first error rule value(nil in default). @@ -52,6 +53,16 @@ func newValidationError(code gcode.Code, rules []fieldRule, fieldRuleErrorMap ma } fieldRuleErrorMap[field] = ruleErrorMap } + // Filter repeated sequence rules. + var ruleNameSet = gset.NewStrSet() + for i := 0; i < len(rules); { + if !ruleNameSet.AddIfNotExist(rules[i].Name) { + // Delete repeated rule. + rules = append(rules[:i], rules[i+1:]...) + continue + } + i++ + } return &validationError{ code: code, rules: rules, diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index a625d6f66..944774f3c 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -19,15 +19,15 @@ import ( // Validator is the validation manager for chaining operations. type Validator struct { - i18nManager *gi18n.Manager // I18n manager for error message translation. - data interface{} // Validation data, which can be a map, struct or a certain value to be validated. - assoc interface{} // Associated data, which is usually a map, for union validation. - rules interface{} // Custom validation data. - messages interface{} // Custom validation error messages, which can be string or type of CustomMsg. - ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator. - useDataInsteadOfObjectAttributes bool // Using `data` as its validation source instead of attribute values from `Object`. - bail bool // Stop validation after the first validation error. - caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison. + i18nManager *gi18n.Manager // I18n manager for error message translation. + data interface{} // Validation data, which can be a map, struct or a certain value to be validated. + assoc interface{} // Associated data, which is usually a map, for union validation. + rules interface{} // Custom validation data. + messages interface{} // Custom validation error messages, which can be string or type of CustomMsg. + ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator. + useAssocInsteadOfObjectAttributes bool // Using `assoc` as its validation source instead of attribute values from `Object`. + bail bool // Stop validation after the first validation error. + caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison. } // New creates and returns a new Validator. @@ -125,14 +125,14 @@ func (v *Validator) Data(data interface{}) *Validator { // Assoc is a chaining operation function, which sets associated validation data for current operation. // The optional parameter `assoc` is usually type of map, which specifies the parameter map used in union validation. -// Calling this function with `assoc` also sets `useDataInsteadOfObjectAttributes` true +// Calling this function with `assoc` also sets `useAssocInsteadOfObjectAttributes` true func (v *Validator) Assoc(assoc interface{}) *Validator { if assoc == nil { return v } newValidator := v.Clone() newValidator.assoc = assoc - newValidator.useDataInsteadOfObjectAttributes = true + newValidator.useAssocInsteadOfObjectAttributes = true return newValidator } diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index e4cee548d..38d570b34 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -109,13 +109,13 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error return nil } // Input parameter map handling. - if v.assoc == nil || !v.useDataInsteadOfObjectAttributes { + if v.assoc == nil || !v.useAssocInsteadOfObjectAttributes { inputParamMap = make(map[string]interface{}) } else { inputParamMap = gconv.Map(v.assoc) } // Checks and extends the parameters map with struct alias tag. - if !v.useDataInsteadOfObjectAttributes { + if !v.useAssocInsteadOfObjectAttributes { for nameOrTag, field := range fieldMap { inputParamMap[nameOrTag] = field.Value.Interface() if nameOrTag != field.Name() { @@ -147,7 +147,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // It here extends the params map using alias names. // Note that the variable `name` might be alias name or attribute name. if _, ok := inputParamMap[name]; !ok { - if !v.useDataInsteadOfObjectAttributes { + if !v.useAssocInsteadOfObjectAttributes { inputParamMap[name] = field.Value.Interface() } else { if name != fieldName { @@ -226,7 +226,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // Temporary variable for value. var value interface{} - // It checks the struct recursively if its attribute is an embedded struct. + // It checks the struct recursively if its attribute is a struct/struct slice. for _, field := range fieldMap { // No validation interface implements check. if _, ok := field.Value.Interface().(iNoValidation); ok { diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index ae6808791..bd69f9f77 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -15,10 +15,10 @@ import ( "time" "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/json" - "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/net/gipv4" "github.com/gogf/gf/v2/net/gipv6" "github.com/gogf/gf/v2/os/gtime" @@ -538,11 +538,11 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, in doCheckBui } type doCheckValueRecursivelyInput struct { - Value interface{} - Type reflect.Type - OriginKind reflect.Kind - ErrorMaps map[string]map[string]error - ResultSequenceRules *[]fieldRule + Value interface{} // Value to be validated. + Type reflect.Type // Struct/map/slice type which to be recursively validated. + OriginKind reflect.Kind // Struct/map/slice kind to be asserted in following switch case. + ErrorMaps map[string]map[string]error // The validated failed error map. + ResultSequenceRules *[]fieldRule // The validated failed rule in sequence. } func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValueRecursivelyInput) { @@ -563,13 +563,16 @@ func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValue } case reflect.Map: - var dataMap = gconv.Map(in.Value) + var ( + dataMap = gconv.Map(in.Value) + mapTypeElem = in.Type.Elem() + mapTypeKind = mapTypeElem.Kind() + ) for _, item := range dataMap { - originTypeAndKind := reflection.OriginTypeAndKind(item) v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ Value: item, - Type: originTypeAndKind.InputType, - OriginKind: originTypeAndKind.OriginKind, + Type: mapTypeElem, + OriginKind: mapTypeKind, ErrorMaps: in.ErrorMaps, ResultSequenceRules: in.ResultSequenceRules, }) @@ -580,16 +583,20 @@ func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValue } case reflect.Slice, reflect.Array: - array := gconv.Interfaces(in.Value) + var array []interface{} + if gjson.Valid(in.Value) { + array = gconv.Interfaces(gconv.Bytes(in.Value)) + } else { + array = gconv.Interfaces(in.Value) + } if len(array) == 0 { return } for _, item := range array { - originTypeAndKind := reflection.OriginTypeAndKind(item) v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ Value: item, - Type: originTypeAndKind.InputType, - OriginKind: originTypeAndKind.OriginKind, + Type: in.Type.Elem(), + OriginKind: in.Type.Elem().Kind(), ErrorMaps: in.ErrorMaps, ResultSequenceRules: in.ResultSequenceRules, }) diff --git a/util/gvalid/gvalid_z_unit_feature_i18n_test.go b/util/gvalid/gvalid_z_unit_feature_i18n_test.go index 2554f1837..d43d4c566 100644 --- a/util/gvalid/gvalid_z_unit_feature_i18n_test.go +++ b/util/gvalid/gvalid_z_unit_feature_i18n_test.go @@ -10,7 +10,6 @@ import ( "context" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/i18n/gi18n" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/gvalid" @@ -19,7 +18,7 @@ import ( func TestValidator_I18n(t *testing.T) { var ( err gvalid.Error - i18nManager = gi18n.New(gi18n.Options{Path: gdebug.TestDataPath("i18n")}) + i18nManager = gi18n.New(gi18n.Options{Path: gtest.DataPath("i18n")}) ctxCn = gi18n.WithLanguage(context.TODO(), "cn") validator = gvalid.New().I18n(i18nManager) ) diff --git a/util/gvalid/gvalid_z_unit_feature_recursive_test.go b/util/gvalid/gvalid_z_unit_feature_recursive_test.go index b441f7c70..269d2eab4 100755 --- a/util/gvalid/gvalid_z_unit_feature_recursive_test.go +++ b/util/gvalid/gvalid_z_unit_feature_recursive_test.go @@ -214,3 +214,47 @@ func Test_CheckMap_Recursive_SliceStruct(t *testing.T) { t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `4` must be the same as field Pass1"}) }) } + +func Test_CheckStruct_Recursively_SliceAttribute(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string `v:"required#Student Name is required"` + Age int `v:"required"` + } + type Teacher struct { + Name string `v:"required#Teacher Name is required"` + Students []Student `v:"required"` + } + var ( + teacher = Teacher{} + data = g.Map{ + "name": "john", + "students": `[{"age":2}, {"name":"jack", "age":4}]`, + } + ) + err := g.Validator().Assoc(data).Data(teacher).Run(ctx) + t.Assert(err, `Student Name is required`) + }) +} + +func Test_CheckStruct_Recursively_MapAttribute(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string `v:"required#Student Name is required"` + Age int `v:"required"` + } + type Teacher struct { + Name string `v:"required#Teacher Name is required"` + Students map[string]Student `v:"required"` + } + var ( + teacher = Teacher{} + data = g.Map{ + "name": "john", + "students": `{"john":{"age":18}}`, + } + ) + err := g.Validator().Assoc(data).Data(teacher).Run(ctx) + t.Assert(err, `Student Name is required`) + }) +}