Skip to content

Commit cc66e1a

Browse files
committed
Task 25 : Implement the process for listing crypto currencies with pagination with revising Open Api and writing JUnit and Integration tests
1 parent 04699b9 commit cc66e1a

File tree

11 files changed

+667
-16
lines changed

11 files changed

+667
-16
lines changed

src/main/java/com/casestudy/cryptoexchangeapi/exchange/controller/CryptoConvertController.java

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,46 @@
11
package com.casestudy.cryptoexchangeapi.exchange.controller;
22

33
import com.casestudy.cryptoexchangeapi.common.model.CustomPage;
4+
import com.casestudy.cryptoexchangeapi.common.model.CustomPaging;
5+
import com.casestudy.cryptoexchangeapi.common.model.dto.request.CustomPagingRequest;
46
import com.casestudy.cryptoexchangeapi.common.model.dto.response.CustomPagingResponse;
57
import com.casestudy.cryptoexchangeapi.common.model.dto.response.CustomResponse;
68
import com.casestudy.cryptoexchangeapi.exchange.model.CryptoConvert;
79
import com.casestudy.cryptoexchangeapi.exchange.model.dto.request.ConvertRequest;
810
import com.casestudy.cryptoexchangeapi.exchange.model.dto.request.FilterServicePagingRequest;
911
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoConvertResponse;
12+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoNameSymbol;
13+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoNameSymbolResponse;
14+
import com.casestudy.cryptoexchangeapi.exchange.model.mapper.CryptoNameSymbolToCryptoNameSymbolResponseMapper;
1015
import com.casestudy.cryptoexchangeapi.exchange.model.mapper.CustomPageCryptoConvertToCustomPagingCryptoConvertResponseMapper;
1116
import com.casestudy.cryptoexchangeapi.exchange.service.CryptoConvertService;
1217
import io.swagger.v3.oas.annotations.Operation;
18+
import io.swagger.v3.oas.annotations.Parameter;
19+
import io.swagger.v3.oas.annotations.enums.ParameterIn;
1320
import io.swagger.v3.oas.annotations.media.Content;
1421
import io.swagger.v3.oas.annotations.media.ExampleObject;
1522
import io.swagger.v3.oas.annotations.media.Schema;
1623
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1724
import io.swagger.v3.oas.annotations.tags.Tag;
1825
import jakarta.annotation.PostConstruct;
1926
import jakarta.validation.Valid;
27+
import jakarta.validation.constraints.Max;
28+
import jakarta.validation.constraints.Min;
2029
import lombok.RequiredArgsConstructor;
2130
import lombok.extern.slf4j.Slf4j;
2231
import org.springframework.cache.annotation.CacheEvict;
2332
import org.springframework.http.HttpStatus;
2433
import org.springframework.scheduling.annotation.Scheduled;
2534
import org.springframework.web.bind.annotation.*;
2635

36+
import java.util.List;
37+
2738
@RestController
2839
@RequestMapping("/api/convert")
2940
@RequiredArgsConstructor
3041
@Slf4j
3142
@Tag(
32-
name = "Crypto Convert API",
43+
name = "01 - Crypto Convert API",
3344
description = "Convert between cryptocurrencies and query persisted conversion history. "
3445
+ "POST /api/convert returns 201 with the saved conversion; "
3546
+ "POST /api/convert/history supports filtering (from/to, amount & convertedAmount ranges, "
@@ -200,6 +211,90 @@ public CustomResponse<CustomPagingResponse<CryptoConvertResponse>> getHistory(
200211

201212
}
202213

214+
@Operation(
215+
operationId = "cryptoMap",
216+
summary = "List cryptocurrencies (name + symbol) with pagination",
217+
description = "Returns a paged slice of cryptocurrencies from CoinMarketCap’s /v1/cryptocurrency/map.",
218+
parameters = {
219+
@Parameter(
220+
name = "page",
221+
description = "1-based page number",
222+
in = ParameterIn.QUERY,
223+
schema = @Schema(type = "integer", defaultValue = "1", minimum = "1")
224+
),
225+
@Parameter(
226+
name = "size",
227+
description = "page size",
228+
in = ParameterIn.QUERY,
229+
schema = @Schema(type = "integer", defaultValue = "20", minimum = "1", maximum = "5000")
230+
)
231+
},
232+
responses = {
233+
@ApiResponse(
234+
responseCode = "200",
235+
description = "Paged list of (name, symbol)",
236+
content = @Content(
237+
schema = @Schema(implementation = CustomPagingResponse.class),
238+
examples = @ExampleObject(
239+
name = "OK",
240+
value = """
241+
{
242+
"time": "2025-10-01T19:27:24.2492919",
243+
"httpStatus": "OK",
244+
"isSuccess": true,
245+
"response": {
246+
"content": [
247+
{ "name": "Bitcoin", "symbol": "BTC" },
248+
{ "name": "Ethereum", "symbol": "ETH" }
249+
],
250+
"pageNumber": 1,
251+
"pageSize": 20,
252+
"totalElementCount": 2,
253+
"totalPageCount": 2
254+
}
255+
}
256+
"""
257+
)
258+
)
259+
),
260+
@ApiResponse(
261+
responseCode = "502",
262+
description = "CMC map call failed",
263+
content = @Content(mediaType = "application/json")
264+
)
265+
}
266+
)
267+
@GetMapping("/map")
268+
public CustomResponse<CustomPagingResponse<CryptoNameSymbolResponse>> cryptoMap(
269+
@RequestParam(defaultValue = "1") @Min(1) int page,
270+
@RequestParam(defaultValue = "20") @Min(1) @Max(5000) int size) {
271+
272+
CustomPagingRequest pagingRequest = CustomPagingRequest.builder()
273+
.pagination(CustomPaging.builder()
274+
.pageNumber(page)
275+
.pageSize(size)
276+
.build())
277+
.build();
278+
279+
CustomPage<CryptoNameSymbol> pageResult = service.listCryptoNamesSymbols(pagingRequest);
280+
281+
// Map domain -> response DTOs
282+
List<CryptoNameSymbolResponse> rows = pageResult.getContent().stream()
283+
.map(CryptoNameSymbolToCryptoNameSymbolResponseMapper.initialize()::map)
284+
.toList();
285+
286+
CustomPagingResponse<CryptoNameSymbolResponse> payload = CustomPagingResponse.<CryptoNameSymbolResponse>builder()
287+
.content(rows)
288+
.pageNumber(pageResult.getPageNumber())
289+
.pageSize(pageResult.getPageSize())
290+
.totalElementCount(pageResult.getTotalElementCount())
291+
.totalPageCount(pageResult.getTotalPageCount())
292+
.build();
293+
294+
return CustomResponse.successOf(payload);
295+
296+
}
297+
203298
@CacheEvict(allEntries = true, cacheNames = {"exchanges"})
204299
@PostConstruct
205300
@Scheduled(fixedRateString = "${cmc.cache-ttl}")

src/main/java/com/casestudy/cryptoexchangeapi/exchange/feign/CmcClient.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ PriceConversionResponse priceConversion(
2323
@RequestParam(value = "convert_id", required = false) String toId);
2424

2525
@GetMapping("/v1/cryptocurrency/map")
26-
CryptoMapResponse cryptoMap(@RequestParam(value = "listing_status", defaultValue = "active") String status,
27-
@RequestParam(value = "start", defaultValue = "1") int start,
28-
@RequestParam(value = "limit", defaultValue = "5000") int limit);
26+
CryptoMapResponse cryptoMap(
27+
@RequestParam("start") Integer start,
28+
@RequestParam("limit") Integer limit,
29+
@RequestParam(value = "sort", required = false, defaultValue = "cmc_rank") String sort
30+
);
2931

3032
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.casestudy.cryptoexchangeapi.exchange.model.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
@Builder
10+
public class CryptoNameSymbol {
11+
12+
private String name;
13+
private String symbol;
14+
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.casestudy.cryptoexchangeapi.exchange.model.dto.response;
2+
3+
import lombok.*;
4+
5+
@Getter
6+
@Setter
7+
@NoArgsConstructor
8+
@AllArgsConstructor
9+
@Builder
10+
public class CryptoNameSymbolResponse {
11+
private String name;
12+
private String symbol;
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.casestudy.cryptoexchangeapi.exchange.model.mapper;
2+
3+
import com.casestudy.cryptoexchangeapi.common.model.mapper.BaseMapper;
4+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoMapResponse;
5+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoNameSymbol;
6+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoNameSymbolResponse;
7+
import org.mapstruct.Mapper;
8+
import org.mapstruct.Mapping;
9+
import org.mapstruct.factory.Mappers;
10+
11+
@Mapper
12+
public interface CryptoMapResponseToCryptoNameSymbolMapper extends BaseMapper<CryptoNameSymbolResponse, CryptoNameSymbol> {
13+
14+
@Mapping(target = "name", source = "name")
15+
@Mapping(target = "symbol", source = "symbol")
16+
CryptoNameSymbol map(CryptoMapResponse.Item source);
17+
18+
static CryptoMapResponseToCryptoNameSymbolMapper initialize() {
19+
return Mappers.getMapper(CryptoMapResponseToCryptoNameSymbolMapper.class);
20+
}
21+
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.casestudy.cryptoexchangeapi.exchange.model.mapper;
2+
3+
import com.casestudy.cryptoexchangeapi.common.model.mapper.BaseMapper;
4+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoNameSymbol;
5+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoNameSymbolResponse;
6+
import org.mapstruct.Mapper;
7+
import org.mapstruct.Mapping;
8+
import org.mapstruct.factory.Mappers;
9+
10+
@Mapper
11+
public interface CryptoNameSymbolToCryptoNameSymbolResponseMapper extends BaseMapper<CryptoNameSymbolResponse, CryptoNameSymbol> {
12+
13+
@Mapping(target = "name", source = "name")
14+
@Mapping(target = "symbol", source = "symbol")
15+
CryptoNameSymbolResponse map(CryptoNameSymbol source);
16+
17+
static CryptoNameSymbolToCryptoNameSymbolResponseMapper initialize() {
18+
return Mappers.getMapper(CryptoNameSymbolToCryptoNameSymbolResponseMapper.class);
19+
}
20+
21+
}

src/main/java/com/casestudy/cryptoexchangeapi/exchange/service/CryptoConvertService.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
import com.casestudy.cryptoexchangeapi.exchange.model.CryptoConvert;
88
import com.casestudy.cryptoexchangeapi.exchange.model.dto.request.ConvertRequest;
99
import com.casestudy.cryptoexchangeapi.exchange.model.dto.request.ListCryptoConvertRequest;
10+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoMapResponse;
11+
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.CryptoNameSymbol;
1012
import com.casestudy.cryptoexchangeapi.exchange.model.dto.response.PriceConversionResponse;
1113
import com.casestudy.cryptoexchangeapi.exchange.model.entity.CryptoConvertEntity;
1214
import com.casestudy.cryptoexchangeapi.exchange.model.mapper.CryptoConvertEntityToCryptoConvertMapper;
15+
import com.casestudy.cryptoexchangeapi.exchange.model.mapper.CryptoMapResponseToCryptoNameSymbolMapper;
1316
import com.casestudy.cryptoexchangeapi.exchange.repository.CryptoConvertRepository;
1417
import com.casestudy.cryptoexchangeapi.exchange.utils.Constants;
1518
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@@ -40,9 +43,12 @@ public class CryptoConvertService {
4043
private final CmcClient cmcClient;
4144
private final CryptoConvertRepository cryptoConvertRepository;
4245

43-
private final CryptoConvertEntityToCryptoConvertMapper mapper =
46+
private final CryptoConvertEntityToCryptoConvertMapper cryptoConvertEntityToCryptoConvertMapper =
4447
CryptoConvertEntityToCryptoConvertMapper.initialize();
4548

49+
private final CryptoMapResponseToCryptoNameSymbolMapper cryptoMapResponseToCryptoNameSymbolMapper =
50+
CryptoMapResponseToCryptoNameSymbolMapper.initialize();
51+
4652
@RateLimiter(name = "cmc")
4753
@Retry(name = "cmc")
4854
@CircuitBreaker(name = "cmc", fallbackMethod = "fallbackConvertAndPersist")
@@ -92,7 +98,7 @@ public CryptoConvert convertAndPersist(ConvertRequest request) {
9298

9399
CryptoConvertEntity saved = cryptoConvertRepository.save(entity);
94100

95-
return mapper.map(saved);
101+
return cryptoConvertEntityToCryptoConvertMapper.map(saved);
96102

97103
}
98104

@@ -113,13 +119,47 @@ public CustomPage<CryptoConvert> getHistory(ListCryptoConvertRequest request,
113119
Page<CryptoConvertEntity> page = cryptoConvertRepository.searchWithCriteria(filter, pageable);
114120

115121
List<CryptoConvert> items = page.getContent().stream()
116-
.map(mapper::map)
122+
.map(cryptoConvertEntityToCryptoConvertMapper::map)
117123
.toList();
118124

119125
return CustomPage.of(items, page);
120126

121127
}
122128

129+
@RateLimiter(name = "cmc")
130+
@Transactional(readOnly = true)
131+
public CustomPage<CryptoNameSymbol> listCryptoNamesSymbols(CustomPagingRequest pagingRequest) {
132+
133+
final Pageable pageable = Optional.ofNullable(pagingRequest)
134+
.map(CustomPagingRequest::toPageable)
135+
.orElse(PageRequest.of(0, 20));
136+
137+
final int page = pageable.getPageNumber(); // zero-based
138+
final int size = pageable.getPageSize();
139+
140+
final int start = page * size + 1;
141+
final String sort = "cmc_rank";
142+
143+
final CryptoMapResponse response = cmcClient.cryptoMap(start, size, sort);
144+
145+
List<CryptoNameSymbol> content = (response != null && response.getData() != null)
146+
? response.getData().stream()
147+
.map(cryptoMapResponseToCryptoNameSymbolMapper::map)
148+
.toList()
149+
: List.of();
150+
151+
int totalPageCount = content.size() < size ? (page + 1) : (page + 2);
152+
153+
return CustomPage.<CryptoNameSymbol>builder()
154+
.content(content)
155+
.pageNumber(page + 1)
156+
.pageSize(size)
157+
.totalElementCount((long) content.size())
158+
.totalPageCount(totalPageCount)
159+
.build();
160+
161+
}
162+
123163
public CryptoConvert fallbackConvertAndPersist(ConvertRequest request, Throwable cause) {
124164
throw new ConversionFailedException("Upstream conversion unavailable", cause);
125165
}

src/main/resources/application.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ springdoc:
1818
api-docs:
1919
enabled: true
2020
show-actuator: true
21+
swagger-ui:
22+
tags-sorter: alpha
23+
operations-sorter: alpha
2124

2225
# CoinMarketCap API props
2326
cmc:

0 commit comments

Comments
 (0)