Skip to content

Commit 565e14b

Browse files
authored
fix: First iteration (#1)
1 parent 7f8d881 commit 565e14b

62 files changed

Lines changed: 3543 additions & 0 deletions

File tree

Some content is hidden

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

.github/dependabot.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
version: 2
2+
registries:
3+
nexus-releases:
4+
type: maven-repository
5+
url: https://repo.extendaretail.com/repository/maven-releases/
6+
username: ${{ secrets.NEXUS_MAVEN_USERNAME }}
7+
password: ${{ secrets.NEXUS_MAVEN_PASSWORD }}
8+
updates:
9+
- package-ecosystem: maven
10+
registries:
11+
- nexus-releases
12+
directory: '/'
13+
schedule:
14+
interval: weekly
15+
groups:
16+
java:
17+
patterns:
18+
- "*"
19+
20+
- package-ecosystem: github-actions
21+
directory: '/'
22+
schedule:
23+
interval: weekly
24+
groups:
25+
pipeline:
26+
patterns:
27+
- "*"

.github/workflows/commit-msg.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: commit-msg
2+
on:
3+
pull_request:
4+
types:
5+
- edited
6+
- opened
7+
- reopened
8+
- synchronize
9+
10+
jobs:
11+
commitlint:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0
17+
18+
- name: Lint pull request title
19+
uses: extenda/actions/commitlint@v0
20+
with:
21+
message: ${{ github.event.pull_request.title }}
22+
23+
- name: Lint commit messages
24+
if: always()
25+
uses: extenda/actions/commitlint@v0
26+
with:
27+
relaxed: ${{ contains(job.status, 'success') }}

.github/workflows/commit.yaml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Commit
2+
on: push
3+
4+
jobs:
5+
test:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v4
9+
with:
10+
fetch-depth: 0
11+
12+
- uses: actions/setup-java@v4
13+
with:
14+
distribution: 'temurin'
15+
java-version-file: .java-version
16+
cache: 'maven'
17+
18+
- name: Run tests
19+
uses: extenda/actions/maven@v0
20+
with:
21+
args: verify
22+
service-account-key: ${{ secrets.SECRET_AUTH }}
23+
24+
- name: Scan with SonarCloud
25+
uses: extenda/actions/sonar-scanner@v0
26+
with:
27+
sonar-host: https://sonarcloud.io
28+
sonar-scanner: maven
29+
main-branch: master
30+
service-account-key: ${{ secrets.SECRET_AUTH }}
31+
32+
# release:
33+
# if: github.ref == 'refs/heads/master'
34+
# runs-on: ubuntu-latest
35+
# needs:
36+
# - test
37+
# steps:
38+
# - uses: actions/checkout@v4
39+
# with:
40+
# fetch-depth: 0
41+
#
42+
# - uses: actions/setup-java@v4
43+
# with:
44+
# distribution: 'temurin'
45+
# java-version-file: .java-version
46+
# cache: 'maven'
47+
#
48+
# - name: Create release
49+
# uses: extenda/actions/conventional-release@v0
50+
# id: release
51+
# env:
52+
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53+
#
54+
# - name: Build release
55+
# uses: extenda/actions/maven@v0
56+
# with:
57+
# args: deploy -DskipTests
58+
# version: ${{ steps.release.outputs.version }}
59+
# service-account-key: ${{ secrets.SECRET_AUTH }}

.github/workflows/pre-commit.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: pre-commit
2+
on: pull_request
3+
4+
jobs:
5+
pre-commit:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v4
9+
with:
10+
fetch-depth: 0
11+
12+
- uses: actions/setup-java@v4
13+
with:
14+
distribution: 'temurin'
15+
java-version-file: .java-version
16+
cache: 'maven'
17+
18+
- name: Setup Python
19+
uses: actions/setup-python@v5
20+
21+
- name: Run pre-commit
22+
uses: pre-commit/actions@v3.0.1
23+
with:
24+
extra_args: --from-ref=${{ github.event.pull_request.base.sha }} --to-ref=${{ github.sha }}

.java-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
21

.pre-commit-config.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
default_stages: [pre-commit]
2+
repos:
3+
- repo: https://github.com/pre-commit/pre-commit-hooks
4+
rev: v4.5.0
5+
hooks:
6+
- id: end-of-file-fixer
7+
- id: trailing-whitespace
8+
- repo: https://github.com/Lucas-C/pre-commit-hooks
9+
rev: v1.5.4
10+
hooks:
11+
- id: remove-crlf
12+
- id: remove-tabs
13+
args: [ --whitespaces-count=2 ]
14+
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
15+
rev: 2.7.3
16+
hooks:
17+
- id: editorconfig-checker
18+
- repo: https://github.com/extenda/pre-commit-hooks
19+
rev: v0.11.0
20+
hooks:
21+
- id: google-java-formatter
22+
- id: commitlint
23+
stages: [commit-msg]

Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM eclipse-temurin:21-jre-alpine
2+
3+
WORKDIR /app
4+
5+
COPY target/lib/ lib/
6+
COPY target/classes classes/
7+
COPY target/test-classes test-classes/
8+
9+
ENTRYPOINT ["java", "-cp", "lib/*:classes:test-classes", "com.retailsvc.http.start.ServerLauncher"]

README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# openapi-httpserver-java
2+
3+
4+
# OpenAPI Server Library
5+
A lightweight Java library for creating HTTP servers based on OpenAPI specifications.
6+
7+
8+
## Overview
9+
This library provides a simple way to create an HTTP server that implements OpenAPI specifications.
10+
11+
12+
## Getting Started
13+
14+
### Prerequisites
15+
- Java SDK 21 or later
16+
- A serialization library, e.g. Gson or Jackson
17+
- OpenAPI specification file in JSON format (`openapi.json`)
18+
19+
20+
### Basic Usage
21+
1. Create an OpenAPI specification file named `openapi.json` in your project resources.
22+
2. Define your HTTP handlers by implementing the `HttpHandler` interface:
23+
``` java
24+
public class GetDataHandler implements HttpHandler {
25+
// Implement your POST endpoint logic
26+
27+
// Example
28+
try (exchange) {
29+
byte[] bytes = """
30+
{
31+
"id": "some-id"
32+
}""".getBytes();
33+
34+
try (var os = exchange.getResponseBody()) {
35+
var responseHeaders = exchange.getResponseHeaders();
36+
responseHeaders.add("content-type", "application/json");
37+
38+
exchange.sendResponseHeaders(HTTP_OK, bytes.length);
39+
40+
os.write(bytes);
41+
}
42+
}
43+
}
44+
45+
public class PostDataHandler implements HttpHandler, GetRequestBody {
46+
// Implement your POST endpoint logic
47+
}
48+
```
49+
50+
1. Initialize the server (using Gson in this example):
51+
``` java
52+
public class YourServerLauncher {
53+
public static void main(String[] args) throws Exception {
54+
final Gson gson = new Gson();
55+
56+
// Parse OpenAPI specification (or build your instance of OpenApi manually)
57+
var specification = parseSpecification("openapi.json", s -> gson.fromJson(s, OpenApi.class));
58+
59+
// Register your handlers (operation-id -> handler)
60+
Map<String, HttpHandler> handlers = new HashMap<>();
61+
handlers.put("get-data", new GetDataHandler());
62+
handlers.put("post-data", new PostDataHandler());
63+
64+
// Create JSON mapper (supports both arrays and objects)
65+
JsonMapper mapper = new JsonMapper() {
66+
@Override
67+
public <T> T mapFrom(byte[] body) {
68+
if (body.length > 0 && body[0] == '[') {
69+
return (T) gson.fromJson(new String(body), List.class);
70+
}
71+
return (T) gson.fromJson(new String(body), Map.class);
72+
}
73+
};
74+
75+
ExceptionHandler exceptionHandler = Handlers.defaultExceptionHandler();
76+
77+
// Start the server
78+
new OpenApiServer(specification, mapper, handlers, exceptionHandler);
79+
}
80+
}
81+
```
82+
83+
## Features
84+
- OpenAPI specification support
85+
- Automatic request body parsing for JSON arrays and objects
86+
- Custom HTTP handler support
87+
- Built on Java's native `HttpServer` with Thread-Per-Request behaviour using Virtual Threads.
88+
- Custom integration for JSON serialization/deserialization
89+
90+
91+
## Handler Registration
92+
Handlers are registered using string keys that correspond to your OpenAPI operation IDs.
93+
94+
95+
## JSON Mapping
96+
The library uses a flexible JSON mapping system that automatically detects and parses (using a mapper of choice):
97+
- JSON arrays (`[...]`)
98+
- JSON objects (`{...}`)
99+
100+
## Known limitations (not exhaustive..)
101+
102+
- OpenAPI refs are not supported yet.
103+
104+
## Notes
105+
- The server uses a default configuration when initialized with a null parameter for the last argument
106+
- Make sure your OpenAPI specification file (`openapi.json`) is accessible in your classpath
107+
- Custom error handling and logging is provided through SLF4J
108+
109+
This library is designed to be simple to use while providing the essential features needed for creating OpenAPI-compliant HTTP servers in Java.

acceptance/k6/script.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import http from 'k6/http';
2+
import { group, check, sleep } from 'k6';
3+
4+
export const options = {
5+
stages: [
6+
{ duration: '30s', target: 10 },
7+
{ duration: '30s', target: 100 },
8+
{ duration: '1m', target: 100 },
9+
{ duration: '10s', target: 0 },
10+
],
11+
};
12+
13+
const body = JSON.stringify({
14+
id: 'some-id',
15+
age: 42,
16+
random: 'd5af5004-8b5a-4db6-838e-38be773eac34',
17+
status: 'ERROR',
18+
feelingGood: true,
19+
aList: [ 'string', 'string' ],
20+
anObject: {
21+
id: 'some-id',
22+
age: 42,
23+
longNumber: 900,
24+
nested: {
25+
nestedValue: 43
26+
}
27+
},
28+
aListOfObjects: [
29+
{ value: 42 },
30+
{ value: 43 }
31+
],
32+
aDate: '2025-03-02',
33+
aDateTime: '2025-03-02T12:34:56Z'
34+
});
35+
36+
const listOfObjects = JSON.stringify([
37+
{ value: 42 },
38+
{ value: 43 }
39+
]);
40+
41+
export default function () {
42+
group('get request', () => {
43+
const url = 'http://localhost:8080/api/v1/data';
44+
const res = http.get(url);
45+
46+
check(res, {
47+
'is status 200': (r) => r.status === 200,
48+
'is response in JSON format': (r) => r.headers['Content-Type'] === 'application/json',
49+
'id exists in response': (r) => JSON.parse(r.body).hasOwnProperty('id'),
50+
});
51+
});
52+
53+
group('post request', () => {
54+
const url = 'http://localhost:8080/api/v1/data';
55+
const res = http.post(url, body, {
56+
headers: {
57+
'Content-Type':'application/json',
58+
}
59+
});
60+
61+
check(res, {
62+
'is status 200': (r) => r.status === 200,
63+
'is response in JSON format': (r) => r.headers['Content-Type'] === 'application/json',
64+
'id exists in response': (r) => JSON.parse(r.body).hasOwnProperty('id'),
65+
});
66+
});
67+
68+
group('post list-of-objects request', () => {
69+
const url = 'http://localhost:8080/api/v1/list/objects';
70+
const res = http.post(url, listOfObjects, {
71+
headers: {
72+
'Content-Type':'application/json',
73+
}
74+
});
75+
76+
check(res, {
77+
'is status 200': (r) => r.status === 200,
78+
});
79+
});
80+
}

docker-compose.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
app:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
ports:
7+
- "8080:8080"
8+
deploy:
9+
resources:
10+
limits:
11+
cpus: "1.0"
12+
memory: "1g"

0 commit comments

Comments
 (0)