Open shafreenAnfar opened 2 years ago
Following is how Spring Boot does.
On a slightly related note, apart from hardcoding the href
value, in case someone is worried about polluting his/her entity recods with the hateoas:Links
record, they can do the following.
type Person record {|
string firstname;
string lastname;
|};
type PersonRepresentation record {|
*hateoas:Links;
*Person;
|};
public function main() returns error? {
Person p = { firstname: "Joe", lastname: "Biden"};
PersonRepresentation pr = { ...p, _links: account: {href: "/org/dep/account"}}};
}
In order to add links programmatically, we need a reference to the service object so that we can extract the path, method and return media-types. I could not find a way to get this reference directly in a service declaration. So the alternative way is to access it using a parameter of the resource function. Since we already have the service reference in RequestContext
for the interceptor execution, RequestContext
seems to be a good candidate for this.
The following new functions can be added to RequestContext
:
# Defines the possible simple parameter types.
public type SimpleParamType boolean|int|float|decimal|string;
# Defines the path parameters used in link creation.
# + resourceName - resourceName which cannot be used as a path parameter
# + relation - relation which cannot be used as a path parameter
# + method - method which cannot be used as a path parameter
public type LinkPathParams record {|
never resourceName?;
never relation?;
never method?;
SimpleParamType...;
|};
public isolated class RequestContext {
...
private map<Link>? links = ();
...
# Adds a link to the context.
#
# + resourceName - The name of the resource specified in the `ResourceConfig` annotation
# + relation - The relation between the current context and the linked resource
# + method - The method of the linked resource
# + pathParams - The path parameters
# + return - Nil or an error if the link creation failed
public isolated function addLink(string resourceName, string relation, string? method = (),
*LinkPathParams pathParams) returns error? {
// Errors :
// Resource not found
// Cannot resolve Resource without method
// Duplicate relation found
// Path params not found
// Path param type not matched
Link link = check externGetResourceLink(self, resourceName, method, pathParams);
lock {
self.links[relation] = link.clone();
}
}
# Returns the links from the context.
#
# + return - The map of `Link` objects
public isolated function getLinks() returns map<Link>? {
lock {
if self.links == {} {
return ();
}
return self.links.clone();
}
}
# Removes the links from the context.
public isolated function removeLinks() {
lock {
self.links = {};
}
}
}
@shafreenAnfar @chamil321 WDYT? Is it ok to use the RequestContext
for this purpose or should we use/introduce some other parameter?
In order to add links programmatically, we need a reference to the service object so that we can extract the path, method and return media-types. I could not find a way to get this reference directly in a service declaration. So the alternative way is to access it using a parameter of the resource function. Since we already have the service reference in
RequestContext
for the interceptor execution,RequestContext
seems to be a good candidate for this.The following new functions can be added to
RequestContext
:# Defines the possible simple parameter types. public type SimpleParamType boolean|int|float|decimal|string; # Defines the path parameters used in link creation. # + resourceName - resourceName which cannot be used as a path parameter # + relation - relation which cannot be used as a path parameter # + method - method which cannot be used as a path parameter public type LinkPathParams record {| never resourceName?; never relation?; never method?; SimpleParamType...; |}; public isolated class RequestContext { ... private map<Link> links = {}; ... # Adds a link to the context. # # + resourceName - The name of the resource specified in the `ResourceConfig` annotation # + relation - The relation between the current context and the linked resource # + method - The method of the linked resource # + return - Nil or an error if the link creation failed public isolated function addLink(string resourceName, string relation, string? method = (), *LinkPathParams pathParams) returns error? { Link link = check externGetResourceLink(self, resourceName, method, pathParams); lock { self.links[relation] = link.clone(); } } # Returns the links from the context. # # + return - The map of `Link` objects public isolated function getLinks() returns map<Link> { lock { return self.links.clone(); } } # Removes the links from the context. public isolated function removeLinks() { lock { self.links = {}; } } }
@shafreenAnfar @chamil321 WDYT? Is it ok to use the
RequestContext
for this purpose or should we use/introduce some other parameter?
+1 RequestContext
will be a good candidate as it contains necessary properties.
Or else, a bit simpler way without relating to other objects would be
type Person record {|
string firstname;
string lastname;
|};
service / on securedEP {
resource function get person() returns record {|*Person; *hateoas:Links links;|} {
hateoas:Links hotelLinks = http:createLinks("hotel", relation, GET, id = 5);
return {firstname: "WSO2", lastname: "Ballerina", links: hotelLinks};
}
}
Since this is an imperative way, user may change the return type along with links
As discussed in the previous meeting, I was thinking of the following way which is similar to SpringBoot.
type Representation record {|
*Links;
anydata...;
|};
resource function get persons/[int id]() returns http:Representation|error {
Person person = getPerson(id);
// Creating an `EntityModel` using the person record. For this person record should be closed one
// and it should not contain `_links` field.
http:EntityModel personModel= new(person);
// Adding links to the model. This can return an error if link creation failed
check personModel.addLink(relation1, resourceName1, method1?, ...PathParams1);
check personModel.addLink(relation2, resourceName2, method2?, ...PathParams2);
...
// Returning the representation from the entity. Since we don't support returning object type from
// a resource, the developer should call this `getRepresentation`.
return personModel.getRepresentation();
}
The user might want to return a custom representation type rather than http:Representation
. Following is an example of returning user-defined representation :
type Person record {|
string name;
int age;
|};
type PersonRepresentation record {|
*http:Links;
*Person;
|};
resource function get persons/[int id]() returns PersonRepresentation|error {
Person person = getPerson(id);
http:EntityModel personModel= new(person);
check personModel.addLink(relation1, resourceName1, method1?, ...PathParams1);
check personModel.addLink(relation2, resourceName2, method2?, ...PathParams2);
...
// The return type of `getRepresentation()`is same as the target type. And this function
// can return an error if the representation of entity is not matched with the target type
return personModel.getRepresentation();
}
The following is the definition of EntityModel
:
type SimpleParamType boolean|int|float|decimal|string;
type LinkPathParams record {|
never resourceName?;
never relation?;
never method?;
SimpleParamType...;
|};
class EntityModel {
private map<Link> links;
private Representation representation;
public function init(record{|never _links?; anydata...;|} representation) {
self.links = {};
self.representation = {
_links: {},
...representation
};
}
public function getLinks() returns map<Link> {
return self.links;
}
public isolated function getRepresentation(RepresentationType representationType = <>)
returns representationType|error = external;
public isolated function addLink(string relation, string resourceName, string? method,
*LinkPathParams pathParams) returns error? = external;
}
Note : This only covers returning a single representation from a resource function. Need to think about returning a collection of representations.
@chamil321 @shafreenAnfar Please add your thoughts on this
At the moment this is how we can add links in Ballerina. Consider the below example.
Now let's say you want to add a link to the response of the first resource to point to the second resource. In that case following things are needed to be done.
Represents available server-provided links
public type Links record {| map _links;
|};