From 49689e36b52e9f7e976b42178d2075700778971b Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Tue, 13 Feb 2024 21:45:35 +0100 Subject: [PATCH] GH-146 Simplify OpenApi plugin config --- .../openapi/plugin/test/JavalinTest.java | 159 +++++++++--------- .../openapi/plugin/OpenApiConfiguration.kt | 46 ++++- .../main/kotlin/io/javalin/openapi/Info.kt | 29 ++++ .../kotlin/io/javalin/openapi/Security.kt | 40 +++-- .../main/kotlin/io/javalin/openapi/Server.kt | 17 +- 5 files changed, 194 insertions(+), 97 deletions(-) diff --git a/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java b/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java index c2f4bd6..a4cf680 100644 --- a/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java +++ b/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java @@ -4,44 +4,33 @@ import io.javalin.Javalin; import io.javalin.http.Context; import io.javalin.http.Handler; -import io.javalin.openapi.ApiKeyAuth; -import io.javalin.openapi.BasicAuth; -import io.javalin.openapi.BearerAuth; -import io.javalin.openapi.ClientCredentials; -import io.javalin.openapi.CookieAuth; import io.javalin.openapi.Custom; import io.javalin.openapi.CustomAnnotation; import io.javalin.openapi.HttpMethod; -import io.javalin.openapi.ImplicitFlow; import io.javalin.openapi.JsonSchema; import io.javalin.openapi.JsonSchemaLoader; import io.javalin.openapi.JsonSchemaResource; -import io.javalin.openapi.OAuth2; import io.javalin.openapi.OneOf; import io.javalin.openapi.OpenApi; import io.javalin.openapi.OpenApiByFields; import io.javalin.openapi.OpenApiCallback; -import io.javalin.openapi.OpenApiContact; import io.javalin.openapi.OpenApiContent; import io.javalin.openapi.OpenApiContentProperty; import io.javalin.openapi.OpenApiDescription; import io.javalin.openapi.OpenApiExample; import io.javalin.openapi.OpenApiExampleProperty; import io.javalin.openapi.OpenApiIgnore; -import io.javalin.openapi.OpenApiLicense; import io.javalin.openapi.OpenApiName; import io.javalin.openapi.OpenApiParam; import io.javalin.openapi.OpenApiPropertyType; import io.javalin.openapi.OpenApiRequestBody; import io.javalin.openapi.OpenApiResponse; import io.javalin.openapi.OpenApiSecurity; -import io.javalin.openapi.OpenID; -import io.javalin.openapi.Security; import io.javalin.openapi.Visibility; import io.javalin.openapi.plugin.OpenApiPlugin; -import io.javalin.openapi.plugin.SecurityComponentConfiguration; import io.javalin.openapi.plugin.redoc.ReDocPlugin; import io.javalin.openapi.plugin.swagger.SwaggerPlugin; +import io.javalin.security.RouteRole; import lombok.Data; import org.bson.types.ObjectId; import org.jetbrains.annotations.NotNull; @@ -66,64 +55,69 @@ */ public final class JavalinTest implements Handler { + enum Rules implements RouteRole { + ANONYMOUS, + USER, + } + /** * Runs server on localhost:8080 * * @param args args */ public static void main(String[] args) { - Javalin.create(config -> { + Javalin.createAndStart(config -> { // config.routing.contextPath = "/custom"; String deprecatedDocsPath = "/api/openapi.json"; // by default it's /openapi config.registerPlugin(new OpenApiPlugin(openApiConfig -> openApiConfig .withDocumentationPath(deprecatedDocsPath) - .withDefinitionConfiguration((version, definition) -> definition - .withOpenApiInfo((openApiInfo) -> { - OpenApiContact openApiContact = new OpenApiContact(); - openApiContact.setName("API Support"); - openApiContact.setUrl("https://www.example.com/support"); - openApiContact.setEmail("support@example.com"); - - OpenApiLicense openApiLicense = new OpenApiLicense(); - openApiLicense.setName("Apache 2.0"); - openApiLicense.setIdentifier("Apache-2.0"); - - openApiInfo.setDescription("App description goes right here"); - openApiInfo.setTermsOfService("https://example.com/tos"); - openApiInfo.setContact(openApiContact); - openApiInfo.setLicense(openApiLicense); - }) - .withServer((openApiServer) -> { - openApiServer.setUrl(("http://localhost:{port}{basePath}/" + version + "/")); - openApiServer.setDescription("Server description goes here"); - openApiServer.addVariable("port", "8080", new String[] { "7070", "8080" }, "Port of the server"); - openApiServer.addVariable("basePath", "", new String[] { "", "v1" }, "Base path of the server"); - }) - // Based on official example: https://swagger.io/docs/specification/authentication/oauth2/ - .withSecurity(new SecurityComponentConfiguration() - .withSecurityScheme("BasicAuth", new BasicAuth()) - .withSecurityScheme("BearerAuth", new BearerAuth()) - .withSecurityScheme("ApiKeyAuth", new ApiKeyAuth()) - .withSecurityScheme("CookieAuth", new CookieAuth("JSESSIONID")) - .withSecurityScheme("OpenID", new OpenID("https://example.com/.well-known/openid-configuration")) - .withSecurityScheme("OAuth2", new OAuth2("This API uses OAuth 2 with the implicit grant flow.") - .withFlow(new ImplicitFlow("https://api.example.com/oauth2/authorize") - .withScope("read_pets", "read your pets") - .withScope("write_pets", "modify pets in your account") - ) - .withFlow(new ClientCredentials("https://api.example.com/credentials/authorize")) + .withRoles(Rules.ANONYMOUS) + .withDefinitionConfiguration((version, openApiDefinition) -> + openApiDefinition + .withInfo(openApiInfo -> + openApiInfo + .description("App description goes right here") + .termsOfService("https://example.com/tos") + .contact("API Support", "https://www.example.com/support", "support@example.com") + .license("Apache 2.0", "https://www.apache.org/licenses/", "Apache-2.0") + ) + .withServer(openApiServer -> + openApiServer + .description("Server description goes here") + .url("http://localhost:{port}{basePath}/" + version + "/") + .variable("port", "Server's port", "8080", "8080", "7070") + .variable("basePath", "Base path of the server", "", "", "v1") ) - .withGlobalSecurity(new Security("OAuth2") - .withScope("write_pets") - .withScope("read_pets")) - ) - .withDefinitionProcessor(content -> { // you can add whatever you want to this document using your favourite json api - content.set("test", new TextNode("Value")); - return content.toPrettyString(); - })) - )); + // Based on official example: https://swagger.io/docs/specification/authentication/oauth2/ + .withSecurity(openApiSecurity -> + openApiSecurity + .withBasicAuth() + .withBearerAuth() + .withApiKeyAuth("ApiKeyAuth", "X-Api-Key") + .withCookieAuth("CookieAuth", "JSESSIONID") + .withOpenID("OpenID", "https://example.com/.well-known/openid-configuration") + .withOAuth2("OAuth2", "This API uses OAuth 2 with the implicit grant flow.", oauth2 -> + oauth2 + .withClientCredentials("https://api.example.com/credentials/authorize") + .withImplicitFlow("https://api.example.com/oauth2/authorize", flow -> + flow + .withScope("read_pets", "read your pets") + .withScope("write_pets", "modify pets in your account") + ) + ) + .withGlobalSecurity("OAuth2", globalSecurity -> + globalSecurity + .withScope("write_pets") + .withScope("read_pets") + ) + ) + .withDefinitionProcessor(content -> { // you can add whatever you want to this document using your favourite json api + content.set("test", new TextNode("Value")); + return content.toPrettyString(); + }) + ))); config.registerPlugin(new SwaggerPlugin(swaggerConfiguration -> { swaggerConfiguration.setDocumentationPath(deprecatedDocsPath); @@ -137,8 +131,7 @@ public static void main(String[] args) { System.out.println(generatedJsonSchema.getName()); System.out.println(generatedJsonSchema.getContentAsString()); } - }) - .start(8080); + }); } @OpenApi( @@ -164,19 +157,19 @@ public static void main(String[] args) { @OpenApiParam(name = "query", description = "Some query", required = true, type = Integer.class) }, requestBody = @OpenApiRequestBody( - description = "Supports multiple request bodies", - content = { - @OpenApiContent(from = String.class, example = "value"), // simple type - @OpenApiContent(from = KotlinEntity.class, mimeType = "app/barbie", exampleObjects = { - @OpenApiExampleProperty(name = "name", value = "Margot Robbie") - }), // kotlin - @OpenApiContent(from = LombokEntity.class, mimeType = "app/lombok"), // lombok - @OpenApiContent(from = EntityWithGenericType.class), // generics - @OpenApiContent(from = RecordEntity.class, mimeType = "app/record"), // record class - @OpenApiContent(from = DtoWithFields.class, mimeType = "app/dto"), // map only fields - @OpenApiContent(from = EnumEntity.class, mimeType = "app/enum"), // enum, - @OpenApiContent(from = CustomNameEntity.class, mimeType = "app/custom-name-entity") // custom name - } + description = "Supports multiple request bodies", + content = { + @OpenApiContent(from = String.class, example = "value"), // simple type + @OpenApiContent(from = KotlinEntity.class, mimeType = "app/barbie", exampleObjects = { + @OpenApiExampleProperty(name = "name", value = "Margot Robbie") + }), // kotlin + @OpenApiContent(from = LombokEntity.class, mimeType = "app/lombok"), // lombok + @OpenApiContent(from = EntityWithGenericType.class), // generics + @OpenApiContent(from = RecordEntity.class, mimeType = "app/record"), // record class + @OpenApiContent(from = DtoWithFields.class, mimeType = "app/dto"), // map only fields + @OpenApiContent(from = EnumEntity.class, mimeType = "app/enum"), // enum, + @OpenApiContent(from = CustomNameEntity.class, mimeType = "app/custom-name-entity") // custom name + } ), responses = { @OpenApiResponse(status = "200", description = "Status of the executed command", content = { @@ -237,7 +230,7 @@ public static void main(String[] args) { ) ) @Override - public void handle(@NotNull Context ctx) { } + public void handle(@NotNull Context ctx) {} @OpenApi( path = "/standalone", @@ -347,8 +340,8 @@ public String getFormattedMessage() { // should contain dedicated foo example @OpenApiExample(objects = { - @OpenApiExampleProperty(name = "name", value = "Margot Robbie"), - @OpenApiExampleProperty(name = "link", value = "Dedicated link") + @OpenApiExampleProperty(name = "name", value = "Margot Robbie"), + @OpenApiExampleProperty(name = "link", value = "Dedicated link") }) public @NotNull Foo getExampleFoo() { return new Foo(); @@ -365,10 +358,10 @@ public String getFormattedMessage() { // should contain objects example @OpenApiExample(objects = { - @OpenApiExampleProperty(name = "Barbie", objects = { - @OpenApiExampleProperty(name = "name", value = "Margot Robbie"), - @OpenApiExampleProperty(name = "link", value = "https://www.youtube.com/watch?v=dQw4w9WgXcQ") - }), + @OpenApiExampleProperty(name = "Barbie", objects = { + @OpenApiExampleProperty(name = "name", value = "Margot Robbie"), + @OpenApiExampleProperty(name = "link", value = "https://www.youtube.com/watch?v=dQw4w9WgXcQ") + }), }) public @NotNull Object[] getExampleObjects() { return new String[] { timestamp }; @@ -404,6 +397,7 @@ public static String getStatic() { } static final class Foo { + @OpenApiExample("https://www.youtube.com/watch?v=dQw4w9WgXcQ") public String getLink() { return ""; @@ -424,6 +418,7 @@ public String getProperty() { // should work with properties generated by another annotation processor @Data static final class LombokEntity { + private String property; } @@ -452,6 +447,7 @@ record RecordEntity( // should query fields @OpenApiByFields(Visibility.PROTECTED) // by default: PUBLIC static final class DtoWithFields { + public String publicName; String defaultName; protected String protectedName; @@ -508,11 +504,14 @@ public boolean isPanda() { } - @Target({ElementType.METHOD, ElementType.TYPE}) + @Target({ ElementType.METHOD, ElementType.TYPE }) @CustomAnnotation @interface Description { + String title(); + String description(); + int statusCode(); } diff --git a/javalin-plugins/javalin-openapi-plugin/src/main/kotlin/io/javalin/openapi/plugin/OpenApiConfiguration.kt b/javalin-plugins/javalin-openapi-plugin/src/main/kotlin/io/javalin/openapi/plugin/OpenApiConfiguration.kt index 56c6921..acac987 100644 --- a/javalin-plugins/javalin-openapi-plugin/src/main/kotlin/io/javalin/openapi/plugin/OpenApiConfiguration.kt +++ b/javalin-plugins/javalin-openapi-plugin/src/main/kotlin/io/javalin/openapi/plugin/OpenApiConfiguration.kt @@ -1,8 +1,14 @@ package io.javalin.openapi.plugin import com.fasterxml.jackson.databind.node.ObjectNode +import io.javalin.openapi.ApiKeyAuth +import io.javalin.openapi.BasicAuth +import io.javalin.openapi.BearerAuth +import io.javalin.openapi.CookieAuth +import io.javalin.openapi.OAuth2 import io.javalin.openapi.OpenApiInfo import io.javalin.openapi.OpenApiServer +import io.javalin.openapi.OpenID import io.javalin.openapi.Security import io.javalin.openapi.SecurityScheme import io.javalin.security.RouteRole @@ -11,9 +17,9 @@ import java.util.function.Consumer /** Configure OpenApi plugin */ class OpenApiPluginConfiguration @JvmOverloads constructor( - @JvmField @JvmSynthetic internal var documentationPath: String = "/openapi", - @JvmField @JvmSynthetic internal var roles: List? = null, - @JvmField @JvmSynthetic internal var definitionConfiguration: BiConsumer? = null + @JvmField var documentationPath: String = "/openapi", + @JvmField var roles: List? = null, + @JvmField var definitionConfiguration: BiConsumer? = null ) { /** Path to host documentation as JSON */ @@ -46,10 +52,14 @@ class DefinitionConfiguration @JvmOverloads constructor( ) { /** Define custom info object */ - fun withOpenApiInfo(openApiInfo: Consumer): DefinitionConfiguration = also { + fun withInfo(openApiInfo: Consumer): DefinitionConfiguration = also { this.info = OpenApiInfo().also { openApiInfo.accept(it) } } + @Deprecated("Use withInfo instead", ReplaceWith("withInfo(openApiInfo)")) + fun withOpenApiInfo(openApiInfo: Consumer): DefinitionConfiguration = + withInfo(openApiInfo) + /** Add custom server **/ fun withServer(server: OpenApiServer): DefinitionConfiguration = also { this.servers.add(server) @@ -88,8 +98,36 @@ class SecurityComponentConfiguration @JvmOverloads constructor( securitySchemes[schemeName] = securityScheme } + @JvmOverloads + fun withBasicAuth(schemeName: String = "BasicAuth", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = + withSecurityScheme(schemeName, BasicAuth().also { securityScheme.accept(it) }) + + @JvmOverloads + fun withBearerAuth(schemeName: String = "BearerAuth", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = + withSecurityScheme(schemeName, BearerAuth().also { securityScheme.accept(it) }) + + @JvmOverloads + fun withApiKeyAuth(schemeName: String = "ApiKeyAuth", apiKeyHeader: String = "X-Api-Key", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = + withSecurityScheme(schemeName, ApiKeyAuth(apiKeyHeader).also { securityScheme.accept(it) }) + + @JvmOverloads + fun withCookieAuth(schemeName: String = "CookieAuth", sessionCookie: String = "JSESSIONID", securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = + withSecurityScheme(schemeName, CookieAuth(sessionCookie).also { securityScheme.accept(it) }) + + @JvmOverloads + fun withOpenID(schemeName: String, openIdConnectUrl: String, securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = + withSecurityScheme(schemeName, OpenID(openIdConnectUrl).also { securityScheme.accept(it) }) + + @JvmOverloads + fun withOAuth2(schemeName: String, description: String, securityScheme: Consumer = Consumer {}): SecurityComponentConfiguration = + withSecurityScheme(schemeName, OAuth2(description).also { securityScheme.accept(it) }) + fun withGlobalSecurity(security: Security): SecurityComponentConfiguration = also { globalSecurity.add(security) } + @JvmOverloads + fun withGlobalSecurity(name: String, security: Consumer = Consumer {}): SecurityComponentConfiguration = + withGlobalSecurity(Security(name).also { security.accept(it) }) + } \ No newline at end of file diff --git a/openapi-specification/src/main/kotlin/io/javalin/openapi/Info.kt b/openapi-specification/src/main/kotlin/io/javalin/openapi/Info.kt index 1b3ffa6..88b30dd 100644 --- a/openapi-specification/src/main/kotlin/io/javalin/openapi/Info.kt +++ b/openapi-specification/src/main/kotlin/io/javalin/openapi/Info.kt @@ -1,39 +1,68 @@ package io.javalin.openapi +import java.util.function.Consumer + /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#infoObject */ class OpenApiInfo { /** REQUIRED. The title of the API */ var title: String? = null + fun title(title: String) = apply { this.title = title } + /** A short summary of the API */ var summary: String? = null + fun summary(summary: String) = apply { this.summary = summary } + /** A description of the API. CommonMark's syntax MAY be used for rich text representation */ var description: String? = null + fun description(description: String) = apply { this.description = description } + /** A URL to the Terms of Service for the API. This MUST be in the form of a URL */ var termsOfService: String? = null + fun termsOfService(termsOfService: String) = apply { this.termsOfService = termsOfService } + /** The contact information for the exposed API */ var contact: OpenApiContact? = null + @JvmOverloads + fun contact(name: String?, url: String? = null, email: String? = null) = withContact { it.name(name).url(url).email(email) } + fun withContact(contact: Consumer) = apply { this.contact = OpenApiContact().apply { contact.accept(this) } } + /** The license information for the exposed API */ var license: OpenApiLicense? = null + @JvmOverloads + fun license(name: String?, url: String? = null, identifier: String? = null) = withLicense { it.name(name).url(url).identifier(identifier) } + fun withLicense(license: Consumer) = apply { this.license = OpenApiLicense().apply { license.accept(this) } } + /** REQUIRED. The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version). */ var version: String? = null + fun version(version: String) = apply { this.version = version } } /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#contactObject */ class OpenApiContact { /** The identifying name of the contact person/organization. */ var name: String? = null + fun name(name: String?) = apply { this.name = name } + /** The URL pointing to the contact information. This MUST be in the form of a URL. */ var url: String? = null + fun url(url: String?) = apply { this.url = url } + /** The email address of the contact person/organization. This MUST be in the form of an email address. */ var email: String? = null + fun email(email: String?) = apply { this.email = email } } /** https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#licenseObject */ class OpenApiLicense { /** REQUIRED. The license name used for the API */ var name: String? = null + fun name(name: String?) = apply { this.name = name } + /** An SPDX license expression for the API. The identifier field is mutually exclusive of the url field. */ var identifier: String? = null + fun identifier(identifier: String?) = apply { this.identifier = identifier } + /** A URL to the license used for the API. This MUST be in the form of a URL. The url field is mutually exclusive of the identifier field */ var url: String? = null + fun url(url: String?) = apply { this.url = url } } \ No newline at end of file diff --git a/openapi-specification/src/main/kotlin/io/javalin/openapi/Security.kt b/openapi-specification/src/main/kotlin/io/javalin/openapi/Security.kt index fb75bd9..edd75c3 100644 --- a/openapi-specification/src/main/kotlin/io/javalin/openapi/Security.kt +++ b/openapi-specification/src/main/kotlin/io/javalin/openapi/Security.kt @@ -1,6 +1,7 @@ package io.javalin.openapi import com.fasterxml.jackson.annotation.JsonIgnore +import java.util.function.Consumer data class Security @JvmOverloads constructor( val name: String, @@ -26,15 +27,15 @@ class BasicAuth : HttpAuth(scheme = "basic") class BearerAuth : HttpAuth(scheme = "bearer") open class ApiKeyAuth( - open val `in`: String = "header", - open val name: String = "X-API-Key" + open var `in`: String = "header", + open var name: String = "X-API-Key" ) : SecurityScheme { override val type: String = "apiKey" } class CookieAuth @JvmOverloads constructor( - override val name: String, - override val `in`: String = "cookie" + override var name: String, + override var `in`: String = "cookie" ) : ApiKeyAuth() class OpenID (val openIdConnectUrl: String) : SecurityScheme { @@ -42,7 +43,7 @@ class OpenID (val openIdConnectUrl: String) : SecurityScheme { } class OAuth2 @JvmOverloads constructor( - val description: String, + var description: String, val flows: MutableMap> = mutableMapOf(), ) : SecurityScheme { override val type: String = "oauth2" @@ -50,6 +51,23 @@ class OAuth2 @JvmOverloads constructor( fun withFlow(flow: OAuth2Flow<*>): OAuth2 = also { flows[flow.flowType] = flow } + + @JvmOverloads + fun withAuthorizationCodeFlow(authorizationUrl: String, tokenUrl: String, flow: Consumer = Consumer {}): OAuth2 = + withFlow(AuthorizationCodeFlow(authorizationUrl, tokenUrl).also { flow.accept(it) }) + + @JvmOverloads + fun withImplicitFlow(authorizationUrl: String, flow: Consumer = Consumer {}): OAuth2 = + withFlow(ImplicitFlow(authorizationUrl).also { flow.accept(it) }) + + @JvmOverloads + fun withPasswordFlow(tokenUrl: String, flow: Consumer = Consumer {}): OAuth2 = + withFlow(PasswordFlow(tokenUrl).also { flow.accept(it) }) + + @JvmOverloads + fun withClientCredentials(tokenUrl: String, flow: Consumer = Consumer {}): OAuth2 = + withFlow(ClientCredentials(tokenUrl).also { flow.accept(it) }) + } interface OAuth2Flow> { @@ -64,29 +82,29 @@ interface OAuth2Flow> { } class AuthorizationCodeFlow @JvmOverloads constructor( - val authorizationUrl: String, - val tokenUrl: String, - override val scopes: MutableMap = mutableMapOf() + var authorizationUrl: String, + var tokenUrl: String, + override var scopes: MutableMap = mutableMapOf() ) : OAuth2Flow { override val flowType: String = "authorizationCode" } class ImplicitFlow @JvmOverloads constructor( - val authorizationUrl: String, + var authorizationUrl: String, override val scopes: MutableMap = mutableMapOf() ) : OAuth2Flow { override val flowType: String = "implicit" } class PasswordFlow @JvmOverloads constructor( - val tokenUrl: String, + var tokenUrl: String, override val scopes: MutableMap = mutableMapOf() ) : OAuth2Flow { override val flowType: String = "password" } class ClientCredentials @JvmOverloads constructor( - val tokenUrl: String, + var tokenUrl: String, override val scopes: MutableMap = mutableMapOf() ) : OAuth2Flow { override val flowType: String = "clientCredentials" diff --git a/openapi-specification/src/main/kotlin/io/javalin/openapi/Server.kt b/openapi-specification/src/main/kotlin/io/javalin/openapi/Server.kt index 7719e23..8ab5b38 100644 --- a/openapi-specification/src/main/kotlin/io/javalin/openapi/Server.kt +++ b/openapi-specification/src/main/kotlin/io/javalin/openapi/Server.kt @@ -7,11 +7,19 @@ class OpenApiServer { /** REQUIRED. A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}. */ var url: String? = null + fun url(url: String) = apply { this.url = url } + /** An optional string describing the host designated by the URL. CommonMark syntax MAY be used for rich text representation. */ var description: String? = null + fun description(description: String) = apply { this.description = description } + /** A map between a variable name and its value. The value is used for substitution in the server's URL template. */ var variables: MutableMap = mutableMapOf() + fun variable(key: String, description: String, defaultValue: String, vararg values: String) = apply { + addVariable(key, OpenApiServerVariable().values(*values).default(defaultValue).description(description)) + } + fun addVariable(key: String, variable: OpenApiServerVariable): OpenApiServer = also { variables[key] = variable } @@ -20,7 +28,7 @@ class OpenApiServer { addVariable( key = key, variable = OpenApiServerVariable().also { - it.values = values + it.values = values.toList() it.default = defaultValue it.description = description } @@ -33,9 +41,14 @@ class OpenApiServer { class OpenApiServerVariable { /** An enumeration of string values to be used if the substitution options are from a limited set. The array MUST NOT be empty. */ @JsonProperty("enum") - var values: Array? = null + var values: List? = null + fun values(vararg values: String) = apply { this.values = values.toList() } + /** REQUIRED. The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different than the Schema Object's treatment of default values, because in those cases parameter values are optional. If the enum is defined, the value MUST exist in the enum's values. */ var default: String? = null + fun default(default: String) = apply { this.default = default } + /** An optional description for the server variable. CommonMark syntax MAY be used for rich text representation. */ var description: String? = null + fun description(description: String) = apply { this.description = description } }