Skip to content

Commit c5168ba

Browse files
authored
Add Swagger Specification Generation from Annotations (opendcs#360)
* Swagger configuration in progress * Fixed Jackson dependencies * endpoint annotations * DTO annotations, auth annotations * Some endpoints annotated using openapi.json as reference * Updated paths, removed invalid openapi.json location information * Ordered ResourceExamples inner classes alphabetically, added role to decode endpoint, updated DTO annotation wording * Reformatted annotations, updated examples, updated response codes * Rename Timeseries group retrieval method to match endpoint usage * Updated annotations on check endpoint, updated build.gradle, updated documentation * Added Swagger doc generation to release workflow * Remove legacy OpenAPI specification JSON
1 parent 5ebc788 commit c5168ba

File tree

98 files changed

+5523
-9757
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+5523
-9757
lines changed

.github/workflows/publish.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
uses: gradle/[email protected]
1919
- name: Build
2020
run: ./gradlew build
21+
- name: Generate OpenAPI
22+
run: ./gradlew generateOpenAPI
2123
- name: Attach OpenDCS REST API WAR file
2224
run: |
2325
gh release upload ${{ github.event.release.tag_name }} opendcs-rest-api/build/libs/opendcs-rest-api-${{ github.event.release.tag_name }}.war --repo ${{ github.repository }}
@@ -28,3 +30,8 @@ jobs:
2830
gh release upload ${{ github.event.release.tag_name }} opendcs-web-client/build/libs/opendcs-web-client-${{ github.event.release.tag_name }}.war --repo ${{ github.repository }}
2931
env:
3032
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
- name: Attach OpenAPI Specification
34+
run: |
35+
gh release upload ${{ github.event.release.tag_name }} opendcs-rest-api/build/swagger/opendcs-openapi.json --repo ${{ github.repository }}
36+
env:
37+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/swagger.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Swagger/OpenAPI Specification Generation
2+
on:
3+
push:
4+
branches:
5+
- main
6+
- feature/task_cwms_support
7+
pull_request:
8+
branches:
9+
- main
10+
- feature/task_cwms_support
11+
permissions:
12+
contents: write
13+
jobs:
14+
build-api-spec:
15+
name: Build OpenAPI Specification
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/[email protected]
19+
- name: Set up JDK 17
20+
uses: actions/[email protected]
21+
with:
22+
java-version: 17
23+
distribution: 'temurin'
24+
- name: Setup Gradle
25+
uses: gradle/[email protected]
26+
- name: Generate OpenAPI
27+
run: ./gradlew generateOpenAPI
28+
- name: Upload OpenAPI Specification
29+
uses: actions/[email protected]
30+
with:
31+
name: openapi
32+
path: ./**/build/swagger/opendcs-openapi.json
33+
retention-days: 1
34+
if-no-files-found: error

doc-source/Swagger-README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Swagger/OpenAPI Configuration
2+
3+
The OpenAPI specification represents the structure and expected input and output values of the
4+
REST API.
5+
6+
It can be visualized and interacted with via the Swagger UI, which represents the API
7+
resources and their functionality in a user-friendly manner.
8+
9+
## Getting Started
10+
11+
To view the API specification, navigate to the Swagger UI located at:
12+
```
13+
https://[REST_API_URL]/odcsapi/swaggerui/index.html
14+
```
15+
16+
On development machines running the REST API locally, this will be located at
17+
```
18+
http://localhost:7000/odcsapi/swaggerui/index.html
19+
```
20+
21+
## OpenAPI Specification Generation
22+
23+
In order to provide multiple options for developers to interact with the API,
24+
the OpenAPI specification is generated in one of two ways.
25+
26+
### Automated Generation
27+
To automatically generate the OpenAPI specification, initiate the `./gradlew run` Gradle task.
28+
29+
The OpenAPI specification will be generated upon runtime and will be available at the Swagger UI
30+
endpoint mentioned above.
31+
32+
The raw JSON or YAML for the specification can be found at
33+
```
34+
http://localhost:7000/odcsapi/openapi.json
35+
```
36+
or
37+
```
38+
http://localhost:7000/odcsapi/openapi.yaml
39+
```
40+
respectively.
41+
42+
### Manual Generation
43+
44+
For ease of use, a manual generation method has been developed to avoid the requirement of running
45+
the webserver. This method is useful for developers who are working on modifying the API specification and
46+
wish to quickly generate the OpenAPI specification to view changes.
47+
48+
> [!TIP]
49+
> Generated JSON and YAML OpenAPI specifications can be viewed by copying the content of the file and pasting it into the [Swagger Editor](https://editor.swagger.io/).
50+
51+
To manually generate the OpenAPI specification, run the `generateOpenAPI` Gradle task found within
52+
the `documentation` group. The default output format is JSON.
53+
54+
The manually generated OpenAPI specification will be placed in the `/build/swagger` directory. The
55+
file will be named `opendcs-openapi.json`.
56+
57+
To change the output format from the default JSON to YAML, edit the `gradle.properties` file in the
58+
opendcs-rest-api project root. Change the `outputFormat` parameter from
59+
`JSON` to `YAML`. The file will be located in the same location as the JSON file, but with the YAML
60+
file extension. To revert back to the JSON format, change the parameter value back to `JSON`.
61+
The default output format is JSON if no format is specified.
62+
63+
To remove the generated OpenAPI specification, run the `./gradlew clean` Gradle task.
64+
65+
## Annotations
66+
67+
The OpenAPI specification is generated using annotations from the `io.swagger.core.v3:swagger-jaxrs2`
68+
library. These annotations are used to describe the API endpoints and request and response bodies.
69+
This is done by annotating the `Resource` endpoint classes and the appropriate DTO classes,
70+
located in the `org.opendcs.odcsapi.res` and `org.opendcs.odcsapi.beans` packages, respectively.
71+
72+
> [!TIP]
73+
> The available annotations and their usage can be found [here](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations).
74+
75+
### Examples
76+
77+
Example input data for the `POST` endpoints can be found in the
78+
[ResourceExamples](../opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/ResourceExamples.java)
79+
class, located in the `org.opendcs.odcsapi.res` package. These examples provide various levels of
80+
input data for the different endpoints, which can be useful for determining the expected input data
81+
for each endpoint.
82+
83+
To add new examples to the [ResourceExamples](../opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/ResourceExamples.java) class,
84+
add a `public static final String` constant with the JSON-formatted example data.
85+
**Remember to escape special characters in the JSON data.**
86+
87+
> [!IMPORTANT]
88+
> Due to a limitation of the Swagger annotations implementation, the examples must be String constants.
89+
> As a result, the examples must be placed within the [ResourceExamples](../opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/ResourceExamples.java)
90+
class as a JSON-formatted String constant and referenced by the `@ExampleObject` annotations located
91+
within the `@RequestBody` annotation of each relevant endpoint.

gradle.properties.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
#opendcs.db.password=
99

1010
### Needed for running the testcontainers
11-
org.gradle.jvmargs=-Xmx4096m
11+
org.gradle.jvmargs=-Xmx4096m
12+
13+
## For Generating the OpenDCS API Documentation
14+
outputFormat=JSON

gradle/libs.versions.toml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ cwms-db-codegen = { strictly = "7.0.0-OpenDCS" }
1212
cwms-ratings = "1.1.0"
1313
monolith = { strictly = "2.0.2" }
1414

15-
swagger = "2.2.24"
16-
swagger-ui = "5.17.14"
15+
swagger = "2.2.28"
16+
swagger-ui = "5.18.3"
17+
jackson = "2.18.2"
1718
glassfish-jaxb = "2.3.3"
1819
websocket = "1.1"
1920
selenium = "3.141.59"
@@ -58,17 +59,16 @@ jersey-hk2 = { module = "org.glassfish.jersey.inject:jersey-hk2", version.ref =
5859
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
5960
oracle-jdbc = { module = "com.oracle.database.jdbc:ojdbc8", version.ref = "oracle" }
6061
oracle-ucp = { module = "com.oracle.database.jdbc:ucp", version.ref = "oracle" }
61-
swagger-jaxrs2 = { module = "io.swagger.core.v3:swagger-jaxrs2-jakarta", version.ref = "swagger" }
62+
swagger-jaxrs2 = { module = "io.swagger.core.v3:swagger-jaxrs2", version.ref = "swagger" }
63+
jackson = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
64+
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
6265
jaxb-runtime = { module = "org.glassfish.jaxb:jaxb-runtime", version.ref = "glassfish-jaxb" }
6366
websocket = { module = "javax.websocket:javax.websocket-api", version.ref = "websocket" }
6467
selenium = { module = "org.seleniumhq.selenium:selenium-java", version.ref = "selenium" }
6568
nimbus = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus" }
6669
jwt = { module = "com.auth0:java-jwt", version.ref = "jwt" }
6770
auto-service = { module = "com.google.auto.service:auto-service", version.ref = "auto-service" }
6871

69-
# webjars
70-
swagger-ui = { module = "org.webjars:swagger-ui", version.ref = "swagger-ui" }
71-
7272
#Test Dependencies
7373
opendcs-integrationtesting-fixtures = { module = "org.opendcs:opendcs-integrationtesting-fixtures", version.ref = "opendcs"}
7474
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
@@ -98,6 +98,9 @@ org-flywaydb-flyway-core = { module = "org.flywaydb:flyway-core", version.ref =
9898
commons-lang = { module = "commons-lang:commons-lang", version.ref = "commons-lang" }
9999
jul-to-slf4j = { module = "org.slf4j:jul-to-slf4j", version = "2.0.16"}
100100

101+
# webjars
102+
swagger-ui = { module ="org.webjars:swagger-ui", version.ref = "swagger-ui" }
103+
101104
[bundles]
102105
tomcat = ["tomcat-embedded-core", "tomcat-embedded-jasper", "tomcat-jdbc"]
103106
jdbi = [ "org-jdbi3-core", "org-jdbi3-sqlobject", "org-jdbi3-postgres" ]

opendcs-integration-test/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ dependencies {
5050

5151
testRuntimeOnly(libs.oracle.jdbc)
5252
testRuntimeOnly(libs.byte.buddy)
53+
testRuntimeOnly(libs.jackson) // for swagger compatibility
54+
testRuntimeOnly(libs.jackson.annotations) // for swagger compatibility
5355
testRuntimeOnly(libs.postgresql)
5456
testRuntimeOnly(libs.byte.buddy)
5557
}

opendcs-rest-api/build.gradle

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ plugins {
1818
id "opendcs-rest-api.deps-conventions"
1919
id "opendcs-rest-api.publishing-conventions"
2020
id "war"
21+
id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.28"
2122
}
2223

2324
configurations {
@@ -43,9 +44,12 @@ dependencies {
4344
implementation(libs.json.jackson)
4445
implementation(libs.jersey.container.servlet)
4546
implementation(libs.nimbus)
47+
implementation(libs.swagger.jaxrs2)
4648
implementation(libs.jwt)
4749
implementation(libs.jersey.hk2)
4850
implementation(libs.slf4j.api)
51+
implementation(libs.jackson)
52+
runtimeOnly(libs.jackson.annotations) // updated for swagger compatibility
4953
runtimeOnly(libs.jaxb.runtime)
5054
runtimeOnly(libs.postgresql)
5155
webjars(libs.swagger.ui)
@@ -73,7 +77,7 @@ dependencies {
7377
task extractWebJars(type: Copy) {
7478
from zipTree(configurations.webjars.singleFile)
7579

76-
into file("${project.layout.getBuildDirectory().get().getAsFile().toString()}/resources/main/swaggerui")
80+
into file("src/main/webapp/swaggerui")
7781
includeEmptyDirs false
7882
eachFile {
7983
path -= ~/^.+?\/.+?\/.+?\/.+?\/.+?\//
@@ -116,3 +120,19 @@ if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
116120
}
117121
}
118122
}
123+
124+
// task to generate OpenAPI JSON file
125+
resolve {
126+
classpath = sourceSets.main.runtimeClasspath
127+
outputFileName = 'opendcs-openapi'
128+
outputFormat = providers.gradleProperty('outputFormat').getOrElse('JSON')
129+
prettyPrint = 'TRUE'
130+
resourcePackages = ['org.opendcs.odcsapi.res', 'org.opendcs.odcsapi.beans', 'org.opendcs.odcsapi.sec']
131+
outputDir = file('build/swagger/')
132+
}
133+
134+
tasks.register('generateOpenAPI') {
135+
group = "documentation"
136+
description = "Generates the Swagger OpenAPI JSON file without running the application"
137+
dependsOn('resolve')
138+
}

opendcs-rest-api/src/main/java/org/opendcs/odcsapi/beans/ApiAlgoParm.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,22 @@
1515

1616
package org.opendcs.odcsapi.beans;
1717

18+
import io.swagger.v3.oas.annotations.media.Schema;
19+
20+
@Schema(description = "Represents an algorithm parameter with a role name and parameter type.")
1821
public final class ApiAlgoParm
1922
{
20-
/** The role name -- must be unique within an algorithm. */
23+
/**
24+
* The role name -- must be unique within an algorithm.
25+
*/
26+
@Schema(description = "The role name of the parameter, must be unique within an algorithm.", example = "input1")
2127
private String roleName;
22-
23-
/** The parameter type -- one of the constant codes defined herein. */
28+
29+
/**
30+
* The parameter type.
31+
*/
32+
@Schema(description = "The type of the parameter. Can be input (i), output (o), or can embed extra information.",
33+
example = "i")
2434
private String parmType;
2535

2636
public String getRoleName()

opendcs-rest-api/src/main/java/org/opendcs/odcsapi/beans/ApiAlgorithm.java

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,58 @@
1919
import java.util.List;
2020
import java.util.Properties;
2121

22+
import io.swagger.v3.oas.annotations.media.Schema;
23+
24+
@Schema(description = "Represents an algorithm configuration with properties, parameters, and associated scripts.")
2225
public final class ApiAlgorithm
2326
{
24-
/** Surrogate key for this algorithm in the time series database. */
27+
/**
28+
* Surrogate key for this algorithm in the time series database.
29+
*/
30+
@Schema(description = "Unique numeric identifier for the algorithm.", example = "4")
2531
private Long algorithmId = null;
2632

27-
/** Name of this algorithm */
33+
/**
34+
* Name of this algorithm
35+
*/
36+
@Schema(description = "Name of the algorithm.", example = "ChooseOne")
2837
private String name = null;
2938

30-
/** Fully qualified Java class name to execut this algorithm. */
39+
/**
40+
* Fully qualified Java class name to execute this algorithm.
41+
*/
42+
@Schema(description = "Fully qualified Java class name used to execute the algorithm.",
43+
example = "decodes.tsdb.algo.ChooseOne")
3144
private String execClass = null;
3245

33-
/** Free form multi-line comment */
46+
/**
47+
* Free form multi-line comment
48+
*/
49+
@Schema(description = "Description or comments about the algorithm.", example = "Given two inputs, "
50+
+ "output the best one: If only one is present at the time-slice, output it. "
51+
+ "If one is outside the specified upper or lower limit (see properties) output the other. "
52+
+ "If both are acceptable, output the first one. Useful in situations where you have redundant sensors.")
3453
private String description = null;
3554

36-
/** Properties associated with this algorithm. */
55+
/**
56+
* Properties associated with this algorithm.
57+
*/
58+
@Schema(description = "Key-value pairs containing properties associated with the algorithm.")
3759
private Properties props = new Properties();
38-
39-
/** parameters to this algorithm */
60+
61+
/**
62+
* parameters to this algorithm
63+
*/
64+
@Schema(description = "List of parameters used by the algorithm.", implementation = ApiAlgoParm.class)
4065
private List<ApiAlgoParm> parms = new ArrayList<>();
4166

42-
/** For use in the editor -- the number of computations using this algo. */
67+
/**
68+
* For use in the editor -- the number of computations using this algo.
69+
*/
70+
@Schema(description = "Number of computations currently using this algorithm.", example = "1")
4371
private int numCompsUsing = 0;
44-
72+
73+
@Schema(description = "List of scripts associated with the algorithm.", implementation = ApiAlgorithmScript.class)
4574
private List<ApiAlgorithmScript> algoScripts = new ArrayList<>();
4675

4776
public Long getAlgorithmId()

opendcs-rest-api/src/main/java/org/opendcs/odcsapi/beans/ApiAlgorithmRef.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,31 @@
3131
*/
3232
package org.opendcs.odcsapi.beans;
3333

34+
import io.swagger.v3.oas.annotations.media.Schema;
3435

3536
/**
3637
* This class holds the info for an algorithm in the on-screen list.
3738
*/
39+
@Schema(description = "API Algorithm Reference DTO that holds the summary information of an algorithm.")
3840
public final class ApiAlgorithmRef
3941
{
42+
@Schema(description = "Unique ID of the algorithm.", example = "4")
4043
private Long algorithmId = null;
44+
45+
@Schema(description = "The name of the algorithm.", example = "Bridge Clearance")
4146
private String algorithmName = "";
47+
48+
@Schema(description = "Fully qualified Java execution class for the algorithm.",
49+
example = "decodes.tsdb.algo.BridgeClearance")
4250
private String execClass = "";
51+
52+
@Schema(description = "The number of computations using this algorithm.", example = "2")
4353
private int numCompsUsing = 0;
54+
55+
@Schema(description = "A brief description of the algorithm.",
56+
example = "Computes bridge clearance by subtracting water level from con")
4457
private String description = "";
45-
58+
4659
public Long getAlgorithmId()
4760
{
4861
return algorithmId;
@@ -83,6 +96,4 @@ public void setDescription(String description)
8396
{
8497
this.description = description;
8598
}
86-
87-
8899
}

0 commit comments

Comments
 (0)