Skip to content
This repository has been archived by the owner on May 3, 2023. It is now read-only.

Commit

Permalink
docs(swagger): set up annotation-based documentation (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
annibalsilva authored Jan 23, 2023
1 parent 03bddc8 commit dd4ad6a
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 172 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ your problem is someone else's problem. Let's figure it out together. So, ask
a question using our channels. We have [our own Stackoverflow](https://stackoverflow.developer.gov.bc.ca/)
and [our Rocket Chat](https://chat.developer.gov.bc.ca/) channel.

# Stack
## Stack

Here you can find a comprehensive list of all languages and tools that are been used
in this service. And also everything you need to get started, build locally, test
Expand Down Expand Up @@ -54,7 +54,7 @@ and deploy it.
- Postman
- DBeaver

# Getting started
## Getting started

Once you have cloned this repository, can get it running by typing: `./mvnw spring-boot:run`
from the project root directory. You **must** provide three environment variables for database
Expand All @@ -72,7 +72,7 @@ the `status` property should have the value *UP*.
Before writing your first line of code, and learn more about the checks, including
tests, please take a moment and check out our [CONTRIBUTING](CONTRIBUTING.md) guide.

## Quick look
### Quick look

But if all you want is to take a quick look on the running service, you can do it by
using Docker.
Expand Down Expand Up @@ -105,6 +105,8 @@ However, if you have docker-compose you can do:
docker-compose --env-file .env -f ./docker-compose.yml up --build --force-recreate --no-deps
```

You can then check the API documentation accessing `localhost:8090/swagger-ui.html`.

## Getting help

As mentioned, we're here to help. Feel free to start a conversation
Expand Down
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>

<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.8</version>
</dependency>

<!-- Authentication and Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
41 changes: 20 additions & 21 deletions src/main/java/ca/bc/gov/backendstartapi/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package ca.bc.gov.backendstartapi.config;

import com.nimbusds.jose.shaded.gson.JsonArray;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -64,27 +62,28 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

private Converter<Jwt, AbstractAuthenticationToken> converter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(roleConverter());
converter.setJwtGrantedAuthoritiesConverter(roleConverter);
return converter;
}

private Converter<Jwt, Collection<GrantedAuthority>> roleConverter() {
return jwt -> {
final JsonArray realmAccess = (JsonArray) jwt.getClaims().get("client_roles");
List<GrantedAuthority> authorities = new ArrayList<>();
if (realmAccess == null || realmAccess.isEmpty()) {
String sub = String.valueOf(jwt.getClaims().get("sub"));
if (sub.startsWith("service-account-nr-fsa")) {
authorities.add(new SimpleGrantedAuthority("ROLE_user_read"));
authorities.add(new SimpleGrantedAuthority("ROLE_user_write"));
/**
* Parse the roles of a client from the JWT, if they're present; if not, subjects with service
* accounts are granted read and write permissions.
*/
private final Converter<Jwt, Collection<GrantedAuthority>> roleConverter =
jwt -> {
if (!jwt.getClaims().containsKey("client_roles")) {
String sub = String.valueOf(jwt.getClaims().get("sub"));
return (sub.startsWith("service-account-nr-fsa"))
? List.of(
new SimpleGrantedAuthority("ROLE_user_read"),
new SimpleGrantedAuthority("ROLE_user_write"))
: List.of();
}
return authorities;
}
StreamSupport.stream(realmAccess.spliterator(), false)
.map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new)
.forEach(authorities::add);
return authorities;
};
}
final List<String> realmAccess = (ArrayList<String>) jwt.getClaims().get("client_roles");
return realmAccess.stream()
.map(roleName -> "ROLE_" + roleName)
.map(roleName -> (GrantedAuthority) new SimpleGrantedAuthority(roleName))
.toList();
};
}
56 changes: 56 additions & 0 deletions src/main/java/ca/bc/gov/backendstartapi/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ca.bc.gov.backendstartapi.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* General information to be displayed in the documentation of our API, following the <a
* href="https://spec.openapis.org/oas/latest.html">OpenAPI specification</a>.
*
* <p>The generated documentation is to be rendered by <a href="https://swagger.io/">Swagger</a>.
*/
@Configuration
public class SwaggerConfig {

private static final String BEARER_SECURITY_SCHEME_NAME = "bearerAuth";

/** General information about our API. */
@Bean
public OpenAPI theRestApi() {
return new OpenAPI()
.info(
new Info()
.title("THE database REST API")
.description("A REST API to fetch information from the THE database.")
.version("v0.0.1")
.termsOfService(
"https://www2.gov.bc.ca/gov/content/data/open-data/api-terms-of-use-for-ogl-information")
.license(
new License()
.name("OGL-BC")
.url(
"https://www2.gov.bc.ca/gov/content/data/open-data/open-government-licence-bc")))
.externalDocs(
new ExternalDocumentation()
.description("Our Jira project")
.url("https://apps.nrs.gov.bc.ca/int/jira/projects/FSADT2"))
.components(
new Components()
.addSecuritySchemes(
BEARER_SECURITY_SCHEME_NAME,
new SecurityScheme()
.name(BEARER_SECURITY_SCHEME_NAME)
.type(Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")))
.addSecurityItem(new SecurityRequirement().addList(BEARER_SECURITY_SCHEME_NAME));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ca.bc.gov.backendstartapi.endpoint;

import ca.bc.gov.backendstartapi.vo.CheckVo;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
Expand All @@ -10,6 +11,7 @@
/** This class represents a check endpoint object. */
@Slf4j
@RestController
@Hidden
public class CheckEndpoint {

@Value("${nrbestapi.version}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ca.bc.gov.backendstartapi.dto.UserDto;
import ca.bc.gov.backendstartapi.exception.UserNotFoundException;
import ca.bc.gov.backendstartapi.repository.UserRepository;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
Expand All @@ -25,6 +26,7 @@
@RestController
@RequestMapping("/api/users")
@Setter
@Hidden
public class UserEndpoint {

private UserRepository userRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package ca.bc.gov.backendstartapi.endpoint;

import ca.bc.gov.backendstartapi.endpoint.parameters.PaginatedViaQuery;
import ca.bc.gov.backendstartapi.endpoint.parameters.PaginationParameters;
import ca.bc.gov.backendstartapi.entity.VegetationCode;
import ca.bc.gov.backendstartapi.repository.VegetationCodeRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -23,6 +31,7 @@
@RestController
@RequestMapping(path = "/api/vegetation-codes")
@Validated
@Tag(name = "vegetationCode", description = "Codes describing various vegetation species")
public class VegetationCodeEndpoint {

@Autowired private VegetationCodeRepository vegetationCodeRepository;
Expand All @@ -35,7 +44,20 @@ public class VegetationCodeEndpoint {
* @throws ResponseStatusException with status code 404 if such code doesn't exist
*/
@GetMapping(path = "/{code}", produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
public VegetationCode findByCode(@PathVariable("code") String code) {
@Operation(
summary = "Fetch a vegetation code by its identifier",
description = "Returns the vegetation code identified by `code`, if there is one.",
responses = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true)))
})
public VegetationCode findByCode(
@PathVariable("code")
@Parameter(
name = "code",
in = ParameterIn.PATH,
description = "Identifier of the vegetation code being sought.")
String code) {
var retrievalResult = vegetationCodeRepository.findByCode(code);
return retrievalResult.orElseThrow(
() ->
Expand All @@ -50,13 +72,32 @@ public VegetationCode findByCode(@PathVariable("code") String code) {
* @param search the string to match the vegetation codes with
* @param paginationParameters parameters for the pagination of the search results; see {@link
* PaginationParameters}
* @return a list of {@code size} or less vegetation codes, ordered by identifier
* @return a list of {@code perPage} or less vegetation codes matching {@code search}, ordered by
* identifier
*/
@GetMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
@Operation(
summary = "Search for valid vegetation codes by their identifier or description",
description =
"Search for valid vegetation codes (ones which `effectiveDate` ≤ today < `expiryDate`) "
+ "with identifier or description matching `search`.",
responses = {
@ApiResponse(
responseCode = "200",
description =
"An array with the vegetation codes found, ordered by their identifiers."),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true)))
})
@PaginatedViaQuery
public List<VegetationCode> findEffectiveByCodeOrDescription(
@RequestParam(name = "search", defaultValue = "") String search,
@RequestParam(name = "search", defaultValue = "")
@Parameter(
description =
"A string to be matched against the codes' identifier or description. Not "
+ "providing a value matches everything.")
String search,
@Valid PaginationParameters paginationParameters) {
return vegetationCodeRepository.findValidByCodeOrDescription(
search, paginationParameters.skip(), paginationParameters.size());
search, paginationParameters.skip(), paginationParameters.perPage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ca.bc.gov.backendstartapi.endpoint.parameters;

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Adds the following pagination parameters to the Swagger documentation of a method:
*
* <ul>
* <li>page: Zero-based page index indicating the page to be returned; defaults to 0
* <li>perPage: The maximum number of results in a page; defaults to 20
* </ul>
*
* <p>The code for such parameters, along with their behaviour, must be implemented and documented
* by the developer. See {@link PaginationParameters} and its uses, for instance.
*
* <p>Concept taken from {@link org.springdoc.core.converters.models.PageableAsQueryParam}.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Parameter(
in = ParameterIn.QUERY,
description = "Zero-based page index indicating the page to be returned.",
name = "page",
schema = @Schema(type = "integer", defaultValue = "0", minimum = "0"))
@Parameter(
in = ParameterIn.QUERY,
description = "The maximum number of results in a page.",
name = "perPage",
schema = @Schema(type = "integer", defaultValue = "20", minimum = "1"))
public @interface PaginatedViaQuery {}
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
package ca.bc.gov.backendstartapi.endpoint.parameters;

import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;

/**
* Pagination parameters to be used in the processing of HTTP GET requests.
*
* <p>Each page contains up to {@code size} results, excluding the first {@code page * size}
* results. For instance, <code>{page = 2, size = 10}</code> will exclude the first 20 results,
* returning results 21 up to 30.
*
* @param page The page to be returned. Zero-based, and must be non-negative; defaults to 0
* @param size The maximum number of results to be returned. Defaults to 20
* @param perPage The maximum number of results in each page. Defaults to 20
*/
public record PaginationParameters(@PositiveOrZero Integer page, @Positive Integer size) {
@Hidden
public record PaginationParameters(@PositiveOrZero Integer page, @Positive Integer perPage) {

/**
* Build an instance of {@link PaginationParameters}, using the default values for {@code page}
* and {@code size} if they're null.
* and {@code perPage} if they're null.
*/
public PaginationParameters {
if (page == null) {
page = 0;
}
if (size == null) {
size = 20;
if (perPage == null) {
perPage = 20;
}
}

/** Get the number of results to be skipped by the search. */
/** Get the number of results to be skipped by the search. Useful for SQL queries. */
public int skip() {
return page * size;
return page * perPage;
}
}
Loading

0 comments on commit dd4ad6a

Please sign in to comment.