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

feat: @RestResource annotation and source generation #97

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

sdelamo
Copy link
Contributor

@sdelamo sdelamo commented May 22, 2024

This PR adds the @RestResource annotation and a visitor. The idea is to generate to controller given Micronaut Data CrudRepository.

For example, for a Book entity:

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;

@Serdeable
@MappedEntity
public record Book(@GeneratedValue @Id @Nullable Long id, String title) {
}

and a JDBC repository:

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

@JdbcRepository(dialect = Dialect.H2)
public interface BookRepository extends CrudRepository<Book, Long> {
}

And a class annotated with @RestResource:

import io.micronaut.sourcegen.annotations.RestResource;

@RestResource(repository = BookRepository.class, rolesAllowed = { "isAnonymous()" })
class BookRestResource {
}

Generates the following controller:

@Controller("/books")
@RolesAllowed("isAnonymous()")
class BookController {
  @Inject
  BookRepository repository;

  @Get
  Iterable<Book> findAll() {
    return this.repository.findAll();
  }

  @Get("/{id}")
  Optional<Book> findById(Long id) {
    return this.repository.findById(id);
  }

  @Delete("/{id}")
  @Status(HttpStatus.NO_CONTENT)
  void deleteById(Long id) {
    this.repository.deleteById(id);
  }

  @Status(HttpStatus.CREATED)
  @Post
  void save(@Body Book entity) {
    this.repository.save(entity);
  }

  @Put
  @Status(HttpStatus.NO_CONTENT)
  void update(@Body Book entity) {
    this.repository.update(entity);
  }
}

I would prefer to generate

repository.deleteById(id);

instead of

this.repository.deleteById(id)

but not sure how to do this with the api.

Also, the save method should probably be: but that was challenging with the current API as well.

Book savedBook = bookRepository.save(book);
return HttpResponse.created(UriBuilder.of("/books").path(savedBook.id().toString()).build());

thoughts ?

This PR adds the @RestResource annotation and a visitor. the idea is to generate to combine the annotation with a Micronaut Data CrudRepository to generate a REST controller.

For example, for a Book entity:

```java
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;

@Serdeable
@MappedEntity
public record Book(@GeneratedValue @id @nullable Long id, String title) {
}
```

and a JDBC repository:

```java
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

@JdbcRepository(dialect = Dialect.H2)
public interface BookRepository extends CrudRepository<Book, Long> {
}
```

And a class annotated with `@RestResource`:

```java
import io.micronaut.sourcegen.annotations.RestResource;

@RestResource(repository = BookRepository.class, rolesAllowed = { "isAnonymous()" })
class BookRestResource {
}
```

Generates the following controller:

```java
@controller("/books")
@RolesAllowed("isAnonymous()")
class BookController {
  @Inject
  BookRepository repository;

  @get
  Iterable<Book> findAll() {
    return this.repository.findAll();
  }

  @get("/{id}")
  Optional<Book> findById(Long id) {
    return this.repository.findById(id);
  }

  @delete("/{id}")
  @Status(HttpStatus.NO_CONTENT)
  void deleteById(Long id) {
    this.repository.deleteById(id);
  }

  @Status(HttpStatus.CREATED)
  @post
  void save(@Body Book entity) {
    this.repository.save(entity);
  }

  @put
  @Status(HttpStatus.NO_CONTENT)
  void update(@Body Book entity) {
    this.repository.update(entity);
  }
}
```
@sdelamo
Copy link
Contributor Author

sdelamo commented May 22, 2024

I used field injection because I did not see in the api how to generate a constructor and use constructor injection.

@sdelamo sdelamo added the enhancement New feature or request label May 22, 2024
@graemerocher
Copy link
Contributor

looks useful but would be good to have meta annotations for create/read/update/delete and have @RestResource be a meta annotation

That said this could also be achieved with an interface with default methods which may be more natural/easier to extend.

Copy link

sonarcloud bot commented May 22, 2024

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 70%)

See analysis details on SonarCloud

@dstepanov
Copy link
Collaborator

It might be good to generate some DTOs and allow to make some fields hidden, readonly etc

@graemerocher
Copy link
Contributor

graemerocher commented May 22, 2024

interface CrudResource<E, I> {
    CrudRepository<E, I> repository();

    @Get
    default Iterable<Book> findAll() {
        return repository().findAll();
    }

    @Get("/{id}")
    default Optional<Book> findById(Long id) {
        return repository().findById(id);
    }

    @Delete("/{id}")
    @Status(HttpStatus.NO_CONTENT)
    default void deleteById(Long id) {
        repository().deleteById(id);
    }

    @Status(HttpStatus.CREATED)
    @Post
    default void save(@Body Book entity) {
        repository().save(entity);
    }

    @Put
    @Status(HttpStatus.NO_CONTENT)
    default void update(@Body Book entity) {
        repository().update(entity);
    }
}

@Controller("/books")
@RolesAllowed("isAnonymous()")
record BookResource(BookRepository repository) implements CrudResource<Book, Long> {
}

CrudResource could be composed of different interfaces for CRUD operations that can be selectively overridden to customise behaviour.

The code generation approach has limitations in terms of how you can customise the output.

@sdelamo
Copy link
Contributor Author

sdelamo commented May 22, 2024

interface CrudResource<E, I> {
    CrudRepository<E, I> repository();

    @Get
    default Iterable<Book> findAll() {
        return repository().findAll();
    }

    @Get("/{id}")
    default Optional<Book> findById(Long id) {
        return repository().findById(id);
    }

    @Delete("/{id}")
    @Status(HttpStatus.NO_CONTENT)
    default void deleteById(Long id) {
        repository().deleteById(id);
    }

    @Status(HttpStatus.CREATED)
    @Post
    default void save(@Body Book entity) {
        repository().save(entity);
    }

    @Put
    @Status(HttpStatus.NO_CONTENT)
    default void update(@Body Book entity) {
        repository().update(entity);
    }
}

@Controller("/books")
@RolesAllowed("isAnonymous()")
record BookResource(BookRepository repository) implements CrudResource<Book, Long> {
}

CrudResource could be composed of different interfaces for CRUD operations that can be selectively overridden to customise behaviour.

The code generation approach has limitations in terms of how you can customise the output.

but such an api will be in Micronaut Data not here right?

@graemerocher
Copy link
Contributor

probably

@sdelamo sdelamo marked this pull request as draft November 6, 2024 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants