- cabana: config_reorder.yaml (title, nameFrom, modelClass, parentColumn,
sortColumn, maxDepth, partition) with fail-loud boot checks and the
built-in reorder toolbar button; list schema carries reorder
- GET/POST {controller}/reorder: scoped set (ListExtendQuery plus the
partition) locked FOR UPDATE, 409 reorder_stale on any set difference,
422 for duplicates, foreign parents, cycles and maxDepth, both columns
written without model hooks
- pact.AdminSingleton: GET {controller}/singleton (first or create, form
scope checked), form schema singleton, list/create/bulk/delete 404,
no config_list.yaml needed
- swag, inventories, token classes, CSRF walk, conformance cases, backend
lang reorder.stale and load_failed, reorder and singleton smoke tests, docs
526 lines
18 KiB
Go
526 lines
18 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"path"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/towel"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// msgReorderStale is the phrase key of the 409 a reorder answers when the
|
|
// records changed since the tree was loaded (backend::lang).
|
|
const msgReorderStale = "backend::lang.reorder.stale"
|
|
|
|
// maxReorderPartitions caps the partition choices a reorder page offers.
|
|
const maxReorderPartitions = 500
|
|
|
|
// reorderDocument is config_reorder.yaml (D-09): WinterCMS's
|
|
// reorder_config.yaml keys title, nameFrom and modelClass, plus the parent
|
|
// and sort columns, the depth limit and an optional partition.
|
|
type reorderDocument struct {
|
|
Title string `yaml:"title"`
|
|
NameFrom string `yaml:"nameFrom"`
|
|
ModelClass string `yaml:"modelClass"`
|
|
ParentColumn string `yaml:"parentColumn"`
|
|
SortColumn string `yaml:"sortColumn"`
|
|
MaxDepth int `yaml:"maxDepth"`
|
|
Partition *reorderPartitionY `yaml:"partition"`
|
|
}
|
|
|
|
type reorderPartitionY struct {
|
|
Column string `yaml:"column"`
|
|
Relation string `yaml:"relation"`
|
|
NameFrom string `yaml:"nameFrom"`
|
|
Label string `yaml:"label"`
|
|
}
|
|
|
|
// compiledReorder is a controller's compiled config_reorder.yaml.
|
|
type compiledReorder struct {
|
|
title string
|
|
nameFrom string
|
|
parentColumn string
|
|
sortColumn string
|
|
maxDepth int
|
|
partition *compiledPartition
|
|
}
|
|
|
|
type compiledPartition struct {
|
|
column string
|
|
relation string
|
|
label string
|
|
labelCol string
|
|
fieldRel *CompiledFieldRelation
|
|
}
|
|
|
|
// ListReorder is the list schema's `reorder` object: the controller has a
|
|
// tree reorder page (config_reorder.yaml). Partition is set when the tree is
|
|
// reordered within one value of a partition column, such as one menu.
|
|
type ListReorder struct {
|
|
Title string `json:"title"`
|
|
Partition *ListReorderPartition `json:"partition,omitempty"`
|
|
}
|
|
|
|
// ListReorderPartition names the partition column and its label.
|
|
type ListReorderPartition struct {
|
|
Column string `json:"column"`
|
|
Label string `json:"label,omitempty"`
|
|
}
|
|
|
|
// compileReorder reads the controller's optional config_reorder.yaml. Every
|
|
// unknown key, a column that is not a column of the controller's record, a
|
|
// partition relation that is not a belongsTo field relation of the form over
|
|
// the partition column, and a negative maxDepth fail boot. It also enforces
|
|
// that toolbar.buttons lists reorder only on a controller that has the file.
|
|
func compileReorder(pluginID string, cc *CompiledController, fsys fs.FS) error {
|
|
ctl := cc.Controller
|
|
dir := strings.Trim(path.Clean(ctl.ConfigDir()), "/")
|
|
cfgPath := path.Join(dir, "config_reorder.yaml")
|
|
listHasReorder := cc.List != nil && hasToolbarButton(cc.List, "reorder")
|
|
if _, err := fs.Stat(fsys, cfgPath); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return bootErr(pluginID, ctl.ID(), cfgPath, err)
|
|
}
|
|
if listHasReorder {
|
|
return bootErr(pluginID, ctl.ID(), "config_list.yaml", fmt.Errorf("toolbar.buttons: reorder needs config_reorder.yaml"))
|
|
}
|
|
return nil
|
|
}
|
|
raw, err := readAsset(fsys, cfgPath)
|
|
if err != nil {
|
|
return bootErr(pluginID, ctl.ID(), cfgPath, err)
|
|
}
|
|
var doc reorderDocument
|
|
if err := decodeStrict(raw, &doc); err != nil {
|
|
return bootErr(pluginID, ctl.ID(), cfgPath, err)
|
|
}
|
|
fail := func(err error) error { return bootErr(pluginID, ctl.ID(), cfgPath, err) }
|
|
if doc.ModelClass != "" && doc.ModelClass != ctl.ModelName() {
|
|
return fail(fmt.Errorf("modelClass %q does not match %q", doc.ModelClass, ctl.ModelName()))
|
|
}
|
|
src, ok := ctl.(pact.AdminRecordSource)
|
|
if !ok || src == nil || src.NewRecord() == nil {
|
|
return fail(fmt.Errorf("a reorder page needs an AdminRecordSource"))
|
|
}
|
|
if cc.List == nil {
|
|
return fail(fmt.Errorf("a reorder page needs config_list.yaml"))
|
|
}
|
|
cols := modelColumns(src.NewRecord())
|
|
out := &compiledReorder{
|
|
title: doc.Title,
|
|
nameFrom: doc.NameFrom,
|
|
parentColumn: doc.ParentColumn,
|
|
sortColumn: doc.SortColumn,
|
|
maxDepth: doc.MaxDepth,
|
|
}
|
|
if out.parentColumn == "" {
|
|
out.parentColumn = "parent_id"
|
|
}
|
|
if out.sortColumn == "" {
|
|
out.sortColumn = "sort_order"
|
|
}
|
|
if doc.MaxDepth < 0 {
|
|
return fail(fmt.Errorf("maxDepth must be 0 (unlimited) or more"))
|
|
}
|
|
if !identifier(out.nameFrom) {
|
|
return fail(fmt.Errorf("nameFrom %q is not a column name", out.nameFrom))
|
|
}
|
|
for key, column := range map[string]string{"nameFrom": out.nameFrom, "parentColumn": out.parentColumn, "sortColumn": out.sortColumn} {
|
|
if _, ok := cols[column]; !ok || !identifier(column) {
|
|
return fail(fmt.Errorf("%s %s is not a column of the record", key, column))
|
|
}
|
|
}
|
|
if p := doc.Partition; p != nil {
|
|
if _, ok := cols[p.Column]; !ok || !identifier(p.Column) {
|
|
return fail(fmt.Errorf("partition.column %s is not a column of the record", p.Column))
|
|
}
|
|
fr, ok := cc.FieldRelations[p.Relation]
|
|
if !ok || fr.Contract.Kind != relationKindBelongsTo || fr.Contract.ForeignKey != p.Column {
|
|
return fail(fmt.Errorf("partition.relation %s must be a belongsTo relation field of the form over %s", p.Relation, p.Column))
|
|
}
|
|
labelCol := fr.LabelColumn
|
|
if p.NameFrom != "" {
|
|
if !identifier(p.NameFrom) {
|
|
return fail(fmt.Errorf("partition.nameFrom %q is not a column name", p.NameFrom))
|
|
}
|
|
if _, ok := modelColumns(fr.Contract.NewRelated())[p.NameFrom]; !ok {
|
|
return fail(fmt.Errorf("partition.nameFrom %s is not a column of the related record", p.NameFrom))
|
|
}
|
|
labelCol = p.NameFrom
|
|
}
|
|
out.partition = &compiledPartition{column: p.Column, relation: p.Relation, label: p.Label, labelCol: labelCol, fieldRel: fr}
|
|
}
|
|
cc.reorder = out
|
|
reorder := &ListReorder{Title: out.title}
|
|
if out.partition != nil {
|
|
reorder.Partition = &ListReorderPartition{Column: out.partition.column, Label: out.partition.label}
|
|
}
|
|
cc.List.Reorder = reorder
|
|
return nil
|
|
}
|
|
|
|
// AdminReorderPartitionOption is one partition a reorder page offers.
|
|
type AdminReorderPartitionOption struct {
|
|
Value uint `json:"value"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
// AdminReorderPartition is the partition of a reorder tree: its label, the
|
|
// partitions the administrator may choose and the one shown (null when there
|
|
// is none to choose).
|
|
type AdminReorderPartition struct {
|
|
Label string `json:"label"`
|
|
Options []AdminReorderPartitionOption `json:"options"`
|
|
Value *uint `json:"value"`
|
|
}
|
|
|
|
// AdminReorderNode is one record of a reorder tree.
|
|
type AdminReorderNode struct {
|
|
ID uint `json:"id"`
|
|
ParentID *uint `json:"parent_id"`
|
|
SortOrder int64 `json:"sort_order"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// AdminReorderTree is the data of the reorder routes: the page title, the
|
|
// partition (null without one) and every record of the scoped set, ordered by
|
|
// the sort column and then the id.
|
|
type AdminReorderTree struct {
|
|
Title string `json:"title"`
|
|
Partition *AdminReorderPartition `json:"partition"`
|
|
Nodes []AdminReorderNode `json:"nodes"`
|
|
}
|
|
|
|
// AdminReorderNodeInput is one node of a reorder save: the record id, its new
|
|
// parent (null at the top) and its new sort value.
|
|
type AdminReorderNodeInput struct {
|
|
ID uint64 `json:"id"`
|
|
ParentID *uint64 `json:"parent_id"`
|
|
SortOrder int64 `json:"sort_order"`
|
|
}
|
|
|
|
// AdminReorderRequest is the body of POST .../reorder: the partition the tree
|
|
// belongs to (required when the page has one) and every node of the scoped
|
|
// set, exactly once.
|
|
type AdminReorderRequest struct {
|
|
Partition *uint64 `json:"partition"`
|
|
Nodes []AdminReorderNodeInput `json:"nodes"`
|
|
}
|
|
|
|
// reorderStale is the 409 of a reorder save whose node set no longer equals
|
|
// the locked scoped set.
|
|
type reorderStale struct{}
|
|
|
|
func (reorderStale) Error() string { return "cabana: reorder set changed" }
|
|
|
|
// reorderGet serves GET {vendor}/{plugin}/{controller}/reorder?partition=<id>.
|
|
func (s *service) reorderGet(w http.ResponseWriter, r *http.Request) {
|
|
s.protect(w, r, func(cc *CompiledController) {
|
|
if cc.reorder == nil || cc.singleton {
|
|
writeNotFound(w, r)
|
|
return
|
|
}
|
|
db, err := s.db()
|
|
if err != nil {
|
|
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
|
return
|
|
}
|
|
var partition *uint64
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("partition")); raw != "" {
|
|
n, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil || n == 0 {
|
|
WriteErrorDetails(w, http.StatusUnprocessableEntity, "validation_failed", "Validation failed", map[string]any{"partition": []string{"The partition field must be an id."}})
|
|
return
|
|
}
|
|
partition = &n
|
|
}
|
|
ctx := towel.WithLocale(r.Context(), schemaLocale(r.Context(), s.translator()))
|
|
var tree AdminReorderTree
|
|
err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var err error
|
|
tree, err = s.reorderTree(withTx(ctx, tx), tx, cc, partition, false)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
s.writeReorderError(w, r, cc, err)
|
|
return
|
|
}
|
|
WriteData(w, http.StatusOK, tree, map[string]any{"locale": schemaLocale(r.Context(), s.translator())})
|
|
})
|
|
}
|
|
|
|
// reorderPost serves POST {vendor}/{plugin}/{controller}/reorder: one
|
|
// transaction locks the scoped set (ListExtendQuery plus the partition),
|
|
// requires the submitted ids to equal it exactly (409 reorder_stale
|
|
// otherwise), refuses a duplicate id, a parent outside the set, a cycle and a
|
|
// depth above maxDepth (422), then writes the parent and sort columns of
|
|
// every node and answers the new tree.
|
|
func (s *service) reorderPost(w http.ResponseWriter, r *http.Request) {
|
|
s.protect(w, r, func(cc *CompiledController) {
|
|
if cc.reorder == nil || cc.singleton {
|
|
writeNotFound(w, r)
|
|
return
|
|
}
|
|
var in AdminReorderRequest
|
|
if err := s.decodeStrictBody(w, r, &in); err != nil {
|
|
writeCRUDError(w, err)
|
|
return
|
|
}
|
|
db, err := s.db()
|
|
if err != nil {
|
|
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
|
return
|
|
}
|
|
ctx := towel.WithLocale(r.Context(), schemaLocale(r.Context(), s.translator()))
|
|
var tree AdminReorderTree
|
|
err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
txCtx := withTx(ctx, tx)
|
|
if cc.reorder.partition != nil && in.Partition == nil {
|
|
return &ValidationError{Details: map[string]any{"partition": []string{"The partition field is required."}}}
|
|
}
|
|
locked, err := s.reorderTree(txCtx, tx, cc, in.Partition, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := checkReorderNodes(cc.reorder, locked.Nodes, in.Nodes); err != nil {
|
|
return err
|
|
}
|
|
proto := cc.Controller.(pact.AdminRecordSource).NewRecord()
|
|
pk := primaryColumn(proto)
|
|
for _, node := range in.Nodes {
|
|
var parent any
|
|
if node.ParentID != nil {
|
|
parent = castPK(proto, uint(*node.ParentID))
|
|
}
|
|
// UpdateColumns writes exactly the two columns and runs no
|
|
// model hooks, so a save hook that renumbers sort values
|
|
// cannot undo the new order.
|
|
err := tx.WithContext(txCtx).Model(cc.Controller.(pact.AdminRecordSource).NewRecord()).
|
|
Where(clause.Eq{Column: clause.Column{Name: pk}, Value: castPK(proto, uint(node.ID))}).
|
|
UpdateColumns(map[string]any{cc.reorder.parentColumn: parent, cc.reorder.sortColumn: node.SortOrder}).Error
|
|
if err != nil {
|
|
return lifecycleFailure(cc, err)
|
|
}
|
|
}
|
|
tree, err = s.reorderTree(txCtx, tx, cc, in.Partition, false)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
s.writeReorderError(w, r, cc, err)
|
|
return
|
|
}
|
|
WriteData(w, http.StatusOK, tree, map[string]any{"locale": schemaLocale(r.Context(), s.translator())})
|
|
})
|
|
}
|
|
|
|
func (s *service) writeReorderError(w http.ResponseWriter, r *http.Request, cc *CompiledController, err error) {
|
|
var stale reorderStale
|
|
if errors.As(err, &stale) {
|
|
ctx := towel.WithLocale(r.Context(), schemaLocale(r.Context(), s.translator()))
|
|
WriteError(w, http.StatusConflict, "reorder_stale", translateKey(ctx, s.translator(), msgReorderStale))
|
|
return
|
|
}
|
|
var ve *ValidationError
|
|
var missing recordNotFound
|
|
if !errors.As(err, &ve) && !errors.As(err, &missing) {
|
|
slog.Error("cabana: reorder failed", "controller", controllerID(cc), "error", err)
|
|
}
|
|
writeCRUDError(w, err)
|
|
}
|
|
|
|
// reorderTree loads the scoped set of a reorder page: the records the list
|
|
// scope (ListExtendQuery) returns within the partition, ordered by the sort
|
|
// column and then the primary key; lock adds FOR UPDATE. A partition that is
|
|
// not among the offered ones is not found; without one requested, the first
|
|
// offered partition is shown.
|
|
func (s *service) reorderTree(ctx context.Context, tx *gorm.DB, cc *CompiledController, requested *uint64, lock bool) (AdminReorderTree, error) {
|
|
ro := cc.reorder
|
|
tr := s.translator()
|
|
tree := AdminReorderTree{Title: translateKey(ctx, tr, ro.title), Nodes: []AdminReorderNode{}}
|
|
proto := cc.Controller.(pact.AdminRecordSource).NewRecord()
|
|
q := tx.WithContext(ctx).Model(proto)
|
|
if ext, ok := cc.Controller.(pact.ListExtendQuery); ok && ext != nil {
|
|
if next := ext.ListExtendQuery(ctx, q); next != nil {
|
|
q = next
|
|
}
|
|
}
|
|
table := tableName(proto)
|
|
if p := ro.partition; p != nil {
|
|
options, err := reorderPartitions(ctx, tx, cc, p)
|
|
if err != nil {
|
|
return AdminReorderTree{}, err
|
|
}
|
|
part := &AdminReorderPartition{Label: translateKey(ctx, tr, p.label), Options: options}
|
|
tree.Partition = part
|
|
switch {
|
|
case requested != nil:
|
|
found := false
|
|
for _, opt := range options {
|
|
found = found || uint64(opt.Value) == *requested
|
|
}
|
|
if !found {
|
|
return AdminReorderTree{}, recordNotFound{}
|
|
}
|
|
v := uint(*requested)
|
|
part.Value = &v
|
|
case len(options) > 0:
|
|
v := options[0].Value
|
|
part.Value = &v
|
|
default:
|
|
return tree, nil
|
|
}
|
|
q = q.Where(clause.Eq{Column: clause.Column{Table: table, Name: p.column}, Value: *part.Value})
|
|
}
|
|
if lock {
|
|
q = q.Clauses(clause.Locking{Strength: "UPDATE"})
|
|
}
|
|
pk := primaryColumn(proto)
|
|
holder := reflect.New(reflect.SliceOf(reflect.TypeOf(proto).Elem()))
|
|
err := q.Order(clause.OrderByColumn{Column: clause.Column{Table: table, Name: ro.sortColumn}}).
|
|
Order(clause.OrderByColumn{Column: clause.Column{Table: table, Name: pk}}).
|
|
Find(holder.Interface()).Error
|
|
if err != nil {
|
|
return AdminReorderTree{}, lifecycleFailure(cc, err)
|
|
}
|
|
rows := holder.Elem()
|
|
for i := 0; i < rows.Len(); i++ {
|
|
row := rows.Index(i)
|
|
node := AdminReorderNode{ID: uintOf(fieldByColumn(row, pk))}
|
|
if parent := uintOf(fieldByColumn(row, ro.parentColumn)); parent != 0 {
|
|
node.ParentID = &parent
|
|
}
|
|
node.SortOrder = intOf(fieldByColumn(row, ro.sortColumn))
|
|
if f := fieldByColumn(row, ro.nameFrom); f.IsValid() && f.CanInterface() {
|
|
if v := derefValue(f.Interface()); v != nil {
|
|
node.Name = fmt.Sprint(v)
|
|
}
|
|
}
|
|
tree.Nodes = append(tree.Nodes, node)
|
|
}
|
|
return tree, nil
|
|
}
|
|
|
|
// reorderPartitions lists the partitions the administrator may choose: the
|
|
// related records the partition relation field offers (its
|
|
// RelationExtendOptionsQuery scope), ordered by label then id.
|
|
func reorderPartitions(ctx context.Context, tx *gorm.DB, cc *CompiledController, p *compiledPartition) ([]AdminReorderPartitionOption, error) {
|
|
related := p.fieldRel.Contract.NewRelated()
|
|
table := tableName(related)
|
|
pk := primaryColumn(related)
|
|
q := scopedRelationQuery(ctx, tx, cc, p.relation, p.fieldRel)
|
|
label := labelExpression(q, table, p.labelCol)
|
|
rows := []AdminReorderPartitionOption{}
|
|
err := q.Select(quotedIdent(q, table) + "." + quotedIdent(q, pk) + " AS value, " + label + " AS label").
|
|
Order(label).
|
|
Order(clause.OrderByColumn{Column: clause.Column{Table: table, Name: pk}}).
|
|
Limit(maxReorderPartitions).
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, lifecycleFailure(cc, err)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
// checkReorderNodes compares a submitted tree with the locked set: a
|
|
// duplicate id is a 422, any difference between the two id sets a 409, and a
|
|
// parent outside the set, a cycle or a depth above maxDepth a 422.
|
|
func checkReorderNodes(ro *compiledReorder, locked []AdminReorderNode, nodes []AdminReorderNodeInput) error {
|
|
invalid := func(msg string) error {
|
|
return &ValidationError{Details: map[string]any{"nodes": []string{msg}}}
|
|
}
|
|
parents := make(map[uint64]uint64, len(nodes))
|
|
for _, node := range nodes {
|
|
if node.ID == 0 {
|
|
return invalid("The nodes field must list record ids.")
|
|
}
|
|
if _, dup := parents[node.ID]; dup {
|
|
return invalid("The nodes field lists a record twice.")
|
|
}
|
|
parent := uint64(0)
|
|
if node.ParentID != nil {
|
|
parent = *node.ParentID
|
|
}
|
|
parents[node.ID] = parent
|
|
}
|
|
if len(parents) != len(locked) {
|
|
return reorderStale{}
|
|
}
|
|
for _, node := range locked {
|
|
if _, ok := parents[uint64(node.ID)]; !ok {
|
|
return reorderStale{}
|
|
}
|
|
}
|
|
ids := make([]uint64, 0, len(parents))
|
|
for id, parent := range parents {
|
|
ids = append(ids, id)
|
|
if parent == 0 {
|
|
continue
|
|
}
|
|
if parent == id {
|
|
return invalid("A record cannot be its own parent.")
|
|
}
|
|
if _, ok := parents[parent]; !ok {
|
|
return invalid("A parent is not one of the reordered records.")
|
|
}
|
|
}
|
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
|
for _, id := range ids {
|
|
depth := 1
|
|
for cur := parents[id]; cur != 0; cur = parents[cur] {
|
|
depth++
|
|
if depth > len(parents) {
|
|
return invalid("The tree has a cycle.")
|
|
}
|
|
}
|
|
if ro.maxDepth > 0 && depth > ro.maxDepth {
|
|
return invalid(fmt.Sprintf("The tree may be at most %d levels deep.", ro.maxDepth))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// uintOf reads an unsigned id from a struct field (a pointer is
|
|
// dereferenced; nil and non-numbers are 0).
|
|
func uintOf(f reflect.Value) uint {
|
|
if !f.IsValid() || !f.CanInterface() {
|
|
return 0
|
|
}
|
|
v := reflect.ValueOf(derefValue(f.Interface()))
|
|
switch v.Kind() {
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return uint(v.Uint())
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if v.Int() > 0 {
|
|
return uint(v.Int())
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// intOf reads an integer from a struct field (a pointer is dereferenced; nil
|
|
// and non-numbers are 0).
|
|
func intOf(f reflect.Value) int64 {
|
|
if !f.IsValid() || !f.CanInterface() {
|
|
return 0
|
|
}
|
|
v := reflect.ValueOf(derefValue(f.Interface()))
|
|
switch v.Kind() {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return v.Int()
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return int64(v.Uint())
|
|
}
|
|
return 0
|
|
}
|