From 5ee2c4697913eecbc0b8ada2491733bc798d10c9 Mon Sep 17 00:00:00 2001 From: Gino Canessa Date: Mon, 27 Jul 2020 15:57:30 -0500 Subject: [PATCH] Open API updates. --- .../Language/LangOpenApi.cs | 304 +++++++++++++----- 1 file changed, 220 insertions(+), 84 deletions(-) diff --git a/src/Microsoft.Health.Fhir.SpecManager/Language/LangOpenApi.cs b/src/Microsoft.Health.Fhir.SpecManager/Language/LangOpenApi.cs index 4b04c2dec..a0d765c6f 100644 --- a/src/Microsoft.Health.Fhir.SpecManager/Language/LangOpenApi.cs +++ b/src/Microsoft.Health.Fhir.SpecManager/Language/LangOpenApi.cs @@ -4,11 +4,13 @@ // using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using Microsoft.Health.Fhir.SpecManager.fhir.r5; using Microsoft.Health.Fhir.SpecManager.Manager; using Microsoft.Health.Fhir.SpecManager.Models; using Microsoft.OpenApi; @@ -27,9 +29,6 @@ public sealed class LangOpenApi : ILanguage /// Options for controlling the export. private ExporterOptions _options; - /// True to export enums. - private bool _exportEnums; - /// List of types of the exported resource names and types. private Dictionary _exportedResourceNamesAndTypes = new Dictionary(); @@ -60,12 +59,21 @@ public sealed class LangOpenApi : ILanguage /// True to include, false to exclude the schemas. private bool _includeSchemas = true; + /// True to include, false to exclude the schema descriptions. + private bool _includeSchemaDescriptions = true; + + /// True to expand references based on allowed profiles. + private bool _expandProfiles = true; + + /// True to generate read only. + private bool _generateReadOnly = false; + /// Dictionary mapping FHIR primitive types to language equivalents. private static readonly Dictionary _primitiveTypeMap = new Dictionary() { // { "base", "Object" }, { "base64Binary", "string:byte" }, - // { "boolean", "boolean" }, + { "boolean", "boolean" }, // note, this is here to simplify primitive mapping { "canonical", "string" }, { "code", "string" }, { "date", "string" }, @@ -78,7 +86,7 @@ public sealed class LangOpenApi : ILanguage { "markdown", "string" }, { "oid", "string" }, { "positiveInt", "integer:int32" }, - { "string", "string" }, + { "string", "string" }, // note, this is here to simplify primitive mapping { "time", "string" }, { "unsignedInt", "integer:int32" }, { "uri", "string" }, @@ -137,6 +145,9 @@ public sealed class LangOpenApi : ILanguage { "Responses", "Response inclusion style (single|multiple)." }, { "Summaries", "If responses should include summaries (true|false)." }, { "Schemas", "If schemas should be included (true|false)" }, + { "SchemaDescriptions", "If schemas should include descriptions (true|false)" }, + { "ExpandProfiles", "If types should expand based on allowed profiles (true|false)" }, + { "ReadOnly", "If the output should only contain GET operations (false|true)" }, }; /// Export the passed FHIR version into the specified directory. @@ -153,15 +164,6 @@ void ILanguage.Export( _info = info; _options = options; - if (options.OptionalClassTypesToExport.Contains(ExporterOptions.FhirExportClassType.Enum)) - { - _exportEnums = true; - } - else - { - _exportEnums = false; - } - if (_options.LanguageOptions != null) { foreach (KeyValuePair kvp in _options.LanguageOptions) @@ -219,6 +221,27 @@ void ILanguage.Export( _includeSchemas = false; } + if (_parameters.ContainsKey("SCHEMADESCRIPTIONS") && + (!string.IsNullOrEmpty(_parameters["SCHEMADESCRIPTIONS"])) && + _parameters["SCHEMADESCRIPTIONS"].StartsWith("F", StringComparison.OrdinalIgnoreCase)) + { + _includeSchemaDescriptions = false; + } + + if (_parameters.ContainsKey("EXPANDPROFILES") && + (!string.IsNullOrEmpty(_parameters["EXPANDPROFILES"])) && + _parameters["EXPANDPROFILES"].StartsWith("F", StringComparison.OrdinalIgnoreCase)) + { + _expandProfiles = false; + } + + if (_parameters.ContainsKey("READONLY") && + (!string.IsNullOrEmpty(_parameters["READONLY"])) && + _parameters["READONLY"].StartsWith("T", StringComparison.OrdinalIgnoreCase)) + { + _generateReadOnly = true; + } + OpenApiDocument document = new OpenApiDocument(); document.Info = BuildInfo(); @@ -293,6 +316,11 @@ private OpenApiSchema BuildSchema( Properties = new Dictionary(), }; + if (_includeSchemaDescriptions) + { + schema.Description = complex.ShortDescription; + } + if (root == null) { root = complex; @@ -310,9 +338,7 @@ private OpenApiSchema BuildSchema( } else { - schema.Properties.Add( - GetElementName(element), - BuildElementSchema(element)); + BuildElementSchema(ref schema, element); } } } @@ -321,13 +347,44 @@ private OpenApiSchema BuildSchema( } /// Builds element schema. - /// The element. - /// An OpenApiSchema. - private OpenApiSchema BuildElementSchema( + /// [in,out] The parent schema. + /// The element. + private void BuildElementSchema( + ref OpenApiSchema parentSchema, FhirElement element) { + string name = GetElementName(element); OpenApiSchema schema = new OpenApiSchema(); + if (_includeSchemaDescriptions) + { + schema.Description = element.ShortDescription; + } + + if ((element.ElementTypes != null) && + (element.ElementTypes.Count > 1)) + { + foreach (FhirElementType elementType in element.ElementTypes.Values) + { + string pascal = FhirUtils.SanitizedToConvention(elementType.Name, FhirTypeBase.NamingConvention.PascalCase); + + OpenApiSchema subSchema = new OpenApiSchema(); + + if (_includeSchemaDescriptions) + { + subSchema.Description = element.ShortDescription; + } + + SetSchemaType(elementType.Name, ref subSchema); + + parentSchema.Properties.Add( + $"{name}{pascal}", + subSchema); + } + + return; + } + string type; if (!string.IsNullOrEmpty(element.BaseTypeName)) @@ -336,27 +393,63 @@ private OpenApiSchema BuildElementSchema( } else if (element.ElementTypes.Count == 1) { - type = element.ElementTypes.First().Value.Name; + FhirElementType elementType = element.ElementTypes.First().Value; + + type = elementType.Name; + + if (_expandProfiles && (type == "Resource")) + { + schema.OneOf = new List(); + + if ((elementType.Profiles == null) || + (elementType.Profiles.Count == 0)) + { + foreach (FhirComplex resource in _info.Resources.Values) + { + OpenApiSchema subSchema = new OpenApiSchema(); + SetSchemaType(resource.Name, ref subSchema); + schema.OneOf.Add(subSchema); + } + } + else + { + foreach (FhirElementProfile profile in elementType.Profiles.Values) + { + OpenApiSchema subSchema = new OpenApiSchema(); + SetSchemaType(profile.Name, ref subSchema); + schema.OneOf.Add(subSchema); + } + } + + parentSchema.Properties.Add( + GetElementName(element), + schema); + + return; + } } else { type = "Element"; } - if (type.Contains('.')) - { - schema.Reference = new OpenApiReference() - { - Id = BuildTypeFromPath(type), - Type = ReferenceType.Schema, - }; + SetSchemaType(type, ref schema); - return schema; - } + parentSchema.Properties.Add( + GetElementName(element), + schema); + } - if (_primitiveTypeMap.ContainsKey(type)) + /// Sets a type. + /// Type of the base. + /// [in,out] The schema. + private static void SetSchemaType( + string baseType, + ref OpenApiSchema schema) + { + if (_primitiveTypeMap.ContainsKey(baseType)) { - type = _primitiveTypeMap[type]; + string type = _primitiveTypeMap[baseType]; if (type.Contains(':')) { @@ -370,10 +463,14 @@ private OpenApiSchema BuildElementSchema( schema.Type = type; } - return schema; + return; } - return schema; + schema.Reference = new OpenApiReference() + { + Id = BuildTypeFromPath(baseType), + Type = ReferenceType.Schema, + }; } /// Builds type from path. @@ -392,12 +489,8 @@ private static string BuildTypeFromPath(string path) sb.Append(components[i]); break; - case 1: - sb.Append("/properties"); - sb.Append(components[i]); - break; - default: + sb.Append("/properties/"); sb.Append(components[i]); break; } @@ -498,6 +591,11 @@ private OpenApiPaths BuildPathsForServer() case FhirServerResourceInfo.FhirInteraction.Patch: case FhirServerResourceInfo.FhirInteraction.Update: + if (_generateReadOnly) + { + continue; + } + if (!instancePath.Operations.ContainsKey(OperationType.Put)) { instancePath.Operations.Add( @@ -509,6 +607,11 @@ private OpenApiPaths BuildPathsForServer() case FhirServerResourceInfo.FhirInteraction.Create: + if (_generateReadOnly) + { + continue; + } + if (!typePath.Operations.ContainsKey(OperationType.Put)) { typePath.Operations.Add( @@ -520,6 +623,11 @@ private OpenApiPaths BuildPathsForServer() case FhirServerResourceInfo.FhirInteraction.Delete: + if (_generateReadOnly) + { + continue; + } + if (!instancePath.Operations.ContainsKey(OperationType.Delete)) { instancePath.Operations.Add( @@ -589,24 +697,58 @@ private static OpenApiParameter BuildPathIdParameter() }; } - /// Builds response content. + /// Builds a content map. + /// Name of the resource. /// A Dictionary of MIME Types and matching ApiOpenMeidaTypes. - private Dictionary BuildResponseContent() + private Dictionary BuildContentMap( + string resourceName) { Dictionary mediaTypes = new Dictionary(); if (_explicitFhirJson) { - mediaTypes.Add( - "application/fhir+json", - new OpenApiMediaType()); + if (string.IsNullOrEmpty(resourceName)) + { + mediaTypes.Add( + "application/fhir+json", + new OpenApiMediaType()); + } + else + { + OpenApiSchema schema = new OpenApiSchema(); + + SetSchemaType(resourceName, ref schema); + + mediaTypes.Add( + "application/fhir+json", + new OpenApiMediaType() + { + Schema = schema, + }); + } } if (_explicitFhirXml) { - mediaTypes.Add( - "application/fhir+xml", - new OpenApiMediaType()); + if (string.IsNullOrEmpty(resourceName)) + { + mediaTypes.Add( + "application/fhir+xml", + new OpenApiMediaType()); + } + else + { + OpenApiSchema schema = new OpenApiSchema(); + + SetSchemaType(resourceName, ref schema); + + mediaTypes.Add( + "application/fhir+xml", + new OpenApiMediaType() + { + Schema = schema, + }); + } } return mediaTypes; @@ -622,6 +764,8 @@ private OpenApiOperation BuildPathOperation( { OpenApiOperation operation = new OpenApiOperation(); + string contentResourceName; + if (isInstanceLevel) { operation.OperationId = $"{pathOpType}{resourceName}I"; @@ -649,10 +793,12 @@ private OpenApiOperation BuildPathOperation( if (isInstanceLevel) { operation.OperationId = $"get{resourceName}"; + contentResourceName = resourceName; } else { operation.OperationId = $"list{resourceName}s"; + contentResourceName = "Bundle"; } if (_singleResponseCode) @@ -662,7 +808,7 @@ private OpenApiOperation BuildPathOperation( ["200"] = new OpenApiResponse() { Description = "OK", - Content = BuildResponseContent(), + Content = BuildContentMap(contentResourceName), }, }; } @@ -673,7 +819,7 @@ private OpenApiOperation BuildPathOperation( ["200"] = new OpenApiResponse() { Description = "OK", - Content = BuildResponseContent(), + Content = BuildContentMap(contentResourceName), }, ["410"] = new OpenApiResponse() { @@ -693,20 +839,27 @@ private OpenApiOperation BuildPathOperation( if (isInstanceLevel) { operation.OperationId = $"replace{resourceName}"; + contentResourceName = resourceName; } else { operation.OperationId = $"replace{resourceName}s"; + contentResourceName = resourceName; } if (_singleResponseCode) { + operation.RequestBody = new OpenApiRequestBody() + { + Content = BuildContentMap(contentResourceName), + }; + operation.Responses = new OpenApiResponses() { ["200"] = new OpenApiResponse() { Description = "OK", - Content = BuildResponseContent(), + Content = BuildContentMap(resourceName), }, }; } @@ -717,12 +870,12 @@ private OpenApiOperation BuildPathOperation( ["200"] = new OpenApiResponse() { Description = "OK", - Content = BuildResponseContent(), + Content = BuildContentMap(contentResourceName), }, ["201"] = new OpenApiResponse() { Description = "CREATED", - Content = BuildResponseContent(), + Content = BuildContentMap(contentResourceName), }, ["400"] = new OpenApiResponse() { @@ -761,20 +914,27 @@ private OpenApiOperation BuildPathOperation( if (isInstanceLevel) { operation.OperationId = $"create{resourceName}"; + contentResourceName = resourceName; } else { operation.OperationId = $"create{resourceName}s"; + contentResourceName = resourceName; } if (_singleResponseCode) { + operation.RequestBody = new OpenApiRequestBody() + { + Content = BuildContentMap(contentResourceName), + }; + operation.Responses = new OpenApiResponses() { ["200"] = new OpenApiResponse() { Description = "OK", - Content = BuildResponseContent(), + Content = BuildContentMap(contentResourceName), }, }; } @@ -785,12 +945,12 @@ private OpenApiOperation BuildPathOperation( ["200"] = new OpenApiResponse() { Description = "OK", - Content = BuildResponseContent(), + Content = BuildContentMap(contentResourceName), }, ["201"] = new OpenApiResponse() { Description = "CREATED", - Content = BuildResponseContent(), + Content = BuildContentMap(contentResourceName), }, ["400"] = new OpenApiResponse() { @@ -827,37 +987,13 @@ private OpenApiOperation BuildPathOperation( operation.OperationId = $"delete{resourceName}s"; } - if (_singleResponseCode) - { - operation.Responses = new OpenApiResponses() - { - ["200"] = new OpenApiResponse() - { - Description = "OK", - Content = BuildResponseContent(), - }, - }; - } - else + operation.Responses = new OpenApiResponses() { - operation.Responses = new OpenApiResponses() + ["204"] = new OpenApiResponse() { - ["200"] = new OpenApiResponse() - { - Description = "OK", - Content = BuildResponseContent(), - }, - ["202"] = new OpenApiResponse() - { - Description = "ACCEPTED", - Content = BuildResponseContent(), - }, - ["204"] = new OpenApiResponse() - { - Description = "NO CONTENT", - }, - }; - } + Description = "NO CONTENT", + }, + }; break;