反序列化AWS/阿里云样式的基于Query的API请求
对于比较了解云计算的人来说,一定接触过AWS、阿里云的API接口,这两者的API调用方式很相似,当然具体谁参考谁这里就不深究了。以给EC2/ECS添加Tag这个接口为例:
AWS:
https://ec2.amazonaws.com/?Action=CreateTags
&ResourceId.1=ami-1a2b3c4d
&ResourceId.2=i-1234567890abcdef0
&Tag.1.Key=webserver
&Tag.1.Value=
&Tag.2.Key=stack
&Tag.2.Value=Production
&AUTHPARAMS
阿里云:
https://ecs.aliyuncs.com/?Action=TagResources
&RegionId=cn-hangzhou
&ResourceId.1=i-bp1j6qtvdm8w0z1o0****
&ResourceId.2=i-bp1j6qtvdm8w0z1oP****
&ResourceType=instance
&Tag.1.Key=TestKey
&Tag.1.Value=TestKey
&<公共请求参数>
这种样式的接口设计,其实没有什么复杂的,相对比较特殊的地方在于,如果需要传入一个数组,则需要使用类似下标一样的Tag.N.Key
这种格式进行传递,这个传递方式,和已有的一些诸如google/go-querystring的传递方式都不太相同,总之是个很特殊的设计。
如果需要写一个类似的服务,使用和这两家相同的API格式的话,针对这种数组格式的请求反序列化是个挺麻烦的事,而且找了一圈也没有类似的开源项目做这个。
今天借助ChatGPT写了一个反序列化函数,专门用来实现服务端对类似形态API的反序列化,通过这个函数可以很方便的将Query反序列化成一个对应的Struct:
package main
import (
"encoding/json"
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
)
type TagRequest struct {
Action string `query:"Action"`
RegionID string `query:"RegionId"`
ResourceIds []string `query:"ResourceId"`
ResourceType string `query:"ResourceType"`
Tags []Tag `query:"Tag"`
}
type Tag struct {
Key string `query:"Key"`
Value string `query:"Value"`
}
func Unmarshal(queryStr string, output interface{}) error {
values, err := url.ParseQuery(queryStr)
if err != nil {
return err
}
return unmarshalData(values, output)
}
func unmarshalData(values url.Values, output interface{}) error {
outputVal := reflect.ValueOf(output)
if outputVal.Kind() != reflect.Ptr {
return fmt.Errorf("output must be a pointer")
}
outputElem := outputVal.Elem()
outputType := outputElem.Type()
for i := 0; i < outputType.NumField(); i++ {
field := outputType.Field(i)
tag := field.Tag.Get("query")
if tag == "" {
continue
}
value := values.Get(tag)
fieldVal := outputElem.FieldByName(field.Name)
if field.Type.Kind() == reflect.Slice {
elemType := field.Type.Elem()
if elemType.Kind() != reflect.Struct {
prefix := tag + "."
arrIndex := 1
for {
currKey := prefix + fmt.Sprint(arrIndex)
currValue := values.Get(currKey)
if currValue == "" {
break
}
currSliceVal := reflect.ValueOf(currValue)
fieldVal.Set(reflect.Append(fieldVal, currSliceVal))
arrIndex++
}
} else {
prefix := tag + "."
objIndex := 1
outer := true
for outer {
innerValues := make(url.Values)
for innerKey, innerValue := range values {
if strings.HasPrefix(innerKey, prefix+strconv.Itoa(objIndex)+".") {
innerValues.Set(strings.TrimPrefix(innerKey, prefix+strconv.Itoa(objIndex)+"."), innerValue[0])
}
}
if len(innerValues) == 0 {
break
}
newStructPtr := reflect.New(elemType)
err := unmarshalData(innerValues, newStructPtr.Interface())
if err != nil {
return err
}
fieldVal.Set(reflect.Append(fieldVal, newStructPtr.Elem()))
objIndex++
}
}
} else {
fieldVal.Set(reflect.ValueOf(value))
}
}
return nil
}
func main() {
queryStr := "?Action=TagResources&RegionId=cn-hangzhou&ResourceId.1=i-bp1j6qtvdm8w0z1o0&ResourceId.2=i-bp1j6qtvdm8w0z1oP&ResourceType=instance&Tag.1.Key=TestKey&Tag.1.Value=TestValue&Tag.2.Key=TestKey&Tag.2.Value=TestValue"
req := TagRequest{}
err := Unmarshal(strings.TrimLeft(queryStr, "?"), &req)
if err != nil {
fmt.Println("Error:", err)
return
}
jsonOutput, _ := json.MarshalIndent(req, "", " ")
fmt.Println("Unmarshaled output:", string(jsonOutput))
}
测试一下:
% go run main.go
Unmarshaled output: {
"Action": "TagResources",
"RegionID": "cn-hangzhou",
"ResourceIds": [
"i-bp1j6qtvdm8w0z1o0",
"i-bp1j6qtvdm8w0z1oP"
],
"ResourceType": "instance",
"Tags": [
{
"Key": "TestKey",
"Value": "TestValue"
},
{
"Key": "TestKey",
"Value": "TestValue"
}
]
}
嗯,ChatGPT牛逼!为了方便大家使用,我创建了一个项目c0refast/aws-querystring,可以方便地作为库使用:
package main
import (
"encoding/json"
"fmt"
"net/url"
"github.com/c0refast/aws-querystring/query"
)
type TagRequest struct {
Action string `query:"Action"`
RegionID string `query:"RegionId"`
ResourceIds []string `query:"ResourceId"`
ResourceType string `query:"ResourceType"`
Tags []Tag `query:"Tag"`
}
type Tag struct {
Key string `query:"Key"`
Value string `query:"Value"`
}
func main() {
queryStr := "Action=TagResources&RegionId=cn-hangzhou&ResourceId.1=i-bp1j6qtvdm8w0z1o0&ResourceId.2=i-bp1j6qtvdm8w0z1oP&ResourceType=instance&Tag.1.Key=TestKey&Tag.1.Value=TestValue&Tag.2.Key=TestKey&Tag.2.Value=TestValue"
urlValues, _ := url.ParseQuery(queryStr)
req := TagRequest{}
err := query.BindQuery(urlValues, &req)
if err != nil {
fmt.Println("Error:", err)
return
}
jsonOutput, _ := json.MarshalIndent(req, "", " ")
fmt.Println("Unmarshaled output:", string(jsonOutput))
}