/// <summary> /// Fetches the value of "summary" tag from xml documentation and populates operation's summary. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <returns>The list of generation errors, if any produced when processing the filter.</returns> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public IList <GenerationError> Apply( OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var generationErrors = new List <GenerationError>(); try { var summaryElement = element.Descendants().FirstOrDefault(i => i.Name == KnownXmlStrings.Summary); if (summaryElement == null) { return(generationErrors); } if (string.IsNullOrWhiteSpace(operation.Summary)) { operation.Summary = summaryElement.GetDescriptionText(); } } catch (Exception ex) { generationErrors.Add( new GenerationError { Message = ex.Message, ExceptionType = ex.GetType().Name }); } return(generationErrors); }
/// <summary> /// Fetches the value of "security" tags from xml documentation and populates operation's security requirement /// values. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <returns>The list of generation errors, if any produced when processing the filter.</returns> public IList <GenerationError> Apply( OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var generationErrors = new List <GenerationError>(); try { if (settings == null) { throw new ArgumentNullException(nameof(settings)); } if (element == null) { throw new ArgumentNullException(nameof(element)); } if (operation == null) { throw new ArgumentNullException(nameof(operation)); } var securityElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Security); if (!securityElements.Any()) { return(generationErrors); } var openApiSecurityRequirement = new OpenApiSecurityRequirement(); var securitySchemeRegistry = settings.ReferenceRegistryManager.SecuritySchemeReferenceRegistry; foreach (var securityElement in securityElements) { var securityScheme = securitySchemeRegistry.FindOrAddReference(securityElement); openApiSecurityRequirement.Add(securityScheme, securitySchemeRegistry.Scopes); } operation.Security.Add(openApiSecurityRequirement); } catch (Exception ex) { generationErrors.Add( new GenerationError { Message = ex.Message, ExceptionType = ex.GetType().Name }); } return(generationErrors); }
/// <summary> /// Fetches the value of "summary" tag from xml documentation and populates operation's summary. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var summaryElement = element.Descendants().FirstOrDefault(i => i.Name == KnownXmlStrings.Summary); if (summaryElement == null) { return; } if (string.IsNullOrWhiteSpace(operation.Summary)) { operation.Summary = summaryElement.GetDescriptionText(); } }
/// <summary> /// Fetches the value of "remarks" tag from xml documentation and populates operation's description. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { string description = null; var remarksElement = element.Descendants().FirstOrDefault(i => i.Name == KnownXmlStrings.Remarks); if (remarksElement != null) { description = remarksElement.GetDescriptionText(); } if (string.IsNullOrWhiteSpace(operation.Description)) { operation.Description = description; } }
/// <summary> /// Fetches the value of "group" tag from xml documentation and populates operation's tag. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var groupElement = element.Descendants().FirstOrDefault(i => i.Name == KnownXmlStrings.Group); var groupValue = groupElement?.Value.Trim(); if (string.IsNullOrWhiteSpace(groupValue)) { return; } if (!operation.Tags.Select(t => t.Name).Contains(groupValue)) { operation.Tags.Add( new OpenApiTag { Name = groupValue } ); } }
/// <summary> /// Fetches the value of "group" tag from xml documentation and populates operation's tag. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> /// <returns>The list of generation errors, if any produced when processing the filter.</returns> public IList <GenerationError> Apply( OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var generationErrors = new List <GenerationError>(); try { var groupElement = element.Descendants().FirstOrDefault(i => i.Name == KnownXmlStrings.Group); var groupValue = groupElement?.Value.Trim(); if (string.IsNullOrWhiteSpace(groupValue)) { return(generationErrors); } if (!operation.Tags.Select(t => t.Name).Contains(groupValue)) { operation.Tags.Add( new OpenApiTag { Name = groupValue } ); } } catch (Exception ex) { generationErrors.Add( new GenerationError { Message = ex.Message, ExceptionType = ex.GetType().Name }); } return(generationErrors); }
/// <summary> /// Fetches the value of "security" tags from xml documentation and populates operation's security requirement /// values. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { if (settings == null) { throw new ArgumentNullException(nameof(settings)); } if (element == null) { throw new ArgumentNullException(nameof(element)); } if (operation == null) { throw new ArgumentNullException(nameof(operation)); } var securityElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Security); if (!securityElements.Any()) { return; } var openApiSecurityRequirement = new OpenApiSecurityRequirement(); var securitySchemeRegistry = settings.ReferenceRegistryManager.SecuritySchemeReferenceRegistry; foreach (var securityElement in securityElements) { var securityScheme = securitySchemeRegistry.FindOrAddReference(securityElement); openApiSecurityRequirement.Add(securityScheme, securitySchemeRegistry.Scopes); } operation.Security.Add(openApiSecurityRequirement); }
/// <summary> /// Fetches the value of "param" tags from xml documentation with in valus of "body" /// and populates operation's request body. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <returns>The list of generation errors, if any produced when processing the filter.</returns> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public IList <GenerationError> Apply( OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var generationErrors = new List <GenerationError>(); try { Dictionary <string, OpenApiSchema> requestSchemas = new Dictionary <string, OpenApiSchema>(); var bodyParams = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param && p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Body) .ToList(); var generationContext = settings.GenerationContext; foreach (var bodyParam in bodyParams) { var name = bodyParam.Attribute(KnownXmlStrings.Name)?.Value.Trim(); var mediaType = bodyParam.Attribute(KnownXmlStrings.Type)?.Value ?? "application/json"; var allListedTypes = bodyParam.GetListedTypes(); if (!allListedTypes.Any()) { throw new InvalidRequestBodyException( string.Format(SpecificationGenerationMessages.MissingSeeCrefTag, name)); } var crefKey = allListedTypes.ToCrefKey(); OpenApiSchema schema = new OpenApiSchema(); if (generationContext.CrefToSchemaMap.ContainsKey(crefKey)) { var schemaInfo = generationContext.CrefToSchemaMap[crefKey]; if (schemaInfo.Error != null) { generationErrors.Add(schemaInfo.Error); return(generationErrors); } schemaInfo.Schema.CopyInto(schema); } if (!requestSchemas.ContainsKey(mediaType)) { requestSchemas[mediaType] = new OpenApiSchema { Type = "object" }; } bool nullable = false; bool required = false; var defaultValue = bodyParam.Attribute(KnownXmlStrings.Default); if (bodyParam.Attribute(KnownXmlStrings.Nullable) != null) { nullable = (element.Attribute(KnownXmlStrings.Nullable) != null && element.Attribute(KnownXmlStrings.Nullable).Value == "true"); required = false; } else if (bodyParam.Attribute(KnownXmlStrings.Required) != null) { nullable = false; required = true; } else if (bodyParam.Attribute(KnownXmlStrings.Default) != null) { nullable = true; required = false; if (defaultValue != null) { if (bodyParam.Attribute(KnownXmlStrings.Cref).Value == $"T:{typeof(int).FullName}") { schema.Default = new OpenApiInteger(int.Parse(defaultValue.Value)); } else if (bodyParam.Attribute(KnownXmlStrings.Cref).Value == $"T:{typeof(string).FullName}") { schema.Default = new OpenApiString(defaultValue.Value); } else if (bodyParam.Attribute(KnownXmlStrings.Cref).Value == $"T:{typeof(bool).FullName}") { schema.Default = new OpenApiBoolean(bool.Parse(defaultValue.Value)); } } } else { required = true; } requestSchemas[mediaType].Properties.Add(name, schema); if (required && !nullable) { if (requestSchemas[mediaType].Required == null) { requestSchemas[mediaType].Required = new HashSet <string>(); } requestSchemas[mediaType].Required.Add(name); } } if (bodyParams.Count > 0) { foreach (var requestSchemaMediaType in requestSchemas.Keys) { var requestSchema = requestSchemas[requestSchemaMediaType]; if (requestSchema.Properties.Count >= 1) { var schemaReferenceDefaultVariant = generationContext.VariantSchemaReferenceMap[DocumentVariantInfo.Default]; string requestbodySchemaName = operation.OperationId + "Request"; var requestbodySchemaNameChars = requestbodySchemaName.ToCharArray(); requestbodySchemaNameChars[0] = requestbodySchemaName[0].ToString().ToUpper()[0]; requestbodySchemaName = new string(requestbodySchemaNameChars); schemaReferenceDefaultVariant[requestbodySchemaName] = requestSchema; OpenApiSchema schema; var bodyParam = bodyParams.First(); if (bodyParams.First().Attribute("requestmodel") != null && bodyParam.Attribute("requestmodel").Value == "true") { var refSchemaName = requestSchema.Properties.Values.ToList()[0].Reference.Id; var refSchemaProperties = schemaReferenceDefaultVariant[refSchemaName].Properties; schema = new OpenApiSchema { Reference = new OpenApiReference { Id = refSchemaName, Type = ReferenceType.Schema } }; } else { schema = new OpenApiSchema { Reference = new OpenApiReference { Id = requestbodySchemaName, Type = ReferenceType.Schema } }; } operation.RequestBody = new OpenApiRequestBody { Content = { [requestSchemaMediaType] = new OpenApiMediaType { Schema = schema } }, Required = true }; } } } } catch (Exception ex) { generationErrors.Add( new GenerationError { Message = ex.Message, ExceptionType = ex.GetType().Name }); } return(generationErrors); }
/// <summary> /// Fetches the value of "param" tags from xml documentation and populates operation's parameters values. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <returns>The list of generation errors(if any) produced when processing the filter.</returns> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public IList <GenerationError> Apply( OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var generationErrors = new List <GenerationError>(); try { var paramElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param) .ToList(); // Query paramElements again to get all the parameter elements that have "in" attribute. // This will include those whose "in" attribute were just populated in PopulateInAttributeFilter, but exclude // those that have "in" attribute being "body" since they will be handled as a request body. var paramElementsWithIn = paramElements.Where( p => KnownXmlStrings.InValuesTranslatableToParameter.Contains( p.Attribute(KnownXmlStrings.In)?.Value)) .ToList(); var generationContext = settings.GenerationContext; foreach (var paramElement in paramElementsWithIn) { var inValue = paramElement.Attribute(KnownXmlStrings.In)?.Value.Trim(); var name = paramElement.Attribute(KnownXmlStrings.Name)?.Value.Trim(); if (settings.RemoveRoslynDuplicateStringFromParamName) { name = name.RemoveRoslynDuplicateString(); } if (inValue == KnownXmlStrings.Path && !settings.Path.Contains($"{{{name}}}", StringComparison.InvariantCultureIgnoreCase)) { continue; } var isRequired = paramElement.Attribute(KnownXmlStrings.Required)?.Value.Trim(); var cref = paramElement.Attribute(KnownXmlStrings.Cref)?.Value.Trim(); var description = paramElement.GetDescriptionTextFromLastTextNode(); var allListedTypes = paramElement.GetListedTypes(); OpenApiSchema schema = new OpenApiSchema(); if (!allListedTypes.Any()) { // Set default schema as string. schema = new OpenApiSchema() { Type = "string" }; } var crefKey = allListedTypes.ToCrefKey(); if (generationContext.CrefToSchemaMap.ContainsKey(crefKey)) { var schemaInfo = generationContext.CrefToSchemaMap[crefKey]; if (schemaInfo.Error != null) { generationErrors.Add(schemaInfo.Error); return(generationErrors); } schemaInfo.Schema.CopyInto(schema); } var parameterLocation = GetParameterKind(inValue); var examples = paramElement.ToOpenApiExamples( generationContext.CrefToFieldValueMap, generationErrors); var schemaReferenceDefaultVariant = generationContext .VariantSchemaReferenceMap[DocumentVariantInfo.Default]; if (examples.Any()) { var firstExample = examples.First().Value?.Value; if (firstExample != null) { if (schema.Reference != null) { if (schemaReferenceDefaultVariant.ContainsKey(schema.Reference.Id)) { schemaReferenceDefaultVariant[schema.Reference.Id].Example = firstExample; } } else { schema.Example = firstExample; } } } var openApiParameter = new OpenApiParameter { Name = name, In = parameterLocation, Description = description, Required = parameterLocation == ParameterLocation.Path || Convert.ToBoolean(isRequired), Schema = schema, Examples = examples.Any() ? examples : null }; operation.Parameters.Add(openApiParameter); } } catch (Exception ex) { generationErrors.Add( new GenerationError { Message = ex.Message, ExceptionType = ex.GetType().Name }); } return(generationErrors); }
/// <summary> /// Fetches the value of "response" tags from xml documentation and populates operation's response. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var responseElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Response || p.Name == KnownXmlStrings.ResponseType); SchemaReferenceRegistry schemaReferenceRegistry = settings.ReferenceRegistryManager.SchemaReferenceRegistry; foreach (var responseElement in responseElements) { var code = responseElement.Attribute(KnownXmlStrings.Code)?.Value; if (string.IsNullOrWhiteSpace(code)) { // Most APIs only document responses for a successful operation, so if code is not specified, // we will assume it is for a successful operation. This also allows us to comply with OpenAPI spec: // The Responses Object MUST contain at least one response code, // and it SHOULD be the response for a successful operation call. code = "200"; } var mediaType = responseElement.Attribute(KnownXmlStrings.Type)?.Value ?? "application/json"; var description = responseElement.GetDescriptionTextFromLastTextNode(); var type = typeof(string); var allListedTypes = responseElement.GetListedTypes(); var responseContractType = settings.TypeFetcher.LoadTypeFromCrefValues(allListedTypes); OpenApiSchema schema = null; if (responseContractType != null) { schema = schemaReferenceRegistry.FindOrAddReference(responseContractType); } var examples = responseElement.ToOpenApiExamples(settings.TypeFetcher); var headers = responseElement.ToOpenApiHeaders( settings.TypeFetcher, settings.ReferenceRegistryManager.SchemaReferenceRegistry); if (schema != null) { if (examples.Count > 0) { var firstExample = examples.First().Value?.Value; if (firstExample != null) { if (schema.Reference != null) { var key = schemaReferenceRegistry.GetKey(responseContractType); if (schemaReferenceRegistry.References.ContainsKey(key)) { settings.ReferenceRegistryManager.SchemaReferenceRegistry.References[key].Example = firstExample; } } else { schema.Example = firstExample; } } } } if (operation.Responses.ContainsKey(code)) { if (string.IsNullOrWhiteSpace(operation.Responses[code].Description)) { operation.Responses[code].Description = description.RemoveBlankLines(); } if (schema != null) { if (!operation.Responses[code].Content.ContainsKey(mediaType)) { operation.Responses[code].Content[mediaType] = new OpenApiMediaType { Schema = schema }; } else { // If the existing schema is just a single schema (not a list of AnyOf), then // we create a new schema and add that schema to AnyOf to allow us to add // more schemas to it later. if (!operation.Responses[code].Content[mediaType].Schema.AnyOf.Any()) { var existingSchema = operation.Responses[code].Content[mediaType].Schema; var newSchema = new OpenApiSchema(); newSchema.AnyOf.Add(existingSchema); operation.Responses[code].Content[mediaType].Schema = newSchema; } operation.Responses[code].Content[mediaType].Schema.AnyOf.Add(schema); } } } else { var response = new OpenApiResponse { Description = description.RemoveBlankLines(), }; if (schema != null) { response.Content[mediaType] = new OpenApiMediaType { Schema = schema }; } if (headers.Any()) { response.Headers = headers; } operation.Responses.Add(code, response); } if (examples.Count > 0) { if (operation.Responses[code].Content[mediaType].Examples.Any()) { examples.CopyInto(operation.Responses[code].Content[mediaType].Examples); } else { operation.Responses[code].Content[mediaType].Examples = examples; } } } if (!operation.Responses.Any()) { operation.Responses.Add( "default", new OpenApiResponse { Description = "Responses cannot be located for this operation." }); } }
/// <summary> /// Fetches the value of "param" tags from xml documentation and populates operation's parameters values. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var paramElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param) .ToList(); // Query paramElements again to get all the parameter elements that have "in" attribute. // This will include those whose "in" attribute were just populated in PopulateInAttributeFilter, but exclude // those that have "in" attribute being "body" since they will be handled as a request body. var paramElementsWithIn = paramElements.Where( p => KnownXmlStrings.InValuesTranslatableToParameter.Contains( p.Attribute(KnownXmlStrings.In)?.Value)) .ToList(); foreach (var paramElement in paramElementsWithIn) { var inValue = paramElement.Attribute(KnownXmlStrings.In)?.Value.Trim(); var name = paramElement.Attribute(KnownXmlStrings.Name)?.Value.Trim(); if (inValue == KnownXmlStrings.Path && !settings.Path.Contains($"{{{name}}}", StringComparison.InvariantCultureIgnoreCase)) { continue; } var isRequired = paramElement.Attribute(KnownXmlStrings.Required)?.Value.Trim(); var cref = paramElement.Attribute(KnownXmlStrings.Cref)?.Value.Trim(); var childNodes = paramElement.DescendantNodes().ToList(); var description = string.Empty; var lastNode = childNodes.LastOrDefault(); if (lastNode != null && lastNode.NodeType == XmlNodeType.Text) { description = lastNode.ToString().Trim().RemoveBlankLines(); } // Fetch if any see tags are present, if present populate listed types with it. var seeNodes = paramElement.Descendants(KnownXmlStrings.See); var allListedTypes = seeNodes .Select(node => node.Attribute(KnownXmlStrings.Cref)?.Value) .Where(crefValue => crefValue != null).ToList(); // If no see tags are present, add the value from cref tag. if (!allListedTypes.Any() && !string.IsNullOrWhiteSpace(cref)) { allListedTypes.Add(cref); } var schema = GenerateSchemaFromCref(allListedTypes, settings); var parameterLocation = GetParameterKind(inValue); operation.Parameters.Add( new OpenApiParameter { Name = name, In = parameterLocation, Description = description, Required = parameterLocation == ParameterLocation.Path || Convert.ToBoolean(isRequired), Schema = schema }); } }
/// <summary> /// Fetches the value of "response" tags from xml documentation and populates operation's response. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var responseElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Response || p.Name == KnownXmlStrings.ResponseType); foreach (var responseElement in responseElements) { var code = responseElement.Attribute(KnownXmlStrings.Code)?.Value; if (string.IsNullOrWhiteSpace(code)) { // Most APIs only document responses for a successful operation, so if code is not specified, // we will assume it is for a successful operation. This also allows us to comply with OpenAPI spec: // The Responses Object MUST contain at least one response code, // and it SHOULD be the response for a successful operation call. code = "200"; } var mediaType = responseElement.Attribute(KnownXmlStrings.Type)?.Value ?? "application/json"; var childNodes = responseElement.DescendantNodes().ToList(); var description = string.Empty; var lastNode = childNodes.LastOrDefault(); if (lastNode != null && lastNode.NodeType == XmlNodeType.Text) { description = lastNode.ToString(); } var seeNodes = responseElement.Descendants(KnownXmlStrings.See); var allListedTypes = seeNodes .Select(node => node.Attribute(KnownXmlStrings.Cref)?.Value) .Where(crefValue => crefValue != null).ToList(); OpenApiSchema schema = null; if (allListedTypes.Any()) { var responseContractType = settings.TypeFetcher.LoadTypeFromCrefValues(allListedTypes); schema = settings.ReferenceRegistryManager.SchemaReferenceRegistry.FindOrAddReference( responseContractType); } if (operation.Responses.ContainsKey(code)) { if (string.IsNullOrWhiteSpace(operation.Responses[code].Description)) { operation.Responses[code].Description = description.RemoveBlankLines(); } if (schema != null) { if (!operation.Responses[code].Content.ContainsKey(mediaType)) { operation.Responses[code].Content[mediaType] = new OpenApiMediaType { Schema = schema }; } else { // If the existing schema is just a single schema (not a list of AnyOf), then // we create a new schema and add that schema to AnyOf to allow us to add // more schemas to it later. if (!operation.Responses[code].Content[mediaType].Schema.AnyOf.Any()) { var existingSchema = operation.Responses[code].Content[mediaType].Schema; var newSchema = new OpenApiSchema(); newSchema.AnyOf.Add(existingSchema); operation.Responses[code].Content[mediaType].Schema = newSchema; } operation.Responses[code].Content[mediaType].Schema.AnyOf.Add(schema); } } } else { var response = new OpenApiResponse { Description = description.RemoveBlankLines(), }; if (schema != null) { response.Content[mediaType] = new OpenApiMediaType { Schema = schema }; } operation.Responses.Add(code, response); } } if (!operation.Responses.Any()) { operation.Responses.Add( "default", new OpenApiResponse { Description = "Responses cannot be located for this operation." }); } }
/// <summary> /// Fetches the value of "response" tags from xml documentation and populates operation's response. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <returns>The list of generation errors, if any produced when processing the filter.</returns> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public IList <GenerationError> Apply( OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var generationErrors = new List <GenerationError>(); try { var responseElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Response || p.Name == KnownXmlStrings.ResponseType); var generationContext = settings.GenerationContext; foreach (var responseElement in responseElements) { var code = responseElement.Attribute(KnownXmlStrings.Code)?.Value; if (string.IsNullOrWhiteSpace(code)) { // Most APIs only document responses for a successful operation, so if code is not specified, // we will assume it is for a successful operation. This also allows us to comply with OpenAPI spec: // The Responses Object MUST contain at least one response code, // and it SHOULD be the response for a successful operation call. code = "200"; } var mediaType = responseElement.Attribute(KnownXmlStrings.Type)?.Value ?? "application/json"; var description = responseElement.GetDescriptionTextFromLastTextNode(); var allListedTypes = responseElement.GetListedTypes(); var crefKey = allListedTypes.ToCrefKey(); OpenApiSchema schema = null; if (generationContext.CrefToSchemaMap.ContainsKey(crefKey)) { var schemaInfo = generationContext.CrefToSchemaMap[crefKey]; if (schemaInfo.Error != null) { generationErrors.Add(schemaInfo.Error); return(generationErrors); } schema = new OpenApiSchema(); schemaInfo.Schema.CopyInto(schema); } var examples = responseElement.ToOpenApiExamples( generationContext.CrefToFieldValueMap, generationErrors); var headers = responseElement.ToOpenApiHeaders(generationContext.CrefToSchemaMap, generationErrors); var schemaReferenceDefaultVariant = generationContext .VariantSchemaReferenceMap[DocumentVariantInfo.Default]; if (schema != null) { if (examples.Count > 0) { var firstExample = examples.First().Value?.Value; if (firstExample != null) { if (schema.Reference != null) { if (schemaReferenceDefaultVariant.ContainsKey(schema.Reference.Id)) { schemaReferenceDefaultVariant[schema.Reference.Id].Example = firstExample; } } else { schema.Example = firstExample; } } } } if (operation.Responses.ContainsKey(code)) { if (string.IsNullOrWhiteSpace(operation.Responses[code].Description)) { operation.Responses[code].Description = description.RemoveBlankLines(); } if (schema != null) { if (!operation.Responses[code].Content.ContainsKey(mediaType)) { operation.Responses[code].Content[mediaType] = new OpenApiMediaType { Schema = schema }; } else { // If the existing schema is just a single schema (not a list of AnyOf), then // we create a new schema and add that schema to AnyOf to allow us to add // more schemas to it later. if (!operation.Responses[code].Content[mediaType].Schema.AnyOf.Any()) { var existingSchema = operation.Responses[code].Content[mediaType].Schema; var newSchema = new OpenApiSchema(); newSchema.AnyOf.Add(existingSchema); operation.Responses[code].Content[mediaType].Schema = newSchema; } operation.Responses[code].Content[mediaType].Schema.AnyOf.Add(schema); } } } else { var response = new OpenApiResponse { Description = description.RemoveBlankLines(), }; if (schema != null) { response.Content[mediaType] = new OpenApiMediaType { Schema = schema }; } if (headers != null && headers.Any()) { response.Headers = headers; } operation.Responses.Add(code, response); } if (examples.Count > 0) { if (operation.Responses[code].Content[mediaType].Examples.Any()) { examples.CopyInto(operation.Responses[code].Content[mediaType].Examples); } else { operation.Responses[code].Content[mediaType].Examples = examples; } } } if (!operation.Responses.Any()) { operation.Responses.Add( "default", new OpenApiResponse { Description = "Responses cannot be located for this operation." }); } } catch (Exception ex) { generationErrors.Add( new GenerationError { Message = ex.Message, ExceptionType = ex.GetType().Name }); } return(generationErrors); }
/// <summary> /// Fetches the value of "param" tags from xml documentation with in valus of "body" /// and populates operation's request body. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <returns>The list of generation errors, if any produced when processing the filter.</returns> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public IList <GenerationError> Apply( OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var generationErrors = new List <GenerationError>(); try { var bodyElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param && p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Body) .ToList(); var generationContext = settings.GenerationContext; foreach (var bodyElement in bodyElements) { var name = bodyElement.Attribute(KnownXmlStrings.Name)?.Value.Trim(); var mediaType = bodyElement.Attribute(KnownXmlStrings.Type)?.Value ?? "application/json"; var description = bodyElement.GetDescriptionTextFromLastTextNode(); var allListedTypes = bodyElement.GetListedTypes(); if (!allListedTypes.Any()) { throw new InvalidRequestBodyException( string.Format(SpecificationGenerationMessages.MissingSeeCrefTag, name)); } var crefKey = allListedTypes.ToCrefKey(); OpenApiSchema schema = new OpenApiSchema(); if (generationContext.CrefToSchemaMap.ContainsKey(crefKey)) { var schemaInfo = generationContext.CrefToSchemaMap[crefKey]; if (schemaInfo.Error != null) { generationErrors.Add(schemaInfo.Error); return(generationErrors); } schemaInfo.Schema.CopyInto(schema); } var examples = bodyElement.ToOpenApiExamples( generationContext.CrefToFieldValueMap, generationErrors); var schemaReferenceDefaultVariant = generationContext .VariantSchemaReferenceMap[DocumentVariantInfo.Default]; if (examples.Count > 0) { var firstExample = examples.First().Value?.Value; if (firstExample != null) { // In case a schema is a reference, find that schema object in schema registry // and update the example. if (schema.Reference != null) { if (schemaReferenceDefaultVariant.ContainsKey(schema.Reference.Id)) { schemaReferenceDefaultVariant[schema.Reference.Id].Example = firstExample; } } else { schema.Example = firstExample; } } } if (operation.RequestBody == null) { operation.RequestBody = new OpenApiRequestBody { Description = description.RemoveBlankLines(), Content = { [mediaType] = new OpenApiMediaType { Schema = schema } }, Required = true }; } else { if (string.IsNullOrWhiteSpace(operation.RequestBody.Description)) { operation.RequestBody.Description = description.RemoveBlankLines(); } if (!operation.RequestBody.Content.ContainsKey(mediaType)) { operation.RequestBody.Content[mediaType] = new OpenApiMediaType { Schema = schema }; } else { if (!operation.RequestBody.Content[mediaType].Schema.AnyOf.Any()) { var existingSchema = operation.RequestBody.Content[mediaType].Schema; var newSchema = new OpenApiSchema(); newSchema.AnyOf.Add(existingSchema); operation.RequestBody.Content[mediaType].Schema = newSchema; } operation.RequestBody.Content[mediaType].Schema.AnyOf.Add(schema); } } if (examples.Count > 0) { if (operation.RequestBody.Content[mediaType].Examples.Any()) { examples.CopyInto(operation.RequestBody.Content[mediaType].Examples); } else { operation.RequestBody.Content[mediaType].Examples = examples; } } } } catch (Exception ex) { generationErrors.Add( new GenerationError { Message = ex.Message, ExceptionType = ex.GetType().Name }); } return(generationErrors); }
/// <summary> /// Fetches the value of "param" tags from xml documentation with in valus of "body" /// and populates operation's request body. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var bodyElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param && p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Body) .ToList(); SchemaReferenceRegistry schemaReferenceRegistry = settings.ReferenceRegistryManager.SchemaReferenceRegistry; foreach (var bodyElement in bodyElements) { var name = bodyElement.Attribute(KnownXmlStrings.Name)?.Value.Trim(); var mediaType = bodyElement.Attribute(KnownXmlStrings.Type)?.Value ?? "application/json"; var description = bodyElement.GetDescriptionTextFromLastTextNode(); var allListedTypes = bodyElement.GetListedTypes(); if (!allListedTypes.Any()) { throw new InvalidRequestBodyException( string.Format(SpecificationGenerationMessages.MissingSeeCrefTag, name)); } var type = settings.TypeFetcher.LoadTypeFromCrefValues(allListedTypes); var schema = schemaReferenceRegistry.FindOrAddReference(type); var examples = bodyElement.ToOpenApiExamples(settings.TypeFetcher); if (examples.Count > 0) { var firstExample = examples.First().Value?.Value; if (firstExample != null) { // In case a schema is a reference, find that schmea object in schema registry // and update the example. if (schema.Reference != null) { var key = schemaReferenceRegistry.GetKey(type); if (schemaReferenceRegistry.References.ContainsKey(key)) { settings.ReferenceRegistryManager.SchemaReferenceRegistry.References[key].Example = firstExample; } } else { schema.Example = firstExample; } } } if (operation.RequestBody == null) { operation.RequestBody = new OpenApiRequestBody { Description = description.RemoveBlankLines(), Content = { [mediaType] = new OpenApiMediaType { Schema = schema } }, Required = true }; } else { if (string.IsNullOrWhiteSpace(operation.RequestBody.Description)) { operation.RequestBody.Description = description.RemoveBlankLines(); } if (!operation.RequestBody.Content.ContainsKey(mediaType)) { operation.RequestBody.Content[mediaType] = new OpenApiMediaType { Schema = schema }; } else { if (!operation.RequestBody.Content[mediaType].Schema.AnyOf.Any()) { var existingSchema = operation.RequestBody.Content[mediaType].Schema; var newSchema = new OpenApiSchema(); newSchema.AnyOf.Add(existingSchema); operation.RequestBody.Content[mediaType].Schema = newSchema; } operation.RequestBody.Content[mediaType].Schema.AnyOf.Add(schema); } } if (examples.Count > 0) { if (operation.RequestBody.Content[mediaType].Examples.Any()) { examples.CopyInto(operation.RequestBody.Content[mediaType].Examples); } else { operation.RequestBody.Content[mediaType].Examples = examples; } } } }
/// <summary> /// Fetches the value of "param" tags from xml documentation and populates operation's parameters values. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var paramElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param) .ToList(); // Query paramElements again to get all the parameter elements that have "in" attribute. // This will include those whose "in" attribute were just populated in PopulateInAttributeFilter, but exclude // those that have "in" attribute being "body" since they will be handled as a request body. var paramElementsWithIn = paramElements.Where( p => KnownXmlStrings.InValuesTranslatableToParameter.Contains( p.Attribute(KnownXmlStrings.In)?.Value)) .ToList(); SchemaReferenceRegistry schemaReferenceRegistry = settings.ReferenceRegistryManager.SchemaReferenceRegistry; foreach (var paramElement in paramElementsWithIn) { var inValue = paramElement.Attribute(KnownXmlStrings.In)?.Value.Trim(); var name = paramElement.Attribute(KnownXmlStrings.Name)?.Value.Trim(); if (inValue == KnownXmlStrings.Path && !settings.Path.Contains($"{{{name}}}", StringComparison.InvariantCultureIgnoreCase)) { continue; } var isRequired = paramElement.Attribute(KnownXmlStrings.Required)?.Value.Trim(); var cref = paramElement.Attribute(KnownXmlStrings.Cref)?.Value.Trim(); var description = paramElement.GetDescriptionTextFromLastTextNode(); var type = typeof(string); var allListedTypes = paramElement.GetListedTypes(); if (allListedTypes.Any()) { type = settings.TypeFetcher.LoadTypeFromCrefValues(allListedTypes); } var schema = schemaReferenceRegistry.FindOrAddReference(type); var parameterLocation = GetParameterKind(inValue); var examples = paramElement.ToOpenApiExamples(settings.TypeFetcher); var openApiParameter = new OpenApiParameter { Name = name, In = parameterLocation, Description = description, Required = parameterLocation == ParameterLocation.Path || Convert.ToBoolean(isRequired), Schema = schema }; if (examples.Count > 0) { var firstExample = examples.First().Value?.Value; if (firstExample != null) { if (openApiParameter.Schema.Reference != null) { var key = schemaReferenceRegistry.GetKey(type); if (schemaReferenceRegistry.References.ContainsKey(key)) { schemaReferenceRegistry.References[key].Example = firstExample; } } else { openApiParameter.Schema.Example = firstExample; } } openApiParameter.Examples = examples; } operation.Parameters.Add(openApiParameter); } }
/// <summary> /// Fetches the value of "param" tags from xml documentation with in valus of "body" /// and populates operation's request body. /// </summary> /// <param name="operation">The operation to be updated.</param> /// <param name="element">The xml element representing an operation in the annotation xml.</param> /// <param name="settings">The operation filter settings.</param> /// <remarks> /// Care should be taken to not overwrite the existing value in Operation if already present. /// This guarantees the predictable behavior that the first tag in the XML will be respected. /// It also guarantees that common annotations in the config file do not overwrite the /// annotations in the main documentation. /// </remarks> public void Apply(OpenApiOperation operation, XElement element, OperationFilterSettings settings) { var bodyElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param && p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Body) .ToList(); foreach (var bodyElement in bodyElements) { var name = bodyElement.Attribute(KnownXmlStrings.Name)?.Value.Trim(); var mediaType = bodyElement.Attribute(KnownXmlStrings.Type)?.Value ?? "application/json"; var childNodes = bodyElement.DescendantNodes().ToList(); var description = string.Empty; var lastNode = childNodes.LastOrDefault(); if (lastNode != null && lastNode.NodeType == XmlNodeType.Text) { description = lastNode.ToString(); } var seeNodes = bodyElement.Descendants(KnownXmlStrings.See); var allListedTypes = seeNodes .Select(node => node.Attribute(KnownXmlStrings.Cref)?.Value) .Where(crefValue => crefValue != null).ToList(); if (!allListedTypes.Any()) { throw new InvalidRequestBodyException( string.Format(SpecificationGenerationMessages.MissingSeeCrefTag, name)); } var type = settings.TypeFetcher.LoadTypeFromCrefValues(allListedTypes); var schema = settings.ReferenceRegistryManager.SchemaReferenceRegistry.FindOrAddReference(type); if (operation.RequestBody == null) { operation.RequestBody = new OpenApiRequestBody { Description = description.RemoveBlankLines(), Content = { [mediaType] = new OpenApiMediaType { Schema = schema } }, Required = true }; } else { if (string.IsNullOrWhiteSpace(operation.RequestBody.Description)) { operation.RequestBody.Description = description.RemoveBlankLines(); } if (!operation.RequestBody.Content.ContainsKey(mediaType)) { operation.RequestBody.Content[mediaType] = new OpenApiMediaType { Schema = schema }; } else { if (!operation.RequestBody.Content[mediaType].Schema.AnyOf.Any()) { var existingSchema = operation.RequestBody.Content[mediaType].Schema; var newSchema = new OpenApiSchema(); newSchema.AnyOf.Add(existingSchema); operation.RequestBody.Content[mediaType].Schema = newSchema; } operation.RequestBody.Content[mediaType].Schema.AnyOf.Add(schema); } } } }
/// <summary> /// Generates schema from type names in cref. /// </summary> /// <returns> /// Schema from type in cref if the type is resolvable. /// Otherwise, default to schema for string type. /// </returns> private static OpenApiSchema GenerateSchemaFromCref(IList <string> crefValues, OperationFilterSettings settings) { var type = typeof(string); if (crefValues.Any()) { type = settings.TypeFetcher.LoadTypeFromCrefValues(crefValues); } return(settings.ReferenceRegistryManager.SchemaReferenceRegistry.FindOrAddReference(type)); }