Skip to content

Commit

Permalink
CodeName and its conventions.
Browse files Browse the repository at this point in the history
  • Loading branch information
Corniel committed Jun 14, 2024
1 parent 994d0e3 commit 326c3b9
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 65 deletions.
75 changes: 75 additions & 0 deletions specs/Qowaiv.CodeGeneration.Specs/Code_name_covention_specs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace Code_name_covention_specs;

public class Splits
{
[TestCase("HelloWorld", "Hello", "World")]
[TestCase("helloWorld", "Hello", "World")]
[TestCase("iAmNuts", "I", "Am", "Nuts")]
[TestCase("XMLIsOld", "XML", "Is", "Old")]
[TestCase("XMLIsOLD", "XML", "Is", "OLD")]
public void With_Pascal_case(string str, params string[] parts)
=> CodeNameConvention.PascalCase.Split(str).Should().BeEquivalentTo(parts);

[TestCase("HelloWorld", "hello", "World")]
[TestCase("helloWorld", "hello", "World")]
[TestCase("iAmNuts", "i", "Am", "Nuts")]
[TestCase("XMLIsOld", "xml", "Is", "Old")]
[TestCase("XMLIsOLD", "xml", "Is", "OLD")]
public void With_Camel_case(string str, params string[] parts)
=> CodeNameConvention.CamelCase.Split(str).Should().BeEquivalentTo(parts);

[TestCase("Hello-World", "hello", "world")]
[TestCase("hello--World", "hello", "world")]
public void With_Kebab_case(string str, params string[] parts)
=> CodeNameConvention.KebabCase.Split(str).Should().BeEquivalentTo(parts);

[TestCase("Hello_World", "hello", "world")]
[TestCase("hello__World", "hello", "world")]
public void With_Snake_case(string str, params string[] parts)
=> CodeNameConvention.SnakeCase.Split(str).Should().BeEquivalentTo(parts);

[TestCase("Hello_World", "HELLO", "WORLD")]
[TestCase("hello__World", "HELLO", "WORLD")]
public void With_Screaming_Snake_case(string str, params string[] parts)
=> CodeNameConvention.ScreamingSnakeCase.Split(str).Should().BeEquivalentTo(parts);

[TestCase("Hello World", "Hello", "World")]
[TestCase("Hello\tWorld", "Hello", "World")]
[TestCase("Hello \tWorld", "Hello", "World")]
[TestCase("Hello_World", "Hello", "World")]
[TestCase("Hello World non-breaking", "Hello", "World", "non-breaking")]
public void With_Sentence_case(string str, params string[] parts)
=> CodeNameConvention.SentenceCase.Split(str).Should().BeEquivalentTo(parts);
}

public class Formats
{
[TestCase("HelloWorld", "hello", "World")]
[TestCase("HelloWorld", "Hello", "World")]
public void With_Pacal_case(string str, params string[] parts)
=> CodeNameConvention.PascalCase.ToString(parts).Should().Be(str);

[TestCase("helloWorld", "hello", "World")]
[TestCase("helloWorld", "Hello", "World")]
public void With_Dromedary_case(string str, params string[] parts)
=> CodeNameConvention.CamelCase.ToString(parts).Should().Be(str);

[TestCase("hello-world", "hello", "world")]
[TestCase("hello-world", "Hello", "World")]
public void With_Kebab_case(string str, params string[] parts)
=> CodeNameConvention.KebabCase.ToString(parts).Should().Be(str);

[TestCase("hello_world", "hello", "world")]
[TestCase("hello_world", "Hello", "World")]
public void With_Snake_case(string str, params string[] parts)
=> CodeNameConvention.SnakeCase.ToString(parts).Should().Be(str);

[TestCase("HELLO_WORLD", "hello", "world")]
[TestCase("HELLO_WORLD", "Hello", "World")]
public void With_Screaming_Snake_case(string str, params string[] parts)
=> CodeNameConvention.ScreamingSnakeCase.ToString(parts).Should().Be(str);

[TestCase("Hello World", "Hello", "World")]
public void With_Sentence_case(string str, params string[] parts)
=> CodeNameConvention.SentenceCase.ToString(parts).Should().Be(str);
}
42 changes: 42 additions & 0 deletions src/Qowaiv.CodeGeneration.OpenApi/OpenApiEnumNameConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Qowaiv.CodeGeneration.OpenApi;

/// <summary>Implements Open API specific name convention for enum values.</summary>
public class OpenApiEnumNameConvention : CodeNameConvention
{
internal static readonly OpenApiEnumNameConvention Instance = new();

/// <inheritdoc />
public override string Name => "Open API enum name";

/// <summary>Splitters used to seperates parts of a name.</summary>
protected virtual IReadOnlyCollection<char> Splitters { get; } = [' ', '-', '_', '\t', '\r', '\n', '.', '!', ',', ';', '#', (char)160];

/// <inheritdoc />
[Pure]
public override IReadOnlyCollection<string> Split(IEnumerable<string?> parts)
=> Guard.NotNull(parts)
.OfType<string>()
.Where(p => p is { Length: > 0 })
.Select(Format)
.SelectMany(p => p.Split([.. Splitters], StringSplitOptions.RemoveEmptyEntries))

Check failure on line 21 in src/Qowaiv.CodeGeneration.OpenApi/OpenApiEnumNameConvention.cs

View workflow job for this annotation

GitHub Actions / build

The call is ambiguous between the following methods or properties: 'string.Split(char[]?, StringSplitOptions)' and 'string.Split(string?, StringSplitOptions)'

Check failure on line 21 in src/Qowaiv.CodeGeneration.OpenApi/OpenApiEnumNameConvention.cs

View workflow job for this annotation

GitHub Actions / build

The call is ambiguous between the following methods or properties: 'string.Split(char[]?, StringSplitOptions)' and 'string.Split(string?, StringSplitOptions)'
.ToArray();

/// <summary>Formats a part.</summary>
[Pure]
protected virtual string Format(string part, int index) => part
.Replace("+", " pls ")
.Replace("(", string.Empty)
.Replace(")", string.Empty);

/// <inheritdoc />
[Pure]
public override string ToString(IReadOnlyCollection<string> parts)
{
var name = string.Join('_', Guard.NotNull(parts).Where(p => p is { Length: > 0 }));
if (char.IsAsciiDigit(name[0]))
{
name = '_' + name;
}
return CodeName.EscapeKeyword(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public partial class OpenApiTypeResolver
var name = schema.ReferenceId ?? schema.Path.Last;
var lastDot = name.LastIndexOf('.');
var ns = lastDot == -1 ? DefaultNamespace : DefaultNamespace.Child(name[..lastDot]);
name = NamingStrategy.PascalCase((lastDot == -1 ? name : name[(lastDot + 1)..]).TrimStart('_'));
return new(ns, name);
var codeName = CodeName.Create(lastDot == -1 ? name : name[(lastDot + 1)..], CodeNameConvention.PascalCase);
return new(ns, codeName.ToString());
}

[Pure]
Expand Down
9 changes: 5 additions & 4 deletions src/Qowaiv.CodeGeneration.OpenApi/OpenApiTypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ protected virtual EnumerationField ResolveEnumField(OpenApiString @enum, Enumera

[Pure]
protected virtual string PropertyName(ResolveOpenApiSchema schema)
=> NamingStrategy.PascalCase(schema.Path.Last, schema.Model!);
=> CodeName.Create(schema.Path.Last, CodeNameConvention.PascalCase).PropertyFor(schema.Model!);

[Pure]
protected virtual string EnumValueName(Enumeration @enum, OpenApiString enumValue)
=> NamingStrategy.Enum(Guard.NotNull(enumValue).Value);
protected virtual string EnumValueName(Enumeration @enum, OpenApiString enumValue) => CodeName
.Create(Guard.NotNull(enumValue).Value, OpenApiEnumNameConvention.Instance)
.ToString();

[Pure]
protected virtual string NormalizeFormat(string? str)
=> (str ?? string.Empty).ToUpperInvariant().Replace("-", "");
=> (str ?? string.Empty).ToUpperInvariant().Replace("-", string.Empty);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.0.1-alpha-013</Version>
<Version>0.0.1-alpha-014</Version>
<PackageId>Qowaiv.CodeGeneration.OpenApi</PackageId>
<PackageReleaseNotes>
ToBeReleased
Expand Down
74 changes: 74 additions & 0 deletions src/Qowaiv.CodeGeneration/CodeName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace Qowaiv.CodeGeneration;

/// <summary>Represents the name of a piece of code (class, property, field, method, etc.).</summary>
[DebuggerDisplay("{ToString()}, Covention = {Convention.Name}")]
public readonly struct CodeName : IFormattable
{
private CodeName(IReadOnlyCollection<string> nameParts, CodeNameConvention nameConvention)
{
parts = nameParts;
convention = nameConvention;
}

private readonly IReadOnlyCollection<string> parts;

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly CodeNameConvention? convention;

/// <summary>The convention applied to create this (code) name.</summary>
public CodeNameConvention Convention => convention ?? CodeNameConvention.None;

/// <summary>Creates a new (code) name applying the specified convention.</summary>
[Pure]
public static CodeName Create(string? name, CodeNameConvention convention)
=> new(Guard.NotNull(convention).Split(name), convention);

/// <inheritdoc />
[Pure]
public override string ToString() => ToString(Convention);

/// <inheritdoc />
[Pure]
public string ToString(string? format, IFormatProvider? formatProvider) => format?.ToUpperInvariant() switch
{
null or "" => ToString(Convention),
"PASCAL" or "PASCALCASE" => ToString(CodeNameConvention.CamelCase),
"CAMEL" or "CAMELCASE" => ToString(CodeNameConvention.CamelCase),
"KEBAB" or "KEBABCASE" => ToString(CodeNameConvention.KebabCase),
"SNAKE" or "SNAKECASE" => ToString(CodeNameConvention.SnakeCase),
"SCREAMINGSNAKE" => ToString(CodeNameConvention.ScreamingSnakeCase),
_ => throw new FormatException($"Format '{format}' is unknown."),
};

/// <summary>Represents the code name as string according to the naming convention.</summary>
[Pure]
public string ToString(CodeNameConvention convention) => Guard.NotNull(convention).ToString(parts);

/// <summary>Represents the code name as property name for the specified enclosing type.</summary>
/// <param name="enclosing">
/// The enclosing type.
/// </param>
/// <param name="convention">
/// The optional naming convention, <see cref="CodeNameConvention.PascalCase"/> if not specified.
/// </param>
[Pure]
public string PropertyFor(Type enclosing, CodeNameConvention? convention = null)
{
Guard.NotNull(enclosing);
convention ??= CodeNameConvention.PascalCase;

var name = convention.ToString(parts);
return enclosing.Name == name || char.IsAsciiDigit(name[0])
? '_' + name
: EscapeKeyword(name);
}

/// <summary>Escapes the name with an '@' if it a C# keyword.</summary>
[Pure]
public static string EscapeKeyword(string name)
=> keywords.Contains(Guard.NotNullOrEmpty(name))
? "@" + name
: name;

private static readonly string[] keywords = ["default", "new", "class"];
}
Loading

0 comments on commit 326c3b9

Please sign in to comment.