//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.` } } } }