Skip to content

Fixes generating TypeSpec files #1146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private async Task<bool> IsEnabledInternalAsync()
_logger.LogError("Model is not set. Language model will be disabled");
return false;
}

_logger.LogDebug("Checking LM availability at {url}...", _configuration.Url);

try
Expand Down Expand Up @@ -132,14 +132,17 @@ private async Task<bool> IsEnabledInternalAsync()
options
}
);
_logger.LogDebug("Response: {response}", response.StatusCode);
_logger.LogDebug("Response status: {response}", response.StatusCode);

var res = await response.Content.ReadFromJsonAsync<OllamaLanguageModelCompletionResponse>();
if (res is null)
{
_logger.LogDebug("Response: null");
return res;
}

_logger.LogDebug("Response: {response}", res.Response);

res.RequestUrl = url;
return res;
}
Expand Down Expand Up @@ -217,7 +220,7 @@ private async Task<bool> IsEnabledInternalAsync()
}
);
_logger.LogDebug("Response: {response}", response.StatusCode);

var res = await response.Content.ReadFromJsonAsync<OllamaLanguageModelChatCompletionResponse>();
if (res is null)
{
Expand Down
162 changes: 143 additions & 19 deletions dev-proxy-plugins/RequestLogs/TypeSpecGeneratorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Web;

namespace DevProxy.Plugins.RequestLogs;
Expand Down Expand Up @@ -113,7 +114,7 @@ request.Method is null ||
var rootModel = models.Last();
op.Parameters.Add(new()
{
Name = (rootModel.IsArray ? (await MakeSingular(rootModel.Name)) : rootModel.Name).ToCamelCase(),
Name = await GetParameterName(rootModel),
Value = rootModel.Name,
In = ParameterLocation.Body
});
Expand Down Expand Up @@ -170,7 +171,7 @@ request.Method is null ||
var rootModel = models.Last();
if (rootModel.IsArray)
{
res.BodyType = $"{await MakeSingular(rootModel.Name)}[]";
res.BodyType = $"{rootModel.Name}[]";
op.Name = await GetOperationName("list", url);
}
else
Expand Down Expand Up @@ -212,6 +213,22 @@ request.Method is null ||
e.GlobalData[GeneratedTypeSpecFilesKey] = generatedTypeSpecFiles;
}

private async Task<string> GetParameterName(Model model)
{
Logger.LogTrace("Entered GetParameterName");

var name = model.IsArray ? SanitizeName(await MakeSingular(model.Name)) : model.Name;
if (string.IsNullOrEmpty(name))
{
name = model.Name;
}

Logger.LogDebug("Parameter name: {name}", name);
Logger.LogTrace("Left GetParameterName");

return name;
}

private async Task<TypeSpecFile> GetOrCreateTypeSpecFile(List<TypeSpecFile> files, Uri url)
{
Logger.LogTrace("Entered GetOrCreateTypeSpecFile");
Expand Down Expand Up @@ -252,7 +269,11 @@ private string GetRootNamespaceName(Uri url)
{
Logger.LogTrace("Entered GetRootNamespaceName");

var ns = string.Join("", url.Host.Split('.').Select(x => x.ToPascalCase()));
var ns = SanitizeName(string.Join("", url.Host.Split('.').Select(x => x.ToPascalCase())));
if (string.IsNullOrEmpty(ns))
{
ns = GetRandomName();
}

Logger.LogDebug("Root namespace name: {ns}", ns);
Logger.LogTrace("Left GetRootNamespaceName");
Expand All @@ -268,14 +289,47 @@ private async Task<string> GetOperationName(string method, Uri url)
Logger.LogDebug("Url: {url}", url);
Logger.LogDebug("Last non-parametrizable segment: {lastSegment}", lastSegment);

var operationName = $"{method.ToLowerInvariant()}{(method == "list" ? lastSegment : await MakeSingular(lastSegment)).ToPascalCase()}";
var name = method == "list" ? lastSegment : await MakeSingular(lastSegment);
if (string.IsNullOrEmpty(name))
{
name = lastSegment;
}
name = SanitizeName(name);
if (string.IsNullOrEmpty(name))
{
name = SanitizeName(lastSegment);
if (string.IsNullOrEmpty(name))
{
name = GetRandomName();
}
}

var operationName = $"{method.ToLowerInvariant()}{name.ToPascalCase()}";
var sanitizedName = SanitizeName(operationName);
if (!string.IsNullOrEmpty(sanitizedName))
{
Logger.LogDebug("Sanitized operation name: {sanitizedName}", sanitizedName);
operationName = sanitizedName;
}

Logger.LogDebug("Operation name: {operationName}", operationName);
Logger.LogTrace("Left GetOperationName");

return operationName;
}

private string GetRandomName()
{
Logger.LogTrace("Entered GetRandomName");

var name = Guid.NewGuid().ToString("N");

Logger.LogDebug("Random name: {name}", name);
Logger.LogTrace("Left GetRandomName");

return name;
}

private async Task<string> GetOperationDescription(string method, Uri url)
{
Logger.LogTrace("Entered GetOperationDescription");
Expand Down Expand Up @@ -392,7 +446,16 @@ private bool IsParametrizable(string segment)
}
else
{
previousSegment = (await MakeSingular(segmentTrimmed)).ToCamelCase();
previousSegment = SanitizeName(await MakeSingular(segmentTrimmed));
if (string.IsNullOrEmpty(previousSegment))
{
previousSegment = SanitizeName(segmentTrimmed);
if (previousSegment.Length == 0)
{
previousSegment = GetRandomName();
}
}
previousSegment = previousSegment.ToCamelCase();
route.Add(segmentTrimmed);
}
}
Expand Down Expand Up @@ -457,12 +520,6 @@ private async Task<string> AddModelFromJsonElement(JsonElement jsonElement, stri
{
Logger.LogTrace("Entered AddModelFromJsonElement");

var model = new Model
{
Name = await MakeSingular(name),
IsError = isError
};

switch (jsonElement.ValueKind)
{
case JsonValueKind.String:
Expand All @@ -483,6 +540,12 @@ private async Task<string> AddModelFromJsonElement(JsonElement jsonElement, stri
return "Empty";
}

var model = new Model
{
Name = await GetModelName(name),
IsError = isError
};

foreach (var p in jsonElement.EnumerateObject())
{
var property = new ModelProperty
Expand All @@ -495,16 +558,50 @@ private async Task<string> AddModelFromJsonElement(JsonElement jsonElement, stri
models.Add(model);
return model.Name;
case JsonValueKind.Array:
await AddModelFromJsonElement(jsonElement.EnumerateArray().FirstOrDefault(), name, isError, models);
model.IsArray = true;
model.Name = name;
models.Add(model);
return $"{name}[]";
// we need to create a model for each item in the array
// in case some items have null values or different shapes
// we'll merge them later
var modelName = string.Empty;
foreach (var item in jsonElement.EnumerateArray())
{
modelName = await AddModelFromJsonElement(item, name, isError, models);
}
models.Add(new Model
{
Name = modelName,
IsError = isError,
IsArray = true,
});
return $"{modelName}[]";
case JsonValueKind.Null:
return "null";
default:
return string.Empty;
}
}

private async Task<string> GetModelName(string name)
{
Logger.LogTrace("Entered GetModelName");

var modelName = SanitizeName(await MakeSingular(name));
if (string.IsNullOrEmpty(modelName))
{
modelName = SanitizeName(name);
if (string.IsNullOrEmpty(modelName))
{
modelName = GetRandomName();
}
}

modelName = modelName.ToPascalCase();

Logger.LogDebug("Model name: {modelName}", modelName);
Logger.LogTrace("Left GetModelName");

return modelName;
}

private async Task<string> MakeSingular(string noun)
{
Logger.LogTrace("Entered MakeSingular");
Expand All @@ -517,11 +614,26 @@ private async Task<string> MakeSingular(string noun)
}
var singular = singularNoun?.Response;

if (singular is null ||
string.IsNullOrEmpty(singular) ||
if (string.IsNullOrEmpty(singular) ||
singular.Contains(' '))
{
singular = noun.EndsWith('s') && !noun.EndsWith("ss") ? noun[0..^1] : noun;
if (noun.EndsWith("ies"))
{
singular = noun[0..^3] + 'y';
}
else if (noun.EndsWith("es"))
{
singular = noun[0..^2];
}
else if (noun.EndsWith('s') && !noun.EndsWith("ss"))
{
singular = noun[0..^1];
}
else
{
singular = noun;
}

Logger.LogDebug("Failed to get singular form of {noun} from LLM. Using fallback: {singular}", noun, singular);
}

Expand All @@ -530,4 +642,16 @@ private async Task<string> MakeSingular(string noun)

return singular;
}

private string SanitizeName(string name)
{
Logger.LogTrace("Entered SanitizeName");

var sanitized = Regex.Replace(name, "[^a-zA-Z0-9_]", "");

Logger.LogDebug("Sanitized name: {name} to: {sanitized}", name, sanitized);
Logger.LogTrace("Left SanitizeName");

return sanitized;
}
}
2 changes: 1 addition & 1 deletion dev-proxy-plugins/TypeSpec/Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ override public string ToString()
internal class ModelProperty
{
public required string Name { get; init; }
public required string Type { get; init; }
public required string Type { get; set; }

override public string ToString()
{
Expand Down
7 changes: 6 additions & 1 deletion dev-proxy-plugins/TypeSpec/TypeSpecExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ public static string MergeModel(this Namespace ns, Model model)

foreach (var prop in model.Properties)
{
if (!existingModel.Properties.Any(p => p.Name == prop.Name))
var existingProp = existingModel.Properties.FirstOrDefault(p => p.Name == prop.Name);
if (existingProp is null)
{
existingModel.Properties.Add(prop);
}
else if (existingProp.Type == "null")
{
existingProp.Type = prop.Type;
}
}

return existingModel.Name;
Expand Down
Loading