Skip to content
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
88 changes: 88 additions & 0 deletions .refitter
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"openApiPath": "./test-anytype.json",
"openApiPaths": [],
"namespace": "TestNS",
"contractsNamespace": null,
"naming": {
"useOpenApiTitle": true,
"interfaceName": "ApiClient"
},
"generateContracts": true,
"generateClients": true,
"generateDisposableClients": false,
"generateXmlDocCodeComments": true,
"generateStatusCodeComments": true,
"addAutoGeneratedHeader": true,
"addAcceptHeaders": true,
"addContentTypeHeaders": true,
"returnIApiResponse": false,
"returnIObservable": false,
"responseTypeOverride": {},
"generateOperationHeaders": true,
"ignoredOperationHeaders": [],
"typeAccessibility": "Public",
"useCancellationTokens": false,
"useIsoDateFormat": false,
"additionalNamespaces": [],
"excludeNamespaces": [],
"multipleInterfaces": "Unset",
"includePathMatches": [],
"includeTags": [],
"generateDeprecatedOperations": true,
"operationNameTemplate": null,
"optionalParameters": false,
"outputFolder": "./Generated",
"contractsOutputFolder": "./test-output.cs",
"outputFilename": null,
"dependencyInjectionSettings": null,
"codeGeneratorSettings": {
"requiredPropertiesMustBeDefined": true,
"generateDataAnnotations": true,
"anyType": "object",
"dateType": "System.DateTimeOffset",
"dateTimeType": "System.DateTimeOffset",
"timeType": "System.TimeSpan",
"timeSpanType": "System.TimeSpan",
"integerType": "Int32",
"arrayType": "System.Collections.Generic.ICollection",
"dictionaryType": "System.Collections.Generic.IDictionary",
"arrayInstanceType": "System.Collections.ObjectModel.Collection",
"dictionaryInstanceType": "System.Collections.Generic.Dictionary",
"arrayBaseType": "System.Collections.ObjectModel.Collection",
"dictionaryBaseType": "System.Collections.Generic.Dictionary",
"propertySetterAccessModifier": "",
"jsonConverters": null,
"generateImmutableArrayProperties": false,
"generateImmutableDictionaryProperties": false,
"handleReferences": false,
"jsonSerializerSettingsTransformationMethod": null,
"generateJsonMethods": false,
"enforceFlagEnums": false,
"inlineNamedDictionaries": false,
"inlineNamedTuples": true,
"inlineNamedArrays": false,
"generateOptionalPropertiesAsNullable": false,
"generateNullableReferenceTypes": false,
"generateNativeRecords": false,
"generateDefaultValues": true,
"inlineNamedAny": false,
"excludedTypeNames": [],
"dateFormat": null,
"dateTimeFormat": null,
"inlineJsonConverters": true,
"customTemplateDirectory": null
},
"trimUnusedSchema": false,
"keepSchemaPatterns": [],
"includeInheritanceHierarchy": false,
"operationNameGenerator": "Default",
"generateDefaultAdditionalProperties": true,
"immutableRecords": false,
"apizrSettings": null,
"useDynamicQuerystringParameters": false,
"generateMultipleFiles": false,
"usePolymorphicSerialization": false,
"generateAuthenticationHeader": false,
"collectionFormat": "Multi",
"customTemplateDirectory": null
}
6 changes: 6 additions & 0 deletions docs/format-mappings-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"int32": "int",
"int64": "ulong",
"float": "float",
"double": "double"
}
24 changes: 24 additions & 0 deletions docs/format-mappings-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Refitter Format Mappings",
"description": "Custom format-to-type mappings for OpenAPI schema formats. Use this to override the default C# type mappings for specific OpenAPI formats.",
"type": "object",
"additionalProperties": {
"type": "string",
"description": "The .NET type name to use for this format"
},
"examples": [
{
"int32": "int",
"int64": "ulong",
"float": "float",
"double": "double",
"byte": "byte",
"binary": "byte[]",
"date": "System.DateTime",
"date-time": "System.DateTime",
"password": "string",
"uuid": "System.Guid"
}
]
}
57 changes: 57 additions & 0 deletions src/Refitter.Core/ContractTypeSuffixApplier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Text.RegularExpressions;

namespace Refitter.Core;

internal static class ContractTypeSuffixApplier
{
private static readonly Regex TypeDeclarationRegex =
new(@"(?:public|internal)\s+(?:partial\s+)?(?:class|record|struct|interface)\s+(\w+)", RegexOptions.Multiline | RegexOptions.Compiled);

private static readonly Regex EnumDeclarationRegex =
new(@"(?:public|internal)\s+(?:partial\s+)?enum\s+(\w+)", RegexOptions.Multiline | RegexOptions.Compiled);

public static string ApplySuffix(string generatedCode, string suffix)
{
if (string.IsNullOrWhiteSpace(suffix))
return generatedCode;

var typeNames = new HashSet<string>();

// First pass: collect all type names (classes, records, structs, enums)
foreach (Match match in TypeDeclarationRegex.Matches(generatedCode))
{
typeNames.Add(match.Groups[1].Value);
}

foreach (Match match in EnumDeclarationRegex.Matches(generatedCode))
{
typeNames.Add(match.Groups[1].Value);
}

// Second pass: replace type names with suffixed versions
// Sort by length (longest first) to avoid partial replacements
var orderedTypeNames = typeNames.OrderByDescending(t => t.Length).ToList();
var result = generatedCode;

foreach (var typeName in orderedTypeNames)
{
var suffixedName = typeName + suffix;

// Replace type declarations
result = Regex.Replace(
result,
$@"(?:public|internal)\s+((?:partial\s+)?(?:class|record|struct|enum))\s+\b{Regex.Escape(typeName)}\b",
m => $"{m.Groups[0].Value.Replace(typeName, suffixedName)}",
RegexOptions.Multiline);

// Replace type references in inheritance, properties, parameters, etc.
// Match word boundaries to avoid partial replacements
result = Regex.Replace(
result,
$@"\b{Regex.Escape(typeName)}\b(?![a-zA-Z0-9_])",
suffixedName);
}

return result;
}
}
38 changes: 38 additions & 0 deletions src/Refitter.Core/CustomCSharpTypeResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using NJsonSchema;
using NJsonSchema.CodeGeneration;
using NJsonSchema.CodeGeneration.CSharp;

namespace Refitter.Core;

internal class CustomCSharpTypeResolver : CSharpTypeResolver
{
private readonly Dictionary<string, string>? formatMappings;

public CustomCSharpTypeResolver(
CSharpGeneratorSettings settings,
Dictionary<string, string>? formatMappings)
: base(settings)
{
this.formatMappings = formatMappings;
}

public override string Resolve(
JsonSchema schema,
bool isNullable,
string? typeNameHint)
{
// Check if this schema has a format with a custom mapping
if (formatMappings != null &&
!string.IsNullOrEmpty(schema.Format) &&
formatMappings.TryGetValue(schema.Format, out var mappedType))
{
// Return the custom mapped type with nullability
return isNullable && !mappedType.EndsWith("?") && !mappedType.Contains("<")
? $"{mappedType}?"
: mappedType;
}

// Fall back to default NSwag type resolution
return base.Resolve(schema, isNullable, typeNameHint);
}
}
75 changes: 75 additions & 0 deletions src/Refitter.Core/JsonSerializerContextGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Text;
using System.Text.RegularExpressions;

namespace Refitter.Core;

/// <summary>
/// Generates JsonSerializerContext for AOT compilation support
/// </summary>
internal static class JsonSerializerContextGenerator
{
/// <summary>
/// Generates a JsonSerializerContext class with all DTO types registered for source generation
/// </summary>
/// <param name="contracts">The generated contracts code containing the DTO types</param>
/// <param name="settings">The generator settings</param>
/// <returns>The generated JsonSerializerContext code</returns>
public static string Generate(string contracts, RefitGeneratorSettings settings)
{
var typeNames = ExtractTypeNames(contracts);
if (typeNames.Count == 0)
return string.Empty;

var contextName = $"{settings.Naming.InterfaceName}SerializerContext";
var sb = new StringBuilder();

// Add all JsonSerializable attributes
foreach (var typeName in typeNames.OrderBy(t => t))
{
sb.AppendLine($"[JsonSerializable(typeof({typeName}))]");
}

// Add context class
sb.AppendLine($"internal partial class {contextName} : JsonSerializerContext");
sb.AppendLine("{");
sb.AppendLine("}");

return sb.ToString();
}

/// <summary>
/// Extracts type names (classes, records, enums) from generated contracts
/// </summary>
private static HashSet<string> ExtractTypeNames(string contracts)
{
var typeNames = new HashSet<string>();

// Match class declarations: public class TypeName, internal class TypeName
var classPattern = new Regex(
@"^\s*(?:public|internal)\s+(?:partial\s+)?(?:class|record)\s+([A-Za-z_][A-Za-z0-9_]*)",
RegexOptions.Multiline);

foreach (Match match in classPattern.Matches(contracts))
{
if (match.Success && match.Groups.Count > 1)
{
typeNames.Add(match.Groups[1].Value);
}
}

// Match enum declarations: public enum TypeName, internal enum TypeName
var enumPattern = new Regex(
@"^\s*(?:public|internal)\s+enum\s+([A-Za-z_][A-Za-z0-9_]*)",
RegexOptions.Multiline);

foreach (Match match in enumPattern.Matches(contracts))
{
if (match.Success && match.Groups.Count > 1)
{
typeNames.Add(match.Groups[1].Value);
}
}

return typeNames;
}
}
Loading
Loading