-
-
Notifications
You must be signed in to change notification settings - Fork 246
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
Typesafe directive - for wrapping case classes on fields (incl. input types) and arguments #2091
Typesafe directive - for wrapping case classes on fields (incl. input types) and arguments #2091
Conversation
d64bcb9
to
9657ad0
Compare
To give some background in case it's not obvious: We're working on improving the story for schema-first development. One thing we identified which we really wanted was to have statically typed IDs on the backend. So given that preference, we identified a few paths forward:
Any thoughts on this @ghostdogpr? |
This looks good! I just need a little bit of time to review because it's code I'm not super familiar with (schema codegen was mostly contributed externally). Worst case scenario I'll take a look during the weekend 👍 @kyri-petrou if you have time feel free to review as well |
Nice! One very obvious todo is that it needs to be documented, likely alongside the Can do that after we agree on the exact path forward here 👍 |
I'm all up for that. Also I think the @lazy directive seems very powerful, so it would be a pity if it where not to be used due to limited documentation. |
Oh, it is! check https://ghostdogpr.github.io/caliban/docs/server-codegen.html#lazy-evaluation . I meant we should document this on the same page |
At a glance it looks good, but I wanna take some more time and understand the code better since I'm not too familiar with it either. One question that comes to mind at a very high level, why PS: I'm don't mind it either way, I'm just curious on whether there's any specific reasoning behind it :) |
There is no particular reason for the directive name, so open for suggestions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the name @typesafe
sounds a little bit too general for what it does here? How about something like @typeAlias
?
|
||
val LazyDirective = "lazy" | ||
val TypesafeDirective = "typesafe" | ||
val DeprecatedDiretive = "deprecated" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: Diretive
=> Directive
def isTypesafe(directives: List[Directive]): Boolean = | ||
directives.exists(_.name == TypesafeDirective) | ||
def typesafeName(directives: List[Directive]): Option[String] = | ||
Directives |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can call findDirective
directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well spotted!
def writeInputValue(value: InputValueDefinition): String = | ||
s"""${writeDescription(value.description)}${safeName(value.name)} : ${writeType(value.ofType)}""" | ||
def writeGQLTypesafeDirective(field: FieldDefinition) = | ||
if (field.directives.exists(_.name == TypesafeDirective)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: rather than exists
+ filter
+ head
, you could use find
and then fold
on the Option. Same below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks I even found some further optimization cause of this :)
val fnName = directive.arguments("name").toInputString.replace("\"", "") | ||
val typesafeType = writeTypesafeType(fieldType) | ||
|
||
s"""case class $fnName(value : $typesafeType) extends AnyVal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to sanitize the name?
I was thinking the same. Another option would be |
678ee91
to
977b8fa
Compare
I refactor to use Also added documentation. |
d950194
to
7389539
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great overall! My main comment / concern is that unless I'm missing something, the summon
keyword wouldn't work when generating Scala 2 code
val LazyDirective = "lazy" | ||
val NewtypeDirective = "newtype" | ||
val DeprecatedDirective = "deprecated" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor, but perhaps make these final val
instead
|
||
s"""case class $fnName(value : $newtype) extends AnyVal | ||
|object $fnName { | ||
| implicit val schema: Schema[Any, $fnName] = summon[Schema[Any, $newtype]].contramap(_.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused, does summon
work for Scala 2?
Also, perhaps just use Schema[$newtype].contramap(_.value)
instead, the Schema.apply
method "summons" the implicit Schema
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could not get that to work, but used implicitly
(Scala 2 version of summon).
Tested it on my Scala 3 setup and that worked, but have to double check my Scala options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To catch these kind of issues it would be nice to add the new annotation to the sbt plug-in "scripted" test suite because it runs both on scala 2 and scala 3
s"""case class $fnName(value : $newtype) extends AnyVal | ||
|object $fnName { | ||
| implicit val schema: Schema[Any, $fnName] = summon[Schema[Any, $newtype]].contramap(_.value) | ||
| implicit val argBuilder: ArgBuilder[$fnName] = summon[ArgBuilder[$newtype]].map($fnName(_)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, I believe you can use [ArgBuilder[$newtype].map($fnName(_))
0666805
to
8e99d73
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've never worked on this part of the codebase, but AFAICT it looks great :) The only thing that "saddens" me a bit is that we don't get to utilize Scala 3's opaque types, but I think it's likely for the best to have a unified Scala 2/3 implementation.
Thank you very much for the contribution, really highly appreciated!
If we are able to support opaque types in the future I think we can just add a type argument to the directive. But I did try bit with opaque types initially and ran into a issue, if I recall it was: Hence similar to the issue for union types: #1926 |
With opaque types, you have to generate the Schema in the companion object of the opaque type for it to work, something along these lines: import scala.compiletime.summonInline
opaque type Foo = Int
object Foo {
inline given Schema[Any, Foo] = summonInline[Schema[Any, Int]]
} Either way, I think let's steer off opaque types for now and we can revisit this if there is interest in having that option |
0950347
to
e155487
Compare
Removed documentation from this PR |
Thanks! |
By using @typesafe directive, the code generator will wrap your typesafe fields in case classes without
imposing further changes to GraphQl schema. Hence allowing type safety on important identifiers and fields.
Tested with Option and List as well.
Schema example:
with ID as scalar mapping to Int you would get something like this:
See SchemaWriterSpec.scala for more extensive type mapping.