Skip to content

generator: support optional directive arguments #1830

Open
@dariuszkuc

Description

@dariuszkuc

Is your feature request related to a problem? Please describe.
Directives are defined as annotations which works great in many cases but we are also constrained by the JVM limitations. Currently JVM does not support null value for annotation attribute so it is not possible to define directive with nullable arguments by using annotation only approach. While we can create custom directive definition using willGenerateDirective hook, annotation arguments cannot be null so it is not possible to apply the annotation with nullable attributes.

We cannot use the existing OptionalInput approach either as JVM has constraints on annotation attributes - attributes can only be a primitive value, String, Enum, Class, Annotation or an array of any of the above. Furthermore, we cannot just recreate generic OptionalInput as an annotation because of A) lack of inheritance support in annotations and B) generic annotation attributes are not supported.

Describe the solution you'd like
Schema generator should provide support for creating and applying directives with nullable arguments. Ideally we wouldn't need any custom hooks to do it and everything would be derived from annotation attributes.

Describe alternatives you've considered

--- Alternative # 1 ---

We could potentially create specific wrappers for each supported attribute type (e.g. @OptionalStringArg(val value = "", val defined: Boolean = false)).

Drawbacks:

  • we would need to create wrappers for each possible type + their array representation
  • since generics are not supported we wouldn't be able to support enums nor custom annotations

--- Alternative # 2 ---

Since we can provide our own directive definition using hooks, we could potentially introduce other hooks to allow for filtering directive arguments , e.g.

// new hook
fun isValidDirectiveArgumentValue(directiveName: String, argumentName: String, value: Any?): Boolean = true

// example usage
override fun isValidDirectiveArgumentValue(directiveName: String, argumentName: String, value: Any?): Boolean {
    if (directiveName == LINK_DIRECTIVE_NAME && argumentName == "as" && value.toString().isBlank()) {
        return false
    }
    return super.isValidDirectiveArgumentValue(directiveName, argumentName, value)
}

Drawbacks:

  • still require manually providing custom directive definition in willGenerateDirective
  • directives could be renamed so passed directiveName would have to be the original one before the renames
  • you could potentially filter out required arguments leading to invalid applied directive in the schema (it would fail the validation so I guess it is fine?)

--- Alternative # 3 ---

OR we could create a hook to transform final applied directive (currently hooks only execute around generation of directive definitions), e.g.

// new hook
fun didGenerateAppliedDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLAppliedDirective): GraphQLAppliedDirective = directive

// example usage
override fun didGenerateAppliedDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLAppliedDirective): GraphQLAppliedDirective {
    if (directiveInfo.effectiveName == LINK_DIRECTIVE_NAME) {
        val asArg = directive.getArgument("as")
        if (asArg.getValue<String>().isEmpty()) {
            // `as` argument was not specified so we need to remove it from arguments
            // since there is no API to do it we have to manually remove ALL and re-add args
            return directive.transform {
                val urlArg = directive.getArgument("url")
                val importArg = directive.getArgument("import")

                it.clearArguments()
                it.argument(urlArg)
                it.argument(importArg)
            }
        }
    }
    return super.didGenerateAppliedDirective(directiveInfo, directive)
}

Drawbacks:

  • still require manually providing custom directive definition in willGenerateDirective
  • transformation logic is much more complex than the simple filtering in alternative # 2 (isValidDirectiveArgumentValue hook) but it is a very flexible approach

Additional context
Example use case from Apollo Federation spec - @link directive defines single required argument

directive @link(url: String!, as: String, import: [link__Import]) repeatable on SCHEMA

but we cannot apply the directive without providing values for all optional fields, i.e. we cannot represent the basic link when there are no imports and namespace is derived. Ideally we would like to be able to specify just

@link(url: "https://myspecs.dev/foo/1.0")

Since this is not possible, currently users will always have to provide ALL optional values

# `as` namespace should default to the spec name from the url
@link(url: "https://myspecs.dev/foo/1.0", as: "foo", imports: [])

Metadata

Metadata

Assignees

No one assigned

    Labels

    module: generatorIssue affects the schema generator and federation codetype: enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions