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
5 changes: 5 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1920,6 +1920,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi-dev</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi-deployment</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: Assistant
description: Assistance for Quarkus
metadata:
hide-in-dev-ui: true
4 changes: 4 additions & 0 deletions extensions/smallrye-openapi/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi-dev</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-open-api-jaxrs</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ Map.<String, Supplier<String>> entry("YAML", openAPI::toYAML))
}
});

openApiDocumentProducer.produce(new OpenApiDocumentBuildItem(toOpenApiDocument(finalOpenAPI)));
openApiDocumentProducer.produce(new OpenApiDocumentBuildItem(toOpenApiDocument(finalOpenAPI), finalOpenAPI));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.Page;
import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig;
import io.quarkus.smallrye.openapi.runtime.dev.OpenApiJsonRpcService;
import io.quarkus.swaggerui.deployment.SwaggerUiConfig;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig;
Expand Down Expand Up @@ -43,7 +45,16 @@ public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRoo
.isJsonContent()
.icon("font-awesome-solid:file-code"));

cardPageBuildItem.addPage(Page.assistantPageBuilder()
.title("Generate clients")
.componentLink("qwc-openapi-generate-client.js"));

return cardPageBuildItem;
}

@BuildStep
JsonRPCProvidersBuildItem createJsonRPCService() {
return new JsonRPCProvidersBuildItem(OpenApiJsonRpcService.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { LitElement, html, css } from 'lit';
import { JsonRpc } from 'jsonrpc';
import '@vaadin/combo-box';
import '@vaadin/progress-bar';
import '@vaadin/button';
import '@vaadin/icon';
import '@qomponent/qui-code-block';
import { themeState } from 'theme-state';
import { observeState } from 'lit-element-state';
import { notifier } from 'notifier';

export class QwcOpenapiGenerateClient extends observeState(LitElement) {
jsonRpc = new JsonRpc(this);

static styles = css`
:host {
display: flex;
flex-direction: column;
padding-right: 10px;
padding-left: 10px;
}

.generatedcode {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 1em;
}

.progress {
margin-top: 1em;
}

.heading {
display: flex;
align-items: center;
justify-content: space-between;
}
.top {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.blurb {
font-size: 0.9em;
color: var(--lumo-secondary-text-color);
margin-bottom: 1em;
}

`;

static properties = {
_codemap: {state: true},
_selectedLanguage: {state: true},
_loading: {state: true},
};

constructor() {
super();
this._codemap = new Map();
this._selectedLanguage = null;
this._loading = false;

this.languages = [
{label: 'Java (Quarkus)', value: 'Java', mode: 'java', context: 'This code should be valid Quarkus Java code that use the quarkus-rest-client-jackson extension. It is very important to use the jakarta.ws namaspace when importing classes. Do NOT use the old javax.ws namespace. Use the org.eclipse.microprofile.rest.client.inject.RegisterRestClient annotation'},
{label: 'Kotlin (Quarkus)', value: 'Kotlin', mode: 'java', context: 'This code should be valid Quarkus Kotlin code that use the quarkus-rest-client-jackson extension. It is very important to use the jakarta.ws namaspace when importing classes. Do NOT use the old javax.ws namespace. Use the org.eclipse.microprofile.rest.client.inject.RegisterRestClient annotation'},
{label: 'Javascript', value: 'Javascript', mode: 'js', context: ''},
{label: 'TypeScript', value: 'Typecript', mode: 'ts', context: ''},
{label: 'C#', value: 'C#', mode: 'cs', context: ''},
{label: 'C++', value: 'C++', mode: 'cpp', context: ''},
{label: 'PHP', value: 'PHP', mode: 'php', context: ''},
{label: 'Python', value: 'Python', mode: 'py', context: ''},
{label: 'Rust', value: 'Rust', mode: 'rust', context: ''},
{label: 'Go', value: 'Golang', mode: 'go', context: ''}
];
}

render() {
return html`
<div class=top">
<vaadin-combo-box
label="Technology / Language"
.items="${this.languages}"
item-label-path="label"
item-value-path="value"
@value-changed="${this._languageSelected}">
</vaadin-combo-box>
<p class="blurb">
Generate client code based on the OpenAPI schema document produced by your Quarkus application at build time.
</p>
</div>

${this._loading ? html`
<div class="progress">
<label class="text-secondary" id="pblbl">Talking to AI...</label>
<vaadin-progress-bar indeterminate aria-labelledby="pblbl" aria-describedby="sublbl"></vaadin-progress-bar>
<span class="text-secondary text-xs" id="sublbl">This can take a while</span>
</div>
` : ''}

${this._selectedLanguage && this._codemap.has(this._selectedLanguage.value) ? this._renderClientResult(this._selectedLanguage) : ''}
`;
}

async _languageSelected(event) {
const selectedValue = event.detail.value;
const lang = this.languages.find(l => l.value === selectedValue);
if (!lang)
return;

this._selectedLanguage = lang;

if (!this._codemap.has(lang.value)) {
this._loading = true;
try {
const res = await this.jsonRpc.generateClient({language: lang.value, extraContext: lang.context});
if(res.result.code){
this._codemap.set(lang.value, res.result.code);
}else {
console.warn("code field not populated");
this._codemap.set(lang.value, JSON.stringify(res.result)); // fallback
}
} catch (e) {
console.error('Failed to generate code:', e);
notifier.showErrorMessage("Failed to generate code: " + e);
} finally {
this._loading = false;
}
}
}

_renderClientResult(lang) {
const code = this._codemap.get(lang.value);
return html`
<div class="generatedcode">
<div class="heading">${lang.label} code generated from the OpenAPI Schema with AI:
<vaadin-button theme="secondary" @click="${() => this._copyGeneratedContent(lang.value)}">
<vaadin-icon icon="font-awesome-solid:copy"></vaadin-icon>
Copy
</vaadin-button>
</div>
<qui-code-block
mode="${lang.mode}"
content="${code}"
theme="${themeState.theme.name}"
showLineNumbers>
</qui-code-block>
</div>
`;
}

_copyGeneratedContent(langName) {
if (this._codemap.has(langName)) {
const content = this._codemap.get(langName);
navigator.clipboard.writeText(content)
.then(() => notifier.showInfoMessage("Content copied to clipboard"))
.catch(err => notifier.showErrorMessage("Failed to copy content: " + err));
} else {
notifier.showWarningMessage("No content");
}
}
}

customElements.define('qwc-openapi-generate-client', QwcOpenapiGenerateClient);
1 change: 1 addition & 0 deletions extensions/smallrye-openapi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
<module>deployment</module>
<module>runtime</module>
<module>spi</module>
<module>runtime-dev</module>
</modules>
</project>
26 changes: 26 additions & 0 deletions extensions/smallrye-openapi/runtime-dev/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi-parent</artifactId>
<version>999-SNAPSHOT</version>
</parent>
<artifactId>quarkus-smallrye-openapi-dev</artifactId>
<packaging>jar</packaging>
<properties>
<exec.mainClass>io.quarkus.smallrye.openapi.dev.SmallryeOpenapiDev</exec.mainClass>
</properties>
<name>Quarkus - SmallRye OpenAPI - Runtime Dev mode</name>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>quarkus-assistant-dev</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.quarkus.smallrye.openapi.runtime.dev;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import jakarta.inject.Inject;

import io.quarkus.assistant.runtime.dev.Assistant;
import io.quarkus.smallrye.openapi.runtime.OpenApiDocumentService;
import io.smallrye.openapi.runtime.io.Format;

public class OpenApiJsonRpcService {

@Inject
Optional<Assistant> assistant;

@Inject
OpenApiDocumentService openApiDocumentService;

public String getOpenAPISchema() {
return new String(openApiDocumentService.getDocument(Format.JSON));
}

public CompletionStage<Map<String, String>> generateClient(String language, String extraContext) {
if (assistant.isPresent()) {
String schemaDocument = getOpenAPISchema();

return assistant.get().assistBuilder()
.userMessage(USER_MESSAGE)
.addVariable("schemaDocument", schemaDocument)
.addVariable("language", language)
.addVariable("extraContext", extraContext)
.assist();
}
return CompletableFuture.failedStage(new RuntimeException("Assistant is not available"));
}

private static final String USER_MESSAGE = """
Given the OpenAPI Schema document :
{{schemaDocument}}
Please generate a {{language}} Object that act as a client to all the operations in the schema.
This {{language}} code must be able to be called like this (pseudo code):
```
var stub = new ResourceNameHereClient();
var response = stub.doOperation(someparam);
```

Don't use ResourceNameHereClient as the name for the generated code (it's just an example). Derive a sensible name from the schema provided.
Your reponse should only contain one field called `code` that contains a value with only the {{language}} code, nothing else, no explanation, and do not put the code in backticks.
The {{language}} code must run and be valid.
Example response: {code: 'package foo.bar; // more code here'}

{{extraContext}}
""";
}
3 changes: 3 additions & 0 deletions extensions/smallrye-openapi/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
<capabilities>
<provides>io.quarkus.smallrye.openapi</provides>
</capabilities>
<conditionalDevDependencies>
<artifact>${project.groupId}:${project.artifactId}-dev:${project.version}</artifact>
</conditionalDevDependencies>
</configuration>
</plugin>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public OpenApiDocumentService(OASFilter autoSecurityFilter,
}
}

byte[] getDocument(Format format) {
public byte[] getDocument(Format format) {
if (format.equals(Format.JSON)) {
return documentHolder.getJsonDocument();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@

import io.quarkus.builder.item.SimpleBuildItem;
import io.smallrye.openapi.api.OpenApiDocument;
import io.smallrye.openapi.api.SmallRyeOpenAPI;

/**
* The final OpenAPI Document as generated by the Extension.
*/
public final class OpenApiDocumentBuildItem extends SimpleBuildItem {

private final OpenApiDocument openApiDocument;
private final SmallRyeOpenAPI smallRyeOpenAPI;

public OpenApiDocumentBuildItem(OpenApiDocument openApiDocument) {
public OpenApiDocumentBuildItem(OpenApiDocument openApiDocument, SmallRyeOpenAPI smallRyeOpenAPI) {
this.openApiDocument = openApiDocument;
this.smallRyeOpenAPI = smallRyeOpenAPI;
}

public OpenApiDocument getOpenApiDocument() {
return openApiDocument;
}

public SmallRyeOpenAPI getSmallRyeOpenAPI() {
return smallRyeOpenAPI;
}
}
Loading
Loading