-
Notifications
You must be signed in to change notification settings - Fork 658
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
Use Kotlin's Inline value classes for custom scalars #6332
Comments
Thanks for the excellent suggestion! The way it works currently doesn't involve typealiases, when using e.g. Currently, you can uses value classes for scalars with adapters looking like this: val IDAdapter = object : Adapter<ID> {
override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): ID {
return ID(reader.nextString()!!)
}
override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: ID) {
writer.value(value.value)
}
} and configuring it like: mapScalar(graphQLName = "ID", targetName = "com.example.ID", expression = "com.example.IDAdapter") To make things less manual, the library could do 2 things:
A/ Generate scalarConfiguration: mapScalarToKotlinLong("Length", asValueClass = true) Would generate your scalar: package package com.example
@JvmInline
value class Length(val value: Long) Generated model adapter: // fromJson
_length = com.example.Length(
LongAdapter.fromJson(reader, customScalarAdapters)
)
// toJson
LongAdapter.toJson(writer, customScalarAdapters, value.length.value) B/ Bring your scalarConfiguration: mapScalarToKotlinLong("Length", valueClass = "com.example.MyLength", valueClassProperty = "length") Generated model adapter: // fromJson
_length = com.example.Length(
LongAdapter.fromJson(reader, customScalarAdapters)
)
// toJson
LongAdapter.toJson(writer, customScalarAdapters, value.length.length)
We could also start with |
Looking into the Kotlin documentation, defining the value class yourself does have some benefits you can't use extension properties/methods for:
The only type that would benefit from being generated by codegen (or just included in the library) would be the built-in ID, as it can be defined with a one-liner. |
Some additional thoughts:
|
Here's the current design notes on how the Kotlin developers want to expand the usage of value classes: https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md#value-classes-and-arrays. They're also looking at Project Valhalla which has a similar goal for the JVM specifically. One of the aims is to replace specialialized types like So the performance benefit of Value types isn't great at this point, but will increase in the future as Kotlin develops. |
Counter-example: The GitLab GraphQL API defines a new scalar type for every single type of ID. Having to manually write a value class for all of these would be a lot of work, and even writing a https://docs.gitlab.com/ee/api/graphql/reference/#scalar-types |
My favorite solution would be to define that in the schema. Something like: extend scalar AbuseReportID @kotlinValueClass(coerceAs: String) This makes it work with other build systems than Gradle out of the box and if you have a lot of such ids, you could always write a task that generates the schema extensions: scalarTypes.forEach {
if (it.name.endsWith("ID")) {
append("extend scalar ${it.name} @kotlinValueClass(coerceAs: String)
}
} Edit: add |
Is there currently a mechanism in place that parses/fetches a schema, appends extensions, then parses it again? |
The Gradle plugin takes a apollo {
service("service") {
schemaFiles.from("src/main/graphql/schema.graphqls", "src/main/graphql/extra.graphqls")
// ...
}
} How apollo {
service("service") {
schemaFiles.from("src/main/graphql/schema.graphqls", generateSchemaExtensionsTaskProvider)
// ...
}
} |
I don't think adding another directive to change the behavior is a good idea. In my opinion, using a separate class should actually be the default behavior for all custom scalars, but that's obviously not possible since it's a major breaking change for users. |
How would you specify the coercing of the wrapped value? GraphQL: scalar AbuseReportID Kotlin: @JvmInline
value class AbuseReportID(
// Is that OK?
val value: Any
) |
Apollo Kotlin's current behavior is replacing every scalar without a mapping to use |
Mmm that shouldn't be the case. They should be of |
Use case
Inline value classes are a thin wrapper over other types, which can potentially be erased entirely by the compiler.
The above is safer to use in Client code, since it's not possible to assign a
String
to anID
or vice versa. It also discourages (but doesn't prevent) string manipulation by the client, which is useful for scalars that are documented to be opaque.Describe the solution you'd like
The various shortcuts like
mapScalarToKotlinLong
,mapScalarToKotlinString
etc. could be expanded to take an additional argumentasValueClass: Boolean
which will generate a value class instead of a typealias.I'm not sure if it's preferable to generate another Adapter for this new type, or if its usage should be applied inside every existing object adapter:
I think avoiding a new adapter would allow the compiler to inline the type in more cases and increase performance.
The text was updated successfully, but these errors were encountered: