Skip to content
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

Fix/c sharp member name casing #806

Merged
merged 10 commits into from
Aug 5, 2024
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