private/model/api/passes.go (461 lines of code) (raw):
//go:build codegen
// +build codegen
package api
import (
"fmt"
"regexp"
"strings"
"unicode"
)
func (a *API) enableGeneratedTypedErrors() {
switch a.Metadata.Protocol {
case "json":
case "rest-json":
default:
return
}
a.WithGeneratedTypedErrors = true
}
// updateTopLevelShapeReferences moves resultWrapper, locationName, and
// xmlNamespace traits from toplevel shape references to the toplevel
// shapes for easier code generation
func (a *API) updateTopLevelShapeReferences() {
for _, o := range a.Operations {
// these are for REST-XML services
if o.InputRef.LocationName != "" {
o.InputRef.Shape.LocationName = o.InputRef.LocationName
}
if o.InputRef.Location != "" {
o.InputRef.Shape.Location = o.InputRef.Location
}
if o.InputRef.Payload != "" {
o.InputRef.Shape.Payload = o.InputRef.Payload
}
if o.InputRef.XMLNamespace.Prefix != "" {
o.InputRef.Shape.XMLNamespace.Prefix = o.InputRef.XMLNamespace.Prefix
}
if o.InputRef.XMLNamespace.URI != "" {
o.InputRef.Shape.XMLNamespace.URI = o.InputRef.XMLNamespace.URI
}
}
}
// writeShapeNames sets each shape's API and shape name values. Binding the
// shape to its parent API. This will set OrigShapeName on each Shape and ShapeRef
// to allow access to the original shape name for code generation.
func (a *API) writeShapeNames() {
writeOrigShapeName := func(s *ShapeRef) {
if len(s.ShapeName) > 0 {
s.OrigShapeName = s.ShapeName
}
}
for n, s := range a.Shapes {
s.API = a
s.ShapeName, s.OrigShapeName = n, n
for _, ref := range s.MemberRefs {
writeOrigShapeName(ref)
}
writeOrigShapeName(&s.MemberRef)
writeOrigShapeName(&s.KeyRef)
writeOrigShapeName(&s.ValueRef)
}
}
func (a *API) resolveReferences() {
resolver := referenceResolver{API: a, visited: map[*ShapeRef]bool{}}
for _, s := range a.Shapes {
resolver.resolveShape(s)
}
for _, o := range a.Operations {
o.API = a // resolve parent reference
resolver.resolveReference(&o.InputRef)
resolver.resolveReference(&o.OutputRef)
// Resolve references for errors also
for i := range o.ErrorRefs {
resolver.resolveReference(&o.ErrorRefs[i])
o.ErrorRefs[i].Shape.Exception = true
o.ErrorRefs[i].Shape.ErrorInfo.Type = o.ErrorRefs[i].Shape.ShapeName
}
}
}
func (a *API) backfillErrorMembers() {
stubShape := &Shape{
ShapeName: "string",
Type: "string",
}
var locName string
switch a.Metadata.Protocol {
case "ec2", "query", "rest-xml":
locName = "Message"
case "json", "rest-json":
locName = "message"
}
for _, s := range a.Shapes {
if !s.Exception {
continue
}
var haveMessage bool
for name := range s.MemberRefs {
if strings.EqualFold(name, "Message") {
haveMessage = true
break
}
}
if !haveMessage {
ref := &ShapeRef{
ShapeName: stubShape.ShapeName,
Shape: stubShape,
LocationName: locName,
}
s.MemberRefs["Message"] = ref
stubShape.refs = append(stubShape.refs, ref)
}
}
if len(stubShape.refs) != 0 {
a.Shapes["SDKStubErrorMessageShape"] = stubShape
}
}
// A referenceResolver provides a way to resolve shape references to
// shape definitions.
type referenceResolver struct {
*API
visited map[*ShapeRef]bool
}
// resolveReference updates a shape reference to reference the API and
// its shape definition. All other nested references are also resolved.
func (r *referenceResolver) resolveReference(ref *ShapeRef) {
if ref.ShapeName == "" {
return
}
shape, ok := r.API.Shapes[ref.ShapeName]
if !ok {
panic(fmt.Sprintf("unable resolve reference, %s", ref.ShapeName))
}
if ref.JSONValue {
if err := setTargetAsJSONValue(r.API, "", "", ref); err != nil {
panic(fmt.Sprintf("failed to set reference as JSONValue, %v", err))
}
}
ref.API = r.API // resolve reference back to API
ref.Shape = shape // resolve shape reference
if r.visited[ref] {
return
}
r.visited[ref] = true
shape.refs = append(shape.refs, ref) // register the ref
// resolve shape's references, if it has any
r.resolveShape(shape)
}
func setTargetAsJSONValue(a *API, parentName, refName string, ref *ShapeRef) error {
ref.ShapeName = "JSONValue"
if _, ok := a.Shapes[ref.ShapeName]; !ok {
a.Shapes[ref.ShapeName] = &Shape{
API: a,
ShapeName: "JSONValue",
Type: "jsonvalue",
ValueRef: ShapeRef{
JSONValue: true,
},
}
}
return nil
}
// resolveShape resolves a shape's Member Key Value, and nested member
// shape references.
func (r *referenceResolver) resolveShape(shape *Shape) {
r.resolveReference(&shape.MemberRef)
r.resolveReference(&shape.KeyRef)
r.resolveReference(&shape.ValueRef)
for _, m := range shape.MemberRefs {
r.resolveReference(m)
}
}
// fixStutterNames fixes all name stuttering based on Go naming conventions.
// "Stuttering" is when the prefix of a structure or function matches the
// package name (case insensitive).
func (a *API) fixStutterNames() {
names, ok := legacyStutterNames[ServiceID(a)]
if !ok {
return
}
shapeNames := names.ShapeOrder
if len(shapeNames) == 0 {
shapeNames = make([]string, 0, len(names.Shapes))
for k := range names.Shapes {
shapeNames = append(shapeNames, k)
}
}
for _, shapeName := range shapeNames {
s := a.Shapes[shapeName]
newName := names.Shapes[shapeName]
if other, ok := a.Shapes[newName]; ok && (other.Type == "structure" || other.Type == "enum") {
panic(fmt.Sprintf(
"shape name already exists, renaming %v to %v\n",
s.ShapeName, newName))
}
s.Rename(newName)
}
for opName, newName := range names.Operations {
if _, ok := a.Operations[newName]; ok {
panic(fmt.Sprintf(
"operation name already exists, renaming %v to %v\n",
opName, newName))
}
op := a.Operations[opName]
delete(a.Operations, opName)
a.Operations[newName] = op
op.ExportedName = newName
}
}
// regexpForValidatingShapeName is used by validateShapeName to filter acceptable shape names
// that may be renamed to a new valid shape name, if not already.
// The regex allows underscores(_) at the beginning of the shape name
// There may be 0 or more underscores(_). The next character would be the leading character
// in the renamed shape name and thus, must be an alphabetic character.
// The regex allows alphanumeric characters along with underscores(_) in rest of the string.
var regexForValidatingShapeName = regexp.MustCompile("^[_]*[a-zA-Z][a-zA-Z0-9_]*$")
// legacyShapeNames is a map of shape names that are supported and bypass the validateShapeNames util
var legacyShapeNames = map[string][]string{
"mediapackage": {
"__AdTriggersElement",
"__PeriodTriggersElement",
},
}
// validateShapeNames is valid only for shapes of type structure or enums
// We validate a shape name to check if its a valid shape name
// A valid shape name would only contain alphanumeric characters and have an alphabet as leading character.
//
// If we encounter a shape name with underscores(_), we remove the underscores, and
// follow a canonical upper camel case naming scheme to create a new shape name.
// If the shape name collides with an existing shape name we return an error.
// The resulting shape name must be a valid shape name or throw an error.
func (a *API) validateShapeNames() error {
loop:
for _, s := range a.Shapes {
if s.Type == "structure" || s.IsEnum() {
for _, legacyname := range legacyShapeNames[a.PackageName()] {
if s.ShapeName == legacyname {
continue loop
}
}
name := s.ShapeName
if b := regexForValidatingShapeName.MatchString(name); !b {
return fmt.Errorf("invalid shape name found: %v", s.ShapeName)
}
// Slice of strings returned after we split a string
// with a non alphanumeric character as delimiter.
slice := strings.FieldsFunc(name, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
})
// Build a string that follows canonical upper camel casing
var b strings.Builder
for _, word := range slice {
b.WriteString(strings.Title(word))
}
name = b.String()
if s.ShapeName != name {
if a.Shapes[name] != nil {
// throw an error if shape with a new shape name already exists
return fmt.Errorf("attempt to rename shape %v to %v for package %v failed, as this rename would result in shape name collision",
s.ShapeName, name, a.PackageName())
}
debugLogger.Logf("Renaming shape %v to %v for package %v \n", s.ShapeName, name, a.PackageName())
s.Rename(name)
}
}
}
return nil
}
// renameExportable renames all operation names to be exportable names.
// All nested Shape names are also updated to the exportable variant.
func (a *API) renameExportable() {
for name, op := range a.Operations {
newName := a.ExportableName(name)
if newName != name {
delete(a.Operations, name)
a.Operations[newName] = op
}
op.ExportedName = newName
}
for k, s := range a.Shapes {
// FIXME SNS has lower and uppercased shape names with the same name,
// except the lowercased variant is used exclusively for string and
// other primitive types. Renaming both would cause a collision.
// We work around this by only renaming the structure shapes.
if s.Type == "string" {
continue
}
for mName, member := range s.MemberRefs {
newName := a.ExportableName(mName)
if newName != mName {
delete(s.MemberRefs, mName)
s.MemberRefs[newName] = member
// also apply locationName trait so we keep the old one
// but only if there's no locationName trait on ref or shape
if member.LocationName == "" && member.Shape.LocationName == "" {
member.LocationName = mName
}
}
member.OriginalMemberName = mName
if newName == "_" {
panic("Shape " + s.ShapeName + " uses reserved member name '_'")
}
}
newName := a.ExportableName(k)
if s.Type == "structure" && newName == a.StructName() {
// If struct collides client's struct type name the shape needs to
// be renamed with a trailing `_` to prevent collision.
newName += "_"
}
if newName != s.ShapeName {
s.Rename(newName)
}
s.Payload = a.ExportableName(s.Payload)
// fix required trait names
for i, n := range s.Required {
s.Required[i] = a.ExportableName(n)
}
}
for _, s := range a.Shapes {
// fix enum names
if s.IsEnum() {
s.EnumConsts = make([]string, len(s.Enum))
for i := range s.Enum {
shape := s.ShapeName
shape = strings.ToUpper(shape[0:1]) + shape[1:]
s.EnumConsts[i] = shape + s.EnumName(i)
}
}
}
}
// renameCollidingFields will rename any fields that uses an SDK or Golang
// specific name.
func (a *API) renameCollidingFields() {
for _, v := range a.Shapes {
namesWithSet := map[string]struct{}{}
for k, field := range v.MemberRefs {
if _, ok := v.MemberRefs["Set"+k]; ok {
namesWithSet["Set"+k] = struct{}{}
}
if collides(k) || (v.Exception && exceptionCollides(k)) {
renameCollidingField(k, v, field)
}
}
// checks if any field names collide with setters.
for name := range namesWithSet {
field := v.MemberRefs[name]
renameCollidingField(name, v, field)
}
}
}
func renameCollidingField(name string, v *Shape, field *ShapeRef) {
newName := name + "_"
debugLogger.Logf("Shape %s's field %q renamed to %q", v.ShapeName, name, newName)
delete(v.MemberRefs, name)
v.MemberRefs[newName] = field
// Set LocationName to the original field name if it is not already set.
// This is to ensure we correctly serialize to the proper member name
if len(field.LocationName) == 0 {
field.LocationName = name
}
}
// collides will return true if it is a name used by the SDK or Golang.
func collides(name string) bool {
switch name {
case "String",
"GoString",
"Validate":
return true
}
return false
}
func exceptionCollides(name string) bool {
switch name {
case "Code",
"Message",
"OrigErr",
"Error",
"String",
"GoString",
"RequestID",
"StatusCode",
"RespMetadata":
return true
}
return false
}
func (a *API) applyShapeNameAliases() {
service, ok := shapeNameAliases[a.name]
if !ok {
return
}
// Generic Shape Aliases
for name, s := range a.Shapes {
if alias, ok := service[name]; ok {
s.Rename(alias)
s.AliasedShapeName = true
}
}
}
// renameIOSuffixedShapeNames renames shapes that have `Input` or `Output`
// as suffix in their shape names. We add `_` and the end to avoid possible
// conflicts with the generated operation input/output types. SDK has already
// released quite a few shapes with input, output name suffixed with Input or Output.
// This change uses a legacy IO suffixed list and does not rename those legacy shapes.
// We do not rename aliased shapes. We do not rename shapes that are an input or output
// shape of an operation.
func (a *API) renameIOSuffixedShapeNames() {
// map all input shapes in service enclosure
inputShapes := make(map[string]*Shape, len(a.Operations))
// map all output shapes in service enclosure
outputShapes := make(map[string]*Shape, len(a.Operations))
for _, op := range a.Operations {
if len(op.InputRef.ShapeName) != 0 {
inputShapes[op.InputRef.Shape.ShapeName] = op.InputRef.Shape
}
if len(op.OutputRef.ShapeName) != 0 {
outputShapes[op.OutputRef.Shape.ShapeName] = op.OutputRef.Shape
}
}
for name, shape := range a.Shapes {
// skip if this shape is already aliased
if shape.AliasedShapeName {
continue
}
// skip if shape name is not suffixed with `Input` or `Output`
if !strings.HasSuffix(name, "Input") && !strings.HasSuffix(name, "Output") {
continue
}
// skip if this shape is an input shape
if s, ok := inputShapes[name]; ok && s == shape {
continue
}
// skip if this shape is an output shape
if s, ok := outputShapes[name]; ok && s == shape {
continue
}
// skip if this shape is a legacy io suffixed shape
if legacyIOSuffixed.LegacyIOSuffix(a, name) {
continue
}
// rename the shape to suffix with `_`
shape.Rename(name + "_")
}
}
// createInputOutputShapes creates toplevel input/output shapes if they
// have not been defined in the API. This normalizes all APIs to always
// have an input and output structure in the signature.
func (a *API) createInputOutputShapes() {
for _, op := range a.Operations {
createAPIParamShape(a, op.Name, &op.InputRef, op.ExportedName+"Input",
shamelist.Input,
)
op.InputRef.Shape.UsedAsInput = true
createAPIParamShape(a, op.Name, &op.OutputRef, op.ExportedName+"Output",
shamelist.Output,
)
op.OutputRef.Shape.UsedAsOutput = true
}
}
func (a *API) renameAPIPayloadShapes() {
for _, op := range a.Operations {
op.InputRef.Payload = a.ExportableName(op.InputRef.Payload)
op.OutputRef.Payload = a.ExportableName(op.OutputRef.Payload)
}
}
func createAPIParamShape(a *API, opName string, ref *ShapeRef, shapeName string, shamelistLookup func(string, string) bool) {
if len(ref.ShapeName) == 0 {
setAsPlacholderShape(ref, shapeName, a)
return
}
// nothing to do if already the correct name.
if s := ref.Shape; s.AliasedShapeName || s.ShapeName == shapeName || shamelistLookup(a.name, opName) {
return
}
if s, ok := a.Shapes[shapeName]; ok {
panic(fmt.Sprintf(
"attempting to create duplicate API parameter shape, %v, %v, %v, %v\n",
shapeName, opName, ref.ShapeName, s.OrigShapeName,
))
}
ref.Shape.removeRef(ref)
ref.ShapeName = shapeName
ref.Shape = ref.Shape.Clone(shapeName)
ref.Shape.refs = append(ref.Shape.refs, ref)
}
func setAsPlacholderShape(tgtShapeRef *ShapeRef, name string, a *API) {
shape := a.makeIOShape(name)
shape.Placeholder = true
*tgtShapeRef = ShapeRef{API: a, ShapeName: shape.ShapeName, Shape: shape}
shape.refs = append(shape.refs, tgtShapeRef)
}
// makeIOShape returns a pointer to a new Shape initialized by the name provided.
func (a *API) makeIOShape(name string) *Shape {
shape := &Shape{
API: a, ShapeName: name, Type: "structure",
MemberRefs: map[string]*ShapeRef{},
}
a.Shapes[name] = shape
return shape
}
// removeUnusedShapes removes shapes from the API which are not referenced by
// any other shape in the API.
func (a *API) removeUnusedShapes() {
for _, s := range a.Shapes {
if len(s.refs) == 0 {
a.removeShape(s)
}
}
}
// Sets the EndpointsID field of Metadata with the value of the
// EndpointPrefix if EndpointsID is not set.
func (a *API) setMetadataEndpointsKey() {
if len(a.Metadata.EndpointsID) != 0 {
return
}
a.Metadata.EndpointsID = a.Metadata.EndpointPrefix
}
func (a *API) findEndpointDiscoveryOp() {
for _, op := range a.Operations {
if op.IsEndpointDiscoveryOp {
a.EndpointDiscoveryOp = op
return
}
}
}
func (a *API) injectUnboundedOutputStreaming() {
for _, op := range a.Operations {
if op.AuthType != V4UnsignedBodyAuthType {
continue
}
for _, ref := range op.InputRef.Shape.MemberRefs {
if ref.Streaming || ref.Shape.Streaming {
if len(ref.Documentation) != 0 {
ref.Documentation += `
//`
}
ref.Documentation += `
// To use an non-seekable io.Reader for this request wrap the io.Reader with
// "aws.ReadSeekCloser". The SDK will not retry request errors for non-seekable
// readers. This will allow the SDK to send the reader's payload as chunked
// transfer encoding.`
}
}
}
}