mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
298 lines
9.3 KiB
Go
298 lines
9.3 KiB
Go
// Package awsmodel loads the AWS api-2.json service definitions used by the
|
|
// conformance suite.
|
|
package awsmodel
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"slices"
|
|
"sync"
|
|
)
|
|
|
|
// SourceSDKVersion is the aws-sdk-go release whose module-cache models are
|
|
// used by the conformance suite.
|
|
const SourceSDKVersion = "v1.55.8"
|
|
|
|
const sourceSDKModule = "github.com/aws/aws-sdk-go"
|
|
|
|
// Service identifies an AWS service model loaded from the SDK module cache.
|
|
type Service string
|
|
|
|
const (
|
|
ACM Service = "acm"
|
|
EC2 Service = "ec2"
|
|
ECR Service = "ecr"
|
|
ECS Service = "ecs"
|
|
ElasticLoadBalancingV2 Service = "elasticloadbalancingv2"
|
|
IAM Service = "iam"
|
|
RDS Service = "rds"
|
|
S3 Service = "s3"
|
|
STS Service = "sts"
|
|
)
|
|
|
|
// Metadata describes the service and wire protocol represented by a model.
|
|
type Metadata struct {
|
|
APIVersion string `json:"apiVersion"`
|
|
EndpointPrefix string `json:"endpointPrefix"`
|
|
Protocol string `json:"protocol"`
|
|
Protocols []string `json:"protocols"`
|
|
ServiceAbbreviation string `json:"serviceAbbreviation"`
|
|
ServiceFullName string `json:"serviceFullName"`
|
|
ServiceID string `json:"serviceId"`
|
|
SignatureVersion string `json:"signatureVersion"`
|
|
UID string `json:"uid"`
|
|
}
|
|
|
|
// Operation describes an AWS API operation and the shapes used by its input,
|
|
// output and declared errors.
|
|
type Operation struct {
|
|
Name string `json:"name"`
|
|
HTTP HTTP `json:"http"`
|
|
Input *ShapeRef `json:"input"`
|
|
Output *ShapeRef `json:"output"`
|
|
Errors []ShapeRef `json:"errors"`
|
|
}
|
|
|
|
// HTTP describes an operation's HTTP binding.
|
|
type HTTP struct {
|
|
Method string `json:"method"`
|
|
RequestURI string `json:"requestUri"`
|
|
ResponseCode int `json:"responseCode"`
|
|
}
|
|
|
|
// Shape describes a value in an AWS service model. Depending on Type, it can
|
|
// refer to structure members, a list member, or map keys and values.
|
|
type Shape struct {
|
|
Type string `json:"type"`
|
|
Required []string `json:"required"`
|
|
Members map[string]ShapeRef `json:"members"`
|
|
Member *ShapeRef `json:"member"`
|
|
Key *ShapeRef `json:"key"`
|
|
Value *ShapeRef `json:"value"`
|
|
Enum []string `json:"enum"`
|
|
Min *float64 `json:"min"`
|
|
Max *float64 `json:"max"`
|
|
Pattern string `json:"pattern"`
|
|
LocationName string `json:"locationName"`
|
|
TimestampFormat string `json:"timestampFormat"`
|
|
Payload string `json:"payload"`
|
|
Flattened bool `json:"flattened"`
|
|
Sensitive bool `json:"sensitive"`
|
|
Exception bool `json:"exception"`
|
|
Fault bool `json:"fault"`
|
|
Error *ErrorInfo `json:"error"`
|
|
}
|
|
|
|
// ErrorInfo describes an error shape's code and HTTP classification.
|
|
type ErrorInfo struct {
|
|
Code string `json:"code"`
|
|
HTTPStatusCode int `json:"httpStatusCode"`
|
|
SenderFault bool `json:"senderFault"`
|
|
}
|
|
|
|
// ShapeRef names another shape and carries any wire binding specific to the
|
|
// place where it is referenced.
|
|
type ShapeRef struct {
|
|
Shape string `json:"shape"`
|
|
ResultWrapper string `json:"resultWrapper"`
|
|
Location string `json:"location"`
|
|
LocationName string `json:"locationName"`
|
|
QueryName string `json:"queryName"`
|
|
TimestampFormat string `json:"timestampFormat"`
|
|
Flattened bool `json:"flattened"`
|
|
Streaming bool `json:"streaming"`
|
|
XMLAttribute bool `json:"xmlAttribute"`
|
|
}
|
|
|
|
// Model is an indexed AWS service definition.
|
|
type Model struct {
|
|
service Service
|
|
metadata Metadata
|
|
operations map[string]*Operation
|
|
shapes map[string]*Shape
|
|
}
|
|
|
|
type modelDocument struct {
|
|
Version string `json:"version"`
|
|
Metadata Metadata `json:"metadata"`
|
|
Operations map[string]*Operation `json:"operations"`
|
|
Shapes map[string]*Shape `json:"shapes"`
|
|
}
|
|
|
|
type loadResult struct {
|
|
model *Model
|
|
err error
|
|
}
|
|
|
|
var modelCache sync.Map
|
|
|
|
var modelRoot = sync.OnceValues(resolveModelRoot)
|
|
|
|
func resolveModelRoot() (string, error) {
|
|
if root := os.Getenv("GOMODCACHE"); root != "" {
|
|
return filepath.Join(root, sourceSDKModule+"@"+SourceSDKVersion), nil
|
|
}
|
|
if output, err := exec.Command("go", "env", "GOMODCACHE").Output(); err == nil {
|
|
if root := string(output); root != "" {
|
|
return filepath.Join(stringTrimSpace(root), sourceSDKModule+"@"+SourceSDKVersion), nil
|
|
}
|
|
}
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return "", fmt.Errorf("awsmodel: resolve Go module cache: %w", err)
|
|
}
|
|
return filepath.Join(currentUser.HomeDir, "go", "pkg", "mod", sourceSDKModule+"@"+SourceSDKVersion), nil
|
|
}
|
|
|
|
func stringTrimSpace(value string) string {
|
|
for len(value) > 0 && (value[len(value)-1] == '\n' || value[len(value)-1] == '\r' || value[len(value)-1] == ' ' || value[len(value)-1] == '\t') {
|
|
value = value[:len(value)-1]
|
|
}
|
|
return value
|
|
}
|
|
|
|
// Services returns the supported service identifiers in stable order.
|
|
func Services() []Service {
|
|
services := make([]Service, 0, len(modelFiles))
|
|
for service := range modelFiles {
|
|
services = append(services, service)
|
|
}
|
|
slices.Sort(services)
|
|
return services
|
|
}
|
|
|
|
// Load parses and indexes the cached SDK model for service. Each service is
|
|
// parsed at most once and the resulting Model is safe for concurrent reads.
|
|
func Load(service Service) (*Model, error) {
|
|
path, ok := modelFiles[service]
|
|
if !ok {
|
|
return nil, fmt.Errorf("awsmodel: unsupported service %q", service)
|
|
}
|
|
|
|
loader, _ := modelCache.LoadOrStore(service, sync.OnceValue(func() loadResult {
|
|
root, err := modelRoot()
|
|
if err != nil {
|
|
return loadResult{err: err}
|
|
}
|
|
contents, err := os.ReadFile(filepath.Join(root, path))
|
|
if err != nil {
|
|
return loadResult{err: fmt.Errorf("awsmodel: read cached %s model at %s: %w", service, filepath.Join(root, path), err)}
|
|
}
|
|
|
|
var document modelDocument
|
|
if err := json.Unmarshal(contents, &document); err != nil {
|
|
return loadResult{err: fmt.Errorf("awsmodel: parse %s model: %w", service, err)}
|
|
}
|
|
if document.Version != "2.0" {
|
|
return loadResult{err: fmt.Errorf("awsmodel: %s model version is %q, want %q", service, document.Version, "2.0")}
|
|
}
|
|
if len(document.Operations) == 0 || len(document.Shapes) == 0 {
|
|
return loadResult{err: fmt.Errorf("awsmodel: %s model has no operations or shapes", service)}
|
|
}
|
|
|
|
model := &Model{
|
|
service: service,
|
|
metadata: document.Metadata,
|
|
operations: document.Operations,
|
|
shapes: document.Shapes,
|
|
}
|
|
if err := model.validateReferences(); err != nil {
|
|
return loadResult{err: err}
|
|
}
|
|
return loadResult{model: model}
|
|
}))
|
|
load, ok := loader.(func() loadResult)
|
|
if !ok {
|
|
return nil, fmt.Errorf("awsmodel: invalid cache entry for service %q", service)
|
|
}
|
|
result := load()
|
|
return result.model, result.err
|
|
}
|
|
|
|
// Service returns the identifier used to load the model.
|
|
func (m *Model) Service() Service { return m.service }
|
|
|
|
// Metadata returns the service metadata from api-2.json.
|
|
func (m *Model) Metadata() Metadata { return m.metadata }
|
|
|
|
// Operation resolves an operation by its model key, such as DescribeInstances.
|
|
func (m *Model) Operation(name string) (*Operation, bool) {
|
|
operation, ok := m.operations[name]
|
|
return operation, ok
|
|
}
|
|
|
|
// Shape resolves a shape by name.
|
|
func (m *Model) Shape(name string) (*Shape, bool) {
|
|
shape, ok := m.shapes[name]
|
|
return shape, ok
|
|
}
|
|
|
|
// Operations returns all operation keys in stable order.
|
|
func (m *Model) Operations() []string { return sortedKeys(m.operations) }
|
|
|
|
// Shapes returns all shape names in stable order.
|
|
func (m *Model) Shapes() []string { return sortedKeys(m.shapes) }
|
|
|
|
func sortedKeys[T any](values map[string]T) []string {
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
slices.Sort(keys)
|
|
return keys
|
|
}
|
|
|
|
func (m *Model) validateReferences() error {
|
|
validate := func(owner string, ref *ShapeRef) error {
|
|
if ref == nil {
|
|
return nil
|
|
}
|
|
if _, ok := m.shapes[ref.Shape]; !ok {
|
|
return fmt.Errorf("awsmodel: %s model %s references unknown shape %q", m.service, owner, ref.Shape)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for name, operation := range m.operations {
|
|
if operation == nil {
|
|
return fmt.Errorf("awsmodel: %s model operation %q is null", m.service, name)
|
|
}
|
|
if err := validate("operation "+name+" input", operation.Input); err != nil {
|
|
return err
|
|
}
|
|
if err := validate("operation "+name+" output", operation.Output); err != nil {
|
|
return err
|
|
}
|
|
for i := range operation.Errors {
|
|
if err := validate(fmt.Sprintf("operation %s error %d", name, i), &operation.Errors[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
for name, shape := range m.shapes {
|
|
if shape == nil {
|
|
return fmt.Errorf("awsmodel: %s model shape %q is null", m.service, name)
|
|
}
|
|
for memberName, member := range shape.Members {
|
|
if err := validate("shape "+name+" member "+memberName, &member); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for label, ref := range map[string]*ShapeRef{
|
|
"member": shape.Member,
|
|
"key": shape.Key,
|
|
"value": shape.Value,
|
|
} {
|
|
if err := validate("shape "+name+" "+label, ref); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|