update codec

main
git 2025-12-10 23:34:57 +08:00
parent 40ea785ec9
commit 17c7c7b139
Signed by: git
GPG Key ID: 3F65EFFA44207ADD
15 changed files with 168 additions and 195 deletions

View File

@ -9,8 +9,9 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"git.ifooth.com/common/pkg/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"git.ifooth.com/common/pkg/version"
) )
var ( var (

View File

@ -1,19 +1,3 @@
/*
* Tencent is pleased to support the open source community by making
* - (BlueKing - CMDB) available.
* Copyright (C) 2025 Tencent. All rights reserved.
* Licensed under the MIT License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/
// Package codec provides encoding and decoding utilities across various formats // Package codec provides encoding and decoding utilities across various formats
package codec package codec
@ -28,9 +12,8 @@ func decodeTo(r *http.Request, val any) error {
rt := reflect.TypeOf(val).Elem() rt := reflect.TypeOf(val).Elem()
rv := reflect.ValueOf(val).Elem() rv := reflect.ValueOf(val).Elem()
// json 整个解析 fields, err := getStructFields(rt, rv)
jsonCodec := NewJsonCodec(r) if err != nil {
if err := jsonCodec.Decode(val); err != nil {
return err return err
} }
@ -41,38 +24,37 @@ func decodeTo(r *http.Request, val any) error {
pathCodec := NewPathCodec(r) pathCodec := NewPathCodec(r)
queryCodec := NewQueryCodec(r) queryCodec := NewQueryCodec(r)
headerCodec := NewHeaderCodec(r) headerCodec := NewHeaderCodec(r)
for _, f := range fields {
for i := 0; i < rt.NumField(); i++ { switch f.tag.In {
field := rt.Field(i) case pathOptName:
if err := pathCodec.Decode(f.field, f.fv, f.tag); err != nil {
// 非导出需要跳过, 无法设置值 return fmt.Errorf("field[%s] decode path: %w", f.field.Name, err)
if !field.IsExported() { }
continue case queryOptName:
if err := queryCodec.Decode(f.field, f.fv, f.tag); err != nil {
return fmt.Errorf("field[%s] decode query: %w", f.field.Name, err)
}
case formOptName:
if err := formCodec.Decode(f.field, f.fv, f.tag); err != nil {
return fmt.Errorf("field[%s] decode form: %w", f.field.Name, err)
}
case headerOptName:
if err := headerCodec.Decode(f.field, f.fv, f.tag); err != nil {
return fmt.Errorf("field[%s] decode header: %w", f.field.Name, err)
}
case "":
return fmt.Errorf("field[%s] in option is required", f.field.Name)
default:
return fmt.Errorf("field[%s] in[%s] option not valid", f.field.Name, f.tag.In)
}
} }
tagStr := field.Tag.Get(tagName) // json 整个解析
if tagStr == "" { jsonCodec := NewJsonCodec(r)
continue if err := jsonCodec.Decode(val); err != nil {
} return fmt.Errorf("decode json: %w", err)
tag, err := parseTag(tagStr)
if err != nil {
return err
}
fv := rv.Field(i)
if err := formCodec.Decode(field, fv, tag); err != nil {
return err
}
if err := queryCodec.Decode(field, fv, tag); err != nil {
return err
}
if err := headerCodec.Decode(field, fv, tag); err != nil {
return err
}
if err := pathCodec.Decode(field, fv, tag); err != nil {
return err
}
} }
return nil return nil
@ -88,12 +70,53 @@ func Decode[T any](r *http.Request) (*T, error) {
t := new(T) t := new(T)
err := decodeTo(r, t) err := decodeTo(r, t)
if err != nil { if err != nil {
return nil, fmt.Errorf("codec decode: %w", err) return nil, fmt.Errorf("decode req: %w", err)
} }
return t, nil return t, nil
} }
type structField struct {
field reflect.StructField // 结构体字段
fv reflect.Value // 字段的值
tag *Tag // 字段解析后的req tag
}
// getStructFields 获取字段列表, 校验json/req的唯一性
func getStructFields(rt reflect.Type, rv reflect.Value) ([]structField, error) {
fields := make([]structField, 0)
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
// 非导出需要跳过, 无法设置值
if !field.IsExported() {
continue
}
reqTagStr := field.Tag.Get(tagName)
if reqTagStr == "" {
continue
}
tag, err := parseTag(reqTagStr)
if err != nil {
return nil, fmt.Errorf("field[%s] %w", field.Name, err)
}
// tag name为空或者-忽略
if tag.Name == "" || tag.Name == "-" {
continue
}
// 可以重复
// jsonTagName := util.GetTagName(field, "json")
// if jsonTagName != "" && jsonTagName != "-" {
// return nil, fmt.Errorf("field[%s] req and json tag are mutually exclusive", field.Name)
// }
fields = append(fields, structField{field: field, fv: rv.Field(i), tag: tag})
}
return fields, nil
}
// getFieldValue 获取字段值 // getFieldValue 获取字段值
func getFieldValue(field reflect.Type, tag *Tag, values []string) (reflect.Value, error) { func getFieldValue(field reflect.Type, tag *Tag, values []string) (reflect.Value, error) {
// 指针类型 // 指针类型

View File

@ -34,10 +34,13 @@ func NewFormCodec(r *http.Request) (*formCodec, error) {
isForm := false isForm := false
contentType := r.Header.Get("Content-Type") contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { if (r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH") &&
strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return nil, err return nil, err
} }
isForm = true isForm = true
} }
@ -54,12 +57,7 @@ func (c *formCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag
return nil return nil
} }
formTag, ok := tag.Option["form"] v := c.values[tag.Name]
if !ok {
return nil
}
v := c.values[formTag]
if len(v) == 0 { if len(v) == 0 {
return nil return nil
} }

View File

@ -34,13 +34,8 @@ func NewHeaderCodec(r *http.Request) *headerCodec {
// Decode ... // Decode ...
func (c *headerCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error { func (c *headerCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error {
headTag, ok := tag.Option["header"]
if !ok {
return nil
}
// header统一格式 // header统一格式
key := http.CanonicalHeaderKey(headTag) key := http.CanonicalHeaderKey(tag.Name)
v := c.values[key] v := c.values[key]
if len(v) == 0 { if len(v) == 0 {
return nil return nil

View File

@ -1,19 +1,3 @@
/*
* Tencent is pleased to support the open source community by making
* - (BlueKing - CMDB) available.
* Copyright (C) 2025 Tencent. All rights reserved.
* Licensed under the MIT License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/
package codec package codec
import ( import (
@ -33,8 +17,10 @@ type jsonCodec struct {
func NewJsonCodec(r *http.Request) *jsonCodec { func NewJsonCodec(r *http.Request) *jsonCodec {
isJson := false isJson := false
// 限制Method, 同ParseForm的一致
contentType := r.Header.Get("Content-Type") contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/json") { if (r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH") &&
strings.HasPrefix(contentType, "application/json") {
isJson = true isJson = true
} }
@ -51,11 +37,15 @@ func (j *jsonCodec) Decode(val any) error {
if err != nil { if err != nil {
return err return err
} }
// body等于空时可能其他解析场景直接正常返回
// 如果需要判断是否有值,可通过指针处理
if len(body) == 0 { if len(body) == 0 {
return fmt.Errorf("json body is empty") return nil
} }
if err := json.Unmarshal(body, val); err != nil { if err := json.Unmarshal(body, val); err != nil {
return fmt.Errorf("unmarshal json body: %s", err) return fmt.Errorf("unmarshal json body: %w", err)
} }
return nil return nil
} }

View File

@ -1,19 +1,3 @@
/*
* Tencent is pleased to support the open source community by making
* - (BlueKing - CMDB) available.
* Copyright (C) 2025 Tencent. All rights reserved.
* Licensed under the MIT License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/
package codec package codec
import ( import (
@ -34,12 +18,7 @@ func NewPathCodec(r *http.Request) *pathCodec {
// Decode ... // Decode ...
func (c *pathCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error { func (c *pathCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error {
pathTag, ok := tag.Option["path"] pv := c.req.PathValue(tag.Name)
if !ok {
return nil
}
pv := c.req.PathValue(pathTag)
if pv == "" { if pv == "" {
return nil return nil
} }

View File

@ -1,19 +1,3 @@
/*
* Tencent is pleased to support the open source community by making
* - (BlueKing - CMDB) available.
* Copyright (C) 2025 Tencent. All rights reserved.
* Licensed under the MIT License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/
package codec package codec
import ( import (
@ -35,12 +19,7 @@ func NewQueryCodec(r *http.Request) *queryCodec {
// Decode ... // Decode ...
func (c *queryCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error { func (c *queryCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error {
queryTag, ok := tag.Option["query"] v := c.values[tag.Name]
if !ok {
return nil
}
v := c.values[queryTag]
if len(v) == 0 { if len(v) == 0 {
return nil return nil
} }

View File

@ -1,19 +1,3 @@
/*
* Tencent is pleased to support the open source community by making
* - (BlueKing - CMDB) available.
* Copyright (C) 2025 Tencent. All rights reserved.
* Licensed under the MIT License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/
package codec package codec
import ( import (
@ -25,11 +9,18 @@ const (
// tagName 结构体tag名称 // tagName 结构体tag名称
// 格式参考 https://pkg.go.dev/encoding/json/v2#example-package-FormatFlags // 格式参考 https://pkg.go.dev/encoding/json/v2#example-package-FormatFlags
tagName = "req" tagName = "req"
inOptName = "in"
queryOptName = "query"
pathOptName = "path"
formOptName = "form"
headerOptName = "header"
) )
// Tag is a struct tag // Tag is a struct tag
type Tag struct { type Tag struct {
Option map[string]string Name string // tag name
In string // query/path参数中
Option map[string]string // 自定义参数
} }
func parseTag(tagStr string) (*Tag, error) { func parseTag(tagStr string) (*Tag, error) {
@ -39,11 +30,14 @@ func parseTag(tagStr string) (*Tag, error) {
} }
parts := strings.Split(tagStr, ",") parts := strings.Split(tagStr, ",")
name := strings.TrimSpace(parts[0])
t := &Tag{ t := &Tag{
Name: name,
Option: map[string]string{}, Option: map[string]string{},
} }
for _, part := range parts { for _, part := range parts[1:] {
part = strings.TrimSpace(part) part = strings.TrimSpace(part)
if part == "" { if part == "" {
return nil, fmt.Errorf("tag option not valid") return nil, fmt.Errorf("tag option not valid")
@ -55,8 +49,13 @@ func parseTag(tagStr string) (*Tag, error) {
if len(opt) == 2 { if len(opt) == 2 {
val = opt[1] val = opt[1]
} }
if key == inOptName {
t.In = val
} else {
t.Option[key] = val t.Option[key] = val
} }
}
return t, nil return t, nil
} }

View File

@ -1,19 +1,3 @@
/*
* Tencent is pleased to support the open source community by making
* - (BlueKing - CMDB) available.
* Copyright (C) 2025 Tencent. All rights reserved.
* Licensed under the MIT License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/
package codec package codec
import ( import (
@ -175,7 +159,7 @@ func (t *Time) New(opt map[string]string) Parser {
} }
func init() { func init() {
// buildin parser // builtin parser
RegisterParser[string](ParserFunc(StringParser)) RegisterParser[string](ParserFunc(StringParser))
RegisterParser[bool](ParserFunc(BoolParser)) RegisterParser[bool](ParserFunc(BoolParser))
RegisterParser[int](Int[int]{0}) RegisterParser[int](Int[int]{0})

View File

@ -51,23 +51,23 @@ func Handle[Req, Resp any](fn UnaryFunc[Req, Resp]) func(w http.ResponseWriter,
in, err := decodeReq[Req](r) in, err := decodeReq[Req](r)
if err != nil { if err != nil {
slog.Error("handle decode request failed", "err", err) slog.Error("handle decode request failed", "err", err)
_ = APIError(err).Render(w, r) APIError(err).Render(w, r)
return return
} }
// 参数校验 // 参数校验
if err = validateReq(r.Context(), in); err != nil { if err = validateReq(r.Context(), in); err != nil {
slog.Error("validate req failed", "err", err) slog.Error("validate req failed", "err", err)
_ = APIError(err).Render(w, r) APIError(err).Render(w, r)
return return
} }
out, err := fn(r.Context(), in) out, err := fn(r.Context(), in)
if err != nil { if err != nil {
_ = APIError(err).Render(w, r) APIError(err).Render(w, r)
return return
} }
_ = APIOK(out).Render(w, r) APIOK(out).Render(w, r)
} }
return f return f
} }
@ -98,14 +98,14 @@ func Stream[Req any](fn StreamFunc[Req]) func(w http.ResponseWriter, r *http.Req
in, err := decodeReq[Req](r) in, err := decodeReq[Req](r)
if err != nil { if err != nil {
slog.Error("handle decode stream request failed", "err", err) slog.Error("handle decode stream request failed", "err", err)
_ = APIError(err).Render(w, r) APIError(err).Render(w, r)
return return
} }
// 参数校验 // 参数校验
if err = validateReq(r.Context(), in); err != nil { if err = validateReq(r.Context(), in); err != nil {
slog.Error("validate stream req failed", "err", err) slog.Error("validate stream req failed", "err", err)
_ = APIError(err).Render(w, r) APIError(err).Render(w, r)
return return
} }
@ -117,7 +117,7 @@ func Stream[Req any](fn StreamFunc[Req]) func(w http.ResponseWriter, r *http.Req
err = fn(in, svr) err = fn(in, svr)
if err != nil { if err != nil {
_ = APIError(err).Render(w, r) APIError(err).Render(w, r)
} }
} }
return f return f

View File

@ -18,12 +18,13 @@ package rest
import ( import (
"encoding/json/v2" "encoding/json/v2"
"log/slog"
"net/http" "net/http"
) )
// Renderer interface for managing response payloads. // Renderer interface for managing response payloads.
type Renderer interface { type Renderer interface {
Render(w http.ResponseWriter, r *http.Request) error Render(w http.ResponseWriter, r *http.Request)
} }
// APIResponse response for api request // APIResponse response for api request
@ -35,12 +36,15 @@ type APIResponse struct {
} }
// Render chi render interface implementation // Render chi render interface implementation
func (e *APIResponse) Render(w http.ResponseWriter, r *http.Request) error { func (e *APIResponse) Render(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(e.HTTPCode) w.WriteHeader(e.HTTPCode)
return json.MarshalWrite(w, e) err := json.MarshalWrite(w, e)
if err != nil {
slog.Error("render resp failed", "err", err)
}
} }
// APIOK 正常返回 // APIOK 正常返回

View File

@ -5,8 +5,9 @@ import (
"strconv" "strconv"
"time" "time"
"git.ifooth.com/common/pkg/config"
redis "github.com/redis/go-redis/v9" redis "github.com/redis/go-redis/v9"
"git.ifooth.com/common/pkg/config"
) )
// RedisSession : // RedisSession :

View File

@ -59,16 +59,16 @@ func NewHandler(mgr *TaskManager, host string, routePrefix string) http.Handler
type Task struct { type Task struct {
*itypes.Task *itypes.Task
Steps []*Step `json:"steps,omitempty"` Steps []*Step `json:"steps,omitempty"`
ExecutionDuration time.Duration `json:"executionDuration" swaggertype:"string"` ExecutionDuration string `json:"executionDuration" swaggertype:"string"`
MaxExecutionDuration time.Duration `json:"maxExecutionDuration" swaggertype:"string"` MaxExecutionDuration string `json:"maxExecutionDuration" swaggertype:"string"`
DetailURL string `json:"detailURL,omitempty"` DetailURL string `json:"detailURL,omitempty"`
} }
// Step bcs_task step with execution duration // Step bcs_task step with execution duration
type Step struct { type Step struct {
*itypes.Step *itypes.Step
ExecutionDuration time.Duration `json:"executionDuration" swaggertype:"string"` ExecutionDuration string `json:"executionDuration,fmt" swaggertype:"string"`
MaxExecutionDuration time.Duration `json:"maxExecutionDuration" swaggertype:"string"` MaxExecutionDuration string `json:"maxExecutionDuration" swaggertype:"string"`
} }
// StepReq ... // StepReq ...
@ -320,16 +320,16 @@ func (s *service) Status(ctx context.Context, req *commonReq) (*Task, error) {
steps := lo.Map(taskData.Steps, func(v *itypes.Step, _ int) *Step { steps := lo.Map(taskData.Steps, func(v *itypes.Step, _ int) *Step {
return &Step{ return &Step{
Step: v, Step: v,
ExecutionDuration: time.Duration(v.ExecutionTime) * time.Millisecond, ExecutionDuration: (time.Duration(v.ExecutionTime) * time.Millisecond).String(),
MaxExecutionDuration: time.Duration(v.MaxExecutionSeconds) * time.Second, MaxExecutionDuration: (time.Duration(v.MaxExecutionSeconds) * time.Second).String(),
} }
}) })
t := &Task{ t := &Task{
Task: taskData, Task: taskData,
Steps: steps, Steps: steps,
ExecutionDuration: time.Duration(taskData.ExecutionTime) * time.Millisecond, ExecutionDuration: (time.Duration(taskData.ExecutionTime) * time.Millisecond).String(),
MaxExecutionDuration: time.Duration(taskData.MaxExecutionSeconds) * time.Second, MaxExecutionDuration: (time.Duration(taskData.MaxExecutionSeconds) * time.Second).String(),
} }
return t, nil return t, nil
@ -404,8 +404,8 @@ func (s *service) List(ctx context.Context, req *ListReq) (*rest.PaginationResp[
items := lo.Map(result.Items, func(v *itypes.Task, _ int) Task { items := lo.Map(result.Items, func(v *itypes.Task, _ int) Task {
return Task{ return Task{
Task: v, Task: v,
ExecutionDuration: time.Duration(v.ExecutionTime) * time.Millisecond, ExecutionDuration: (time.Duration(v.ExecutionTime) * time.Millisecond).String(),
MaxExecutionDuration: time.Duration(v.MaxExecutionSeconds) * time.Second, MaxExecutionDuration: (time.Duration(v.MaxExecutionSeconds) * time.Second).String(),
DetailURL: fmt.Sprintf("%s?taskID=%s", detailURL, v.TaskID), DetailURL: fmt.Sprintf("%s?taskID=%s", detailURL, v.TaskID),
} }
}) })

14
util/util.go Normal file
View File

@ -0,0 +1,14 @@
// Package util provides common utility functions and helpers for various operations across the project.
package util
import (
"reflect"
"strings"
)
// GetTagName extracts the first comma-separated value from a struct field's tag.
// For example, given a tag `json:"name,omitempty"`, it returns "name".
func GetTagName(field reflect.StructField, tag string) string {
name := strings.SplitN(field.Tag.Get(tag), ",", 2)[0]
return name
}

View File

@ -4,7 +4,6 @@ package validator
import ( import (
"context" "context"
"reflect" "reflect"
"strings"
"github.com/go-playground/locales/en" "github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh" "github.com/go-playground/locales/zh"
@ -15,6 +14,7 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
"git.ifooth.com/common/pkg/i18n" "git.ifooth.com/common/pkg/i18n"
"git.ifooth.com/common/pkg/util"
) )
var ( var (
@ -73,18 +73,24 @@ func Struct(ctx context.Context, s any) error {
return nil return nil
} }
// tagNameFunc 优先从 json tag 获取名称 // readableTagName 返回可读的json/req校验字段名称, 唯一性由codec校验
func tagNameFunc(fld reflect.StructField) string { func readableTagName(field reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] name := util.GetTagName(field, "json")
if name == "-" { if name != "" && name != "-" {
return ""
}
return name return name
} }
name = util.GetTagName(field, "req")
if name != "" && name != "-" {
return name
}
return ""
}
func init() { func init() {
validate = validator.New(validator.WithRequiredStructEnabled()) validate = validator.New(validator.WithRequiredStructEnabled())
validate.RegisterTagNameFunc(tagNameFunc) validate.RegisterTagNameFunc(readableTagName)
// 默认使用英文 // 默认使用英文
en := en.New() en := en.New()