diff --git a/pom.xml b/pom.xml index 81d5f181..3bdbf2c3 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ - + 2.15.2 43.0.0 gridsuite org.gridsuite:network-map-server @@ -80,6 +80,12 @@ + + com.fasterxml.jackson.module + jackson-module-jsonSchema + ${jackson-module-jsonSchema.version} + + org.junit @@ -110,6 +116,10 @@ + + com.fasterxml.jackson.module + jackson-module-jsonSchema + com.powsybl powsybl-network-store-client diff --git a/src/main/java/org/gridsuite/network/map/NetworkMapApi.java b/src/main/java/org/gridsuite/network/map/NetworkMapApi.java new file mode 100644 index 00000000..95065773 --- /dev/null +++ b/src/main/java/org/gridsuite/network/map/NetworkMapApi.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.network.map; + +/** + * @author Hugo Marcellin + */ + +public final class NetworkMapApi { + + private NetworkMapApi() { + } + + public static final String API_VERSION = "v1"; +} diff --git a/src/main/java/org/gridsuite/network/map/NetworkMapController.java b/src/main/java/org/gridsuite/network/map/NetworkMapController.java index 3d912bd5..b85a6d90 100644 --- a/src/main/java/org/gridsuite/network/map/NetworkMapController.java +++ b/src/main/java/org/gridsuite/network/map/NetworkMapController.java @@ -16,6 +16,7 @@ import lombok.AllArgsConstructor; import org.gridsuite.network.map.dto.*; import org.gridsuite.network.map.dto.definition.hvdc.HvdcShuntCompensatorsInfos; +import org.gridsuite.network.map.services.NetworkMapService; import org.springframework.context.annotation.ComponentScan; import org.springframework.web.bind.annotation.*; @@ -28,14 +29,11 @@ * @author Franck Lecuyer */ @RestController -@RequestMapping(value = "/" + NetworkMapController.API_VERSION + "/") -@Tag(name = "network-map-server") +@RequestMapping(value = "/" + NetworkMapApi.API_VERSION + "/") +@Tag(name = "Network map server") @ComponentScan(basePackageClasses = NetworkMapService.class) @AllArgsConstructor public class NetworkMapController { - - public static final String API_VERSION = "v1"; - private final NetworkMapService networkMapService; @PostMapping(value = "/networks/{networkUuid}/elements-ids", produces = APPLICATION_JSON_VALUE) diff --git a/src/main/java/org/gridsuite/network/map/NetworkMapSwaggerConfig.java b/src/main/java/org/gridsuite/network/map/NetworkMapSwaggerConfig.java index 43517e80..5feb32af 100644 --- a/src/main/java/org/gridsuite/network/map/NetworkMapSwaggerConfig.java +++ b/src/main/java/org/gridsuite/network/map/NetworkMapSwaggerConfig.java @@ -22,6 +22,6 @@ public OpenAPI createOpenApi() { .info(new Info() .title("Network map API") .description("This is the documentation of network map REST API") - .version(NetworkMapController.API_VERSION)); + .version(NetworkMapApi.API_VERSION)); } } diff --git a/src/main/java/org/gridsuite/network/map/SchemaController.java b/src/main/java/org/gridsuite/network/map/SchemaController.java new file mode 100644 index 00000000..38669798 --- /dev/null +++ b/src/main/java/org/gridsuite/network/map/SchemaController.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.network.map; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.gridsuite.network.map.dto.ElementInfos.InfoType; +import org.gridsuite.network.map.dto.ElementType; +import org.gridsuite.network.map.services.SchemaService; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController +@RequestMapping(value = "/" + NetworkMapApi.API_VERSION + "/schemas") +@Tag(name = "Network map server - Schemas") +@AllArgsConstructor +public class SchemaController { + public static final String APPLICATION_JSON_SCHEMA_VALUE = "application/schema+json"; + private final SchemaService schemaService; + + @GetMapping(value = "/{elementType}/{infoType}", produces = APPLICATION_JSON_SCHEMA_VALUE) + @Operation(summary = "Get element schema description") + @ApiResponse(responseCode = "200", description = "Element schema") + public JsonSchema getElementSchema(@Parameter(description = "Element type") @PathVariable(name = "elementType") ElementType elementType, + @Parameter(description = "Info type") @PathVariable(name = "infoType") InfoType infoType) { + try { + return schemaService.getSchema(elementType, infoType); + } catch (final UnsupportedOperationException | JsonMappingException ex) { + throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "The view " + infoType + " for " + elementType + " type is not available yet."); + } + } +} diff --git a/src/main/java/org/gridsuite/network/map/NetworkMapService.java b/src/main/java/org/gridsuite/network/map/services/NetworkMapService.java similarity index 99% rename from src/main/java/org/gridsuite/network/map/NetworkMapService.java rename to src/main/java/org/gridsuite/network/map/services/NetworkMapService.java index 0f380add..92ba0ac4 100644 --- a/src/main/java/org/gridsuite/network/map/NetworkMapService.java +++ b/src/main/java/org/gridsuite/network/map/services/NetworkMapService.java @@ -4,7 +4,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.gridsuite.network.map; +package org.gridsuite.network.map.services; import com.powsybl.commons.PowsyblException; import com.powsybl.iidm.network.*; diff --git a/src/main/java/org/gridsuite/network/map/services/SchemaService.java b/src/main/java/org/gridsuite/network/map/services/SchemaService.java new file mode 100644 index 00000000..9a853f7f --- /dev/null +++ b/src/main/java/org/gridsuite/network/map/services/SchemaService.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.network.map.services; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import org.gridsuite.network.map.dto.ElementInfos.InfoType; +import org.gridsuite.network.map.dto.ElementType; +import org.gridsuite.network.map.dto.definition.battery.BatteryTabInfos; +import org.gridsuite.network.map.dto.definition.branch.BranchTabInfos; +import org.gridsuite.network.map.dto.definition.branch.line.LineTabInfos; +import org.gridsuite.network.map.dto.definition.branch.twowindingstransformer.TwoWindingsTransformerTabInfos; +import org.gridsuite.network.map.dto.definition.bus.BusTabInfos; +import org.gridsuite.network.map.dto.definition.busbarsection.BusBarSectionTabInfos; +import org.gridsuite.network.map.dto.definition.danglingline.DanglingLineTabInfos; +import org.gridsuite.network.map.dto.definition.generator.GeneratorTabInfos; +import org.gridsuite.network.map.dto.definition.hvdc.HvdcTabInfos; +import org.gridsuite.network.map.dto.definition.lccconverterstation.LccConverterStationTabInfos; +import org.gridsuite.network.map.dto.definition.load.LoadTabInfos; +import org.gridsuite.network.map.dto.definition.shuntcompensator.ShuntCompensatorTabInfos; +import org.gridsuite.network.map.dto.definition.staticvarcompensator.StaticVarCompensatorTabInfos; +import org.gridsuite.network.map.dto.definition.substation.SubstationTabInfos; +import org.gridsuite.network.map.dto.definition.threewindingstransformer.ThreeWindingsTransformerTabInfos; +import org.gridsuite.network.map.dto.definition.tieline.TieLineTabInfos; +import org.gridsuite.network.map.dto.definition.voltagelevel.VoltageLevelTabInfos; +import org.gridsuite.network.map.dto.definition.vscconverterstation.VscConverterStationTabInfos; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class SchemaService { + + private final ObjectMapper objectMapper; + + /** + * @apiNote use class instance to be more secure with enum and classes rename/moving/etc with IDE + */ + private static Class getTabInfosClass(final ElementType elementType) { + return switch (elementType) { + case BATTERY -> BatteryTabInfos.class; + case BUS -> BusTabInfos.class; + case BUSBAR_SECTION -> BusBarSectionTabInfos.class; + case DANGLING_LINE -> DanglingLineTabInfos.class; + case GENERATOR -> GeneratorTabInfos.class; + case HVDC_LINE, HVDC_LINE_LCC, HVDC_LINE_VSC -> HvdcTabInfos.class; + case LCC_CONVERTER_STATION -> LccConverterStationTabInfos.class; + case LINE -> LineTabInfos.class; + case LOAD -> LoadTabInfos.class; + case SHUNT_COMPENSATOR -> ShuntCompensatorTabInfos.class; + case STATIC_VAR_COMPENSATOR -> StaticVarCompensatorTabInfos.class; + case SUBSTATION -> SubstationTabInfos.class; + case THREE_WINDINGS_TRANSFORMER -> ThreeWindingsTransformerTabInfos.class; + case TIE_LINE -> TieLineTabInfos.class; + case TWO_WINDINGS_TRANSFORMER -> TwoWindingsTransformerTabInfos.class; + case VOLTAGE_LEVEL -> VoltageLevelTabInfos.class; + case VSC_CONVERTER_STATION -> VscConverterStationTabInfos.class; + case BRANCH -> BranchTabInfos.class; + }; + } + + public JsonSchema getSchema(@NonNull final ElementType elementType, @NonNull final InfoType infoType) throws JsonMappingException { + if (infoType != InfoType.TAB) { + throw new UnsupportedOperationException("This info type is not currently supported."); + } + JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(objectMapper); + return schemaGen.generateSchema(getTabInfosClass(elementType)); + } +} diff --git a/src/test/java/org/gridsuite/network/map/ListHandlingControllerTest.java b/src/test/java/org/gridsuite/network/map/ListHandlingControllerTest.java index ddf9e2c7..3317c76d 100644 --- a/src/test/java/org/gridsuite/network/map/ListHandlingControllerTest.java +++ b/src/test/java/org/gridsuite/network/map/ListHandlingControllerTest.java @@ -7,6 +7,7 @@ import org.gridsuite.network.map.dto.ElementType; import org.gridsuite.network.map.dto.InfoTypeParameters; +import org.gridsuite.network.map.services.NetworkMapService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/org/gridsuite/network/map/SchemaControllerTest.java b/src/test/java/org/gridsuite/network/map/SchemaControllerTest.java new file mode 100644 index 00000000..fbf79a69 --- /dev/null +++ b/src/test/java/org/gridsuite/network/map/SchemaControllerTest.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.network.map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import org.apache.commons.lang3.tuple.Pair; +import org.gridsuite.network.map.dto.ElementInfos.InfoType; +import org.gridsuite.network.map.dto.ElementType; +import org.gridsuite.network.map.services.SchemaService; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.lang.NonNull; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(SchemaController.class) +class SchemaControllerTest { + @Autowired + private MockMvc mockMvc; + + @SpyBean + private SchemaService schemaService; + + private static Stream schemaRequestValues() { + final List> cases = new ArrayList<>(); + for (ElementType elementType : ElementType.values()) { + for (InfoType infoType : InfoType.values()) { + cases.add(Pair.of(elementType, infoType)); + } + } + return cases.stream().map(e1 -> Arguments.of(e1.getKey(), e1.getValue())); + } + + @ParameterizedTest(name = "{0} (view {1})") + @MethodSource("schemaRequestValues") + void schemaRequest(@NonNull final ElementType eType, @NonNull final InfoType iType) throws Exception { + ResultActions result = this.mockMvc.perform(get("/v1/schemas/{eType}/{iType}", eType, iType)).andDo(log()); + if (iType.equals(InfoType.TAB)) { + JsonSchema schema = schemaService.getSchema(eType, iType); + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String json = ow.writeValueAsString(schema); + result.andExpectAll( + status().isOk(), + content().contentType(SchemaController.APPLICATION_JSON_SCHEMA_VALUE), + content().json(json) + ); + } else { + result.andExpect(status().isNotImplemented()); + } + } +}