Skip to content

Commit bede263

Browse files
author
gabriele.sisinna
committed
Export OpenAPI YAML to repo root
1 parent 7dff5d9 commit bede263

3 files changed

Lines changed: 299 additions & 0 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ After starting the app with `./gradlew bootRun`, open:
101101

102102
Swagger UI is generated from the controller and model annotations in the code.
103103

104+
To export the generated YAML to a file automatically, run:
105+
106+
```bash
107+
./gradlew exportOpenApiYaml
108+
```
109+
110+
That writes:
111+
112+
```text
113+
openapi.yaml
114+
```
115+
104116
## Run the tests
105117

106118
```bash

build.gradle

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,81 @@ dependencies {
2828
tasks.named('test') {
2929
useJUnitPlatform()
3030
}
31+
32+
tasks.register('exportOpenApiYaml') {
33+
group = 'documentation'
34+
description = 'Starts the app temporarily and exports the generated OpenAPI YAML to openapi.yaml in the project root.'
35+
dependsOn tasks.named('bootJar')
36+
37+
doLast {
38+
def outputFile = layout.projectDirectory.file('openapi.yaml').asFile
39+
def jarFile = tasks.named('bootJar').get().archiveFile.get().asFile
40+
def port = 9091
41+
def processOutput = new StringBuffer()
42+
43+
def process = new ProcessBuilder(
44+
'java',
45+
'-jar',
46+
jarFile.absolutePath,
47+
"--server.port=${port}",
48+
'--spring.main.banner-mode=off'
49+
)
50+
.directory(project.projectDir)
51+
.redirectErrorStream(true)
52+
.start()
53+
54+
def processLogThread = Thread.startDaemon('openapi-export-log') {
55+
try {
56+
process.inputStream.withReader('UTF-8') { reader ->
57+
char[] buffer = new char[1024]
58+
int read
59+
while ((read = reader.read(buffer)) != -1) {
60+
processOutput.append(buffer, 0, read)
61+
}
62+
}
63+
} catch (IOException ignored) {
64+
// The process stream closes during shutdown.
65+
}
66+
}
67+
68+
try {
69+
def yamlUrl = new URL("http://127.0.0.1:${port}/v3/api-docs.yaml")
70+
def deadline = System.currentTimeMillis() + 30000
71+
def exported = false
72+
73+
while (System.currentTimeMillis() < deadline) {
74+
if (!process.isAlive()) {
75+
throw new GradleException("Application exited before OpenAPI YAML export completed.\n${processOutput}")
76+
}
77+
78+
try {
79+
def connection = yamlUrl.openConnection()
80+
connection.connectTimeout = 1000
81+
connection.readTimeout = 1000
82+
connection.setRequestProperty('Accept', 'application/vnd.oai.openapi')
83+
84+
connection.inputStream.withCloseable { stream ->
85+
outputFile.setText(stream.getText('UTF-8'), 'UTF-8')
86+
}
87+
88+
exported = true
89+
break
90+
} catch (IOException ignored) {
91+
sleep(500)
92+
}
93+
}
94+
95+
if (!exported) {
96+
throw new GradleException("Timed out waiting for ${yamlUrl}.\n${processOutput}")
97+
}
98+
99+
logger.lifecycle("OpenAPI YAML exported to ${outputFile}")
100+
} finally {
101+
process.destroy()
102+
if (!process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS)) {
103+
process.destroyForcibly()
104+
}
105+
processLogThread.join(1000)
106+
}
107+
}
108+
}

openapi.yaml

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Java Spring Learning API
4+
description: "Learning project for core Java concepts, Spring Boot, Swagger UI,\
5+
\ and OpenAPI YAML."
6+
contact:
7+
name: Learning Repo
8+
url: https://example.com
9+
license:
10+
name: For learning use
11+
url: https://opensource.org/licenses/MIT
12+
version: v1
13+
servers:
14+
- url: http://127.0.0.1:9091
15+
description: Generated server url
16+
tags:
17+
- name: Students
18+
description: "Student examples for learning Spring Boot controllers, services, and\
19+
\ dependency injection."
20+
paths:
21+
/api/students:
22+
get:
23+
tags:
24+
- Students
25+
summary: List students
26+
description: Returns the in-memory student list used by this learning project.
27+
operationId: findAll
28+
responses:
29+
"404":
30+
description: Not Found
31+
content:
32+
'*/*':
33+
schema:
34+
$ref: "#/components/schemas/ErrorResponse"
35+
"400":
36+
description: Bad Request
37+
content:
38+
'*/*':
39+
schema:
40+
$ref: "#/components/schemas/ErrorResponse"
41+
"200":
42+
description: Students loaded successfully
43+
content:
44+
'*/*':
45+
schema:
46+
type: array
47+
items:
48+
$ref: "#/components/schemas/StudentResponse"
49+
post:
50+
tags:
51+
- Students
52+
summary: Create a student
53+
description: Creates a new student from a request model and returns the response
54+
model.
55+
operationId: create
56+
requestBody:
57+
content:
58+
application/json:
59+
schema:
60+
$ref: "#/components/schemas/CreateStudentRequest"
61+
required: true
62+
responses:
63+
"404":
64+
description: Not Found
65+
content:
66+
'*/*':
67+
schema:
68+
$ref: "#/components/schemas/ErrorResponse"
69+
"400":
70+
description: Invalid request data
71+
content:
72+
'*/*':
73+
schema:
74+
$ref: "#/components/schemas/ErrorResponse"
75+
"201":
76+
description: Student created
77+
content:
78+
'*/*':
79+
schema:
80+
$ref: "#/components/schemas/StudentResponse"
81+
/api/students/{id}:
82+
get:
83+
tags:
84+
- Students
85+
summary: Get a student by id
86+
description: Returns one student or a 404 error if the student does not exist.
87+
operationId: findById
88+
parameters:
89+
- name: id
90+
in: path
91+
required: true
92+
schema:
93+
type: integer
94+
format: int64
95+
responses:
96+
"404":
97+
description: Student not found
98+
content:
99+
'*/*':
100+
schema:
101+
$ref: "#/components/schemas/ErrorResponse"
102+
"400":
103+
description: Bad Request
104+
content:
105+
'*/*':
106+
schema:
107+
$ref: "#/components/schemas/ErrorResponse"
108+
"200":
109+
description: Student found
110+
content:
111+
'*/*':
112+
schema:
113+
$ref: "#/components/schemas/StudentResponse"
114+
components:
115+
schemas:
116+
ErrorResponse:
117+
type: object
118+
description: Error payload returned when the API rejects a request.
119+
properties:
120+
message:
121+
type: string
122+
hint:
123+
type: string
124+
CreateStudentRequest:
125+
type: object
126+
description: Request body used to create a new student.
127+
properties:
128+
name:
129+
type: string
130+
description: Student name
131+
example: Lina
132+
age:
133+
type: integer
134+
format: int32
135+
description: Student age
136+
example: 23
137+
active:
138+
type: boolean
139+
description: Whether the student starts as active
140+
example: true
141+
subjects:
142+
type: array
143+
description: Topics that the student is studying
144+
example:
145+
- annotations
146+
- controller
147+
items:
148+
type: string
149+
scores:
150+
type: object
151+
additionalProperties:
152+
type: integer
153+
format: int32
154+
description: Example scores by subject
155+
example:
156+
java: 91
157+
spring: 89
158+
StudentResponse:
159+
type: object
160+
description: Response returned by the student API.
161+
properties:
162+
id:
163+
type: integer
164+
format: int64
165+
description: Generated student id
166+
example: 3
167+
name:
168+
type: string
169+
description: Student name
170+
example: Lina
171+
age:
172+
type: integer
173+
format: int32
174+
description: Student age
175+
example: 23
176+
active:
177+
type: boolean
178+
description: Whether the student is active
179+
example: true
180+
level:
181+
type: string
182+
description: Derived level based on age
183+
example: adult
184+
subjects:
185+
type: array
186+
description: Topics being studied
187+
example:
188+
- annotations
189+
- controller
190+
items:
191+
type: string
192+
scores:
193+
type: object
194+
additionalProperties:
195+
type: integer
196+
format: int32
197+
description: Scores by subject
198+
example:
199+
java: 91
200+
spring: 89
201+
totalScore:
202+
type: integer
203+
format: int32
204+
description: Total of all score values
205+
example: 180
206+
subjectSummary:
207+
type: string
208+
description: Readable summary of the subjects list
209+
example: "annotations, controller"

0 commit comments

Comments
 (0)