update codec
parent
40ea785ec9
commit
17c7c7b139
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
// 指针类型
|
// 指针类型
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 正常返回
|
||||||
|
|
|
||||||
|
|
@ -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 :
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue