diff options
| author | Kostya <mail@sartin.in> | 2026-08-18 18:19:50 +0300 |
|---|---|---|
| committer | Kostya <mail@sartin.in> | 2026-08-18 18:19:50 +0300 |
| commit | 755d2265054e5140e82d322681566c3034583c95 (patch) | |
| tree | 0220f9d04affa03fabf3c2bc68c51c01ee1475d9 /service/src/main/java | |
| parent | 9dab2b3d624ecae08439532157fb8b9805fe9274 (diff) | |
| download | guess-market-755d2265054e5140e82d322681566c3034583c95.tar.gz guess-market-755d2265054e5140e82d322681566c3034583c95.tar.xz guess-market-755d2265054e5140e82d322681566c3034583c95.zip | |
Break down purchase receipts by cost/commission, harden XML validation and state I/O
Adds a full pre-load validation pass for GM XML seeds (ids, names,
options, commission, LMSR liquidity), replaces IO.print with
System.out, and surfaces detailed error messages across load/save
commands instead of generic failures.
Diffstat (limited to 'service/src/main/java')
5 files changed, 142 insertions, 27 deletions
diff --git a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java index 909a808..f04324b 100644 --- a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java +++ b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java | |||
| @@ -10,6 +10,7 @@ import market.guess.model.event.EventDetailDTO; | |||
| 10 | import market.guess.service.catalog.infrastructure.MarketContext; | 10 | import market.guess.service.catalog.infrastructure.MarketContext; |
| 11 | import market.guess.service.domain.Order; | 11 | import market.guess.service.domain.Order; |
| 12 | import market.guess.service.fulfillment.FulfillmentContext; | 12 | import market.guess.service.fulfillment.FulfillmentContext; |
| 13 | import market.guess.service.helpers.BigDecimalOptions; | ||
| 13 | import market.guess.service.helpers.InstantOptions; | 14 | import market.guess.service.helpers.InstantOptions; |
| 14 | import market.guess.service.matching.MatchingEngine; | 15 | import market.guess.service.matching.MatchingEngine; |
| 15 | import market.guess.service.risk.RiskEngine; | 16 | import market.guess.service.risk.RiskEngine; |
| @@ -55,6 +56,20 @@ public final class LocalGuessMarketContext implements GuessMarketContext { | |||
| 55 | 56 | ||
| 56 | var rawTrades = matching.match(order); | 57 | var rawTrades = matching.match(order); |
| 57 | var trades = fulfillment.fulfill(user, order, rawTrades); | 58 | var trades = fulfillment.fulfill(user, order, rawTrades); |
| 59 | |||
| 60 | var totalSharesCost = | ||
| 61 | trades.stream() | ||
| 62 | .map(t -> t.sharesCost()) | ||
| 63 | .reduce(BigDecimalOptions.ZERO_MONEY, BigDecimal::add); | ||
| 64 | var totalCommission = | ||
| 65 | trades.stream() | ||
| 66 | .map(t -> t.commission()) | ||
| 67 | .reduce(BigDecimalOptions.ZERO_MONEY, BigDecimal::add); | ||
| 68 | var totalPaid = | ||
| 69 | trades.stream() | ||
| 70 | .map(t -> t.totalPaid()) | ||
| 71 | .reduce(BigDecimalOptions.ZERO_MONEY, BigDecimal::add); | ||
| 72 | |||
| 58 | return Result.ok( | 73 | return Result.ok( |
| 59 | new PurchaseReceiptDTO( | 74 | new PurchaseReceiptDTO( |
| 60 | trades.stream() | 75 | trades.stream() |
| @@ -65,8 +80,11 @@ public final class LocalGuessMarketContext implements GuessMarketContext { | |||
| 65 | t.buyerUserName(), | 80 | t.buyerUserName(), |
| 66 | t.marketName(), | 81 | t.marketName(), |
| 67 | String.valueOf(t.quantity()), | 82 | String.valueOf(t.quantity()), |
| 68 | t.totalPaid().toPlainString())) | 83 | t.sharesCost().toPlainString())) |
| 69 | .toList(), | 84 | .toList(), |
| 85 | totalSharesCost.toPlainString(), | ||
| 86 | totalCommission.toPlainString(), | ||
| 87 | totalPaid.toPlainString(), | ||
| 70 | event.toMarketState())); | 88 | event.toMarketState())); |
| 71 | } | 89 | } |
| 72 | 90 | ||
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java b/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java index cb2b467..5fc2e24 100644 --- a/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java +++ b/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java | |||
| @@ -67,15 +67,22 @@ public final class MarketContext { | |||
| 67 | GSON.toJson(new Wrapper(events.getAll(), users.getAll())), | 67 | GSON.toJson(new Wrapper(events.getAll(), users.getAll())), |
| 68 | StandardCharsets.UTF_8); | 68 | StandardCharsets.UTF_8); |
| 69 | } catch (Exception e) { | 69 | } catch (Exception e) { |
| 70 | throw new GuessMarketException("Failed to sove the file."); | 70 | throw new GuessMarketException("Failed to save state to " + target + ": " + e.getMessage()); |
| 71 | } | 71 | } |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | public void load(Path path) throws GuessMarketException { | 74 | public void load(Path path) throws GuessMarketException { |
| 75 | var target = withJsonExtension(path); | ||
| 76 | if (!Files.isRegularFile(target)) { | ||
| 77 | throw new GuessMarketException("State file not found: " + target); | ||
| 78 | } | ||
| 75 | try { | 79 | try { |
| 76 | var wrapper = | 80 | var wrapper = |
| 77 | GSON.fromJson( | 81 | GSON.fromJson( |
| 78 | Files.readString(withJsonExtension(path), StandardCharsets.UTF_8), Wrapper.class); | 82 | Files.readString(target, StandardCharsets.UTF_8), Wrapper.class); |
| 83 | if (wrapper == null || wrapper.events() == null) { | ||
| 84 | throw new GuessMarketException("State file contains invalid or empty data."); | ||
| 85 | } | ||
| 79 | events.clear(); | 86 | events.clear(); |
| 80 | users.clear(); | 87 | users.clear(); |
| 81 | 88 | ||
| @@ -88,14 +95,15 @@ public final class MarketContext { | |||
| 88 | } | 95 | } |
| 89 | 96 | ||
| 90 | } catch (JsonSyntaxException e) { | 97 | } catch (JsonSyntaxException e) { |
| 91 | throw new GuessMarketException("The JSON file is malformed."); | 98 | throw new GuessMarketException("The state file is malformed JSON: " + e.getMessage()); |
| 92 | } catch (IOException e) { | 99 | } catch (IOException e) { |
| 93 | throw new GuessMarketException("Unable the file."); | 100 | throw new GuessMarketException("Unable to read state file: " + e.getMessage()); |
| 94 | } | 101 | } |
| 95 | } | 102 | } |
| 96 | 103 | ||
| 97 | private static Path withJsonExtension(Path path) { | 104 | private static Path withJsonExtension(Path path) { |
| 105 | if (path == null) return null; | ||
| 98 | var name = path.getFileName().toString(); | 106 | var name = path.getFileName().toString(); |
| 99 | return name.endsWith(".json") ? path : path.resolveSibling(name + ".json"); | 107 | return name.toLowerCase().endsWith(".json") ? path : path.resolveSibling(name + ".json"); |
| 100 | } | 108 | } |
| 101 | } | 109 | } |
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java index f45eb46..b9c43bb 100644 --- a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java +++ b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java | |||
| @@ -19,8 +19,8 @@ public final class EventMapperV1 implements Mapper<GMEvent, Event> { | |||
| 19 | return new Event( | 19 | return new Event( |
| 20 | getEventKey(source), | 20 | getEventKey(source), |
| 21 | source.getId(), | 21 | source.getId(), |
| 22 | joinName(source.getName()), | 22 | joinName(source.getName()).trim(), |
| 23 | source.getDescription(), | 23 | source.getDescription() != null ? source.getDescription().trim() : "", |
| 24 | source.getComision().getValue(), | 24 | source.getComision().getValue(), |
| 25 | getTiming(source.getComision().getType()), | 25 | getTiming(source.getComision().getType()), |
| 26 | new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()), | 26 | new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()), |
| @@ -38,17 +38,20 @@ public final class EventMapperV1 implements Mapper<GMEvent, Event> { | |||
| 38 | } | 38 | } |
| 39 | 39 | ||
| 40 | private static CommissionTiming getTiming(String type) throws IllegalArgumentException { | 40 | private static CommissionTiming getTiming(String type) throws IllegalArgumentException { |
| 41 | return switch (type) { | 41 | if (type == null) throw new IllegalArgumentException("Commission type cannot be null"); |
| 42 | return switch (type.toLowerCase().trim()) { | ||
| 42 | case "on-close" -> CommissionTiming.ON_CLOSE; | 43 | case "on-close" -> CommissionTiming.ON_CLOSE; |
| 43 | case "on-purchase" -> CommissionTiming.ON_PURCHASE; | 44 | case "on-purchase" -> CommissionTiming.ON_PURCHASE; |
| 44 | default -> throw new IllegalArgumentException("Unknown comision type: " + type); | 45 | default -> throw new IllegalArgumentException("Unknown commission type: " + type); |
| 45 | }; | 46 | }; |
| 46 | } | 47 | } |
| 47 | 48 | ||
| 48 | private static List<Market> getOptions(GMEvent x) { | 49 | private static List<Market> getOptions(GMEvent x) { |
| 49 | var ret = new ArrayList<Market>(x.getGMOptions().getGMOption().size()); | 50 | var raw = x.getGMOptions().getGMOption(); |
| 50 | for (var i = 0; i < x.getGMOptions().getGMOption().size(); i++) { | 51 | var ret = new ArrayList<Market>(raw.size()); |
| 51 | ret.add(new Market(getEventKey(x) + ":" + i, x.getGMOptions().getGMOption().get(i))); | 52 | for (var i = 0; i < raw.size(); i++) { |
| 53 | var opt = raw.get(i); | ||
| 54 | ret.add(new Market(getEventKey(x) + ":" + i, opt != null ? opt.trim() : "")); | ||
| 52 | } | 55 | } |
| 53 | return ret; | 56 | return ret; |
| 54 | } | 57 | } |
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java index bcae787..44e1c7f 100644 --- a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java +++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java | |||
| @@ -2,8 +2,10 @@ package market.guess.service.catalog.infrastructure.provider.v1; | |||
| 2 | 2 | ||
| 3 | import jakarta.xml.bind.JAXBContext; | 3 | import jakarta.xml.bind.JAXBContext; |
| 4 | import jakarta.xml.bind.JAXBException; | 4 | import jakarta.xml.bind.JAXBException; |
| 5 | import java.math.BigDecimal; | ||
| 5 | import java.nio.file.Files; | 6 | import java.nio.file.Files; |
| 6 | import java.nio.file.Path; | 7 | import java.nio.file.Path; |
| 8 | import java.util.ArrayList; | ||
| 7 | import market.guess.api.CatalogContext; | 9 | import market.guess.api.CatalogContext; |
| 8 | import market.guess.api.LoadResult; | 10 | import market.guess.api.LoadResult; |
| 9 | import market.guess.api.Result; | 11 | import market.guess.api.Result; |
| @@ -12,6 +14,7 @@ import market.guess.service.catalog.infrastructure.provider.Loader; | |||
| 12 | import market.guess.service.catalog.infrastructure.repository.EventRepository; | 14 | import market.guess.service.catalog.infrastructure.repository.EventRepository; |
| 13 | import market.guess.service.catalog.infrastructure.repository.UserRepository; | 15 | import market.guess.service.catalog.infrastructure.repository.UserRepository; |
| 14 | import market.guess.service.catalog.model.v1.GuessMarket; | 16 | import market.guess.service.catalog.model.v1.GuessMarket; |
| 17 | import market.guess.service.domain.Event; | ||
| 15 | import market.guess.service.domain.User; | 18 | import market.guess.service.domain.User; |
| 16 | 19 | ||
| 17 | public final class XMLLoaderV1 implements Loader { | 20 | public final class XMLLoaderV1 implements Loader { |
| @@ -35,19 +38,23 @@ public final class XMLLoaderV1 implements Loader { | |||
| 35 | try { | 38 | try { |
| 36 | context = JAXBContext.newInstance(GuessMarket.class); | 39 | context = JAXBContext.newInstance(GuessMarket.class); |
| 37 | } catch (JAXBException e) { | 40 | } catch (JAXBException e) { |
| 38 | throw new IllegalStateException(); | 41 | throw new IllegalStateException("Failed to initialize JAXB context: " + e.getMessage(), e); |
| 39 | } | 42 | } |
| 40 | } | 43 | } |
| 41 | 44 | ||
| 42 | @Override | 45 | @Override |
| 43 | public Result<LoadResult> load(Path path) { | 46 | public Result<LoadResult> load(Path path) { |
| 47 | if (path == null) { | ||
| 48 | return Result.error("INVALID_PATH", "File path cannot be null."); | ||
| 49 | } | ||
| 50 | |||
| 44 | if (!Files.isRegularFile(path)) { | 51 | if (!Files.isRegularFile(path)) { |
| 45 | return Result.error("FILE_NOT_FOUND", "No such file."); | 52 | return Result.error("FILE_NOT_FOUND", "No such file: " + path); |
| 46 | } | 53 | } |
| 47 | 54 | ||
| 48 | if (path.getFileName() == null | 55 | if (path.getFileName() == null |
| 49 | || !path.getFileName().toString().toLowerCase().endsWith(".xml")) { | 56 | || !path.getFileName().toString().toLowerCase().endsWith(".xml")) { |
| 50 | return Result.error("NOT_XML", "The file must be an XML."); | 57 | return Result.error("NOT_XML", "The file must have a .xml extension."); |
| 51 | } | 58 | } |
| 52 | 59 | ||
| 53 | try (var stream = Files.newInputStream(path)) { | 60 | try (var stream = Files.newInputStream(path)) { |
| @@ -59,21 +66,34 @@ public final class XMLLoaderV1 implements Loader { | |||
| 59 | return Result.error(validation.getMessage(), validation.getDetails()); | 66 | return Result.error(validation.getMessage(), validation.getDetails()); |
| 60 | } | 67 | } |
| 61 | 68 | ||
| 69 | var domainEvents = new ArrayList<Event>(); | ||
| 62 | for (var event : seed.getGMEvents().getGMEvent()) { | 70 | for (var event : seed.getGMEvents().getGMEvent()) { |
| 63 | eventRepository.add(mapper.toDomain(event)); | 71 | domainEvents.add(mapper.toDomain(event)); |
| 72 | } | ||
| 73 | |||
| 74 | eventRepository.clear(); | ||
| 75 | userRepository.clear(); | ||
| 76 | |||
| 77 | for (var domainEvent : domainEvents) { | ||
| 78 | eventRepository.add(domainEvent); | ||
| 64 | } | 79 | } |
| 65 | 80 | ||
| 66 | userRepository.add(new User("Tester", 500)); | 81 | userRepository.add(new User("Tester", new BigDecimal("500.00"))); |
| 67 | 82 | ||
| 68 | return Result.ok(new LoadResult(path.toString(), eventRepository.getAll().size())); | 83 | return Result.ok(new LoadResult(path.toString(), eventRepository.getAll().size())); |
| 84 | } catch (JAXBException e) { | ||
| 85 | var cause = | ||
| 86 | e.getLinkedException() != null ? e.getLinkedException().getMessage() : e.getMessage(); | ||
| 87 | return Result.error( | ||
| 88 | "XML_PARSE_ERROR", | ||
| 89 | "The XML file is malformed or invalid: " + (cause != null ? cause : e.getMessage())); | ||
| 69 | } catch (Exception e) { | 90 | } catch (Exception e) { |
| 70 | return Result.error("UNREADABLE", "The file could not be read."); | 91 | return Result.error("UNREADABLE", "The file could not be read: " + e.getMessage()); |
| 71 | } | 92 | } |
| 72 | } | 93 | } |
| 73 | 94 | ||
| 74 | @Override | 95 | @Override |
| 75 | public Result<LoadResult> seed(CatalogContext context) { | 96 | public Result<LoadResult> seed(CatalogContext context) { |
| 76 | // TODO Auto-generated method stub | ||
| 77 | throw new UnsupportedOperationException("Unimplemented method 'seed'"); | 97 | throw new UnsupportedOperationException("Unimplemented method 'seed'"); |
| 78 | } | 98 | } |
| 79 | } | 99 | } |
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java index 567558a..5a4e5ea 100644 --- a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java +++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java | |||
| @@ -9,27 +9,93 @@ public final class XMLValidatorV1 { | |||
| 9 | private static final int MAX_COMMISSION = 90; | 9 | private static final int MAX_COMMISSION = 90; |
| 10 | 10 | ||
| 11 | public Result<Void> validate(GuessMarket seed) { | 11 | public Result<Void> validate(GuessMarket seed) { |
| 12 | if (seed == null || seed.getGMEvents() == null || seed.getGMEvents().getGMEvent() == null) { | ||
| 13 | return Result.error("INVALID_XML", "The XML file contains no GM-events element."); | ||
| 14 | } | ||
| 12 | 15 | ||
| 13 | var seenIds = new HashSet<Integer>(); | 16 | var events = seed.getGMEvents().getGMEvent(); |
| 14 | var duplicated = new HashSet<Integer>(); | 17 | if (events.isEmpty()) { |
| 18 | return Result.error("EMPTY_EVENTS", "The XML file does not contain any events."); | ||
| 19 | } | ||
| 15 | 20 | ||
| 16 | for (var e : seed.getGMEvents().getGMEvent()) { | 21 | var seenIds = new HashSet<Integer>(); |
| 17 | 22 | ||
| 18 | if (!seenIds.add(e.getId()) && duplicated.add(e.getId())) { | 23 | for (var e : events) { |
| 24 | // 1. Event ID must be positive integer (> 0) | ||
| 25 | if (e.getId() <= 0) { | ||
| 26 | return Result.error( | ||
| 27 | "INVALID_ID", | ||
| 28 | "Event ID must be a positive integer (> 0), found: " + e.getId()); | ||
| 29 | } | ||
| 30 | if (!seenIds.add(e.getId())) { | ||
| 19 | return Result.error( | 31 | return Result.error( |
| 20 | "DUPLICATE_ID", | 32 | "DUPLICATE_ID", |
| 21 | "Event id " + e.getId() + " appears more than once, every event id must be unique."); | 33 | "Event ID " + e.getId() + " appears more than once. Every event ID must be unique."); |
| 22 | } | 34 | } |
| 23 | 35 | ||
| 36 | // 2. Event name must not be empty | ||
| 37 | if (e.getName() == null || e.getName().isEmpty() || String.join(" ", e.getName()).isBlank()) { | ||
| 38 | return Result.error("MISSING_NAME", "Event ID " + e.getId() + " has an empty name."); | ||
| 39 | } | ||
| 40 | |||
| 41 | // 3. Event description must not be empty | ||
| 42 | if (e.getDescription() == null || e.getDescription().isBlank()) { | ||
| 43 | return Result.error("MISSING_DESCRIPTION", "Event ID " + e.getId() + " has an empty description."); | ||
| 44 | } | ||
| 45 | |||
| 46 | // 4. Commission validation | ||
| 47 | if (e.getComision() == null) { | ||
| 48 | return Result.error("MISSING_COMMISSION", "Event ID " + e.getId() + " is missing commission."); | ||
| 49 | } | ||
| 24 | var pct = e.getComision().getValue(); | 50 | var pct = e.getComision().getValue(); |
| 25 | if (pct < MIN_COMMISSION || pct > MAX_COMMISSION) { | 51 | if (pct < MIN_COMMISSION || pct > MAX_COMMISSION) { |
| 26 | return Result.error( | 52 | return Result.error( |
| 27 | "COMMISSION_OUT_OF_RANGE", | 53 | "COMMISSION_OUT_OF_RANGE", |
| 28 | "Commission is %d%%, it must be between %d%% and %d%%." | 54 | "Event ID " + e.getId() + " commission is " + pct + "%. It must be between " + MIN_COMMISSION + "% and " + MAX_COMMISSION + "%."); |
| 29 | .formatted(pct, MIN_COMMISSION, MAX_COMMISSION)); | 55 | } |
| 56 | |||
| 57 | var type = e.getComision().getType(); | ||
| 58 | if (type == null || (!type.equalsIgnoreCase("on-close") && !type.equalsIgnoreCase("on-purchase"))) { | ||
| 59 | return Result.error( | ||
| 60 | "INVALID_COMMISSION_TYPE", | ||
| 61 | "Event ID " + e.getId() + " has invalid commission type '" + type + "'. Must be 'on-close' or 'on-purchase'."); | ||
| 62 | } | ||
| 63 | |||
| 64 | // 5. GM-options validation (exactly 2 options for Exercise 1) | ||
| 65 | if (e.getGMOptions() == null || e.getGMOptions().getGMOption() == null) { | ||
| 66 | return Result.error("MISSING_OPTIONS", "Event ID " + e.getId() + " is missing GM-options."); | ||
| 67 | } | ||
| 68 | var options = e.getGMOptions().getGMOption(); | ||
| 69 | if (options.size() != 2) { | ||
| 70 | return Result.error( | ||
| 71 | "INVALID_OPTIONS_COUNT", | ||
| 72 | "Event ID " + e.getId() + " must have exactly 2 options for Exercise 1, but found " + options.size() + "."); | ||
| 73 | } | ||
| 74 | var opt0 = options.get(0) == null ? "" : options.get(0).trim(); | ||
| 75 | var opt1 = options.get(1) == null ? "" : options.get(1).trim(); | ||
| 76 | if (opt0.isEmpty() || opt1.isEmpty()) { | ||
| 77 | return Result.error("EMPTY_OPTION", "Event ID " + e.getId() + " has an empty option name."); | ||
| 78 | } | ||
| 79 | if (opt0.equalsIgnoreCase(opt1)) { | ||
| 80 | return Result.error("DUPLICATE_OPTION", "Event ID " + e.getId() + " has duplicate option names: '" + opt0 + "'."); | ||
| 81 | } | ||
| 82 | |||
| 83 | // 6. GM-method and LMSR validation | ||
| 84 | if (e.getGMMethod() == null) { | ||
| 85 | return Result.error("MISSING_METHOD", "Event ID " + e.getId() + " is missing GM-method."); | ||
| 86 | } | ||
| 87 | if (e.getGMMethod().getGMLMSR() == null) { | ||
| 88 | return Result.error("INVALID_METHOD", "Event ID " + e.getId() + " must use LMSR trading method in Exercise 1."); | ||
| 89 | } | ||
| 90 | var b = e.getGMMethod().getGMLMSR().getB(); | ||
| 91 | if (b <= 0) { | ||
| 92 | return Result.error( | ||
| 93 | "INVALID_LIQUIDITY", | ||
| 94 | "Event ID " + e.getId() + " has invalid liquidity b=" + b + ". b must be a positive integer (> 0)."); | ||
| 30 | } | 95 | } |
| 31 | } | 96 | } |
| 32 | 97 | ||
| 33 | return Result.ok(); | 98 | return Result.ok(); |
| 34 | } | 99 | } |
| 35 | } | 100 | } |
| 101 | |||