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"
"syscall"
"git.ifooth.com/common/pkg/version"
"github.com/spf13/cobra"
"git.ifooth.com/common/pkg/version"
)
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
@ -28,9 +12,8 @@ func decodeTo(r *http.Request, val any) error {
rt := reflect.TypeOf(val).Elem()
rv := reflect.ValueOf(val).Elem()
// json 整个解析
jsonCodec := NewJsonCodec(r)
if err := jsonCodec.Decode(val); err != nil {
fields, err := getStructFields(rt, rv)
if err != nil {
return err
}
@ -41,38 +24,37 @@ func decodeTo(r *http.Request, val any) error {
pathCodec := NewPathCodec(r)
queryCodec := NewQueryCodec(r)
headerCodec := NewHeaderCodec(r)
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
// 非导出需要跳过, 无法设置值
if !field.IsExported() {
continue
for _, f := range fields {
switch f.tag.In {
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)
}
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)
if tagStr == "" {
continue
}
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
}
// json 整个解析
jsonCodec := NewJsonCodec(r)
if err := jsonCodec.Decode(val); err != nil {
return fmt.Errorf("decode json: %w", err)
}
return nil
@ -88,12 +70,53 @@ func Decode[T any](r *http.Request) (*T, error) {
t := new(T)
err := decodeTo(r, t)
if err != nil {
return nil, fmt.Errorf("codec decode: %w", err)
return nil, fmt.Errorf("decode req: %w", err)
}
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 获取字段值
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
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 {
return nil, err
}
isForm = true
}
@ -54,12 +57,7 @@ func (c *formCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag
return nil
}
formTag, ok := tag.Option["form"]
if !ok {
return nil
}
v := c.values[formTag]
v := c.values[tag.Name]
if len(v) == 0 {
return nil
}

View File

@ -34,13 +34,8 @@ func NewHeaderCodec(r *http.Request) *headerCodec {
// Decode ...
func (c *headerCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error {
headTag, ok := tag.Option["header"]
if !ok {
return nil
}
// header统一格式
key := http.CanonicalHeaderKey(headTag)
key := http.CanonicalHeaderKey(tag.Name)
v := c.values[key]
if len(v) == 0 {
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
import (
@ -33,8 +17,10 @@ type jsonCodec struct {
func NewJsonCodec(r *http.Request) *jsonCodec {
isJson := false
// 限制Method, 同ParseForm的一致
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
}
@ -51,11 +37,15 @@ func (j *jsonCodec) Decode(val any) error {
if err != nil {
return err
}
// body等于空时可能其他解析场景直接正常返回
// 如果需要判断是否有值,可通过指针处理
if len(body) == 0 {
return fmt.Errorf("json body is empty")
return 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
}

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
import (
@ -34,12 +18,7 @@ func NewPathCodec(r *http.Request) *pathCodec {
// Decode ...
func (c *pathCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error {
pathTag, ok := tag.Option["path"]
if !ok {
return nil
}
pv := c.req.PathValue(pathTag)
pv := c.req.PathValue(tag.Name)
if pv == "" {
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
import (
@ -35,12 +19,7 @@ func NewQueryCodec(r *http.Request) *queryCodec {
// Decode ...
func (c *queryCodec) Decode(field reflect.StructField, fv reflect.Value, tag *Tag) error {
queryTag, ok := tag.Option["query"]
if !ok {
return nil
}
v := c.values[queryTag]
v := c.values[tag.Name]
if len(v) == 0 {
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
import (
@ -25,11 +9,18 @@ const (
// tagName 结构体tag名称
// 格式参考 https://pkg.go.dev/encoding/json/v2#example-package-FormatFlags
tagName = "req"
inOptName = "in"
queryOptName = "query"
pathOptName = "path"
formOptName = "form"
headerOptName = "header"
)
// Tag is a struct tag
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) {
@ -39,11 +30,14 @@ func parseTag(tagStr string) (*Tag, error) {
}
parts := strings.Split(tagStr, ",")
name := strings.TrimSpace(parts[0])
t := &Tag{
Name: name,
Option: map[string]string{},
}
for _, part := range parts {
for _, part := range parts[1:] {
part = strings.TrimSpace(part)
if part == "" {
return nil, fmt.Errorf("tag option not valid")
@ -55,8 +49,13 @@ func parseTag(tagStr string) (*Tag, error) {
if len(opt) == 2 {
val = opt[1]
}
if key == inOptName {
t.In = val
} else {
t.Option[key] = val
}
}
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
import (
@ -175,7 +159,7 @@ func (t *Time) New(opt map[string]string) Parser {
}
func init() {
// buildin parser
// builtin parser
RegisterParser[string](ParserFunc(StringParser))
RegisterParser[bool](ParserFunc(BoolParser))
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)
if err != nil {
slog.Error("handle decode request failed", "err", err)
_ = APIError(err).Render(w, r)
APIError(err).Render(w, r)
return
}
// 参数校验
if err = validateReq(r.Context(), in); err != nil {
slog.Error("validate req failed", "err", err)
_ = APIError(err).Render(w, r)
APIError(err).Render(w, r)
return
}
out, err := fn(r.Context(), in)
if err != nil {
_ = APIError(err).Render(w, r)
APIError(err).Render(w, r)
return
}
_ = APIOK(out).Render(w, r)
APIOK(out).Render(w, r)
}
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)
if err != nil {
slog.Error("handle decode stream request failed", "err", err)
_ = APIError(err).Render(w, r)
APIError(err).Render(w, r)
return
}
// 参数校验
if err = validateReq(r.Context(), in); err != nil {
slog.Error("validate stream req failed", "err", err)
_ = APIError(err).Render(w, r)
APIError(err).Render(w, r)
return
}
@ -117,7 +117,7 @@ func Stream[Req any](fn StreamFunc[Req]) func(w http.ResponseWriter, r *http.Req
err = fn(in, svr)
if err != nil {
_ = APIError(err).Render(w, r)
APIError(err).Render(w, r)
}
}
return f

View File

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

View File

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

View File

@ -59,16 +59,16 @@ func NewHandler(mgr *TaskManager, host string, routePrefix string) http.Handler
type Task struct {
*itypes.Task
Steps []*Step `json:"steps,omitempty"`
ExecutionDuration time.Duration `json:"executionDuration" swaggertype:"string"`
MaxExecutionDuration time.Duration `json:"maxExecutionDuration" swaggertype:"string"`
ExecutionDuration string `json:"executionDuration" swaggertype:"string"`
MaxExecutionDuration string `json:"maxExecutionDuration" swaggertype:"string"`
DetailURL string `json:"detailURL,omitempty"`
}
// Step bcs_task step with execution duration
type Step struct {
*itypes.Step
ExecutionDuration time.Duration `json:"executionDuration" swaggertype:"string"`
MaxExecutionDuration time.Duration `json:"maxExecutionDuration" swaggertype:"string"`
ExecutionDuration string `json:"executionDuration,fmt" swaggertype:"string"`
MaxExecutionDuration string `json:"maxExecutionDuration" swaggertype:"string"`
}
// 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 {
return &Step{
Step: v,
ExecutionDuration: time.Duration(v.ExecutionTime) * time.Millisecond,
MaxExecutionDuration: time.Duration(v.MaxExecutionSeconds) * time.Second,
ExecutionDuration: (time.Duration(v.ExecutionTime) * time.Millisecond).String(),
MaxExecutionDuration: (time.Duration(v.MaxExecutionSeconds) * time.Second).String(),
}
})
t := &Task{
Task: taskData,
Steps: steps,
ExecutionDuration: time.Duration(taskData.ExecutionTime) * time.Millisecond,
MaxExecutionDuration: time.Duration(taskData.MaxExecutionSeconds) * time.Second,
ExecutionDuration: (time.Duration(taskData.ExecutionTime) * time.Millisecond).String(),
MaxExecutionDuration: (time.Duration(taskData.MaxExecutionSeconds) * time.Second).String(),
}
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 {
return Task{
Task: v,
ExecutionDuration: time.Duration(v.ExecutionTime) * time.Millisecond,
MaxExecutionDuration: time.Duration(v.MaxExecutionSeconds) * time.Second,
ExecutionDuration: (time.Duration(v.ExecutionTime) * time.Millisecond).String(),
MaxExecutionDuration: (time.Duration(v.MaxExecutionSeconds) * time.Second).String(),
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 (
"context"
"reflect"
"strings"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
@ -15,6 +14,7 @@ import (
"github.com/samber/lo"
"git.ifooth.com/common/pkg/i18n"
"git.ifooth.com/common/pkg/util"
)
var (
@ -73,18 +73,24 @@ func Struct(ctx context.Context, s any) error {
return nil
}
// tagNameFunc 优先从 json tag 获取名称
func tagNameFunc(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
// readableTagName 返回可读的json/req校验字段名称, 唯一性由codec校验
func readableTagName(field reflect.StructField) string {
name := util.GetTagName(field, "json")
if name != "" && name != "-" {
return name
}
name = util.GetTagName(field, "req")
if name != "" && name != "-" {
return name
}
return ""
}
func init() {
validate = validator.New(validator.WithRequiredStructEnabled())
validate.RegisterTagNameFunc(tagNameFunc)
validate.RegisterTagNameFunc(readableTagName)
// 默认使用英文
en := en.New()