Skip to content
Open
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
1 change: 1 addition & 0 deletions examples/express/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "express-example",
"type": "module",
"dependencies": {
"@wesleytodd/openapi": "^1.1.0",
"express": "^5.2.0"
Expand Down
152 changes: 150 additions & 2 deletions examples/express/src/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const express = require('express');
const openapi = require('@wesleytodd/openapi');
// @ts-check

import express from 'express';
import openapi from '@wesleytodd/openapi';

const app = express();
const port = 8080;

Expand All @@ -12,6 +15,79 @@ const API_VERSION_HEADER_SCHEMA = {
type: 'string'
}
};
const BAD_REQUEST_RESPONSE = {
'application/problem+json': {
schema: {
type: 'object',
properties: {
status: {
type: 'integer'
},
title: {
type: 'string'
},
detail: {
type: 'string'
},
errors: {
type: 'array',
items: {
type: 'object',
properties: {
in: {
type: 'string'
},
location: {
type: 'string'
},
code: {
type: 'string'
},
detail: {
type: 'string'
},
index: {
type: 'integer'
}
}
}
}
}
}
}
}

/**
* @typedef {{ in: "body"|"query", detail: string, location?: string, index?: number, code?: string }} BadRequestError
*/

class BadRequestResponseWithErrors extends Error {
/**
* @param {string} title
* @param {string} detail
* @param {Array<BadRequestError>} errors
*/
constructor(title, detail, errors) {
super();

this.title = title;
this.detail = detail;
this.errors = errors;
}
toJSON() {
const {
title,
detail,
errors
} = this;
return {
status: 400,
title,
detail,
errors,
};
}
}

const oapi = openapi({
openapi: '3.0.0',
Expand Down Expand Up @@ -89,6 +165,78 @@ app.get('/', oapi.path({
res.send('Dit is de landing pagina van deze API');
});

app.post('/input', oapi.path({
description: 'Input method',
tags: [
'input'
],
operationId: 'postInput',
responses: {
200: {
description: 'Input',
content: {
'text/plain': {}
},
headers: {
[API_VERSION_HEADER_NAME]: oapi.headers(API_VERSION_HEADER_SCHEMA_NAME)
}
},
400: {
description: 'Invalid input',
content: BAD_REQUEST_RESPONSE,
headers: {
[API_VERSION_HEADER_NAME]: oapi.headers(API_VERSION_HEADER_SCHEMA_NAME)
}
}
}
}), (req, res) => {
/**
* @type {Array<BadRequestError>}
*/
const errors = [];
const queryInput = req.query.queryInput;
if (!queryInput) {
errors.push({
in: 'query',
detail: 'Missing query parameter',
location: 'queryInput',
code: 'input.query.missing',
})
} else {
let params;
if (typeof queryInput === 'string') {
params = [queryInput];
} else {
params = queryInput;
}
for (const [index, param] of params.entries()) {
if (param !== '42') {
errors.push({
in: 'query',
detail: 'Invalid query parameter. Expected 42, but was ' + param,
location: 'queryInput',
index,
code: 'input.query.invalid',
});
}
}
}

if (errors.length > 0) {
throw new BadRequestResponseWithErrors('title', 'detail', errors);
}

res.send('Succesvolle response');
});

app.use((err, req, res, next) => {
if (err instanceof BadRequestResponseWithErrors) {
return res.status(400).json(err.toJSON());
}

next();
});

app.listen(port, () => {
console.log(`Example express app beschikbaar op poort ${port}`);
});
17 changes: 15 additions & 2 deletions examples/quarkus/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.21.1</quarkus.platform.version>
<quarkus.platform.version>3.34.3</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.5.2</surefire-plugin.version>
</properties>
Expand Down Expand Up @@ -44,7 +44,20 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.resteasy-problem</groupId>
<artifactId>quarkus-resteasy-problem</artifactId>
<version>3.32.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
84 changes: 84 additions & 0 deletions examples/quarkus/src/main/java/org/acme/BadRequestProblem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.acme;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.quarkiverse.resteasy.problem.HttpProblem;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

import java.util.List;

public class BadRequestProblem extends HttpProblem {
Comment thread
TimvdLippe marked this conversation as resolved.
protected BadRequestProblem(String message, List<BadRequestDetails> errors) {
super(
builder()
.withTitle("Bad hello request")
.withStatus(Response.Status.BAD_REQUEST)
.withDetail(message)
.with("errors", errors));
}

@Schema(name = "errors", description = "All bad request errors")
public List<BadRequestDetails> errors;

@Schema(
name = "BadRequestDetails",
description = "Request details according to API Design Rules")
public static class BadRequestDetails {
@Schema(description = "Where the error occurs")
public BadRequestLocation in;

@Schema(description = "Human-readable message describing the violation")
public String detail;

@Schema(nullable = true, description = "A locator for the offending value")
@JsonInclude(value = JsonInclude.Include.NON_NULL)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zo zorg je ervoor dat als er geen location is, dat als je null hierin stopt hij niet "location": null in de JSON response zet, maar hem gewoon weglaat.

public String location;

@Schema(
nullable = true,
description =
"A zero-based index position when multiple query parameters have the same name")
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public Integer index;

@Schema(
nullable = true,
description = "A short, stable machine-readable code as a rule identifier")
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public String code;

private BadRequestDetails(
BadRequestLocation in, String detail, String location, Integer index, String code) {
this.in = in;
this.detail = detail;
this.location = location;
this.index = index;
this.code = code;
}

public static BadRequestDetails forSingleQuery(
String queryParameterName, String detail, String code) {
return new BadRequestDetails(
BadRequestLocation.Query, detail, queryParameterName, null, code);
}

public static BadRequestDetails forIndexedQuery(
String queryParameterName, String detail, Integer index, String code) {
return new BadRequestDetails(
BadRequestLocation.Query, detail, queryParameterName, index, code);
}

public static BadRequestDetails forBody(String location, String detail, String code) {
return new BadRequestDetails(BadRequestLocation.Body, detail, location, null, code);
}
}

public enum BadRequestLocation {
@JsonProperty("body")
Body,
@JsonProperty("query")
Query,
;
}
}
75 changes: 65 additions & 10 deletions examples/quarkus/src/main/java/org/acme/GreetingResource.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,90 @@
package org.acme;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.headers.Header;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.RestQuery;
import org.jboss.resteasy.reactive.Separator;

import java.util.ArrayList;
import java.util.List;

@Path("/hello")
@Tag(name = "greeting")
public class GreetingResource {

@GET
@Operation(
description = "Hello world endpoint"
)
@Operation(description = "Hello world endpoint")
@APIResponse(
responseCode = "200",
content = @Content(mediaType = MediaType.TEXT_PLAIN),
headers = {
@Header(
name = OpenApiConstants.API_VERSION_HEADER_NAME,
schema = @Schema(implementation = String.class)
)
}
)
@Header(
name = OpenApiConstants.API_VERSION_HEADER_NAME,
schema = @Schema(implementation = String.class))
})
public String hello() {
return "Hello from Quarkus REST";
}

@POST
@Operation(description = "Test bad request with query parameter")
@APIResponse(
responseCode = "200",
content = @Content(mediaType = MediaType.TEXT_PLAIN),
headers = {
@Header(
name = OpenApiConstants.API_VERSION_HEADER_NAME,
schema = @Schema(implementation = String.class))
})
@APIResponse(
responseCode = "400",
description = "Bad request response",
content =
@Content(
mediaType = "application/problem+json",
schema = @Schema(implementation = BadRequestProblem.class)))
public String withInput(
@RestQuery("queryInput") @Separator(",") List<String> queryInputParam,
PostRequestyBody body) {
var errors = new ArrayList<BadRequestProblem.BadRequestDetails>();
if (queryInputParam.isEmpty()) {
errors.add(
BadRequestProblem.BadRequestDetails.forSingleQuery(
"queryInput",
"Missing required query parameter",
"input.query.missing"));
}
var queryParams = queryInputParam.iterator();
for (var index = 0; queryParams.hasNext(); index++) {
var param = queryParams.next();
if (!param.equals("42")) {
errors.add(
BadRequestProblem.BadRequestDetails.forIndexedQuery(
"queryInput",
"Value for query parameter is not 42, but was %s".formatted(param),
index,
"input.query.invalid"));
}
}
if (!body.field.equals("foo")) {
errors.add(
BadRequestProblem.BadRequestDetails.forBody(
"field",
"Value for field in body should be foo, but was %s"
.formatted(body.field),
"input.body.field.invalid"));
}
if (!errors.isEmpty()) {
throw new BadRequestProblem("Failed to process request input", errors);
}
return "Successful response";
}
}
Loading
Loading