Skip to content
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

JAX RS with generic #219

Closed
hacker-cb opened this issue Feb 12, 2018 · 13 comments
Closed

JAX RS with generic #219

hacker-cb opened this issue Feb 12, 2018 · 13 comments

Comments

@hacker-cb
Copy link

hacker-cb commented Feb 12, 2018

Hello!

Seems that there is bug when generating JAX RS. (I have tried it online on https://jechlin.github.io/ts-gen-aws/)

JAVA source:

import javax.ws.rs.*;

public class AccountDto {
    public Integer id;
    public String name;
}

interface AbstractCrudResource<ENTITY,ID> {
    @GET
    @Path("{id}")
    public ENTITY get(@PathParam("id") ID id);
}

@Path("/account")
interface AccountResource extends AbstractCrudResource<AccountDto,Integer> {
    @GET
    @Path("/test")
    void test();
}

Config:

{
        "$schema"       : "http://dummy/tsgen.json",
        "outputFileType": "implementationFile",
        "outputKind"    : "module",
        "jsonLibrary"   : "jackson2",
        "noFileComment" : true,
        "generateJaxrsApplicationInterface": true,
        "generateJaxrsApplicationClient": true,
        "jaxrsNamespacing": "perResource",
        "restOptionsType": "Object",
        "excludeClasses": [
            "groovy.lang.GroovyObject",
            "AbstractCrudResource"
        ]
    }

Output:

export interface AccountDto {
    id: number;
    name: string;
}

export interface HttpClient {

    request<R>(requestConfig: { method: string; url: string; queryParams?: any; data?: any; copyFn?: (data: R) => R; options?: Object; }): RestResponse<R>;
}

export type RestResponse<R> = Promise<R>;

function uriEncoding(template: TemplateStringsArray, ...substitutions: any[]): string {
    let result = "";
    for (let i = 0; i < substitutions.length; i++) {
        result += template[i];
        result += encodeURIComponent(substitutions[i]);
    }
    result += template[template.length - 1];
    return result;
}

So, whats wrong?

@hacker-cb hacker-cb changed the title JAXRS with generic JAX RS with generic Feb 12, 2018
@vojtechhabarta
Copy link
Owner

It is probably some limitation of the playground, how it parses the input.
Here is how it is implemented: https://github.com/jechlin/ts-gen-aws/blob/master/src/main/groovy/org/jamieechlin/ts/Lambda.groovy

If I put just AccountResource into input text area like this:

import javax.ws.rs.*;

@Path("/account")
interface AccountResource {
    @GET
    @Path("/test")
    void test();
}

it successfully generated AccountResourceClient TypeScript class:

export class AccountResourceClient {

    constructor(protected httpClient: HttpClient) {
    }

    /**
     * HTTP GET /account/test
     * Java method: AccountResource.test
     */
    test(options?: Object): RestResponse<void> {
        return this.httpClient.request({ method: "GET", url: uriEncoding`account/test`, options: options });
    }
}

@jechlin do you have any comments how groovy parses the input?

@hacker-cb
Copy link
Author

Ok, in online example it can be groovy bug. But in real app typescript generator still don't want to resolve generics. ENTITY and ID was not resolved.

I have tried with version 2.1.406.

Source:

import java.io.Serializable;
import java.util.List;
import javax.ws.rs.*;

public class AccountDto {

    public Integer id;

    public String login;
}

public interface AbstractCrudResource<ENTITY, ID extends Serializable> {

    @GET
    @Path("{id}")
    ENTITY get(@PathParam("id") ID id);

    @POST
    @Path("")
    ENTITY save(ENTITY entity);

     /**
     * Delete entity
     */
    @DELETE
    @Path("{id}")
    void delete(@PathParam("id") ID id);
}

@Path("/accounts/")
public interface AccountResource extends AbstractCrudResource<AccountDto,Integer>{

    @GET
    @Path("")
    List<AccountDto> list(
            @QueryParam("start") Long offset,
            @QueryParam("limit") Integer limit,
            @QueryParam("sort") List<String> sort);
}

Result:

// Generated using typescript-generator version 2.1.406 on 2018-02-13 20:45:54.

import { Observable } from 'rxjs/Observable';;

export interface HttpClient {

    request<R>(requestConfig: { method: string; url: string; queryParams?: any; data?: any; copyFn?: (data: R) => R; options?: Object; }): RestResponse<R>;
}

export interface AccountResource {

    /**
     * HTTP DELETE /accounts/{id}
     * Java method: com.test.api.providerapi.test.AccountResource.delete
     */
    delete(id: ID, options?: Object): RestResponse<void>;

    /**
     * HTTP GET /accounts/{id}
     * Java method: com.test.api.providerapi.test.AccountResource.get
     */
    get(id: ID, options?: Object): RestResponse<ENTITY>;

    /**
     * HTTP GET /accounts
     * Java method: com.test.api.providerapi.test.AccountResource.list
     */
    list(queryParams?: { start?: number; limit?: number; sort?: string[]; }, options?: Object): RestResponse<AccountDto[]>;

    /**
     * HTTP POST /accounts
     * Java method: com.test.api.providerapi.test.AccountResource.save
     */
    save(arg0: ENTITY, options?: Object): RestResponse<ENTITY>;
}

export class AccountResourceClient implements AccountResource {

    constructor(protected httpClient: HttpClient) {
    }

    /**
     * HTTP DELETE /accounts/{id}
     * Java method: com.test.api.providerapi.test.AccountResource.delete
     */
    delete(id: ID, options?: Object): RestResponse<void> {
        return this.httpClient.request({ method: "DELETE", url: uriEncoding`accounts/${id}`, options: options });
    }

    /**
     * HTTP GET /accounts/{id}
     * Java method: com.test.api.providerapi.test.AccountResource.get
     */
    get(id: ID, options?: Object): RestResponse<ENTITY> {
        return this.httpClient.request({ method: "GET", url: uriEncoding`accounts/${id}`, options: options });
    }

    /**
     * HTTP GET /accounts
     * Java method: com.test.api.providerapi.test.AccountResource.list
     */
    list(queryParams?: { start?: number; limit?: number; sort?: string[]; }, options?: Object): RestResponse<AccountDto[]> {
        return this.httpClient.request({ method: "GET", url: uriEncoding`accounts`, queryParams: queryParams, options: options });
    }

    /**
     * HTTP POST /accounts
     * Java method: com.test.api.providerapi.test.AccountResource.save
     */
    save(arg0: ENTITY, options?: Object): RestResponse<ENTITY> {
        return this.httpClient.request({ method: "POST", url: uriEncoding`accounts`, data: arg0, options: options });
    }
}

export interface AbstractCrudResource<ENTITY, ID> {
}

export interface AccountDto {
    id: number;
    login: string;
}

export type RestResponse<R> = Observable<R>;

function uriEncoding(template: TemplateStringsArray, ...substitutions: any[]): string {
    let result = "";
    for (let i = 0; i < substitutions.length; i++) {
        result += template[i];
        result += encodeURIComponent(substitutions[i]);
    }
    result += template[template.length - 1];
    return result;
}

Config:

            <plugin>
                <groupId>cz.habarta.typescript-generator</groupId>
                <artifactId>typescript-generator-maven-plugin</artifactId>
                <version>${maven.typescript.generator.version}</version>
                <configuration>
                    <!--Common configuration-->
                    <jsonLibrary>jackson2</jsonLibrary>
                    <outputKind>module</outputKind>
                    <!--<removeTypeNameSuffix>Dto</removeTypeNameSuffix>-->
                    <sortDeclarations>true</sortDeclarations>
                    <sortTypeDeclarations>true</sortTypeDeclarations>
                </configuration>
                <executions>
                    <execution>
                        <id>providerapi-generate-rest</id>
                        <phase>prepare-package</phase>
                        <goals><goal>generate</goal></goals>
                        <configuration>
                            <jsonLibrary>jackson2</jsonLibrary>
                            <outputFileType>implementationFile</outputFileType>
                            <outputFile>${providerapi.ts.dir}/src/providerapi.ts</outputFile>
                            <outputKind>module</outputKind>
                            <generateJaxrsApplicationInterface>true</generateJaxrsApplicationInterface>
                            <generateJaxrsApplicationClient>true</generateJaxrsApplicationClient>
                            <jaxrsNamespacing>perResource</jaxrsNamespacing>
                            <classPatterns>
                                <pattern>com.test.api.providerapi.test.**</pattern>
                            </classPatterns>
                            <importDeclarations>
                                <importDeclaration>import { Observable } from 'rxjs/Observable';</importDeclaration>
                            </importDeclarations>
                            <restOptionsType>Object</restOptionsType>
                            <restResponseType>Observable&lt;R&gt;</restResponseType>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

@vojtechhabarta
Copy link
Owner

Now I understand what you mean. Thanks for the example.

And how does it work when AccountResource is just an interface without implementation?
Having AbstractCrudResource as interface seems reasonable but I would expect AccountResource to be a class.

@hacker-cb
Copy link
Author

Actually, I have separted all interfaces for my REST API in the shared maven module.
This module has only a few dependencies (just jackson-annotations, jsr311-api and validation-api).
So, this shared module can be used from different projects very easy, without any extra dependencies.
This shared module contain only interfaces, it does not contain any classes.
I'm successfully using this common module to communicate between JAVA projects, but now I also would like to use it from TypeScript.
So, it will be great, if you can add this kind of generation to your project.

@hacker-cb
Copy link
Author

So, @vojtechhabarta , how do you think about my idea? Can we expect such functionality of your generator in near future?

@vojtechhabarta
Copy link
Owner

I would add this functionality but I am still thinking how to resolve generic variables in inherited methods. It seems that it is not possible to get resolved types directly using reflections. It is needed to 1. find recursively path from processed class to ancestor class which declares inherited method and 2. manually resolve generic variables on this path.

Consider this example:

interface Parent1<T> {
    T get1();
}
interface Parent2<T> {
    T get2();
}
interface Intermediate<T, U, V> extends Parent1<U>, Parent2<V> {
}
interface Child<Z> extends Comparable<Double>, Intermediate<Number, List<String>, Z> {
}

When resolving return type of Child.get1() method we need to

  • substitute U in Intermediate with List<String> and then
  • substitute T in Parent1 with List<String>.

Or do you think there is an easier way how to resolve generic variables correctly?

@vojtechhabarta
Copy link
Owner

Currently I don't have plans to implement this since it is too complex for such special (but perfectly valid) use case.

@ruediste
Copy link
Contributor

ruediste commented Jun 16, 2019

I ran into the same issue. One way would be to use guava's TypeToken. Either by adding a dependency on guava (guess you are reluctant to this), or by shading that class and it's dependencies (Guava is under the Apache 2.0 license). Using that, it should not be too complicated.

@RobbinBaauw
Copy link
Contributor

RobbinBaauw commented Aug 21, 2019

I was hoping a temporary workaround to this would be the following:

import javax.ws.rs.*;

public class AccountDto {
    public Integer id;
    public String name;
}

interface AbstractCrudResource<ENTITY, ID> {
    @GET
    @Path("{id}")
    public ENTITY get(@PathParam("id") ID id);
}

@Path("/account")
interface AccountResource extends AbstractCrudResource<AccountDto, Integer> {
    @GET
    @Path("/test")
    void test();

   @Override
    public AccountDto get(Integer id); // <-- added this
}

Unfortunately, this doesn't do anything as the JAX annotations aren't annotated with @Inherit. Maybe it is possible to search up the super-class chain to the right annotation for an overridden method? Adding path annotations and such is also possible but this could lead to many errors, this way it gets checked by the compiler.

(This would probably not be of any help for #380, but for JAX RS users it's at least something)

Edit: tried adding the annotations to the overridden method, then it generates a duplicate method with a weird name. In my case: getEntityById$GET$entities_id (whereas the method was called getEntityById, with HTTP method GET and with PathParam id). I'm using version 2.16.538 with Kotlin 1.3.41

@vojtechhabarta
Copy link
Owner

I pushed generics resolution in JAX-RS resources.
Feel free to try it.

@RobbinBaauw
Copy link
Contributor

Thanks! I will try it in my project tomorrow!

@RobbinBaauw
Copy link
Contributor

Yes it works! Thanks a lot!

@vojtechhabarta
Copy link
Owner

Released in version 2.17.558.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants