反序列化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))
}