diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7b2023..b640d8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,9 @@ repos: - id: check-yaml - id: no-commit-to-branch - id: mixed-line-ending + - id: pretty-format-json + args: [ --autofix, --no-ensure-ascii, '--top-keys=openapi,info,servers,paths,components' ] + files: docs/openapi.json - repo: https://github.com/zricethezav/gitleaks rev: v8.20.1 hooks: diff --git a/README.md b/README.md index 313c6de..1f75ca5 100644 --- a/README.md +++ b/README.md @@ -32,61 +32,7 @@ Changes only take effect after restart of Polarion. ### REST API -Get version: -```bash -curl --location 'https://:/polarion/api-extender/rest/api/version' \ - --header 'Authorization: Bearer ' -``` -Response example: -```json -{ - "bundleName":"API Extension for Polarion ALM", - "bundleVendor":"SBB AG", - "automaticModuleName":"ch.sbb.polarion.extension.api_extender", - "bundleVersion":"1.0.0", - "bundleBuildTimestamp":"2023-06-27 12:43", - "bundleBuildTimestampDigitsOnly":"202306271243" -} -``` - -Get custom field value: -```bash -curl --location 'https://:/polarion/api-extender/rest/api/projects//keys/' \ - --header 'Authorization: Bearer ' -``` - -Get global record value: -```bash -curl --location 'https://:/polarion/api-extender/rest/api/records/' \ - --header 'Authorization: Bearer ' -``` - -Response example: -```json -{ - "value": "custom_value" -} -``` - -Set custom field value: -```bash -curl --location 'https://:/polarion/api-extender/rest/api/projects//keys/' \ - --header 'Content-Type: application/json' \ - --header 'Authorization: Bearer ' \ - --data '{ - "value": "" - }' -``` - -Set global record value: -```bash -curl --location 'https://:/polarion/api-extender/rest/api/records/' \ - --header 'Content-Type: application/json' \ - --header 'Authorization: Bearer ' \ - --data '{ - "value": "" - }' -``` +This extension provides REST API. OpenAPI Specification can be obtained [here](docs/openapi.json). ### Live Report Page diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 0000000..03cf391 --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,576 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "API Extender REST API", + "version" : "v1" + }, + "paths" : { + "/api/context" : { + "get" : { + "operationId" : "getContext", + "responses" : { + "default" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Context" + } + } + }, + "description" : "Context information" + } + }, + "summary" : "Returns basic context information of Polarion's extension", + "tags" : [ "Extension Information" ] + } + }, + "/api/projects/{projectId}/keys/{key}" : { + "delete" : { + "operationId" : "deleteCustomValue", + "parameters" : [ { + "in" : "path", + "name" : "projectId", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "key", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "Successfully deleted the custom field value" + } + }, + "summary" : "Removes custom field", + "tags" : [ "Project custom fields" ] + }, + "get" : { + "operationId" : "getCustomValue", + "parameters" : [ { + "in" : "path", + "name" : "projectId", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "key", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Field" + } + } + }, + "description" : "Successfully retrieved custom field value" + } + }, + "summary" : "Returns custom field value", + "tags" : [ "Project custom fields" ] + }, + "post" : { + "operationId" : "setCustomValue", + "parameters" : [ { + "in" : "path", + "name" : "projectId", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "key", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Field" + } + } + } + }, + "responses" : { + "200" : { + "description" : "Successfully saved the custom field value" + } + }, + "summary" : "Saves custom field", + "tags" : [ "Project custom fields" ] + } + }, + "/api/records/{key}" : { + "delete" : { + "operationId" : "deleteRecordValue", + "parameters" : [ { + "in" : "path", + "name" : "key", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "Successfully deleted the global record" + } + }, + "summary" : "Removes global record", + "tags" : [ "Global Records" ] + }, + "get" : { + "operationId" : "getRecordValue", + "parameters" : [ { + "in" : "path", + "name" : "key", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Field" + } + } + }, + "description" : "Successfully retrieved the global record value" + } + }, + "summary" : "Returns global record value", + "tags" : [ "Global Records" ] + }, + "post" : { + "operationId" : "setRecordValue", + "parameters" : [ { + "in" : "path", + "name" : "key", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Field" + } + } + } + }, + "responses" : { + "200" : { + "description" : "Successfully saved the global record" + } + }, + "summary" : "Saves global record", + "tags" : [ "Global Records" ] + } + }, + "/api/settings" : { + "get" : { + "operationId" : "readFeaturesList_1", + "responses" : { + "default" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "List of supported features" + } + }, + "summary" : "Returns the complete list of all supported features", + "tags" : [ "Settings" ] + } + }, + "/api/settings/{feature}/default-content" : { + "get" : { + "operationId" : "getDefaultValues_1", + "parameters" : [ { + "in" : "path", + "name" : "feature", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "default" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SettingsModel" + } + } + }, + "description" : "Default values" + } + }, + "summary" : "Returns default values of specified setting", + "tags" : [ "Settings" ] + } + }, + "/api/settings/{feature}/names" : { + "get" : { + "operationId" : "readSettingNames_1", + "parameters" : [ { + "in" : "path", + "name" : "feature", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "default" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SettingName" + } + } + }, + "description" : "List of setting names" + } + }, + "summary" : "Returns names of specified setting", + "tags" : [ "Settings" ] + } + }, + "/api/settings/{feature}/names/{name}" : { + "delete" : { + "operationId" : "deleteSetting_1", + "parameters" : [ { + "in" : "path", + "name" : "feature", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "name", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "Setting deleted successfully" + } + }, + "summary" : "Deletes specified setting by id", + "tags" : [ "Settings" ] + }, + "post" : { + "operationId" : "renameSetting_1", + "parameters" : [ { + "in" : "path", + "name" : "feature", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "name", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "string" + } + } + } + }, + "responses" : { + "204" : { + "description" : "Setting name updated successfully" + } + }, + "summary" : "Updates name of specified named setting", + "tags" : [ "Settings" ] + } + }, + "/api/settings/{feature}/names/{name}/content" : { + "get" : { + "operationId" : "readSetting_1", + "parameters" : [ { + "in" : "path", + "name" : "feature", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "name", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "query", + "name" : "revision", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "default" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SettingsModel" + } + } + }, + "description" : "Setting content" + } + }, + "summary" : "Returns values (content) of specified setting by its id and revision", + "tags" : [ "Settings" ] + }, + "put" : { + "operationId" : "saveSetting_1", + "parameters" : [ { + "in" : "path", + "name" : "feature", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "name", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "string" + } + } + } + }, + "responses" : { + "204" : { + "description" : "Setting created/updated successfully" + } + }, + "summary" : "Creates or updates named setting. Creation scenario will use default setting value if no body specified in the request.", + "tags" : [ "Settings" ] + } + }, + "/api/settings/{feature}/names/{name}/revisions" : { + "get" : { + "operationId" : "readRevisionsList_1", + "parameters" : [ { + "in" : "path", + "name" : "feature", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "path", + "name" : "name", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "default" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Revision" + } + } + }, + "description" : "List of revisions" + } + }, + "summary" : "Returns revisions history of specified setting with specified id", + "tags" : [ "Settings" ] + } + }, + "/api/version" : { + "get" : { + "operationId" : "getVersion", + "responses" : { + "default" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Version" + } + } + }, + "description" : "Version information" + } + }, + "summary" : "Returns version of Polarion's extension", + "tags" : [ "Extension Information" ] + } + } + }, + "components" : { + "schemas" : { + "Context" : { + "type" : "object", + "description" : "Represents the context for building URLs related to Polarion services", + "properties" : { + "baseUrl" : { + "type" : "string", + "description" : "Returns the base URL constructed with the extension context", + "example" : "/polarion/pdf-exporter" + }, + "extensionContext" : { + "type" : "string", + "description" : "The extension context used as a base for URL construction", + "example" : "pdf-exporter" + }, + "restUrl" : { + "type" : "string", + "description" : "Returns the REST API URL constructed with the extension context", + "example" : "/polarion/pdf-exporter/rest" + }, + "swaggerUiUrl" : { + "type" : "string", + "description" : "Returns the Swagger UI URL for the REST API" + } + } + }, + "Field" : { + "type" : "object", + "description" : "Represents a field object with an ID and value", + "properties" : { + "value" : { + "type" : "string", + "description" : "The value associated with the field" + } + } + }, + "Revision" : { + "type" : "object", + "description" : "Revision details", + "properties" : { + "author" : { + "type" : "string", + "description" : "The author of the revision" + }, + "baseline" : { + "type" : "string", + "description" : "The baseline of the revision" + }, + "date" : { + "type" : "string", + "description" : "The date of the revision" + }, + "description" : { + "type" : "string", + "description" : "The description of the revision" + }, + "name" : { + "type" : "string", + "description" : "The name of the revision" + } + } + }, + "SettingName" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "description" : "The name of the setting" + }, + "scope" : { + "type" : "string", + "description" : "The scope of the setting" + } + } + }, + "SettingsModel" : { + "type" : "object", + "description" : "Settings model", + "properties" : { + "bundleTimestamp" : { + "type" : "string", + "description" : "The bundle timestamp of the setting" + } + } + }, + "Version" : { + "type" : "object", + "description" : "Details about the software version", + "properties" : { + "automaticModuleName" : { + "type" : "string", + "description" : "The automatic module name" + }, + "bundleBuildTimestamp" : { + "type" : "string", + "description" : "The build timestamp of the bundle" + }, + "bundleName" : { + "type" : "string", + "description" : "The name of the bundle" + }, + "bundleVendor" : { + "type" : "string", + "description" : "The vendor of the bundle" + }, + "bundleVersion" : { + "type" : "string", + "description" : "The version of the bundle" + }, + "projectURL" : { + "type" : "string", + "description" : "The project URL" + }, + "supportEmail" : { + "type" : "string", + "description" : "Support email for the bundle" + } + } + } + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 51d49e2..0145d3a 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,21 @@ org.apache.maven.plugins maven-source-plugin + + + io.swagger.core.v3 + swagger-maven-plugin + + JSON + true + + ch.sbb.polarion.extension.generic.rest.controller + ch.sbb.polarion.extension.generic.rest.model + ch.sbb.polarion.extension.api.extender.rest.controller + ch.sbb.polarion.extension.api.extender.rest.model + + + diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java index 796b0c7..80ba9ca 100644 --- a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java @@ -11,6 +11,9 @@ import ch.sbb.polarion.extension.generic.settings.SettingId; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; +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 io.swagger.v3.oas.annotations.tags.Tag; import lombok.SneakyThrows; import org.jetbrains.annotations.VisibleForTesting; @@ -45,10 +48,21 @@ public GlobalRecordInternalController() { this.polarionService = polarionService; } - @Operation(summary = "Returns global record value") @GET @Path("/records/{key}") @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Returns global record value", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully retrieved the global record value", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Field.class) + ) + ) + } + ) public Field getRecordValue(@PathParam("key") String key) throws JAXBException { final GlobalRecords globalRecords = new GlobalRecords(); Field field = globalRecords.getRecord(key); @@ -59,11 +73,18 @@ public Field getRecordValue(@PathParam("key") String key) throws JAXBException { } } - @Operation(summary = "Saves global record") @POST @Path("/records/{key}") @Consumes(MediaType.APPLICATION_JSON) @SneakyThrows + @Operation(summary = "Saves global record", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully saved the global record" + ) + } + ) public void setRecordValue(@PathParam("key") String key, Field field) { checkPermissions(); @@ -75,10 +96,16 @@ public void setRecordValue(@PathParam("key") String key, Field field) { globalRecords.setRecord(key, field.getValue()); } - @Operation(summary = "Removes global record") @DELETE @Path("/records/{key}") @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "Removes global record", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully deleted the global record") + } + ) @SneakyThrows public void deleteRecordValue(@PathParam("key") String key) { checkPermissions(); diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/OpenAPIInfo.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/OpenAPIInfo.java new file mode 100644 index 0000000..af5b6c9 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/OpenAPIInfo.java @@ -0,0 +1,17 @@ +package ch.sbb.polarion.extension.api.extender.rest.controller; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; + +@OpenAPIDefinition( + info = @Info( + title = "API Extender REST API", + version = "v1", + description = "", + termsOfService = "", + contact = @Contact(name = "", url = ""), + license = @License(name = "", url = ""))) +public class OpenAPIInfo { +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java index c8e0887..dc18101 100644 --- a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java @@ -11,6 +11,9 @@ import ch.sbb.polarion.extension.generic.util.ScopeUtils; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; +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 io.swagger.v3.oas.annotations.tags.Tag; import lombok.SneakyThrows; import org.jetbrains.annotations.VisibleForTesting; @@ -45,10 +48,21 @@ public ProjectCustomFieldInternalController() { this.polarionService = polarionService; } - @Operation(summary = "Returns custom field value") @GET @Path("/projects/{projectId}/keys/{key}") @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Returns custom field value", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully retrieved custom field value", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Field.class) + ) + ) + } + ) public Field getCustomValue(@PathParam("projectId") String projectId, @PathParam("key") String key) throws JAXBException { final CustomFieldsProject customFieldsProject = new CustomFieldsProject(projectId); Field field = customFieldsProject.getCustomField(key); @@ -59,10 +73,17 @@ public Field getCustomValue(@PathParam("projectId") String projectId, @PathParam } } - @Operation(summary = "Saves custom field") @POST @Path("/projects/{projectId}/keys/{key}") @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "Saves custom field", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully saved the custom field value" + ) + } + ) @SneakyThrows public void setCustomValue(@PathParam("projectId") String projectId, @PathParam("key") String key, Field field) { checkPermissions(projectId); @@ -75,10 +96,17 @@ public void setCustomValue(@PathParam("projectId") String projectId, @PathParam( customFieldsProject.setCustomField(key, field.getValue()); } - @Operation(summary = "Removes custom field") @DELETE @Path("/projects/{projectId}/keys/{key}") @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "Removes custom field", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully deleted the custom field value" + ) + } + ) @SneakyThrows public void deleteCustomValue(@PathParam("projectId") String projectId, @PathParam("key") String key) { checkPermissions(projectId); diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java index edeb0e4..e134324 100644 --- a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java @@ -1,6 +1,7 @@ package ch.sbb.polarion.extension.api.extender.rest.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,10 +21,15 @@ @Getter @Setter @ToString +@Schema(description = "Represents a field object with an ID and value") public class Field { + @XmlAttribute(name = "id") @JsonIgnore + @Schema(description = "The key of the field, ignored in JSON serialization") public String key; + @XmlValue + @Schema(description = "The value associated with the field") public String value; } diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java index 8c62ae4..ce8b53b 100644 --- a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java @@ -1,5 +1,6 @@ package ch.sbb.polarion.extension.api.extender.rest.model; +import io.swagger.v3.oas.annotations.media.Schema; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -7,8 +8,11 @@ import java.util.ArrayList; import java.util.List; +@Schema(description = "A generic class representing a list of fields") public abstract class GenericFields { + @XmlElement(name = "field") + @Schema(description = "List of fields represented as key-value pairs") protected List fields = new ArrayList<>(); public void setField(@NotNull String key, @Nullable String value) { diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java index 1e96ca9..f08a8eb 100644 --- a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java @@ -1,6 +1,7 @@ package ch.sbb.polarion.extension.api.extender.rest.model; import ch.sbb.polarion.extension.api.extender.util.CustomFieldUtils; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.ToString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -12,6 +13,8 @@ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "project") @ToString + +@Schema(description = "Represents a project with a set of custom fields") public class Project extends GenericFields { @Override