AnyEndpoint
A specific HTTP method in a resource.
All instances of this interface must be immutable.
Example
Endpoints are declared in the body of a resource, by calling one of the HTTP method names.
object User : DynamicResource<Users>("user", parent = Users) {
// GET …/{user}
val get by get()
.response<UserDto>()
// POST …/{user}
val create by post()
.request<UserCreationDto>()
.response<UserDto>()
// PATCH …/{user}
val edit by patch()
.request<UserEditionDto>()
.response<UserDto>()
// DELETE …/{user}
val delete by delete()
}Configuration options on an endpoint during its creation are listed in AnyEndpoint.Builder.
The trick
To represent endpoints in a type-safe manner, they must declare their information as type parameters. For example, the requestType and responseType must appear in type parameters.
However, adding a type parameter to a class in Kotlin is a source-incompatible change: all usages of a type must explicitly say which type parameters are used.
Because endpoints are the core of this library, and we expect to add more information to endpoints in the future, we know that we will want to add new type parameters, which would break source compatibility for all users!
To avoid this, the only part of the public API is AnyEndpoint, which does not declare the type parameters at all. AnyEndpoint is therefore safe to use in user code. However, it lacks typing information (though the exact types are still available as reflection entities in requestType and responseType). AnyEndpoint is a sealed interface with a single implementation that is hidden in the library, Endpoint.
When we declare an endpoint, we do not specify the type explicitly:
val list by get()
.response<List<UserDto>>()Because we did not declare a type, Kotlin infers it to the real type returned by the function, even though it is hidden. In fact, it infers the type to be:
val list: Endpoint<Unit, UserDto, Parameters.Empty> by get()
.response<List<UserDto>>()As you can see, all type parameters are indeed declared. You can see this by enabling inlay hints in IntelliJ.
However, if you try to write the type yourself, you will see that it will not compile, because Endpoint cannot be accessed. This is an intended protection: you can create a value of type Endpoint, but you cannot write the type Endpoint in your code, because the type Endpoint will change in source-incompatible ways in the future. Writing a value of type Endpoint without the type appearance in your code is safe, so it is allowed.
The type you are allowed to use is AnyEndpoint, this interface:
val list: AnyEndpoint by get()
.response<List<UserDto>>()However, this interface doesn't have type parameters, so this removes all type safety. As a consequence, if you do this, none of the other functions of this library will compile for this endpoint, as it cannot be used safely.
This interface is still useful because you may want to create operations that act on any endpoint without caring about the type of a specific one. For example, you may create a function that accepts an endpoint and prints information about it:
fun AnyEndpoint.print() {
println("$method $fullSlug")
println(" - Input: $requestType")
println(" - Output: $responseType")
}As a rule of thumb:
Endpoints declaration should not have an explicit type declaration, and should instead rely on the inferred type.
If you want to process endpoints, use
AnyEndpoint.If you really absolutely must use type parameters, you can force access to
Endpointvia a suppression. Note, however, that this guarantees that your code will stop compiling in future versions of this library.
Types
Properties
Constructor for query parameters.
The complete URL for this endpoint, starting at its RootResource.
The HttpMethod used when invoking this Endpoint.
Optional path extension from the Resource this endpoint is a part of.
The type of the body went the client sends data to the server.
The type of the body when the server responds to a client.