diff --git a/src/annotations/build-directive-annotations.ts b/src/annotations/build-directive-annotations.ts index e3626ad..6876184 100644 --- a/src/annotations/build-directive-annotations.ts +++ b/src/annotations/build-directive-annotations.ts @@ -17,6 +17,7 @@ import { ConstDirectiveNode } from "graphql/language"; import { GraphQLSchema, isInputObjectType, Kind } from "graphql"; import { shouldConsolidateTypes } from "../utils/should-consolidate-types"; import { sanitizeName } from "../utils/sanitize-name"; +import { titleCase } from "../utils/title-case"; export function buildDirectiveAnnotations( definitionNode: DefinitionNode, @@ -50,17 +51,32 @@ export function buildDirectiveAnnotations( return !typeWillBeConsolidated; }, ); - if (!directiveReplacementFromConfig) return ""; - const kotlinAnnotations = buildKotlinAnnotations( - directive, - directiveReplacementFromConfig.kotlinAnnotations, + + if (directiveReplacementFromConfig) { + return ( + buildKotlinAnnotationsFromConfig( + directive, + directiveReplacementFromConfig.kotlinAnnotations, + ).join("\n") + "\n" + ); + } + const customDirectiveFromConfig = config.customDirectives?.find( + (directive) => directive === directiveName, ); - return kotlinAnnotations.join("\n") + "\n"; + if (customDirectiveFromConfig) { + return buildCustomDirective(directive); + } + return ""; }) .join(""); } -function buildKotlinAnnotations( +function buildCustomDirective(directive: ConstDirectiveNode) { + const directiveName = directive.name.value; + return `@${titleCase(directiveName)}\n`; +} + +function buildKotlinAnnotationsFromConfig( directive: ConstDirectiveNode, kotlinAnnotations: NonNullable< CodegenConfigWithDefaults["directiveReplacements"] diff --git a/src/config/schema.ts b/src/config/schema.ts index c8189ba..6278cba 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -32,6 +32,16 @@ export const configSchema = object({ * @link https://opensource.expediagroup.com/graphql-kotlin-codegen/docs/class-consolidation */ classConsolidationEnabled: optional(boolean()), + /** + * Denotes directives to generate as @GraphQLDirective annotations. + * + * Directive arguments are not yet supported and will be ignored. + * + * @example ["myCustomDirective"] + * + * @link https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/customizing-schemas/directives/#custom-directives + */ + customDirectives: optional(array(string())), /** * Limits dependent types to include from `onlyTypes` list. Can be used to exclude classes that are imported from external packages. * diff --git a/src/definitions/directive.ts b/src/definitions/directive.ts new file mode 100644 index 0000000..2fc3dd7 --- /dev/null +++ b/src/definitions/directive.ts @@ -0,0 +1,33 @@ +/* +Copyright 2024 Expedia, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DirectiveDefinitionNode } from "graphql"; +import { CodegenConfigWithDefaults } from "../config/build-config-with-defaults"; +import { titleCase } from "../utils/title-case"; + +export function buildDirectiveDefinition( + node: DirectiveDefinitionNode, + config: CodegenConfigWithDefaults, +): string { + const directiveName = node.name.value; + const isCustomDirective = config.customDirectives?.includes(directiveName); + if (!isCustomDirective) { + return ""; + } + return `@GraphQLDirective( + name = "${titleCase(directiveName)}", + description = "${node.description?.value ?? ""}", + locations = [${node.locations.map((location) => `graphql.introspection.Introspection.DirectiveLocation.${location.value}`).join(", ")}] +) +annotation class ${titleCase(directiveName)}`; +} diff --git a/src/visitor.ts b/src/visitor.ts index 85e85f1..b4248c7 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -13,6 +13,7 @@ limitations under the License. import { BaseVisitor, RawConfig } from "@graphql-codegen/visitor-plugin-common"; import { + DirectiveDefinitionNode, EnumTypeDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, @@ -21,6 +22,7 @@ import { UnionTypeDefinitionNode, } from "graphql"; import { CodegenConfigWithDefaults } from "./config/build-config-with-defaults"; +import { buildDirectiveDefinition } from "./definitions/directive"; import { buildEnumTypeDefinition } from "./definitions/enum"; import { buildInterfaceDefinition } from "./definitions/interface"; import { buildInputObjectDefinition } from "./definitions/input"; @@ -39,6 +41,10 @@ export class KotlinVisitor extends BaseVisitor< super(rawConfig, rawConfig); } + DirectiveDefinition(node: DirectiveDefinitionNode): string { + return buildDirectiveDefinition(node, this.config); + } + EnumTypeDefinition(node: EnumTypeDefinitionNode): string { return buildEnumTypeDefinition(node, this._schema, this.config); } diff --git a/test/unit/should_generate_custom_directives/codegen.config.ts b/test/unit/should_generate_custom_directives/codegen.config.ts new file mode 100644 index 0000000..9cea54d --- /dev/null +++ b/test/unit/should_generate_custom_directives/codegen.config.ts @@ -0,0 +1,12 @@ +import { GraphQLKotlinCodegenConfig } from "../../../src/plugin"; + +export default { + customDirectives: ["myCustomDirective", "myCustomDirective2"], + extraImports: ["should_honor_directiveReplacements_config.*"], + directiveReplacements: [ + { + directive: "someDirective1", + kotlinAnnotations: ["@SomeAnnotation1"], + }, + ], +} satisfies GraphQLKotlinCodegenConfig; diff --git a/test/unit/should_generate_custom_directives/expected.kt b/test/unit/should_generate_custom_directives/expected.kt new file mode 100644 index 0000000..16e4e5c --- /dev/null +++ b/test/unit/should_generate_custom_directives/expected.kt @@ -0,0 +1,44 @@ +package com.kotlin.generated + +import com.expediagroup.graphql.generator.annotations.* +import should_honor_directiveReplacements_config.* + +@GraphQLDirective( + name = "MyCustomDirective", + description = "A description for MyCustomDirective", + locations = [graphql.introspection.Introspection.DirectiveLocation.OBJECT, graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION, graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT, graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION] +) +annotation class MyCustomDirective + +@GraphQLDirective( + name = "MyCustomDirective2", + description = "", + locations = [graphql.introspection.Introspection.DirectiveLocation.OBJECT, graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION, graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT, graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION] +) +annotation class MyCustomDirective2 + +@MyCustomDirective +@MyCustomDirective2 +@SomeAnnotation1 +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +data class MyTypeWithCustomDirectiveOnObject( + val field: String? = null +) + +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT]) +data class MyTypeWithCustomDirectiveOnField( + @MyCustomDirective + val field: String? = null +) + +@MyCustomDirective +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT]) +data class MyInputWithCustomDirectiveOnObject( + val field: String? = null +) + +@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT]) +data class MyInputWithCustomDirectiveOnField( + @MyCustomDirective + val field: String? = null +) diff --git a/test/unit/should_generate_custom_directives/schema.graphql b/test/unit/should_generate_custom_directives/schema.graphql new file mode 100644 index 0000000..0708d5c --- /dev/null +++ b/test/unit/should_generate_custom_directives/schema.graphql @@ -0,0 +1,24 @@ +"A description for MyCustomDirective" +directive @myCustomDirective on OBJECT | FIELD_DEFINITION | INPUT_OBJECT | INPUT_FIELD_DEFINITION +directive @myCustomDirective2 on OBJECT | FIELD_DEFINITION | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @someDirective1 on OBJECT + +type MyTypeWithCustomDirectiveOnObject + @myCustomDirective + @myCustomDirective2 + @someDirective1 { + field: String +} + +type MyTypeWithCustomDirectiveOnField { + field: String @myCustomDirective +} + +input MyInputWithCustomDirectiveOnObject @myCustomDirective { + field: String +} + +input MyInputWithCustomDirectiveOnField { + field: String @myCustomDirective +}