using Json.LitJson; using ServiceClientGenerator.Endpoints; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ServiceClientGenerator { /// <summary> /// An object that represents the operations for a service api /// </summary> public class Operation : BaseModel { /// <summary> /// The name of the operation as found in the json model /// </summary> readonly string name; private OperationPaginatorConfig _operationPaginatorConfig; /// <summary> /// Constructs on operation from a service model, operation name, and the json model /// </summary> /// <param name="model">The model of the service the operation belongs to</param> /// <param name="name">The name of the operation</param> /// <param name="data">The json data from the model file</param> public Operation(ServiceModel model, string name, JsonData data) : base(model, data) { this.model = model; this.name = name; this.data = data; } /// <summary> /// Constructs on operation from a service model, operation name, the json model, and the json paginators /// </summary> /// <param name="model">The model of the service the operation belongs to</param> /// <param name="name">The name of the operation</param> /// <param name="data">The json data from the model file</param> /// <param name="paginatorData">The json data from the paginators file</param> public Operation(ServiceModel model, string name, JsonData data, JsonData paginatorData) : this(model, name, data) { if (paginatorData[ServiceModel.OutputTokenKey] != null && paginatorData[ServiceModel.InputTokenKey] != null) { _operationPaginatorConfig = new OperationPaginatorConfig(this, name, paginatorData); } } /// <summary> /// The name of the operation determined by checking if there are any customizations for a change of the name /// then using the provided name if there isn't a custom name. /// </summary> public string Name { get { var modifiers = this.model.Customizations.GetOperationModifiers(ShapeName); if (modifiers != null && !string.IsNullOrEmpty(modifiers.Name)) return modifiers.Name; return ShapeName; } } /// <summary> /// Returns the raw shape name without customization /// </summary> public string ShapeName { get { return this.name; } } public override string Documentation { get { var modifiers = this.model.Customizations.GetOperationModifiers(this.name); if (modifiers != null && !string.IsNullOrEmpty(modifiers.Documentation)) return modifiers.Documentation; return base.Documentation; } } /// <summary> /// Determines if the operation is Deprecated. /// </summary> public bool IsDeprecated { get { if (data[ServiceModel.DeprecatedKey] != null && data[ServiceModel.DeprecatedKey].IsBoolean) return (bool)data[ServiceModel.DeprecatedKey]; return false; } } /// <summary> /// Returns the deprecation message specified in the model or in the customization file. /// </summary> public string DeprecationMessage { get { string message = this.model.Customizations.GetOperationModifiers(this.name)?.DeprecatedMessage ?? data[ServiceModel.DeprecatedMessageKey].CastToString(); if (message == null) throw new Exception(string.Format("The 'message' property of the 'deprecated' trait is missing for operation {0}.\nFor example: \"OperationName\":{{\"name\":\"OperationName\", ... \"deprecated\":true, \"deprecatedMessage\":\"This operation is deprecated\"}}", this.name)); return message; } } /// <summary> /// Determines if a checksum needs to be sent in the Content-MD5 header. /// Checks both the older "httpChecksumRequired" property as well as the newer /// "httpChecksum.requestChecksumRequired" property /// </summary> public bool HttpChecksumRequired { get { if (data[ServiceModel.HttpChecksumRequiredKey] != null && data[ServiceModel.HttpChecksumRequiredKey].IsBoolean) { if ((bool)data[ServiceModel.HttpChecksumRequiredKey]) { return true; } } if (ChecksumConfiguration != null && ChecksumConfiguration.RequestChecksumRequired) { return true; } return false; } } /// <summary> /// Request and response flexible checksum configuration, read from the "httpChecksum" object /// </summary> public ChecksumConfiguration ChecksumConfiguration { get { if (data[ServiceModel.HttpChecksumKey] != null) return new ChecksumConfiguration(data[ServiceModel.HttpChecksumKey]); return null; } } /// <summary> /// Determines whether this operation's marshaller needs to call the checksum /// handling in Core. This means it either requires a MD5 checksum and/or supports /// flexible checksums. /// </summary> public bool RequiresChecksumDuringMarshalling { get { if (HttpChecksumRequired) { return true; } if (!string.IsNullOrEmpty(ChecksumConfiguration?.RequestAlgorithmMember)) { return true; } return false; } } /// <summary> /// Determines if the operation is customized to be only internally accessible. /// </summary> public bool IsInternal { get { var modifiers = this.model.Customizations.GetOperationModifiers(this.name); if (modifiers != null) return modifiers.IsInternal; return false; } } /// <summary> /// Determines if the operation is to be excluded from generation /// </summary> public bool IsExcluded { get { var modifiers = this.model.Customizations.GetOperationModifiers(this.name); if (modifiers != null) return modifiers.IsExcluded; return false; } } // set if we need to 'demote' the response output members into a shape hosted // within a result class that doesn't exist in the service model. As an example // EC2's DescribeImageAttribute outputs a response with multiple members. We take // those and put them into an ImageAttribute shape that is referenced by the // DescribeImageAttributeResult class (which doesn't exist in EC2's model). public bool UseWrappingResult { get { var modifiers = this.model.Customizations.GetOperationModifiers(this.name); if (modifiers != null) return modifiers.UseWrappingResult; return false; } } /// <summary> /// Allows the operation to return an empty result when the operation is modeled to return body result structure. /// </summary> public bool AllowEmptyResult { get { var modifiers = this.model.Customizations.GetOperationModifiers(this.name); if (modifiers != null) return modifiers.AllowEmptyResult; return false; } } public bool WrapsResultShape(string shapeName) { var modifiers = this.model.Customizations.GetOperationModifiers(this.name); if (modifiers != null && modifiers.UseWrappingResult) { var wrappedShape = modifiers.WrappedResultShape; return (!string.IsNullOrEmpty(wrappedShape) && wrappedShape.Equals(shapeName, StringComparison.Ordinal)); } return false; } /// <summary> /// If the operation uses a payload member, return that member so that it can be generated for the operation as well. /// Payloads are place holders in the json models. They are represented as another shape and contain the actual necessary attributes. /// </summary> public Member RequestPayloadMember { get { if (this.RequestStructure != null) { var payload = this.RequestStructure.PayloadMemberName; if (!string.IsNullOrWhiteSpace(payload)) { return this.RequestStructure.Members.Single(m => m.MarshallName.Equals(payload, StringComparison.InvariantCultureIgnoreCase)); } } return null; } } /// <summary> /// The response payload member, null if one does not exist. /// </summary> public Member ResponsePayloadMember { get { if (this.ResponseStructure != null) { var payload = this.ResponseStructure.PayloadMemberName; if (!string.IsNullOrWhiteSpace(payload)) { return this.ResponseStructure.Members.Single(m => m.MarshallName.Equals(payload, StringComparison.InvariantCultureIgnoreCase)); } } return null; } } /// <summary> /// Gets the namespace of the payload for an XML object /// </summary> public string XmlNamespace { get { if (this.RequestPayloadMember != null) { return RequestPayloadMember.XmlNamespace; } return this.Input.XmlNamespace; } } /// <summary> /// A list of the members that are in the request header, empty if none /// </summary> public IList<Member> RequestHeaderMembers { get { return this.RequestStructure == null ? new List<Member>() : this.RequestStructure.Members.Where( m => m.MarshallLocation == MarshallLocation.Header || m.MarshallLocation == MarshallLocation.Headers).ToList(); } } /// <summary> /// A list of members that are in the response header, empty if none /// </summary> public IList<Member> ResponseHeaderMembers { get { return this.ResponseStructure == null ? new List<Member>() : this.ResponseStructure.Members.Where( m => m.MarshallLocation == MarshallLocation.Header || m.MarshallLocation == MarshallLocation.Headers).ToList(); } } /// <summary> /// A member that is defined by the status code of the response, null if none /// </summary> public Member ResponseStatusCodeMember { get { return this.ResponseStructure == null ? null : this.ResponseStructure.Members.SingleOrDefault( m => m.MarshallLocation == MarshallLocation.StatusCode); } } /// <summary> /// A member in the response that is sent as a stream, null if none /// </summary> public Member ResponseStreamingMember { get { return this.ResponseStructure == null ? null : this.ResponseStructure.Members.SingleOrDefault(m => m.IsStreaming); } } /// <summary> /// Members that are part of the URI for the request, empty if none /// </summary> public IList<Member> RequestUriMembers { get { return this.RequestStructure == null ? new List<Member>() : this.RequestStructure.Members.Where( m => m.MarshallLocation == MarshallLocation.Uri).ToList(); } } /// <summary> /// List of members that are part of the QueryString, empty if none /// </summary> public IList<Member> RequestQueryStringMembers { get { return this.RequestStructure == null ? new List<Member>() : this.RequestStructure.Members.Where( m => m.MarshallLocation == MarshallLocation.QueryString).ToList(); } } /// <summary> /// A member in the request that is sent as a stream, null if none /// </summary> public Member RequestStreamingMember { get { return this.RequestStructure == null ? null : this.RequestStructure.Members.SingleOrDefault(m => m.IsStreaming); } } /// <summary> /// Members who are part of the request's body /// </summary> public IList<Member> RequestBodyMembers { get { if (this.RequestStructure == null) return new List<Member>(); var payloadName = this.RequestStructure.PayloadMemberName; return this.RequestStructure.Members.Where( m => m.MarshallLocation == MarshallLocation.Body && !string.Equals(m.MarshallName, payloadName, StringComparison.Ordinal)).ToList(); } } /// <summary> /// Members who are part of the response's body /// </summary> public IList<Member> ResponseBodyMembers { get { if (this.ResponseStructure == null) return new List<Member>(); var payloadName = this.ResponseStructure.PayloadMemberName; return this.ResponseStructure.Members.Where( m => m.MarshallLocation == MarshallLocation.Body && !string.Equals(m.MarshallName, payloadName, StringComparison.Ordinal)).ToList(); } } /// <summary> /// List of members that are decorated with a hostLabel value equal to true /// </summary> public IEnumerable<Member> RequestHostPrefixMembers { get { return this.RequestStructure == null ? new List<Member>() : this.RequestStructure.Members.Where(m => m.IsHostLabel); } } public bool IsEventStreamOutput => ResponseStructure?.Members?.Any( member => member.Shape?.IsEventStream ?? false) ?? false; /// <summary> /// Determines if the request structure will have members in the header /// </summary> public bool RequestHasHeaderMembers { get { return (this.RequestHeaderMembers.Count > 0); } } /// <summary> /// Determines if the request structure will have members in the body /// </summary> public bool RequestHasBodyMembers { get { // Has any members which are marshalled as part of the request body return (this.RequestBodyMembers.Count > 0); } } /// <summary> /// Determines if the request structure has uri members /// </summary> public bool RequestHasUriMembers { get { return (this.RequestUriMembers.Count > 0); } } /// <summary> /// Determines if the request structure has query string members /// </summary> public bool RequestHasQueryStringMembers { get { // Has any members which are marshalled as part of the request body return this.RequestStructure != null && this.RequestStructure.Members.Any(m => m.MarshallLocation == MarshallLocation.QueryString); } } /// <summary> /// Determines if the response structure will have members in the body /// </summary> public bool ResponseHasBodyMembers { get { // Has any members which are marshalled as part of the response body return (this.ResponseBodyMembers.Count > 0); } } /// <summary> /// Determines if the response structure will have members in the header /// </summary> public bool ResponseHasHeaderMembers { get { return (this.ResponseHeaderMembers.Count > 0); } } /// <summary> /// Use query string if there are body members or a streamed member /// </summary> public bool UseQueryString { get { return this.RequestStructure != null && this.RequestStructure.Members.Any(m => m.MarshallLocation == MarshallLocation.QueryString); } } /// <summary> /// The input of the operation, a shape for a request structure /// </summary> public OperationInput Input { get { JsonData inputNode = this.data[ServiceModel.InputKey]; if (inputNode == null) return null; return new OperationInput(this.model, inputNode); } } public OperationAuthType? AuthType { get { return OperationAuthTypeParser.Parse(this.data[ServiceModel.AuthTypeKey]); } } /// <summary> /// The method of the operation (i.e. POST, GET, ...) /// </summary> public string HttpMethod { get { JsonData httpNode = this.data[ServiceModel.HttpKey]; if (httpNode == null) return string.Empty; JsonData methodNode = httpNode[ServiceModel.MethodKey]; if (methodNode == null) return string.Empty; return methodNode.ToString(); } } /// <summary> /// The endpoint hostPrefix of the operation /// </summary> public string EndpointHostPrefix { get { JsonData endpointNode = this.data[ServiceModel.EndpointKey]; if (endpointNode == null) return string.Empty; return endpointNode[ServiceModel.HostPrefixKey]?.ToString() ?? string.Empty; } } /// <summary> /// The endpointoperation flag marking if this is the operation to use for endpoint discovery. /// </summary> public bool IsEndpointOperation { get { return (bool)(this.data[ServiceModel.EndpointOperationKey] ?? false); } } /// <summary> /// The endpointdiscovery flag specifying if this operation is to use endpoint discovery. /// </summary> public bool EndpointDiscoveryEnabled { get { return this.data[ServiceModel.EndpointDiscoveryKey] != null ? true : false; } } /// <summary> /// The endpointdiscovery required flag specifying if this operation is required use endpoint discovery. /// </summary> public bool IsEndpointDiscoveryRequired { get { JsonData endpointDiscovery = this.data[ServiceModel.EndpointDiscoveryKey]; if (endpointDiscovery == null) return false; JsonData required = endpointDiscovery[ServiceModel.RequiredKey]; if (required == null) return false; if (required.IsBoolean) return (bool)(required ?? false); return Convert.ToBoolean((string)required); } } /// <summary> /// Members that are marked with the "endpointdiscoveryid" = true trait /// </summary> public IList<Member> RequestEndpointDiscoveryIdMembers { get { if (this.RequestStructure == null) return new List<Member>(); return this.RequestStructure.Members.Where(m => m.IsEndpointDiscoveryId && m.GetPrimitiveType() == "String").ToList(); } } /// <summary> /// Determines if the structure has any members that are marked with the "endpointdiscoveryid" = true trait /// </summary> public bool RequestHasEndpointDiscoveryIdMembers { get { return (this.RequestEndpointDiscoveryIdMembers.Count > 0); } } /// <summary> /// Determines if there is an Operation member for the operation with the request structure marked with "endpointoperation" = true. /// </summary> public bool RequestHasOperationEndpointOperationMember { get { if (this.RequestStructure == null) return false; return this.RequestStructure.Members.FirstOrDefault(m => m.PropertyName == "Operation") != null; } } /// <summary> /// Determines if there is an Identifiers member for the operation with the request structure marked with "endpointoperation" = true. /// </summary> public bool RequestHasIdentifiersEndpointOperationMember { get { if (this.RequestStructure == null) return false; return this.RequestStructure.Members.FirstOrDefault(m => m.PropertyName == "Operation") != null; } } /// <summary> /// Returns the Request URI without any query parameters. /// </summary> public string RequestUri { get { return this.RequestUriRaw.Split(new char[] { '?' }, 2)[0]; } } /// <summary> /// Returns the static query parameters which are specified in the RequestUri itself. /// Example : Cloud Search Domain's Search operation (/2013-01-01/search?format=sdk&pretty=true). /// </summary> public Dictionary<string, string> StaticQueryParameters { get { var queryParams = new Dictionary<string, string>(); var segments = this.RequestUriRaw.Split(new char[] { '?' }, 2); if (segments.Count() > 1) { var staticQueryParams = segments[1]; foreach (string s in staticQueryParams.Split('&', ';')) { string[] nameValuePair = s.Split(new char[] { '=' }, 2); if (nameValuePair.Length == 2 && nameValuePair[1].Length > 0) { queryParams.Add(nameValuePair[0], nameValuePair[1]); } else { queryParams.Add(nameValuePair[0], null); } } } return queryParams; } } private string RequestUriRaw { get { JsonData httpNode = this.data[ServiceModel.HttpKey]; if (httpNode == null) return string.Empty; JsonData requestUri = httpNode[ServiceModel.RequestUriKey]; if (requestUri == null) return string.Empty; return requestUri.ToString(); } } /// <summary> /// The parent xml node for the response data coming back from the service. /// </summary> public string ResultWrapper { get { JsonData output = this.data[ServiceModel.OutputKey]; if (output == null) return string.Empty; JsonData wrapper = output[ServiceModel.ResultWrapperKey]; if (wrapper == null) return string.Empty; return wrapper.ToString(); } } /// <summary> /// The list of errors that the service returns which will be turned into exceptions. /// </summary> public IList<ExceptionShape> Exceptions { get { var hashSet = new HashSet<ExceptionShape>(); var errors = this.data[ServiceModel.ErrorsKey]; if (errors != null && errors.IsArray) { foreach (JsonData error in errors) { var extendsNode = error[ServiceModel.ShapeKey]; if (extendsNode == null) continue; var structure = this.model.FindShape(extendsNode.ToString()); hashSet.Add((ExceptionShape)structure); } } return hashSet.OrderBy(x => x.Name).ToList(); } } /// <summary> /// The structure of the request for the operation as a shape, used to generate the request object /// </summary> public Shape RequestStructure { get { var inputNode = this.data[ServiceModel.InputKey]; if (inputNode == null) return null; var structure = this.model.FindShape(inputNode[ServiceModel.ShapeKey].ToString()); return structure; } } /// <summary> /// The shape of the response for the operation as a shape, used to generate the response object /// </summary> public Shape ResponseStructure { get { var outputNode = this.data[ServiceModel.OutputKey]; if (outputNode == null) return null; var structure = this.model.FindShape(outputNode[ServiceModel.ShapeKey].ToString()); return structure; } } /// <summary> /// For Set to true when the service model specifies a shape that should be wrapped in a response. /// ElastiCache CreateCacheCluster is an example of this. /// </summary> public bool IsResponseWrapped { get { var outputNode = this.data[ServiceModel.OutputKey]; if (outputNode == null) return false; var wrappedNode = outputNode[ServiceModel.WrapperKey]; if (wrappedNode == null) return false; return bool.Parse(wrappedNode.ToString()); } } public IList<Example> Examples { get { var list = new List<Example>(); var data = this.model.Customizations.GetExamples(this.name); foreach(JsonData example in data) { list.Add(new Example(this.model, this.name, example)); } return list; } } public bool HasExamples { get { return this.model.Customizations.GetExamples(this.Name).Count > 0; } } /// <summary> /// Determines if a given member should be treated as a greedy path, meaning /// that the resource path contains {MEMBER_NAME+} instead of simply {MEMBER_NAME}. /// </summary> /// <param name="member"></param> /// <returns></returns> public string GetUriResourcePathTarget(Member member,out bool isGreedy) { var greedyPathResourcePathIdentifier = "{" + member.MarshallLocationName + "+}"; var simplePathResourcePathIdentifier = "{" + member.MarshallLocationName + "}"; isGreedy = this.RequestUri.IndexOf(greedyPathResourcePathIdentifier, StringComparison.Ordinal) >= 0; var isSimple = this.RequestUri.IndexOf(simplePathResourcePathIdentifier, StringComparison.Ordinal) >= 0; if (isGreedy && isSimple) throw new Exception(string.Format("Unexpected behavior/model, member {1} of operation {0} is both a simple and a greedy parameter", this.Name, member.PropertyName)); else if (!isGreedy && !isSimple) throw new Exception(string.Format("Unexpected behavior/model, member {1} of operation {0} is neither a simple nor a greedy parameter", this.Name, member.PropertyName)); else if (isGreedy) return greedyPathResourcePathIdentifier; else return simplePathResourcePathIdentifier; } /// <summary> /// Represents the operation as a string /// </summary> /// <returns>The name of the operation, customized if specified</returns> public override string ToString() { return Name; } /// <summary> /// Returns any operation modifiers that have been set for this operation. /// </summary> public CustomizationsModel.OperationModifiers OperationModifiers { get { return this.model.Customizations.GetOperationModifiers(this.Name); } } public string RestAPIDocUrl { get { string serviceId = this.model.ServiceUid; if (!string.IsNullOrEmpty(serviceId)) { return string.Format(@"http://docs.aws.amazon.com/goto/WebAPI/{0}/{1}", serviceId, ShapeName); } else { return null; } } } /// <summary> /// Paginators for an operation /// </summary> public OperationPaginatorConfig Paginators { get { return this._operationPaginatorConfig; } } /// <summary> /// Whether or not this operation has properly configured /// paginators based on provided json configuration /// </summary> public bool UnsupportedPaginatorConfig { get; set; } private static ConcurrentDictionary<string, bool> _checkedService = new ConcurrentDictionary<string, bool>(); /// <summary> /// Gets list of static context parameters, used on operation to drive endpoint resolution /// </summary> public List<StaticContextParameter> StaticContextParameters { get { var result = new List<StaticContextParameter>(); var parameters = data.SafeGet("staticContextParams"); if (parameters != null) { foreach (var param in parameters.GetMap()) { result.Add(new StaticContextParameter { name = param.Key, value = param.Value["value"] }); } } return result; } } } }