private void CreateSchemas(IList <HttpFunctionDefinition> functionDefinitions, IOpenApiHttpFunctionFilter functionFilter, OpenApiDocument openApiDocument, SchemaReferenceRegistry registry) { IOpenApiHttpFunctionFilterContext functionFilterContext = new OpenApiHttpFunctionFilterContext(); foreach (HttpFunctionDefinition functionDefinition in functionDefinitions) { if (functionDefinition.OpenApiIgnore) { continue; } var filterdVerbs = new HashSet <HttpMethod>(functionDefinition.Verbs); functionFilter.Apply(functionDefinition.RouteConfiguration.Route, filterdVerbs, functionFilterContext); if (filterdVerbs.Count == 0) { continue; } if (filterdVerbs.Contains(HttpMethod.Patch) || filterdVerbs.Contains(HttpMethod.Post) || filterdVerbs.Contains(HttpMethod.Put)) { registry.FindOrAddReference(functionDefinition.CommandType); } if (functionDefinition.CommandResultType != null && functionDefinition.CommandResultType != typeof(IActionResult)) { registry.FindOrAddReference(functionDefinition.CommandResultType); } } openApiDocument.Components.Schemas = registry.References; }
/// <summary> /// Generates schema for the provided list of types and store them in the provided dictionary. /// </summary> /// <param name="allListedTypes">The listed types to fetch schema for.</param> /// <param name="crefSchemaMap">The cref to <see cref="InternalSchemaGenerationInfo"/> map.</param> /// <param name="typeFetcher">The type fetcher used to fetch type information using reflection.</param> /// <param name="schemaReferenceRegistry"><see cref="SchemaReferenceRegistry"/>.</param> private void BuildCrefSchemaMap( IList <string> allListedTypes, Dictionary <string, InternalSchemaGenerationInfo> crefSchemaMap, TypeFetcher typeFetcher, SchemaReferenceRegistry schemaReferenceRegistry) { var key = allListedTypes.ToCrefKey(); var schemaInfo = new InternalSchemaGenerationInfo(); try { var type = typeFetcher.LoadTypeFromCrefValues(allListedTypes); var schema = schemaReferenceRegistry.FindOrAddReference(type); schemaInfo.Schema = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); } catch (Exception e) { var error = new GenerationError { ExceptionType = e.GetType().Name, Message = e.Message }; schemaInfo.Error = error; } if (!crefSchemaMap.ContainsKey(key)) { crefSchemaMap.Add(key, schemaInfo); } }
private void CreateSchemas(HttpFunctionDefinition[] functionDefinitions, OpenApiDocument openApiDocument, SchemaReferenceRegistry registry) { foreach (HttpFunctionDefinition functionDefinition in functionDefinitions) { if (functionDefinition.Verbs.Contains(HttpMethod.Patch) || functionDefinition.Verbs.Contains(HttpMethod.Post) || functionDefinition.Verbs.Contains(HttpMethod.Put)) { registry.FindOrAddReference(functionDefinition.CommandType); } if (functionDefinition.CommandResultType != null && functionDefinition.CommandResultType != typeof(IActionResult)) { registry.FindOrAddReference(functionDefinition.CommandResultType); } } if (registry.References.Any()) { openApiDocument.Components.Schemas = registry.References; } }
/// <summary> /// Processes the "header" tag child elements of the provide XElement /// and generates a map of string to OpenApiHeader. /// </summary> /// <param name="xElement">The XElement to process.</param> /// <param name="typeFetcher">The type fetcher.</param> /// <param name="schemaReferenceRegistry">The schema reference registry.</param> /// <returns>The map of string to OpenApiHeader.</returns> internal static Dictionary <string, OpenApiHeader> ToOpenApiHeaders( this XElement xElement, TypeFetcher typeFetcher, SchemaReferenceRegistry schemaReferenceRegistry) { var headerElements = xElement.Elements() .Where( p => p.Name == KnownXmlStrings.Header) .ToList(); var openApiHeaders = new Dictionary <string, OpenApiHeader>(); foreach (var headerElement in headerElements) { var name = headerElement.Attribute(KnownXmlStrings.Name)?.Value.Trim(); if (string.IsNullOrWhiteSpace(name)) { throw new InvalidHeaderException( string.Format(SpecificationGenerationMessages.UndocumentedName, "header")); } var description = headerElement .Elements() .FirstOrDefault(p => p.Name == KnownXmlStrings.Description)?.Value.Trim().RemoveBlankLines(); var listedTypes = headerElement.GetListedTypes(); var type = typeFetcher.LoadTypeFromCrefValues(listedTypes); var schema = schemaReferenceRegistry.FindOrAddReference(type); openApiHeaders.Add( name, new OpenApiHeader { Description = description, Schema = schema }); } return(openApiHeaders); }
private static void CreateOperationsFromRoutes( HttpFunctionDefinition[] functionDefinitions, OpenApiDocument openApiDocument, SchemaReferenceRegistry registry, string apiPrefix, OpenApiCompilerConfiguration compilerConfiguration) { string prependedApiPrefix = string.IsNullOrEmpty(apiPrefix) ? $"" : $"/{apiPrefix}"; var operationsByRoute = functionDefinitions.Where(x => x.Route != null).GroupBy(x => $"{prependedApiPrefix}/{x.Route}"); foreach (IGrouping <string, HttpFunctionDefinition> route in operationsByRoute) { OpenApiPathItem pathItem = new OpenApiPathItem() { Operations = new Dictionary <OperationType, OpenApiOperation>() }; foreach (HttpFunctionDefinition functionByRoute in route) { Type commandType = functionByRoute.CommandType; foreach (HttpMethod method in functionByRoute.Verbs) { OpenApiOperation operation = new OpenApiOperation { Description = functionByRoute.OpenApiDescription, Summary = functionByRoute.OpenApiSummary, Responses = new OpenApiResponses(), Tags = string.IsNullOrWhiteSpace(functionByRoute.RouteConfiguration.OpenApiName) ? null : new List <OpenApiTag>() { new OpenApiTag { Name = functionByRoute.RouteConfiguration.OpenApiName } } }; var operationFilterContext = new OpenApiOperationFilterContext { CommandType = commandType, PropertyNames = new Dictionary <string, string>() }; foreach (KeyValuePair <int, OpenApiResponseConfiguration> kvp in functionByRoute.OpenApiResponseConfigurations) { operation.Responses.Add(kvp.Key.ToString(), new OpenApiResponse { Description = kvp.Value.Description, Content = { ["application/json"] = new OpenApiMediaType() { Schema = kvp.Value.ResponseType == null ? null : registry.FindOrAddReference(kvp.Value.ResponseType) } } }); } // Does any HTTP success response (2xx) exist if (operation.Responses.Keys.FirstOrDefault(x => x.StartsWith("2")) == null) { OpenApiResponse response = new OpenApiResponse { Description = "Successful API operation" }; if (functionByRoute.CommandResultType != null) { OpenApiSchema schema = registry.FindOrAddReference(functionByRoute.CommandResultType); response.Content = new Dictionary <string, OpenApiMediaType> { { "application/json", new OpenApiMediaType { Schema = schema } } }; } operation.Responses.Add("200", response); } if (method == HttpMethod.Get || method == HttpMethod.Delete) { var schema = registry.GetOrCreateSchema(commandType); foreach (HttpParameter property in functionByRoute.QueryParameters) { var propertyInfo = commandType.GetProperty(property.Name); // Property Name var propertyName = propertyInfo.GetAttributeValue((JsonPropertyAttribute attribute) => attribute.PropertyName); if (string.IsNullOrWhiteSpace(propertyName)) { propertyName = propertyInfo.GetAttributeValue((DataMemberAttribute attribute) => attribute.Name); } if (string.IsNullOrWhiteSpace(propertyName)) { propertyName = propertyInfo.Name.ToCamelCase(); } // Property Required var propertyRequired = !property.IsOptional; if (!propertyRequired) { propertyRequired = propertyInfo.GetAttributeValue((JsonPropertyAttribute attribute) => attribute.Required) == Required.Always; } if (!propertyRequired) { propertyRequired = propertyInfo.GetAttributeValue((RequiredAttribute attribute) => attribute) != null; } var propertySchema = schema.Properties[propertyName]; var parameter = new OpenApiParameter { Name = propertyName, In = ParameterLocation.Query, Required = propertyRequired, Schema = propertySchema, // property.Type.MapToOpenApiSchema(), Description = propertySchema.Description }; FilterParameter(compilerConfiguration.ParameterFilters, parameter); operation.Parameters.Add(parameter); operationFilterContext.PropertyNames[parameter.Name] = propertyInfo.Name; } } if (functionByRoute.Authorization == AuthorizationTypeEnum.Function && (method == HttpMethod.Get || method == HttpMethod.Delete)) { operation.Parameters.Add(new OpenApiParameter { Name = "code", In = ParameterLocation.Query, Required = true, Schema = typeof(string).MapToOpenApiSchema(), Description = "" }); } foreach (HttpParameter property in functionByRoute.RouteParameters) { var parameter = new OpenApiParameter { Name = property.RouteName.ToCamelCase(), In = ParameterLocation.Path, Required = !property.IsOptional, Schema = property.Type.MapToOpenApiSchema(), Description = "" }; FilterParameter(compilerConfiguration.ParameterFilters, parameter); operation.Parameters.Add(parameter); // TODO: We need to consider what to do with the payload model here - if its a route parameter // we need to ignore it in the payload model } if (method == HttpMethod.Post || method == HttpMethod.Put || method == HttpMethod.Patch) { OpenApiRequestBody requestBody = new OpenApiRequestBody(); OpenApiSchema schema = registry.FindReference(commandType); requestBody.Content = new Dictionary <string, OpenApiMediaType> { { "application/json", new OpenApiMediaType { Schema = schema } } }; operation.RequestBody = requestBody; } FilterOperation(compilerConfiguration.OperationFilters, operation, operationFilterContext); pathItem.Operations.Add(MethodToOperationMap[method], operation); } } openApiDocument.Paths.Add(route.Key, pathItem); } }
/// <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(); 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(); 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; } } } }