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

Controller and interfaces with generic methods result in wrong generated client #1025

Open
SomMeri opened this issue Sep 11, 2023 · 3 comments

Comments

@SomMeri
Copy link

SomMeri commented Sep 11, 2023

I have a controller that inherits form a generic interface:

public interface GenericInterface<T extends MyInterface> {
  void doSomething(T thing);
}

The controller itself is using a concrete implemenations of the interface:

  public void doSomething(@RequestBody MyDto thing) {}

This results in two different client methods generated by typescript generator:

    doSomething$GET$something(arg0: MyDto): RestResponse<void> { }
    doSomething$GET$something(arg0: MyInterface): RestResponse<void> { }

However, in java, the controller is able to deal only with MyDto instances. The controller is NOT accepting an arbitrary MyInterface instance. The doSomething method is available only once - with the MyDto parameter.

How to reproduce

// data structures
public interface MyInterface {} 
public class MyDto implements MyInterface {}


// controller and its interface
public interface GenericInterface<T extends MyInterface> {
  void doSomething(T thing);
}

@RequestMapping("/something")
@RestController
public class ConcreteController implements GenericInterface<MyDto> {

  @GetMapping
  @Override
  public void doSomething(@RequestBody MyDto thing) {

  }
}

Main class:

public static void main(String[] args) {
    Settings settings = new Settings();
    settings.outputKind = TypeScriptOutputKind.module;
    settings.outputFileType = TypeScriptFileType.implementationFile;
    settings.jsonLibrary = JsonLibrary.jackson2;
    settings.generateSpringApplicationClient = true;
    settings.generateSpringApplicationInterface = true;

    TypeScriptGenerator generator = new TypeScriptGenerator(settings);

    String result = generator.generateTypeScript(
      Input.from(ConcreteController.class)
    );

    System.out.println(result);
  }

Actual Result

export class RestApplicationClient implements RestApplication {

    constructor(protected httpClient: HttpClient) {
    }

    /**
     * HTTP GET /something
     * Java method: com.meri.ConcreteController.doSomething
     */
    doSomething$GET$something(arg0: MyDto): RestResponse<void> {
        return this.httpClient.request({ method: "GET", url: uriEncoding`something`, data: arg0 });
    }

    /**
     * HTTP GET /something
     * Java method: com.meri.ConcreteController.doSomething
     */
    doSomething$GET$something(arg0: MyInterface): RestResponse<void> {
        return this.httpClient.request({ method: "GET", url: uriEncoding`something`, data: arg0 });
    }
}

Expected Result

export class RestApplicationClient implements RestApplication {

    constructor(protected httpClient: HttpClient) {
    }

    /**
     * HTTP GET /something
     * Java method: com.meri.ConcreteController.doSomething
     */
    doSomething(arg0: MyDto): RestResponse<void> {
        return this.httpClient.request({ method: "GET", url: uriEncoding`something`, data: arg0 });
    }

}
@SomMeri
Copy link
Author

SomMeri commented Sep 11, 2023

Workaround

Create an extension so that typescript generator ignores bridge methods:

class WorkaroundExtension extends Extension {

  public List<TransformerDefinition> getTransformers() {
    return Arrays.asList(new TransformerDefinition(ModelCompiler.TransformationPhase.BeforeTsModel, new Transformer()));
  }

  @Override
  public EmitterExtensionFeatures getFeatures() {
    return new EmitterExtensionFeatures();
  }
}

class Transformer implements ModelTransformer {
  public Model transformModel(SymbolTable symbolTable, Model model) {

    List<RestApplicationModel> newRestApplications = model.getRestApplications().stream().map(restApplication ->
      new RestApplicationModel(
        restApplication.getType(),
        restApplication.getApplicationPath(),
        restApplication.getApplicationName(),
        restApplication.getMethods().stream().filter(it -> !it.getOriginalMethod().isBridge()).collect(Collectors.toList()))
    ).collect(Collectors.toList());

    return new Model(model.getBeans(), model.getEnums(), newRestApplications);
  }
}

And configure it:

  public static void main(String[] args) {
    Settings settings = new Settings();
    settings.outputKind = TypeScriptOutputKind.module;
    settings.outputFileType = TypeScriptFileType.implementationFile;
    settings.jsonLibrary = JsonLibrary.jackson2;
    settings.generateSpringApplicationClient = true;
    settings.generateSpringApplicationInterface = true;
    settings.extensions.add(new WorkaroundExtension()); //<-- here

    TypeScriptGenerator generator = new TypeScriptGenerator(settings);

    String result = generator.generateTypeScript(
      Input.from(ConcreteController.class)
    );

    System.out.println(result);
  }

@chrisdutz
Copy link

Having the same problem:

I want to create a set of controllers for various types of entities and accompany the frontend with a generic component to handle displaying and editing them.

So I have a base interface like this:

import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public interface AdminController<T> {

    @GetMapping
    List<T> findAll();

    @GetMapping("/{id}")
    T findById(@PathVariable long id);

    @PostMapping
    T save(@RequestBody T item);

    @DeleteMapping("/{id}")
    void deleteById(@PathVariable long id);

}

However as soon as I implement this interface in a concrete controller, I get duplicate "save" methods with this strange "$" notation. If I use your workaround, the generated code seems to be missing quite a bit.

Would be really great if this issue could be solved. Please tell me if there's a way I can help.

@chrisdutz
Copy link

chrisdutz commented Dec 24, 2024

Ok ... I found out what was going on ... Problem was, that I was already using an extension for Axios.

So I ended up with this:

import cz.habarta.typescript.generator.compiler.ModelCompiler;
import cz.habarta.typescript.generator.ext.AxiosClientExtension;

import java.util.List;

public class GenericInterfaceWorkaroundExtension extends AxiosClientExtension {

    public List<TransformerDefinition> getTransformers() {
        return List.of(new TransformerDefinition(ModelCompiler.TransformationPhase.BeforeTsModel, new Transformer()));
    }

}

And:

import cz.habarta.typescript.generator.compiler.ModelTransformer;
import cz.habarta.typescript.generator.compiler.SymbolTable;
import cz.habarta.typescript.generator.parser.Model;
import cz.habarta.typescript.generator.parser.RestApplicationModel;

import java.util.List;
import java.util.stream.Collectors;

class Transformer implements ModelTransformer {
    public Model transformModel(SymbolTable symbolTable, Model model) {

        List<RestApplicationModel> newRestApplications = model.getRestApplications().stream().map(restApplication ->
                new RestApplicationModel(
                        restApplication.getType(),
                        restApplication.getApplicationPath(),
                        restApplication.getApplicationName(),
                        restApplication.getMethods().stream().filter(it -> {
                            return !it.getOriginalMethod().isBridge();
                        }).collect(Collectors.toList()))
        ).collect(Collectors.toList());

        return new Model(model.getBeans(), model.getEnums(), newRestApplications);
    }
}

But I needed to also copy the two files:

  • AxiosClientExtension-client.template.ts
  • AxiosClientExtension-shared.template.ts

From the original AxiosClientExtension package (cz.habarta.typescript.generator.ext) into the package of my own extension. Because internally it uses getClass().getResourceAsStream to generate the axios client code at the bottom.

With these two classes (and the templates in place) I could now generate my client.

Thanks for your help with the original Extension. Wouldn't have been able to finish it before Christmas Dinner otherwise ;-)

Merry Christmas to all :-)

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

2 participants