diff options
| author | Martial Simon <msimon_fr@hotmail.com> | 2025-09-15 01:07:58 +0200 |
|---|---|---|
| committer | Martial Simon <msimon_fr@hotmail.com> | 2025-09-15 01:07:58 +0200 |
| commit | 967be9e750221ab2ab783f95df79bb26d290a45e (patch) | |
| tree | 6802900a5e975f9f68b169f0f503f040056d6952 /jws/epibazaar/shop/src/main | |
Diffstat (limited to 'jws/epibazaar/shop/src/main')
15 files changed, 628 insertions, 0 deletions
diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/converter/ShopConverter.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/converter/ShopConverter.java new file mode 100644 index 0000000..1b3e8cb --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/converter/ShopConverter.java @@ -0,0 +1,24 @@ +package fr.epita.assistants.shop.converter; + +import fr.epita.assistants.common.aggregate.ItemAggregate; +import fr.epita.assistants.common.aggregate.ResetInventoryAggregate; +import fr.epita.assistants.shop.data.model.ItemModel; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.HashMap; +import java.util.Map; + +@ApplicationScoped +public class ShopConverter { + public static Map<ItemAggregate.ResourceType, Float> DeleteItems(ResetInventoryAggregate items) { + Map<ItemAggregate.ResourceType, Float> map = new HashMap<>(); + for (ItemAggregate i : items.getItems()) { + map.merge(i.getType(), i.getQuantity(), Float::sum); + } + return map; + } + + public static ItemAggregate updatedItem(ItemModel item) { + return new ItemAggregate(item.getType(), item.getQuantity()); + } +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/model/ItemModel.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/model/ItemModel.java new file mode 100644 index 0000000..a16f7bc --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/model/ItemModel.java @@ -0,0 +1,28 @@ +package fr.epita.assistants.shop.data.model; + +import fr.epita.assistants.common.aggregate.ItemAggregate; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Setter +@Table(name = "item") +public class ItemModel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + @Enumerated(value = EnumType.STRING) + ItemAggregate.ResourceType type; + Float quantity; + + public ItemModel(float quantity, ItemAggregate.ResourceType type) { + this.quantity = quantity; + this.type = type; + } +}
\ No newline at end of file diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/model/ShopModel.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/model/ShopModel.java new file mode 100644 index 0000000..5f1c3ff --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/model/ShopModel.java @@ -0,0 +1,18 @@ +package fr.epita.assistants.shop.data.model; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "shop") +public class ShopModel { + Float priceMultiplier; + Float upgradePrice; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + Integer id; +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/repository/ItemRepository.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/repository/ItemRepository.java new file mode 100644 index 0000000..c471eb4 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/repository/ItemRepository.java @@ -0,0 +1,39 @@ +package fr.epita.assistants.shop.data.repository; + +import fr.epita.assistants.common.aggregate.ItemAggregate; +import fr.epita.assistants.shop.data.model.ItemModel; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +import java.util.Map; +import java.util.Optional; + +@ApplicationScoped +public class ItemRepository implements PanacheRepository<ItemModel> { + @Transactional + public void deleteItems(Map<ItemAggregate.ResourceType, Float> toDelete) { + for (ItemAggregate.ResourceType type : toDelete.keySet()) { + ItemModel item = find("type = ?1", type).firstResult(); + if (item != null) + update("quantity = ?1 where type = ?2", item.getQuantity() - toDelete.get(type), type); + } + } + + @Transactional + public Optional<ItemModel> exists(ItemAggregate.ResourceType type) { + return find("type = ?1", type).firstResultOptional(); + } + + @Transactional + public void addItem(ItemAggregate.ResourceType type, Float amount) { + ItemModel res = new ItemModel(amount, type); + persist(res); + } + + // amount should already be the updated amount + @Transactional + public void fillInventory(ItemAggregate.ResourceType type, Float amount) { + update("quantity = ?1 where type = ?2", amount, type); + } +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/repository/ShopRepository.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/repository/ShopRepository.java new file mode 100644 index 0000000..1bc7c78 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/data/repository/ShopRepository.java @@ -0,0 +1,30 @@ +package fr.epita.assistants.shop.data.repository; + +import fr.epita.assistants.common.api.request.StartRequest; +import fr.epita.assistants.common.api.response.StartResponse; +import fr.epita.assistants.common.utils.ErrorInfo; +import fr.epita.assistants.shop.converter.ShopConverter; +import fr.epita.assistants.shop.data.model.ShopModel; +import fr.epita.assistants.shop.domain.service.ShopService; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +@ApplicationScoped +public class ShopRepository implements PanacheRepository<ShopModel> { + @Transactional + public void clearDB() { + deleteAll(); + } + @Transactional + public void addEntry(Float priceMultiplier, Float upgradePriceCost) { + ShopModel shop = new ShopModel(); + shop.setUpgradePrice(upgradePriceCost); + shop.setPriceMultiplier(priceMultiplier); + persist(shop); + } +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/entity/ErwenEntity.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/entity/ErwenEntity.java new file mode 100644 index 0000000..3bb22f1 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/entity/ErwenEntity.java @@ -0,0 +1,4 @@ +package fr.epita.assistants.shop.domain.entity; + +public class ErwenEntity { +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/entity/StartEntity.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/entity/StartEntity.java new file mode 100644 index 0000000..d26ac90 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/entity/StartEntity.java @@ -0,0 +1,9 @@ +package fr.epita.assistants.shop.domain.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class StartEntity { +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/service/ErwenService.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/service/ErwenService.java new file mode 100644 index 0000000..b7e8ad7 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/service/ErwenService.java @@ -0,0 +1,4 @@ +package fr.epita.assistants.shop.domain.service; + +public class ErwenService { +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/service/ShopService.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/service/ShopService.java new file mode 100644 index 0000000..06ed11c --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/domain/service/ShopService.java @@ -0,0 +1,79 @@ +package fr.epita.assistants.shop.domain.service; + +import fr.epita.assistants.common.aggregate.ItemAggregate; +import fr.epita.assistants.common.aggregate.ResetInventoryAggregate; +import fr.epita.assistants.common.aggregate.SyncInventoryAggregate; +import fr.epita.assistants.common.command.CollectItemCommand; +import fr.epita.assistants.common.command.ResetInventoryCommand; +import fr.epita.assistants.common.command.SyncInventoryCommand; +import fr.epita.assistants.shop.converter.ShopConverter; +import fr.epita.assistants.shop.data.model.ItemModel; +import fr.epita.assistants.shop.data.repository.ItemRepository; +import fr.epita.assistants.shop.data.repository.ShopRepository; +import io.smallrye.reactive.messaging.annotations.Broadcast; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@ApplicationScoped +public class ShopService { + @ConfigProperty(name = "JWS_SHOP_PRICE") + String shopPrice; + @ConfigProperty(name = "JWS_UPGRADE_PRICE_COST") + String upgradePriceCost; + @Inject + ItemRepository itemRepository; + @Inject + ShopRepository shopRepository; + + @Inject + @Channel("sync-inventory-command") + @Broadcast + Emitter<SyncInventoryCommand> syncInventoryCommandEmitter; + + public void clearItems(ResetInventoryAggregate items) { + itemRepository.deleteItems(ShopConverter.DeleteItems(items)); + } + + public void collectItem(ItemAggregate agr) { + Optional<ItemModel> item = itemRepository.exists(agr.getType()); + if (item.isPresent()) { + itemRepository.fillInventory(agr.getType(), agr.getQuantity()); + } else { + itemRepository.addItem(agr.getType(), agr.getQuantity()); + } + } + + public void startShop() { + shopRepository.clearDB(); + shopRepository.addEntry(1f, Float.parseFloat(upgradePriceCost)); + syncInventoryCommandEmitter.send(new SyncInventoryCommand()); + } + + @Transactional + public void updateItems(SyncInventoryAggregate agr) { + for (ItemAggregate item : agr.getItems()) { + Optional<ItemModel> it = itemRepository.exists(item.getType()); + if (it.isPresent()) { + itemRepository.fillInventory(item.getType(), item.getQuantity() + it.get().getQuantity()); + } else { + itemRepository.addItem(item.getType(), item.getQuantity()); + } + } + } + + public void pay(Float newMoney) { + itemRepository.fillInventory(ItemAggregate.ResourceType.MONEY, newMoney); + } +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/rest/HelloWorldResource.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/rest/HelloWorldResource.java new file mode 100644 index 0000000..e384c22 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/rest/HelloWorldResource.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.shop.presentation.rest; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +@Produces(MediaType.TEXT_PLAIN) +@Consumes(MediaType.TEXT_PLAIN) +public class HelloWorldResource { + @GET @Path("/") + public String helloWorld() { + return "Hello World!"; + } + + @GET @Path("/{name}") + public String helloWorld(@PathParam("name") String name) { + return "Hello " + name + "!"; + } +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/rest/ShopResource.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/rest/ShopResource.java new file mode 100644 index 0000000..71719d1 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/rest/ShopResource.java @@ -0,0 +1,63 @@ +package fr.epita.assistants.shop.presentation.rest; + +import fr.epita.assistants.common.api.request.ItemsRequest; +import fr.epita.assistants.common.api.request.StartRequest; +import fr.epita.assistants.common.utils.ErrorInfo; +import fr.epita.assistants.shop.domain.service.ShopService; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ShopResource { + private final ShopService shopService; + + @jakarta.inject.Inject + public ShopResource(ShopService shopService) { + this.shopService = shopService; + } + + @Path("/") + @GET + public Response getRoot() { + return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorInfo("Not implemented")).build(); + } + @Path("/") + @POST + public Response postRoot() { + return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorInfo("Not implemented")).build(); + } + @Path("resources") + @GET + public Response resources() { + return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorInfo("Not implemented")).build(); + } + @Path("start") + @POST + public Response start() { + shopService.startShop(); + return Response.status(Response.Status.NO_CONTENT).build(); + } + @Path("price") + @GET + public Response price() { + return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorInfo("Not implemented")).build(); + } + @Path("{id}") + @POST + public Response getShop(@PathParam("id") int id) { + return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorInfo("Not implemented")).build(); + } + @Path("sell/{id}") + @PATCH + public Response sellItems(@PathParam("id") int id, ItemsRequest itemsRequest) { + return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorInfo("Not implemented")).build(); + } + @Path("upgrade/price/{id}") + @PATCH + public Response upgradeShop(@PathParam("id") int id) { + return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorInfo("Not implemented")).build(); + } +} diff --git a/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/subscriber/AggregateSubscriber.java b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/subscriber/AggregateSubscriber.java new file mode 100644 index 0000000..26247b9 --- /dev/null +++ b/jws/epibazaar/shop/src/main/java/fr/epita/assistants/shop/presentation/subscriber/AggregateSubscriber.java @@ -0,0 +1,41 @@ +package fr.epita.assistants.shop.presentation.subscriber; + +import fr.epita.assistants.common.aggregate.ItemAggregate; +import fr.epita.assistants.common.aggregate.ResetInventoryAggregate; +import fr.epita.assistants.common.aggregate.SyncInventoryAggregate; +import fr.epita.assistants.common.aggregate.UpgradeItemProducerAggregate; +import fr.epita.assistants.common.command.CollectItemCommand; +import fr.epita.assistants.shop.domain.service.ShopService; +import io.smallrye.reactive.messaging.annotations.Broadcast; +import jakarta.inject.Inject; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +public class AggregateSubscriber { + @Inject + ShopService shopService; + + @Broadcast + @Incoming("sync-inventory-aggregate") + public void commandListener(SyncInventoryAggregate aggregate) { + shopService.updateItems(aggregate); + } + + @Broadcast + @Incoming("upgrade-collect-rate-aggregate") + public void collectCommandListener(UpgradeItemProducerAggregate agr) { + shopService.pay(agr.getNewMoney()); + } + + @Broadcast + @Incoming("upgrade-movement-speed-aggregate") + public void moveCommandListener(UpgradeItemProducerAggregate agr) { + shopService.pay(agr.getNewMoney()); + } + + @Broadcast + @Incoming("upgrade-stamina-aggregate") + public void staminaCommandListener(UpgradeItemProducerAggregate agr) { + shopService.pay(agr.getNewMoney()); + } +} diff --git a/jws/epibazaar/shop/src/main/resources/application.properties b/jws/epibazaar/shop/src/main/resources/application.properties new file mode 100644 index 0000000..e2fa130 --- /dev/null +++ b/jws/epibazaar/shop/src/main/resources/application.properties @@ -0,0 +1,13 @@ +%dev.quarkus.http.port=8082 +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=postgres +quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/shop?currentSchema=public +quarkus.transaction-manager.default-transaction-timeout=3000s +quarkus.hibernate-orm.log.queries-slower-than-ms=200 +quarkus.http.cors=true +quarkus.http.cors.origins=* +quarkus.hibernate-orm.database.generation=drop-and-create + +quarkus.kafka.devservices.image-name=reg.undercloud.cri.epita.fr/docker/redpandadata/redpanda:v24.1.2 +quarkus.devservices.enabled=true +%test.quarkus.devservices.enabled=false diff --git a/jws/epibazaar/shop/src/main/resources/db/migration/V1__Init.sql b/jws/epibazaar/shop/src/main/resources/db/migration/V1__Init.sql new file mode 100644 index 0000000..0161cb5 --- /dev/null +++ b/jws/epibazaar/shop/src/main/resources/db/migration/V1__Init.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "shop" +( + id SERIAL PRIMARY KEY NOT NULL, + + price_multiplier FLOAT NOT NULL, + upgrade_price FLOAT NOT NULL +); + +CREATE TABLE IF NOT EXISTS "item" +( + id SERIAL PRIMARY KEY NOT NULL, + + type VARCHAR(64) NOT NULL, + quantity FLOAT NOT NULL +); diff --git a/jws/epibazaar/shop/src/main/resources/openapi.yaml b/jws/epibazaar/shop/src/main/resources/openapi.yaml new file mode 100644 index 0000000..7b6928f --- /dev/null +++ b/jws/epibazaar/shop/src/main/resources/openapi.yaml @@ -0,0 +1,242 @@ +--- +openapi: 3.1.0 +tags: +- name: Game Management +- name: Inventory Management +- name: Shop Management +- name: Shop Operations +- name: Shop Upgrades +components: + schemas: + ItemRequest: + type: object + properties: + quantity: + type: number + format: float + type: + $ref: "#/components/schemas/ResourceType" + ItemResponse: + type: object + properties: + id: + type: integer + format: int32 + quantity: + type: number + format: float + type: + $ref: "#/components/schemas/ResourceType" + ItemsRequest: + type: object + properties: + itemsRequest: + type: array + items: + $ref: "#/components/schemas/ItemRequest" + ItemsResponse: + type: object + properties: + itemsResponse: + type: array + items: + $ref: "#/components/schemas/ItemResponse" + ResourceType: + type: string + enum: + - MONEY + - GROUND + - WATER + - ROCK + - WOOD + ShopPriceResponse: + type: object + properties: + shopPrice: + type: number + format: float + ShopResponse: + type: object + properties: + id: + type: integer + format: int32 + priceMultiplier: + type: number + format: float + upgradePrice: + type: number + format: float + ShopsResponse: + type: object + properties: + shops: + type: array + items: + $ref: "#/components/schemas/ShopResponse" +paths: + /: + get: + summary: Get all shops. + description: Retrieves all available shops. + x-quarkus-openapi-method-ref: m869844210_-222983088 + tags: + - Shop Management + responses: + "200": + description: The shops were successfully retrieved. + content: + application/json: + schema: + $ref: "#/components/schemas/ShopsResponse" + "400": + description: The game is not running. + servers: + - url: http://localhost:8082/ + post: + summary: Create a new shop. + description: Creates a new shop. + x-quarkus-openapi-method-ref: m869844210_1764851887 + tags: + - Shop Management + responses: + "204": + description: The shop creation request was successfully sent. + "400": + description: "The game has reached its max capacity, you do not have enough\ + \ money, or the game is not running." + "404": + description: No money item found. + servers: + - url: http://localhost:8082/ + /price: + get: + summary: Get the current shop price. + description: Retrieves the current shop price. + x-quarkus-openapi-method-ref: m869844210_-271831484 + tags: + - Shop Management + responses: + "200": + description: The shop price was successfully retrieved. + content: + application/json: + schema: + $ref: "#/components/schemas/ShopPriceResponse" + "400": + description: The game is not running. + servers: + - url: http://localhost:8082/ + /resources: + get: + summary: Retrieve available resources in inventory. + description: Fetch all resources currently available in the inventory. + x-quarkus-openapi-method-ref: m911209932_-1184004262 + tags: + - Inventory Management + responses: + "200": + description: The resources were successfully retrieved. + content: + application/json: + schema: + $ref: "#/components/schemas/ItemsResponse" + "400": + description: The game is not running or the request is invalid. + servers: + - url: http://localhost:8082/ + /sell/{id}: + patch: + summary: Sell items to a client. + description: Processes the sale of items to a client. + x-quarkus-openapi-method-ref: m869844210_-1356306674 + tags: + - Shop Operations + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ItemsRequest" + required: true + responses: + "204": + description: The sell request was successfully sent. + "400": + description: "You do not have enough resources, an item is not sellable,\ + \ an amount is invalid, or the game is not running." + "404": + description: The shop or an item was not found. + servers: + - url: http://localhost:8082/ + /start: + post: + summary: Start the game. + description: Starts the game by sending a Kafka command. + x-quarkus-openapi-method-ref: m869844210_1873672464 + tags: + - Game Management + responses: + "204": + description: The game started successfully. + servers: + - url: http://localhost:8082/ + /upgrade/price/{id}: + patch: + summary: Upgrade the resource price for a specific shop. + description: Increases the resource price for a specific shop. + x-quarkus-openapi-method-ref: m869844210_-999917025 + tags: + - Shop Upgrades + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "204": + description: The price upgrade request was successfully sent. + "400": + description: You do not have enough money or the game is not running. + "404": + description: The shop was not found or the money was not found. + servers: + - url: http://localhost:8082/ + /{id}: + get: + summary: Get a specific shop. + description: Retrieves details of a specific shop. + x-quarkus-openapi-method-ref: m869844210_868322035 + tags: + - Shop Management + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: The shop was successfully retrieved. + content: + application/json: + schema: + $ref: "#/components/schemas/ShopResponse" + "400": + description: The game is not running or the ID is not valid. + "404": + description: The shop was not found. + servers: + - url: http://localhost:8082/ +info: + title: shop API + version: 1.0.0 |
