mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
451 lines
12 KiB
Go
451 lines
12 KiB
Go
package awsmodel
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
// EncodedRequest is an operation input serialized for the service's protocol,
|
|
// ready to be addressed to an endpoint and signed.
|
|
type EncodedRequest struct {
|
|
Method string
|
|
// Path is escaped; it may carry a query string of its own.
|
|
Path string
|
|
Query url.Values
|
|
Header http.Header
|
|
Body []byte
|
|
}
|
|
|
|
// URL joins the request's path and query onto endpoint.
|
|
func (r *EncodedRequest) URL(endpoint string) string {
|
|
target := strings.TrimSuffix(endpoint, "/") + r.Path
|
|
if len(r.Query) == 0 {
|
|
return target
|
|
}
|
|
separator := "?"
|
|
if strings.Contains(r.Path, "?") {
|
|
separator = "&"
|
|
}
|
|
return target + separator + r.Query.Encode()
|
|
}
|
|
|
|
// EncodeRequest serializes input, as produced by GenerateRequests, for the
|
|
// service's protocol. S3's rest-xml is not supported.
|
|
func EncodeRequest(service Service, operationName string, input map[string]any) (*EncodedRequest, error) {
|
|
model, err := Load(service)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
operation, ok := model.Operation(operationName)
|
|
if !ok {
|
|
return nil, fmt.Errorf("awsmodel: %s operation %q is not modelled", service, operationName)
|
|
}
|
|
encoder := requestEncoder{model: model}
|
|
switch model.metadata.Protocol {
|
|
case "query", "ec2":
|
|
return encoder.query(operation, input)
|
|
case "json":
|
|
return encoder.json(operation, input)
|
|
case "rest-json":
|
|
return encoder.restJSON(operation, input)
|
|
default:
|
|
return nil, fmt.Errorf("awsmodel: encoding %s requests is not implemented", model.metadata.Protocol)
|
|
}
|
|
}
|
|
|
|
type requestEncoder struct {
|
|
model *Model
|
|
}
|
|
|
|
func (e requestEncoder) query(operation *Operation, input map[string]any) (*EncodedRequest, error) {
|
|
values := url.Values{
|
|
"Action": {operation.Name},
|
|
"Version": {e.model.metadata.APIVersion},
|
|
}
|
|
if operation.Input != nil {
|
|
if err := e.queryStructure(values, "", operation.Input.Shape, input); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &EncodedRequest{
|
|
Method: http.MethodPost,
|
|
Path: "/",
|
|
Header: http.Header{"Content-Type": {"application/x-www-form-urlencoded; charset=utf-8"}},
|
|
Body: []byte(values.Encode()),
|
|
}, nil
|
|
}
|
|
|
|
func (e requestEncoder) queryStructure(values url.Values, prefix, shapeName string, input map[string]any) error {
|
|
shape := e.model.shapes[shapeName]
|
|
for _, member := range slices.Sorted(maps.Keys(input)) {
|
|
ref, ok := shape.Members[member]
|
|
if !ok {
|
|
return fmt.Errorf("%s has no member %q", shapeName, member)
|
|
}
|
|
if err := e.queryValue(values, prefix+e.queryName(member, ref), ref, input[member]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// queryName is the parameter name for a member: the ec2 protocol capitalises
|
|
// its XML name unless ec2QueryName gives one; awsQuery uses the XML name.
|
|
func (e requestEncoder) queryName(member string, ref ShapeRef) string {
|
|
if e.model.metadata.Protocol != "ec2" {
|
|
if ref.LocationName != "" {
|
|
return ref.LocationName
|
|
}
|
|
return member
|
|
}
|
|
if ref.QueryName != "" {
|
|
return ref.QueryName
|
|
}
|
|
name := member
|
|
if ref.LocationName != "" {
|
|
name = ref.LocationName
|
|
}
|
|
runes := []rune(name)
|
|
runes[0] = unicode.ToUpper(runes[0])
|
|
return string(runes)
|
|
}
|
|
|
|
func (e requestEncoder) queryValue(values url.Values, name string, ref ShapeRef, value any) error {
|
|
shape := e.model.shapes[ref.Shape]
|
|
switch shape.Type {
|
|
case "structure":
|
|
fields, ok := value.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("%s: want a structure, got %T", name, value)
|
|
}
|
|
return e.queryStructure(values, name+".", ref.Shape, fields)
|
|
case "list":
|
|
items, ok := value.([]any)
|
|
if !ok {
|
|
return fmt.Errorf("%s: want a list, got %T", name, value)
|
|
}
|
|
if len(items) == 0 {
|
|
// awsQuery sends an empty list as an empty parameter; ec2 cannot.
|
|
if e.model.metadata.Protocol != "ec2" {
|
|
values.Set(name, "")
|
|
}
|
|
return nil
|
|
}
|
|
itemPrefix := name + "."
|
|
if e.model.metadata.Protocol != "ec2" && !ref.Flattened && !shape.Flattened {
|
|
memberName := "member"
|
|
if shape.Member.LocationName != "" {
|
|
memberName = shape.Member.LocationName
|
|
}
|
|
itemPrefix += memberName + "."
|
|
}
|
|
for i, item := range items {
|
|
if err := e.queryValue(values, itemPrefix+strconv.Itoa(i+1), *shape.Member, item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case "map":
|
|
entries, ok := value.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("%s: want a map, got %T", name, value)
|
|
}
|
|
entryPrefix := name + "."
|
|
if e.model.metadata.Protocol != "ec2" && !ref.Flattened && !shape.Flattened {
|
|
entryPrefix += "entry."
|
|
}
|
|
keyName, valueName := "key", "value"
|
|
if shape.Key.LocationName != "" {
|
|
keyName = shape.Key.LocationName
|
|
}
|
|
if shape.Value.LocationName != "" {
|
|
valueName = shape.Value.LocationName
|
|
}
|
|
for i, key := range slices.Sorted(maps.Keys(entries)) {
|
|
entry := entryPrefix + strconv.Itoa(i+1) + "."
|
|
values.Set(entry+keyName, key)
|
|
if err := e.queryValue(values, entry+valueName, *shape.Value, entries[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
default:
|
|
scalar, err := scalarString(value, ref, shape, "iso8601")
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
values.Set(name, scalar)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (e requestEncoder) json(operation *Operation, input map[string]any) (*EncodedRequest, error) {
|
|
body := []byte("{}")
|
|
if operation.Input != nil {
|
|
document, err := e.jsonValue(*operation.Input, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if body, err = json.Marshal(document); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
metadata := e.model.metadata
|
|
return &EncodedRequest{
|
|
Method: http.MethodPost,
|
|
Path: "/",
|
|
Header: http.Header{
|
|
"Content-Type": {"application/x-amz-json-" + metadata.JSONVersion},
|
|
"X-Amz-Target": {metadata.TargetPrefix + "." + operation.Name},
|
|
},
|
|
Body: body,
|
|
}, nil
|
|
}
|
|
|
|
// jsonValue converts a generated value into its JSON document form.
|
|
func (e requestEncoder) jsonValue(ref ShapeRef, value any) (any, error) {
|
|
shape := e.model.shapes[ref.Shape]
|
|
switch shape.Type {
|
|
case "structure":
|
|
fields, ok := value.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: want a structure, got %T", ref.Shape, value)
|
|
}
|
|
document := make(map[string]any, len(fields))
|
|
for member, field := range fields {
|
|
memberRef, ok := shape.Members[member]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s has no member %q", ref.Shape, member)
|
|
}
|
|
encoded, err := e.jsonValue(memberRef, field)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document[member] = encoded
|
|
}
|
|
return document, nil
|
|
case "list":
|
|
items, ok := value.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: want a list, got %T", ref.Shape, value)
|
|
}
|
|
document := make([]any, len(items))
|
|
for i, item := range items {
|
|
encoded, err := e.jsonValue(*shape.Member, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document[i] = encoded
|
|
}
|
|
return document, nil
|
|
case "map":
|
|
entries, ok := value.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: want a map, got %T", ref.Shape, value)
|
|
}
|
|
document := make(map[string]any, len(entries))
|
|
for key, entry := range entries {
|
|
encoded, err := e.jsonValue(*shape.Value, entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document[key] = encoded
|
|
}
|
|
return document, nil
|
|
case "timestamp":
|
|
stamp, ok := value.(time.Time)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: want a timestamp, got %T", ref.Shape, value)
|
|
}
|
|
if format := timestampFormatFor(ref, shape, "unixTimestamp"); format != "unixTimestamp" {
|
|
return formatTimestamp(stamp, format), nil
|
|
}
|
|
return stamp.Unix(), nil
|
|
case "blob":
|
|
data, ok := value.([]byte)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: want a blob, got %T", ref.Shape, value)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(data), nil
|
|
default:
|
|
return value, nil
|
|
}
|
|
}
|
|
|
|
func (e requestEncoder) restJSON(operation *Operation, input map[string]any) (*EncodedRequest, error) {
|
|
request := &EncodedRequest{Method: operation.HTTP.Method, Query: url.Values{}, Header: http.Header{}}
|
|
path := operation.HTTP.RequestURI
|
|
if operation.Input == nil {
|
|
request.Path = path
|
|
return request, nil
|
|
}
|
|
shape := e.model.shapes[operation.Input.Shape]
|
|
body := map[string]any{}
|
|
for _, member := range slices.Sorted(maps.Keys(input)) {
|
|
ref, ok := shape.Members[member]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s has no member %q", operation.Input.Shape, member)
|
|
}
|
|
value := input[member]
|
|
target := e.model.shapes[ref.Shape]
|
|
switch ref.Location {
|
|
case "uri":
|
|
text, err := scalarString(value, ref, target, "iso8601")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", member, err)
|
|
}
|
|
path = strings.Replace(path, "{"+member+"+}", escapeGreedyLabel(text), 1)
|
|
path = strings.Replace(path, "{"+member+"}", url.PathEscape(text), 1)
|
|
case "querystring":
|
|
if err := e.restQuery(request.Query, ref, target, value); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", member, err)
|
|
}
|
|
case "header":
|
|
text, err := e.headerValue(ref, target, value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", member, err)
|
|
}
|
|
request.Header.Set(ref.LocationName, text)
|
|
case "headers":
|
|
entries, _ := value.(map[string]any)
|
|
for key, entry := range entries {
|
|
request.Header.Set(ref.LocationName+key, fmt.Sprint(entry))
|
|
}
|
|
default:
|
|
encoded, err := e.jsonValue(ref, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name := member
|
|
if ref.LocationName != "" {
|
|
name = ref.LocationName
|
|
}
|
|
body[name] = encoded
|
|
}
|
|
}
|
|
request.Path = path
|
|
if payload := shape.Payload; payload != "" {
|
|
if document, ok := body[payload]; ok {
|
|
body = nil
|
|
encoded, err := json.Marshal(document)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request.Body = encoded
|
|
}
|
|
}
|
|
if len(body) > 0 {
|
|
encoded, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request.Body = encoded
|
|
}
|
|
if request.Body != nil {
|
|
request.Header.Set("Content-Type", "application/json")
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
func (e requestEncoder) restQuery(query url.Values, ref ShapeRef, shape *Shape, value any) error {
|
|
switch shape.Type {
|
|
case "list":
|
|
items, _ := value.([]any)
|
|
for _, item := range items {
|
|
text, err := scalarString(item, *shape.Member, e.model.shapes[shape.Member.Shape], "iso8601")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
query.Add(ref.LocationName, text)
|
|
}
|
|
return nil
|
|
case "map":
|
|
entries, _ := value.(map[string]any)
|
|
for key, entry := range entries {
|
|
query.Add(key, fmt.Sprint(entry))
|
|
}
|
|
return nil
|
|
default:
|
|
text, err := scalarString(value, ref, shape, "iso8601")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
query.Set(ref.LocationName, text)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (e requestEncoder) headerValue(ref ShapeRef, shape *Shape, value any) (string, error) {
|
|
if shape.Type == "list" {
|
|
items, _ := value.([]any)
|
|
texts := make([]string, len(items))
|
|
for i, item := range items {
|
|
text, err := scalarString(item, *shape.Member, e.model.shapes[shape.Member.Shape], "rfc822")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
texts[i] = text
|
|
}
|
|
return strings.Join(texts, ","), nil
|
|
}
|
|
return scalarString(value, ref, shape, "rfc822")
|
|
}
|
|
|
|
// escapeGreedyLabel escapes a greedy label's segments but keeps its slashes.
|
|
func escapeGreedyLabel(value string) string {
|
|
segments := strings.Split(value, "/")
|
|
for i, segment := range segments {
|
|
segments[i] = url.PathEscape(segment)
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|
|
|
|
func scalarString(value any, ref ShapeRef, shape *Shape, defaultTimestamp string) (string, error) {
|
|
switch value := value.(type) {
|
|
case string:
|
|
return value, nil
|
|
case bool:
|
|
return strconv.FormatBool(value), nil
|
|
case int64:
|
|
return strconv.FormatInt(value, 10), nil
|
|
case float64:
|
|
return strconv.FormatFloat(value, 'g', -1, 64), nil
|
|
case []byte:
|
|
return base64.StdEncoding.EncodeToString(value), nil
|
|
case time.Time:
|
|
return formatTimestamp(value, timestampFormatFor(ref, shape, defaultTimestamp)), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported scalar %T", value)
|
|
}
|
|
}
|
|
|
|
func timestampFormatFor(ref ShapeRef, shape *Shape, defaultFormat string) string {
|
|
if ref.TimestampFormat != "" {
|
|
return ref.TimestampFormat
|
|
}
|
|
if shape.TimestampFormat != "" {
|
|
return shape.TimestampFormat
|
|
}
|
|
return defaultFormat
|
|
}
|
|
|
|
func formatTimestamp(value time.Time, format string) string {
|
|
switch format {
|
|
case "rfc822":
|
|
return value.UTC().Format(http.TimeFormat)
|
|
case "unixTimestamp":
|
|
return strconv.FormatInt(value.Unix(), 10)
|
|
default:
|
|
return value.UTC().Format(time.RFC3339)
|
|
}
|
|
}
|