Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: 2
registries:
nexus-releases:
type: maven-repository
url: https://repo.extendaretail.com/repository/maven-releases/
username: ${{ secrets.NEXUS_MAVEN_USERNAME }}
password: ${{ secrets.NEXUS_MAVEN_PASSWORD }}
updates:
- package-ecosystem: maven
registries:
- nexus-releases
directory: '/'
schedule:
interval: weekly
groups:
java:
patterns:
- "*"

- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
groups:
pipeline:
patterns:
- "*"
27 changes: 27 additions & 0 deletions .github/workflows/commit-msg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: commit-msg
on:
pull_request:
types:
- edited
- opened
- reopened
- synchronize

jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Lint pull request title
uses: extenda/actions/commitlint@v0
with:
message: ${{ github.event.pull_request.title }}

- name: Lint commit messages
if: always()
uses: extenda/actions/commitlint@v0
with:
relaxed: ${{ contains(job.status, 'success') }}
59 changes: 59 additions & 0 deletions .github/workflows/commit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Commit
on: push

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version-file: .java-version
cache: 'maven'

- name: Run tests
uses: extenda/actions/maven@v0
with:
args: verify
service-account-key: ${{ secrets.SECRET_AUTH }}

- name: Scan with SonarCloud
uses: extenda/actions/sonar-scanner@v0
with:
sonar-host: https://sonarcloud.io
sonar-scanner: maven
main-branch: master
service-account-key: ${{ secrets.SECRET_AUTH }}

# release:
# if: github.ref == 'refs/heads/master'
# runs-on: ubuntu-latest
# needs:
# - test
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
#
# - uses: actions/setup-java@v4
# with:
# distribution: 'temurin'
# java-version-file: .java-version
# cache: 'maven'
#
# - name: Create release
# uses: extenda/actions/conventional-release@v0
# id: release
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
#
# - name: Build release
# uses: extenda/actions/maven@v0
# with:
# args: deploy -DskipTests
# version: ${{ steps.release.outputs.version }}
# service-account-key: ${{ secrets.SECRET_AUTH }}
24 changes: 24 additions & 0 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: pre-commit
on: pull_request

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version-file: .java-version
cache: 'maven'

- name: Setup Python
uses: actions/setup-python@v5

- name: Run pre-commit
uses: pre-commit/actions@v3.0.1
with:
extra_args: --from-ref=${{ github.event.pull_request.base.sha }} --to-ref=${{ github.sha }}
1 change: 1 addition & 0 deletions .java-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
21
23 changes: 23 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
default_stages: [pre-commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4
hooks:
- id: remove-crlf
- id: remove-tabs
args: [ --whitespaces-count=2 ]
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 2.7.3
hooks:
- id: editorconfig-checker
- repo: https://github.com/extenda/pre-commit-hooks
rev: v0.11.0
hooks:
- id: google-java-formatter
- id: commitlint
stages: [commit-msg]
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

COPY target/lib/ lib/
COPY target/classes classes/
COPY target/test-classes test-classes/

ENTRYPOINT ["java", "-cp", "lib/*:classes:test-classes", "com.retailsvc.http.start.ServerLauncher"]
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# openapi-httpserver-java


# OpenAPI Server Library
A lightweight Java library for creating HTTP servers based on OpenAPI specifications.


## Overview
This library provides a simple way to create an HTTP server that implements OpenAPI specifications.


## Getting Started

### Prerequisites
- Java SDK 21 or later
- A serialization library, e.g. Gson or Jackson
- OpenAPI specification file in JSON format (`openapi.json`)


### Basic Usage
1. Create an OpenAPI specification file named `openapi.json` in your project resources.
2. Define your HTTP handlers by implementing the `HttpHandler` interface:
``` java
public class GetDataHandler implements HttpHandler {
// Implement your POST endpoint logic

// Example
try (exchange) {
byte[] bytes = """
{
"id": "some-id"
}""".getBytes();

try (var os = exchange.getResponseBody()) {
var responseHeaders = exchange.getResponseHeaders();
responseHeaders.add("content-type", "application/json");

exchange.sendResponseHeaders(HTTP_OK, bytes.length);

os.write(bytes);
}
}
}

public class PostDataHandler implements HttpHandler, GetRequestBody {
// Implement your POST endpoint logic
}
```

1. Initialize the server (using Gson in this example):
``` java
public class YourServerLauncher {
public static void main(String[] args) throws Exception {
final Gson gson = new Gson();

// Parse OpenAPI specification (or build your instance of OpenApi manually)
var specification = parseSpecification("openapi.json", s -> gson.fromJson(s, OpenApi.class));

// Register your handlers (operation-id -> handler)
Map<String, HttpHandler> handlers = new HashMap<>();
handlers.put("get-data", new GetDataHandler());
handlers.put("post-data", new PostDataHandler());

// Create JSON mapper (supports both arrays and objects)
JsonMapper mapper = new JsonMapper() {
@Override
public <T> T mapFrom(byte[] body) {
if (body.length > 0 && body[0] == '[') {
return (T) gson.fromJson(new String(body), List.class);
}
return (T) gson.fromJson(new String(body), Map.class);
}
};

ExceptionHandler exceptionHandler = Handlers.defaultExceptionHandler();

// Start the server
new OpenApiServer(specification, mapper, handlers, exceptionHandler);
}
}
```

## Features
- OpenAPI specification support
- Automatic request body parsing for JSON arrays and objects
- Custom HTTP handler support
- Built on Java's native `HttpServer` with Thread-Per-Request behaviour using Virtual Threads.
- Custom integration for JSON serialization/deserialization


## Handler Registration
Handlers are registered using string keys that correspond to your OpenAPI operation IDs.


## JSON Mapping
The library uses a flexible JSON mapping system that automatically detects and parses (using a mapper of choice):
- JSON arrays (`[...]`)
- JSON objects (`{...}`)

## Known limitations (not exhaustive..)

- OpenAPI refs are not supported yet.

## Notes
- The server uses a default configuration when initialized with a null parameter for the last argument
- Make sure your OpenAPI specification file (`openapi.json`) is accessible in your classpath
- Custom error handling and logging is provided through SLF4J

This library is designed to be simple to use while providing the essential features needed for creating OpenAPI-compliant HTTP servers in Java.
80 changes: 80 additions & 0 deletions acceptance/k6/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import http from 'k6/http';
import { group, check, sleep } from 'k6';

export const options = {
stages: [
{ duration: '30s', target: 10 },
{ duration: '30s', target: 100 },
{ duration: '1m', target: 100 },
{ duration: '10s', target: 0 },
],
};

const body = JSON.stringify({
id: 'some-id',
age: 42,
random: 'd5af5004-8b5a-4db6-838e-38be773eac34',
status: 'ERROR',
feelingGood: true,
aList: [ 'string', 'string' ],
anObject: {
id: 'some-id',
age: 42,
longNumber: 900,
nested: {
nestedValue: 43
}
},
aListOfObjects: [
{ value: 42 },
{ value: 43 }
],
aDate: '2025-03-02',
aDateTime: '2025-03-02T12:34:56Z'
});

const listOfObjects = JSON.stringify([
{ value: 42 },
{ value: 43 }
]);

export default function () {
group('get request', () => {
const url = 'http://localhost:8080/api/v1/data';
const res = http.get(url);

check(res, {
'is status 200': (r) => r.status === 200,
'is response in JSON format': (r) => r.headers['Content-Type'] === 'application/json',
'id exists in response': (r) => JSON.parse(r.body).hasOwnProperty('id'),
});
});

group('post request', () => {
const url = 'http://localhost:8080/api/v1/data';
const res = http.post(url, body, {
headers: {
'Content-Type':'application/json',
}
});

check(res, {
'is status 200': (r) => r.status === 200,
'is response in JSON format': (r) => r.headers['Content-Type'] === 'application/json',
'id exists in response': (r) => JSON.parse(r.body).hasOwnProperty('id'),
});
});

group('post list-of-objects request', () => {
const url = 'http://localhost:8080/api/v1/list/objects';
const res = http.post(url, listOfObjects, {
headers: {
'Content-Type':'application/json',
}
});

check(res, {
'is status 200': (r) => r.status === 200,
});
});
}
12 changes: 12 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
deploy:
resources:
limits:
cpus: "1.0"
memory: "1g"
Loading