diff --git a/JShellAPI/build.gradle b/JShellAPI/build.gradle index 7a2380b..8eca29f 100644 --- a/JShellAPI/build.gradle +++ b/JShellAPI/build.gradle @@ -3,6 +3,7 @@ import java.time.Instant plugins { id 'org.springframework.boot' version '3.2.5' id 'io.spring.dependency-management' version '1.1.5' + id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' id 'com.google.cloud.tools.jib' version '3.4.2' id 'com.github.johnrengelman.shadow' version '8.1.1' } @@ -10,9 +11,13 @@ plugins { dependencies { implementation project(':JShellWrapper') implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'com.github.docker-java:docker-java-transport-httpclient5:3.3.6' implementation 'com.github.docker-java:docker-java-core:3.3.6' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" } @@ -36,4 +41,4 @@ shadowJar { archiveBaseName.set('JShellPlaygroundBackend') archiveClassifier.set('') archiveVersion.set('') -} \ No newline at end of file +} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index 2c60570..5df07ee 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -1,5 +1,11 @@ package org.togetherjava.jshellapi.rest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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 jakarta.validation.constraints.Pattern; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -18,14 +24,27 @@ @RequestMapping("jshell") @RestController public class JShellController { + private static final String ID_REGEX = "^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"; + + @Autowired private JShellSessionService service; + @Autowired private StartupScriptsService startupScriptsService; @PostMapping("/eval/{id}") - public JShellResult eval(@PathVariable String id, + @Operation(summary = "Evaluate code in a JShell session", + description = "Evaluate code in a JShell session, create a session from this id, or use an" + + " existing session if this id already exists.") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = JShellResult.class))}) + public JShellResult eval( + @Parameter(description = "id of the session, must follow the regex " + ID_REGEX) + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + @PathVariable String id, + @Parameter(description = "id of the startup script to use") @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) throws DockerException { - validateId(id); + @Parameter(description = "Java code to evaluate") @RequestBody String code) + throws DockerException { return service.session(id, startupScriptId) .eval(code) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -33,8 +52,15 @@ public JShellResult eval(@PathVariable String id, } @PostMapping("/eval") - public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) throws DockerException { + @Operation(summary = "Evaluate code in a JShell session", + description = "Evaluate code in a JShell session, creates a new session each time, with a random id") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = JShellResultWithId.class))}) + public JShellResultWithId eval( + @Parameter(description = "id of the startup script to use") + @RequestParam(required = false) StartupScriptId startupScriptId, + @Parameter(description = "Java code to evaluate") @RequestBody String code) + throws DockerException { JShellService jShellService = service.session(startupScriptId); return new JShellResultWithId(jShellService.id(), jShellService.eval(code) @@ -43,8 +69,15 @@ public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId s } @PostMapping("/single-eval") - public JShellResult singleEval(@RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) throws DockerException { + @Operation(summary = "Evaluate code in JShell", + description = "Evaluate code in a JShell session, creates a session that can only be used once, and has lower timeout") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = JShellResult.class))}) + public JShellResult singleEval( + @Parameter(description = "id of the startup script to use") + @RequestParam(required = false) StartupScriptId startupScriptId, + @Parameter(description = "Java code to evaluate") @RequestBody String code) + throws DockerException { JShellService jShellService = service.oneTimeSession(startupScriptId); return jShellService.eval(code) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -52,11 +85,15 @@ public JShellResult singleEval(@RequestParam(required = false) StartupScriptId s } @GetMapping("/snippets/{id}") - public List snippets(@PathVariable String id, - @RequestParam(required = false) boolean includeStartupScript) throws DockerException { - validateId(id); - if (!service.hasSession(id)) - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found"); + @Operation(summary = "Retreive all snippets from a JShell session") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = List.class))}) + public List snippets( + @Parameter(description = "id of the session, must follow the regex " + ID_REGEX) + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + @PathVariable String id, @RequestParam(required = false) boolean includeStartupScript) + throws DockerException { + checkIdExists(id); return service.session(id, null) .snippets(includeStartupScript) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -64,32 +101,26 @@ public List snippets(@PathVariable String id, } @DeleteMapping("/{id}") - public void delete(@PathVariable String id) throws DockerException { - validateId(id); - if (!service.hasSession(id)) - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found"); + @Operation(summary = "Delete a JShell session") + public void delete( + @Parameter(description = "id of the session, must follow the regex " + ID_REGEX) + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + @PathVariable String id) throws DockerException { + checkIdExists(id); service.deleteSession(id); } @GetMapping("/startup_script/{id}") - public String startupScript(@PathVariable StartupScriptId id) { + @Operation(summary = "Get a startup script") + public String startupScript(@Parameter(description = "id of the startup script to fetch") + @PathVariable StartupScriptId id) { return startupScriptsService.get(id); } - @Autowired - public void setService(JShellSessionService service) { - this.service = service; - } - - @Autowired - public void setStartupScriptsService(StartupScriptsService startupScriptsService) { - this.startupScriptsService = startupScriptsService; - } - - private static void validateId(String id) throws ResponseStatusException { - if (!id.matches("[a-zA-Z0-9][a-zA-Z0-9_.-]+")) { + private void checkIdExists(String id) { + if (!id.matches(ID_REGEX)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, - "Id " + id + " doesn't match the regex [a-zA-Z0-9][a-zA-Z0-9_.-]+"); + "Id " + id + " doesn't match regex " + ID_REGEX); } } } diff --git a/JShellAPI/src/main/resources/application.yaml b/JShellAPI/src/main/resources/application.yaml index 831580c..d7e8eda 100644 --- a/JShellAPI/src/main/resources/application.yaml +++ b/JShellAPI/src/main/resources/application.yaml @@ -23,6 +23,7 @@ jshellapi: server: error: include-message: always + include-binding-errors: always logging: level: diff --git a/JShellWrapper/build.gradle b/JShellWrapper/build.gradle index 279e8c0..debf2c9 100644 --- a/JShellWrapper/build.gradle +++ b/JShellWrapper/build.gradle @@ -41,4 +41,4 @@ shadowJar { archiveBaseName.set('JShellWrapper') archiveClassifier.set('') archiveVersion.set('') -} \ No newline at end of file +}