File: Modeler\ServiceClientModelFactory.cs
Web Access
Project: src\src\Microsoft.DotNet.SwaggerGenerator\Microsoft.DotNet.SwaggerGenerator.CodeGenerator\Microsoft.DotNet.SwaggerGenerator.CodeGenerator.csproj (Microsoft.DotNet.SwaggerGenerator.CodeGenerator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
 
namespace Microsoft.DotNet.SwaggerGenerator.Modeler
{
    public class ServiceClientModelFactory
    {
        public static OpenApiDocument ReadDocument(Stream docStream, out OpenApiDiagnostic diagnostic)
        {
            var reader = new OpenApiStreamReader(
                new OpenApiReaderSettings
                {
                    ReferenceResolution = ReferenceResolutionSetting.ResolveLocalReferences,
                    ExtensionParsers =
                    {
                        ["x-ms-enum"] = EnumOpenApiExtension.Parse,
                        ["x-ms-paginated"] = PaginatedOpenApiExtension.Parse,
                    },
                });
 
            return reader.Read(docStream, out diagnostic);
        }
 
        private readonly Dictionary<string, EnumTypeModel> _enumTypeModels = new Dictionary<string, EnumTypeModel>();
 
        private readonly GeneratorOptions _generatorOptions;
 
        private readonly AsyncLocal<Stack<string>> _propertyNameStack = new AsyncLocal<Stack<string>>();
        private readonly AsyncLocal<Stack<string>> _typeNameStack = new AsyncLocal<Stack<string>>();
        private readonly AsyncLocal<Stack<string>> _parameterNameStack = new AsyncLocal<Stack<string>>();
        private readonly AsyncLocal<Stack<string>> _methodNameStack = new AsyncLocal<Stack<string>>();
 
        private readonly Dictionary<string, TypeModel> _types = new Dictionary<string, TypeModel>();
        private OpenApiDocument _rootDocument = null;
 
        public ServiceClientModelFactory(GeneratorOptions options)
        {
            _generatorOptions = options;
            _typeNameStack.Value = new Stack<string>();
            _propertyNameStack.Value = new Stack<string>();
            _parameterNameStack.Value = new Stack<string>();
            _methodNameStack.Value = new Stack<string>();
        }
        private string CurrentMethodName => _methodNameStack.Value.Count != 0 ? _methodNameStack.Value.Peek() : null;
 
        private string CurrentParameterName => _parameterNameStack.Value.Count != 0 ? _parameterNameStack.Value.Peek() : "Value";
 
        private string CurrentTypeName => _typeNameStack.Value.Count != 0 ? _typeNameStack.Value.Peek() : null;
 
        private string CurrentPropertyName => _propertyNameStack.Value.Count != 0 ? _propertyNameStack.Value.Peek() : "Value";
 
        private IDisposable WithMethodName(string name)
        {
            _methodNameStack.Value.Push(name);
            return Disposable.Create(
                () =>
                {
                    if (_methodNameStack.Value.Pop() != name)
                    {
                        throw new InvalidOperationException(
                            $"Method name '{name}' popped when it wasn't the top of the stack.");
                    }
                });
        }
 
        private IDisposable WithParameterName(string name)
        {
            _parameterNameStack.Value.Push(name);
            return Disposable.Create(
                () =>
                {
                    if (_parameterNameStack.Value.Pop() != name)
                    {
                        throw new InvalidOperationException(
                            $"Parameter name '{name}' popped when it wasn't the top of the stack.");
                    }
                });
        }
 
        private IDisposable WithTypeName(string name)
        {
            _typeNameStack.Value.Push(name);
            return Disposable.Create(
                () =>
                {
                    if (_typeNameStack.Value.Pop() != name)
                    {
                        throw new InvalidOperationException(
                            $"Type name '{name}' popped when it wasn't the top of the stack.");
                    }
                });
        }
 
        private IDisposable WithPropertyName(string name)
        {
            _propertyNameStack.Value.Push(name);
            return Disposable.Create(
                () =>
                {
                    if (_propertyNameStack.Value.Pop() != name)
                    {
                        throw new InvalidOperationException(
                            $"Property name '{name}' popped when it wasn't the top of the stack.");
                    }
                });
        }
 
        public ServiceClientModel Create(OpenApiDocument document)
        {
            _rootDocument = document;
            GeneratorOptions options = _generatorOptions;
            ImmutableList<MethodGroupModel> methodGroups = document.Paths
                .SelectMany(p => p.Value.Operations.Select(o => (path: p.Key, type: o.Key, operation: o.Value)))
                .ToLookup(t => t.operation.Tags.FirstOrDefault()?.Name)
                .Select(g => CreateMethodGroupModel(g.Key, g))
                .ToImmutableList();
            return new ServiceClientModel(
                options.ClientName,
                options.Namespace,
                document.Servers.First().Url,
                _types.Values.Concat(_enumTypeModels.Values).OrderBy(m => m.Name),
                methodGroups);
        }
 
        private MethodGroupModel CreateMethodGroupModel(
            string name,
            IEnumerable<(string path, OperationType type, OpenApiOperation operation)> operations)
        {
            var methods = new List<MethodModel>();
            foreach ((string path, OperationType type, OpenApiOperation operation) in operations)
            {
                methods.Add(CreateMethodModel(path, type, operation));
            }
 
            return new MethodGroupModel(name, _generatorOptions.Namespace, methods);
        }
 
        private MethodModel CreateMethodModel(string path, OperationType type, OpenApiOperation operation)
        {
 
            string name = operation.OperationId;
            string firstTag = operation.Tags.FirstOrDefault()?.Name;
            if (firstTag != null && name.StartsWith(firstTag))
            {
                name = name.Substring(firstTag.Length);
            }
 
            name = name.TrimStart('_');
            using (WithMethodName(name))
            {
                IList<ParameterModel> parameters = (operation.Parameters ?? Array.Empty<OpenApiParameter>())
                    .Select(CreateParameterModel)
                    .ToList();
                if (operation.RequestBody != null)
                {
                    parameters.Add(CreateParameterModel(operation.RequestBody));
                }
 
                TypeReference errorType = null;
 
                if (operation.Responses.TryGetValue("default", out OpenApiResponse errorResponse))
                {
                    errorType = ResolveTypeForResponse(errorResponse, name);
                }
 
                TypeReference responseType = null;
                var selectedResponse = operation.Responses.Where(r => r.Key.StartsWith("2")).Select(p => p.Value).FirstOrDefault();
 
                if (selectedResponse != null)
                {
                    responseType = ResolveTypeForResponse(selectedResponse, name);
                }
 
                if (responseType == null)
                {
                    responseType = TypeReference.Void;
                }
 
                PaginatedOpenApiExtension paginated = null;
 
                if (responseType is TypeReference.ArrayTypeReference &&
                    type == OperationType.Get &&
                    operation.Extensions.ContainsKey("x-ms-paginated"))
                {
                    paginated = operation.Extensions["x-ms-paginated"] as PaginatedOpenApiExtension;
                }
 
                return new MethodModel(name, path, GetHttpMethod(type), responseType, errorType, parameters, paginated);
            }
        }
 
        private TypeReference ResolveTypeForResponse(OpenApiResponse response, string name)
        {
            var schema = response.Content.Values.Select(t => t.Schema).FirstOrDefault(s => s != null);
 
            if (schema != null)
            {
                return ResolveType(schema, name);
            }
 
            return null;
        }
 
        private HttpMethod GetHttpMethod(OperationType type)
        {
            switch (type)
            {
                case OperationType.Get:
                    return HttpMethod.Get;
                case OperationType.Put:
                    return HttpMethod.Put;
                case OperationType.Post:
                    return HttpMethod.Post;
                case OperationType.Delete:
                    return HttpMethod.Delete;
                case OperationType.Options:
                    return HttpMethod.Options;
                case OperationType.Head:
                    return HttpMethod.Head;
                case OperationType.Patch:
                    return new HttpMethod("PATCH");
                default:
                    throw new NotSupportedException(type.ToString());
            }
        }
 
        private ParameterModel CreateParameterModel(OpenApiRequestBody body)
        {
            const string parameterName = "body"; // TODO: Get parameter name from the request body object once https://github.com/Microsoft/OpenAPI.NET/issues/378 is fixed
            using (WithParameterName(parameterName))
            {
                TypeReference type = ResolveType(body.Content["application/json"].Schema);
 
                return new ParameterModel(parameterName, body.Required, null, type);
            }
        }
 
        private ParameterModel CreateParameterModel(OpenApiParameter parameter)
        {
            using (WithParameterName(parameter.Name))
            {
                TypeReference type = ResolveType(parameter.Schema);
                return new ParameterModel(parameter.Name, parameter.Required, parameter.In, type);
            }
        }
 
        private TypeReference ResolveType(OpenApiSchema schema)
        {
            if (schema == null)
            {
                return TypeReference.Void;
            }
 
            if (schema.UnresolvedReference)
            {
                schema = (OpenApiSchema) _rootDocument.ResolveReference(schema.Reference);
            }
 
            string type = schema.Type;
            string format = schema.Format;
            OpenApiSchema items = schema.Items;
            IList<string> enumeration = schema.Enum.OfType<OpenApiString>().Select(s => s.Value).ToList();
            IDictionary<string, IOpenApiExtension> extensions = schema.Extensions;
 
            switch (type)
            {
                case "boolean":
                    return TypeReference.Boolean;
                case "integer":
                    switch (format)
                    {
                        case "int32":
                            return TypeReference.Int32;
                        case "int64":
                        default:
                            return TypeReference.Int64;
                    }
                case "number":
                    switch (format)
                    {
                        case "float":
                            return TypeReference.Float;
                        case "double":
                        default:
                            return TypeReference.Double;
                    }
                case "string":
                    if (enumeration.Any())
                    {
                        if (enumeration.Count == 1)
                        {
                            return TypeReference.Constant(enumeration[0]);
                        }
 
                        string enumName = schema.Reference?.Id;
                        if (extensions.TryGetValue("x-ms-enum", out IOpenApiExtension ext) && ext is EnumOpenApiExtension enumExtension)
                        {
                            enumName = enumExtension.Name;
                        }
 
                        if (enumName == null)
                        {
                            if (CurrentTypeName != null)
                            {
                                enumName =
                                    $"{Helpers.PascalCase(CurrentTypeName.AsSpan())}{Helpers.PascalCase(CurrentPropertyName.AsSpan())}";
                            }
                            else
                            {
                                enumName =
                                    $"{Helpers.PascalCase(CurrentMethodName.AsSpan())}{Helpers.PascalCase(CurrentParameterName.AsSpan())}";
 
                            }
                        }
 
 
                        return TypeReference.Object(ResolveEnumTypeModel(enumeration, enumName));
                    }
 
                    switch (format)
                    {
                        case "byte":
                            return TypeReference.Byte;
                        case "date":
                            return TypeReference.Date;
                        case "date-time":
                            return TypeReference.DateTime;
                        case "uuid":
                            return TypeReference.Uuid;
                        default:
                            return TypeReference.String;
                    }
                case "array":
                    return TypeReference.Array(ResolveType(items));
                case "object":
                    bool hasProperties = schema.Properties?.Count > 0;
                    bool hasAdditionalProperties = schema.AdditionalProperties != null;
                    if (!hasProperties && hasAdditionalProperties)
                    {
                        return TypeReference.Dictionary(ResolveType(schema.AdditionalProperties));
                    }
 
                    if (!hasProperties)
                    {
                        return TypeReference.Any;
                    }
 
                    return TypeReference.Object(ResolveTypeModel(schema));
                case "file":
                    return TypeReference.File;
                case null:
                    return TypeReference.Any;
                case "null":
                default:
                    throw new NotSupportedException(type);
            }
        }
 
        private TypeModel ResolveEnumTypeModel(IList<string> enumeration, string enumName)
        {
            if (!_enumTypeModels.TryGetValue(enumName, out EnumTypeModel value))
            {
                _enumTypeModels[enumName] = value = new EnumTypeModel(
                    enumName,
                    _generatorOptions.Namespace,
                    enumeration.Select(v => v));
            }
 
            return value;
        }
 
        private TypeReference ResolveType(OpenApiSchema schema, string operationId)
        {
            using (WithTypeName(operationId))
            using (WithPropertyName("Response"))
            {
                return ResolveType(schema);
            }
        }
 
        private TypeModel ResolveTypeModel(OpenApiSchema schema)
        {
            if (schema.Type != "object")
            {
                throw new ArgumentException("Schema must be object", nameof(schema));
            }
 
            string id = schema.Reference?.Id;
            if (id == null)
            {
                id = Helpers.PascalCase((CurrentTypeName + "-" + CurrentPropertyName).AsSpan());
            }
 
            if (_types.TryGetValue(id, out TypeModel model))
            {
                return model;
            }
 
            return _types[id] = CreateTypeModel(id, schema);
        }
 
        private TypeModel CreateTypeModel(string name, OpenApiSchema schema)
        {
            using (WithTypeName(name))
            {
                ISet<string> requiredProperties = schema.Required;
                IEnumerable<PropertyModel> properties = schema.Properties != null
                    ? schema.Properties.Select(
                        p =>
                        {
                            using (WithPropertyName(p.Key))
                            {
                                return CreatePropertyModel(
                                    p.Key,
                                    p.Value,
                                    requiredProperties?.Contains(p.Key) ?? false);
                            }
                        })
                    : Array.Empty<PropertyModel>();
                TypeReference additionalProperties;
                using (WithPropertyName("Items"))
                {
                    additionalProperties = schema.AdditionalProperties != null
                        ? ResolveType(schema.AdditionalProperties)
                        : null;
                }
 
                return new ClassTypeModel(name, _generatorOptions.Namespace, properties, additionalProperties);
            }
        }
 
        private PropertyModel CreatePropertyModel(string name, OpenApiSchema type, bool required)
        {
            var propertyType = ResolveType(type);
            return new PropertyModel(name, required, type.ReadOnly, propertyType);
        }
    }
}