diff --git a/.changeset/strong-turkeys-sneeze.md b/.changeset/strong-turkeys-sneeze.md new file mode 100644 index 000000000..d87191ebc --- /dev/null +++ b/.changeset/strong-turkeys-sneeze.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/c-sharp': Minor +--- + +Added `memberNameConvention` which allows you to customize the naming convention for +interface/class/record members. diff --git a/packages/plugins/c-sharp/c-sharp/src/config.ts b/packages/plugins/c-sharp/c-sharp/src/config.ts index de4ac3060..893a39e54 100644 --- a/packages/plugins/c-sharp/c-sharp/src/config.ts +++ b/packages/plugins/c-sharp/c-sharp/src/config.ts @@ -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'; } diff --git a/packages/plugins/c-sharp/c-sharp/src/member-naming.ts b/packages/plugins/c-sharp/c-sharp/src/member-naming.ts new file mode 100644 index 000000000..88754870a --- /dev/null +++ b/packages/plugins/c-sharp/c-sharp/src/member-naming.ts @@ -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); + } +} diff --git a/packages/plugins/c-sharp/c-sharp/src/visitor.ts b/packages/plugins/c-sharp/c-sharp/src/visitor.ts index 74688575b..648150a20 100644 --- a/packages/plugins/c-sharp/c-sharp/src/visitor.ts +++ b/packages/plugins/c-sharp/c-sharp/src/visitor.ts @@ -1,4 +1,3 @@ -import { pascalCase } from 'change-case-all'; import { DirectiveNode, EnumTypeDefinitionNode, @@ -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; @@ -52,6 +52,7 @@ export interface CSharpResolverParsedConfig extends ParsedConfig { emitRecords: boolean; emitJsonAttributes: boolean; jsonAttributesSource: JsonAttributesSource; + memberNamingFunction: MemberNamingFn; } export class CSharpResolversVisitor extends BaseVisitor< @@ -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) { @@ -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 + @@ -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}`; }) @@ -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'); @@ -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; }'; } @@ -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; }`); }) diff --git a/packages/plugins/c-sharp/c-sharp/test/c-sharp.spec.ts b/packages/plugins/c-sharp/c-sharp/test/c-sharp.spec.ts index 431123637..1aeb2132c 100644 --- a/packages/plugins/c-sharp/c-sharp/test/c-sharp.spec.ts +++ b/packages/plugins/c-sharp/c-sharp/test/c-sharp.spec.ts @@ -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 { @@ -283,6 +307,56 @@ 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 { @@ -290,8 +364,36 @@ describe('C#', () => { } `); 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 { @@ -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); @@ -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 {