RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.73k stars 1.29k forks source link

TypeScript client generator not generating multiple enums for same parameter names (OData) #4816

Open lorenyaSICKAG opened 6 months ago

lorenyaSICKAG commented 6 months ago

Hello,

in OData there are several default query parameters (like $expand). But having multiple endpoints with the same name of query parameters ($expand), the TypeScript client generator generates only ONE _expand enum type instead of multiple types. I would say it should be fine, if it would generate multiple enum types with incrementing numeric suffixes like (_expand1, _expand2 and so on).

Example Endpoint in OpenAPI specification:

{
  "/odata/Sections":{
    "description":"Provides operations to manage the collection of Section entities.",
    "get":{
      "tags":[
        "Sections.Section"
      ],
      "summary":"Get entities from Sections",
      "operationId":"Sections.Section.ListSection",
      "parameters":[
        {
          "$ref":"#/components/parameters/top"
        },
        {
          "$ref":"#/components/parameters/skip"
        },
        {
          "$ref":"#/components/parameters/search"
        },
        {
          "$ref":"#/components/parameters/filter"
        },
        {
          "$ref":"#/components/parameters/count"
        },
        {
          "name":"$orderby",
          "in":"query",
          "description":"Order items by property values",
          "style":"form",
          "explode":false,
          "schema":{
            "uniqueItems":true,
            "type":"array",
            "items":{
              "enum":[
                "Id",
                "Id desc",
                "Name",
                "Name desc",
                "DepartmentId",
                "DepartmentId desc",
                "ParentId",
                "ParentId desc",
                "Created",
                "Created desc",
                "LastModified",
                "LastModified desc"
              ],
              "type":"string"
            }
          }
        },
        {
          "name":"$select",
          "in":"query",
          "description":"Select properties to be returned",
          "style":"form",
          "explode":false,
          "schema":{
            "uniqueItems":true,
            "type":"array",
            "items":{
              "enum":[
                "Id",
                "Name",
                "DepartmentId",
                "ParentId",
                "Created",
                "LastModified",
                "Department",
                "Pages",
                "Parent",
                "Children"
              ],
              "type":"string"
            }
          }
        },
        {
          "name":"$expand",
          "in":"query",
          "description":"Expand related entities",
          "style":"form",
          "explode":false,
          "schema":{
            "uniqueItems":true,
            "type":"array",
            "items":{
              "enum":[
                "*",
                "Department",
                "Pages",
                "Parent",
                "Children"
              ],
              "type":"string"
            }
          }
        }
      ],
      "responses":{
        "200":{
          "$ref":"#/components/responses/DigitalShopfloor.Backend.Data.Entities.SectionCollectionResponse"
        },
        "default":{
          "$ref":"#/components/responses/error"
        }
      }
    },
    "post":{
      "tags":[
        "Sections.Section"
      ],
      "summary":"Add new entity to Sections",
      "operationId":"Sections.Section.CreateSection",
      "requestBody":{
        "description":"New entity",
        "content":{
          "application/json":{
            "schema":{
              "$ref":"#/components/schemas/DigitalShopfloor.Backend.Data.Entities.Section"
            }
          }
        },
        "required":true
      },
      "responses":{
        "201":{
          "description":"Created entity",
          "content":{
            "application/json":{
              "schema":{
                "$ref":"#/components/schemas/DigitalShopfloor.Backend.Data.Entities.Section"
              }
            }
          }
        },
        "default":{
          "$ref":"#/components/responses/error"
        }
      },
      "x-ms-docs-operation-type":"operation"
    }
  }
}

Regards

lorenyaSICKAG commented 6 months ago

I workaround' this issue by creating such class and using it in the TypeScriptGeneratorSettings:

public class ODataTypeNameGenerator : TypeScriptTypeNameGenerator
{

    /// <inheritdoc />
    public override string Generate(JsonSchema schema, string typeNameHint, IEnumerable<string> reservedTypeNames)
    {
        // Replace illegal characters first when its an odata query parameter
        if (typeNameHint.StartsWith("$"))
            typeNameHint = RemoveIllegalCharacters(typeNameHint);

        return base.Generate(schema, typeNameHint, reservedTypeNames);
    }

    /// <summary>
    /// Replaces all characters that are not normals letters, numbers or underscore, with an underscore.
    /// Will prepend an underscore if the first characters is a number.
    /// In case there are this would result in multiple underscores in a row, strips down to one underscore.
    /// Will trim any underscores at the end of the type name.
    /// </summary>
    private static string RemoveIllegalCharacters(string typeName)
    {
        // TODO: Find a way to support unicode characters up to 3.0

        // first check if all are valid and we skip altogether
        var invalid = false;
        for (var i = 0; i < typeName.Length; i++)
        {
            var c = typeName[i];
            if (i == 0 && (!IsEnglishLetterOrUnderScore(c) || char.IsDigit(c)))
            {
                invalid = true;
                break;
            }

            if (!IsEnglishLetterOrUnderScore(c) && !char.IsDigit(c))
            {
                invalid = true;
                break;
            }
        }

        if (!invalid)
        {
            return typeName;
        }

        return DoRemoveIllegalCharacters(typeName);
    }

    private static string DoRemoveIllegalCharacters(string typeName)
    {
        var firstCharacter = typeName[0];
        var regexInvalidCharacters = new Regex("\\W");

        var legalTypeName = new StringBuilder(typeName);
        if (!IsEnglishLetterOrUnderScore(firstCharacter) || firstCharacter == '_')
        {
            if (!regexInvalidCharacters.IsMatch(firstCharacter.ToString()))
            {
                legalTypeName.Insert(0, "_");
            }
            else
            {
                legalTypeName[0] = '_';
            }
        }

        var illegalMatches = regexInvalidCharacters.Matches(legalTypeName.ToString());

        for (int i = illegalMatches.Count - 1; i >= 0; i--)
        {
            var illegalMatchIndex = illegalMatches[i].Index;
            legalTypeName[illegalMatchIndex] = '_';
        }

        var regexMoreThanOneUnderscore = new Regex("[_]{2,}");

        var legalTypeNameString = regexMoreThanOneUnderscore.Replace(legalTypeName.ToString(), "_");
        return legalTypeNameString.TrimEnd('_');
    }

    private static bool IsEnglishLetterOrUnderScore(char c)
    {
        return c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or '_';
    }

}