Kotlin optional parameters in annotation class (Cache Control Directive for GraphQL) - kotlin

I am using GraphQL Kotlin to generate a GraphQL schema. I would like to implement Caching for a subgraph, according to the documentation for inheritMaxAge:
If true, this field inherits the maxAge of its parent field instead of using the default maxAge. Do not provide maxAge if you provide this argument.
This implies that either maxAge or inheritMaxAge should be set for a given field. The problem I see for the actual implementation in Kotlin is that you have to define an Annotation, but as far as I know there is no way to add optional parameters for annotations (except for providing a default value for each optional field which does not solve this problem).
enum class CacheControlScope {
PUBLIC,
PRIVATE
}
annotation class CacheControlDirective(
val maxAge: Int = 0,
val scope: CacheControlScope = CacheControlScope.PUBLIC,
val inheritMaxAge: Boolean = true
)
If I now set #CacheControlDirective for some field this will always result in such a cache hint in the corresponding schema:
// #CacheControlDirective results in:
#cacheControl(maxAge : 0, scope : PUBLIC, inheritMaxAge : true)
// #CacheControlDirective(maxAge = 10) results in:
#cacheControl(maxAge : 10, scope : PUBLIC, inheritMaxAge : true)
Instead I would like to only get the fields that are explicitly declared in the schema:
// #CacheControlDirective results in:
#cacheControl
// #CacheControlDirective(maxAge = 10) results in:
#cacheControl(maxAge : 10)
Can someone help with this question?

Related

In kotlin, when #Column(nullable = false) is marked, the queried entity must always be written "!!"

In kotlin,
When writing entity classes, the default values of attributes are null,
The annotation of the attribute is #Column(nullable = false), which indicates that it cannot be empty.
For example, the following username:
#Entity
class Users {
#Column(nullable = false)
var username: String? = null
}
When querying, findById returns the following result:
val user = Users(username:"MyName")
When calling user. username!! I have to bring !!,
because the annotation is #Column(nullable = false).
This creates a problem:
When I get the username, I always have to check whether the #Column annotation of username is marked as empty or not, and then I can judge whether I should write !! or not.
This is very troublesome, and I always write what should not be written on !! carelessly.
What can I do to prevent the IDEA compiler from reporting errors when I mark #Column(nullable = false) and do not write !!?
If it is written like this (give the attribute a default value):
#Column(nullable = false)
var username: String = "default"
When using findById to query, I must bring withIgnorePaths, which is more troublesome:
val matcher = ExampleMatcher.matching().withIgnorePaths("username", ...))
Example.of(Users(username:""),matcher)
Approaches to avoid !! in your case.
The easiest one, make username not null, so each time you will create Users you will need to provide username.
Create new layer, let say UsersDto which looks like this:
class UsersDto {
var username: String
}
Which requires implementing some mapping between dto layer and entity. However, you'll need to handle nullable username, you can do it will forcing in the mapper !! (but only once) or
dto.username = entity.username ?: throw IllegalArgumentException("Broken user, username is null!")

Validating combination of values in constructor

I'm trying to make a class Box<T>. It should have two public immutable(val) properties, isContentMandatory: Boolean, content: T?. This is which combination of values I want to accept:
isContentMandatory
content
Should allow?
false
null
YES
false
non-null
YES
true
null
NO
true
non-null
YES
I want to make sure that the constructor(s) I provide won't allow the illegal state of an object. Also, I want multiple constructors(or use default values) so that creation of an object is straight-forward for the client. Following are examples of instantiations:
Box() // OK -> isContentMandatory = false, content = null
Box("some-content") // OK -> isContentMandatory = false, content = "some-content"
Box(false, "some-content") // OK -> isContentMandatory = false, content = "some-content"
Box(true, "some-content") // OK -> isContentMandatory = true, content = "some-content"
Box(true, null) // DON'T ALLOW
Box(true) // DON'T ALLOW
The DON'T ALLOWs from above should preferably be forbidden at compile-time(no constructor available for that combination) if it's possible. Otherwise, fail with the exception during creation.
I'm coming from Java world so all those primary/secondary constructors, default values and etc. are a bit fuzzy so please explain along with a solution. I'm also open to different class design if it supports the same business logic.
EDIT: this is how it would look in Java.
public class Box<T> {
private final boolean isContentMandatory;
private final T content;
public Box() {
this(null);
}
public Box(T content) {
this(false, content);
}
public Box(boolean isContentMandatory, T content) {
if (isContentMandatory && content == null) {
throw new IllegalArgumentException("Invalid combination of parameters");
}
this.isContentMandatory = isContentMandatory;
this.content = content;
}
...getters...
}
Whether or not this is a good approach for the problem is hard to answer without actual domain knowledge of your use case, but it feels to me like it makes little sense that you would make a single class to model those cases which carries around an (otherwise pointless?) boolean to separate the cases.
You could just have 2 classes BoxWithOptionalcontent<T?> and BoxWithContent<T> and you wouldn't need more than the default constructor for either afaict.
sealed interface Box<T: Any?> {
abstract val content: T?
}
data class BoxWithContent<T: Any>(override val content: T): Box<T>
data class BoxWithOptionalContent<T: Any?>(override val content: T? = null): Box<T?>
This shouldn't change much on the initialization site, on the side of the usage you will probably need to add a case statement to decide which case it is and handle appropriately. But you probably already have some similar logic there anyway, and this will probably be a bit more typesafe and readable.
From your Java approach it seems you just want a runtime check, not really preventing bad calls. Just having default arguments and one init block to validate should work:
class Box(val content: T? = null, val isContentMandatory: Boolean = false)
init {
if(content == null && isContentMandatory)
throw RuntimeException("Cannot have no content if content is mandatory")
}
}
Any content given though as a first argument will be valid, so to make this break you have to try harder using Box(isContentMandatory=true) or Box(null, true) explicitly.

kotlin - how to reuse value of one parameter in another parameter

i've created an object but for one of the parameters the creation is complex. after i create it i want to use that same value for another parameter. let me show you an example simply:
MyModel(convenienceFee = raw.data?.fee,
data = raw.data?.order?.map {/*complex operation that returns a list*/} ?: emptyList(),
summary = createSummaryFrom(data), //compiler wont allow me using data here
)
the createSummaryFrom is irrelevant it just takes a list and does some operations on it.
but the issue is i am not able to use "data" on the last line. it says its a unresolved field. how can i use the value of a already declared parameter in another parameter ?
clearly i can move the call outside of the declaration but wondering why i can do it inside while declaring it.
A .run or a .let scope function could be used to achieve something similar, but the reference to another parameter name is forbidden. I should note this code could also be achieved through typical use of variables, but, syntactically, a .let is more inline with what you desire.
val model: MyModel = raw.data.let { rawData ->
(rawData?.order?.map { ... } ?: emptyList()).let { transformedData ->
MyModel(
convenienceFee = rawData?.fee,
data = transformedData,
summary = createSummaryFrom(transformedData)
)
}
}
If you would like, you can read about Kotlin's five scope functions here.
Edit upon valid feedback regarding readability:
Scope functions should be used appropriately. Outside of the scope of this question, I would prefer the following:
val rawData = raw.data
val transformedData = rawData?.order?.map { ... } ?: emptyList()
val model = MyModel(
convenienceFee = rawData?.fee,
data = transformedData,
summary = createSummaryFrom(transformedData)
)
The simplest way to reuse a value would be the obvious: use local variables. But I guess you know that.
Another option would be to define your computation for summary as a default value for it, at the declaration site of the MyModel class. There, it is allowed to use the value of the parameter data:
class MyModel(
val convenienceFee: Double,
val data: DataType,
val summary: Summary = createSummaryFrom(data),
)
Then when you actually instantiate your class, you don't need to provide summary:
MyModel(
convenienceFee = raw.data?.fee,
data = raw.data?.order?.map {/*complex operation that returns a list*/} ?: emptyList()
)

Getting annotation from constructor value parameter using kotlinpoet-metadata

I have data class like this
#Tagme("Response")
#TagResponse
data class Response(
val id: Int,
val title: String,
val label: String,
val images: List<String>,
#Input(Parameter::class)
val slug: Slug
)
Using annotation processor, I was able to get Response properties using this approach:
val parentMetadata = (element as TypeElement).toImmutableKmClass()
parentMetadata.constructors[0].valueParameters.map { prop ->
// this will loop through class properties and return ImmutableKmValueParameter
// eg: id, title, label, images, slug.
// then, I need to get the annotation that property has here.
// eg: #Input(Parameter::class) on slug property.
// if prop is slug, this will return true.
val isAnnotationAvailable = prop.hasAnnotations
// Here I need to get the annotated value
// val annotation = [..something..].getAnnotation(Input::class)
// How to get the annotation? So I can do this:
if ([prop annotation] has [#Input]) {
do the something.
}
}
Previously I tried to get the annotation like this:
val annotations = prop.type?.annotations
But, I got empty list even isAnnotationAvailable value is true
Thanks in advance!
Annotations are only stored in metadata if they have nowhere else they can be stored. For parameters, you must read them directly off of the Parameter (reflection) or VariableElement (elements API). This is why we have the ClassInspector API. You almost never want to try to read anything other than basic class data. Anything that's already contained in the bytecode or elements is basically never duplicated into metadata as well. Treat metadata as added signal, not a wholesale replacement.

Boolean getter is serialized twice when annotated with #JsonProperty

Suppose there is a class with a boolean property, its name starts with is:
class Preferrable {
var isPreferred: Boolean = true
}
It is serialized to {"preferred":true}, dropping is part.
As mentioned in this question, to prevent it, we need to specify property name explicitly, using #JsonProperty("isPreferred") annotation.
That approach works perfectly with Java. But in case of Kotlin class with annotated property serialized form contains property duplication: {"preferred":true,"isPreferred":true}.
The workaround is to apply annotation to property getter. It doesn't work for data classes and as for me, this code looks a bit too much for just keeping property name as is:
class Preferrable {
var isPreferred: Boolean = true
#JsonProperty(value = "isPreferred")
get() = field
}
What is the reason behind such behavior? Is it just a bug? Is there a simpler way to prevent is prefix dropping for Kotlin?
Booleans are handled a bit differently from other data types. You need to explicitly use #get in the annotation:
#get:JsonProperty("isPreferred")
var isPreferred: Boolean = true
Note that this does work with data classes, e.g.
data class Preferrable(
#get:JsonProperty("isPreferred")
var isPreferred: Boolean = true
)
See this question for a bit more info (and a link to where this is discussed in more detail).