Skip to content

Commit

Permalink
Fix/c sharp member name casing (#806)
Browse files Browse the repository at this point in the history
* added test for namingConvention=pascalCase

* added support for choosing the field naming convention (camel case or pascal case) for a class' properties

* Renamed fieldNamingConvention to memberNamingConvention to reflect that it actually applies to all members, not only fields

* added a test for camelCase naming class properties

* the casing of record member names can be controlled too

* control the casing of input type class properties

* the casing of interface member names can be controlled too

* typo

* removed unused import

* added changeset
  • Loading branch information
mariusmuntean authored Aug 5, 2024
1 parent f29802b commit d1d6d6e
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 12 deletions.
6 changes: 6 additions & 0 deletions .changeset/strong-turkeys-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-codegen/c-sharp': Minor
---

Added `memberNameConvention` which allows you to customize the naming convention for
interface/class/record members.
17 changes: 17 additions & 0 deletions packages/plugins/c-sharp/c-sharp/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,21 @@ export interface CSharpResolversPluginRawConfig extends RawConfig {
* ```
*/
jsonAttributesSource?: JsonAttributesSource;

/**
* @default camelCase
* Supported: camelCase, pascalCase
* @description Allows you to customize the naming convention for interface/class/record members.
*
* @exampleMarkdown
* ```yaml
* generates:
* src/main/c-sharp/my-org/my-app/MyTypes.cs:
* plugins:
* - c-sharp
* config:
* fieldNameConvention: pascalCase
* ```
*/
memberNameConvention?: 'camelCase' | 'pascalCase';
}
21 changes: 21 additions & 0 deletions packages/plugins/c-sharp/c-sharp/src/member-naming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { camelCase, pascalCase } from 'change-case-all';
import { NameNode } from 'graphql';
import { CSharpResolversPluginRawConfig } from './config';

type MemberNamingFunctionInput = string | NameNode;

export type MemberNamingFn = (nameOrNameNode: MemberNamingFunctionInput) => string;

export function getMemberNamingFunction(rawConfig: CSharpResolversPluginRawConfig): MemberNamingFn {
switch (rawConfig.memberNameConvention) {
case 'camelCase':
return (input: MemberNamingFunctionInput) =>
camelCase(typeof input === 'string' ? input : input.value);
case 'pascalCase':
return (input: MemberNamingFunctionInput) =>
pascalCase(typeof input === 'string' ? input : input.value);
default:
return (input: MemberNamingFunctionInput) =>
camelCase(typeof input === 'string' ? input : input.value);
}
}
27 changes: 17 additions & 10 deletions packages/plugins/c-sharp/c-sharp/src/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { pascalCase } from 'change-case-all';
import {
DirectiveNode,
EnumTypeDefinitionNode,
Expand Down Expand Up @@ -43,6 +42,7 @@ import {
JsonAttributesSource,
JsonAttributesSourceConfiguration,
} from './json-attributes.js';
import { getMemberNamingFunction, MemberNamingFn } from './member-naming.js';

export interface CSharpResolverParsedConfig extends ParsedConfig {
namespaceName: string;
Expand All @@ -52,6 +52,7 @@ export interface CSharpResolverParsedConfig extends ParsedConfig {
emitRecords: boolean;
emitJsonAttributes: boolean;
jsonAttributesSource: JsonAttributesSource;
memberNamingFunction: MemberNamingFn;
}

export class CSharpResolversVisitor extends BaseVisitor<
Expand All @@ -70,6 +71,7 @@ export class CSharpResolversVisitor extends BaseVisitor<
emitJsonAttributes: rawConfig.emitJsonAttributes ?? true,
jsonAttributesSource: rawConfig.jsonAttributesSource || 'Newtonsoft.Json',
scalars: buildScalarsFromConfig(_schema, rawConfig, C_SHARP_SCALARS),
memberNamingFunction: getMemberNamingFunction(rawConfig),
});

if (this._parsedConfig.emitJsonAttributes) {
Expand Down Expand Up @@ -284,7 +286,9 @@ export class CSharpResolversVisitor extends BaseVisitor<
.map(arg => {
const fieldType = this.resolveInputFieldType(arg.type);
const fieldHeader = this.getFieldHeader(arg, fieldType);
const fieldName = convertSafeName(pascalCase(this.convertName(arg.name)));
const fieldName = convertSafeName(
this._parsedConfig.memberNamingFunction(this.convertName(arg.name)),
);
const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
return (
fieldHeader +
Expand All @@ -295,7 +299,9 @@ export class CSharpResolversVisitor extends BaseVisitor<
const recordInitializer = inputValueArray
.map(arg => {
const fieldType = this.resolveInputFieldType(arg.type);
const fieldName = convertSafeName(pascalCase(this.convertName(arg.name)));
const fieldName = convertSafeName(
this._parsedConfig.memberNamingFunction(this.convertName(arg.name)),
);
const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
return `${csharpFieldType} ${fieldName}`;
})
Expand Down Expand Up @@ -324,10 +330,10 @@ ${recordMembers}
const classMembers = inputValueArray
.map(arg => {
const fieldType = this.resolveInputFieldType(arg.type);
const fieldHeader = this.getFieldHeader(arg, fieldType);
const fieldName = convertSafeName(arg.name);
const fieldAttribute = this.getFieldHeader(arg, fieldType);
const fieldName = convertSafeName(this._parsedConfig.memberNamingFunction(arg.name));
const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
return fieldHeader + indent(`public ${csharpFieldType} ${fieldName} { get; set; }`);
return fieldAttribute + indent(`public ${csharpFieldType} ${fieldName} { get; set; }`);
})
.join('\n\n');

Expand Down Expand Up @@ -357,11 +363,12 @@ ${classMembers}

if (this.config.emitRecords) {
// record
fieldName = convertSafeName(pascalCase(this.convertName(arg.name)));
fieldName = convertSafeName(
this._parsedConfig.memberNamingFunction(this.convertName(arg.name)),
);
getterSetter = '{ get; }';
} else {
// class
fieldName = convertSafeName(arg.name);
fieldName = convertSafeName(this._parsedConfig.memberNamingFunction(arg.name));
getterSetter = '{ get; set; }';
}

Expand All @@ -387,7 +394,7 @@ ${classMembers}
.map(arg => {
const fieldType = this.resolveInputFieldType(arg.type, !!arg.defaultValue);
const fieldHeader = this.getFieldHeader(arg, fieldType);
const fieldName = convertSafeName(arg.name);
const fieldName = convertSafeName(this._parsedConfig.memberNamingFunction(arg.name));
const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
return fieldHeader + indent(`public ${csharpFieldType} ${fieldName} { get; set; }`);
})
Expand Down
144 changes: 142 additions & 2 deletions packages/plugins/c-sharp/c-sharp/test/c-sharp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,20 +218,44 @@ describe('C#', () => {
expect(result).toContain('public class UserInput {');
});

it('Should generate properties for input type fields', async () => {
it('Should generate camelCase properties for input type fields', async () => {
const schema = buildSchema(/* GraphQL */ `
input UserInput {
id: Int
email: String
}
`);
const result = await plugin(schema, [], {}, { outputFile: '' });
const result = await plugin(
schema,
[],
{ memberNameConvention: 'camelCase' },
{ outputFile: '' },
);
expect(result).toBeSimilarStringTo(`
public int? id { get; set; }
public string email { get; set; }
`);
});

it('Should generate pascalCase properties for input type fields', async () => {
const schema = buildSchema(/* GraphQL */ `
input UserInput {
id: Int
email: String
}
`);
const result = await plugin(
schema,
[],
{ memberNameConvention: 'pascalCase' },
{ outputFile: '' },
);
expect(result).toBeSimilarStringTo(`
public int? Id { get; set; }
public string Email { get; set; }
`);
});

it('Should generate C# method for creating input object', async () => {
const schema = buildSchema(/* GraphQL */ `
input UserInput {
Expand Down Expand Up @@ -283,15 +307,93 @@ describe('C#', () => {
const result = await plugin(schema, [], {}, { outputFile: '' });
expect(result).toContain('public class User {');
});
it('Should generate a C# class with camel case property names for type', async () => {
const schema = buildSchema(/* GraphQL */ `
type User {
id: Int
chosenName: String
}
`);
const result = await plugin(
schema,
[],
{
memberNameConvention: 'camelCase',
},
{
outputFile: '',
},
);
expect(result).toBeSimilarStringTo(`
[JsonProperty("id")]
public int? id { get; set; }
[JsonProperty("chosenName")]
public string chosenName { get; set; }
`);
});
it('Should generate a C# class with pascal case property names for type', async () => {
const schema = buildSchema(/* GraphQL */ `
type User {
id: Int
chosenName: String
}
`);
const result = await plugin(
schema,
[],
{
memberNameConvention: 'pascalCase',
},
{
outputFile: '',
},
);
expect(result).toBeSimilarStringTo(`
[JsonProperty("id")]
public int? Id { get; set; }
[JsonProperty("chosenName")]
public string ChosenName { get; set; }
`);
});
it('Should generate C# record for type', async () => {
const schema = buildSchema(/* GraphQL */ `
type User {
id: Int
}
`);
const result = await plugin(schema, [], { emitRecords: true }, { outputFile: '' });
expect(result).toContain('public record User(int? id) {');
});
it('Should generate C# record with pascal case property names', async () => {
const schema = buildSchema(/* GraphQL */ `
type User {
id: Int
}
`);
const result = await plugin(
schema,
[],
{ emitRecords: true, memberNameConvention: 'pascalCase' },
{ outputFile: '' },
);
expect(result).toContain('public record User(int? Id) {');
});
it('Should generate C# record with camel case property names', async () => {
const schema = buildSchema(/* GraphQL */ `
type User {
id: Int
}
`);
const result = await plugin(
schema,
[],
{ emitRecords: true, memberNameConvention: 'camelCase' },
{ outputFile: '' },
);
expect(result).toContain('public record User(int? id) {');
});
it('Should wrap generated classes in Type class', async () => {
const schema = buildSchema(/* GraphQL */ `
type User {
Expand Down Expand Up @@ -339,6 +441,7 @@ describe('C#', () => {
`);
const config: CSharpResolversPluginRawConfig = {
jsonAttributesSource: source,
namingConvention: 'change-case-all#pascalCase',
};
const result = await plugin(schema, [], config, { outputFile: '' });
const jsonConfig = getJsonAttributeSourceConfiguration(source);
Expand Down Expand Up @@ -444,6 +547,43 @@ describe('C#', () => {
expect(result).toContain('public interface Node {');
});

it('Should generate C# interface with pascalCase properties', async () => {
const schema = buildSchema(/* GraphQL */ `
interface Node {
id: ID!
}
`);
const result = await plugin(
schema,
[],
{ memberNameConvention: 'pascalCase' },
{ outputFile: '' },
);

expect(result).toBeSimilarStringTo(`public interface Node {
[JsonProperty("id")]
string Id { get; set; }
}`);
});
it('Should generate C# interface with camelCase properties', async () => {
const schema = buildSchema(/* GraphQL */ `
interface Node {
id: ID!
}
`);
const result = await plugin(
schema,
[],
{ memberNameConvention: 'camelCase' },
{ outputFile: '' },
);

expect(result).toBeSimilarStringTo(`public interface Node {
[JsonProperty("id")]
string id { get; set; }
}`);
});

it('Should generate C# class that implements given interfaces', async () => {
const schema = buildSchema(/* GraphQL */ `
interface INode {
Expand Down

0 comments on commit d1d6d6e

Please sign in to comment.