diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5f3d55d..d7de140 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,7 +29,7 @@ jobs: - name: Publish run: | ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache - ./gradlew closeAndReleaseRepository + ./gradlew releaseRepository env: SONATYPE_CONNECT_TIMEOUT_SECONDS: 180 SONATYPE_CLOSE_TIMEOUT_SECONDS: 900 diff --git a/HowToRelease.md b/HowToRelease.md deleted file mode 100644 index afa9565..0000000 --- a/HowToRelease.md +++ /dev/null @@ -1,9 +0,0 @@ -# How to release - -https://vanniktech.github.io/gradle-maven-publish-plugin/central/ - -1. Credentials should be configured in a gradle.properties file (in user home) - -2. `./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache` - -3. `./gradlew closeAndReleaseRepository` \ No newline at end of file diff --git a/README.md b/README.md index f40b538..b65f7d0 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,19 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.smiley4/ktor-swagger-ui/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.smiley4/ktor-swagger-ui) [![Checks Passing](https://github.com/SMILEY4/ktor-swagger-ui/actions/workflows/checks.yml/badge.svg?branch=develop)](https://github.com/SMILEY4/ktor-swagger-ui/actions/workflows/checks.yml) - This library provides a Ktor plugin to document routes, generate an OpenApi Specification and serve a Swagger UI. It is meant to be minimally invasive, meaning it can be plugged into existing application without requiring immediate changes to the code. Routes can then be gradually enhanced with documentation. ## Features - minimally invasive (no immediate change to existing code required) -- provides swagger-ui with no initial configuration required -- supports most of the [OpenAPI 3.0.3 Specification](https://swagger.io/specification/) -- automatic json-schema generation from arbitrary types/classes for bodies and parameters -- use custom encoder/serializers for examples and json-schemas -- provide custom schemas or a custom schema-builder -- external/custom json-schemas for bodies -- protect Swagger-UI and OpenApi-Spec with custom authentication +- provides swagger-ui and openapi-spec with minimal configuration +- supports most of the [OpenAPI 3.1.0 Specification](https://swagger.io/specification/) +- automatic [json-schema generation](https://github.com/SMILEY4/schema-kenerator) from arbitrary types/classes for bodies and parameters + - supports generics, inheritance, collections, ... + - support for Jackson-annotations and swagger Schema-annotations (optional) + - use with reflection or kotlinx-serialization + - customizable schema-generation ## Documentation @@ -32,9 +31,14 @@ dependencies { } ``` -## Example -Full examples can be found in [src/test/examples](https://github.com/SMILEY4/ktor-swagger-ui/tree/develop/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples). + +## Examples + +Runnable examples can be found in [ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples](https://github.com/SMILEY4/ktor-swagger-ui/tree/release/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples). + + ### Configuration + ```kotlin install(SwaggerUI) { swagger { @@ -52,7 +56,9 @@ install(SwaggerUI) { } } ``` + ### Routes + ```kotlin get("hello", { description = "Hello World Endpoint." diff --git a/build.gradle.kts b/build.gradle.kts index 757ed3c..6557668 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,135 +1,11 @@ -import com.vanniktech.maven.publish.SonatypeHost -import io.gitlab.arturbosch.detekt.Detekt - -object Meta { - const val groupId = "io.github.smiley4" - const val artifactId = "ktor-swagger-ui" - const val version = "2.10.0" - const val name = "Ktor Swagger-UI" - const val description = "Ktor plugin to document routes and provide Swagger UI" - const val licenseName = "The Apache License, Version 2.0" - const val licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt" - const val scmUrl = "https://github.com/SMILEY4/ktor-swagger-ui" - const val scmConnection = "scm:git:git://github.com/SMILEY4/ktor-swagger-ui.git" - const val developerName = "smiley4" - const val developerUrl = "https://github.com/SMILEY4" -} - -group = Meta.groupId -version = Meta.version - plugins { - kotlin("jvm") version "1.8.21" - kotlin("plugin.serialization") version "1.8.21" - id("org.owasp.dependencycheck") version "8.2.1" - id("com.vanniktech.maven.publish") version "0.25.2" - id("io.gitlab.arturbosch.detekt") version "1.23.0" + kotlin("jvm") version "1.9.21" + id("org.jetbrains.dokka") version "1.9.20" apply false + id("org.owasp.dependencycheck") version "8.2.1" apply false + id("io.gitlab.arturbosch.detekt") version "1.23.0" apply false + id("com.vanniktech.maven.publish") version "0.28.0" apply false } repositories { mavenCentral() - maven(url = "https://raw.githubusercontent.com/glureau/json-schema-serialization/mvn-repo") -} - -dependencies { - - val ktorVersion = "2.3.7" - implementation("io.ktor:ktor-server-core-jvm:$ktorVersion") - implementation("io.ktor:ktor-server-webjars:$ktorVersion") - implementation("io.ktor:ktor-server-auth:$ktorVersion") - implementation("io.ktor:ktor-server-resources:$ktorVersion") - testImplementation("io.ktor:ktor-server-netty-jvm:$ktorVersion") - testImplementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") - testImplementation("io.ktor:ktor-serialization-jackson:$ktorVersion") - testImplementation("io.ktor:ktor-server-auth:$ktorVersion") - testImplementation("io.ktor:ktor-server-call-logging:$ktorVersion") - testImplementation("io.ktor:ktor-server-test-host:$ktorVersion") - - val swaggerUiVersion = "5.9.0" // this version must match the version declared in the code (SwaggerPlugin#SWAGGER_UI_WEBJARS_VERSION) - implementation("org.webjars:swagger-ui:$swaggerUiVersion") - - val swaggerParserVersion = "2.1.19" - implementation("io.swagger.parser.v3:swagger-parser:$swaggerParserVersion") - - val jsonSchemaGeneratorVersion = "4.33.1" - implementation("com.github.victools:jsonschema-generator:$jsonSchemaGeneratorVersion") - implementation("com.github.victools:jsonschema-module-jackson:$jsonSchemaGeneratorVersion") - implementation("com.github.victools:jsonschema-module-swagger-2:$jsonSchemaGeneratorVersion") - - val jacksonVersion = "2.15.3" - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}") - - val kotlinLoggingVersion = "3.0.5" - implementation("io.github.microutils:kotlin-logging-jvm:$kotlinLoggingVersion") - - val logbackVersion = "1.4.11" - testImplementation("ch.qos.logback:logback-classic:$logbackVersion") - - val versionMockk = "1.13.8" - testImplementation("io.mockk:mockk:$versionMockk") - - val versionKotest = "5.8.0" - testImplementation("io.kotest:kotest-runner-junit5:$versionKotest") - testImplementation("io.kotest:kotest-assertions-core:$versionKotest") - - val versionKotlinTest = "1.8.21" - testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest") - - testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") - testImplementation("com.github.Ricky12Awesome:json-schema-serialization:0.9.9") - testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") -} - -kotlin { - jvmToolchain(11) -} - -tasks.withType().configureEach { - useJUnitPlatform() -} - -detekt { - buildUponDefaultConfig = true - allRules = false - config.setFrom("$projectDir/config/detekt.yml") - baseline = file("$projectDir/config/baseline.xml") -} - -tasks.withType().configureEach { - reports { - html.required.set(true) - md.required.set(true) - xml.required.set(false) - txt.required.set(false) - sarif.required.set(false) - } -} - -mavenPublishing { - publishToMavenCentral(SonatypeHost.S01) - signAllPublications() - coordinates(Meta.groupId, Meta.artifactId, Meta.version) - pom { - name.set(Meta.name) - description.set(Meta.description) - url.set(Meta.scmUrl) - licenses { - license { - name.set(Meta.licenseName) - url.set(Meta.licenseUrl) - distribution.set(Meta.licenseUrl) - } - } - scm { - url.set(Meta.scmUrl) - connection.set(Meta.scmConnection) - } - developers { - developer { - id.set(Meta.developerName) - name.set(Meta.developerName) - url.set(Meta.developerUrl) - } - } - } -} +} \ No newline at end of file diff --git a/config/detekt.yml b/config/detekt.yml deleted file mode 100644 index db9252e..0000000 --- a/config/detekt.yml +++ /dev/null @@ -1,788 +0,0 @@ -build: - maxIssues: 0 - excludeCorrectable: false - weights: - # complexity: 2 - # LongParameterList: 1 - # style: 1 - # comments: 1 - -config: - validation: true - warningsAsErrors: false - checkExhaustiveness: false - # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' - excludes: '' - -processors: - active: true - exclude: - - 'DetektProgressListener' - # - 'KtFileCountProcessor' - # - 'PackageCountProcessor' - # - 'ClassCountProcessor' - # - 'FunctionCountProcessor' - # - 'PropertyCountProcessor' - # - 'ProjectComplexityProcessor' - # - 'ProjectCognitiveComplexityProcessor' - # - 'ProjectLLOCProcessor' - # - 'ProjectCLOCProcessor' - # - 'ProjectLOCProcessor' - # - 'ProjectSLOCProcessor' - # - 'LicenseHeaderLoaderExtension' - -console-reports: - active: true - exclude: - - 'ProjectStatisticsReport' - - 'ComplexityReport' - - 'NotificationReport' - - 'FindingsReport' - - 'FileBasedFindingsReport' - # - 'LiteFindingsReport' - -output-reports: - active: true - exclude: - # - 'TxtOutputReport' - # - 'XmlOutputReport' - # - 'HtmlOutputReport' - # - 'MdOutputReport' - # - 'SarifOutputReport' - -comments: - active: true - AbsentOrWrongFileLicense: - active: false - licenseTemplateFile: 'license.template' - licenseTemplateIsRegex: false - CommentOverPrivateFunction: - active: false - CommentOverPrivateProperty: - active: false - DeprecatedBlockTag: - active: false - EndOfSentenceFormat: - active: false - endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' - KDocReferencesNonPublicProperty: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - OutdatedDocumentation: - active: false - matchTypeParameters: true - matchDeclarationsOrder: true - allowParamOnConstructorProperties: false - UndocumentedPublicClass: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - searchInNestedClass: true - searchInInnerClass: true - searchInInnerObject: true - searchInInnerInterface: true - searchInProtectedClass: false - UndocumentedPublicFunction: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - searchProtectedFunction: false - UndocumentedPublicProperty: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - searchProtectedProperty: false - -complexity: - active: true - CognitiveComplexMethod: - active: false - threshold: 15 - ComplexCondition: - active: true - threshold: 4 - ComplexInterface: - active: false - threshold: 10 - includeStaticDeclarations: false - includePrivateDeclarations: false - ignoreOverloaded: false - CyclomaticComplexMethod: - active: true - threshold: 20 - ignoreSingleWhenExpression: false - ignoreSimpleWhenEntries: false - ignoreNestingFunctions: false - nestingFunctions: - - 'also' - - 'apply' - - 'forEach' - - 'isNotNull' - - 'ifNull' - - 'let' - - 'run' - - 'use' - - 'with' - LabeledExpression: - active: false - ignoredLabels: [] - LargeClass: - active: true - threshold: 600 - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] - LongMethod: - active: true - threshold: 60 - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] - LongParameterList: - active: true - functionThreshold: 6 - constructorThreshold: 15 - ignoreDefaultParameters: false - ignoreDataClasses: true - ignoreAnnotatedParameter: [] - MethodOverloading: - active: false - threshold: 6 - NamedArguments: - active: false - threshold: 3 - ignoreArgumentsMatchingNames: false - NestedBlockDepth: - active: true - threshold: 4 - NestedScopeFunctions: - active: false - threshold: 1 - functions: - - 'kotlin.apply' - - 'kotlin.run' - - 'kotlin.with' - - 'kotlin.let' - - 'kotlin.also' - ReplaceSafeCallChainWithRun: - active: false - StringLiteralDuplication: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - threshold: 3 - ignoreAnnotation: true - excludeStringsWithLessThan5Characters: true - ignoreStringsRegex: '$^' - TooManyFunctions: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - thresholdInFiles: 11 - thresholdInClasses: 11 - thresholdInInterfaces: 11 - thresholdInObjects: 11 - thresholdInEnums: 11 - ignoreDeprecated: false - ignorePrivate: false - ignoreOverridden: false - -coroutines: - active: true - GlobalCoroutineUsage: - active: false - InjectDispatcher: - active: true - dispatcherNames: - - 'IO' - - 'Default' - - 'Unconfined' - RedundantSuspendModifier: - active: true - SleepInsteadOfDelay: - active: true - SuspendFunSwallowedCancellation: - active: false - SuspendFunWithCoroutineScopeReceiver: - active: false - SuspendFunWithFlowReturnType: - active: true - -empty-blocks: - active: true - EmptyCatchBlock: - active: true - allowedExceptionNameRegex: '_|(ignore|expected).*' - EmptyClassBlock: - active: true - EmptyDefaultConstructor: - active: true - EmptyDoWhileBlock: - active: true - EmptyElseBlock: - active: true - EmptyFinallyBlock: - active: true - EmptyForBlock: - active: true - EmptyFunctionBlock: - active: true - ignoreOverridden: false - EmptyIfBlock: - active: true - EmptyInitBlock: - active: true - EmptyKtFile: - active: true - EmptySecondaryConstructor: - active: true - EmptyTryBlock: - active: true - EmptyWhenBlock: - active: true - EmptyWhileBlock: - active: true - -exceptions: - active: true - ExceptionRaisedInUnexpectedLocation: - active: true - methodNames: - - 'equals' - - 'finalize' - - 'hashCode' - - 'toString' - InstanceOfCheckForException: - active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - NotImplementedDeclaration: - active: false - ObjectExtendsThrowable: - active: false - PrintStackTrace: - active: true - RethrowCaughtException: - active: true - ReturnFromFinally: - active: true - ignoreLabeled: false - SwallowedException: - active: true - ignoredExceptionTypes: - - 'InterruptedException' - - 'MalformedURLException' - - 'NumberFormatException' - - 'ParseException' - allowedExceptionNameRegex: '_|(ignore|expected).*' - ThrowingExceptionFromFinally: - active: true - ThrowingExceptionInMain: - active: false - ThrowingExceptionsWithoutMessageOrCause: - active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - exceptions: - - 'ArrayIndexOutOfBoundsException' - - 'Exception' - - 'IllegalArgumentException' - - 'IllegalMonitorStateException' - - 'IllegalStateException' - - 'IndexOutOfBoundsException' - - 'NullPointerException' - - 'RuntimeException' - - 'Throwable' - ThrowingNewInstanceOfSameException: - active: true - TooGenericExceptionCaught: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - exceptionNames: - - 'ArrayIndexOutOfBoundsException' - - 'Error' - - 'Exception' - - 'IllegalMonitorStateException' - - 'IndexOutOfBoundsException' - - 'NullPointerException' - - 'RuntimeException' - - 'Throwable' - allowedExceptionNameRegex: '_|(ignore|expected).*' - TooGenericExceptionThrown: - active: true - exceptionNames: - - 'Error' - - 'Exception' - - 'RuntimeException' - - 'Throwable' - -naming: - active: true - BooleanPropertyNaming: - active: false - allowedPattern: '^(is|has|are)' - ClassNaming: - active: true - classPattern: '[A-Z][a-zA-Z0-9]*' - ConstructorParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*' - privateParameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - EnumNaming: - active: true - enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' - ForbiddenClassName: - active: false - forbiddenName: [] - FunctionMaxLength: - active: false - maximumFunctionNameLength: 30 - FunctionMinLength: - active: false - minimumFunctionNameLength: 3 - FunctionNaming: - active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - functionPattern: '[a-z][a-zA-Z0-9]*' - excludeClassPattern: '$^' - FunctionParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - InvalidPackageDeclaration: - active: true - rootPackage: '' - requireRootInDeclaration: false - LambdaParameterNaming: - active: false - parameterPattern: '[a-z][A-Za-z0-9]*|_' - MatchingDeclarationName: - active: true - mustBeFirst: true - MemberNameEqualsClassName: - active: true - ignoreOverridden: true - NoNameShadowing: - active: true - NonBooleanPropertyPrefixedWithIs: - active: false - ObjectPropertyNaming: - active: true - constantPattern: '[A-Za-z][_A-Za-z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' - PackageNaming: - active: true - packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' - TopLevelPropertyNaming: - active: true - constantPattern: '[A-Z][_A-Z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' - VariableMaxLength: - active: false - maximumVariableNameLength: 64 - VariableMinLength: - active: false - minimumVariableNameLength: 1 - VariableNaming: - active: true - variablePattern: '[a-z][A-Za-z0-9]*' - privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - -performance: - active: true - ArrayPrimitive: - active: true - CouldBeSequence: - active: false - threshold: 3 - ForEachOnRange: - active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - SpreadOperator: - active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - UnnecessaryPartOfBinaryExpression: - active: false - UnnecessaryTemporaryInstantiation: - active: true - -potential-bugs: - active: true - AvoidReferentialEquality: - active: true - forbiddenTypePatterns: - - 'kotlin.String' - CastNullableToNonNullableType: - active: false - CastToNullableType: - active: false - Deprecation: - active: false - DontDowncastCollectionTypes: - active: false - DoubleMutabilityForCollection: - active: true - mutableTypes: - - 'kotlin.collections.MutableList' - - 'kotlin.collections.MutableMap' - - 'kotlin.collections.MutableSet' - - 'java.util.ArrayList' - - 'java.util.LinkedHashSet' - - 'java.util.HashSet' - - 'java.util.LinkedHashMap' - - 'java.util.HashMap' - ElseCaseInsteadOfExhaustiveWhen: - active: false - ignoredSubjectTypes: [] - EqualsAlwaysReturnsTrueOrFalse: - active: true - EqualsWithHashCodeExist: - active: true - ExitOutsideMain: - active: false - ExplicitGarbageCollectionCall: - active: true - HasPlatformType: - active: true - IgnoredReturnValue: - active: true - restrictToConfig: true - returnValueAnnotations: - - 'CheckResult' - - '*.CheckResult' - - 'CheckReturnValue' - - '*.CheckReturnValue' - ignoreReturnValueAnnotations: - - 'CanIgnoreReturnValue' - - '*.CanIgnoreReturnValue' - returnValueTypes: - - 'kotlin.sequences.Sequence' - - 'kotlinx.coroutines.flow.*Flow' - - 'java.util.stream.*Stream' - ignoreFunctionCall: [] - ImplicitDefaultLocale: - active: true - ImplicitUnitReturnType: - active: false - allowExplicitReturnType: true - InvalidRange: - active: true - IteratorHasNextCallsNextMethod: - active: true - IteratorNotThrowingNoSuchElementException: - active: true - LateinitUsage: - active: false - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - ignoreOnClassesPattern: '' - MapGetWithNotNullAssertionOperator: - active: true - MissingPackageDeclaration: - active: false - excludes: ['**/*.kts'] - NullCheckOnMutableProperty: - active: false - NullableToStringCall: - active: false - PropertyUsedBeforeDeclaration: - active: false - UnconditionalJumpStatementInLoop: - active: false - UnnecessaryNotNullCheck: - active: false - UnnecessaryNotNullOperator: - active: true - UnnecessarySafeCall: - active: true - UnreachableCatchBlock: - active: true - UnreachableCode: - active: true - UnsafeCallOnNullableType: - active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] - UnsafeCast: - active: true - UnusedUnaryOperator: - active: true - UselessPostfixExpression: - active: true - WrongEqualsTypeParameter: - active: true - -style: - active: true - AlsoCouldBeApply: - active: false - BracesOnIfStatements: - active: false - singleLine: 'never' - multiLine: 'always' - BracesOnWhenStatements: - active: false - singleLine: 'necessary' - multiLine: 'consistent' - CanBeNonNullable: - active: false - CascadingCallWrapping: - active: false - includeElvis: true - ClassOrdering: - active: false - CollapsibleIfStatements: - active: false - DataClassContainsFunctions: - active: false - conversionFunctionPrefix: - - 'to' - allowOperators: false - DataClassShouldBeImmutable: - active: false - DestructuringDeclarationWithTooManyEntries: - active: true - maxDestructuringEntries: 3 - DoubleNegativeLambda: - active: false - negativeFunctions: - - reason: 'Use `takeIf` instead.' - value: 'takeUnless' - - reason: 'Use `all` instead.' - value: 'none' - negativeFunctionNameParts: - - 'not' - - 'non' - EqualsNullCall: - active: true - EqualsOnSignatureLine: - active: false - ExplicitCollectionElementAccessMethod: - active: false - ExplicitItLambdaParameter: - active: true - ExpressionBodySyntax: - active: false - includeLineWrapping: false - ForbiddenAnnotation: - active: false - annotations: - - reason: 'it is a java annotation. Use `Suppress` instead.' - value: 'java.lang.SuppressWarnings' - - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' - value: 'java.lang.Deprecated' - - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' - value: 'java.lang.annotation.Documented' - - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' - value: 'java.lang.annotation.Target' - - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' - value: 'java.lang.annotation.Retention' - - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' - value: 'java.lang.annotation.Repeatable' - - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' - value: 'java.lang.annotation.Inherited' - ForbiddenComment: - active: true - comments: - - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' - value: 'FIXME:' - - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' - value: 'STOPSHIP:' - - reason: 'Forbidden TODO todo marker in comment, please do the changes.' - value: 'TODO:' - allowedPatterns: '' - ForbiddenImport: - active: false - imports: [] - forbiddenPatterns: '' - ForbiddenMethodCall: - active: false - methods: - - reason: 'print does not allow you to configure the output stream. Use a logger instead.' - value: 'kotlin.io.print' - - reason: 'println does not allow you to configure the output stream. Use a logger instead.' - value: 'kotlin.io.println' - ForbiddenSuppress: - active: false - rules: [] - ForbiddenVoid: - active: true - ignoreOverridden: false - ignoreUsageInGenerics: false - FunctionOnlyReturningConstant: - active: true - ignoreOverridableFunction: true - ignoreActualFunction: true - excludedFunctions: [] - LoopWithTooManyJumpStatements: - active: true - maxJumpCount: 1 - MagicNumber: - active: true - excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] - ignoreNumbers: - - '-1' - - '0' - - '1' - - '2' - ignoreHashCodeFunction: true - ignorePropertyDeclaration: false - ignoreLocalVariableDeclaration: false - ignoreConstantDeclaration: true - ignoreCompanionObjectPropertyDeclaration: true - ignoreAnnotation: false - ignoreNamedArgument: true - ignoreEnums: false - ignoreRanges: false - ignoreExtensionFunctions: true - MandatoryBracesLoops: - active: false - MaxChainedCallsOnSameLine: - active: false - maxChainedCalls: 5 - MaxLineLength: - active: true - maxLineLength: 140 - excludePackageStatements: true - excludeImportStatements: true - excludeCommentStatements: false - excludeRawStrings: true - MayBeConst: - active: true - ModifierOrder: - active: true - MultilineLambdaItParameter: - active: false - MultilineRawStringIndentation: - active: false - indentSize: 4 - trimmingMethods: - - 'trimIndent' - - 'trimMargin' - NestedClassesVisibility: - active: true - NewLineAtEndOfFile: - active: true - NoTabs: - active: false - NullableBooleanCheck: - active: false - ObjectLiteralToLambda: - active: true - OptionalAbstractKeyword: - active: true - OptionalUnit: - active: false - OptionalWhenBraces: - active: false - PreferToOverPairSyntax: - active: false - ProtectedMemberInFinalClass: - active: true - RedundantExplicitType: - active: false - RedundantHigherOrderMapUsage: - active: true - RedundantVisibilityModifierRule: - active: false - ReturnCount: - active: true - max: 2 - excludedFunctions: - - 'equals' - excludeLabeled: false - excludeReturnFromLambda: true - excludeGuardClauses: false - SafeCast: - active: true - SerialVersionUIDInSerializableClass: - active: true - SpacingBetweenPackageAndImports: - active: false - StringShouldBeRawString: - active: false - maxEscapedCharacterCount: 2 - ignoredCharacters: [] - ThrowsCount: - active: true - max: 2 - excludeGuardClauses: false - TrailingWhitespace: - active: false - TrimMultilineRawString: - active: false - trimmingMethods: - - 'trimIndent' - - 'trimMargin' - UnderscoresInNumericLiterals: - active: false - acceptableLength: 4 - allowNonStandardGrouping: false - UnnecessaryAbstractClass: - active: true - UnnecessaryAnnotationUseSiteTarget: - active: false - UnnecessaryApply: - active: true - UnnecessaryBackticks: - active: false - UnnecessaryBracesAroundTrailingLambda: - active: false - UnnecessaryFilter: - active: true - UnnecessaryInheritance: - active: true - UnnecessaryInnerClass: - active: false - UnnecessaryLet: - active: false - UnnecessaryParentheses: - active: false - allowForUnclearPrecedence: false - UntilInsteadOfRangeTo: - active: false - UnusedImports: - active: false - UnusedParameter: - active: true - allowedNames: 'ignored|expected' - UnusedPrivateClass: - active: true - UnusedPrivateMember: - active: true - allowedNames: '' - UnusedPrivateProperty: - active: true - allowedNames: '_|ignored|expected|serialVersionUID' - UseAnyOrNoneInsteadOfFind: - active: true - UseArrayLiteralsInAnnotations: - active: true - UseCheckNotNull: - active: true - UseCheckOrError: - active: true - UseDataClass: - active: false - allowVars: false - UseEmptyCounterpart: - active: false - UseIfEmptyOrIfBlank: - active: false - UseIfInsteadOfWhen: - active: false - ignoreWhenContainingVariableDeclaration: false - UseIsNullOrEmpty: - active: true - UseLet: - active: false - UseOrEmpty: - active: true - UseRequire: - active: false - UseRequireNotNull: - active: false - UseSumOfInsteadOfFlatMapSize: - active: false - UselessCallOnNotNull: - active: true - UtilityClassWithPublicConstructor: - active: true - VarCouldBeVal: - active: true - ignoreLateinitVar: false - WildcardImport: - active: false - excludeImports: - - 'java.util.*' diff --git a/detekt/detekt.yml b/detekt/detekt.yml new file mode 100644 index 0000000..960d68d --- /dev/null +++ b/detekt/detekt.yml @@ -0,0 +1,788 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 20 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + LongMethod: + active: true + threshold: 60 + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 15 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 140 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: false + UseRequireNotNull: + active: false + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: false + excludeImports: + - 'java.util.*' \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7fc6f1f..dad08b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,27 @@ kotlin.code.style=official + +# project id +projectGroupId=io.github.smiley4 +projectArtifactIdBase=ktor-swagger-ui +projectVersion=3.0.0 + +# publishing information +projectNameBase=Ktor Swagger UI +projectDescriptionBase=Ktor plugin to document routes and provide a Swagger-UI +projectScmUrl=https://github.com/SMILEY4/ktor-swagger-ui +projectScmConnection=scm:git:git://github.com/SMILEY4/ktor-swagger-ui.git +projectLicenseName=The Apache License, Version 2.0 +projectLicenseUrl=https://www.apache.org/licenses/LICENSE-2.0.txt +projectDeveloperName=smiley4 +projectDeveloperUrl=https://github.com/SMILEY4 + +# dependency versions +versionKtor=2.3.11 +versionSwaggerUI=5.17.11 +versionSwaggerParser=2.1.22 +versionSchemaKenerator=1.0.0 +versionKotlinLogging=3.0.5 +versionKotest=5.8.0 +versionKotlinTest=1.8.21 +versionMockk=1.13.8 +versionLogback=1.4.11 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180..41d9927 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb87..15de902 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index c53aefa..1b6c787 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # diff --git a/ktor-swagger-ui-examples/build.gradle.kts b/ktor-swagger-ui-examples/build.gradle.kts new file mode 100644 index 0000000..8109498 --- /dev/null +++ b/ktor-swagger-ui-examples/build.gradle.kts @@ -0,0 +1,44 @@ +val projectGroupId: String by project +val projectVersion: String by project +group = projectGroupId +version = projectVersion + +plugins { + kotlin("jvm") + kotlin("plugin.serialization") version "1.9.21" +} + +repositories { + mavenCentral() +} + +dependencies { + val versionKtor: String by project + val versionSwaggerParser: String by project + val versionSchemaKenerator: String by project + val versionKotlinLogging: String by project + val versionLogback: String by project + + implementation(project(":ktor-swagger-ui")) + + implementation("io.ktor:ktor-server-netty-jvm:$versionKtor") + implementation("io.ktor:ktor-server-content-negotiation:$versionKtor") + implementation("io.ktor:ktor-serialization-jackson:$versionKtor") + implementation("io.ktor:ktor-server-auth:$versionKtor") + implementation("io.ktor:ktor-server-call-logging:$versionKtor") + implementation("io.ktor:ktor-server-test-host:$versionKtor") + + implementation("io.github.smiley4:schema-kenerator-core:$versionSchemaKenerator") + implementation("io.github.smiley4:schema-kenerator-reflection:$versionSchemaKenerator") + implementation("io.github.smiley4:schema-kenerator-serialization:$versionSchemaKenerator") + implementation("io.github.smiley4:schema-kenerator-swagger:$versionSchemaKenerator") + implementation("io.github.smiley4:schema-kenerator-jackson:$versionSchemaKenerator") + + implementation("io.swagger.parser.v3:swagger-parser:$versionSwaggerParser") + implementation("io.github.microutils:kotlin-logging-jvm:$versionKotlinLogging") + implementation("ch.qos.logback:logback-classic:$versionLogback") +} + +kotlin { + jvmToolchain(11) +} \ No newline at end of file diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Authentication.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Authentication.kt new file mode 100644 index 0000000..fcd0520 --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Authentication.kt @@ -0,0 +1,98 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.data.AuthScheme +import io.github.smiley4.ktorswaggerui.data.AuthType +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.UserIdPrincipal +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.basic +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install "Authentication"-Plugin and setup e.g. Basic-Auth + // username = "user", password = "pass" + install(Authentication) { + basic { + realm = "Access to the API" + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + + // Install and configure the "SwaggerUI"-Plugin + install(SwaggerUI) { + security { + // configure a basic-auth security scheme + securityScheme("MySecurityScheme") { + type = AuthType.HTTP + scheme = AuthScheme.BASIC + } + // if no other security scheme is specified for a route, the one with this name is used instead + defaultSecuritySchemeNames = setOf("MySecurityScheme") + // if no other response is documented for "401 Unauthorized", this information is used instead + defaultUnauthorizedResponse { + description = "Username or password is invalid" + } + } + } + + routing { + + // add the routes for swagger-ui and api-spec + route("swagger") { + swaggerUI("/api.json") + } + route("api.json") { + openApiSpec() + } + + authenticate { + // route is in an "authenticate"-block -> default security-scheme will be used (if not specified otherwise) + get("protected", { + // response for "401 Unauthorized" is automatically added if configured in the plugin-config and not specified otherwise + }) { + call.respondText("Hello World!") + } + } + + // route is not in an "authenticate"-block but "protected"-flag is set (e.g. because is it protected by an external reverse-proxy + // -> specified or default security scheme is used and default "401 Unauthorized" is added if not specified otherwise + get("externally-protected", { + // manually specify that this route requires authentication + protected = true + }) { + call.respondText("Hello World!") + } + + // route is not in an "authenticate"-block and "protected"-flag is not set + // -> security schemes will be ignored and not default "401 Unauthorized" response is added + get("unprotected", { + securitySchemeNames = listOf("MySecurityScheme") + }) { + call.respondText("Hello World!") + } + + } + +} diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Basics.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Basics.kt new file mode 100644 index 0000000..95222a4 --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Basics.kt @@ -0,0 +1,84 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install and configure the "SwaggerUI"-Plugin + install(SwaggerUI) { + // configure basic information about the api + info { + title = "Example API" + description = "An example api to showcase basic swagger-ui functionality." + } + // provide a reference to an external documentation + externalDocs { + url = "https://github.com/SMILEY4/ktor-swagger-ui/wiki" + description = "Sample external documentation" + } + // configure the servers from where the api is being served + server { + url = "http://localhost:8080" + description = "Development Server" + } + server { + url = "https://www.example.com" + description = "Production Server" + } + } + + routing { + + // Create a route for the swagger-ui using the openapi-spec at "/api.json". + // This route will not be included in the spec. + route("swagger") { + swaggerUI("/api.json") + } + // Create a route for the openapi-spec file. + // This route will not be included in the spec. + route("api.json") { + openApiSpec() + } + + // a documented route + get("hello", { + // description of the route + description = "A Hello-World route" + // information about the request + request { + // information about the query-parameter "name" of type "string" + queryParameter("name") { + description = "the name to greet" + } + } + // information about possible responses + response { + // information about a "200 OK" response + HttpStatusCode.OK to { + // a description of the response + description = "successful request - always returns 'Hello World!'" + } + } + }) { + call.respondText("Hello ${call.request.queryParameters["name"]}") + } + + } + +} diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteConfig.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteConfig.kt new file mode 100644 index 0000000..f6312db --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteConfig.kt @@ -0,0 +1,212 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.data.AuthScheme +import io.github.smiley4.ktorswaggerui.data.AuthType +import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort +import io.github.smiley4.ktorswaggerui.data.SwaggerUiSyntaxHighlight +import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.github.smiley4.schemakenerator.reflection.processReflection +import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot +import io.github.smiley4.schemakenerator.swagger.data.TitleType +import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema +import io.github.smiley4.schemakenerator.swagger.withAutoTitle +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import io.swagger.v3.oas.models.media.Schema +import java.io.File + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +class Greeting( + val name: String +) + +/** + * A (nearly) complete - and mostly nonsensical - plugin configuration + */ +private fun Application.myModule() { + + + install(SwaggerUI) { + info { + title = "Example API" + version = "latest" + description = "An example api." + termsOfService = "example.com" + contact { + name = "Mr. Example" + url = "example.com" + email = "example@example.com" + } + license { + name = "Example License" + url = "example.com" + identifier = "Apache-2.0" + } + } + externalDocs { + url = "example.com" + description = "Project documentation" + } + server { + url = "localhost" + description = "local dev-server" + variable("version") { + default = "1.0" + enum = setOf("1.0", "2.0", "3.0") + description = "the version of the server api" + } + } + server { + url = "example.com" + description = "productive server" + variable("version") { + default = "1.0" + enum = setOf("1.0", "2.0") + description = "the version of the server api" + } + } + swagger { + displayOperationId = true + showTagFilterInput = true + sort = SwaggerUiSort.HTTP_METHOD + syntaxHighlight = SwaggerUiSyntaxHighlight.MONOKAI + withCredentials = false + } + security { + defaultUnauthorizedResponse { + description = "Username or password is invalid" + } + defaultSecuritySchemeNames = setOf("MySecurityScheme") + securityScheme("MySecurityScheme") { + type = AuthType.HTTP + scheme = AuthScheme.BASIC + } + } + tags { + tagGenerator = { url -> listOf(url.firstOrNull()) } + tag("users") { + description = "routes to manage users" + externalDocUrl = "example.com" + externalDocDescription = "Users documentation" + } + tag("documents") { + description = "routes to manage documents" + externalDocUrl = "example.com" + externalDocDescription = "Document documentation" + } + } + schemas { + schema("string") + generator = { type -> + type + .processReflection() + .generateSwaggerSchema() + .withAutoTitle(TitleType.SIMPLE) + .compileReferencingRoot() + } + overwrite(Schema().also { + it.type = "string" + it.format = "binary" + }) + } + examples { + example("Id 1") { + description = "First example id" + value = "12345" + } + example("Id 2") { + description = "Second example id" + value = "54321" + + } + } + specAssigner = { _, _ -> PluginConfigDsl.DEFAULT_SPEC_ID } + pathFilter = { _, url -> url.firstOrNull() != "hidden" } + ignoredRouteSelectors = emptySet() + postBuild = { api -> println("Completed api: $api") } + } + + + routing { + + // add the routes for swagger-ui and api-spec + route("swagger") { + swaggerUI("/api.json") + } + route("api.json") { + openApiSpec() + } + + // a documented route + get("hello", { + operationId = "hello" + summary = "hello world route" + description = "A Hello-World route as an example." + tags = setOf("hello", "example") + specId = PluginConfigDsl.DEFAULT_SPEC_ID + deprecated = false + hidden = false + protected = false + securitySchemeNames = emptyList() + externalDocs { + url = "example.com/hello" + description = "external documentation of 'hello'-route" + } + request { + queryParameter("name") { + description = "the name to greet" + example("Josh") { + value = "Josh" + summary = "Example name" + description = "An example name for this query parameter" + } + } + body() + } + response { + HttpStatusCode.OK to { + description = "successful request - always returns 'Hello World!'" + header("x-random") { + description = "A header with some random number" + required = true + deprecated = false + explode = false + } + body { + description = "the greeting object with the name of the person to greet." + mediaTypes = setOf(ContentType.Application.Json) + required = true + } + } + } + server { + url = "example.com" + description = "productive server for 'hello'-route" + variable("version") { + default = "1.0" + enum = setOf("1.0", "2.0") + description = "the version of the server api" + } + } + }) { + call.respondText("Hello ${call.request.queryParameters["name"]}") + } + + } + +} diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomizedSchemaGenerator.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomizedSchemaGenerator.kt new file mode 100644 index 0000000..5940244 --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomizedSchemaGenerator.kt @@ -0,0 +1,80 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.github.smiley4.schemakenerator.serialization.processKotlinxSerialization +import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot +import io.github.smiley4.schemakenerator.swagger.data.TitleType +import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema +import io.github.smiley4.schemakenerator.swagger.withAutoTitle +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respond +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import kotlinx.serialization.Serializable + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install and configure the "SwaggerUI"-Plugin + install(SwaggerUI) { + schemas { + // replace default schema-generator with customized one + generator = { type -> + type + // process type using kotlinx-serialization instead of reflection + // requires additional dependency "io.github.smiley4:schema-kenerator-kotlinx-serialization:" + // see https://github.com/SMILEY4/schema-kenerator for more information + .processKotlinxSerialization() + .generateSwaggerSchema() + .withAutoTitle(TitleType.SIMPLE) + .compileReferencingRoot() + } + } + } + + routing { + + // Create a route for the swagger-ui using the openapi-spec at "/api.json". + // This route will not be included in the spec. + route("swagger") { + swaggerUI("/api.json") + } + // Create a route for the openapi-spec file. + // This route will not be included in the spec. + route("api.json") { + openApiSpec() + } + + // a documented route + get("hello", { + // information about the request + response { + // information about a "200 OK" response + HttpStatusCode.OK to { + // body of the response + body() + } + } + }) { + call.respond(HttpStatusCode.NotImplemented, "...") + } + + } + +} + +@Serializable +private class MyResponseBody( + val name: String, +) \ No newline at end of file diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Examples.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Examples.kt new file mode 100644 index 0000000..4c47d4d --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Examples.kt @@ -0,0 +1,96 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install and customize the "SwaggerUI"-Plugin + install(SwaggerUI) { + examples { + + // specify two shared examples + example("Shared A") { + description = "first shared example" + value = MyExampleClass( + someValue = "shared a" + ) + } + example("Shared B") { + description = "second shared example" + value = MyExampleClass( + someValue = "shared b" + ) + } + + } + } + + routing { + + // add the routes for swagger-ui and api-spec + route("swagger") { + swaggerUI("/api.json") + } + route("api.json") { + openApiSpec() + } + + + get("basic", { + request { + body { + // specify two example values + example("Example 1") { + description = "A first example value" + value = MyExampleClass( + someValue = "example 1" + ) + } + example("Example 2") { + description = "A second example value" + value = MyExampleClass( + someValue = "example 2" + ) + } + } + } + }) { + call.respondText("...") + } + + + get("reference-shared", { + request { + body { + // reference two shared examples specified in the plugin-config (and placed in the component section) + exampleRef("Example 1", "Shared A") + exampleRef("Example 2", "Shared B") + } + } + }) { + call.respondText("...") + } + + } + +} + + +private data class MyExampleClass( + val someValue: String +) diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/FileUpload.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/FileUpload.kt new file mode 100644 index 0000000..348af73 --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/FileUpload.kt @@ -0,0 +1,89 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.post +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respond +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import io.swagger.v3.oas.models.media.Schema +import java.io.File + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install the "SwaggerUI"-Plugin and use the default configuration + install(SwaggerUI) { + schemas { + // overwrite type "File" with custom schema for binary data + overwrite(Schema().also { + it.type = "string" + it.format = "binary" + }) + } + } + + routing { + + // add the routes for swagger-ui and api-spec + route("swagger") { + swaggerUI("/api.json") + } + route("api.json") { + openApiSpec() + } + + // upload a single file, either as png, jpeg or svg + post("single", { + request { + body { + mediaTypes = setOf( + ContentType.Image.PNG, + ContentType.Image.JPEG, + ContentType.Image.SVG, + ) + } + } + }) { + call.respond(HttpStatusCode.NotImplemented, "...") + } + + // upload multiple files + post("multipart", { + request { + multipartBody { + mediaTypes = setOf(ContentType.MultiPart.FormData) + part("first-image",) { + mediaTypes = setOf( + ContentType.Image.PNG, + ContentType.Image.JPEG, + ContentType.Image.SVG + ) + } + part("second-image") { + mediaTypes = setOf( + ContentType.Image.PNG, + ContentType.Image.JPEG, + ContentType.Image.SVG + ) + } + } + } + }) { + call.respond(HttpStatusCode.NotImplemented, "...") + } + + } + +} diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Minimal.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Minimal.kt new file mode 100644 index 0000000..81d105e --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Minimal.kt @@ -0,0 +1,48 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install the "SwaggerUI"-Plugin and use the default configuration + install(SwaggerUI) + + routing { + + // Create a route for the swagger-ui using the openapi-spec at "/api.json". + // This route will not be included in the spec. + route("swagger") { + swaggerUI("/api.json") + } + // Create a route for the openapi-spec file. + // This route will not be included in the spec. + route("api.json") { + openApiSpec() + } + + // a documented route + get("hello", { + // description of this route + description = "A Hello-World route" + }) { + call.respondText("Hello World!") + } + + } + +} diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecs.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecs.kt new file mode 100644 index 0000000..00d0bc9 --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecs.kt @@ -0,0 +1,109 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.dsl.routing.route +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install and configure the "SwaggerUI"-Plugin + install(SwaggerUI) { + // "global" configuration for all specs + info { + title = "Example API" + } + // configuration specific for spec "v1", overwrites global config + spec("version1") { + info { + version = "1.0" + } + } + // configuration specific for spec "v2", overwrites global config + spec("version2") { + info { + version = "2.0" + } + } + // assign all unassigned routes to spec "v2" (here only route '/greet') + specAssigner = {_, _ -> "version2"} + } + + routing { + + // add routes for "v1"-spec and swagger-ui + route("v1") { + route("swagger") { + // swagger-ui using '/v1/api.json' + swaggerUI("/v1/api.json") + } + route("api.json") { + // api-spec containing all routes assigned to "v1" + openApiSpec("version1") + } + } + + // add routes for "v2"-spec and swagger-ui + route("v2") { + route("swagger") { + // swagger-ui using '/v2/api.json' + swaggerUI("/v2/api.json") + } + route("api.json") { + // api-spec containing all routes assigned to "v2" + openApiSpec("version2") + } + } + + + // version 1.0 routes + route("v1", { + specId = "version1" + }) { + + // "hello"-route in version 1.0 + get("hello", { + description = "Version 1 'Hello World'" + }) { + call.respondText("Hello World!") + } + + } + + // version 2.0 routes + route("v2", { + specId = "version2" + }) { + + // "hello"-route in version 2.0 + get("hello", { + description = "Version 2 'Hello World'" + }) { + call.respondText("Hello World! (improved)") + } + + } + + // unassigned route + get("greet", { + description = "Alternative route not manually assigned to any spec." + }) { + call.respondText("Alternative Hello World!") + } + + } + +} \ No newline at end of file diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt new file mode 100644 index 0000000..6cb299f --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt @@ -0,0 +1,261 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.delete +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.dsl.routing.post +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respond +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + + +/** + * Uses the OpenApi-Example "petstore-simple" to demonstrate ktor with swagger-ui + * https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v2.0/json/petstore-simple.json + */ +private fun Application.myModule() { + + install(SwaggerUI) { + info { + title = "Swagger Petstore" + version = "1.0.0" + description = "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification" + termsOfService = "http://swagger.io/terms/" + contact { + name = "Swagger API Team" + } + license { + name = "MIT" + } + } + examples { + example("Unexpected Error") { + value = ErrorModel("Unexpected Error") + } + } + } + + routing { + + route("swagger") { + swaggerUI("/api.json") + } + route("api.json") { + openApiSpec() + } + + route("/pets") { + + get({ + operationId = "findPets" + description = "Returns all pets from the system that the user has access to" + request { + queryParameter>("tags") { + description = "tags to filter by" + required = false + example("dog") { + value = "default" + } + } + queryParameter("limit") { + description = "maximum number of results to return" + required = false + example("default") { + value = 100 + } + } + } + response { + HttpStatusCode.OK to { + body> { + description = "the list of available pets" + example("Pet List") { + value = listOf( + Pet( + id = 123, + name = "Big Bird", + tag = "bird" + ), + Pet( + id = 456, + name = "Charlie", + tag = "dog" + ) + ) + } + } + } + default { + body { + description = "unexpected error" + exampleRef("Unexpected Error") + } + } + } + }) { + call.respond(HttpStatusCode.NotImplemented, Unit) + } + + post({ + operationId = "addPet" + description = "Creates a new pet in the store. Duplicates are allowed" + request { + body { + description = "Pet to add to the store" + required = true + example("New Bird") { + value = NewPet( + name = "Big Bird", + tag = "bird" + ) + } + example("New Dog") { + value = NewPet( + name = "Charlie", + tag = "dog" + ) + } + } + } + response { + HttpStatusCode.OK to { + body { + description = "the created pet" + example("Bird") { + value = Pet( + id = 123, + name = "Big Bird", + tag = "bird" + ) + } + example("Dog") { + value = Pet( + id = 456, + name = "Charlie", + tag = "dog" + ) + } + } + } + default { + body { + description = "unexpected error" + exampleRef("Unexpected Error") + } + } + } + }) { + call.respond(HttpStatusCode.NotImplemented, Unit) + } + + route("{id}") { + + get({ + operationId = "findBetById" + description = "Returns a pet based on a single ID." + request { + pathParameter("id") { + description = "Id of pet to fetch" + required = true + example("default") { + value = 123L + } + } + } + response { + HttpStatusCode.OK to { + body{ + description = "the pet with the given id" + example("Bird") { + value = Pet( + id = 123, + name = "Big Bird", + tag = "bird" + ) + } + example("Dog") { + value = Pet( + id = 123, + name = "Charlie", + tag = "dog" + ) + } + } + } + HttpStatusCode.NotFound to { + description = "the pet with the given id was not found" + } + default { + body { + description = "unexpected error" + exampleRef("Unexpected Error") + } + } + } + }) { + call.respond(HttpStatusCode.NotImplemented, Unit) + } + + delete({ + operationId = "deletePet" + description = "deletes a single pet based on the supplied ID" + request { + pathParameter("id") { + description = "Id of pet to delete" + required = true + example("default") { + value = 123L + } + } + } + response { + HttpStatusCode.NoContent to { + description = "the pet was successfully deleted" + } + HttpStatusCode.NotFound to { + description = "the pet with the given id was not found" + } + default { + body { + description = "unexpected error" + exampleRef("Unexpected Error") + } + } + } + }) { + call.respond(HttpStatusCode.NotImplemented, Unit) + } + + } + } + + } + +} + +private data class Pet( + val id: Long, + val name: String, + val tag: String +) + +private data class NewPet( + val name: String, + val tag: String +) + +private data class ErrorModel( + val message: String +) \ No newline at end of file diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/RequestResponse.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/RequestResponse.kt new file mode 100644 index 0000000..3dc9229 --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/RequestResponse.kt @@ -0,0 +1,137 @@ +package io.github.smiley4.ktorswaggerui.examples + +import com.fasterxml.jackson.core.util.DefaultIndenter +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter +import com.fasterxml.jackson.databind.SerializationFeature +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.routing.post +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.jackson.jackson +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.route +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install the "SwaggerUI"-Plugin and use the default configuration + install(SwaggerUI) + + install(ContentNegotiation) { + jackson { + configure(SerializationFeature.INDENT_OUTPUT, true) + setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { + indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) + indentObjectsWith(DefaultIndenter(" ", "\n")) + }) + } + } + + routing { + + // add the routes for swagger-ui and api-spec + route("swagger") { + swaggerUI("/api.json") + } + route("api.json") { + openApiSpec() + } + + // a documented route + post("calculate", { + // information about the request + request { + // specify the schema of the request-body and some additional information + body { + description = "the requested operation and values to perform the operation on" + required = true + } + } + // information the possible responses + response { + // document the "200 OK"-response + HttpStatusCode.OK to { + description = "Calculation was performed successfully." + // specify the schema of the response-body and some additional information + body { + description = "the result of an operation together with the original request" + } + } + // document the "422 UnprocessableEntity"-response + HttpStatusCode.UnprocessableEntity to { + description = "The requested calculation could not be performed, e.g. due to division by zero." + } + } + }) { + call.receive().let { calculation -> + when (calculation.operation) { + OperationType.ADD -> { + call.respond( + HttpStatusCode.OK, CalculationResult( + calculation = calculation, + result = calculation.a + calculation.b + ) + ) + } + OperationType.SUB -> { + call.respond( + HttpStatusCode.OK, CalculationResult( + calculation = calculation, + result = calculation.a - calculation.b + ) + ) + } + OperationType.MUL -> { + call.respond( + HttpStatusCode.OK, CalculationResult( + calculation = calculation, + result = calculation.a * calculation.b + ) + ) + } + OperationType.DIV -> { + if (calculation.b == 0f) { + call.respond(HttpStatusCode.UnprocessableEntity) + } else { + call.respond( + HttpStatusCode.OK, CalculationResult( + calculation = calculation, + result = calculation.a / calculation.b + ) + ) + } + } + } + } + } + + } + +} + +private enum class OperationType { + ADD, SUB, MUL, DIV +} + +private data class Calculation( + val operation: OperationType, + val a: Float, + val b: Float +) + +private data class CalculationResult( + val calculation: Calculation, + val result: Float +) \ No newline at end of file diff --git a/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Schemas.kt b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Schemas.kt new file mode 100644 index 0000000..b078b9a --- /dev/null +++ b/ktor-swagger-ui-examples/src/main/kotlin/io/github/smiley4/ktorswaggerui/examples/Schemas.kt @@ -0,0 +1,175 @@ +package io.github.smiley4.ktorswaggerui.examples + +import com.fasterxml.jackson.annotation.JsonSubTypes +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.data.anyOf +import io.github.smiley4.ktorswaggerui.data.array +import io.github.smiley4.ktorswaggerui.data.ref +import io.github.smiley4.ktorswaggerui.dsl.routing.get +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.github.smiley4.schemakenerator.core.connectSubTypes +import io.github.smiley4.schemakenerator.jackson.collectJacksonSubTypes +import io.github.smiley4.schemakenerator.reflection.processReflection +import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot +import io.github.smiley4.schemakenerator.swagger.data.TitleType +import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema +import io.github.smiley4.schemakenerator.swagger.withAutoTitle +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import io.swagger.v3.oas.models.media.Schema +import java.time.LocalDateTime + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + // Install and customize the "SwaggerUI"-Plugin + install(SwaggerUI) { + schemas { + + // add a swagger schema to the component-section of the api-spec with the id "swagger-schema" + schema("swagger-schema", Schema().also { + it.type = "number" + it.title = "Custom Type" + }) + + // add a type to the component-section of the api-spec with the id "type-schema" + schema("type-schema") + + // overwrite 'LocalDateTime' with custom schema (root only) + overwrite(Schema().also { + it.title = "timestamp" + it.type = "integer" + }) + + // customized schema generation pipeline + generator = { type -> + type + .collectJacksonSubTypes(typeProcessing = { types -> types.processReflection() }) // include types from jackson subtype-annotation + .processReflection() + .connectSubTypes() // connect the supertypes with their subtypes + .generateSwaggerSchema() + .withAutoTitle(TitleType.SIMPLE) + .compileReferencingRoot() + } + + } + } + + routing { + + // add the routes for swagger-ui and api-spec + route("swagger") { + swaggerUI("/api.json") + } + route("api.json") { + openApiSpec() + } + + + get("basic", { + request { + // directly specify the schema type + body() + } + }) { + call.respondText("...") + } + + + get("global-swagger-schema", { + request { + // reference and use the schema from the component-section with the id "swagger-schema" + body(ref("swagger-schema")) + } + }) { + call.respondText("...") + } + + + get("global-type-schema", { + request { + // reference and use the schema from the component-section with the id "type-schema" + body(ref("type-schema")) + } + }) { + call.respondText("...") + } + + + get("array-schema", { + request { + // an array of items with the referenced schema with the id "type-schema" + body( + array( + ref("type-schema") + ) + ) + } + }) { + call.respondText("...") + } + + + get("anyof-schema", { + request { + // either the referenced schema with id "type-schema" or "swagger-schema" + body( + anyOf( + ref("type-schema"), + ref("swagger-schema") + ) + ) + } + }) { + call.respondText("...") + } + + + get("type-overwrite", { + request { + // schema is not generated the normal way but the overwriting schema from the config is used instead + body() + } + }) { + call.respondText("...") + } + + + get("jackson-subtypes", { + request { + // jackson subtypes are detected automatically + body() + } + }) { + call.respondText("...") + } + + } + +} + + +private data class MySchemaClass( + val someValue: String +) + + +@JsonSubTypes( + JsonSubTypes.Type(value = SubTypeA::class), + JsonSubTypes.Type(value = SubTypeB::class), +) +private open class BaseType(val base: String) + +private class SubTypeA(base: String, val a: Int) : BaseType(base) + +private class SubTypeB(base: String, val b: Boolean) : BaseType(base) \ No newline at end of file diff --git a/src/test/resources/logback.xml b/ktor-swagger-ui-examples/src/main/resources/logback.xml similarity index 90% rename from src/test/resources/logback.xml rename to ktor-swagger-ui-examples/src/main/resources/logback.xml index 52ce2ed..b397c54 100644 --- a/src/test/resources/logback.xml +++ b/ktor-swagger-ui-examples/src/main/resources/logback.xml @@ -13,6 +13,5 @@ - \ No newline at end of file diff --git a/ktor-swagger-ui/build.gradle.kts b/ktor-swagger-ui/build.gradle.kts new file mode 100644 index 0000000..50b4389 --- /dev/null +++ b/ktor-swagger-ui/build.gradle.kts @@ -0,0 +1,124 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinJvm +import com.vanniktech.maven.publish.SonatypeHost +import io.gitlab.arturbosch.detekt.Detekt + +val projectGroupId: String by project +val projectVersion: String by project +group = projectGroupId +version = projectVersion + +plugins { + kotlin("jvm") + id("org.owasp.dependencycheck") + id("io.gitlab.arturbosch.detekt") + id("com.vanniktech.maven.publish") + id("org.jetbrains.dokka") +} + +repositories { + mavenCentral() +} + +dependencies { + val versionKtor: String by project + val versionSwaggerUI: String by project + val versionSwaggerParser: String by project + val versionSchemaKenerator: String by project + val versionKotlinLogging: String by project + val versionKotest: String by project + val versionKotlinTest: String by project + val versionMockk: String by project + + implementation("io.ktor:ktor-server-core-jvm:$versionKtor") + implementation("io.ktor:ktor-server-webjars:$versionKtor") + implementation("io.ktor:ktor-server-auth:$versionKtor") + implementation("io.ktor:ktor-server-resources:$versionKtor") + + implementation("org.webjars:swagger-ui:$versionSwaggerUI") + + implementation("io.swagger.parser.v3:swagger-parser:$versionSwaggerParser") + + implementation("io.github.smiley4:schema-kenerator-core:$versionSchemaKenerator") + implementation("io.github.smiley4:schema-kenerator-reflection:$versionSchemaKenerator") + implementation("io.github.smiley4:schema-kenerator-swagger:$versionSchemaKenerator") + + implementation("io.github.microutils:kotlin-logging-jvm:$versionKotlinLogging") + + testImplementation("io.ktor:ktor-server-netty-jvm:$versionKtor") + testImplementation("io.ktor:ktor-server-content-negotiation:$versionKtor") + testImplementation("io.ktor:ktor-serialization-jackson:$versionKtor") + testImplementation("io.ktor:ktor-server-auth:$versionKtor") + testImplementation("io.ktor:ktor-server-call-logging:$versionKtor") + testImplementation("io.ktor:ktor-server-test-host:$versionKtor") + testImplementation("io.kotest:kotest-runner-junit5:$versionKotest") + testImplementation("io.kotest:kotest-assertions-core:$versionKotest") + testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest") + testImplementation("io.mockk:mockk:$versionMockk") +} + +kotlin { + jvmToolchain(11) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +detekt { + ignoreFailures = false + buildUponDefaultConfig = true + allRules = false + config.setFrom("$projectDir/../detekt/detekt.yml") +} +tasks.withType().configureEach { + reports { + html.required.set(true) + md.required.set(true) + xml.required.set(false) + txt.required.set(false) + sarif.required.set(false) + } +} + +mavenPublishing { + val projectGroupId: String by project + val projectVersion: String by project + val projectArtifactIdBase: String by project + val projectNameBase: String by project + val projectDescriptionBase: String by project + val projectScmUrl: String by project + val projectScmConnection: String by project + val projectLicenseName: String by project + val projectLicenseUrl: String by project + val projectDeveloperName: String by project + val projectDeveloperUrl: String by project + + configure(KotlinJvm(JavadocJar.Dokka("dokkaHtml"), true)) + publishToMavenCentral(SonatypeHost.S01) + signAllPublications() + coordinates(projectGroupId, projectArtifactIdBase, projectVersion) + pom { + name.set(projectNameBase) + description.set(projectDescriptionBase) + url.set(projectScmUrl) + licenses { + license { + name.set(projectLicenseName) + url.set(projectLicenseUrl) + distribution.set(projectLicenseUrl) + } + } + scm { + url.set(projectScmUrl) + connection.set(projectScmConnection) + } + developers { + developer { + id.set(projectDeveloperName) + name.set(projectDeveloperName) + url.set(projectDeveloperUrl) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt similarity index 62% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index 559423d..6833c6a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -1,36 +1,53 @@ package io.github.smiley4.ktorswaggerui -import com.fasterxml.jackson.databind.ObjectMapper import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext -import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.builder.openapi.* +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextImpl +import io.github.smiley4.ktorswaggerui.builder.openapi.ComponentsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContactBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.InfoBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.LicenseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OpenApiBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecuritySchemesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagExternalDocumentationBuilder import io.github.smiley4.ktorswaggerui.builder.route.RouteCollector import io.github.smiley4.ktorswaggerui.builder.route.RouteDocumentationMerger import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextImpl import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl import io.github.smiley4.ktorswaggerui.routing.ApiSpec -import io.github.smiley4.ktorswaggerui.routing.ControllerUtils -import io.github.smiley4.ktorswaggerui.routing.ForwardRouteController -import io.github.smiley4.ktorswaggerui.routing.SwaggerController -import io.ktor.server.application.* -import io.ktor.server.application.hooks.* -import io.ktor.server.routing.* -import io.ktor.server.webjars.* +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationStarted +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.install +import io.ktor.server.application.plugin +import io.ktor.server.application.pluginOrNull +import io.ktor.server.routing.Routing +import io.ktor.server.webjars.Webjars import io.swagger.v3.core.util.Json import mu.KotlinLogging -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set /** * This version must match the version of the gradle dependency */ -internal const val SWAGGER_UI_WEBJARS_VERSION = "5.9.0" +internal const val SWAGGER_UI_WEBJARS_VERSION = "5.17.11" private val logger = KotlinLogging.logger {} @@ -45,36 +62,20 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration } try { - ControllerUtils.appConfig = applicationConfig val routes = routes(application, config) ApiSpec.setAll(buildOpenApiSpecs(config, routes)) + ApiSpec.swaggerUiConfig = config.swagger } catch (e: Exception) { logger.error("Error during application startup in swagger-ui-plugin", e) } - if (config.swaggerUI.automaticRouter) { - ApiSpec.getAll().forEach { (specId, json) -> - val specConfig = config.specConfigs[specId] ?: config - SwaggerController( - applicationConfig!!, - specConfig, - SWAGGER_UI_WEBJARS_VERSION, - if (ApiSpec.getAll().size > 1) specId else null, - json - ).setup(application) - if (ApiSpec.getAll().size == 1 && config.swaggerUI.forwardRoot) { - ForwardRouteController(applicationConfig!!, config).setup(application) - } - } - } - } } private fun buildOpenApiSpecs(config: PluginConfigData, routes: List): Map { val routesBySpec = buildMap> { routes.forEach { route -> - val specName = route.documentation.specId ?: config.specAssigner(route.path, route.documentation.tags) + val specName = route.documentation.specId ?: config.specAssigner(route.path, route.documentation.tags.toList()) computeIfAbsent(specName) { mutableListOf() }.add(route) } } @@ -88,10 +89,16 @@ private fun buildOpenApiSpecs(config: PluginConfigData, routes: List) private fun buildOpenApiSpec(pluginConfig: PluginConfigData, routes: List): String { return try { - val schemaContext = schemaContext(pluginConfig, routes) - val exampleContext = exampleContext(pluginConfig, routes) + val schemaContext = SchemaContextImpl(pluginConfig.schemaConfig).also { + it.addGlobal(pluginConfig.schemaConfig) + it.add(routes) + } + val exampleContext = ExampleContextImpl().also { + it.addShared(pluginConfig.exampleConfig) + it.add(routes) + } val openApi = builder(pluginConfig, schemaContext, exampleContext).build(routes) - pluginConfig.whenBuildOpenApiSpecs?.invoke(openApi) + pluginConfig.postBuild?.invoke(openApi) Json.pretty(openApi) } catch (e: Exception) { logger.error("Error during openapi-generation", e) @@ -105,30 +112,10 @@ private fun routes(application: Application, config: PluginConfigData): List): SchemaContext { - return SchemaContextBuilder( - config = config, - schemaBuilder = SchemaBuilder( - definitionsField = config.encoding.schemaDefsField, - schemaEncoder = config.encoding.schemaEncoder, - ObjectMapper(), - TypeOverwrites.get() - ), - ).build(routes.toList()) -} - -private fun exampleContext(config: PluginConfigData, routes: List): ExampleContext { - return ExampleContextBuilder( - exampleBuilder = ExampleBuilder( - config = config - ) - ).build(routes.toList()) -} - private fun builder( config: PluginConfigData, schemaContext: SchemaContext, - exampleContext: ExampleContext + exampleContext: ExampleContext, ): OpenApiBuilder { return OpenApiBuilder( config = config, @@ -149,7 +136,7 @@ private fun builder( operationTagsBuilder = OperationTagsBuilder(config), parameterBuilder = ParameterBuilder( schemaContext = schemaContext, - exampleContext = exampleContext + exampleContext = exampleContext, ), requestBodyBuilder = RequestBodyBuilder( contentBuilder = ContentBuilder( @@ -170,6 +157,8 @@ private fun builder( config = config ), securityRequirementsBuilder = SecurityRequirementsBuilder(config), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ) ) ), diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt new file mode 100644 index 0000000..1e2de1f --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt @@ -0,0 +1,21 @@ +package io.github.smiley4.ktorswaggerui.builder.example + +import io.github.smiley4.ktorswaggerui.data.ExampleDescriptor +import io.swagger.v3.oas.models.examples.Example + +/** + * Provides examples for an openapi-spec + */ +interface ExampleContext { + + /** + * Get an [Example] (or a ref to an example) by its [ExampleDescriptor] + */ + fun getExample(descriptor: ExampleDescriptor): Example + + + /** + * Get all examples placed in the components-section of the spec. + */ + fun getComponentSection(): Map +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextImpl.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextImpl.kt new file mode 100644 index 0000000..5673180 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextImpl.kt @@ -0,0 +1,97 @@ +package io.github.smiley4.ktorswaggerui.builder.example + +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.data.ExampleConfigData +import io.github.smiley4.ktorswaggerui.data.ExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiSimpleBodyData +import io.github.smiley4.ktorswaggerui.data.RefExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.SwaggerExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.ValueExampleDescriptor +import io.swagger.v3.oas.models.examples.Example + +/** + * Implementation of an [ExampleContext]. + */ +class ExampleContextImpl : ExampleContext { + + private val rootExamples = mutableMapOf() + private val componentExamples = mutableMapOf() + + + /** + * Add all global/shared examples from the config that are placed in the components-section of the openapi-spec + */ + fun addShared(config: ExampleConfigData) { + config.sharedExamples.forEach { (_, exampleDescriptor) -> + val example = generateExample(exampleDescriptor) + componentExamples[exampleDescriptor.name] = example + } + } + + + /** + * Collect and add all examples for the given routes + */ + fun add(routes: Collection) { + collectExampleDescriptors(routes).forEach { exampleDescriptor -> + rootExamples[exampleDescriptor] = generateExample(exampleDescriptor) + } + } + + + /** + * Collect all [ExampleDescriptor]s from the given routes + */ + private fun collectExampleDescriptors(routes: Collection): List { + val descriptors = mutableListOf() + routes + .filter { !it.documentation.hidden } + .forEach { route -> + route.documentation.request.also { request -> + request.parameters.forEach { parameter -> + parameter.example?.also { descriptors.add(it) } + } + request.body?.also { body -> + if (body is OpenApiSimpleBodyData) { + descriptors.addAll(body.examples) + } + } + } + route.documentation.responses.forEach { response -> + response.body?.also { body -> + if (body is OpenApiSimpleBodyData) { + descriptors.addAll(body.examples) + } + } + } + } + return descriptors + } + + + /** + * Generate a swagger [Example] from the given [ExampleDescriptor] + */ + private fun generateExample(exampleDescriptor: ExampleDescriptor): Example { + return when (exampleDescriptor) { + is ValueExampleDescriptor -> Example().also { + it.value = exampleDescriptor.value + it.summary = exampleDescriptor.summary + it.description = exampleDescriptor.description + } + is RefExampleDescriptor -> Example().also { + it.`$ref` = "#/components/examples/${exampleDescriptor.refName}" + } + is SwaggerExampleDescriptor -> exampleDescriptor.example + } + } + + override fun getExample(descriptor: ExampleDescriptor): Example { + return rootExamples[descriptor] ?: throw NoSuchElementException("no root-example for given example-descriptor") + } + + override fun getComponentSection(): Map { + return componentExamples + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt similarity index 68% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt index 7d09097..c39dc3b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ComponentsBuilder.kt @@ -5,6 +5,10 @@ import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.examples.Example import io.swagger.v3.oas.models.media.Schema +/** + * Builds the openapi [Components]-object containing shared reusable schemas and examples. + * See [OpenAPI Specification - Components Object](https://swagger.io/specification/#components-object). + */ class ComponentsBuilder( private val config: PluginConfigData, private val securitySchemesBuilder: SecuritySchemesBuilder @@ -14,8 +18,8 @@ class ComponentsBuilder( return Components().also { it.schemas = schemas it.examples = examples - if (config.securitySchemes.isNotEmpty()) { - it.securitySchemes = securitySchemesBuilder.build(config.securitySchemes) + if (config.securityConfig.securitySchemes.isNotEmpty()) { + it.securitySchemes = securitySchemesBuilder.build(config.securityConfig.securitySchemes) } } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt similarity index 65% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt index 5fc0e48..b09e227 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContactBuilder.kt @@ -3,6 +3,10 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.data.ContactData import io.swagger.v3.oas.models.info.Contact +/** + * Builds the openapi [Contact]-object. Holds Contact information for the exposed API. + * See [OpenAPI Specification - Contact Object](https://swagger.io/specification/#contact-object). + */ class ContactBuilder { fun build(contact: ContactData): Contact = diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt new file mode 100644 index 0000000..bd9b970 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt @@ -0,0 +1,110 @@ +package io.github.smiley4.ktorswaggerui.builder.openapi + +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.data.OpenApiBaseBodyData +import io.github.smiley4.ktorswaggerui.data.OpenApiMultipartBodyData +import io.github.smiley4.ktorswaggerui.data.OpenApiSimpleBodyData +import io.ktor.http.ContentType +import io.swagger.v3.oas.models.media.Content +import io.swagger.v3.oas.models.media.Encoding +import io.swagger.v3.oas.models.media.MediaType +import io.swagger.v3.oas.models.media.Schema +import kotlin.collections.set + +/** + * Builds the openapi [Content]-object for request and response bodies. + * See [OpenAPI Specification - Request Body Object](https://swagger.io/specification/#request-body-object) + * and [OpenAPI Specification - Response Object](https://swagger.io/specification/#response-object). + */ +class ContentBuilder( + private val schemaContext: SchemaContext, + private val exampleContext: ExampleContext, + private val headerBuilder: HeaderBuilder +) { + + fun build(body: OpenApiBaseBodyData): Content = + when (body) { + is OpenApiSimpleBodyData -> buildSimpleBody(body) + is OpenApiMultipartBodyData -> buildMultipartBody(body) + } + + private fun buildSimpleBody(body: OpenApiSimpleBodyData): Content = + Content().also { content -> + buildSimpleMediaTypes(body, schemaContext.getSchema(body.type)).forEach { (contentType, mediaType) -> + content.addMediaType(contentType.toString(), mediaType) + } + } + + private fun buildMultipartBody(body: OpenApiMultipartBodyData): Content { + return Content().also { content -> + buildMultipartMediaTypes(body).forEach { (contentType, mediaType) -> + content.addMediaType(contentType.toString(), mediaType) + } + } + } + + private fun buildSimpleMediaTypes(body: OpenApiSimpleBodyData, schema: Schema<*>?): Map { + val mediaTypes = body.mediaTypes.ifEmpty { schema?.let { setOf(chooseMediaType(schema)) } ?: setOf() } + return mediaTypes.associateWith { buildSimpleMediaType(schema, body) } + } + + private fun buildSimpleMediaType(schema: Schema<*>?, body: OpenApiSimpleBodyData): MediaType { + return MediaType().also { + it.schema = schema + body.examples.forEach { descriptor -> + it.addExamples(descriptor.name, exampleContext.getExample(descriptor)) + } + } + } + + private fun buildMultipartMediaTypes(body: OpenApiMultipartBodyData): Map { + val mediaTypes = body.mediaTypes.ifEmpty { setOf(ContentType.MultiPart.FormData) } + return mediaTypes.associateWith { buildMultipartMediaType(body) } + } + + private fun buildMultipartMediaType(body: OpenApiMultipartBodyData): MediaType { + return MediaType().also { mediaType -> + mediaType.schema = Schema().also { schema -> + schema.type = "object" + schema.properties = mutableMapOf?>().also { props -> + body.parts.forEach { part -> + props[part.name] = schemaContext.getSchema(part.type) + } + } + } + mediaType.encoding = buildMultipartEncoding(body) + } + } + + private fun buildMultipartEncoding(body: OpenApiMultipartBodyData): MutableMap? { + return if (body.parts.flatMap { it.mediaTypes }.isEmpty()) { + null + } else { + mutableMapOf().also { encodings -> + body.parts + .filter { it.mediaTypes.isNotEmpty() || it.headers.isNotEmpty() } + .forEach { part -> + encodings[part.name] = Encoding().apply { + contentType = part.mediaTypes.joinToString(", ") { it.toString() } + headers = part.headers.mapValues { headerBuilder.build(it.value) } + } + } + } + } + } + + private fun chooseMediaType(schema: Schema<*>): ContentType { + return when (schema.type) { + "integer" -> ContentType.Text.Plain + "number" -> ContentType.Text.Plain + "boolean" -> ContentType.Text.Plain + "string" -> ContentType.Text.Plain + "object" -> ContentType.Application.Json + "array" -> ContentType.Application.Json + null -> ContentType.Application.Json + else -> ContentType.Text.Plain + } + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt similarity index 62% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt index 49f4fd2..437dd49 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExternalDocumentationBuilder.kt @@ -3,6 +3,10 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.data.ExternalDocsData import io.swagger.v3.oas.models.ExternalDocumentation +/** + * Build the openapi [ExternalDocumentation]-object. Allows referencing an external resource for extended documentation. + * See [OpenAPI Specification - External Documentation Object](https://swagger.io/specification/#external-documentation-object). + */ class ExternalDocumentationBuilder { fun build(externalDocs: ExternalDocsData): ExternalDocumentation = diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt similarity index 56% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt index 4c0fd1a..0ecece3 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/HeaderBuilder.kt @@ -1,19 +1,26 @@ package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiHeader import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.data.OpenApiHeaderData +import io.swagger.v3.oas.models.ExternalDocumentation import io.swagger.v3.oas.models.headers.Header +/** + * Build the openapi [Header]-object. + * See [OpenAPI Specification - Header Object](https://swagger.io/specification/#header-object). + */ class HeaderBuilder( private val schemaContext: SchemaContext ) { - fun build(header: OpenApiHeader): Header = + fun build(header: OpenApiHeaderData): Header = Header().also { it.description = header.description it.required = header.required it.deprecated = header.deprecated it.schema = header.type?.let { t -> schemaContext.getSchema(t) } + it.explode = header.explode +// it.example = TODO() } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt similarity index 78% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt index bfc03dc..e620be4 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/InfoBuilder.kt @@ -3,6 +3,10 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.data.InfoData import io.swagger.v3.oas.models.info.Info +/** + * Build the openapi [Info]-object. Holds metadata about the API. + * See [OpenAPI Specification - Info Object](https://swagger.io/specification/#info-object). + */ class InfoBuilder( private val contactBuilder: ContactBuilder, private val licenseBuilder: LicenseBuilder @@ -20,6 +24,7 @@ class InfoBuilder( info.license?.also { license -> it.license = licenseBuilder.build(license) } + it.summary = info.summary } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt similarity index 58% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt index 30f727c..a6d5e1f 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/LicenseBuilder.kt @@ -3,12 +3,17 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.data.LicenseData import io.swagger.v3.oas.models.info.License +/** + * Build the openapi [License]-object. Holds license information for the exposed API. + * See [OpenAPI Specification - License Object](https://swagger.io/specification/#license-object). + */ class LicenseBuilder { fun build(license: LicenseData): License = License().also { it.name = license.name it.url = license.url + it.identifier = license.identifier } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt similarity index 85% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt index c8f8c96..4469201 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OAuthFlowsBuilder.kt @@ -6,6 +6,10 @@ import io.swagger.v3.oas.models.security.OAuthFlow import io.swagger.v3.oas.models.security.OAuthFlows import io.swagger.v3.oas.models.security.Scopes +/** + * Build the openapi [OAuthFlows]-object. Holds configuration of the supported OAuth Flows. + * See [OpenAPI Specification - OAuth Flows Object](https://swagger.io/specification/#oauth-flows-object). + */ class OAuthFlowsBuilder { fun build(flows: OpenIdOAuthFlowsData): OAuthFlows { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt similarity index 73% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt index f3adac8..359217a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OpenApiBuilder.kt @@ -1,11 +1,16 @@ package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext +import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.SpecVersion +/** + * Build the openapi [OpenAPI]-object. Is the root of the openapi document. + * See [OpenAPI Specification - OpenAPI Object](https://swagger.io/specification/#openapi-object). + */ class OpenApiBuilder( private val config: PluginConfigData, private val schemaContext: SchemaContext, @@ -20,12 +25,14 @@ class OpenApiBuilder( fun build(routes: Collection): OpenAPI { return OpenAPI().also { + it.specVersion = SpecVersion.V31 + it.openapi = "3.1.0" it.info = infoBuilder.build(config.info) it.externalDocs = externalDocumentationBuilder.build(config.externalDocs) it.servers = config.servers.map { server -> serverBuilder.build(server) } - it.tags = config.tags.map { tag -> tagBuilder.build(tag) } + it.tags = config.tagsConfig.tags.map { tag -> tagBuilder.build(tag) } it.paths = pathsBuilder.build(routes) - it.components = componentsBuilder.build(schemaContext.getComponentsSection(), exampleContext.getComponentsSection()) + it.components = componentsBuilder.build(schemaContext.getComponentSection(), exampleContext.getComponentSection()) } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt similarity index 60% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt index 3e48ce8..d793cb7 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationBuilder.kt @@ -3,12 +3,18 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.swagger.v3.oas.models.Operation +/** + * Build the openapi [Operation]-object. Holds information describing a single API operation on a path. + * See [OpenAPI Specification - Operation Object](https://swagger.io/specification/#operation-object). + */ class OperationBuilder( private val operationTagsBuilder: OperationTagsBuilder, private val parameterBuilder: ParameterBuilder, private val requestBodyBuilder: RequestBodyBuilder, private val responsesBuilder: ResponsesBuilder, - private val securityRequirementsBuilder: SecurityRequirementsBuilder + private val securityRequirementsBuilder: SecurityRequirementsBuilder, + private val externalDocumentationBuilder: ExternalDocumentationBuilder, + private val serverBuilder: ServerBuilder ) { fun build(route: RouteMeta): Operation = @@ -18,11 +24,11 @@ class OperationBuilder( it.operationId = route.documentation.operationId it.deprecated = route.documentation.deprecated it.tags = operationTagsBuilder.build(route) - it.parameters = route.documentation.getRequest().getParameters().map { param -> parameterBuilder.build(param) } - route.documentation.getRequest().getBody()?.let { body -> + it.parameters = route.documentation.request.parameters.map { param -> parameterBuilder.build(param) } + route.documentation.request.body?.let { body -> it.requestBody = requestBodyBuilder.build(body) } - it.responses = responsesBuilder.build(route.documentation.getResponses(), route.protected) + it.responses = responsesBuilder.build(route.documentation.responses, route.protected) if (route.protected) { securityRequirementsBuilder.build(route).also { securityRequirements -> if (securityRequirements.isNotEmpty()) { @@ -30,6 +36,10 @@ class OperationBuilder( } } } + it.externalDocs = route.documentation.externalDocs?.let { docs -> externalDocumentationBuilder.build(docs) } + if (route.documentation.servers.isNotEmpty()) { + it.servers = route.documentation.servers.map { server -> serverBuilder.build(server) } + } } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt similarity index 75% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt index 962ae04..3e8f7bf 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/OperationTagsBuilder.kt @@ -3,6 +3,9 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +/** + * Builds the list of tags for a single route. + */ class OperationTagsBuilder( private val config: PluginConfigData ) { @@ -16,6 +19,6 @@ class OperationTagsBuilder( private fun getRouteTags(route: RouteMeta) = route.documentation.tags - private fun getGeneratedTags(route: RouteMeta) = config.tagGenerator(route.path.split("/").filter { it.isNotEmpty() }) + private fun getGeneratedTags(route: RouteMeta) = config.tagsConfig.generator(route.path.split("/").filter { it.isNotEmpty() }) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt similarity index 51% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt index bbf7603..6f852c6 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ParameterBuilder.kt @@ -1,21 +1,26 @@ package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.data.OpenApiRequestParameterData +import io.github.smiley4.ktorswaggerui.data.ParameterLocation import io.swagger.v3.oas.models.parameters.Parameter +/** + * Build the openapi [Parameter]-object. Holds information describing a single operation (query, path or header) parameter. + * See [OpenAPI Specification - Parameter Object](https://swagger.io/specification/#parameter-object). + */ class ParameterBuilder( private val schemaContext: SchemaContext, - private val exampleContext: ExampleContext, + private val exampleContext: ExampleContext ) { - fun build(parameter: OpenApiRequestParameter): Parameter = + fun build(parameter: OpenApiRequestParameterData): Parameter = Parameter().also { it.`in` = when (parameter.location) { - OpenApiRequestParameter.Location.QUERY -> "query" - OpenApiRequestParameter.Location.HEADER -> "header" - OpenApiRequestParameter.Location.PATH -> "path" + ParameterLocation.QUERY -> "query" + ParameterLocation.HEADER -> "header" + ParameterLocation.PATH -> "path" } it.name = parameter.name it.description = parameter.description @@ -23,9 +28,10 @@ class ParameterBuilder( it.deprecated = parameter.deprecated it.allowEmptyValue = parameter.allowEmptyValue it.explode = parameter.explode - it.example = exampleContext.getExample(parameter) + it.example = parameter.example?.let { e -> exampleContext.getExample(e).value } // todo: example"S" ? it.allowReserved = parameter.allowReserved it.schema = schemaContext.getSchema(parameter.type) + it.style = parameter.style } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt similarity index 79% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt index a6c5c93..a0328e5 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathBuilder.kt @@ -4,6 +4,10 @@ import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta import io.ktor.http.HttpMethod import io.swagger.v3.oas.models.PathItem +/** + * Build the openapi [PathItem]-object. Holds information describing the operations available on a single path. + * See [OpenAPI Specification - Path Item Object](https://swagger.io/specification/#path-item-object). + */ class PathBuilder( private val operationBuilder: OperationBuilder ) { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt similarity index 81% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt index 60a2f15..f34ad9f 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/PathsBuilder.kt @@ -1,10 +1,13 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.routing.ControllerUtils import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.Paths +/** + * Build the openapi [Paths]-object. Holds the relative paths to the individual endpoints and their operations. + * See [OpenAPI Specification - Paths Object](https://swagger.io/specification/#paths-object). + */ class PathsBuilder( private val pathBuilder: PathBuilder ) { @@ -22,8 +25,7 @@ class PathsBuilder( } private fun addAsNewPath(paths: Paths, route: RouteMeta) { - val rootPath = ControllerUtils.appConfig?.let { ControllerUtils.getRootPath(it) } ?: "" - paths.addPathItem("$rootPath${route.path}", pathBuilder.build(route)) + paths.addPathItem(route.path, pathBuilder.build(route)) } private fun addToExistingPath(existing: PathItem, route: RouteMeta) { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt similarity index 52% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt index f5a6383..9e5c82f 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/RequestBodyBuilder.kt @@ -1,13 +1,17 @@ package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody +import io.github.smiley4.ktorswaggerui.data.OpenApiBaseBodyData import io.swagger.v3.oas.models.parameters.RequestBody +/** + * Build the openapi [RequestBody]-object. Holds information describing a single request body. + * See [OpenAPI Specification - Request Body Object](https://swagger.io/specification/#request-body-object). + */ class RequestBodyBuilder( private val contentBuilder: ContentBuilder ) { - fun build(body: OpenApiBaseBody): RequestBody = + fun build(body: OpenApiBaseBodyData): RequestBody = RequestBody().also { it.description = body.description it.required = body.required diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt new file mode 100644 index 0000000..27512d4 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt @@ -0,0 +1,24 @@ +package io.github.smiley4.ktorswaggerui.builder.openapi + +import io.github.smiley4.ktorswaggerui.data.OpenApiResponseData +import io.swagger.v3.oas.models.responses.ApiResponse + +/** + * Build the openapi [ApiResponse]-objects by status-code. Holds information describing status-codes and responses from an API Operation. + * See [OpenAPI Specification - Response Object](https://swagger.io/specification/#response-object). + */ +class ResponseBuilder( + private val headerBuilder: HeaderBuilder, + private val contentBuilder: ContentBuilder +) { + + fun build(response: OpenApiResponseData): Pair = + response.statusCode to ApiResponse().also { + it.description = response.description + it.headers = response.headers.mapValues { header -> headerBuilder.build(header.value) } + response.body?.let { body -> + it.content = contentBuilder.build(body) + } + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt similarity index 55% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt index bd148dd..957dacc 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponsesBuilder.kt @@ -1,32 +1,36 @@ package io.github.smiley4.ktorswaggerui.builder.openapi +import io.github.smiley4.ktorswaggerui.data.OpenApiResponseData import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponses import io.ktor.http.HttpStatusCode import io.swagger.v3.oas.models.responses.ApiResponses +/** + * Build the openapi [ApiResponses]-object. A container for the expected responses of an operation. + * See [OpenAPI Specification - Responses Object](https://swagger.io/specification/#responses-object). + */ class ResponsesBuilder( private val responseBuilder: ResponseBuilder, private val config: PluginConfigData ) { - fun build(responses: OpenApiResponses, isProtected: Boolean): ApiResponses = + fun build(responses: List, isProtected: Boolean): ApiResponses = ApiResponses().also { - responses.getResponses() + responses .map { response -> responseBuilder.build(response) } .forEach { (name, response) -> it.addApiResponse(name, response) } if (shouldAddUnauthorized(responses, isProtected)) { - config.defaultUnauthorizedResponse + config.securityConfig.defaultUnauthorizedResponse ?.let { response -> responseBuilder.build(response) } ?.also { (name, response) -> it.addApiResponse(name, response) } } } - private fun shouldAddUnauthorized(responses: OpenApiResponses, isProtected: Boolean): Boolean { + private fun shouldAddUnauthorized(responses: List, isProtected: Boolean): Boolean { val unauthorizedCode = HttpStatusCode.Unauthorized.value.toString(); - return config.defaultUnauthorizedResponse != null + return config.securityConfig.defaultUnauthorizedResponse != null && isProtected - && responses.getResponses().count { it.statusCode == unauthorizedCode } == 0 + && responses.count { it.statusCode == unauthorizedCode } == 0 } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt similarity index 53% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt index 3b1df67..52d3ec2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecurityRequirementsBuilder.kt @@ -1,22 +1,24 @@ package io.github.smiley4.ktorswaggerui.builder.openapi -import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.data.PluginConfigData import io.swagger.v3.oas.models.security.SecurityRequirement +/** + * Build the openapi [SecurityRequirement]-objects. + * See [OpenAPI Specification - Security Requirement Object](https://swagger.io/specification/#security-requirement-object). + */ class SecurityRequirementsBuilder( private val config: PluginConfigData ) { fun build(route: RouteMeta): List { - val securitySchemes = mutableSetOf().also { schemes -> - route.documentation.securitySchemeName?.also { schemes.add(it) } - route.documentation.securitySchemeNames?.also { schemes.addAll(it) } - } - if (securitySchemes.isEmpty()) { - config.defaultSecuritySchemeNames.also { securitySchemes.addAll(it) } - } - return securitySchemes.map { + return buildSet { + addAll(route.documentation.securitySchemeNames) + if(route.documentation.securitySchemeNames.isEmpty()) { + addAll(config.securityConfig.defaultSecuritySchemeNames) + } + }.map { SecurityRequirement().apply { addList(it, emptyList()) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt similarity index 82% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt index 086aa89..1a354e8 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/SecuritySchemesBuilder.kt @@ -4,6 +4,10 @@ import io.github.smiley4.ktorswaggerui.data.AuthType import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData import io.swagger.v3.oas.models.security.SecurityScheme +/** + * Build the openapi [SecurityScheme]-objects with their names. Holds information defining security schemes that can be used by operations. + * See [OpenAPI Specification - Security Scheme Object](https://swagger.io/specification/#security-scheme-object). + */ class SecuritySchemesBuilder( private val oAuthFlowsBuilder: OAuthFlowsBuilder ) { diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt new file mode 100644 index 0000000..09d41fe --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt @@ -0,0 +1,31 @@ +package io.github.smiley4.ktorswaggerui.builder.openapi + +import io.github.smiley4.ktorswaggerui.data.ServerData +import io.swagger.v3.oas.models.servers.Server +import io.swagger.v3.oas.models.servers.ServerVariable +import io.swagger.v3.oas.models.servers.ServerVariables + +/** + * Build the openapi [Server]-object. Holds information representing a Server. + * See [OpenAPI Specification - Server Object](https://swagger.io/specification/#server-object). + */ +class ServerBuilder { + + fun build(server: ServerData): Server = + Server().also { + it.url = server.url + it.description = server.description + if (server.variables.isNotEmpty()) { + it.variables = ServerVariables().also { variables -> + server.variables.forEach { entry -> + variables.addServerVariable(entry.name, ServerVariable().also { variable -> + variable.enum = entry.enum.toList() + variable.default = entry.default + variable.description = entry.description + }) + } + } + } + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt similarity index 78% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt index d550e6a..c1d9ed4 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagBuilder.kt @@ -3,6 +3,10 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.github.smiley4.ktorswaggerui.data.TagData import io.swagger.v3.oas.models.tags.Tag +/** + * Build the openapi [Tag]-object. Holds metadata of a single tag. + * See [OpenAPI Specification - Tag Object](https://swagger.io/specification/#tag-object). + */ class TagBuilder( private val tagExternalDocumentationBuilder: TagExternalDocumentationBuilder ) { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt similarity index 63% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt index 4e53c08..a388349 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/TagExternalDocumentationBuilder.kt @@ -2,6 +2,10 @@ package io.github.smiley4.ktorswaggerui.builder.openapi import io.swagger.v3.oas.models.ExternalDocumentation +/** + * Build the openapi [ExternalDocumentation]-object for a tag. + * See [OpenAPI Specification - External Documentation Object](https://swagger.io/specification/#external-documentation-object). + */ class TagExternalDocumentationBuilder { fun build(url: String, description: String): ExternalDocumentation = diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt similarity index 83% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt index a9b257a..da25fd3 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteCollector.kt @@ -1,13 +1,23 @@ package io.github.smiley4.ktorswaggerui.builder.route import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.DocumentedRouteSelector -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.dsl.routing.DocumentedRouteSelector +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiRoute import io.ktor.http.HttpMethod import io.ktor.server.auth.AuthenticationRouteSelector -import io.ktor.server.routing.* +import io.ktor.server.routing.ConstantParameterRouteSelector +import io.ktor.server.routing.HttpMethodRouteSelector +import io.ktor.server.routing.OptionalParameterRouteSelector +import io.ktor.server.routing.ParameterRouteSelector +import io.ktor.server.routing.RootRouteSelector +import io.ktor.server.routing.Route +import io.ktor.server.routing.RouteSelector +import io.ktor.server.routing.TrailingSlashRouteSelector import kotlin.reflect.full.isSubclassOf +/** + * Collect all routes from the given application + */ class RouteCollector( private val routeDocumentationMerger: RouteDocumentationMerger ) { @@ -23,7 +33,7 @@ class RouteCollector( RouteMeta( method = getMethod(route), path = getPath(route, config), - documentation = documentation, + documentation = documentation.build(), protected = documentation.protected ?: isProtected(route) ) } @@ -31,11 +41,11 @@ class RouteCollector( .filter { path -> config.pathFilter(path.method, path.path.split("/").filter { it.isNotEmpty() }) } } + private fun getDocumentation(route: Route, base: OpenApiRoute): OpenApiRoute { var documentation = base if (route.selector is DocumentedRouteSelector) { - documentation = - routeDocumentationMerger.merge(documentation, (route.selector as DocumentedRouteSelector).documentation) + documentation = routeDocumentationMerger.merge(documentation, (route.selector as DocumentedRouteSelector).documentation) } return if (route.parent != null) { getDocumentation(route.parent!!, documentation) @@ -44,10 +54,12 @@ class RouteCollector( } } + private fun getMethod(route: Route): HttpMethod { return (route.selector as HttpMethodRouteSelector).method } + @Suppress("CyclomaticComplexMethod") private fun getPath(route: Route, config: PluginConfigData): String { val selector = route.selector @@ -68,6 +80,7 @@ class RouteCollector( } } + private fun isIgnoredSelector(selector: RouteSelector, config: PluginConfigData): Boolean { return when (selector) { is TrailingSlashRouteSelector -> false @@ -82,6 +95,7 @@ class RouteCollector( } } + private fun isProtected(route: Route): Boolean { return when (route.selector) { is AuthenticationRouteSelector -> true @@ -90,6 +104,7 @@ class RouteCollector( is DocumentedRouteSelector -> route.parent?.let { isProtected(it) } ?: false is HttpMethodRouteSelector -> route.parent?.let { isProtected(it) } ?: false else -> route.parent?.let { isProtected(it) } ?: false + } } @@ -98,5 +113,4 @@ class RouteCollector( .filter { it.selector is HttpMethodRouteSelector } } - } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt similarity index 81% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt index f0509b9..5981858 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteDocumentationMerger.kt @@ -1,6 +1,6 @@ package io.github.smiley4.ktorswaggerui.builder.route -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiRoute class RouteDocumentationMerger { @@ -17,7 +17,6 @@ class RouteDocumentationMerger { summary = a.summary ?: b.summary description = a.description ?: b.description operationId = a.operationId ?: b.operationId - securitySchemeName = a.securitySchemeName ?: b.securitySchemeName securitySchemeNames = mutableSetOf().also { merged -> a.securitySchemeNames?.let { merged.addAll(it) } b.securitySchemeNames?.let { merged.addAll(it) } @@ -26,9 +25,9 @@ class RouteDocumentationMerger { hidden = a.hidden || b.hidden protected = a.protected ?: b.protected request { - (getParameters() as MutableList).also { - it.addAll(a.getRequest().getParameters()) - it.addAll(b.getRequest().getParameters()) + parameters.also { + it.addAll(a.getRequest().parameters) + it.addAll(b.getRequest().parameters) } setBody(a.getRequest().getBody() ?: b.getRequest().getBody()) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt similarity index 68% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt index 2145292..b9d8823 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/route/RouteMeta.kt @@ -1,6 +1,6 @@ package io.github.smiley4.ktorswaggerui.builder.route -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.data.OpenApiRouteData import io.ktor.http.HttpMethod /** @@ -9,6 +9,6 @@ import io.ktor.http.HttpMethod data class RouteMeta( val path: String, val method: HttpMethod, - val documentation: OpenApiRoute, + val documentation: OpenApiRouteData, val protected: Boolean ) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt new file mode 100644 index 0000000..3af0259 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt @@ -0,0 +1,20 @@ +package io.github.smiley4.ktorswaggerui.builder.schema + +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.swagger.v3.oas.models.media.Schema + +/** + * Provides schemas for an openapi-spec + */ +interface SchemaContext { + + /** + * Get a [Schema] (or a ref to a schema) by its [TypeDescriptor] + */ + fun getSchema(typeDescriptor: TypeDescriptor): Schema<*> + + /** + * Get all schemas placed in the components-section of the spec. + */ + fun getComponentSection(): Map> +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextImpl.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextImpl.kt new file mode 100644 index 0000000..13a3d38 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextImpl.kt @@ -0,0 +1,174 @@ +package io.github.smiley4.ktorswaggerui.builder.schema + +import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta +import io.github.smiley4.ktorswaggerui.data.AnyOfTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.ArrayTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.EmptyTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiMultipartBodyData +import io.github.smiley4.ktorswaggerui.data.OpenApiSimpleBodyData +import io.github.smiley4.ktorswaggerui.data.RefTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.SchemaConfigData +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.schemakenerator.core.data.TypeId +import io.github.smiley4.schemakenerator.core.data.WildcardTypeData +import io.github.smiley4.schemakenerator.swagger.data.CompiledSwaggerSchema +import io.github.smiley4.schemakenerator.swagger.steps.SwaggerSchemaUtils +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType + +class SchemaContextImpl(private val schemaConfig: SchemaConfigData) : SchemaContext { + + private val rootSchemas = mutableMapOf>() + private val componentSchemas = mutableMapOf>() + + fun addGlobal(config: SchemaConfigData) { + config.schemas.forEach { (schemaId, typeDescriptor) -> + val schema = collapseRootRef(generateSchema(typeDescriptor)) + componentSchemas[schemaId] = schema.swagger + schema.componentSchemas.forEach { (k, v) -> + componentSchemas[k] = v + } + } + } + + private fun collapseRootRef(schema: CompiledSwaggerSchema): CompiledSwaggerSchema { + if (schema.swagger.`$ref` == null) { + return schema + } else { + val referencedSchemaId = schema.swagger.`$ref`!!.replace("#/components/schemas/", "") + val referencedSchema = schema.componentSchemas[referencedSchemaId]!! + return CompiledSwaggerSchema( + typeData = schema.typeData, + swagger = referencedSchema, + componentSchemas = schema.componentSchemas.toMutableMap().also { + it.remove(referencedSchemaId) + } + ) + } + } + + fun add(routes: Collection) { + collectTypeDescriptor(routes).forEach { typeDescriptor -> + val schema = generateSchema(typeDescriptor) + rootSchemas[typeDescriptor] = schema.swagger + schema.componentSchemas.forEach { (k, v) -> + componentSchemas[k] = v + } + } + } + + private fun generateSchema(typeDescriptor: TypeDescriptor): CompiledSwaggerSchema { + return when (typeDescriptor) { + is KTypeDescriptor -> { + if (schemaConfig.overwrite.containsKey(typeDescriptor.type)) { + generateSchema(schemaConfig.overwrite[typeDescriptor.type]!!) + } else { + generateSchema(typeDescriptor.type) + } + } + is SwaggerTypeDescriptor -> { + CompiledSwaggerSchema( + typeData = WildcardTypeData(), + swagger = typeDescriptor.schema, + componentSchemas = emptyMap() + ) + } + is ArrayTypeDescriptor -> { + val itemSchema = generateSchema(typeDescriptor.type) + CompiledSwaggerSchema( + typeData = WildcardTypeData(), + swagger = SwaggerSchemaUtils().arraySchema( + itemSchema.swagger + ), + componentSchemas = itemSchema.componentSchemas + ) + } + is AnyOfTypeDescriptor -> { + val optionSchemas = typeDescriptor.types.map { generateSchema(it) } + CompiledSwaggerSchema( + typeData = WildcardTypeData(), + swagger = SwaggerSchemaUtils().subtypesSchema( + optionSchemas.map { it.swagger } + ), + componentSchemas = buildMap { + optionSchemas.forEach { optionSchema -> + this.putAll(optionSchema.componentSchemas) + } + } + ) + } + is EmptyTypeDescriptor -> { + CompiledSwaggerSchema( + typeData = WildcardTypeData(), + swagger = SwaggerSchemaUtils().anyObjectSchema(), + componentSchemas = emptyMap() + ) + } + is RefTypeDescriptor -> { + CompiledSwaggerSchema( + typeData = WildcardTypeData(), + swagger = SwaggerSchemaUtils().referenceSchema(typeDescriptor.schemaId, true), + componentSchemas = emptyMap() + ) + } + } + } + + private fun generateSchema(type: KType): CompiledSwaggerSchema { + return schemaConfig.generator(type) + } + + private fun collectTypeDescriptor(routes: Collection): List { + val descriptors = mutableListOf() + routes + .filter { !it.documentation.hidden } + .forEach { route -> + route.documentation.request.also { request -> + request.parameters.forEach { parameter -> + descriptors.add(parameter.type) + } + request.body?.also { body -> + when (body) { + is OpenApiSimpleBodyData -> { + descriptors.add(body.type) + } + is OpenApiMultipartBodyData -> { + body.parts.forEach { part -> + descriptors.add(part.type) + } + } + } + } + } + route.documentation.responses.forEach { response -> + response.headers.forEach { (_, header) -> + header.type?.also { descriptors.add(it) } + } + response.body?.also { body -> + when (body) { + is OpenApiSimpleBodyData -> { + descriptors.add(body.type) + } + is OpenApiMultipartBodyData -> { + body.parts.forEach { part -> + descriptors.add(part.type) + } + } + } + } + } + } + return descriptors + } + + override fun getSchema(typeDescriptor: TypeDescriptor): Schema<*> { + return rootSchemas[typeDescriptor] ?: throw NoSuchElementException("no root-schema for given type-descriptor") + } + + override fun getComponentSection(): Map> { + return componentSchemas + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt similarity index 87% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt index d94ab76..5865f60 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthKeyLocation.kt @@ -2,6 +2,9 @@ package io.github.smiley4.ktorswaggerui.data import io.swagger.v3.oas.models.security.SecurityScheme +/** + * The locations of the API key. + */ enum class AuthKeyLocation(val swaggerType: SecurityScheme.In) { QUERY(SecurityScheme.In.QUERY), HEADER(SecurityScheme.In.HEADER), diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthScheme.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthScheme.kt similarity index 100% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthScheme.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthScheme.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt similarity index 90% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt index 897ae47..e2f053e 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/AuthType.kt @@ -2,6 +2,9 @@ package io.github.smiley4.ktorswaggerui.data import io.swagger.v3.oas.models.security.SecurityScheme +/** + * The type of security schemes + */ enum class AuthType(val swaggerType: SecurityScheme.Type) { API_KEY(SecurityScheme.Type.APIKEY), HTTP(SecurityScheme.Type.HTTP), diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt similarity index 73% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt index 4a5f696..920ecaf 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ContactData.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - Contact Object](https://swagger.io/specification/#contact-object). + */ data class ContactData( val name: String?, val url: String?, diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/DataUtils.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/DataUtils.kt similarity index 100% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/DataUtils.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/DataUtils.kt diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleConfigData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleConfigData.kt new file mode 100644 index 0000000..878569f --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleConfigData.kt @@ -0,0 +1,13 @@ +package io.github.smiley4.ktorswaggerui.data + +class ExampleConfigData( + val sharedExamples: Map +) { + + companion object { + val DEFAULT = ExampleConfigData( + sharedExamples = emptyMap() + ) + } + +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleDescriptor.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleDescriptor.kt new file mode 100644 index 0000000..dfbe272 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExampleDescriptor.kt @@ -0,0 +1,35 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.swagger.v3.oas.models.examples.Example + +/** + * Identifier and description of an example + */ +sealed class ExampleDescriptor( + val name: String, +) + + +/** + * Describes an example as an object. + */ +class ValueExampleDescriptor( + name: String, + val value: Any?, + val summary: String? = null, + val description: String? = null, +) : ExampleDescriptor(name) + + +/** + * Describes a reference to a shared example placed in the components section + * @param name the name of the example in the operation + * @param refName the name/id of the example to reference in the components section + */ +class RefExampleDescriptor(name: String, val refName: String) : ExampleDescriptor(name) + + +/** + * Describes an example as a swagger [Example]-object + */ +class SwaggerExampleDescriptor(name: String, val example: Example) : ExampleDescriptor(name) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt similarity index 66% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt index 8ef5db0..ac3d72a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ExternalDocsData.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - External Documentation Object](https://swagger.io/specification/#external-documentation-object). + */ data class ExternalDocsData( val url: String, val description: String?, diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt similarity index 67% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt index fe8e164..4de183a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/InfoData.kt @@ -1,21 +1,26 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - Info Object](https://swagger.io/specification/#info-object). + */ data class InfoData( val title: String, val version: String?, val description: String?, + val summary: String?, val termsOfService: String?, val contact: ContactData?, - val license: LicenseData? + val license: LicenseData?, ) { companion object { val DEFAULT = InfoData( title = "API", version = null, description = null, + summary = null, termsOfService = null, contact = null, - license = null + license = null, ) } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt similarity index 59% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt index 154be47..bea8f45 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/LicenseData.kt @@ -1,13 +1,18 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - License Object](https://swagger.io/specification/#license-object). + */ data class LicenseData( val name: String?, val url: String?, + val identifier: String? ) { companion object { val DEFAULT = LicenseData( name = null, url = null, + identifier = null ) } } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiBaseBodyData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiBaseBodyData.kt new file mode 100644 index 0000000..eb91c3d --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiBaseBodyData.kt @@ -0,0 +1,35 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.ktor.http.ContentType + +/** + * The common information for request and response bodies. + */ +sealed class OpenApiBaseBodyData( + val description: String?, + val required: Boolean, + val mediaTypes: Set, +) + + +/** + * Information for a "simple" request or response body. + */ +class OpenApiSimpleBodyData( + description: String?, + required: Boolean, + mediaTypes: Set, + val type: TypeDescriptor, + val examples: List +) : OpenApiBaseBodyData(description, required, mediaTypes) + + +/** + * Information for a multipart request or response body. + */ +class OpenApiMultipartBodyData( + description: String?, + required: Boolean, + mediaTypes: Set, + val parts: List +) : OpenApiBaseBodyData(description, required, mediaTypes) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiHeaderData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiHeaderData.kt new file mode 100644 index 0000000..aa4c4f9 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiHeaderData.kt @@ -0,0 +1,12 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * See [OpenAPI Specification - Header Object](https://swagger.io/specification/#header-object). + */ +data class OpenApiHeaderData( + val description: String?, + val type: TypeDescriptor?, + val required: Boolean, + val deprecated: Boolean, + val explode: Boolean?, +) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiMultipartPartData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiMultipartPartData.kt new file mode 100644 index 0000000..4f2da8c --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiMultipartPartData.kt @@ -0,0 +1,13 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.ktor.http.ContentType + +/** + * Information about a part of a multipart request or response body. + */ +data class OpenApiMultipartPartData( + val name: String, + val type: TypeDescriptor, + val mediaTypes: Set, + val headers: Map, +) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestData.kt new file mode 100644 index 0000000..6405acc --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestData.kt @@ -0,0 +1,9 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * Information about a request + */ +data class OpenApiRequestData( + val parameters: List, + val body: OpenApiBaseBodyData?, +) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestParameterData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestParameterData.kt new file mode 100644 index 0000000..5f7f83c --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRequestParameterData.kt @@ -0,0 +1,20 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.swagger.v3.oas.models.parameters.Parameter + +/** + * Information about a request (query, path or header) parameter. + */ +data class OpenApiRequestParameterData( + val name: String, + val type: TypeDescriptor, + val location: ParameterLocation, + val description: String?, + val example: ExampleDescriptor?, + val required: Boolean, + val deprecated: Boolean, + val allowEmptyValue: Boolean, + val explode: Boolean, + val allowReserved: Boolean, + val style: Parameter.StyleEnum?, +) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiResponseData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiResponseData.kt new file mode 100644 index 0000000..6693df3 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiResponseData.kt @@ -0,0 +1,11 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * Information about a response for a status-code. + */ +data class OpenApiResponseData( + val statusCode: String, + val description: String?, + val headers: Map, + val body: OpenApiBaseBodyData?, +) diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRouteData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRouteData.kt new file mode 100644 index 0000000..67364ef --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenApiRouteData.kt @@ -0,0 +1,20 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * Information about a single route. + */ +data class OpenApiRouteData( + val specId: String?, + val tags: Set, + val summary: String?, + val description: String?, + val operationId: String?, + val deprecated: Boolean, + val hidden: Boolean, + val securitySchemeNames: List, + val protected: Boolean?, + val request: OpenApiRequestData, + val responses: List, + val externalDocs: ExternalDocsData?, + val servers: List, +) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt similarity index 79% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt index 3072dec..a9563d2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowData.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - OAuth Flow Object](https://swagger.io/specification/#oauth-flow-object). + */ data class OpenIdOAuthFlowData( val authorizationUrl: String? = null, val tokenUrl: String? = null, diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt similarity index 80% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt index 44acedf..639820a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/OpenIdOAuthFlowsData.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - OAuth Flows Object](https://swagger.io/specification/#oauth-flows-object). + */ data class OpenIdOAuthFlowsData( val implicit: OpenIdOAuthFlowData?, val password: OpenIdOAuthFlowData?, diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ParameterLocation.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ParameterLocation.kt new file mode 100644 index 0000000..7052726 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ParameterLocation.kt @@ -0,0 +1,8 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * Locations for request parameters. + */ +enum class ParameterLocation { + QUERY, HEADER, PATH +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt similarity index 56% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt index 73a7553..6eaa326 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PathFilter.kt @@ -2,5 +2,7 @@ package io.github.smiley4.ktorswaggerui.data import io.ktor.http.HttpMethod - +/** + * Filters paths to determine which to include (return 'true') in the spec and which to hide (return 'true'). + */ typealias PathFilter = (method: HttpMethod, url: List) -> Boolean diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt new file mode 100644 index 0000000..1ba537c --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt @@ -0,0 +1,43 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl +import kotlin.reflect.KClass + +/** + * Complete plugin configuration + */ +data class PluginConfigData( + val specAssigner: SpecAssigner, + val pathFilter: PathFilter, + val ignoredRouteSelectors: Set>, + val swagger: SwaggerUIData, + val info: InfoData, + val servers: List, + val externalDocs: ExternalDocsData, + val specConfigs: MutableMap, + val postBuild: PostBuild?, + val schemaConfig: SchemaConfigData, + val exampleConfig: ExampleConfigData, + val securityConfig: SecurityData, + val tagsConfig: TagsData +) { + + companion object { + val DEFAULT = PluginConfigData( + specAssigner = { _, _ -> PluginConfigDsl.DEFAULT_SPEC_ID }, + pathFilter = { _, _ -> true }, + ignoredRouteSelectors = emptySet(), + swagger = SwaggerUIData.DEFAULT, + info = InfoData.DEFAULT, + servers = emptyList(), + externalDocs = ExternalDocsData.DEFAULT, + specConfigs = mutableMapOf(), + postBuild = null, + schemaConfig = SchemaConfigData.DEFAULT, + exampleConfig = ExampleConfigData.DEFAULT, + securityConfig = SecurityData.DEFAULT, + tagsConfig = TagsData.DEFAULT + ) + } + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/WhenBuildOpenApiSpecs.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PostBuild.kt similarity index 61% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/WhenBuildOpenApiSpecs.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PostBuild.kt index 175ddc9..5934a97 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/WhenBuildOpenApiSpecs.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PostBuild.kt @@ -3,6 +3,7 @@ package io.github.smiley4.ktorswaggerui.data import io.swagger.v3.oas.models.OpenAPI /** + * Function executed after building the openapi-spec. * @author yuefeng in 2024/3/25. */ -typealias WhenBuildOpenApiSpecs = (openApi: OpenAPI) -> Unit +typealias PostBuild = (openApi: OpenAPI) -> Unit diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SchemaConfigData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SchemaConfigData.kt new file mode 100644 index 0000000..807eb0c --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SchemaConfigData.kt @@ -0,0 +1,40 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.github.smiley4.schemakenerator.core.connectSubTypes +import io.github.smiley4.schemakenerator.core.handleNameAnnotation +import io.github.smiley4.schemakenerator.reflection.collectSubTypes +import io.github.smiley4.schemakenerator.reflection.processReflection +import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot +import io.github.smiley4.schemakenerator.swagger.data.CompiledSwaggerSchema +import io.github.smiley4.schemakenerator.swagger.data.TitleType +import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema +import io.github.smiley4.schemakenerator.swagger.handleCoreAnnotations +import io.github.smiley4.schemakenerator.swagger.withAutoTitle +import kotlin.reflect.KType + +/** + * Common configuration for schemas. + */ +data class SchemaConfigData( + val schemas: Map, + val generator: (type: KType) -> CompiledSwaggerSchema, + val overwrite: Map +) { + companion object { + val DEFAULT = SchemaConfigData( + schemas = emptyMap(), + generator = { type -> + type + .collectSubTypes() + .processReflection() + .connectSubTypes() + .handleNameAnnotation() + .generateSwaggerSchema() + .handleCoreAnnotations() + .withAutoTitle(TitleType.SIMPLE) + .compileReferencingRoot() + }, + overwrite = emptyMap() + ) + } +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecurityData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecurityData.kt new file mode 100644 index 0000000..ee66378 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecurityData.kt @@ -0,0 +1,18 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * Common security configuration information. + */ +data class SecurityData( + val defaultUnauthorizedResponse: OpenApiResponseData?, + val defaultSecuritySchemeNames: Set, + val securitySchemes: List, +) { + companion object { + val DEFAULT = SecurityData( + defaultUnauthorizedResponse = null, + defaultSecuritySchemeNames = emptySet(), + securitySchemes = emptyList() + ) + } +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt similarity index 85% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt index 8537866..ddb0505 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SecuritySchemeData.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - Security Scheme Object](https://swagger.io/specification/#security-scheme-object). + */ data class SecuritySchemeData( val schemeName: String, val type: AuthType?, diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt similarity index 50% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt index a98ca61..7e9e334 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerData.kt @@ -1,14 +1,19 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - Server Object](https://swagger.io/specification/#server-object). + */ data class ServerData( val url: String, val description: String?, + val variables: List ) { companion object { val DEFAULT = ServerData( url = "/", - description = null + description = null, + variables = emptyList() ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerVariableData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerVariableData.kt new file mode 100644 index 0000000..c61ecc2 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/ServerVariableData.kt @@ -0,0 +1,12 @@ +package io.github.smiley4.ktorswaggerui.data + + +/** + * See [OpenAPI Specification - Server Variable Object](https://swagger.io/specification/#server-variable-object). + */ +data class ServerVariableData( + val name: String, + val enum: Set, + val default: String, + val description: String? +) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigned.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigner.kt similarity index 82% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigned.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigner.kt index 28a5e84..f87522d 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigned.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SpecAssigner.kt @@ -1,6 +1,7 @@ package io.github.smiley4.ktorswaggerui.data /** + * Assigns (unassigned) routes to api-specs. * url - the parts of the route-url split at all `/`. * tags - the tags assigned to the route */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt similarity index 65% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt index 7bb2b67..c57dfb2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUIData.kt @@ -1,11 +1,9 @@ package io.github.smiley4.ktorswaggerui.data +/** + * Common configuration for the swagger-ui. + */ data class SwaggerUIData( - val automaticRouter: Boolean, - val forwardRoot: Boolean, - val swaggerUrl: String, - val rootHostPath: String, - val authentication: String?, val validatorUrl: String?, val displayOperationId: Boolean, val showTagFilterInput: Boolean, @@ -16,11 +14,6 @@ data class SwaggerUIData( companion object { val DEFAULT = SwaggerUIData( - automaticRouter = true, - forwardRoot = false, - swaggerUrl = "swagger-ui", - rootHostPath = "", - authentication = null, validatorUrl = null, displayOperationId = false, showTagFilterInput = false, diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt similarity index 81% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt index 84a40b1..8dccbc5 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSort.kt @@ -1,18 +1,17 @@ package io.github.smiley4.ktorswaggerui.data +/** + * Determines the order to sort the operations in the swagger-ui. + */ enum class SwaggerUiSort(val value: String) { /** * The order returned by the server unchanged */ NONE("undefined"), - - /** * sort by paths alphanumerically */ ALPHANUMERICALLY("alpha"), - - /** * sort by HTTP method */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt similarity index 72% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt index 3d38d95..07c17db 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/SwaggerUiSyntaxHighlight.kt @@ -1,6 +1,10 @@ package io.github.smiley4.ktorswaggerui.data +/** + * The syntax-highlight theme to use for code-blocks in swagger-ui. + */ enum class SwaggerUiSyntaxHighlight(val value: String) { + DISABLED("disabled"), AGATE("agate"), ARTA("arta"), MONOKAI("monokai"), diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt similarity index 80% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt index 29b30b6..60b438f 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagData.kt @@ -1,5 +1,8 @@ package io.github.smiley4.ktorswaggerui.data +/** + * See [OpenAPI Specification - Tag Object](https://swagger.io/specification/#tag-object). + */ data class TagData( val name: String, val description: String?, diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt similarity index 85% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt index 6ba542b..0a6372b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagGenerator.kt @@ -1,6 +1,7 @@ package io.github.smiley4.ktorswaggerui.data /** + * Generates additional tags for routes. * url - the parts of the route-url split at all `/`. * return a collection of tags. "Null"-entries will be ignored. */ diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagsData.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagsData.kt new file mode 100644 index 0000000..d54479e --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TagsData.kt @@ -0,0 +1,18 @@ +package io.github.smiley4.ktorswaggerui.data + +/** + * Common configuration for tags. + */ +data class TagsData( + val tags: List, + val generator: TagGenerator, +) { + + companion object { + val DEFAULT = TagsData( + tags = emptyList(), + generator = { emptyList() } + ) + } + +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TypeDescriptor.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TypeDescriptor.kt new file mode 100644 index 0000000..69d4b4c --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/TypeDescriptor.kt @@ -0,0 +1,64 @@ +package io.github.smiley4.ktorswaggerui.data + +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Describes and identifies types and schemas. + */ +sealed interface TypeDescriptor + + +/** + * Describes a type from a swagger [Schema] + */ +class SwaggerTypeDescriptor(val schema: Schema<*>) : TypeDescriptor + + +/** + * Describes a type from a kotlin [KType] + */ +class KTypeDescriptor(val type: KType) : TypeDescriptor + +/** + * Describes an array of types. + */ +class ArrayTypeDescriptor(val type: TypeDescriptor) : TypeDescriptor + +/** + * Describes an object matching any of the given types. + */ +class AnyOfTypeDescriptor(val types: List) : TypeDescriptor + +/** + * Describes an empty type/schema. + */ +class EmptyTypeDescriptor : TypeDescriptor + +/** + * Describes a reference to a schema in the component section. + */ +class RefTypeDescriptor(val schemaId: String) : TypeDescriptor + +inline fun type() = KTypeDescriptor(typeOf()) + +fun empty() = EmptyTypeDescriptor() + +fun ref(schemaId: String) = RefTypeDescriptor(schemaId) + +fun array(type: TypeDescriptor) = ArrayTypeDescriptor(type) +fun array(type: Schema<*>) = ArrayTypeDescriptor(SwaggerTypeDescriptor(type)) +fun array(type: KType) = ArrayTypeDescriptor(KTypeDescriptor(type)) +inline fun array() = ArrayTypeDescriptor(KTypeDescriptor(typeOf())) + +fun anyOf(vararg types: TypeDescriptor) = AnyOfTypeDescriptor(types.toList()) +fun anyOf(types: Collection) = AnyOfTypeDescriptor(types.toList()) + +fun anyOf(vararg types: Schema<*>) = AnyOfTypeDescriptor(types.map { SwaggerTypeDescriptor(it) }) +@JvmName("anyOfSwagger") +fun anyOf(types: Collection>) = AnyOfTypeDescriptor(types.map { SwaggerTypeDescriptor(it) }) + +fun anyOf(vararg types: KType) = AnyOfTypeDescriptor(types.map { KTypeDescriptor(it) }) +@JvmName("anyOfKType") +fun anyOf(types: Collection) = AnyOfTypeDescriptor(types.map { KTypeDescriptor(it) }) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiDslMarker.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiDslMarker.kt similarity index 100% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiDslMarker.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiDslMarker.kt diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/ExampleConfig.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/ExampleConfig.kt new file mode 100644 index 0000000..542d110 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/ExampleConfig.kt @@ -0,0 +1,42 @@ +package io.github.smiley4.ktorswaggerui.dsl.config + +import io.github.smiley4.ktorswaggerui.data.ExampleConfigData +import io.github.smiley4.ktorswaggerui.data.ExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.SwaggerExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.ValueExampleDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.github.smiley4.ktorswaggerui.dsl.routes.ValueExampleDescriptorDsl +import io.swagger.v3.oas.models.examples.Example + +/** + * Configuration for schemas + */ +@OpenApiDslMarker +class ExampleConfig { + + val sharedExamples = mutableMapOf() + + fun example(example: ExampleDescriptor) { + sharedExamples[example.name] = example + } + + fun example(name: String, example: Example) = example(SwaggerExampleDescriptor(name, example)) + + fun example(name: String, example: ValueExampleDescriptorDsl.() -> Unit) = example( + ValueExampleDescriptorDsl() + .apply(example) + .let { result -> + ValueExampleDescriptor( + name = name, + value = result.value, + summary = result.summary, + description = result.description + ) + } + ) + + fun build() = ExampleConfigData( + sharedExamples = sharedExamples + ) + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiContact.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiContact.kt similarity index 88% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiContact.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiContact.kt index 7ab26b6..3936ef6 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiContact.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiContact.kt @@ -1,7 +1,8 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.ContactData import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** * Contact information for the exposed API. diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExternalDocs.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiExternalDocs.kt similarity index 84% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExternalDocs.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiExternalDocs.kt index 7fd85bc..6510081 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExternalDocs.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiExternalDocs.kt @@ -1,13 +1,15 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.DataUtils import io.github.smiley4.ktorswaggerui.data.ExternalDocsData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** * An object representing external documentation. */ @OpenApiDslMarker class OpenApiExternalDocs { + /** * A short description of the external documentation */ @@ -19,6 +21,7 @@ class OpenApiExternalDocs { */ var url: String = "/" + fun build(base: ExternalDocsData) = ExternalDocsData( url = DataUtils.mergeDefault(base.url, url, ExternalDocsData.DEFAULT.url), description = DataUtils.merge(base.description, description) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiInfo.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiInfo.kt similarity index 83% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiInfo.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiInfo.kt index 98efa89..f78facd 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiInfo.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiInfo.kt @@ -1,11 +1,15 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.ContactData import io.github.smiley4.ktorswaggerui.data.DataUtils.merge import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault import io.github.smiley4.ktorswaggerui.data.InfoData import io.github.smiley4.ktorswaggerui.data.LicenseData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +/** + * Basic information for the exposed API. + */ @OpenApiDslMarker class OpenApiInfo { @@ -26,6 +30,11 @@ class OpenApiInfo { */ var description: String? = null + /** + * A short summary of the API + */ + var summary: String? = null + /** * A URL to the Terms of Service for the API. MUST be in the format of a URL. @@ -53,6 +62,7 @@ class OpenApiInfo { license = OpenApiLicense().apply(block) } + fun build(base: InfoData): InfoData { return InfoData( title = mergeDefault(base.title, this.title, InfoData.DEFAULT.title), @@ -60,7 +70,8 @@ class OpenApiInfo { description = merge(base.description, this.description), termsOfService = merge(base.termsOfService, this.termsOfService), contact = contact?.build(base.contact ?: ContactData.DEFAULT) ?: base.contact, - license = license?.build(base.license ?: LicenseData.DEFAULT) ?: base.license + license = license?.build(base.license ?: LicenseData.DEFAULT) ?: base.license, + summary = merge(base.summary, this.summary) ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiLicense.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiLicense.kt similarity index 60% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiLicense.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiLicense.kt index 5faf8d9..3d05b85 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiLicense.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiLicense.kt @@ -1,7 +1,8 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.DataUtils import io.github.smiley4.ktorswaggerui.data.LicenseData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** * License information for the exposed API. @@ -21,8 +22,14 @@ class OpenApiLicense { var url: String? = LicenseData.DEFAULT.url + /** + * An SPDX (https://spdx.org/licenses/) license expression for the API. The identifier field is mutually exclusive of the url field. + */ + var identifier: String? = LicenseData.DEFAULT.identifier + fun build(base: LicenseData) = LicenseData( name = DataUtils.merge(base.name, name), url = DataUtils.merge(base.url, url), + identifier = DataUtils.merge(base.identifier, identifier) ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiSecurity.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiSecurity.kt new file mode 100644 index 0000000..112af92 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiSecurity.kt @@ -0,0 +1,54 @@ +package io.github.smiley4.ktorswaggerui.dsl.config + +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.SecurityData +import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiResponse +import io.ktor.http.HttpStatusCode + +/** + * Configuration for security and authentication. + */ +@OpenApiDslMarker +class OpenApiSecurity { + + /** + * Default response to automatically add to each protected route for the "Unauthorized"-Response-Code. + * Generated response can be overwritten with custom response. + */ + fun defaultUnauthorizedResponse(block: OpenApiResponse.() -> Unit) { + defaultUnauthorizedResponse = OpenApiResponse(HttpStatusCode.Unauthorized.value.toString()).apply(block) + } + + private var defaultUnauthorizedResponse: OpenApiResponse? = null + + + /** + * The names of the security schemes available for use for the protected paths + */ + var defaultSecuritySchemeNames: Collection? = SecurityData.DEFAULT.defaultSecuritySchemeNames + + + /** + * Defines security schemes that can be used by operations + */ + fun securityScheme(name: String, block: OpenApiSecurityScheme.() -> Unit) { + securitySchemes.add(OpenApiSecurityScheme(name).apply(block)) + } + + private val securitySchemes = mutableListOf() + + fun build(base: SecurityData) = SecurityData( + defaultUnauthorizedResponse = merge(base.defaultUnauthorizedResponse, defaultUnauthorizedResponse?.build()), + defaultSecuritySchemeNames = buildSet { + addAll(base.defaultSecuritySchemeNames) + defaultSecuritySchemeNames?.also { addAll(it) } + }, + securitySchemes = buildList { + addAll(base.securitySchemes) + addAll(securitySchemes.map { it.build(SecuritySchemeData.DEFAULT) }) + } + ) + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSecurityScheme.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiSecurityScheme.kt similarity index 90% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSecurityScheme.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiSecurityScheme.kt index f3b4c29..83f8bf6 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSecurityScheme.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiSecurityScheme.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.AuthKeyLocation import io.github.smiley4.ktorswaggerui.data.AuthScheme @@ -6,6 +6,7 @@ import io.github.smiley4.ktorswaggerui.data.AuthType import io.github.smiley4.ktorswaggerui.data.DataUtils.merge import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowsData import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** @@ -17,7 +18,7 @@ class OpenApiSecurityScheme( /** * The name of the security scheme. */ - val schemeName: String + private val schemeName: String ) { /** @@ -54,7 +55,7 @@ class OpenApiSecurityScheme( /** - * information for the oauth flow types supported. + * Information for the oauth flow types supported. * Required for type [AuthType.OAUTH2] */ fun flows(block: OpenIdOAuthFlows.() -> Unit) { @@ -70,7 +71,7 @@ class OpenApiSecurityScheme( /** - * A short description for security scheme. + * A short description of the security scheme. */ var description: String? = null diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiServer.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServer.kt similarity index 54% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiServer.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServer.kt index 74367b6..5c667d6 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiServer.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServer.kt @@ -1,8 +1,9 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.DataUtils.merge import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault import io.github.smiley4.ktorswaggerui.data.ServerData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** * An object representing a Server. @@ -16,15 +17,28 @@ class OpenApiServer { */ var url: String = ServerData.DEFAULT.url - /** * An optional string describing the host designated by the URL */ var description: String? = ServerData.DEFAULT.description + private val variables = mutableMapOf() + + + /** + * Adds a new server variable with the given name + */ + fun variable(name: String, block: OpenApiServerVariable.() -> Unit) { + variables[name] = OpenApiServerVariable(name).apply(block) + } + fun build(base: ServerData) = ServerData( url = mergeDefault(base.url, url, ServerData.DEFAULT.url), - description = merge(base.description, description) + description = merge(base.description, description), + variables = buildMap { + base.variables.forEach { this[it.name] = it } + variables.values.map { it.build() }.forEach { this[it.name] = it } + }.values.toList() ) } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServerVariable.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServerVariable.kt new file mode 100644 index 0000000..dd2fc39 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiServerVariable.kt @@ -0,0 +1,41 @@ +package io.github.smiley4.ktorswaggerui.dsl.config + +import io.github.smiley4.ktorswaggerui.data.ServerVariableData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker + +/** + * An object representing a Server Variable for server URL template substitution. + */ +@OpenApiDslMarker +class OpenApiServerVariable( + /** + * The name of this variable + */ + private val name: String +) { + + /** + * An enumeration of string values to be used if the substitution options are from a limited set. Must not be empty. + */ + var enum: Collection = emptyList() + + + /** + * The default value to use for substitution. Must be in the list of enums. + */ + var default: String? = null + + + /** + * An optional description for this server variable. + */ + var description: String? = null + + fun build() = ServerVariableData( + name = name, + enum = enum.toSet(), + default = (default ?: enum.firstOrNull()) ?: "", + description = description + ) + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiTag.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiTag.kt similarity index 82% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiTag.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiTag.kt index 2d81c93..27b38c6 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiTag.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiTag.kt @@ -1,7 +1,8 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.DataUtils.merge import io.github.smiley4.ktorswaggerui.data.TagData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** * Adds metadata to a single tag. @@ -19,15 +20,13 @@ class OpenApiTag( */ var description: String? = null - /** * A short description of additional external documentation for this tag. */ var externalDocDescription: String? = null - /** - *The URL for additional external documentation for this tag. + * The URL for additional external documentation for this tag. */ var externalDocUrl: String? = null diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiTags.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiTags.kt new file mode 100644 index 0000000..8d2928b --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenApiTags.kt @@ -0,0 +1,42 @@ +package io.github.smiley4.ktorswaggerui.dsl.config + +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.data.TagData +import io.github.smiley4.ktorswaggerui.data.TagGenerator +import io.github.smiley4.ktorswaggerui.data.TagsData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker + +/** + * Configuration for tags + */ +@OpenApiDslMarker +class OpenApiTags { + + private val tags = mutableListOf() + + + /** + * Tags used by the specification with additional metadata. Not all tags that are used must be declared + */ + fun tag(name: String, block: OpenApiTag.() -> Unit) { + tags.add(OpenApiTag(name).apply(block)) + } + + + /** + * Automatically add tags to the route with the given url. + * The returned (non-null) tags will be added to the tags specified in the route-specific documentation. + */ + var tagGenerator: TagGenerator = TagsData.DEFAULT.generator + + + fun build(base: TagsData) = TagsData( + tags = buildList { + addAll(base.tags) + addAll(tags.map { it.build(TagData.DEFAULT) }) + }, + generator = merge(base.generator, tagGenerator) ?: TagsData.DEFAULT.generator, + ) + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlow.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenIdOAuthFlow.kt similarity index 90% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlow.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenIdOAuthFlow.kt index 55ececa..d0d0664 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlow.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenIdOAuthFlow.kt @@ -1,7 +1,8 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.DataUtils.merge import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** * Configuration details for a supported OAuth Flow @@ -14,19 +15,16 @@ class OpenIdOAuthFlow { */ var authorizationUrl: String? = null - /** * The token URL to be used for this flow */ var tokenUrl: String? = null - /** * The URL to be used for obtaining refresh tokens */ var refreshUrl: String? = null - /** * The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlows.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenIdOAuthFlows.kt similarity index 94% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlows.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenIdOAuthFlows.kt index b13bb47..5b21dca 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenIdOAuthFlows.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/OpenIdOAuthFlows.kt @@ -1,7 +1,8 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowData import io.github.smiley4.ktorswaggerui.data.OpenIdOAuthFlowsData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker /** * An object containing configuration information for the oauth flow types supported diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/PluginConfigDsl.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/PluginConfigDsl.kt new file mode 100644 index 0000000..addbd55 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/PluginConfigDsl.kt @@ -0,0 +1,165 @@ +package io.github.smiley4.ktorswaggerui.dsl.config + +import io.github.smiley4.ktorswaggerui.data.* +import io.github.smiley4.ktorswaggerui.data.DataUtils.merge +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.ktor.http.* +import io.ktor.server.routing.* +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set +import kotlin.reflect.KClass + +/** + * Main-Configuration of the "SwaggerUI"-Plugin + */ +@OpenApiDslMarker +class PluginConfigDsl { + + companion object { + const val DEFAULT_SPEC_ID = "api" + } + + + /** + * OpenAPI info configuration - provides metadata about the API + */ + fun info(block: OpenApiInfo.() -> Unit) { + info = OpenApiInfo().apply(block) + } + + private var info = OpenApiInfo() + + + /** + * OpenAPI external docs configuration - link and description of an external documentation + */ + fun externalDocs(block: OpenApiExternalDocs.() -> Unit) { + externalDocs = OpenApiExternalDocs().apply(block) + } + + private var externalDocs = OpenApiExternalDocs() + + + /** + * OpenAPI server configuration - an array of servers, which provide connectivity information to a target server + */ + fun server(block: OpenApiServer.() -> Unit) { + servers.add(OpenApiServer().apply(block)) + } + + private val servers = mutableListOf() + + + /** + * Swagger-UI configuration + */ + fun swagger(block: SwaggerUIDsl.() -> Unit) { + swaggerUI = SwaggerUIDsl().apply(block) + } + + private var swaggerUI = SwaggerUIDsl() + + + /** + * Configuration for security and authentication. + */ + fun security(block: OpenApiSecurity.() -> Unit) { + security.apply(block) + } + + private val security = OpenApiSecurity() + + + /** + * Configuration for openapi-tags + */ + fun tags(block: OpenApiTags.() -> Unit) { + tags.also(block) + } + + private val tags = OpenApiTags() + + + /** + * Configure schemas + */ + fun schemas(block: SchemaConfig.() -> Unit) { + schemaConfig.also(block) + } + + private val schemaConfig = SchemaConfig() + + + /** + * Configure examples + */ + fun examples(block: ExampleConfig.() -> Unit) { + exampleConfig.apply(block) + } + + private val exampleConfig = ExampleConfig() + + + /** + * Configure specific separate specs + */ + fun spec(specId: String, block: PluginConfigDsl.() -> Unit) { + specConfigs[specId] = PluginConfigDsl().apply(block) + } + + private val specConfigs = mutableMapOf() + + /** + * Assigns routes without an [io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute.specId] to a specified openapi-spec. + */ + var specAssigner: SpecAssigner? = PluginConfigData.DEFAULT.specAssigner + + + /** + * Filter to apply to all routes. Return 'false' for routes to not include them in the OpenApi-Spec and Swagger-UI. + * The url of the paths are already split at '/'. + */ + var pathFilter: PathFilter? = PluginConfigData.DEFAULT.pathFilter + + + /** + * List of all [RouteSelector] types in that should be ignored in the resulting url of any route. + */ + var ignoredRouteSelectors: Set> = PluginConfigData.DEFAULT.ignoredRouteSelectors + + + /** + * Invoked after generating the openapi-spec. Can be to e.g. further customize the spec. + */ + var postBuild: PostBuild? = null + + + internal fun build(base: PluginConfigData): PluginConfigData { + return PluginConfigData( + info = info.build(base.info), + externalDocs = externalDocs.build(base.externalDocs), + servers = buildList { + addAll(base.servers) + addAll(servers.map { it.build(ServerData.DEFAULT) }) + }, + swagger = swaggerUI.build(base.swagger), + securityConfig = security.build(base.securityConfig), + tagsConfig = tags.build(base.tagsConfig), + schemaConfig = schemaConfig.build(), + exampleConfig = exampleConfig.build(), + specAssigner = merge(base.specAssigner, specAssigner) ?: PluginConfigData.DEFAULT.specAssigner, + pathFilter = merge(base.pathFilter, pathFilter) ?: PluginConfigData.DEFAULT.pathFilter, + ignoredRouteSelectors = buildSet { + addAll(base.ignoredRouteSelectors) + addAll(ignoredRouteSelectors) + }, + specConfigs = mutableMapOf(), + postBuild = postBuild, + ).also { + specConfigs.forEach { (specId, config) -> + it.specConfigs[specId] = config.build(it) + } + } + } +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/SchemaConfig.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/SchemaConfig.kt new file mode 100644 index 0000000..8d48bdd --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/SchemaConfig.kt @@ -0,0 +1,54 @@ +package io.github.smiley4.ktorswaggerui.dsl.config + +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.SchemaConfigData +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.github.smiley4.schemakenerator.swagger.data.CompiledSwaggerSchema +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Configuration for schemas + */ +@OpenApiDslMarker +class SchemaConfig { + + var generator: (type: KType) -> CompiledSwaggerSchema = SchemaConfigData.DEFAULT.generator + + val schemas = mutableMapOf() + + val overwrite = mutableMapOf() + + fun overwrite(type: KType, replacement: TypeDescriptor) { + overwrite[type] = replacement + } + + inline fun overwrite(replacement: TypeDescriptor) = overwrite(typeOf(), replacement) + + inline fun overwrite(replacement: Schema<*>) = overwrite(typeOf(), SwaggerTypeDescriptor(replacement)) + + inline fun overwrite(replacement: KType) = overwrite(typeOf(), KTypeDescriptor(replacement)) + + inline fun overwrite() = overwrite(typeOf(), KTypeDescriptor(typeOf())) + + + fun schema(schemaId: String, descriptor: TypeDescriptor) { + schemas[schemaId] = descriptor + } + + fun schema(schemaId: String, schema: Schema<*>) = schema(schemaId, SwaggerTypeDescriptor(schema)) + + fun schema(schemaId: String, schema: KType) = schema(schemaId, KTypeDescriptor(schema)) + + inline fun schema(schemaId: String) = schema(schemaId, KTypeDescriptor(typeOf())) + + fun build() = SchemaConfigData( + generator = generator, + schemas = schemas, + overwrite = overwrite + ) + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/SwaggerUIDsl.kt similarity index 61% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/SwaggerUIDsl.kt index 22c7de2..1aa4fe0 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/config/SwaggerUIDsl.kt @@ -1,44 +1,17 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.config import io.github.smiley4.ktorswaggerui.data.DataUtils.merge import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeBoolean import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault import io.github.smiley4.ktorswaggerui.data.SwaggerUIData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker - +/** + * Configuration for the swagger-ui + */ @OpenApiDslMarker class SwaggerUIDsl { - /** - * Whether to use the automatic swagger-ui router or create swagger-ui router manually. - * 'false' results in [forwardRoot], [swaggerUrl], [rootHostPath], [authentication] being ignored. - */ - var automaticRouter: Boolean = SwaggerUIData.DEFAULT.automaticRouter - - /** - * Whether to forward the root-url to the swagger-url - */ - var forwardRoot: Boolean = SwaggerUIData.DEFAULT.forwardRoot - - - /** - * the url to the swagger-ui - */ - var swaggerUrl: String = SwaggerUIData.DEFAULT.swaggerUrl - - - /** - * the path under which the KTOR app gets deployed. can be useful if reverse proxy is in use. - */ - var rootHostPath: String = SwaggerUIData.DEFAULT.rootHostPath - - - /** - * The name of the authentication to use for the swagger routes. Null to not protect the swagger-ui. - */ - var authentication: String? = SwaggerUIData.DEFAULT.authentication - - /** * Swagger UI can attempt to validate specs against swagger.io's online validator. * You can use this parameter to set a different validator URL, for example for locally deployed validators. @@ -60,9 +33,6 @@ class SwaggerUIDsl { specValidator("https://validator.swagger.io/validator") } - fun getSpecValidatorUrl() = validatorUrl - - /** * Whether to show the operation-id of endpoints in the list */ @@ -93,17 +63,12 @@ class SwaggerUIDsl { internal fun build(base: SwaggerUIData): SwaggerUIData { return SwaggerUIData( - automaticRouter = automaticRouter, - forwardRoot = mergeBoolean(base.forwardRoot, this.forwardRoot), - swaggerUrl = mergeDefault(base.swaggerUrl, this.swaggerUrl, SwaggerUIData.DEFAULT.swaggerUrl), - rootHostPath = mergeDefault(base.rootHostPath, this.rootHostPath, SwaggerUIData.DEFAULT.rootHostPath), - authentication = merge(base.authentication, this.authentication), validatorUrl = merge(base.validatorUrl, this.validatorUrl), displayOperationId = mergeBoolean(base.displayOperationId, this.displayOperationId), showTagFilterInput = mergeBoolean(base.showTagFilterInput, this.showTagFilterInput), sort = mergeDefault(base.sort, this.sort, SwaggerUIData.DEFAULT.sort), syntaxHighlight = mergeDefault(base.syntaxHighlight, this.syntaxHighlight, SwaggerUIData.DEFAULT.syntaxHighlight), - withCredentials = base.withCredentials + withCredentials = mergeBoolean(base.withCredentials, this.withCredentials) ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiBaseBody.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiBaseBody.kt similarity index 65% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiBaseBody.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiBaseBody.kt index 394ff74..20adb16 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiBaseBody.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiBaseBody.kt @@ -1,5 +1,7 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.routes +import io.github.smiley4.ktorswaggerui.data.OpenApiBaseBodyData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker import io.ktor.http.ContentType /** @@ -21,12 +23,7 @@ sealed class OpenApiBaseBody { /** * Allowed Media Types for this body. If none specified, a media type will be chosen automatically based on the provided schema */ - private val mediaTypes = mutableSetOf() - - fun mediaType(type: ContentType) { - mediaTypes.add(type) - } - - fun getMediaTypes(): Set = mediaTypes + var mediaTypes: Collection = emptySet() + abstract fun build(): OpenApiBaseBodyData } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiHeader.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiHeader.kt new file mode 100644 index 0000000..099214b --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiHeader.kt @@ -0,0 +1,80 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiHeaderData +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Describes a single header. + */ +@OpenApiDslMarker +class OpenApiHeader { + + /** + * A description of the header + */ + var description: String? = null + + + /** + * The schema of the header + */ + var type: TypeDescriptor? = null + + + /** + * The schema of the header + */ + fun type(type: TypeDescriptor) { + this.type = type + } + + + /** + * The schema of the header + */ + fun type(type: Schema<*>) = type(SwaggerTypeDescriptor(type)) + + + /** + * The schema of the header + */ + fun type(type: KType) = type(KTypeDescriptor(type)) + + + /** + * The schema of the header + */ + inline fun type() = type(KTypeDescriptor(typeOf())) + + + /** + * Determines whether this header is mandatory + */ + var required: Boolean? = null + + + /** + * Specifies that a header is deprecated and SHOULD be transitioned out of usage + */ + var deprecated: Boolean? = null + + + /** + * Specifies whether arrays and objects should generate separate parameters for each array item or object property. + */ + var explode: Boolean? = null + + fun build() = OpenApiHeaderData( + description = description, + type = type, + required = required ?: false, + deprecated = deprecated ?: false, + explode = explode, + ) +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartBody.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartBody.kt new file mode 100644 index 0000000..fc89dee --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartBody.kt @@ -0,0 +1,56 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiMultipartBodyData +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType +import kotlin.reflect.typeOf + + +/** + * Describes a single request/response body with multipart content. + * See https://swagger.io/docs/specification/describing-request-body/multipart-requests/ for more info + */ +@OpenApiDslMarker +class OpenApiMultipartBody : OpenApiBaseBody() { + + private val parts = mutableListOf() + + + /** + * One part of a multipart-body + */ + fun part(name: String, type: TypeDescriptor, block: OpenApiMultipartPart.() -> Unit = {}) { + parts.add(OpenApiMultipartPart(name, type).apply(block)) + } + + + /** + * One part of a multipart-body + */ + fun part(name: String, type: Schema<*>, block: OpenApiMultipartPart.() -> Unit = {}) = part(name, SwaggerTypeDescriptor(type), block) + + + /** + * One part of a multipart-body + */ + fun part(name: String, type: KType, block: OpenApiMultipartPart.() -> Unit = {}) = part(name, KTypeDescriptor(type), block) + + + /** + * One part of a multipart-body + */ + inline fun part(name: String, noinline block: OpenApiMultipartPart.() -> Unit = {}) = + part(name, KTypeDescriptor(typeOf()), block) + + + override fun build() = OpenApiMultipartBodyData( + description = description, + required = required ?: false, + mediaTypes = mediaTypes.toSet(), + parts = parts.map { it.build() } + ) +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartPart.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartPart.kt new file mode 100644 index 0000000..98d0902 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiMultipartPart.kt @@ -0,0 +1,75 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiMultipartPartData +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.ktor.http.ContentType +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Describes one section of a multipart-body. + * See https://swagger.io/docs/specification/describing-request-body/multipart-requests/ for more info + */ +@OpenApiDslMarker +class OpenApiMultipartPart( + /** + * The name of this part + */ + val name: String, + + val type: TypeDescriptor +) { + + /** + * Set a specific content types for this part + */ + var mediaTypes: Collection = setOf() + + + /** + * List of headers of this part + */ + val headers = mutableMapOf() + + + /** + * Possible headers for this part + */ + fun header(name: String, type: TypeDescriptor, block: OpenApiHeader.() -> Unit = {}) { + headers[name] = OpenApiHeader().apply(block).apply { + this.type = type + } + } + + + /** + * Possible headers for this part + */ + fun header(name: String, type: Schema<*>, block: OpenApiHeader.() -> Unit = {}) = header(name, SwaggerTypeDescriptor(type), block) + + + /** + * Possible headers for this part + */ + fun header(name: String, type: KType, block: OpenApiHeader.() -> Unit = {}) = header(name, KTypeDescriptor(type), block) + + + /** + * Possible headers for this part + */ + inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit = {}) = + header(name, KTypeDescriptor(typeOf()), block) + + + fun build() = OpenApiMultipartPartData( + name = name, + type = type, + mediaTypes = mediaTypes.toSet(), + headers = headers.mapValues { it.value.build() } + ) + +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequest.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequest.kt new file mode 100644 index 0000000..21a34a1 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequest.kt @@ -0,0 +1,181 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiRequestData +import io.github.smiley4.ktorswaggerui.data.ParameterLocation +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Describes a single request. + */ +@OpenApiDslMarker +class OpenApiRequest { + + /** + * A list of parameters that are applicable for this operation + */ + val parameters = mutableListOf() + + + /** + * A path parameters that is applicable for this operation + */ + fun parameter(location: ParameterLocation, name: String, type: TypeDescriptor, block: OpenApiRequestParameter.() -> Unit = {}) { + parameters.add(OpenApiRequestParameter(name, type, location).apply(block)) + } + + + /** + * A path parameters that is applicable for this operation + */ + fun pathParameter(name: String, type: TypeDescriptor, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.PATH, name, type, block) + + + /** + * A path parameters that is applicable for this operation + */ + fun pathParameter(name: String, type: Schema<*>, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.PATH, name, SwaggerTypeDescriptor(type), block) + + + /** + * A path parameters that is applicable for this operation + */ + fun pathParameter(name: String, type: KType, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.PATH, name, KTypeDescriptor(type), block) + + + /** + * A path parameters that is applicable for this operation + */ + inline fun pathParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.PATH, name, KTypeDescriptor(typeOf()), block) + + + /** + * A query parameters that is applicable for this operation + */ + fun queryParameter(name: String, type: TypeDescriptor, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.QUERY, name, type, block) + + + /** + * A query parameters that is applicable for this operation + */ + fun queryParameter(name: String, type: Schema<*>, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.QUERY, name, SwaggerTypeDescriptor(type), block) + + + /** + * A query parameters that is applicable for this operation + */ + fun queryParameter(name: String, type: KType, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.QUERY, name, KTypeDescriptor(type), block) + + + /** + * A query parameters that is applicable for this operation + */ + inline fun queryParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.QUERY, name, KTypeDescriptor(typeOf()), block) + + + /** + * A header parameters that is applicable for this operation + */ + fun headerParameter(name: String, type: TypeDescriptor, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.HEADER, name, type, block) + + + /** + * A header parameters that is applicable for this operation + */ + fun headerParameter(name: String, type: Schema<*>, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.HEADER, name, SwaggerTypeDescriptor(type), block) + + + /** + * A header parameters that is applicable for this operation + */ + fun headerParameter(name: String, type: KType, block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.HEADER, name, KTypeDescriptor(type), block) + + + /** + * A header parameters that is applicable for this operation + */ + inline fun headerParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit = {}) = + parameter(ParameterLocation.HEADER, name, KTypeDescriptor(typeOf()), block) + + + private var body: OpenApiBaseBody? = null + + fun getBody() = body + + + /** + * The body returned with this request + */ + fun body(type: TypeDescriptor, block: OpenApiSimpleBody.() -> Unit = {}) { + val result = OpenApiSimpleBody(type).apply(block) + if (!result.isEmptyBody()) { + body = result + } + } + + + /** + * The body returned with this request + */ + fun body(type: Schema<*>, block: OpenApiSimpleBody.() -> Unit = {}) = body(SwaggerTypeDescriptor(type), block) + + + /** + * The body returned with this request + */ + fun body(type: KType, block: OpenApiSimpleBody.() -> Unit = {}) = body(KTypeDescriptor(type), block) + + + /** + * The body returned with this request + */ + inline fun body(noinline block: OpenApiSimpleBody.() -> Unit = {}) = body(KTypeDescriptor(typeOf()), block) + + + /** + * The multipart-body returned with this request + */ + fun multipartBody(block: OpenApiMultipartBody.() -> Unit) { + body = OpenApiMultipartBody().apply(block) + } + + + /** + * Set the body of this request. Intended for internal use. + */ + fun setBody(body: OpenApiBaseBody?) { + this.body = body + } + + fun build() = OpenApiRequestData( + parameters = parameters.map { it.build() }, + body = body?.build() + ) + + private fun OpenApiBaseBody.isEmptyBody(): Boolean { + return when (this) { + is OpenApiSimpleBody -> when (type) { + is KTypeDescriptor -> type.type == typeOf() + else -> false + } + is OpenApiMultipartBody -> false + } + } + +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequestParameter.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequestParameter.kt new file mode 100644 index 0000000..41ce904 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRequestParameter.kt @@ -0,0 +1,124 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.ExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiRequestParameterData +import io.github.smiley4.ktorswaggerui.data.ParameterLocation +import io.github.smiley4.ktorswaggerui.data.SwaggerExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.data.ValueExampleDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.swagger.v3.oas.models.examples.Example +import io.swagger.v3.oas.models.parameters.Parameter + +/** + * Describes a single request parameter. + */ +@OpenApiDslMarker +class OpenApiRequestParameter( + /** + * The name (case-sensitive) of the parameter + */ + val name: String, + /** + * The type defining the schema used for the parameter. + */ + val type: TypeDescriptor, + /** + * Location of the parameter + */ + val location: ParameterLocation +) { + + /** + * A brief description of the parameter + */ + var description: String? = null + + + /** + * An example value for this parameter + */ + var example: ExampleDescriptor? = null + + /** + * An example value for this parameter + */ + fun example(example: ExampleDescriptor) { + this.example = example + } + + /** + * An example value for this parameter + */ + fun example(name: String, example: Example) = example(SwaggerExampleDescriptor(name, example)) + + /** + * An example value for this parameter + */ + fun example(name: String, example: ValueExampleDescriptorDsl.() -> Unit) = example( + ValueExampleDescriptorDsl() + .apply(example) + .let { result -> + ValueExampleDescriptor( + name = name, + value = result.value, + summary = result.summary, + description = result.description + ) + } + ) + + + /** + * Determines whether this parameter is mandatory + */ + var required: Boolean? = null + + + /** + * Specifies that a parameter is deprecated and SHOULD be transitioned out of usage + */ + var deprecated: Boolean? = null + + + /** + * Sets the ability to pass empty-valued parameters. + * This is valid only for query parameters and allows sending a parameter with an empty value. + */ + var allowEmptyValue: Boolean? = null + + + /** + * When this is true, parameter values of type array or object generate separate parameters for each value of the array or key-value + * pair of the map. For other types of parameters this property has no effect + */ + var explode: Boolean? = null + + + /** + * Determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included + * without percent-encoding. This property only applies to parameters with an in value of query + */ + var allowReserved: Boolean? = null + + + /** + * Describes how the parameter value will be serialized depending on the type of the parameter value. + */ + var style: Parameter.StyleEnum? = null + + fun build() = OpenApiRequestParameterData( + name = name, + type = type, + location = location, + description = description, + example = example, + required = required ?: false, + deprecated = deprecated ?: false, + allowEmptyValue = allowEmptyValue ?: true, + explode = explode ?: false, + allowReserved = allowReserved ?: true, + style = style + ) + +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiResponse.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiResponse.kt new file mode 100644 index 0000000..5e0af62 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiResponse.kt @@ -0,0 +1,99 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiResponseData +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * A container for the expected responses of an operation. The container maps an HTTP response code to the expected response. + * A response code can only have one response object. + */ +@OpenApiDslMarker +class OpenApiResponse(val statusCode: String) { + + /** + * A short description of the response + */ + var description: String? = null + + val headers = mutableMapOf() + + + /** + * Possible headers returned with this response + */ + fun header(name: String, type: TypeDescriptor, block: OpenApiHeader.() -> Unit = {}) { + headers[name] = OpenApiHeader().apply(block).apply { + this.type = type + } + } + + + /** + * Possible headers returned with this response + */ + fun header(name: String, type: Schema<*>, block: OpenApiHeader.() -> Unit = {}) = header(name, SwaggerTypeDescriptor(type), block) + + + /** + * Possible headers returned with this response + */ + fun header(name: String, type: KType, block: OpenApiHeader.() -> Unit = {}) = header(name, KTypeDescriptor(type), block) + + + /** + * Possible headers returned with this response + */ + inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit = {}) = + header(name, KTypeDescriptor(typeOf()), block) + + + private var body: OpenApiBaseBody? = null + + + /** + * The body returned with this response + */ + fun body(type: TypeDescriptor, block: OpenApiSimpleBody.() -> Unit = {}) { + body = OpenApiSimpleBody(type).apply(block) + } + + /** + * The body returned with this response + */ + fun body(type: Schema<*>, block: OpenApiSimpleBody.() -> Unit = {}) = body(SwaggerTypeDescriptor(type), block) + + /** + * The body returned with this response + */ + fun body(type: KType, block: OpenApiSimpleBody.() -> Unit = {}) = body(KTypeDescriptor(type), block) + + /** + * The body returned with this response + */ + inline fun body(noinline block: OpenApiSimpleBody.() -> Unit = {}) = body(KTypeDescriptor(typeOf()), block) + + + + + /** + * The multipart-body returned with this response + */ + fun multipartBody(block: OpenApiMultipartBody.() -> Unit) { + body = OpenApiMultipartBody().apply(block) + } + + + fun build() = OpenApiResponseData( + statusCode = statusCode, + description = description, + headers = headers.mapValues { it.value.build() }, + body = body?.build() + ) + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponses.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiResponses.kt similarity index 89% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponses.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiResponses.kt index 5499ecc..0a41cf2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponses.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiResponses.kt @@ -1,5 +1,6 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.routes +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker import io.ktor.http.HttpStatusCode /** diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRoute.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRoute.kt similarity index 52% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRoute.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRoute.kt index 68dc0db..c85084c 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRoute.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiRoute.kt @@ -1,5 +1,15 @@ -package io.github.smiley4.ktorswaggerui.dsl - +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.ExternalDocsData +import io.github.smiley4.ktorswaggerui.data.OpenApiRouteData +import io.github.smiley4.ktorswaggerui.data.ServerData +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiExternalDocs +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiServer + +/** + * Describes a single route including request and responses. + */ @OpenApiDslMarker class OpenApiRoute { @@ -8,10 +18,11 @@ class OpenApiRoute { */ var specId: String? = null + /** * A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier */ - var tags: List = emptyList() + var tags: Collection = emptyList() /** @@ -46,27 +57,20 @@ class OpenApiRoute { /** - * A declaration of which security mechanism can be used for this operation. - * If not specified (and none specified with [securitySchemeNames]), defaultSecuritySchemeName (global plugin config) will be used. - * Only applied to [protected] operations. + * Specifies whether this operation is protected. + * If not specified, the authentication state of the Ktor route will be used + * (i.e. whether it is surrounded by an [authenticate][io.ktor.server.auth.authenticate] block or not). */ - var securitySchemeName: String? = null + var protected: Boolean? = null /** * A declaration of which security mechanisms can be used for this operation (i.e. any of the specified ones). - * If none specified (and none with [securitySchemeName]), defaultSecuritySchemeName (global plugin config) will be used. + * If none is specified, defaultSecuritySchemeName (global plugin config) will be used. * Only applied to [protected] operations. */ var securitySchemeNames: Collection? = null - /** - * Specifies whether this operation is protected. - * If not specified, the authentication state of the Ktor route will be used - * (i.e. whether it is surrounded by an [authenticate][io.ktor.server.auth.authenticate] block or not). - */ - var protected: Boolean? = null - private val request = OpenApiRequest() @@ -91,4 +95,41 @@ class OpenApiRoute { fun getResponses() = responses + + /** + * OpenAPI external docs configuration - link and description of an external documentation + */ + fun externalDocs(block: OpenApiExternalDocs.() -> Unit) { + externalDocs = OpenApiExternalDocs().apply(block) + } + + private var externalDocs: OpenApiExternalDocs? = null + + + /** + * OpenAPI server configuration - an array of servers, which provide connectivity information to a target server + */ + fun server(block: OpenApiServer.() -> Unit) { + servers.add(OpenApiServer().apply(block)) + } + + private val servers = mutableListOf() + + + fun build() = OpenApiRouteData( + specId = specId, + tags = tags.toSet(), + summary = summary, + description = description, + operationId = operationId, + deprecated = deprecated, + hidden = hidden, + securitySchemeNames = securitySchemeNames?.toList() ?: emptyList(), + protected = protected, + request = request.build(), + responses = responses.getResponses().map { it.build() }, + externalDocs = externalDocs?.build(ExternalDocsData.DEFAULT), + servers = servers.map { it.build(ServerData.DEFAULT) } + ) + } diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiSimpleBody.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiSimpleBody.kt new file mode 100644 index 0000000..517dc52 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/OpenApiSimpleBody.kt @@ -0,0 +1,80 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +import io.github.smiley4.ktorswaggerui.data.ExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.OpenApiSimpleBodyData +import io.github.smiley4.ktorswaggerui.data.RefExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.SwaggerExampleDescriptor +import io.github.smiley4.ktorswaggerui.data.TypeDescriptor +import io.github.smiley4.ktorswaggerui.data.ValueExampleDescriptor +import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker +import io.swagger.v3.oas.models.examples.Example + + +/** + * Describes the base of a single request/response body. + */ +@OpenApiDslMarker +class OpenApiSimpleBody( + /** + * The type defining the schema used for the body. + */ + val type: TypeDescriptor, +) : OpenApiBaseBody() { + + /** + * Examples for this body + */ + private val examples = mutableListOf() + + /** + * Add the given example as an example to this body + */ + fun example(example: ExampleDescriptor) { + examples.add(example) + } + + /** + * Add the given example as an example to this body + */ + fun example(name: String, example: Example) = example(SwaggerExampleDescriptor(name, example)) + + /** + * Add the given example as an example to this body + */ + fun example(name: String, example: ValueExampleDescriptorDsl.() -> Unit) = example( + ValueExampleDescriptorDsl() + .apply(example) + .let { result -> + ValueExampleDescriptor( + name = name, + value = result.value, + summary = result.summary, + description = result.description + ) + } + ) + + + /** + * Add the given example as an example to this body + * @param name the name of the example to display at this body + * @param refName the name of the referenced example + */ + fun exampleRef(name: String, refName: String) = example(RefExampleDescriptor(name, refName)) + + /** + * Add the given example as an example to this body + * @param name the name of the example + */ + fun exampleRef(name: String) = example(RefExampleDescriptor(name, name)) + + + override fun build() = OpenApiSimpleBodyData( + description = description, + required = required ?: false, + mediaTypes = mediaTypes.toSet(), + type = type, + examples = examples, + ) + +} diff --git a/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/ValueExampleDescriptorDsl.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/ValueExampleDescriptorDsl.kt new file mode 100644 index 0000000..a66ac23 --- /dev/null +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routes/ValueExampleDescriptorDsl.kt @@ -0,0 +1,22 @@ +package io.github.smiley4.ktorswaggerui.dsl.routes + +class ValueExampleDescriptorDsl { + + /** + * the example value + */ + var value: Any? = null + + + /** + * a short summary of the example + */ + var summary: String? = null + + + /** + * a description of the example + */ + var description: String? = null + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/DocumentedRouteSelector.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routing/DocumentedRouteSelector.kt similarity index 98% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/DocumentedRouteSelector.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routing/DocumentedRouteSelector.kt index f15f99d..9becb49 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/DocumentedRouteSelector.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routing/DocumentedRouteSelector.kt @@ -1,5 +1,6 @@ -package io.github.smiley4.ktorswaggerui.dsl +package io.github.smiley4.ktorswaggerui.dsl.routing +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiRoute import io.ktor.http.HttpMethod import io.ktor.server.application.ApplicationCall import io.ktor.server.routing.Route diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/resources/DocumentedResourceRoutes.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routing/resources/DocumentedResourceRoutes.kt similarity index 93% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/resources/DocumentedResourceRoutes.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routing/resources/DocumentedResourceRoutes.kt index ed6c553..fb30d31 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/resources/DocumentedResourceRoutes.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/routing/resources/DocumentedResourceRoutes.kt @@ -1,8 +1,8 @@ -package io.github.smiley4.ktorswaggerui.dsl.resources +package io.github.smiley4.ktorswaggerui.dsl.routing.resources -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.dsl.documentation -import io.github.smiley4.ktorswaggerui.dsl.method +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiRoute +import io.github.smiley4.ktorswaggerui.dsl.routing.documentation +import io.github.smiley4.ktorswaggerui.dsl.routing.method import io.ktor.http.HttpMethod import io.ktor.server.application.ApplicationCall import io.ktor.server.resources.handle diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ApiSpec.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ApiSpec.kt similarity index 74% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ApiSpec.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ApiSpec.kt index 5d97431..c3ff84e 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ApiSpec.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ApiSpec.kt @@ -1,7 +1,11 @@ package io.github.smiley4.ktorswaggerui.routing +import io.github.smiley4.ktorswaggerui.data.SwaggerUIData + object ApiSpec { + var swaggerUiConfig: SwaggerUIData = SwaggerUIData.DEFAULT + private val apiSpecs = mutableMapOf() fun setAll(specs: Map) { @@ -14,7 +18,7 @@ object ApiSpec { } fun get(name: String): String { - return apiSpecs[name] ?: throw NoSuchElementException("No api-spec with name $name registered.") + return apiSpecs[name] ?: throw NoSuchElementException("No api-spec with name '$name' registered.") } fun getAll(): Map { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ResourceContent.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ResourceContent.kt similarity index 100% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ResourceContent.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ResourceContent.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/manualRouting.kt b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/routing.kt similarity index 78% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/manualRouting.kt rename to ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/routing.kt index 1e90969..ecff6b2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/manualRouting.kt +++ b/ktor-swagger-ui/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/routing.kt @@ -4,15 +4,18 @@ import io.github.smiley4.ktorswaggerui.SWAGGER_UI_WEBJARS_VERSION import io.github.smiley4.ktorswaggerui.SwaggerUI import io.github.smiley4.ktorswaggerui.data.SwaggerUIData import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl -import io.github.smiley4.ktorswaggerui.dsl.route +import io.github.smiley4.ktorswaggerui.data.SwaggerUiSyntaxHighlight +import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.dsl.routing.route import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* - +/** + * Registers the route for serving an openapi-spec. When multiple specs are configured, the id of the one to serve has to be provided. + */ fun Route.openApiSpec(specId: String = PluginConfigDsl.DEFAULT_SPEC_ID) { route({ hidden = true }) { get { @@ -21,6 +24,9 @@ fun Route.openApiSpec(specId: String = PluginConfigDsl.DEFAULT_SPEC_ID) { } } +/** + * Registers the route for serving all swagger-ui resources. The path to the openapi-spec file to use has to be given. + */ fun Route.swaggerUI(apiUrl: String) { route({ hidden = true }) { get { @@ -30,7 +36,7 @@ fun Route.swaggerUI(apiUrl: String) { serveStaticResource(call.parameters["filename"]!!, SWAGGER_UI_WEBJARS_VERSION, call) } get("swagger-initializer.js") { - serveSwaggerInitializer(call, SwaggerUIData.DEFAULT, apiUrl) + serveSwaggerInitializer(call, ApiSpec.swaggerUiConfig, apiUrl) } } } @@ -43,7 +49,9 @@ private suspend fun serveSwaggerInitializer(call: ApplicationCall, swaggerUiConf val propSort = "operationsSorter: " + if (swaggerUiConfig.sort == SwaggerUiSort.NONE) "undefined" else "\"${swaggerUiConfig.sort.value}\"" - val propSyntaxHighlight = "syntaxHighlight: { theme: \"${swaggerUiConfig.syntaxHighlight.value}\" }" + val propSyntaxHighlight = "syntaxHighlight: " + + if(swaggerUiConfig.syntaxHighlight == SwaggerUiSyntaxHighlight.DISABLED) "false" + else "{ theme: \"${swaggerUiConfig.syntaxHighlight.value}\" }" val content = """ window.onload = function() { window.ui = SwaggerUIBundle({ diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ExternalDocsBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ExternalDocsBuilderTest.kt similarity index 90% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ExternalDocsBuilderTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ExternalDocsBuilderTest.kt index d49ab31..b82b2cc 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ExternalDocsBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ExternalDocsBuilderTest.kt @@ -1,8 +1,8 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi +package io.github.smiley4.ktorswaggerui.builder import io.github.smiley4.ktorswaggerui.data.ExternalDocsData -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExternalDocs import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiExternalDocs import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.swagger.v3.oas.models.ExternalDocumentation diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt similarity index 89% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt index 51475ef..efa98ce 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/InfoBuilderTest.kt @@ -1,10 +1,9 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi - +package io.github.smiley4.ktorswaggerui.builder import io.github.smiley4.ktorswaggerui.data.InfoData -import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo import io.github.smiley4.ktorswaggerui.builder.openapi.ContactBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.InfoBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.LicenseBuilder +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiInfo import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe @@ -30,6 +29,7 @@ class InfoBuilderTest : StringSpec({ buildInfoObject { title = "Test Api" version = "1.0" + summary = "testing api" description = "Api for testing" termsOfService = "test-tos" contact { @@ -41,10 +41,12 @@ class InfoBuilderTest : StringSpec({ license { name = "Test License" url = "example.com" + identifier = "Example" } }.also { info -> info.title shouldBe "Test Api" info.version shouldBe "1.0" + info.summary shouldBe "testing api" info.description shouldBe "Api for testing" info.termsOfService shouldBe "test-tos" info.contact @@ -59,9 +61,9 @@ class InfoBuilderTest : StringSpec({ ?.also { license -> license.name shouldBe "Test License" license.url shouldBe "example.com" + license.identifier shouldBe "Example" } info.extensions shouldBe null - info.summary shouldBe null } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt similarity index 64% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt index f682ce8..b4a3fa3 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt @@ -1,16 +1,34 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi +package io.github.smiley4.ktorswaggerui.builder -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext -import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.builder.openapi.* +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextImpl +import io.github.smiley4.ktorswaggerui.builder.openapi.ComponentsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContactBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.InfoBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.LicenseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OpenApiBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.PathsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.SecuritySchemesBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.TagExternalDocumentationBuilder import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextImpl +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize @@ -49,20 +67,28 @@ class OpenApiBuilderTest : StringSpec({ } buildOpenApiObject(emptyList(), config).also { openapi -> openapi.servers shouldHaveSize 2 - openapi.servers.map { it.url } shouldContainExactlyInAnyOrder listOf( - "http://localhost:8080", - "https://127.0.0.1" - ) + openapi.servers.find { it.url == "http://localhost:8080" }!!.also { server -> + server.url shouldBe "http://localhost:8080" + server.description shouldBe "Development Server" + server.variables shouldBe null + } + openapi.servers.find { it.url == "https://127.0.0.1" }!!.also { server -> + server.url shouldBe "https://127.0.0.1" + server.description shouldBe "Production Server" + server.variables shouldBe null + } } } "multiple tags" { val config = PluginConfigDsl().also { - it.tag("tag-1") { - description = "first test tag" - } - it.tag("tag-2") { - description = "second test tag" + it.tags { + tag("tag-1") { + description = "first test tag" + } + tag("tag-2") { + description = "second test tag" + } } } buildOpenApiObject(emptyList(), config).also { openapi -> @@ -82,23 +108,18 @@ class OpenApiBuilderTest : StringSpec({ private fun schemaContext(routes: List, pluginConfig: PluginConfigDsl): SchemaContext { val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) - return SchemaContextBuilder( - config =pluginConfigData, - schemaBuilder = SchemaBuilder( - definitionsField = pluginConfigData.encoding.schemaDefsField, - schemaEncoder = pluginConfigData.encoding.schemaEncoder, - ObjectMapper(), - TypeOverwrites.get() - ) - ).build(routes) + return SchemaContextImpl(pluginConfigData.schemaConfig).also { + it.addGlobal(pluginConfigData.schemaConfig) + it.add(routes) + } } private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl): ExampleContext { - return ExampleContextBuilder( - exampleBuilder = ExampleBuilder( - config = pluginConfig.build(PluginConfigData.DEFAULT) - ) - ).build(routes.toList()) + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) + return ExampleContextImpl().also { + it.addShared(pluginConfigData.exampleConfig) + it.add(routes) + } } private fun buildOpenApiObject(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): OpenAPI { @@ -145,6 +166,8 @@ class OpenApiBuilderTest : StringSpec({ config = pluginConfigData ), securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ) ) ), diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt similarity index 85% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt index 39184ed..775c562 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OperationBuilderTest.kt @@ -1,13 +1,9 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi +package io.github.smiley4.ktorswaggerui.builder -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext -import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextImpl import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder -import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder @@ -16,13 +12,17 @@ import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.custom +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextImpl +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.data.RefTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.SwaggerTypeDescriptor +import io.github.smiley4.ktorswaggerui.data.ValueExampleDescriptor +import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiRoute import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder @@ -37,6 +37,7 @@ import io.ktor.http.HttpStatusCode import io.swagger.v3.oas.models.Operation import io.swagger.v3.oas.models.media.Schema import java.io.File +import kotlin.reflect.typeOf class OperationBuilderTest : StringSpec({ @@ -44,7 +45,7 @@ class OperationBuilderTest : StringSpec({ val route = RouteMeta( path = "/test", method = HttpMethod.Get, - documentation = OpenApiRoute(), + documentation = OpenApiRoute().build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -75,7 +76,7 @@ class OperationBuilderTest : StringSpec({ route.summary = "this is some test route" route.operationId = "testRoute" route.deprecated = true - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -98,14 +99,16 @@ class OperationBuilderTest : StringSpec({ "operation with auto-generated tags" { val config = PluginConfigDsl().also { - it.generateTags { url -> listOf(url.firstOrNull()) } + it.tags { + tagGenerator = { url -> listOf(url.firstOrNull()) } + } } val routeA = RouteMeta( path = "a/test", method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.tags = listOf("defaultTag") - }, + }.build(), protected = false ) val routeB = RouteMeta( @@ -113,7 +116,7 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.tags = listOf("defaultTag") - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(routeA, routeB), config) @@ -132,7 +135,7 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.securitySchemeNames = listOf("security1", "security2") - }, + }.build(), protected = true ) val schemaContext = schemaContext(listOf(route)) @@ -152,7 +155,7 @@ class OperationBuilderTest : StringSpec({ val route = RouteMeta( path = "/test", method = HttpMethod.Get, - documentation = OpenApiRoute(), + documentation = OpenApiRoute().build(), protected = true ) val schemaContext = schemaContext(listOf(route)) @@ -179,7 +182,7 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.securitySchemeNames = listOf("security1", "security2") - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -206,12 +209,12 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.request { - queryParameter("queryParam") - pathParameter("pathParam") - headerParameter("headerParam") - body>() + queryParameter("queryParam", KTypeDescriptor(typeOf())) {} + pathParameter("pathParam", KTypeDescriptor(typeOf())) {} + headerParameter("headerParam", KTypeDescriptor(typeOf())) {} + body(KTypeDescriptor(typeOf>())) } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -229,13 +232,13 @@ class OperationBuilderTest : StringSpec({ ?.also { param -> param.`in` shouldBe "query" param.description shouldBe null - param.required shouldBe null - param.deprecated shouldBe null - param.allowEmptyValue shouldBe null + param.required shouldBe false + param.deprecated shouldBe false + param.allowEmptyValue shouldBe true param.`$ref` shouldBe null param.style shouldBe null - param.explode shouldBe null - param.allowReserved shouldBe null + param.explode shouldBe false + param.allowReserved shouldBe true param.schema .also { it.shouldNotBeNull() } ?.also { it.type = "string" } @@ -249,13 +252,13 @@ class OperationBuilderTest : StringSpec({ ?.also { param -> param.`in` shouldBe "path" param.description shouldBe null - param.required shouldBe null - param.deprecated shouldBe null - param.allowEmptyValue shouldBe null + param.required shouldBe false + param.deprecated shouldBe false + param.allowEmptyValue shouldBe true param.`$ref` shouldBe null param.style shouldBe null - param.explode shouldBe null - param.allowReserved shouldBe null + param.explode shouldBe false + param.allowReserved shouldBe true param.schema .also { it.shouldNotBeNull() } ?.also { it.type = "integer" } @@ -269,13 +272,13 @@ class OperationBuilderTest : StringSpec({ ?.also { param -> param.`in` shouldBe "header" param.description shouldBe null - param.required shouldBe null - param.deprecated shouldBe null - param.allowEmptyValue shouldBe null + param.required shouldBe false + param.deprecated shouldBe false + param.allowEmptyValue shouldBe true param.`$ref` shouldBe null param.style shouldBe null - param.explode shouldBe null - param.allowReserved shouldBe null + param.explode shouldBe false + param.allowReserved shouldBe true param.schema .also { it.shouldNotBeNull() } ?.also { it.type = "boolean" } @@ -293,7 +296,7 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { content -> content shouldHaveSize 1 - content.get("application/json") + content["application/json"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema @@ -310,7 +313,7 @@ class OperationBuilderTest : StringSpec({ } } - body.required shouldBe null + body.required shouldBe false body.extensions shouldBe null body.`$ref` shouldBe null } @@ -330,11 +333,11 @@ class OperationBuilderTest : StringSpec({ route.response { "test" to { description = "Test Response" - header("test-header") - body>() + header("test-header", KTypeDescriptor(typeOf())) {} + body(KTypeDescriptor(typeOf>())) } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -349,7 +352,7 @@ class OperationBuilderTest : StringSpec({ operation.requestBody shouldBe null operation.responses .also { it shouldHaveSize 1 } - .let { it.get("test") } + .let { it["test"] } .also { it.shouldNotBeNull() } ?.also { response -> response.description shouldBe "Test Response" @@ -365,7 +368,7 @@ class OperationBuilderTest : StringSpec({ response.content .also { it.shouldNotBeNull() } - .let { it.get("application/json") } + .let { it["application/json"] } .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema @@ -397,9 +400,9 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.request { - queryParameter("param") { + queryParameter("param", KTypeDescriptor(typeOf())) { description = "test parameter" - example = "MyExample" + example = ValueExampleDescriptor(name = "Example", value = "MyExample") required = true deprecated = true allowEmptyValue = true @@ -407,7 +410,7 @@ class OperationBuilderTest : StringSpec({ allowReserved = true } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -443,18 +446,24 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.request { - body { + body(KTypeDescriptor(typeOf())) { description = "the test body" required = true - mediaType(ContentType.Application.Json) - mediaType(ContentType.Application.Xml) - example("example 1", "MyExample1") { - summary = "the example 1" - description = "the first example" - } + mediaTypes = setOf( + ContentType.Application.Json, + ContentType.Application.Xml + ) + example( + ValueExampleDescriptor( + name = "example 1", + value = "MyExample1", + summary = "the example 1", + description = "the first example" + ) + ) } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -531,20 +540,22 @@ class OperationBuilderTest : StringSpec({ documentation = OpenApiRoute().also { route -> route.request { multipartBody { - mediaType(ContentType.MultiPart.FormData) - part("image") { + mediaTypes = setOf( + ContentType.MultiPart.FormData + ) + part("image", KTypeDescriptor(typeOf())) { mediaTypes = setOf( ContentType.Image.PNG, ContentType.Image.JPEG, ContentType.Image.GIF ) } - part("data") { + part("data", KTypeDescriptor(typeOf())) { mediaTypes = setOf(ContentType.Text.Plain) } } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -557,7 +568,7 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { content -> content shouldHaveSize 1 - content.get("multipart/form-data") + content["multipart/form-data"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema @@ -576,7 +587,7 @@ class OperationBuilderTest : StringSpec({ .also { it shouldHaveSize 2 } .also { it.keys shouldContainExactlyInAnyOrder listOf("image", "data") } .also { encoding -> - encoding.get("image")?.also { image -> + encoding["image"]?.also { image -> image.contentType shouldBe "image/png, image/jpeg, image/gif" image.headers shouldHaveSize 0 image.style shouldBe null @@ -584,7 +595,7 @@ class OperationBuilderTest : StringSpec({ image.allowReserved shouldBe null image.extensions shouldBe null } - encoding.get("data")?.also { data -> + encoding["data"]?.also { data -> data.contentType shouldBe "text/plain" data.headers shouldHaveSize 0 data.style shouldBe null @@ -608,10 +619,12 @@ class OperationBuilderTest : StringSpec({ documentation = OpenApiRoute().also { route -> route.request { multipartBody { - mediaType(ContentType.MultiPart.FormData) + mediaTypes = setOf( + ContentType.MultiPart.FormData + ) } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -624,7 +637,7 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { content -> content shouldHaveSize 1 - content.get("multipart/form-data") + content["multipart/form-data"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema.shouldNotBeNull() @@ -658,7 +671,7 @@ class OperationBuilderTest : StringSpec({ description = "Custom Response" } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -667,16 +680,16 @@ class OperationBuilderTest : StringSpec({ operation.responses .also { it shouldHaveSize 4 } ?.also { responses -> - responses.get("default") + responses["default"] .also { it.shouldNotBeNull() } ?.also { it.description shouldBe "Default Response" } - responses.get("200") + responses["200"] .also { it.shouldNotBeNull() } ?.also { it.description shouldBe "Successful Request" } - responses.get("500") + responses["500"] .also { it.shouldNotBeNull() } ?.also { it.description shouldBe "Failed Request" } - responses.get("Custom") + responses["Custom"] .also { it.shouldNotBeNull() } ?.also { it.description shouldBe "Custom Response" } } @@ -685,8 +698,10 @@ class OperationBuilderTest : StringSpec({ "automatic unauthorized response for protected route" { val config = PluginConfigDsl().also { - it.defaultUnauthorizedResponse { - description = "Default unauthorized Response" + it.security { + defaultUnauthorizedResponse { + description = "Default unauthorized Response" + } } } val route = RouteMeta( @@ -698,7 +713,7 @@ class OperationBuilderTest : StringSpec({ description = "Default Response" } } - }, + }.build(), protected = true ) val schemaContext = schemaContext(listOf(route), config) @@ -707,10 +722,10 @@ class OperationBuilderTest : StringSpec({ operation.responses .also { it shouldHaveSize 2 } ?.also { responses -> - responses.get("401") + responses["401"] .also { it.shouldNotBeNull() } ?.also { it.description shouldBe "Default unauthorized Response" } - responses.get("default") + responses["default"] .also { it.shouldNotBeNull() } ?.also { it.description shouldBe "Default Response" } } @@ -719,8 +734,10 @@ class OperationBuilderTest : StringSpec({ "automatic unauthorized response for unprotected route" { val config = PluginConfigDsl().also { - it.defaultUnauthorizedResponse { - description = "Default unauthorized Response" + it.security { + defaultUnauthorizedResponse { + description = "Default unauthorized Response" + } } } val route = RouteMeta( @@ -732,7 +749,7 @@ class OperationBuilderTest : StringSpec({ description = "Default Response" } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route), config) @@ -741,7 +758,7 @@ class OperationBuilderTest : StringSpec({ operation.responses .also { it shouldHaveSize 1 } ?.also { responses -> - responses.get("default") + responses["default"] .also { it.shouldNotBeNull() } ?.also { it.description shouldBe "Default Response" } } @@ -754,9 +771,9 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.request { - body>() + body(KTypeDescriptor(typeOf>())) {} } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route)) @@ -779,7 +796,8 @@ class OperationBuilderTest : StringSpec({ schema.type shouldBe "array" schema.items.also { item -> item.type shouldBe null - item.`$ref` shouldBe "#/components/schemas/SimpleObject" + item.`$ref` shouldBe "#/components/schemas/io.github.smiley4." + + "ktorswaggerui.builder.OperationBuilderTest.Companion.SimpleObject" } } mediaType.example shouldBe null @@ -790,14 +808,16 @@ class OperationBuilderTest : StringSpec({ } } - body.required shouldBe null + body.required shouldBe false body.extensions shouldBe null body.`$ref` shouldBe null } } - schemaContext.getComponentsSection().also { section -> - section.keys shouldContainExactlyInAnyOrder listOf("SimpleObject") - section["SimpleObject"]?.also { schema -> + schemaContext.getComponentSection().also { section -> + section.keys shouldContainExactlyInAnyOrder listOf( + "io.github.smiley4.ktorswaggerui.builder.OperationBuilderTest.Companion.SimpleObject" + ) + section["io.github.smiley4.ktorswaggerui.builder.OperationBuilderTest.Companion.SimpleObject"]?.also { schema -> schema.type shouldBe "object" schema.properties.keys shouldContainExactlyInAnyOrder listOf("number", "text") } @@ -806,8 +826,8 @@ class OperationBuilderTest : StringSpec({ "custom body schema" { val config = PluginConfigDsl().also { - it.customSchemas { - openApi("myCustomSchema") { + it.schemas { + schema("myCustomSchema", SwaggerTypeDescriptor( Schema().also { schema -> schema.type = "object" schema.properties = mapOf( @@ -816,7 +836,7 @@ class OperationBuilderTest : StringSpec({ } ) } - } + )) } } val route = RouteMeta( @@ -824,9 +844,9 @@ class OperationBuilderTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().also { route -> route.request { - body(custom("myCustomSchema")) + body(RefTypeDescriptor("myCustomSchema")) } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route), config) @@ -854,7 +874,7 @@ class OperationBuilderTest : StringSpec({ } } - body.required shouldBe null + body.required shouldBe false body.extensions shouldBe null body.`$ref` shouldBe null } @@ -863,8 +883,8 @@ class OperationBuilderTest : StringSpec({ "custom multipart-body schema" { val config = PluginConfigDsl().also { - it.customSchemas { - openApi("myCustomSchema") { + it.schemas { + schema("myCustomSchema", SwaggerTypeDescriptor( Schema().also { schema -> schema.type = "object" schema.properties = mapOf( @@ -873,7 +893,7 @@ class OperationBuilderTest : StringSpec({ } ) } - } + )) } } val route = RouteMeta( @@ -882,11 +902,13 @@ class OperationBuilderTest : StringSpec({ documentation = OpenApiRoute().also { route -> route.request { multipartBody { - mediaType(ContentType.MultiPart.FormData) - part("customData", custom("myCustomSchema")) + mediaTypes = setOf( + ContentType.MultiPart.FormData + ) + part("customData", RefTypeDescriptor("myCustomSchema")) {} } } - }, + }.build(), protected = false ) val schemaContext = schemaContext(listOf(route), config) @@ -931,34 +953,23 @@ class OperationBuilderTest : StringSpec({ private val defaultPluginConfig = PluginConfigDsl() - private fun schemaContext( - routes: List, - pluginConfig: PluginConfigDsl = defaultPluginConfig - ): SchemaContext { + + private fun schemaContext(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): SchemaContext { val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) - return SchemaContextBuilder( - config = pluginConfigData, - schemaBuilder = SchemaBuilder( - definitionsField = pluginConfigData.encoding.schemaDefsField, - schemaEncoder = pluginConfigData.encoding.schemaEncoder, - ObjectMapper(), - TypeOverwrites.get() - ) - ).build(routes) + return SchemaContextImpl(pluginConfigData.schemaConfig).also { + it.addGlobal(pluginConfigData.schemaConfig) + it.add(routes) + } } - private fun exampleContext( - routes: List, - pluginConfig: PluginConfigDsl = defaultPluginConfig - ): ExampleContext { - return ExampleContextBuilder( - exampleBuilder = ExampleBuilder( - config = pluginConfig.build(PluginConfigData.DEFAULT) - ) - ).build(routes.toList()) + private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): ExampleContext { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) + return ExampleContextImpl().also { + it.addShared(pluginConfigData.exampleConfig) + it.add(routes) + } } - private fun buildOperationObject( route: RouteMeta, schemaContext: SchemaContext, @@ -991,6 +1002,8 @@ class OperationBuilderTest : StringSpec({ config = pluginConfigData ), securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ).build(route) } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt similarity index 78% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt index 29f69a4..ff5534d 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/PathsBuilderTest.kt @@ -1,13 +1,9 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi +package io.github.smiley4.ktorswaggerui.builder -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext -import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder +import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextImpl import io.github.smiley4.ktorswaggerui.builder.openapi.ContentBuilder -import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ExternalDocumentationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.HeaderBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.OperationTagsBuilder @@ -18,11 +14,13 @@ import io.github.smiley4.ktorswaggerui.builder.openapi.RequestBodyBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.ResponseBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.ResponsesBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites +import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextImpl +import io.github.smiley4.ktorswaggerui.data.PluginConfigData +import io.github.smiley4.ktorswaggerui.dsl.config.PluginConfigDsl +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiRoute import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldHaveSize @@ -38,8 +36,8 @@ class PathsBuilderTest : StringSpec({ route(HttpMethod.Delete, "/test/path"), route(HttpMethod.Post, "/other/test/route") ) - val schemaContext = schemaContext(routes) - val exampleContext = exampleContext(routes) + val schemaContext = schemaContext(routes, defaultPluginConfig) + val exampleContext = exampleContext(routes, defaultPluginConfig) buildPathsObject(routes, schemaContext, exampleContext).also { paths -> paths shouldHaveSize 3 paths.keys shouldContainExactlyInAnyOrder listOf( @@ -52,13 +50,9 @@ class PathsBuilderTest : StringSpec({ paths["/other/test/route"]!!.post.shouldNotBeNull() } } + "merge paths" { - val config = defaultPluginConfig.also { - it.swagger { - swaggerUrl = "swagger-ui" - forwardRoot = true - } - } + val config = defaultPluginConfig val routes = listOf( route(HttpMethod.Get, "/different/path"), route(HttpMethod.Get, "/test/path"), @@ -85,31 +79,26 @@ class PathsBuilderTest : StringSpec({ private fun route(method: HttpMethod, url: String) = RouteMeta( path = url, method = method, - documentation = OpenApiRoute(), + documentation = OpenApiRoute().build(), protected = false ) private val defaultPluginConfig = PluginConfigDsl() - private fun schemaContext(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): SchemaContext { + private fun schemaContext(routes: List, pluginConfig: PluginConfigDsl): SchemaContext { val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) - return SchemaContextBuilder( - config = pluginConfigData, - schemaBuilder = SchemaBuilder( - definitionsField = pluginConfigData.encoding.schemaDefsField, - schemaEncoder = pluginConfigData.encoding.schemaEncoder, - ObjectMapper(), - TypeOverwrites.get() - ) - ).build(routes) + return SchemaContextImpl(pluginConfigData.schemaConfig).also { + it.addGlobal(pluginConfigData.schemaConfig) + it.add(routes) + } } - private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl = defaultPluginConfig): ExampleContext { - return ExampleContextBuilder( - exampleBuilder = ExampleBuilder( - config = pluginConfig.build(PluginConfigData.DEFAULT) - ) - ).build(routes.toList()) + private fun exampleContext(routes: List, pluginConfig: PluginConfigDsl): ExampleContext { + val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) + return ExampleContextImpl().also { + it.addShared(pluginConfigData.exampleConfig) + it.add(routes) + } } private fun buildPathsObject( @@ -146,6 +135,8 @@ class PathsBuilderTest : StringSpec({ config = pluginConfigData ), securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData), + externalDocumentationBuilder = ExternalDocumentationBuilder(), + serverBuilder = ServerBuilder() ) ) ).build(routes) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/SecuritySchemesBuilderTest.kt similarity index 98% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/SecuritySchemesBuilderTest.kt index 14a47ef..9484aa3 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/SecuritySchemesBuilderTest.kt @@ -1,12 +1,12 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi +package io.github.smiley4.ktorswaggerui.builder import io.github.smiley4.ktorswaggerui.data.AuthKeyLocation import io.github.smiley4.ktorswaggerui.data.AuthScheme import io.github.smiley4.ktorswaggerui.data.AuthType import io.github.smiley4.ktorswaggerui.data.SecuritySchemeData -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme import io.github.smiley4.ktorswaggerui.builder.openapi.OAuthFlowsBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.SecuritySchemesBuilder +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiSecurityScheme import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldBeEmpty diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt new file mode 100644 index 0000000..afa7dec --- /dev/null +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/ServersBuilderTest.kt @@ -0,0 +1,65 @@ +package io.github.smiley4.ktorswaggerui.builder + +import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder +import io.github.smiley4.ktorswaggerui.data.ServerData +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiServer +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.swagger.v3.oas.models.servers.Server + +class ServersBuilderTest : StringSpec({ + + "default server object" { + buildServerObject {}.also { server -> + server.url shouldBe "/" + server.description shouldBe null + server.variables shouldBe null + server.extensions shouldBe null + } + } + + "complete server object" { + buildServerObject { + url = "Test URL" + description = "Test Description" + variable("version") { + description = "the version of the api" + default = "2" + enum = setOf("1", "2", "3") + } + variable("region") { + description = "the region of the api" + default = "somewhere" + enum = setOf("somewhere", "else") + } + }.also { server -> + server.url shouldBe "Test URL" + server.description shouldBe "Test Description" + server.variables.keys shouldContainExactlyInAnyOrder listOf("version", "region") + server.variables["version"]!!.also { variable -> + variable.description shouldBe "the version of the api" + variable.default shouldBe "2" + variable.enum shouldContainExactlyInAnyOrder listOf("1", "2", "3") + } + server.variables["region"]!!.also { variable -> + variable.description shouldBe "the region of the api" + variable.default shouldBe "somewhere" + variable.enum shouldContainExactlyInAnyOrder listOf("somewhere", "else") + } + server.extensions shouldBe null + } + } + +}) { + + companion object { + + private fun buildServerObject(builder: OpenApiServer.() -> Unit): Server { + return ServerBuilder().build(OpenApiServer().apply(builder).build(ServerData.DEFAULT)) + } + + } + +} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/TagsBuilderTest.kt similarity index 94% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/TagsBuilderTest.kt index 54123f8..0c1e537 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/TagsBuilderTest.kt @@ -1,9 +1,9 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi +package io.github.smiley4.ktorswaggerui.builder -import io.github.smiley4.ktorswaggerui.data.TagData -import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag import io.github.smiley4.ktorswaggerui.builder.openapi.TagBuilder import io.github.smiley4.ktorswaggerui.builder.openapi.TagExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.data.TagData +import io.github.smiley4.ktorswaggerui.dsl.config.OpenApiTag import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/misc/RouteDocumentationMergerTest.kt similarity index 81% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt rename to ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/misc/RouteDocumentationMergerTest.kt index d4c8124..5f9095c 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/misc/RouteDocumentationMergerTest.kt @@ -1,12 +1,14 @@ -package io.github.smiley4.ktorswaggerui.tests.route +package io.github.smiley4.ktorswaggerui.misc -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.builder.route.RouteDocumentationMerger +import io.github.smiley4.ktorswaggerui.data.KTypeDescriptor +import io.github.smiley4.ktorswaggerui.dsl.routes.OpenApiRoute import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import kotlin.reflect.typeOf class RouteDocumentationMergerTest : StringSpec({ @@ -21,11 +23,10 @@ class RouteDocumentationMergerTest : StringSpec({ route.operationId shouldBe null route.deprecated shouldBe false route.hidden shouldBe false - route.securitySchemeName shouldBe null route.securitySchemeNames.shouldBeEmpty() route.protected shouldBe null route.getRequest().also { requests -> - requests.getParameters().shouldBeEmpty() + requests.parameters.shouldBeEmpty() requests.getBody() shouldBe null } route.getResponses().also { responses -> @@ -42,16 +43,15 @@ class RouteDocumentationMergerTest : StringSpec({ summary = "Summary A" description = "Description A" operationId = "operationA" - securitySchemeName = "securitySchemeNameA" securitySchemeNames = listOf("securitySchemeNameA1", "securitySchemeNameA2") deprecated = true hidden = false protected = true request { - queryParameter("query") - pathParameter("pathA1") - pathParameter("pathA2") - body { + queryParameter("query", KTypeDescriptor(typeOf())) {} + pathParameter("pathA1", KTypeDescriptor(typeOf())) {} + pathParameter("pathA2", KTypeDescriptor(typeOf())) {} + body(KTypeDescriptor(typeOf())) { description = "body A" } } @@ -66,16 +66,15 @@ class RouteDocumentationMergerTest : StringSpec({ summary = "Summary B" description = "Description B" operationId = "operationB" - securitySchemeName = "securitySchemeNameB" securitySchemeNames = listOf("securitySchemeNameB1", "securitySchemeNameB2") deprecated = false hidden = true protected = false request { - queryParameter("query") - pathParameter("pathB1") - pathParameter("pathB2") - body { + queryParameter("query", KTypeDescriptor(typeOf())) {} + pathParameter("pathB1", KTypeDescriptor(typeOf())) {} + pathParameter("pathB2", KTypeDescriptor(typeOf())) {} + body(KTypeDescriptor(typeOf())) { description = "body B" } } @@ -92,7 +91,6 @@ class RouteDocumentationMergerTest : StringSpec({ route.operationId shouldBe "operationA" route.deprecated shouldBe true route.hidden shouldBe true - route.securitySchemeName shouldBe "securitySchemeNameA" route.securitySchemeNames shouldContainExactlyInAnyOrder listOf( "securitySchemeNameA1", "securitySchemeNameA2", @@ -101,7 +99,7 @@ class RouteDocumentationMergerTest : StringSpec({ ) route.protected shouldBe true route.getRequest().also { requests -> - requests.getParameters().map { it.name } shouldContainExactlyInAnyOrder listOf( + requests.parameters.map { it.name } shouldContainExactlyInAnyOrder listOf( "query", "pathA1", "pathA2", diff --git a/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/misc/RoutingTests.kt b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/misc/RoutingTests.kt new file mode 100644 index 0000000..de02725 --- /dev/null +++ b/ktor-swagger-ui/src/test/kotlin/io/github/smiley4/ktorswaggerui/misc/RoutingTests.kt @@ -0,0 +1,106 @@ +package io.github.smiley4.ktorswaggerui.misc + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.routing.openApiSpec +import io.github.smiley4.ktorswaggerui.routing.swaggerUI +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotBeEmpty +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.application.call +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.route +import io.ktor.server.testing.testApplication +import kotlin.test.Test + +class RoutingTests { + + @Test + fun basicRouting() = swaggerUITestApplication { + get("hello").also { + it.status shouldBe HttpStatusCode.OK + it.body shouldBe "Hello Test" + } + get("/").also { + it.status shouldBe HttpStatusCode.NotFound + } + get("/swagger").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger/index.html").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Text.Html + it.body.shouldNotBeEmpty() + } + get("/swagger/swagger-initializer.js").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.JavaScript + it.body shouldContain "url: \"/api.json\"" + } + get("/api.json").also { + it.status shouldBe HttpStatusCode.OK + it.contentType shouldBe ContentType.Application.Json + it.body.shouldNotBeEmpty() + } + } + + private fun swaggerUITestApplication(block: suspend TestContext.() -> Unit) { + testApplication { + val client = createClient { + this.followRedirects = followRedirects + } + install(SwaggerUI) + routing { + route("api.json") { + openApiSpec() + } + route("swagger") { + swaggerUI("/api.json") + } + get("hello") { + call.respondText("Hello Test") + } + } + TestContext(client).apply { block() } + } + } + + class TestContext(private val client: HttpClient) { + + suspend fun get(path: String): GetResult { + return client.get(path) + .let { + GetResult( + path = path, + status = it.status, + contentType = it.contentType(), + body = it.bodyAsText(), + redirect = it.headers["Location"] + ) + } + .also { it.print() } + } + + + private fun GetResult.print() { + println("GET ${this.path} => ${this.status} (${this.contentType}): ${this.body}") + } + } + + data class GetResult( + val path: String, + val status: HttpStatusCode, + val contentType: ContentType?, + val body: String, + val redirect: String? + ) + +} diff --git a/ktor-swagger-ui/src/test/resources/logback.xml b/ktor-swagger-ui/src/test/resources/logback.xml new file mode 100644 index 0000000..b397c54 --- /dev/null +++ b/ktor-swagger-ui/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) | %highlight(%-5.5level{5}) | %gray(%-16.16thread{16}) | %magenta(%-25.25logger{25}) | %m%n + + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d7fed3a..5a7009c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,11 @@ rootProject.name = "ktor-swagger-ui" + +include("ktor-swagger-ui-examples") +include("ktor-swagger-ui") + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt deleted file mode 100644 index 8c031c3..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContext.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.example - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.swagger.v3.oas.models.examples.Example - -class ExampleContext { - - private val examplesParameters = mutableMapOf() - private val examplesBody = mutableMapOf, Example>() - - fun addExample(parameter: OpenApiRequestParameter, value: String) { - examplesParameters[parameter] = value - } - - fun addExample(body: OpenApiSimpleBody, name: String, value: Example) { - examplesBody[body to name] = value - } - - fun getComponentsSection(): Map = emptyMap() - - fun getExample(parameter: OpenApiRequestParameter): String? = examplesParameters[parameter] - - fun getExample(body: OpenApiSimpleBody, name: String): Example? = examplesBody[body to name] - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextBuilder.kt deleted file mode 100644 index 608eef5..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/example/ExampleContextBuilder.kt +++ /dev/null @@ -1,80 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.example - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.CollectionBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.CustomRefBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.EmptyBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.OneOfBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.SchemaBodyTypeDescriptor -import io.swagger.v3.oas.models.examples.Example - -class ExampleContextBuilder( - private val exampleBuilder: ExampleBuilder -) { - - fun build(routes: Collection): ExampleContext { - return ExampleContext() - .also { ctx -> routes.forEach { handle(ctx, it) } } - } - - - private fun handle(ctx: ExampleContext, route: RouteMeta) { - route.documentation.getRequest().getBody()?.also { handle(ctx, it) } - route.documentation.getRequest().getParameters().forEach { handle(ctx, it) } - route.documentation.getResponses().getResponses().forEach { handle(ctx, it) } - } - - private fun handle(ctx: ExampleContext, response: OpenApiResponse) { - response.getBody()?.also { handle(ctx, it) } - } - - - private fun handle(ctx: ExampleContext, body: OpenApiBaseBody) { - return when (body) { - is OpenApiSimpleBody -> handle(ctx, body) - else -> Unit - } - } - - - private fun handle(ctx: ExampleContext, body: OpenApiSimpleBody) { - body.getExamples().forEach { (name, value) -> - val bodyType = getRelevantSchemaType(body.type, getSchemaType()) - ctx.addExample(body, name, createExample(bodyType, value)) - } - } - - private fun getRelevantSchemaType(typeDescriptor: BodyTypeDescriptor, fallback: SchemaType): SchemaType { - return when(typeDescriptor) { - is EmptyBodyTypeDescriptor -> fallback - is SchemaBodyTypeDescriptor -> typeDescriptor.schemaType - is CollectionBodyTypeDescriptor -> getRelevantSchemaType(typeDescriptor.schemaType, fallback) - is OneOfBodyTypeDescriptor -> fallback - is CustomRefBodyTypeDescriptor -> fallback - } - } - - private fun handle(ctx: ExampleContext, parameter: OpenApiRequestParameter) { - parameter.example?.also { example -> - ctx.addExample(parameter, createExample(parameter.type, example)) - } - } - - private fun createExample(type: SchemaType?, value: Any): String { - return exampleBuilder.buildExampleValue(type, value) - } - - private fun createExample(type: SchemaType?, example: OpenApiExample): Example { - return exampleBuilder.build(type, example) - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt deleted file mode 100644 index ee2bd7d..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ContentBuilder.kt +++ /dev/null @@ -1,150 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.openapi - -import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.CollectionBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.CustomRefBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.EmptyBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.OneOfBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartPart -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.SchemaBodyTypeDescriptor -import io.ktor.http.ContentType -import io.swagger.v3.oas.models.media.Content -import io.swagger.v3.oas.models.media.Encoding -import io.swagger.v3.oas.models.media.MediaType -import io.swagger.v3.oas.models.media.Schema -import kotlin.collections.set - -class ContentBuilder( - private val schemaContext: SchemaContext, - private val exampleContext: ExampleContext, - private val headerBuilder: HeaderBuilder -) { - - fun build(body: OpenApiBaseBody): Content = - when (body) { - is OpenApiSimpleBody -> buildSimpleBody(body) - is OpenApiMultipartBody -> buildMultipartBody(body) - } - - private fun buildSimpleBody(body: OpenApiSimpleBody): Content = - Content().also { content -> - buildSimpleMediaTypes(body, getSchema(body)).forEach { (contentType, mediaType) -> - content.addMediaType(contentType.toString(), mediaType) - } - } - - private fun buildMultipartBody(body: OpenApiMultipartBody): Content { - return Content().also { content -> - buildMultipartMediaTypes(body).forEach { (contentType, mediaType) -> - content.addMediaType(contentType.toString(), mediaType) - } - } - } - - private fun buildSimpleMediaTypes(body: OpenApiSimpleBody, schema: Schema<*>?): Map { - val mediaTypes = body.getMediaTypes().ifEmpty { schema?.let { setOf(chooseMediaType(schema)) } ?: setOf() } - return mediaTypes.associateWith { buildSimpleMediaType(schema, body) } - } - - private fun buildSimpleMediaType(schema: Schema<*>?, body: OpenApiSimpleBody): MediaType { - return MediaType().also { - it.schema = schema - body.getExamples().forEach { (name, _) -> - exampleContext.getExample(body, name) - ?.also { example -> it.addExamples(name, example) } - } - } - } - - private fun buildMultipartMediaTypes(body: OpenApiMultipartBody): Map { - val mediaTypes = body.getMediaTypes().ifEmpty { setOf(ContentType.MultiPart.FormData) } - return mediaTypes.associateWith { buildMultipartMediaType(body) } - } - - private fun buildMultipartMediaType(body: OpenApiMultipartBody): MediaType { - return MediaType().also { mediaType -> - mediaType.schema = Schema().also { schema -> - schema.type = "object" - schema.properties = mutableMapOf?>().also { props -> - body.getParts().forEach { part -> - getSchema(part)?.also { - props[part.name] = getSchema(part) - } - } - } - } - mediaType.encoding = buildMultipartEncoding(body) - } - } - - private fun buildMultipartEncoding(body: OpenApiMultipartBody): MutableMap? { - return if (body.getParts().flatMap { it.mediaTypes }.isEmpty()) { - null - } else { - mutableMapOf().also { encodings -> - body.getParts() - .filter { it.mediaTypes.isNotEmpty() || it.getHeaders().isNotEmpty() } - .forEach { part -> - encodings[part.name] = Encoding().apply { - contentType = part.mediaTypes.joinToString(", ") { it.toString() } - headers = part.getHeaders().mapValues { headerBuilder.build(it.value) } - } - } - } - } - } - - private fun getSchema(body: OpenApiSimpleBody): Schema<*>? { - return getSchema(body.type) - } - - private fun getSchema(part: OpenApiMultipartPart): Schema<*>? { - return getSchema(part.type) - } - - private fun getSchema(typeDescriptor: BodyTypeDescriptor): Schema<*>? { - return when (typeDescriptor) { - is EmptyBodyTypeDescriptor -> { - null - } - is SchemaBodyTypeDescriptor -> { - schemaContext.getSchema(typeDescriptor.schemaType) - } - is OneOfBodyTypeDescriptor -> { - Schema().also { schema -> - typeDescriptor.elements.forEach { - schema.addOneOfItem(getSchema(it)) - } - } - } - is CollectionBodyTypeDescriptor -> { - Schema().also { schema -> - schema.type = "array" - schema.items = getSchema(typeDescriptor.schemaType) - } - } - is CustomRefBodyTypeDescriptor -> { - schemaContext.getSchema(typeDescriptor.customSchemaId) - } - } - } - - private fun chooseMediaType(schema: Schema<*>): ContentType { - return when (schema.type) { - "integer" -> ContentType.Text.Plain - "number" -> ContentType.Text.Plain - "boolean" -> ContentType.Text.Plain - "string" -> ContentType.Text.Plain - "object" -> ContentType.Application.Json - "array" -> ContentType.Application.Json - null -> ContentType.Application.Json - else -> ContentType.Text.Plain - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExampleBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExampleBuilder.kt deleted file mode 100644 index 2b452fa..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ExampleBuilder.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.openapi - -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.swagger.v3.oas.models.examples.Example - -class ExampleBuilder( - private val config: PluginConfigData -) { - - fun build(type: SchemaType?, example: OpenApiExample): Example = - Example().also { - it.value = buildExampleValue(type, example.value) - it.summary = example.summary - it.description = example.description - } - - fun buildExampleValue(type: SchemaType?, value: Any): String { - return config.encoding.exampleEncoder(type, value) ?: value.toString() - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt deleted file mode 100644 index 7e59eec..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ResponseBuilder.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.openapi - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.swagger.v3.oas.models.responses.ApiResponse - -class ResponseBuilder( - private val headerBuilder: HeaderBuilder, - private val contentBuilder: ContentBuilder -) { - - fun build(response: OpenApiResponse): Pair = - response.statusCode to ApiResponse().also { - it.description = response.description - it.headers = response.getHeaders().mapValues { header -> headerBuilder.build(header.value) } - response.getBody()?.let { body -> - it.content = contentBuilder.build(body) - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt deleted file mode 100644 index 5878483..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/openapi/ServerBuilder.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.openapi - -import io.github.smiley4.ktorswaggerui.data.ServerData -import io.swagger.v3.oas.models.servers.Server - -class ServerBuilder { - - fun build(server: ServerData): Server = - Server().also { - it.url = server.url - it.description = server.description - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaBuilder.kt deleted file mode 100644 index 5d85e60..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaBuilder.kt +++ /dev/null @@ -1,110 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.schema - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.BooleanNode -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.node.TextNode -import io.github.smiley4.ktorswaggerui.dsl.SchemaEncoder -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.swagger.v3.core.util.Json -import io.swagger.v3.oas.models.media.Schema - - -data class SchemaDefinitions( - val root: Schema<*>, - val definitions: Map> -) - -class SchemaBuilder( - private val definitionsField: String? = null, - private val schemaEncoder: SchemaEncoder, - private val json: ObjectMapper, - private val typeOverwrites: Map, -) { - - fun create(type: SchemaType): SchemaDefinitions { - val jsonSchema = typeOverwrites[type]?.let { json.readTree(it) } ?: createJsonSchema(type) - return create(jsonSchema) - } - - fun create(jsonSchema: String): SchemaDefinitions { - return create(json.readTree(jsonSchema)) - } - - fun create(jsonSchema: JsonNode): SchemaDefinitions { - normalizeRefs(jsonSchema) { normalizeRef(it) } - val additionalDefinitions = extractAdditionalDefinitions(jsonSchema) - val rootSchema = toOpenApiSchema(jsonSchema) - val additionalSchemas = additionalDefinitions.mapValues { toOpenApiSchema(it.value) } - return SchemaDefinitions( - root = rootSchema, - definitions = additionalSchemas - ) - } - - private fun createJsonSchema(type: SchemaType): JsonNode { - val str = schemaEncoder(type) - return json.readTree(str) - } - - private fun normalizeRefs(jsonSchema: JsonNode, normalizer: (ref: String) -> String) { - iterateTree(jsonSchema) { node -> - if (node is ObjectNode) { - node.get("\$ref")?.also { - node.set("\$ref", TextNode(normalizer(it.asText()))) - } - } - } - } - - private fun normalizeRef(ref: String): String { - val prefix = "#/$definitionsField" - return if (ref.startsWith(prefix)) { - ref.replace(prefix, "#/components/schemas") - } else { - ref - } - } - - private fun extractAdditionalDefinitions(schema: JsonNode): Map { - val definitionsPath = definitionsField?.let { "/$definitionsField" } - return if (definitionsPath == null || schema.at(definitionsPath).isMissingNode) { - emptyMap() - } else { - (schema.at(definitionsPath) as ObjectNode) - .fields().asSequence().toList() - .associate { (key, node) -> key to node } - } - } - - private fun toOpenApiSchema(jsonSchema: JsonNode): Schema<*> { - iterateTree(jsonSchema) { node -> - node.get("type")?.also { typeNode -> - if (typeNode is ArrayNode && node is ObjectNode) { - val types = typeNode.asSequence().filterIsInstance().map { it.asText() }.toSet() - node.set("type", TextNode(types.first { it != "null" })) - if (types.contains("null")) { - node.set("nullable", BooleanNode.TRUE) - } - } - } - } - return Json.mapper().readValue(jsonSchema.toString(), Schema::class.java) - } - - - private fun iterateTree(node: JsonNode, consumer: (node: JsonNode) -> Unit) { - consumer(node) - when (node) { - is ObjectNode -> { - node.elements().asSequence().forEach { iterateTree(it, consumer) } - } - is ArrayNode -> { - node.elements().asSequence().forEach { iterateTree(it, consumer) } - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt deleted file mode 100644 index ab0f9a5..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContext.kt +++ /dev/null @@ -1,243 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.schema - -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.dsl.getSimpleArrayElementTypeName -import io.github.smiley4.ktorswaggerui.dsl.getSimpleTypeName -import io.swagger.v3.oas.models.media.Schema -import kotlin.collections.set - - -class SchemaContext { - - companion object { - - private data class SchemaKeyWrapper( - val type: SchemaType, - val schemaId: String, - val isCustom: Boolean - ) { - - companion object { - val PLACEHOLDER_TYPE = getSchemaType() - const val PLACEHOLDER_SCHEMAID = "" - - fun type(type: SchemaType) = SchemaKeyWrapper( - type = type, - schemaId = PLACEHOLDER_SCHEMAID, - isCustom = false - ) - - fun custom(schemaId: String) = SchemaKeyWrapper( - type = PLACEHOLDER_TYPE, - schemaId = schemaId, - isCustom = true - ) - } - } - } - - private val schemas = mutableMapOf() - private val schemasCustom = mutableMapOf() - - private val componentsSection = mutableMapOf>() - private val inlineSchemas = mutableMapOf>() - private val inlineSchemasCustom = mutableMapOf>() - - - fun addSchema(type: SchemaType, schema: SchemaDefinitions) { - schemas[type] = schema - } - - - fun addSchema(customSchemaId: String, schema: SchemaDefinitions) { - schemasCustom[customSchemaId] = schema - } - - - fun getComponentsSection(): Map> = componentsSection - - - fun getSchema(type: SchemaType) = getSchemaOrNull(type) - ?: throw NoSuchElementException("No schema for type '$type'!") - - fun getSchemaOrNull(type: SchemaType) = inlineSchemas[type] - - - fun getSchema(customSchemaId: String) = getSchemaOrNull(customSchemaId) - ?: throw NoSuchElementException("No schema for ref '$customSchemaId'!") - - fun getSchemaOrNull(customSchemaId: String) = inlineSchemasCustom[customSchemaId] - - - fun finalize() { - schemas.forEach { (type, schemaDefinitions) -> - finalize(SchemaKeyWrapper.type(type), schemaDefinitions) - } - schemasCustom.forEach { (schemaId, schemaDefinitions) -> - finalize(SchemaKeyWrapper.custom(schemaId), schemaDefinitions) - } - } - - private fun finalize(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - if (schemaDefinitions.definitions.isEmpty()) { - finalizeOnlyRootDefinition(key, schemaDefinitions) - } - if (schemaDefinitions.definitions.size == 1) { - finalizeOneAdditionalDefinition(key, schemaDefinitions) - } - if (schemaDefinitions.definitions.size > 1) { - finalizeMultipleAdditionalDefinitions(key, schemaDefinitions) - } - } - - private fun finalizeOnlyRootDefinition(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - val root = schemaDefinitions.root - if (root.isPrimitive() || root.isPrimitiveArray()) { - inlineRoot(key, schemaDefinitions) - } else if (root.isObjectArray()) { - unwrapRootArray(key, schemaDefinitions) - } else { - createInlineReference(key, schemaDefinitions) - } - } - - private fun finalizeOneAdditionalDefinition(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - val root = schemaDefinitions.root - val definition = schemaDefinitions.definitions.entries.first().value - if (root.isReference() && (definition.isPrimitive() || definition.isPrimitiveArray())) { - inlineSingleDefinition(key, schemaDefinitions) - } else if (root.isReference() || root.isReferenceArray()) { - inlineRoot(key, schemaDefinitions) - } else if (root.isObjectArray()) { - unwrapRootArray(key, schemaDefinitions) - } else { - createInlineReference(key, schemaDefinitions) - } - } - - private fun finalizeMultipleAdditionalDefinitions(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - val root = schemaDefinitions.root - if (root.isReference() || root.isReferenceArray()) { - inlineRoot(key, schemaDefinitions) - } else if (root.isObjectArray()) { - unwrapRootArray(key, schemaDefinitions) - } else { - createInlineReference(key, schemaDefinitions) - } - } - - - private fun inlineRoot(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - /* - - root-schema: inline - - definitions: in components section - */ - addInline(key, schemaDefinitions.root) - schemaDefinitions.definitions.forEach { (name, schema) -> - addToComponentsSection(name, schema) - } - } - - private fun inlineSingleDefinition(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - /* - - assumption: size(definitions) == 1 - - root-schema: discard - - definition: inline - */ - if (schemaDefinitions.definitions.size != 1) { - throw IllegalArgumentException("Unexpected amount of additional schema-definitions: ${schemaDefinitions.definitions.size}") - } - schemaDefinitions.definitions.entries.first() - .also { addInline(key, it.value) } - } - - private fun createInlineReference(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - /* - - root-schema: in components section - - definitions: in components section - - create inline ref to root - */ - schemaDefinitions.definitions.forEach { (name, schema) -> - addToComponentsSection(name, schema) - } - val rootName = schemaName(key) - addToComponentsSection(rootName, schemaDefinitions.root) - addInline(key, Schema().also { - it.`$ref` = "#/components/schemas/$rootName" - }) - } - - private fun unwrapRootArray(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { - /* - - assumption: root schema.type == array - - root-schema: unwrap - - item -> component section - - create inline array-ref to item - - definitions: in components section - */ - if (schemaDefinitions.root.items == null) { - throw IllegalArgumentException("Expected items for array-schema but items were 'null'.") - } - schemaDefinitions.definitions.forEach { (name, schema) -> - addToComponentsSection(name, schema) - } - val rootName = getWrappedSchemaName(key) - addToComponentsSection(rootName, schemaDefinitions.root.items) - addInline(key, Schema().also { array -> - array.type = "array" - array.items = Schema().also { item -> - item.`$ref` = "#/components/schemas/$rootName" - } - }) - } - - private fun schemaName(key: SchemaKeyWrapper): String { - return if (key.isCustom) { - key.schemaId - } else { - key.type.getSimpleTypeName() - } - } - - private fun getWrappedSchemaName(key: SchemaKeyWrapper): String { - return if (key.isCustom) { - key.schemaId - } else { - key.type.getSimpleArrayElementTypeName() - } - } - - private fun addToComponentsSection(name: String, schema: Schema<*>) { - componentsSection[name] = schema - } - - private fun addInline(key: SchemaKeyWrapper, schema: Schema<*>) { - if (key.isCustom) { - inlineSchemasCustom[key.schemaId] = schema - } else { - inlineSchemas[key.type] = schema - } - } - - private fun Schema<*>.isPrimitive(): Boolean { - return type != "object" && type != "array" && type != null - } - - private fun Schema<*>.isPrimitiveArray(): Boolean { - return type == "array" && (items.isPrimitive() || items.isPrimitiveArray()) - } - - private fun Schema<*>.isObjectArray(): Boolean { - return type == "array" && !items.isPrimitive() && !items.isPrimitiveArray() - } - - private fun Schema<*>.isReference(): Boolean { - return type == null && `$ref` != null - } - - private fun Schema<*>.isReferenceArray(): Boolean { - return type == "array" && items.isReference() - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextBuilder.kt deleted file mode 100644 index 3dbea96..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaContextBuilder.kt +++ /dev/null @@ -1,143 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.schema - -import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.data.BaseCustomSchema -import io.github.smiley4.ktorswaggerui.data.CustomJsonSchema -import io.github.smiley4.ktorswaggerui.data.CustomOpenApiSchema -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.data.RemoteSchema -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.CollectionBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.CustomRefBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.EmptyBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.OneOfBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.SchemaBodyTypeDescriptor -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.swagger.v3.oas.models.media.Schema - -class SchemaContextBuilder( - private val config: PluginConfigData, - private val schemaBuilder: SchemaBuilder -) { - - fun build(routes: Collection): SchemaContext { - return SchemaContext() - .also { ctx -> routes.forEach { handle(ctx, it) } } - .also { ctx -> - if (config.includeAllCustomSchemas) { - config.customSchemas.forEach { (id, schema) -> - ctx.addSchema(id, createSchema(schema)) - } - } - } - .also { ctx -> ctx.finalize() } - } - - - private fun handle(ctx: SchemaContext, route: RouteMeta) { - route.documentation.getRequest().getBody()?.also { handle(ctx, it) } - route.documentation.getRequest().getParameters().forEach { handle(ctx, it) } - route.documentation.getResponses().getResponses().forEach { handle(ctx, it) } - } - - - private fun handle(ctx: SchemaContext, response: OpenApiResponse) { - response.getHeaders().forEach { (_, header) -> - header.type?.also { headerType -> - ctx.addSchema(headerType, createSchema(headerType)) - } - } - response.getBody()?.also { handle(ctx, it) } - } - - - private fun handle(ctx: SchemaContext, body: OpenApiBaseBody) { - return when (body) { - is OpenApiSimpleBody -> handle(ctx, body) - is OpenApiMultipartBody -> handle(ctx, body) - } - } - - - private fun handle(ctx: SchemaContext, body: OpenApiSimpleBody) { - addSchemas(ctx, body.type) - } - - private fun handle(ctx: SchemaContext, body: OpenApiMultipartBody) { - body.getParts().forEach { part -> - part.type.also { addSchemas(ctx, part.type) } - } - } - - private fun addSchemas(ctx: SchemaContext, typeDescriptor: BodyTypeDescriptor) { - when (typeDescriptor) { - is EmptyBodyTypeDescriptor -> Unit - is SchemaBodyTypeDescriptor -> { - ctx.addSchema(typeDescriptor.schemaType, createSchema(typeDescriptor.schemaType)) - } - is CollectionBodyTypeDescriptor -> { - addSchemas(ctx, typeDescriptor.schemaType) - } - is OneOfBodyTypeDescriptor -> { - typeDescriptor.elements.forEach { addSchemas(ctx, it) } - } - is CustomRefBodyTypeDescriptor -> { - ctx.addSchema(typeDescriptor.customSchemaId, createSchema(typeDescriptor.customSchemaId)) - } - } - } - - - private fun handle(ctx: SchemaContext, parameter: OpenApiRequestParameter) { - ctx.addSchema(parameter.type, createSchema(parameter.type)) - } - - - private fun createSchema(type: SchemaType): SchemaDefinitions { - return schemaBuilder.create(type) - } - - - private fun createSchema(customSchemaId: String): SchemaDefinitions { - val customSchema = config.customSchemas[customSchemaId] - return if (customSchema == null) { - SchemaDefinitions( - root = Schema(), - definitions = emptyMap() - ) - } else { - createSchema(customSchema) - } - } - - - private fun createSchema(customSchema: BaseCustomSchema): SchemaDefinitions { - return when (customSchema) { - is CustomJsonSchema -> { - schemaBuilder.create(customSchema.provider()) - } - is CustomOpenApiSchema -> { - SchemaDefinitions( - // provided schema should not have a 'definitions'-section, i.e. schema should be inline-able as is. - root = customSchema.provider(), - definitions = emptyMap() - ) - } - is RemoteSchema -> { - SchemaDefinitions( - root = Schema().apply { - type = "object" - `$ref` = customSchema.url - }, - definitions = emptyMap() - ) - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaTypeAttributeOverride.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaTypeAttributeOverride.kt deleted file mode 100644 index f160b36..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/SchemaTypeAttributeOverride.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.schema - -import com.fasterxml.jackson.databind.node.ObjectNode -import com.github.victools.jsonschema.generator.FieldScope -import com.github.victools.jsonschema.generator.SchemaGenerationContext -import com.github.victools.jsonschema.generator.TypeAttributeOverrideV2 -import com.github.victools.jsonschema.generator.TypeScope -import io.github.smiley4.ktorswaggerui.dsl.Example -import io.swagger.v3.oas.annotations.media.Schema - -/** - * Customizes the generates json-schema by adding fields from annotations ([Schema], [Example]) - */ -class SchemaTypeAttributeOverride : TypeAttributeOverrideV2 { - - override fun overrideTypeAttributes(objectNode: ObjectNode, scope: TypeScope?, context: SchemaGenerationContext?) { - if (scope is FieldScope) { - scope.getAnnotation(Schema::class.java)?.also { annotation -> - if (annotation.example != "") { - objectNode.put("example", annotation.example) - } - } - scope.getAnnotation(Example::class.java)?.also { annotation -> - objectNode.put("example", annotation.value) - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/TypeOverwrites.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/TypeOverwrites.kt deleted file mode 100644 index c74646f..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/builder/schema/TypeOverwrites.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.smiley4.ktorswaggerui.builder.schema - -import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import java.io.File - -object TypeOverwrites { - - /** - * overwrite the schemas of the given types with the given custom json-schemas instead of generating them. - */ - val entries = mutableMapOf( - getSchemaType() to """{"type":"string", "format":"binary"}""", - ) - - - /** - * @return the type-overwrite-[entries] - */ - fun get() = entries - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/BaseCustomSchema.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/BaseCustomSchema.kt deleted file mode 100644 index 5be9f7f..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/BaseCustomSchema.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.smiley4.ktorswaggerui.data - -import io.swagger.v3.oas.models.media.Schema - -sealed class BaseCustomSchema - -class CustomJsonSchema(val provider: () -> String) : BaseCustomSchema() - -class CustomOpenApiSchema(val provider: () -> Schema) : BaseCustomSchema() - -class RemoteSchema(val url: String) : BaseCustomSchema() diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/EncodingData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/EncodingData.kt deleted file mode 100644 index 9ee925d..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/EncodingData.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.github.smiley4.ktorswaggerui.data - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.module.jackson.JacksonModule -import com.github.victools.jsonschema.module.swagger2.Swagger2Module -import io.github.smiley4.ktorswaggerui.dsl.ExampleEncoder -import io.github.smiley4.ktorswaggerui.dsl.SchemaEncoder -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaTypeAttributeOverride -import kotlin.reflect.jvm.javaType - -data class EncodingData( - val exampleEncoder: ExampleEncoder, - val schemaEncoder: SchemaEncoder, - val schemaDefsField: String -) { - - companion object { - val DEFAULT = EncodingData( - exampleEncoder = defaultExampleEncoder(), - schemaEncoder = defaultSchemaEncoder(), - schemaDefsField = "\$defs" - ) - - - /** - * The default jackson object mapper used for encoding examples to json. - */ - var DEFAULT_EXAMPLE_OBJECT_MAPPER = jacksonObjectMapper() - - - /** - * The default [SchemaGenerator] used to encode types to json-schema. - * See https://victools.github.io/jsonschema-generator/#generator-options for more information. - */ - var DEFAULT_SCHEMA_GENERATOR = SchemaGenerator(schemaGeneratorConfigBuilder().build()) - - - /** - * The default [ExampleEncoder] - */ - fun defaultExampleEncoder(): ExampleEncoder { - return { _, value -> encodeExample(value) } - } - - - /** - * encode the given value to a json string - */ - fun encodeExample(value: Any?): String { - return if (value is String) { - value - } else { - DEFAULT_EXAMPLE_OBJECT_MAPPER.writeValueAsString(value) - } - } - - - /** - * The default [SchemaEncoder] - */ - fun defaultSchemaEncoder(): SchemaEncoder { - return { type -> encodeSchema(type) } - } - - - /** - * encode the given type to a json-schema - */ - fun encodeSchema(type: SchemaType): String { - return DEFAULT_SCHEMA_GENERATOR.generateSchema(type.javaType).toPrettyString() - } - - - /** - * The default [SchemaGeneratorConfigBuilder] - */ - fun schemaGeneratorConfigBuilder(): SchemaGeneratorConfigBuilder = - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(Swagger2Module()) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.INLINE_NULLABLE_SCHEMAS) - .with(JacksonModule()) - .without(Option.INLINE_ALL_SCHEMAS) - .also { - it.forTypesInGeneral() - .withTypeAttributeOverride(SchemaTypeAttributeOverride()) - } - - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt deleted file mode 100644 index 7e07e7c..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/data/PluginConfigData.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.github.smiley4.ktorswaggerui.data - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl -import kotlin.reflect.KClass - -data class PluginConfigData( - val defaultUnauthorizedResponse: OpenApiResponse?, - val defaultSecuritySchemeNames: Set, - val tagGenerator: TagGenerator, - val specAssigner: SpecAssigner, - val pathFilter: PathFilter, - val ignoredRouteSelectors: Set>, - val swaggerUI: SwaggerUIData, - val info: InfoData, - val servers: List, - val externalDocs: ExternalDocsData, - val securitySchemes: List, - val tags: List, - val customSchemas: Map, - val includeAllCustomSchemas: Boolean, - val encoding: EncodingData, - val specConfigs: MutableMap, - val whenBuildOpenApiSpecs: WhenBuildOpenApiSpecs? -) { - - companion object { - val DEFAULT = PluginConfigData( - defaultUnauthorizedResponse = null, - defaultSecuritySchemeNames = emptySet(), - tagGenerator = { emptyList() }, - specAssigner = { _, _ -> PluginConfigDsl.DEFAULT_SPEC_ID }, - pathFilter = { _, _ -> true }, - ignoredRouteSelectors = emptySet(), - swaggerUI = SwaggerUIData.DEFAULT, - info = InfoData.DEFAULT, - servers = emptyList(), - externalDocs = ExternalDocsData.DEFAULT, - securitySchemes = emptyList(), - tags = emptyList(), - customSchemas = emptyMap(), - includeAllCustomSchemas = false, - encoding = EncodingData.DEFAULT, - specConfigs = mutableMapOf(), - whenBuildOpenApiSpecs = null - ) - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/BodyTypeDescriptor.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/BodyTypeDescriptor.kt deleted file mode 100644 index 72d74f8..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/BodyTypeDescriptor.kt +++ /dev/null @@ -1,130 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import kotlin.reflect.KClass - -/** - * Describes the type/schema of a request or response body. - * [BodyTypeDescriptor]s can be nested to build more specific bodies from simple types - */ -sealed interface BodyTypeDescriptor { - - companion object { - - /** - * A [BodyTypeDescriptor] of the specific given type (or empty if type is null). - */ - fun typeOf(type: KClass<*>?) = type?.let { SchemaBodyTypeDescriptor(it.asSchemaType()) } ?: EmptyBodyTypeDescriptor() - - - /** - * A [BodyTypeDescriptor] of the specific given type (or empty if type is null). - */ - fun typeOf(type: SchemaType?) = type?.let { SchemaBodyTypeDescriptor(it) } ?: EmptyBodyTypeDescriptor() - - - /** - * A [BodyTypeDescriptor] of the specific given generic type. - */ - inline fun typeOf() = SchemaBodyTypeDescriptor(getSchemaType()) - - - /** - * Type can be any one of the given types. - */ - fun oneOf(vararg type: KClass<*>) = OneOfBodyTypeDescriptor(type.toList().map { typeOf(it.asSchemaType()) }) - - - /** - * Type can be any one of the given types. - */ - @JvmName("oneOfClass") - fun oneOf(types: Collection>) = OneOfBodyTypeDescriptor(types.map { typeOf(it.asSchemaType()) }) - - - /** - * Type can be any one of the given types. - */ - fun oneOf(vararg type: SchemaType) = OneOfBodyTypeDescriptor(type.map { typeOf(it) }) - - - /** - * Type can be any one of the given types. - */ - @JvmName("oneOfType") - fun oneOf(types: Collection) = OneOfBodyTypeDescriptor(types.map { typeOf(it) }) - - - /** - * Type can be any one of the given types. - */ - fun oneOf(vararg type: BodyTypeDescriptor) = OneOfBodyTypeDescriptor(type.toList()) - - - /** - * Type can be any one of the given types. - */ - @JvmName("oneOfDescriptor") - fun oneOf(types: Collection) = OneOfBodyTypeDescriptor(types.toList()) - - - /** - * Type is an array of the specific given type. - */ - fun multipleOf(type: KClass<*>) = CollectionBodyTypeDescriptor(typeOf(type.asSchemaType())) - - - /** - * Type is an array of the specific given type. - */ - fun multipleOf(type: SchemaType) = CollectionBodyTypeDescriptor(typeOf(type)) - - - /** - * Type is an array of the given type. - */ - fun multipleOf(type: BodyTypeDescriptor) = CollectionBodyTypeDescriptor(type) - - - /** - * A [BodyTypeDescriptor] of the specific given custom schema. - */ - fun custom(customSchemaId: String) = CustomRefBodyTypeDescriptor(customSchemaId) - - - /** - * An empty type. - */ - fun empty() = EmptyBodyTypeDescriptor() - } - -} - - -/** - * Describes an empty type - */ -class EmptyBodyTypeDescriptor : BodyTypeDescriptor - - -/** - * Describes a specific type/schema - */ -class SchemaBodyTypeDescriptor(val schemaType: SchemaType) : BodyTypeDescriptor - - -/** - * Describes any one of the given types - */ -class OneOfBodyTypeDescriptor(val elements: List) : BodyTypeDescriptor - - -/** - * Describes an array of the given type - */ -class CollectionBodyTypeDescriptor(val schemaType: BodyTypeDescriptor) : BodyTypeDescriptor - - -/** - * Describes the custom schema/type with the given id - */ -class CustomRefBodyTypeDescriptor(val customSchemaId: String) : BodyTypeDescriptor diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemaRef.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemaRef.kt deleted file mode 100644 index 417bef8..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemaRef.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -@Deprecated( - "Use BodyTypeDescriptor instead", - ReplaceWith("BodyTypeDescriptor.custom(schemaId)") -) -fun obj(schemaId: String) = BodyTypeDescriptor.custom(schemaId) - - -@Deprecated( - "Use BodyTypeDescriptor instead", - ReplaceWith("BodyTypeDescriptor.multipleOf(BodyTypeDescriptor.custom(schemaId))") -) -fun array(schemaId: String) = BodyTypeDescriptor.multipleOf(BodyTypeDescriptor.custom(schemaId)) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt deleted file mode 100644 index 11f5e8f..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import io.github.smiley4.ktorswaggerui.data.BaseCustomSchema -import io.github.smiley4.ktorswaggerui.data.CustomJsonSchema -import io.github.smiley4.ktorswaggerui.data.CustomOpenApiSchema -import io.github.smiley4.ktorswaggerui.data.RemoteSchema -import io.swagger.v3.oas.models.media.Schema - -@OpenApiDslMarker -class CustomSchemas { - - private val schemas = mutableMapOf() - - fun getSchema(id: String): BaseCustomSchema? = schemas[id] - - fun getSchemas() = schemas - - /** - * Define the json-schema for an object/body with the given id - */ - fun json(id: String, provider: () -> String) { - schemas[id] = CustomJsonSchema(provider) - } - - - /** - * Define the [Schema] for an object/body with the given id - */ - fun openApi(id: String, provider: () -> Schema) { - schemas[id] = CustomOpenApiSchema(provider) - } - - - /** - * Define the external url for an object/body with the given id - */ - fun remote(id: String, url: String) { - schemas[id] = RemoteSchema(url) - } - - - /** - * Whether to include all custom-schemas or only the ones directly used in any route-documentation - */ - var includeAll = false - -} - - diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt deleted file mode 100644 index 1714a22..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeDefault -import io.github.smiley4.ktorswaggerui.data.EncodingData - - -typealias ExampleEncoder = (type: SchemaType?, example: Any) -> String? - -typealias SchemaEncoder = (type: SchemaType) -> String? - -/** - * Configuration for encoding examples, schemas, ... - */ -@OpenApiDslMarker -class EncodingConfig { - - /** - * Encode the given example object into a json-string. - */ - fun exampleEncoder(encoder: ExampleEncoder) { - exampleEncoder = encoder - } - - private var exampleEncoder: ExampleEncoder = EncodingData.DEFAULT.exampleEncoder - - - /** - * Encode the given type into a valid json-schema. - * This encoder does not affect custom-schemas provided in the plugin-config. - */ - fun schemaEncoder(encoder: SchemaEncoder) { - schemaEncoder = encoder - } - - private var schemaEncoder: SchemaEncoder = EncodingData.DEFAULT.schemaEncoder - - - /** - * the name of the field (if it exists) in the json-schema containing schema-definitions. - */ - var schemaDefinitionsField = EncodingData.DEFAULT.schemaDefsField - - - fun build(base: EncodingData) = EncodingData( - exampleEncoder = mergeDefault(base.exampleEncoder, exampleEncoder, EncodingData.DEFAULT.exampleEncoder), - schemaEncoder = mergeDefault(base.schemaEncoder, schemaEncoder, EncodingData.DEFAULT.schemaEncoder), - schemaDefsField = mergeDefault(base.schemaDefsField, schemaDefinitionsField, EncodingData.DEFAULT.schemaDefsField), - ) - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/Example.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/Example.kt deleted file mode 100644 index 1e4541c..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/Example.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -/** - * Annotation to add an example value to the field of an object. - */ -@Target(AnnotationTarget.FIELD) -annotation class Example( - val value: String -) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExample.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExample.kt deleted file mode 100644 index 77667cc..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiExample.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -/** - * Documentation for an example object - */ -@OpenApiDslMarker -class OpenApiExample( - /** - * The actual example object/value - */ - val value: Any -) { - - /** - * A short description of the example - */ - var summary: String? = null - - - /** - * A long description of the example - */ - var description: String? = null - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiHeader.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiHeader.kt deleted file mode 100644 index 2a7472b..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiHeader.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - - - -@OpenApiDslMarker -class OpenApiHeader { - - /** - * A description of the header - */ - var description: String? = null - - - /** - * The schema of the header - */ - var type: SchemaType? = null - - - /** - * Determines whether this header is mandatory - */ - var required: Boolean? = null - - - /** - * Specifies that a header is deprecated and SHOULD be transitioned out of usage - */ - var deprecated: Boolean? = null - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt deleted file mode 100644 index fd5a61d..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import kotlin.reflect.KClass - - -/** - * Describes a single request/response body with multipart content. - * See https://swagger.io/docs/specification/describing-request-body/multipart-requests/ for more info - */ -@OpenApiDslMarker -class OpenApiMultipartBody : OpenApiBaseBody() { - - private val parts = mutableListOf() - - fun getParts(): List = parts - - - /** - * One part of a multipart-body - */ - fun part(name: String, type: BodyTypeDescriptor, block: OpenApiMultipartPart.() -> Unit) { - parts.add(OpenApiMultipartPart(name, type).apply(block)) - } - - - /** - * One part of a multipart-body - */ - fun part(name: String, type: BodyTypeDescriptor) = part(name, type) {} - - - /** - * One part of a multipart-body - */ - fun part(name: String, type: SchemaType, block: OpenApiMultipartPart.() -> Unit) = part(name, BodyTypeDescriptor.typeOf(type), block) - - - /** - * One part of a multipart-body - */ - fun part(name: String, type: KClass<*>) = part(name, type.asSchemaType()) {} - - - /** - * One part of a multipart-body - */ - inline fun part(name: String) = part(name, getSchemaType()) {} - - - /** - * One part of a multipart-body - */ - inline fun part(name: String, noinline block: OpenApiMultipartPart.() -> Unit) = part(name, getSchemaType(), block) - - - /** - * One part of a multipart-body - */ - fun part(name: String, customSchemaId: String, block: OpenApiMultipartPart.() -> Unit) = - part(name, BodyTypeDescriptor.custom(customSchemaId), block) - - - /** - * One part of a multipart-body - */ - fun part(name: String, customSchemaId: String) = part(name, customSchemaId) {} - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt deleted file mode 100644 index 60ae1f7..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt +++ /dev/null @@ -1,61 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import io.ktor.http.ContentType -import kotlin.reflect.KClass - -/** - * Describes one section of a multipart-body. - * See https://swagger.io/docs/specification/describing-request-body/multipart-requests/ for more info - */ -@OpenApiDslMarker -class OpenApiMultipartPart( - /** - * The name of this part - */ - val name: String, - - val type: BodyTypeDescriptor -) { - - /** - * Set a specific content type for this part - */ - var mediaTypes: Collection = setOf() - - private val headers = mutableMapOf() - - fun getHeaders(): Map = headers - - - /** - * Possible headers for this part - */ - fun header(name: String, type: SchemaType, block: OpenApiHeader.() -> Unit) { - headers[name] = OpenApiHeader().apply(block).apply { - this.type = type - } - } - - /** - * Possible headers for this part - */ - fun header(name: String, type: KClass<*>, block: OpenApiHeader.() -> Unit) = header(name, type.asSchemaType(), block) - - /** - * Possible headers for this part - */ - fun header(name: String, type: KClass<*>) = header(name, type) {} - - - /** - * Possible headers for this part - */ - inline fun header(name: String) = header(name, getSchemaType()) {} - - - /** - * Possible headers for this part - */ - inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit) = header(name, getSchemaType(), block) - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt deleted file mode 100644 index 0f851e7..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt +++ /dev/null @@ -1,188 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter.Location -import kotlin.reflect.KClass - - -@OpenApiDslMarker -class OpenApiRequest { - - /** - * A list of parameters that are applicable for this operation - */ - private val parameters = mutableListOf() - - fun getParameters(): List = parameters - - - /** - * A path parameters that is applicable for this operation - */ - fun parameter(location: Location, name: String, type: SchemaType, block: OpenApiRequestParameter.() -> Unit) { - parameters.add(OpenApiRequestParameter(name, type, location).apply(block)) - } - - - /** - * A path parameters that is applicable for this operation - */ - fun pathParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = - parameter(Location.PATH, name, type.asSchemaType(), block) - - - /** - * A path parameters that is applicable for this operation - */ - fun pathParameter(name: String, type: KClass<*>) = pathParameter(name, type) {} - - - /** - * A path parameters that is applicable for this operation - */ - inline fun pathParameter(name: String) = - parameter(Location.PATH, name, getSchemaType()) {} - - - /** - * A path parameters that is applicable for this operation - */ - inline fun pathParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit) = - parameter(Location.PATH, name, getSchemaType(), block) - - - /** - * A query parameters that is applicable for this operation - */ - fun queryParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = - parameter(Location.QUERY, name, type.asSchemaType(), block) - - - /** - * A query parameters that is applicable for this operation - */ - fun queryParameter(name: String, type: KClass<*>) = queryParameter(name, type) {} - - - /** - * A query parameters that is applicable for this operation - */ - inline fun queryParameter(name: String) = - parameter(Location.QUERY, name, getSchemaType()) {} - - - /** - * A query parameters that is applicable for this operation - */ - inline fun queryParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit) = - parameter(Location.QUERY, name, getSchemaType(), block) - - - /** - * A header parameters that is applicable for this operation - */ - fun headerParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = - parameter(Location.HEADER, name, type.asSchemaType(), block) - - - /** - * A header parameters that is applicable for this operation - */ - fun headerParameter(name: String, type: SchemaType) = parameter(Location.HEADER, name, type) {} - - - /** - * A header parameters that is applicable for this operation - */ - inline fun headerParameter(name: String) = - parameter(Location.HEADER, name, getSchemaType()) {} - - - /** - * A header parameters that is applicable for this operation - */ - inline fun headerParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit) = - parameter(Location.HEADER, name, getSchemaType(), block) - - - private var body: OpenApiBaseBody? = null - - fun getBody() = body - - - /** - * The body returned with this request - */ - fun body(typeDescriptor: BodyTypeDescriptor, block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(typeDescriptor).apply(block) - } - - /** - * The body returned with this request - */ - fun body(typeDescriptor: BodyTypeDescriptor) = body(typeDescriptor) {} - - /** - * The request body applicable for this operation - */ - fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.typeOf(type), block) - - - /** - * The request body applicable for this operation - */ - fun body(type: KClass<*>) = body(type) {} - - - /** - * The request body applicable for this operation - */ - fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) - - - /** - * The request body applicable for this operation - */ - @JvmName("bodyGenericType") - inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) - - - /** - * The request body applicable for this operation - */ - inline fun body() = body(getSchemaType()) {} - - - /** - * The request body applicable for this operation - */ - fun body(block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.empty(), block) - - - /** - * The body returned with this request - */ - fun body(customSchemaId: String) = body(customSchemaId) {} - - - /** - * The body returned with this request - */ - fun body(customSchemaId: String, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.custom(customSchemaId), block) - - - /** - * The multipart-body returned with this request - */ - fun multipartBody(block: OpenApiMultipartBody.() -> Unit) { - body = OpenApiMultipartBody().apply(block) - } - - - /** - * Set the body of this request. Intended for internal use. - */ - fun setBody(body: OpenApiBaseBody?) { - this.body = body - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequestParameter.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequestParameter.kt deleted file mode 100644 index 9f07798..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequestParameter.kt +++ /dev/null @@ -1,69 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - - -@OpenApiDslMarker -class OpenApiRequestParameter( - /** - * The name (case-sensitive) of the parameter - */ - val name: String, - /** - * The type defining the schema used for the parameter. - */ - val type: SchemaType, - /** - * Location of the parameter - */ - val location: Location -) { - - enum class Location { - QUERY, HEADER, PATH - } - - - /** - * A brief description of the parameter - */ - var description: String? = null - - - /** - * An example value for this parameter - */ - var example: Any? = null - - - /** - * Determines whether this parameter is mandatory - */ - var required: Boolean? = null - - - /** - * Specifies that a parameter is deprecated and SHOULD be transitioned out of usage - */ - var deprecated: Boolean? = null - - - /** - * Sets the ability to pass empty-valued parameters. - * This is valid only for query parameters and allows sending a parameter with an empty value. - */ - var allowEmptyValue: Boolean? = null - - - /** - * When this is true, parameter values of type array or object generate separate parameters for each value of the array or key-value - * pair of the map. For other types of parameters this property has no effect - */ - var explode: Boolean? = null - - - /** - * Determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included - * without percent-encoding. This property only applies to parameters with an in value of query - */ - var allowReserved: Boolean? = null - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt deleted file mode 100644 index 4d9c4b9..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt +++ /dev/null @@ -1,130 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import kotlin.reflect.KClass - -/** - * A container for the expected responses of an operation. The container maps an HTTP response code to the expected response. - * A response code can only have one response object. - */ -@OpenApiDslMarker -class OpenApiResponse(val statusCode: String) { - - /** - * A short description of the response - */ - var description: String? = null - - private val headers = mutableMapOf() - - fun getHeaders(): Map = headers - - - /** - * Possible headers returned with this response - */ - fun header(name: String, type: SchemaType, block: OpenApiHeader.() -> Unit) { - headers[name] = OpenApiHeader().apply(block).apply { - this.type = type - } - } - - - /** - * Possible headers returned with this response - */ - fun header(name: String, type: KClass<*>, block: OpenApiHeader.() -> Unit) = header(name, type.asSchemaType(), block) - - - /** - * Possible headers returned with this response - */ - fun header(name: String, type: KClass<*>) = header(name, type.asSchemaType()) {} - - - /** - * Possible headers returned with this response - */ - inline fun header(name: String) = header(name, getSchemaType()) {} - - - /** - * Possible headers returned with this response - */ - inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit) = header(name, getSchemaType(), block) - - - private var body: OpenApiBaseBody? = null - - fun getBody() = body - - - /** - * The body returned with this response - */ - fun body(type: BodyTypeDescriptor, block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(type).apply(block) - } - - - /** - * The body returned with this response - */ - fun body(type: BodyTypeDescriptor) = body(type) {} - - - /** - * The body returned with this response - */ - fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.typeOf(type), block) - - - /** - * The body returned with this response - */ - fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) - - - /** - * The body returned with this response - */ - @JvmName("bodyGenericType") - inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) - - - /** - * The body returned with this response - */ - fun body(type: KClass<*>) = body(type) {} - - - /** - * The body returned with this response - */ - inline fun body() = body(getSchemaType()) {} - - - /** - * The body returned with this response - */ - fun body(block: OpenApiSimpleBody.() -> Unit) = body(null, block) - - /** - * The body returned with this response - */ - fun body(customSchemaId: String, block: OpenApiSimpleBody.() -> Unit) = body(BodyTypeDescriptor.custom(customSchemaId), block) - - - /** - * The body returned with this response - */ - fun body(customSchemaId: String) = body(customSchemaId) {} - - - /** - * The multipart-body returned with this response - */ - fun multipartBody(block: OpenApiMultipartBody.() -> Unit) { - body = OpenApiMultipartBody().apply(block) - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt deleted file mode 100644 index cd2de62..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - - -/** - * Describes the base of a single request/response body. - */ -@OpenApiDslMarker -class OpenApiSimpleBody( - /** - * The type defining the schema used for the body. - */ - val type: BodyTypeDescriptor, -) : OpenApiBaseBody() { - - /** - * Examples for this body - */ - private val examples = mutableMapOf() - - fun example(name: String, value: Any, block: OpenApiExample.() -> Unit) { - examples[name] = OpenApiExample(value).apply(block) - } - - fun example(name: String, value: Any) = example(name, value) {} - - fun getExamples(): Map = examples - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/PluginConfigDsl.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/PluginConfigDsl.kt deleted file mode 100644 index 02e1d1f..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/PluginConfigDsl.kt +++ /dev/null @@ -1,223 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import io.github.smiley4.ktorswaggerui.data.* -import io.github.smiley4.ktorswaggerui.data.DataUtils.merge -import io.github.smiley4.ktorswaggerui.data.DataUtils.mergeBoolean -import io.ktor.http.* -import io.ktor.server.routing.* -import kotlin.collections.Collection -import kotlin.collections.Set -import kotlin.collections.buildList -import kotlin.collections.buildMap -import kotlin.collections.buildSet -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.forEach -import kotlin.collections.map -import kotlin.collections.mutableListOf -import kotlin.collections.mutableMapOf -import kotlin.collections.set -import kotlin.reflect.KClass - -/** - * Main-Configuration of the "SwaggerUI"-Plugin - */ -@OpenApiDslMarker -class PluginConfigDsl { - - companion object { - const val DEFAULT_SPEC_ID = "api" - } - - - private val specConfigs = mutableMapOf() - - fun spec(specId: String, block: PluginConfigDsl.() -> Unit) { - specConfigs[specId] = PluginConfigDsl().apply(block) - } - - - /** - * Default response to automatically add to each protected route for the "Unauthorized"-Response-Code. - * Generated response can be overwritten with custom response. - */ - fun defaultUnauthorizedResponse(block: OpenApiResponse.() -> Unit) { - defaultUnauthorizedResponse = OpenApiResponse(HttpStatusCode.Unauthorized.value.toString()).apply(block) - } - - private var defaultUnauthorizedResponse: OpenApiResponse? = PluginConfigData.DEFAULT.defaultUnauthorizedResponse - - - /** - * The name of the security scheme to use for the protected paths - */ - var defaultSecuritySchemeName: String? = null - - - /** - * The names of the security schemes available for use for the protected paths - */ - var defaultSecuritySchemeNames: Collection? = PluginConfigData.DEFAULT.defaultSecuritySchemeNames - - - /** - * Automatically add tags to the route with the given url. - * The returned (non-null) tags will be added to the tags specified in the route-specific documentation. - */ - fun generateTags(generator: TagGenerator) { - tagGenerator = generator - } - - private var tagGenerator: TagGenerator? = PluginConfigData.DEFAULT.tagGenerator - - - /** - * Assigns routes without an [io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute.specId] to a specified openapi-spec. - */ - var specAssigner: SpecAssigner? = PluginConfigData.DEFAULT.specAssigner - - - /** - * Filter to apply to all routes. Return 'false' for routes to not include them in the OpenApi-Spec and Swagger-UI. - * The url of the paths are already split at '/'. - */ - var pathFilter: PathFilter? = PluginConfigData.DEFAULT.pathFilter - - - /** - * Swagger-UI configuration - */ - fun swagger(block: SwaggerUIDsl.() -> Unit) { - swaggerUI = SwaggerUIDsl().apply(block) - } - - private var swaggerUI = SwaggerUIDsl() - - - /** - * OpenAPI info configuration - provides metadata about the API - */ - fun info(block: OpenApiInfo.() -> Unit) { - info = OpenApiInfo().apply(block) - } - - private var info = OpenApiInfo() - - - /** - * OpenAPI server configuration - an array of servers, which provide connectivity information to a target server - */ - fun server(block: OpenApiServer.() -> Unit) { - servers.add(OpenApiServer().apply(block)) - } - - private val servers = mutableListOf() - - - /** - * OpenAPI external docs configuration - link and description of an external documentation - */ - fun externalDocs(block: OpenApiExternalDocs.() -> Unit) { - externalDocs = OpenApiExternalDocs().apply(block) - } - - private var externalDocs = OpenApiExternalDocs() - - - /** - * Defines security schemes that can be used by operations - */ - fun securityScheme(name: String, block: OpenApiSecurityScheme.() -> Unit) { - securitySchemes.add(OpenApiSecurityScheme(name).apply(block)) - } - - private val securitySchemes = mutableListOf() - - - /** - * Tags used by the specification with additional metadata. Not all tags that are used must be declared - */ - fun tag(name: String, block: OpenApiTag.() -> Unit) { - tags.add(OpenApiTag(name).apply(block)) - } - - private val tags = mutableListOf() - - - /** - * Custom schemas to reference via [io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef] - */ - fun customSchemas(block: CustomSchemas.() -> Unit) { - this.customSchemas = CustomSchemas().apply(block) - } - - private var customSchemas = CustomSchemas() - - - /** - * customize the behaviour of different encoders (examples, schemas, ...) - */ - fun encoding(block: EncodingConfig.() -> Unit) { - block(encodingConfig) - } - - val encodingConfig: EncodingConfig = EncodingConfig() - - - /** - * List of all [RouteSelector] types in that should be ignored in the resulting url of any route. - */ - var ignoredRouteSelectors: Set> = PluginConfigData.DEFAULT.ignoredRouteSelectors - - - /** - * Invoked after generating the openapi-spec. Can be to e.g. further customize the spec. - */ - var whenBuildOpenApiSpecs: WhenBuildOpenApiSpecs? = null - - - internal fun build(base: PluginConfigData): PluginConfigData { - return PluginConfigData( - defaultUnauthorizedResponse = merge(base.defaultUnauthorizedResponse, defaultUnauthorizedResponse), - defaultSecuritySchemeNames = buildSet { - addAll(base.defaultSecuritySchemeNames) - defaultSecuritySchemeNames?.also { addAll(it) } - defaultSecuritySchemeName?.also { add(it) } - }, - tagGenerator = merge(base.tagGenerator, tagGenerator) ?: PluginConfigData.DEFAULT.tagGenerator, - specAssigner = merge(base.specAssigner, specAssigner) ?: PluginConfigData.DEFAULT.specAssigner, - pathFilter = merge(base.pathFilter, pathFilter) ?: PluginConfigData.DEFAULT.pathFilter, - ignoredRouteSelectors = buildSet { - addAll(base.ignoredRouteSelectors) - addAll(ignoredRouteSelectors) - }, - swaggerUI = swaggerUI.build(base.swaggerUI), - info = info.build(base.info), - servers = buildList { - addAll(base.servers) - addAll(servers.map { it.build(ServerData.DEFAULT) }) - }, - externalDocs = externalDocs.build(base.externalDocs), - securitySchemes = buildList { - addAll(base.securitySchemes) - addAll(securitySchemes.map { it.build(SecuritySchemeData.DEFAULT) }) - }, - tags = buildList { - addAll(base.tags) - addAll(tags.map { it.build(TagData.DEFAULT) }) - }, - customSchemas = buildMap { - putAll(base.customSchemas) - putAll(customSchemas.getSchemas()) - }, - includeAllCustomSchemas = mergeBoolean(base.includeAllCustomSchemas, customSchemas.includeAll), - encoding = encodingConfig.build(base.encoding), - specConfigs = mutableMapOf(), - whenBuildOpenApiSpecs = whenBuildOpenApiSpecs, - ).also { - specConfigs.forEach { (specId, config) -> - it.specConfigs[specId] = config.build(it) - } - } - } -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt deleted file mode 100644 index b52dcc1..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.full.starProjectedType -import kotlin.reflect.typeOf - -typealias SchemaType = KType - - -inline fun getSchemaType(): SchemaType { - return typeOf() -} - -fun SchemaType.getTypeName() = this.toString() - -fun SchemaType.getSimpleTypeName(): String { - val rawName = getTypeName() - return if (rawName.contains("<") || rawName.contains(">")) { - rawName - } else { - (this.classifier as KClass<*>).simpleName ?: rawName - } -} - -fun SchemaType.getSimpleArrayElementTypeName(): String { - if (this.arguments.size != 1) { - throw IllegalArgumentException("Could not determine type of array-elements") - } else { - return this.arguments.first().let { arg -> - arg.type?.getSimpleTypeName() ?: arg.toString() - } - } -} - -fun KClass<*>.asSchemaType() = this.starProjectedType diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ControllerUtils.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ControllerUtils.kt index 715e23b..e69de29 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ControllerUtils.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ControllerUtils.kt @@ -1,20 +0,0 @@ -package io.github.smiley4.ktorswaggerui.routing - -import io.ktor.server.config.* - -object ControllerUtils { - - var appConfig: ApplicationConfig? = null - - fun getRootPath(appConfig: ApplicationConfig): String { - return appConfig.propertyOrNull("ktor.deployment.rootPath")?.getString()?.let { "/${dropSlashes(it)}" } ?: "" - } - - fun dropSlashes(str: String): String { - var value = str - value = if (value.startsWith("/")) value.substring(1) else value - value = if (value.endsWith("/")) value.substring(0, value.length - 1) else value - return value - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ForwardRouteController.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ForwardRouteController.kt deleted file mode 100644 index bcaa16b..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/ForwardRouteController.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.smiley4.ktorswaggerui.routing - -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.config.ApplicationConfig -import io.ktor.server.response.respondRedirect -import io.ktor.server.routing.get -import io.ktor.server.routing.routing - -class ForwardRouteController( - private val appConfig: ApplicationConfig, - private val swaggerUiConfig: PluginConfigData, -) { - - fun setup(app: Application) { - app.routing { - get { - call.respondRedirect("${getRootUrl()}/index.html") - } - } - } - - private fun getRootUrl(): String { - return "/" + listOf( - ControllerUtils.getRootPath(appConfig), - swaggerUiConfig.swaggerUI.rootHostPath, - swaggerUiConfig.swaggerUI.swaggerUrl, - ) - .filter { it.isNotBlank() } - .map { ControllerUtils.dropSlashes(it) } - .joinToString("/") - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/SwaggerController.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/SwaggerController.kt deleted file mode 100644 index bb680ef..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/routing/SwaggerController.kt +++ /dev/null @@ -1,130 +0,0 @@ -package io.github.smiley4.ktorswaggerui.routing - -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call -import io.ktor.server.auth.authenticate -import io.ktor.server.config.ApplicationConfig -import io.ktor.server.request.uri -import io.ktor.server.response.respond -import io.ktor.server.response.respondRedirect -import io.ktor.server.response.respondText -import io.ktor.server.routing.Route -import io.ktor.server.routing.get -import io.ktor.server.routing.route -import io.ktor.server.routing.routing - -class SwaggerController( - private val appConfig: ApplicationConfig, - private val pluginConfig: PluginConfigData, - private val swaggerWebjarVersion: String, - private val specName: String?, - private val jsonSpec: String, -) { - - companion object { - const val DEFAULT_SPEC_NAME: String = "api" - } - - fun setup(app: Application) { - app.routing { - if (pluginConfig.swaggerUI.authentication == null) { - setup() - } else { - authenticate(pluginConfig.swaggerUI.authentication) { - setup() - } - } - } - } - - private fun Route.setup() { - route(getSubUrl()) { - get { - val rootHostPath = if (pluginConfig.swaggerUI.rootHostPath.isNotBlank()) { - ControllerUtils.dropSlashes("/${pluginConfig.swaggerUI.rootHostPath}") - } else "" - call.respondRedirect("$rootHostPath${call.request.uri}/index.html") - } - get("{filename}") { - serveStaticResource(call.parameters["filename"]!!, call) - } - get("swagger-initializer.js") { - serveSwaggerInitializer(call) - } - get("${specName ?: DEFAULT_SPEC_NAME}.json") { - serveOpenApiSpec(call) - } - } - } - - private suspend fun serveSwaggerInitializer(call: ApplicationCall) { - val swaggerUiConfig = pluginConfig.swaggerUI - // see https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md for reference - val propValidatorUrl = swaggerUiConfig.validatorUrl?.let { "validatorUrl: \"$it\"" } ?: "validatorUrl: false" - val propDisplayOperationId = "displayOperationId: ${swaggerUiConfig.displayOperationId}" - val propFilter = "filter: ${swaggerUiConfig.showTagFilterInput}" - val propSort = "operationsSorter: " + - if (swaggerUiConfig.sort == SwaggerUiSort.NONE) "undefined" - else "\"${swaggerUiConfig.sort.value}\"" - val propSyntaxHighlight = "syntaxHighlight: { theme: \"${swaggerUiConfig.syntaxHighlight.value}\" }" - val content = """ - window.onload = function() { - window.ui = SwaggerUIBundle({ - url: "${getRootUrl(appConfig)}/${specName ?: DEFAULT_SPEC_NAME}.json", - dom_id: '#swagger-ui', - deepLinking: true, - presets: [ - SwaggerUIBundle.presets.apis, - SwaggerUIStandalonePreset - ], - plugins: [ - SwaggerUIBundle.plugins.DownloadUrl - ], - layout: "StandaloneLayout", - withCredentials: ${swaggerUiConfig.withCredentials}, - $propValidatorUrl, - $propDisplayOperationId, - $propFilter, - $propSort, - $propSyntaxHighlight - }); - }; - """.trimIndent() - call.respondText(ContentType.Application.JavaScript, HttpStatusCode.OK) { content } - } - - private suspend fun serveOpenApiSpec(call: ApplicationCall) { - call.respondText(ContentType.Application.Json, HttpStatusCode.OK) { jsonSpec } - } - - private suspend fun serveStaticResource(filename: String, call: ApplicationCall) { - val resource = this::class.java.getResource("/META-INF/resources/webjars/swagger-ui/$swaggerWebjarVersion/$filename") - if (resource != null) { - call.respond(ResourceContent(resource)) - } else { - call.respond(HttpStatusCode.NotFound, "$filename could not be found") - } - } - - private fun getRootUrl(appConfig: ApplicationConfig): String { - return "${ControllerUtils.getRootPath(appConfig)}${getSubUrl(true)}" - } - - private fun getSubUrl(withRootHostPath: Boolean = false): String { - return "/" + listOfNotNull( - if (withRootHostPath) pluginConfig.swaggerUI.rootHostPath else null, - pluginConfig.swaggerUI.swaggerUrl, - specName - ) - .filter { it.isNotBlank() } - .map { ControllerUtils.dropSlashes(it) } - .joinToString("/") - } - - -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/AuthExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/AuthExample.kt deleted file mode 100644 index e061db7..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/AuthExample.kt +++ /dev/null @@ -1,120 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.data.AuthScheme -import io.github.smiley4.ktorswaggerui.data.AuthType -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.auth.Authentication -import io.ktor.server.auth.UserIdPrincipal -import io.ktor.server.auth.authenticate -import io.ktor.server.auth.basic -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing -import io.swagger.v3.oas.models.servers.Server - -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -/** - * Example to show how to access protected routes via swagger-ui - * USERNAME = "user" - * PASSWORD = "pass" - */ -private fun Application.myModule() { - - // Install "Authentication"-Plugin and setup Basic-Auth - install(Authentication) { - basic { - realm = "Access to the API" - validate { credentials -> - if (credentials.name == "user" && credentials.password == "pass") { - UserIdPrincipal(credentials.name) - } else { - null - } - } - } - } - - // Install "Swagger-UI"-Plugin - install(SwaggerUI) { - // default value for "401 Unauthorized"-responses. - // the name of the security scheme (see below) to use for each route when nothing else is specified - defaultSecuritySchemeName = "MySecurityScheme" - defaultUnauthorizedResponse { - description = "Username or password is invalid." - } - // specify a security scheme - securityScheme("MySecurityScheme") { - type = AuthType.HTTP - scheme = AuthScheme.BASIC - } - // specify another security scheme - securityScheme("MyOtherSecurityScheme") { - type = AuthType.HTTP - scheme = AuthScheme.BASIC - } - } - - // configure routes - routing { - authenticate { - // route is in an "authenticate"-block -> default security scheme will be used (see plugin-config "defaultSecuritySchemeName") - get("hello", { - // Set the security schemes to be used by this route - securitySchemeNames = setOf("MyOtherSecurityScheme", "MySecurityScheme") - description = "Protected 'Hello World'-Route" - response { - HttpStatusCode.OK to { - description = "Successful Request" - body { description = "the response" } - } - // response for "401 Unauthorized" is automatically added (see plugin-config "defaultUnauthorizedResponse"). - } - }) { - call.respondText("Hello World!") - } - } - // route is not in an "authenticate"-block and does not set the `protected` property -> security schemes will be ignored - get("hello-unprotected", { - // Security scheme will be ignored since the operation is not protected - securitySchemeNames = setOf("MyOtherSecurityScheme", "MySecurityScheme") - description = "Unprotected 'Hello World'-Route" - response { - HttpStatusCode.OK to { - description = "Successful Request" - body { description = "the response" } - } - // no response for "401 Unauthorized" is added - } - }) { - call.respondText("Hello World!") - } - // route is not in an "authenticate"-block but sets the `protected` property - // -> security scheme (or default security scheme) will be used - get("hello-externally-protected", { - // mark the route as protected even though there is no "authenticate"-block - // (e.g. because the route is protected by an external proxy) - protected = true - // Set the security scheme to be used by this route - securitySchemeName = "MyOtherSecurityScheme" - description = "Externally protected 'Hello World'-Route" - response { - HttpStatusCode.OK to { - description = "Successful Request" - body { description = "the response" } - } - // response for "401 Unauthorized" is automatically added (see plugin-config "defaultUnauthorizedResponse"). - } - }) { - call.respondText("Hello World!") - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt deleted file mode 100644 index 94593d7..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt +++ /dev/null @@ -1,288 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import com.fasterxml.jackson.core.util.DefaultIndenter -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter -import com.fasterxml.jackson.databind.SerializationFeature -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.delete -import io.github.smiley4.ktorswaggerui.dsl.get -import io.github.smiley4.ktorswaggerui.dsl.post -import io.github.smiley4.ktorswaggerui.dsl.route -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.serialization.jackson.jackson -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing -import io.swagger.v3.oas.models.Operation -import io.swagger.v3.oas.models.PathItem -import io.swagger.v3.oas.models.responses.ApiResponse -import io.swagger.v3.oas.models.responses.ApiResponses -import java.util.Random - -/** - * Arbitrary examples to show (and test) as many features as possible (for authentication see "AuthExample") - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -data class MathRequest( - val a: Int, - val b: Int -) - -data class MathResult( - val value: Int -) - -enum class Color { - RED, GREEN, BLUE -} - -data class GenericResponse( - val success: Boolean, - val data: T -) - -data class SpecificResponseData( - val text: String -) - -private fun Application.myModule() { - - install(SwaggerUI) { - swagger { - swaggerUrl = "swagger-ui" - forwardRoot = true - } - info { - title = "Example API" - version = "latest" - description = "Example API for testing and demonstration purposes." - } - externalDocs { - url = "https://github.com/SMILEY4/ktor-swagger-ui/wiki" - description = "Sample external documentation object" - } - server { - url = "http://localhost:8080" - description = "Development Server" - } - tag("random") { - description = "Routes that return random things" - } - tag("math") { - description = "Routes for math related operations" - } - generateTags { url -> listOf(url.firstOrNull()) } - whenBuildOpenApiSpecs = { spec -> - spec.paths.addPathItem("customPath", PathItem().also { path -> - path.get = Operation().also { op -> - op.description = "This path was added after generating the openapi-spec" - op.responses = ApiResponses().also { responses -> - responses.addApiResponse("200", ApiResponse()) - responses.addApiResponse("404", ApiResponse()) - } - } - }) - } - } - - install(ContentNegotiation) { - jackson { - configure(SerializationFeature.INDENT_OUTPUT, true) - setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { - indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) - indentObjectsWith(DefaultIndenter(" ", "\n")) - }) - } - } - - routing { - - get("hello", { - tags = listOf("test") - description = "Hello World Endpoint" - operationId = "hello" - response { - default { - description = "Default Response" - } - HttpStatusCode.OK to { - description = "Successful Request" - body { description = "the response" } - } - HttpStatusCode.InternalServerError to { - description = "Something unexpected happened" - } - "Custom" to { - description = "Custom Response" - } - } - }) { - call.respondText("Hello World!") - } - - post("math/{operation}", { - tags = listOf("test") - description = "Performs the given operation on the given values and returns the result" - operationId = "performMathOperation" - request { - pathParameter("operation") { - description = "the math operation to perform. Either 'add' or 'sub'" - example = "add" - } - body { - example("First", MathRequest(13, 19)) { - description = "Either an addition of 13 and 19 or a subtraction of 19 from 13" - } - example("Second", MathRequest(20, 7)) { - description = "Either an addition of 20 and 7 or a subtraction of 7 from 20" - } - } - } - response { - HttpStatusCode.OK to { - description = "The operation was successful" - body { - description = "The result of the operation" - example("First", MathResult(42)) { - summary = "The first example" - description = "For example the result of an addition of 13 and 29" - } - example("Second", MathResult(-13)) { - summary = "The second example" - description = "For example the result of an subtracting 20 from 7" - } - } - } - HttpStatusCode.BadRequest to { - description = "An invalid operation was provided" - } - } - }) { - val operation = call.parameters["operation"]!! - call.receive().let { request -> - when (operation) { - "add" -> call.respond(HttpStatusCode.OK, MathResult(request.a + request.b)) - "sub" -> call.respond(HttpStatusCode.OK, MathResult(request.a - request.b)) - else -> call.respond(HttpStatusCode.BadRequest, Unit) - } - } - } - - post("random/results", { - response { - HttpStatusCode.OK to { - body> { - mediaType(ContentType.Application.Json) - mediaType(ContentType.Application.Xml) - } - } - } - }) { - call.respond(HttpStatusCode.OK, (0..5).map { MathResult(Random().nextInt()) }) - } - - post("random/numbers", { - response { - HttpStatusCode.OK to { - body>() - } - } - }) { - call.respond(HttpStatusCode.OK, (0..5).map { Random().nextInt() }) - } - - post("echo/{color}", { - request { - pathParameter("color") { - example = Color.BLUE - } - } - response { - HttpStatusCode.OK to { - body() - } - } - }) { - call.respond(HttpStatusCode.OK, Color.valueOf(call.parameters["color"]!!).toString()) - } - - get("generics", { - response { - HttpStatusCode.OK to { - body>() - } - } - }) { - call.respond(HttpStatusCode.NotImplemented, "not-inplemented") - } - - route("images", { - tags = listOf("Image Operations") - description = "Access images " - }) { - - post({ - response { - HttpStatusCode.OK to { - body { - mediaType(ContentType.Image.PNG) - mediaType(ContentType.Image.JPEG) - mediaType(ContentType.Image.SVG) - } - header(HttpHeaders.ContentLength) - } - } - }) { - call.respond(HttpStatusCode.NotImplemented, "...") - } - - delete("{id}", { - description = "Delete the image with the given id." - request { - pathParameter("id") { - description = "The id of the image to delete" - } - } - response { - HttpStatusCode.OK to { - description = "The image was deleted" - } - } - }) { - call.respond(HttpStatusCode.NotImplemented, "...") - } - - } - - get("2dIntArray", { - description = "Returns a 2d-array of integers" - response { - HttpStatusCode.OK to { - body>>() - } - } - }) { - call.respond(HttpStatusCode.NotImplemented, "...") - } - - get("hidden", { - hidden = true - description = "This route is hidden and not visible in swagger" - }) { - call.respond(HttpStatusCode.NotImplemented, "...") - } - - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt deleted file mode 100644 index 70f21ba..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt +++ /dev/null @@ -1,112 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.github.victools.jsonschema.generator.SchemaGenerator -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.data.AuthScheme -import io.github.smiley4.ktorswaggerui.data.AuthType -import io.github.smiley4.ktorswaggerui.data.EncodingData -import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort -import io.github.smiley4.ktorswaggerui.data.SwaggerUiSyntaxHighlight -import io.github.smiley4.ktorswaggerui.dsl.EncodingConfig -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.swagger.v3.oas.models.media.Schema -import kotlin.reflect.jvm.javaType - -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - - -/** - * Example of an (almost) complete plugin config. - * This config will (probably) not work, but is only supposed to show all/most configuration options. - */ -private fun Application.myModule() { - - install(SwaggerUI) { - securityScheme("ApiAuth") { - type = AuthType.HTTP - scheme = AuthScheme.BASIC - } - securityScheme("SwaggerAuth") { - type = AuthType.HTTP - scheme = AuthScheme.BASIC - } - defaultSecuritySchemeName = "ApiAuth" - defaultUnauthorizedResponse { - description = "invalid username or password" - } - swagger { - forwardRoot = false - swaggerUrl = "/api/swagger-ui" - rootHostPath = "/my-ktor-web-app" - authentication = "SwaggerAuth" - disableSpecValidator() - displayOperationId = true - showTagFilterInput = true - sort = SwaggerUiSort.ALPHANUMERICALLY - syntaxHighlight = SwaggerUiSyntaxHighlight.AGATE - } - pathFilter = { _, url -> url.firstOrNull() != "test" } - info { - title = "Example API" - version = "latest" - description = "This is an example api" - termsOfService = "example.com" - contact { - name = "Mr. Example" - url = "example.com/contact" - email = "example@mail.com" - } - license { - name = "Mr. Example" - url = "example.com/license" - } - } - externalDocs { - url = "https://docs.example.com" - description = "Example external documentation description" - } - server { - url = "localhost:8080" - description = "develop server" - } - server { - url = "127.0.0.1:8080" - description = "production server" - } - tag("greet") { - description = "routes for greeting" - externalDocDescription = "documentation for greetings" - externalDocUrl = "example.com/doc" - } - generateTags { url -> listOf(url.firstOrNull()) } - customSchemas { - json("customSchema1") { - """{"type": "string"}""" - } - openApi("customSchema2") { - Schema().also { - it.type = "string" - } - } - remote("customSchema3", "example.com/schema") - includeAll = false - } - encoding { - schemaEncoder { type -> - SchemaGenerator(EncodingData.schemaGeneratorConfigBuilder().build()) - .generateSchema(type.javaType) - .toPrettyString() - } - schemaDefinitionsField = "\$defs" - exampleEncoder { type, example -> - jacksonObjectMapper().writeValueAsString(example) - } - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomSchemaExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomSchemaExample.kt deleted file mode 100644 index 73b71f7..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomSchemaExample.kt +++ /dev/null @@ -1,149 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.custom -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.multipleOf -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.oneOf -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.typeOf -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing - -/** - * An example for defining custom json-schemas - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - - data class MyRequestData( - val someText: String, - val someBoolean: Boolean - ) - - data class MyResponseData( - val someText: String, - val someNumber: Long - ) - - data class Rectangle( - val width: Int, - val height: Int - ) - - data class Circle( - val radius: Int - ) - - data class Point( - val x: Int, - val y: Int - ) - - install(SwaggerUI) { - // don't show the test-routes providing json-schemas - pathFilter = { _, url -> url.firstOrNull() != "schema" } - customSchemas { - // specify a custom json-schema with the id 'myRequestData' - json("myRequestData") { - """ - { - "type": "object", - "properties": { - "someBoolean": { - "type": "boolean" - }, - "someText": { - "type": "string" - } - } - } - """.trimIndent() - } - // specify a remote json-schema with the id 'myRequestData' - remote("myResponseData", "http://localhost:8080/schema/myResponseData") - } - } - - routing { - - get("something", { - request { - // body referencing the custom schema with id 'myRequestData' - body("myRequestData") - } - response { - HttpStatusCode.OK to { - // body referencing the custom schema with id 'myResponseData' - body("myResponseData") - } - } - }) { - val text = call.receive().someText - call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) - } - - get("something/many", { - request { - // body referencing the custom schema with id 'myRequestData' - body(multipleOf(custom("myRequestData"))) - } - response { - HttpStatusCode.OK to { - // body referencing the custom schema with id 'myResponseData' - body(multipleOf(custom("myResponseData"))) - } - } - }) { - val text = call.receive().someText - call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) - } - - get("oneof/shapes", { - request { - // body allowing a mixed list of rectangles, circles and points - body( - multipleOf( - oneOf( - typeOf(Rectangle::class), - typeOf(Circle::class), - typeOf(Point::class), - ) - ) - ) - } - }) { - call.respond(HttpStatusCode.OK, Unit) - } - - // (external) endpoint providing a json-schema - get("schema/myResponseData") { - call.respondText( - """ - { - "type": "object", - "properties": { - "someNumber": { - "type": "integer", - "format": "int64" - }, - "someText": { - "type": "string" - } - } - } - """.trimIndent() - ) - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ExampleAnnotationExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ExampleAnnotationExample.kt deleted file mode 100644 index ab810cd..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ExampleAnnotationExample.kt +++ /dev/null @@ -1,119 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.Example -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing -import io.swagger.v3.oas.annotations.media.ArraySchema -import io.swagger.v3.oas.annotations.media.Schema - -/** - * An example showcasing examples with the [Schema] and [io.github.smiley4.ktorswaggerui.dsl.Example]-Annotation - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - install(SwaggerUI) - routing { - get("person/example", { - request { - body() - } - response { - HttpStatusCode.OK to { - body() - } - } - }) { - call.respondText("...") - } - get("person/schema", { - request { - body() - } - response { - HttpStatusCode.OK to { - body() - } - } - }) { - call.respondText("...") - } - } -} - -data class ExamplePerson( - - @Example("red") - val favColor: String, - - @Example("Steve") - val name: String, - - @Example("42") - val age: Int, - - @Example("172") - val size: Float, - - @Example("false") - val robot: Boolean, - - val address: ExampleAddress, - - val secondaryAddresses: List -) - -data class ExampleAddress( - - @Example("New City") - val city: String, - - @Example("12345") - val code: Int - -) - -@Schema(description = "Schema of some person", title = "Person") -data class SchemaPerson( - - @field:Schema(example = "red") - val favColor: String, - - @field:Schema(example = "Steve", minLength = 1, maxLength = 32) - val name: String, - - @field:Schema(example = "42", minimum = "18", maximum = "99") - val age: Int, - - @field:Schema(example = "172", format = "int32") - val size: Float, - - @field:Schema(example = "false") - val robot: Boolean, - - val address: SchemaAddress, - - @field:ArraySchema(minItems = 1, maxItems = 32, uniqueItems = true) - val secondaryAddresses: List -) - -@Schema(description = "Schema of some address", title = "Address") -data class SchemaAddress( - - @field:Schema(example = "New City") - val city: String, - - @field:Schema(example = "12345") - val code: Int - -) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/FileUploadExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/FileUploadExample.kt deleted file mode 100644 index 9d249ce..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/FileUploadExample.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.post -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respond -import io.ktor.server.routing.routing -import java.io.File - -/** - * An example showcasing file uploads - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - - install(SwaggerUI) - - routing { - - post("single", { - request { - body { - mediaType(ContentType.Image.PNG) - mediaType(ContentType.Image.JPEG) - mediaType(ContentType.Image.SVG) - } - } - }) { - call.respond(HttpStatusCode.NotImplemented, "...") - } - - post("multipart", { - request { - multipartBody { - mediaType(ContentType.MultiPart.FormData) - part("firstImage") { - mediaTypes = setOf( - ContentType.Image.PNG, - ContentType.Image.JPEG, - ContentType.Image.GIF - ) - } - part("secondImage") { - mediaTypes = setOf( - ContentType.Image.PNG, - ContentType.Image.JPEG, - ContentType.Image.GIF - ) - } - part("name") - } - } - }) { - call.respond(HttpStatusCode.NotImplemented, "...") - } - - } - -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxExample.kt deleted file mode 100644 index 800a833..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxExample.kt +++ /dev/null @@ -1,92 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing -import kotlinx.datetime.Instant -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer -import com.github.ricky12awesome.jss.encodeToSchema - -/** - * An example showing compatibility with kotlinx serializer and kotlinx multiplatform using: - * - https://github.com/Kotlin/kotlinx.serialization - * - https://github.com/Kotlin/kotlinx-datetime - * - https://github.com/tillersystems/json-schema-serialization - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - // create own [Json] instance - val kotlinxJson = Json { - prettyPrint = true - encodeDefaults = true - } - install(SwaggerUI) { - encoding { - // custom implementation of schema-encoder supporting kotlinx - schemaEncoder { type -> - kotlinxJson.encodeToSchema(serializer(type), generateDefinitions = false) - } - // generated schemas have the definitions in the field 'definitions' - schemaDefinitionsField = "definitions" - // custom implementation of json serializer for converting examples to json supporting kotlinx - exampleEncoder { type, value -> - kotlinxJson.encodeToString(serializer(type!!), value) - } - } - } - routing { - get("example/one", { - request { - body { - example( - "default", ExampleRequest.B( - thisIsB = Instant.fromEpochMilliseconds(System.currentTimeMillis()) - ) - ) - } - } - }) { - call.respondText("...") - } - get("example/many", { - request { - body> { - example("default", listOf( - ExampleRequest.B(Instant.fromEpochMilliseconds(System.currentTimeMillis())), - ExampleRequest.A(true) - )) - } - } - }) { - call.respondText("...") - } - } -} - - -@Serializable -private sealed class ExampleRequest { - - @Serializable - data class A( - val thisIsA: Boolean - ) : ExampleRequest() - - - @Serializable - data class B( - val thisIsB: Instant - ) : ExampleRequest() - -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ManualRouting.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ManualRouting.kt deleted file mode 100644 index 50247ea..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ManualRouting.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.github.smiley4.ktorswaggerui.routing.openApiSpec -import io.github.smiley4.ktorswaggerui.routing.swaggerUI -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -/** - * An example showcasing manual swaggerui-routing - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - - install(SwaggerUI) { - swagger { - automaticRouter = false - } - } - - routing { - - route("swagger") { - swaggerUI("/api.json") - } - route("api.json") { - openApiSpec() - } - - get("hello", { - description = "Simple 'Hello World'- Route" - }) { - call.respondText("Hello World!") - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ManualRoutingMultipleSpecsExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ManualRoutingMultipleSpecsExample.kt deleted file mode 100644 index a736eb1..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ManualRoutingMultipleSpecsExample.kt +++ /dev/null @@ -1,121 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.github.smiley4.ktorswaggerui.dsl.route -import io.github.smiley4.ktorswaggerui.routing.openApiSpec -import io.github.smiley4.ktorswaggerui.routing.swaggerUI -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -/** - * An example showcasing manual routing with multiple openapi-specs in a single application - * - localhost:8080/swagger-ui/v1/index.html - * * /v1/hello - * - localhost:8080/swagger-ui/v2/index.html - * * /v2/hello - * * /hi - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - - install(Authentication) { - basic("auth-swagger") { - realm = "Access to the Swagger UI" - validate { credentials -> - if (credentials.name == "user" && credentials.password == "pass") { - UserIdPrincipal(credentials.name) - } else { - null - } - } - } - } - - install(SwaggerUI) { - swagger { - automaticRouter = false - } - // general configuration - info { - title = "Example API" - } - specAssigner = { _, _ -> "v2" } // assign all unassigned routes to spec "v2" (here e.g. '/hi') - - // configuration specific for spec "v1" - spec("v1") { - info { - version = "1.0" - } - } - - // configuration specific for spec "v2" - spec("v2") { - info { - version = "2.0" - } - swagger { - authentication = "auth-swagger" - } - } - } - - - routing { - - route("api") { - route("version-1.json") { - openApiSpec("v1") - } - route("version-2.json") { - openApiSpec("v2") - } - } - - route("swagger") { - route("version-1") { - swaggerUI("/api/version-1.json") - } - route("version-2") { - swaggerUI("/api/version-2.json") - } - } - - // version 1.0 routes - route("v1", { - specId = "v1" // assign all sub-routes to spec "v1" - }) { - get("hello", { - description = "Simple version 1 'Hello World'-Route" - }) { - call.respondText("Hello World!") - } - } - - // version 2.0 routes - route("v2", { - specId = "v2" // assign all sub-routes to spec "v2" - }) { - get("hello", { - description = "Simple version 2 'Hello World'-Route" - }) { - call.respondText("Improved Hello World!") - } - } - - // other routes - get("hi", { - description = "Alternative version of 'Hello World'-Route" - }) { - call.respondText("Alternative Hello World!") - } - - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MinimalExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MinimalExample.kt deleted file mode 100644 index 3dc897f..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MinimalExample.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing - -/** - * A minimal working example - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - // Install the "SwaggerUI"-Plugin and use the default configuration - // By default, swagger is available at /swagger-ui (i.e. localhost:8080/swagger-ui) - install(SwaggerUI) - - routing { - // documented "get"-route - get("hello", { - - request { - body { - example("example", 42) - } - } - - // a description of the route - description = "Simple 'Hello World'- Route" - // information about possible responses - response { - // information about a "200 OK" response - HttpStatusCode.OK to { - // the description of the response - description = "Successful Response" - } - } - }) { - call.respondText("Hello World!") - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipartBody.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipartBody.kt deleted file mode 100644 index c6a79e9..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipartBody.kt +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.ktor.server.application.Application -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.post -import io.ktor.http.ContentType -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing -import java.io.File - -/** - * An example of a multipart-body - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - - -private fun Application.myModule() { - - data class Coords( - val lat: Float, - val long: Float - ) - - data class Metadata( - val format: String, - val location: Coords - ) - - install(SwaggerUI) - - routing { - post("example", { - request { - multipartBody { - mediaType(ContentType.MultiPart.FormData) - part("image") { - mediaTypes = setOf( - ContentType.Image.PNG, - ContentType.Image.JPEG, - ContentType.Image.GIF - ) - } - part("metadata") { - mediaTypes = setOf(ContentType.Application.Json) - } - } - } - }) { - call.respondText("Upload complete") - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecsExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecsExample.kt deleted file mode 100644 index 9b712d4..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MultipleSpecsExample.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.github.smiley4.ktorswaggerui.dsl.route -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.auth.Authentication -import io.ktor.server.auth.UserIdPrincipal -import io.ktor.server.auth.basic -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing - -/** - * An example showcasing multiple openapi-specs in a single application - * - localhost:8080/swagger-ui/v1/index.html - * * /v1/hello - * - localhost:8080/swagger-ui/v2/index.html - * * /v2/hello - * * /hi - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - - install(Authentication) { - basic("auth-swagger") { - realm = "Access to the Swagger UI" - validate { credentials -> - if (credentials.name == "user" && credentials.password == "pass") { - UserIdPrincipal(credentials.name) - } else { - null - } - } - } - } - - install(SwaggerUI) { - // general configuration - info { - title = "Example API" - } - specAssigner = { _, _ -> "v2" } // assign all unassigned routes to spec "v2" (here e.g. '/hi') - - // configuration specific for spec "v1" - spec("v1") { - info { - version = "1.0" - } - } - - // configuration specific for spec "v2" - spec("v2") { - info { - version = "2.0" - } - swagger { - authentication = "auth-swagger" - } - } - } - - - routing { - - // version 1.0 routes - route("v1", { - specId = "v1" // assign all sub-routes to spec "v1" - }) { - get("hello", { - description = "Simple version 1 'Hello World'-Route" - }) { - call.respondText("Hello World!") - } - } - - // version 2.0 routes - route("v2", { - specId = "v2" // assign all sub-routes to spec "v2" - }) { - get("hello", { - description = "Simple version 2 'Hello World'-Route" - }) { - call.respondText("Improved Hello World!") - } - } - - // other routes - get("hi", { - description = "Alternative version of 'Hello World'-Route" - }) { - call.respondText("Alternative Hello World!") - } - - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt deleted file mode 100644 index de3068f..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt +++ /dev/null @@ -1,148 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.ktor.server.application.Application -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.delete -import io.github.smiley4.ktorswaggerui.dsl.get -import io.github.smiley4.ktorswaggerui.dsl.post -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respond -import io.ktor.server.routing.routing - - -/** - * Uses the OpenApi-Example "petstore-simple" to demonstrate ktor with swagger-ui - * https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v2.0/json/petstore-simple.json - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - - data class Pet( - val id: Int, - val name: String, - val tag: String - ) - - data class NewPet( - val name: String, - val tag: String - ) - - install(SwaggerUI) { - info { - title = "Swagger Petstore" - description = "A sample API that uses a petstore as an example" - } - } - - routing { - - get("pets", { - description = "Returns all pets from the system that the user has access to." - request { - queryParameter>("tags") { - description = "tags to filter by" - required = false - } - queryParameter("limit", Int::class) { - description = "maximum number of results to return" - required = false - } - } - response { - HttpStatusCode.OK to { - description = "pet response" - body>() { - mediaType(ContentType.Application.Json) - mediaType(ContentType.Application.Xml) - example( - "example", listOf( - Pet(1, "Chloe", "cat"), - Pet(2, "Oliver", "dog") - ) - ) - } - } - } - }) { - // handle request ... - call.respond(HttpStatusCode.NotImplemented, Unit) - } - - post("pets", { - description = "Creates a new pet in the store. Duplicates are allowed." - request { - body { - description = "Pet to add to the store" - required = true - mediaType(ContentType.Application.Json) - mediaType(ContentType.Application.Xml) - example("example", NewPet("Max", "bird")) - } - } - response { - HttpStatusCode.OK to { - description = "pet response" - body> { - mediaType(ContentType.Application.Json) - mediaType(ContentType.Application.Xml) - example("example", Pet(3, "Max", "bird")) - } - } - } - }) { - // handle request ... - call.respond(HttpStatusCode.NotImplemented, Unit) - } - - get("pets/{id}", { - description = "Returns a pet based on a single ID, if the user has access to the pet." - request { - pathParameter("id") { - description = "ID of pet to fetch" - required = true - } - } - response { - HttpStatusCode.OK to { - description = "pet response" - body { - mediaType(ContentType.Application.Json) - mediaType(ContentType.Application.Xml) - example("example", Pet(4, "Bella", "dog")) - } - } - } - }) { - // handle request ... - call.respond(HttpStatusCode.NotImplemented, Unit) - } - - delete("pets/{id}", { - description = "deletes a single pet based on the ID supplied." - request { - pathParameter("id") { - description = "ID of pet to delete" - required = true - } - } - response { - HttpStatusCode.NoContent to { - description = "pet deleted" - } - } - }) { - // handle request ... - call.respond(HttpStatusCode.NotImplemented, Unit) - } - - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ProtectedSwaggerExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ProtectedSwaggerExample.kt deleted file mode 100644 index feccf6e..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ProtectedSwaggerExample.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.auth.Authentication -import io.ktor.server.auth.UserIdPrincipal -import io.ktor.server.auth.basic -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing - -/** - * An example demonstrating a swagger protected by custom authentication - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - install(Authentication) { - basic("auth-swagger") { - realm = "Access to the Swagger UI" - validate { credentials -> - if (credentials.name == "user" && credentials.password == "pass") { - UserIdPrincipal(credentials.name) - } else { - null - } - } - } - } - - install(SwaggerUI) { - swagger { - // protect Swagger UI and OpenApi-Spec with the authentication method defined above - authentication = "auth-swagger" - } - } - - routing { - get("hello", { - description = "Simple 'Hello World'- Route" - response { - HttpStatusCode.OK to { - description = "Successful Response" - } - } - }) { - call.respondText("Hello World!") - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ResourceExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ResourceExample.kt deleted file mode 100644 index e35898a..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/ResourceExample.kt +++ /dev/null @@ -1,132 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import com.fasterxml.jackson.core.util.DefaultIndenter -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter -import com.fasterxml.jackson.databind.SerializationFeature -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.resources.get -import io.github.smiley4.ktorswaggerui.dsl.resources.post -import io.ktor.http.HttpStatusCode -import io.ktor.resources.Resource -import io.ktor.serialization.jackson.jackson -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.resources.Resources -import io.ktor.server.response.respond -import io.ktor.server.routing.routing - -/** - * Example to showcase usage with the resources plugin - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -@Resource("articles") -class ArticlesRoute(val sorting: String?) - -data class Article( - val title: String, - val content: String -) - -private fun Application.myModule() { - - install(Resources) - install(SwaggerUI) { - swagger { - swaggerUrl = "swagger-ui" - forwardRoot = true - } - info { - title = "Example API" - version = "latest" - description = "Example API for testing and demonstration purposes." - } - externalDocs { - url = "https://github.com/SMILEY4/ktor-swagger-ui/wiki" - description = "Sample external documentation object" - } - server { - url = "http://localhost:8080" - description = "Development Server" - } - tag("articles") { - description = "Routes that return articles" - } - generateTags { url -> listOf(url.firstOrNull()) } - } - install(ContentNegotiation) { - jackson { - configure(SerializationFeature.INDENT_OUTPUT, true) - setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { - indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) - indentObjectsWith(DefaultIndenter(" ", "\n")) - }) - } - } - - routing { - get({ - tags = listOf("articles") - description = "Articles endpoint" - operationId = "get-articles" - request { - queryParameter("sorting") { - description = "Optional sorting applied to articles" - } - } - response { - default { - description = "Default Response" - } - HttpStatusCode.OK to { - description = "Successful Request" - body { description = "the response" } - } - HttpStatusCode.InternalServerError to { - description = "Something unexpected happened" - } - } - }) { - if (it.sorting != null) - call.respond(HttpStatusCode.OK, "No articles yet (with ${it.sorting} sorting)") - else - call.respond(HttpStatusCode.OK, "No articles yet") - } - - post({ - tags = listOf("articles") - description = "Creates a new article" - operationId = "createArticle" - request { - pathParameter("id") { - description = "The id of the requested article" - } - body
{ - example("First", Article("Ktor openapi resources", "ktor now support openapi for resources!")) { - description = "Create a ktor article" - } - } - } - response { - default { - description = "Default Response" - } - HttpStatusCode.OK to { - description = "Successful Request" - body { description = "the response" } - } - HttpStatusCode.InternalServerError to { - description = "Something unexpected happened" - } - } - }) { - call.respond(HttpStatusCode.OK, "Article not saved ^^") - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/RootPathExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/RootPathExample.kt deleted file mode 100644 index e87eb94..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/RootPathExample.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.EngineMain -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing - - -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - - install(SwaggerUI) { - swagger { - swaggerUrl = "swagger-ui" - forwardRoot = true - } - info { - title = "Example API" - version = "latest" - description = "Example api for testing" - } - server { - url = "http://localhost:8080" - description = "Development server" - } - } - - routing { - get("hello", { - description = "Simple 'Hello World'- Route" - response { - HttpStatusCode.OK to { - description = "Successful Response" - } - } - }) { - call.respondText("Hello World!") - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SchemaAnnotationExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SchemaAnnotationExample.kt deleted file mode 100644 index 4f1073a..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SchemaAnnotationExample.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import com.fasterxml.jackson.core.util.DefaultIndenter -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter -import com.fasterxml.jackson.databind.SerializationFeature -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.serialization.jackson.jackson -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.response.respond -import io.ktor.server.routing.routing -import io.swagger.v3.oas.annotations.media.Schema - -/** - * An example showing the [Schema]-annotation, adding additional information to models - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - install(SwaggerUI) - install(ContentNegotiation) { - jackson { - configure(SerializationFeature.INDENT_OUTPUT, true) - setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { - indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) - indentObjectsWith(DefaultIndenter(" ", "\n")) - }) - } - } - routing { - get("somebody", { - response { - HttpStatusCode.OK to { - body() - } - } - }) { - call.respond(Person("Somebody", 42)) - } - } -} - -@Schema(title = "The Schema for a person") -data class Person( - - @field:Schema(description = "the name of the person") - val name: String, - - @field:Schema(description = "the age of the person in years", nullable = true) - val age: Int - -) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ApplicationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ApplicationTests.kt deleted file mode 100644 index 33ed640..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ApplicationTests.kt +++ /dev/null @@ -1,481 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl -import io.github.smiley4.ktorswaggerui.dsl.get -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import io.kotest.matchers.string.shouldNotBeEmpty -import io.ktor.client.HttpClient -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.auth.Authentication -import io.ktor.server.auth.UserIdPrincipal -import io.ktor.server.auth.basic -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing -import io.ktor.server.testing.testApplication -import kotlin.test.Test - -class ApplicationTests { - - @Test - fun minimal() = swaggerUITestApplication { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.JavaScript - it.body shouldContain "url: \"/swagger-ui/api.json\"" - } - get("/swagger-ui/api.json").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.Json - it.body.shouldNotBeEmpty() - } - } - - - @Test - fun customRootHost() = swaggerUITestApplication( - { - swagger { - rootHostPath = "my-root" - } - }, - followRedirects = false, - ) { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("swagger-ui").also { - it.status shouldBe HttpStatusCode.Found - it.redirect shouldBe "my-root/swagger-ui/index.html" - } - get("swagger-ui/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("swagger-ui/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.JavaScript - it.body shouldContain "url: \"/my-root/swagger-ui/api.json\"" - } - get("swagger-ui/api.json").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.Json - it.body.shouldNotBeEmpty() - } - } - - - @Test - fun forwardRoot() = swaggerUITestApplication({ - swagger { - forwardRoot = true - } - }) { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.JavaScript - it.body shouldContain "url: \"/swagger-ui/api.json\"" - } - get("/swagger-ui/api.json").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.Json - it.body.shouldNotBeEmpty() - } - } - - - @Test - fun forwardRootWithCustomSwaggerUrl() = swaggerUITestApplication({ - swagger { - forwardRoot = true - swaggerUrl = "test-swagger" - } - }) { - get("/").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/test-swagger").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/test-swagger/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - } - - - @Test - fun protectedSwaggerUI() = swaggerUITestApplication({ - swagger { - authentication = "my-auth" - } - }) { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui/index.html").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui/api.json").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - } - - - @Test - fun forwardRootAndProtectedSwaggerUI() = swaggerUITestApplication({ - swagger { - authentication = "my-auth" - forwardRoot = true - } - }) { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui/index.html").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui/api.json").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - } - - - @Test - fun customSwaggerUrl() = swaggerUITestApplication({ - swagger { - swaggerUrl = "test-swagger" - } - }) { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui/index.html").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui/api.json").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/test-swagger").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/test-swagger/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/test-swagger/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.JavaScript - it.body shouldContain "url: \"/test-swagger/api.json\"" - - } - get("/test-swagger/api.json").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.Json - it.body.shouldNotBeEmpty() - } - } - - - @Test - fun customSwaggerUrlAndProtected() = swaggerUITestApplication({ - swagger { - authentication = "my-auth" - swaggerUrl = "test-swagger" - } - }) { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui/index.html").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui/api.json").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/test-swagger").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/test-swagger/index.html").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/test-swagger/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/test-swagger/api.json").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - } - - - @Test - fun multipleSwaggerUI() = swaggerUITestApplication({ - specAssigner = { _, tags -> tags.firstOrNull() ?: "other" } - }) { - get("hello").also { - it.status shouldBe HttpStatusCode.OK - it.body shouldBe "Hello Test" - } - get("/").also { - it.status shouldBe HttpStatusCode.NotFound - } - get("/swagger-ui/hello").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/hello/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/hello/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.JavaScript - it.body shouldContain "url: \"/swagger-ui/hello/hello.json\"" - } - get("/swagger-ui/hello/hello.json").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.Json - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/world").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/world/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/world/swagger-initializer.js").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.JavaScript - it.body shouldContain "url: \"/swagger-ui/world/world.json\"" - } - get("/swagger-ui/world/world.json").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.Json - it.body.shouldNotBeEmpty() - } - } - - - @Test - fun multipleSwaggerUIWithDifferentAuthConfig() = swaggerUITestApplication({ - specAssigner = { _, tags -> tags.firstOrNull() ?: "other" } - spec("hello") { - swagger { - authentication = null - } - } - spec("world") { - swagger { - authentication = "my-auth" - } - } - }) { - get("/swagger-ui/hello/index.html").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Text.Html - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/hello/hello.json").also { - it.status shouldBe HttpStatusCode.OK - it.contentType shouldBe ContentType.Application.Json - it.body.shouldNotBeEmpty() - } - get("/swagger-ui/world/index.html").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - get("/swagger-ui/world/world.json").also { - it.status shouldBe HttpStatusCode.Unauthorized - } - } - - - private fun swaggerUITestApplication(followRedirects: Boolean = true, block: suspend TestContext.() -> Unit) { - swaggerUITestApplication({}, followRedirects, block) - } - - private fun swaggerUITestApplication( - pluginConfig: PluginConfigDsl.() -> Unit, - followRedirects: Boolean = true, - block: suspend TestContext.() -> Unit - ) { - testApplication { - val client = createClient { - this.followRedirects = followRedirects - } - application { - install(Authentication) { - basic("my-auth") { - validate { credentials -> - if (credentials.name == "user" && credentials.password == "pass") { - UserIdPrincipal(credentials.name) - } else { - null - } - } - } - } - install(SwaggerUI, pluginConfig) - routing { - get("hello", { - tags = listOf("hello") - description = "Simple 'Hello World'- Route" - response { - HttpStatusCode.OK to { - description = "Successful Response" - } - } - }) { - call.respondText("Hello Test") - } - get("world", { - tags = listOf("world") - description = "Another simple 'Hello World'- Route" - response { - HttpStatusCode.OK to { - description = "Successful Response" - } - } - }) { - call.respondText("Hello World") - } - } - Thread.sleep(500) - } - TestContext(client).apply { block() } - } - } - - class TestContext(private val client: HttpClient) { - - suspend fun get(path: String): GetResult { - return client.get(path) - .let { - GetResult( - path = path, - status = it.status, - contentType = it.contentType(), - body = it.bodyAsText(), - redirect = it.headers["Location"] - ) - } - .also { it.print() } - } - - - private fun GetResult.print() { - println("GET ${this.path} => ${this.status} (${this.contentType}): ${this.body}") - } - } - - - data class GetResult( - val path: String, - val status: HttpStatusCode, - val contentType: ContentType?, - val body: String, - val redirect: String? - ) - -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/example/ExampleTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/example/ExampleTest.kt deleted file mode 100644 index 7dae2e5..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/example/ExampleTest.kt +++ /dev/null @@ -1,201 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests.example - -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.builder.example.ExampleContext -import io.github.smiley4.ktorswaggerui.builder.example.ExampleContextBuilder -import io.github.smiley4.ktorswaggerui.builder.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.ktor.http.HttpMethod - -class ExampleTest : StringSpec({ - - "no request parameter example" { - val route = route { - request { - queryParameter("param") - } - } - val exampleContext = exampleContext(listOf(route)) - exampleContext.getExample(route.getRequestParameter("param")) shouldBe null - } - - "primitive request parameter examples" { - val route = route { - request { - queryParameter("stringParam") { - example = "Example Value" - } - queryParameter("intParam") { - example = 42 - } - queryParameter("boolParam") { - example = true - } - } - } - val exampleContext = exampleContext(listOf(route)) - exampleContext.getExample(route.getRequestParameter("stringParam")) - .also { it shouldNotBe null } - ?.also { example -> - example shouldBe "Example Value" - } - exampleContext.getExample(route.getRequestParameter("intParam")) - .also { it shouldNotBe null } - ?.also { example -> - example shouldBe "42" - } - exampleContext.getExample(route.getRequestParameter("boolParam")) - .also { it shouldNotBe null } - ?.also { example -> - example shouldBe "true" - } - } - - - "no body example" { - val route = route { - request { - body { - example("differentExample", "Example Value") - } - } - } - val exampleContext = exampleContext(listOf(route)) - exampleContext.getExample(route.getRequestBody(), "testExample") shouldBe null - } - - "simple body example" { - val route = route { - request { - body { - example("testExample", "Example Value") { - summary = "test summary" - description = "test description" - } - } - } - } - val exampleContext = exampleContext(listOf(route)) - exampleContext.getExample(route.getRequestBody(), "testExample") - .also { it shouldNotBe null } - ?.also { example -> - example.value shouldBe "Example Value" - example.description shouldBe "test description" - example.summary shouldBe "test summary" - } - } - - "object body example" { - val route = route { - request { - body { - example("testExample", SimpleObject("someText", 42)) { - summary = "test summary" - description = "test description" - } - } - } - } - val exampleContext = exampleContext(listOf(route)) - exampleContext.getExample(route.getRequestBody(), "testExample") - .also { it shouldNotBe null } - ?.also { example -> - example.value shouldBe "{\"text\":\"someText\",\"number\":42}" - example.description shouldBe "test description" - example.summary shouldBe "test summary" - } - } - - "object inheritance body example" { - val route = route { - request { - body { - example("a", ExampleRequest.A("test a")) { - summary = "a summary" - description = "a description" - } - example("b", ExampleRequest.B(42)) { - summary = "b summary" - description = "b description" - } - } - } - } - val exampleContext = exampleContext(listOf(route)) - exampleContext.getExample(route.getRequestBody(), "a") - .also { it shouldNotBe null } - ?.also { example -> - example.value shouldBe "{\"thisIsA\":\"test a\"}" - example.description shouldBe "a description" - example.summary shouldBe "a summary" - } - exampleContext.getExample(route.getRequestBody(), "b") - .also { it shouldNotBe null } - ?.also { example -> - example.value shouldBe "{\"thisIsB\":42}" - example.description shouldBe "b description" - example.summary shouldBe "b summary" - } - } - -}) { - - companion object { - - private data class SimpleObject( - val text: String, - val number: Int - ) - - private sealed class ExampleRequest { - - data class A( - val thisIsA: String - ) : ExampleRequest() - - - data class B( - val thisIsB: Int - ) : ExampleRequest() - - } - - private val defaultPluginConfig = PluginConfigDsl() - - private fun exampleContext( - routes: List, - pluginConfig: PluginConfigDsl = defaultPluginConfig - ): ExampleContext { - return ExampleContextBuilder( - exampleBuilder = ExampleBuilder( - config = pluginConfig.build(PluginConfigData.DEFAULT) - ) - ).build(routes.toList()) - } - - fun route(block: OpenApiRoute.() -> Unit): RouteMeta { - return RouteMeta( - path = "/test", - method = HttpMethod.Get, - documentation = OpenApiRoute().apply(block), - protected = false - ) - } - - fun RouteMeta.getRequestParameter(name: String): OpenApiRequestParameter { - return this.documentation.getRequest().getParameters().find { it.name == name }!! - } - - fun RouteMeta.getRequestBody(): OpenApiSimpleBody { - return this.documentation.getRequest().getBody()!! as OpenApiSimpleBody - } - - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt deleted file mode 100644 index 44192f6..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests.openapi - -import io.github.smiley4.ktorswaggerui.data.ServerData -import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer -import io.github.smiley4.ktorswaggerui.builder.openapi.ServerBuilder -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.swagger.v3.oas.models.servers.Server - -class ServersBuilderTest : StringSpec({ - - "default server object" { - buildServerObject {}.also { server -> - server.url shouldBe "/" - server.description shouldBe null - server.variables shouldBe null - server.extensions shouldBe null - - } - } - - "complete server object" { - buildServerObject { - url = "Test URL" - description = "Test Description" - }.also { server -> - server.url shouldBe "Test URL" - server.description shouldBe "Test Description" - server.variables shouldBe null - server.extensions shouldBe null - } - } - -}) { - - companion object { - - private fun buildServerObject(builder: OpenApiServer.() -> Unit): Server { - return ServerBuilder().build(OpenApiServer().apply(builder).build(ServerData.DEFAULT)) - } - - } - -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt deleted file mode 100644 index 7102e82..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt +++ /dev/null @@ -1,519 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests.schema - -import com.fasterxml.jackson.databind.ObjectMapper -import com.github.ricky12awesome.jss.encodeToSchema -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.module.swagger2.Swagger2Module -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaDefinitions -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaTypeAttributeOverride -import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites -import io.github.smiley4.ktorswaggerui.dsl.Example -import io.github.smiley4.ktorswaggerui.dsl.SchemaEncoder -import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.maps.shouldHaveSize -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.swagger.v3.oas.annotations.media.Schema -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer -import java.io.File -import kotlin.reflect.jvm.javaType - -class SchemaBuilderTest : StringSpec({ - - "primitive (victools, all definitions)" { - createSchemaVictools(true).also { defs -> - defs.definitions.keys shouldContainExactly setOf("int") - defs.root.also { schema -> - schema.`$ref` shouldBe "#/components/schemas/int" - schema.type shouldBe null - } - defs.definitions["int"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "integer" - } - } - } - - "primitive (victools, no definitions)" { - createSchemaVictools(false).also { defs -> - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "integer" - schema.properties shouldBe null - } - } - } - - "primitive (kotlinx, definitions)" { - createSchemaKotlinX(true).also { defs -> - defs.definitions.keys shouldContainExactly setOf("x1xjd3yo2dbzzz") - defs.root.also { schema -> - schema.`$ref` shouldBe "#/components/schemas/x1xjd3yo2dbzzz" - schema.type shouldBe null - } - defs.definitions["x1xjd3yo2dbzzz"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "number" - schema.properties shouldBe null - } - } - } - - "primitive (kotlinx, no definitions)" { - createSchemaKotlinX(false).also { defs -> - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "number" - schema.properties shouldBe null - } - } - } - - "simple object (victools, all definitions)" { - createSchemaVictools(true).also { defs -> - defs.definitions.keys shouldContainExactly setOf("Pet") - defs.root.also { schema -> - schema.`$ref` shouldBe "#/components/schemas/Pet" - schema.type shouldBe null - } - defs.definitions["Pet"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("id", "name", "tag") - } - } - } - - "simple object (victools, no definitions)" { - createSchemaVictools(false).also { defs -> - println(defs) - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("id", "name", "tag") - schema.properties["id"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "integer" - } - schema.properties["name"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - schema.properties["tag"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - } - } - } - - "object with special field names (victools)" { - createSchemaVictools(false).also { defs -> - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("flag", "getAnotherText", "text", "isSomething", "isSomeText") - schema.properties["flag"]!!.also { prop -> - prop.type shouldBe "boolean" - } - schema.properties["isSomething"]!!.also { prop -> - prop.type shouldBe "boolean" - } - schema.properties["text"]!!.also { prop -> - prop.type shouldBe "string" - } - schema.properties["getAnotherText"]!!.also { prop -> - prop.type shouldBe "string" - } - schema.properties["isSomeText"]!!.also { prop -> - prop.type shouldBe "string" - } - } - } - } - - "simple object (kotlinx, definitions)" { - createSchemaKotlinX(true).also { defs -> - defs.definitions.keys shouldContainExactly setOf("1d8t6cs0dbcap", "x1xjd3yo2dbzzz", "xq0zwcprkn9j3") - defs.root.also { schema -> - schema.`$ref` shouldBe "#/components/schemas/1d8t6cs0dbcap" - schema.type shouldBe null - } - defs.definitions["1d8t6cs0dbcap"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("id", "name", "tag") - schema.properties["id"]!!.also { prop -> - prop.`$ref` shouldBe "#/components/schemas/x1xjd3yo2dbzzz" - prop.type shouldBe null - } - schema.properties["name"]!!.also { prop -> - prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" - prop.type shouldBe null - } - schema.properties["tag"]!!.also { prop -> - prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" - prop.type shouldBe null - } - } - defs.definitions["x1xjd3yo2dbzzz"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "number" - schema.properties shouldBe null - } - defs.definitions["xq0zwcprkn9j3"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "string" - schema.properties shouldBe null - } - } - } - - "simple object (kotlinx, no definitions)" { - createSchemaKotlinX(false).also { defs -> - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("id", "name", "tag") - schema.properties["id"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "number" - } - schema.properties["name"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - schema.properties["tag"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - } - } - } - - "object with special field names (kotlinx)" { - createSchemaKotlinX(false).also { defs -> - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("flag", "getAnotherText", "text", "isSomething", "isSomeText") - schema.properties["flag"]!!.also { prop -> - prop.type shouldBe "boolean" - } - schema.properties["isSomething"]!!.also { prop -> - prop.type shouldBe "boolean" - } - schema.properties["text"]!!.also { prop -> - prop.type shouldBe "string" - } - schema.properties["getAnotherText"]!!.also { prop -> - prop.type shouldBe "string" - } - schema.properties["isSomeText"]!!.also { prop -> - prop.type shouldBe "string" - } - } - } - } - - //==== SIMPLE LIST ===================================================== - - "simple list (victools, all definitions)" { - createSchemaVictools>(true).also { defs -> - defs.definitions.keys shouldContainExactly setOf("List(Pet)", "Pet") - defs.root.also { schema -> - schema.`$ref` shouldBe "#/components/schemas/List(Pet)" - schema.type shouldBe null - } - defs.definitions["List(Pet)"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "array" - schema.items - .also { it shouldNotBe null } - ?.also { item -> - item.type shouldBe null - item.`$ref` shouldBe "#/components/schemas/Pet" - } - } - defs.definitions["Pet"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("id", "name", "tag") - } - } - } - - "simple list (victools, no definitions)" { - createSchemaVictools>(false).also { defs -> - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "array" - schema.properties shouldBe null - schema.items - .also { it shouldNotBe null } - ?.also { item -> - item.type shouldBe "object" - item.`$ref` shouldBe null - item.properties.keys shouldContainExactly setOf("id", "name", "tag") - item.properties["id"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "integer" - } - item.properties["name"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - item.properties["tag"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - } - } - } - } - - - "simple list (kotlinx, definitions)" { - createSchemaKotlinX>(true).also { defs -> - defs.definitions.keys shouldContainExactly setOf( - "1tonzv7il5q0x", - "1d8t6cs0dbcap", - "x1xjd3yo2dbzzz", - "xq0zwcprkn9j3" - ) - defs.root.also { schema -> - schema.`$ref` shouldBe "#/components/schemas/1tonzv7il5q0x" - schema.type shouldBe null - } - defs.definitions["1tonzv7il5q0x"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "array" - schema.properties shouldBe null - schema.items - .also { it shouldNotBe null } - ?.also { item -> - item.type shouldBe null - item.`$ref` shouldBe "#/components/schemas/1d8t6cs0dbcap" - } - } - defs.definitions["1d8t6cs0dbcap"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("id", "name", "tag") - schema.properties["id"]!!.also { prop -> - prop.`$ref` shouldBe "#/components/schemas/x1xjd3yo2dbzzz" - prop.type shouldBe null - } - schema.properties["name"]!!.also { prop -> - prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" - prop.type shouldBe null - } - schema.properties["tag"]!!.also { prop -> - prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" - prop.type shouldBe null - } - } - defs.definitions["x1xjd3yo2dbzzz"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "number" - schema.properties shouldBe null - } - defs.definitions["xq0zwcprkn9j3"]!!.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "string" - schema.properties shouldBe null - } - } - } - - "simple list (kotlinx, no definitions)" { - createSchemaKotlinX>(false).also { defs -> - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.`$ref` shouldBe null - schema.type shouldBe "array" - schema.properties shouldBe null - schema.items - .also { it shouldNotBe null } - ?.also { item -> - item.type shouldBe "object" - item.`$ref` shouldBe null - item.properties.keys shouldContainExactly setOf("id", "name", "tag") - item.properties["id"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "number" - } - item.properties["name"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - item.properties["tag"]!!.also { prop -> - prop.`$ref` shouldBe null - prop.type shouldBe "string" - } - } - } - } - } - - "schema with Schema-Annotations" { - createSchemaVictools(false).also { defs -> - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.title = "The Schema for a person" - schema.type shouldBe "object" - schema.properties.keys shouldContainExactly setOf("age", "city_code", "name") - schema.properties["age"]!!.also { age -> - age.type shouldBe "integer" - age.description shouldBe "the age of the person in years" - age.format shouldBe "int32" - age.nullable shouldBe true - age.minimum.toInt() shouldBe 1 - age.maximum.toInt() shouldBe 99 - age.example shouldBe 42 - } - schema.properties["name"]!!.also { name -> - name.type shouldBe "string" - name.description shouldBe "the name of the person" - name.minLength shouldBe 1 - name.maxLength shouldBe 32 - name.example shouldBe "Mr. Example" - } - schema.properties["city_code"]!!.also { cityCode -> - cityCode.type shouldBe "integer" - cityCode.format shouldBe "int32" - cityCode.example shouldBe 12345 - } - } - } - } - - "file type-overwrite" { - createSchemaVictools(false).also { defs -> - defs.definitions shouldHaveSize 0 - defs.root.also { schema -> - schema.type shouldBe "string" - schema.format shouldBe "binary" - } - } - } - -}) { - - companion object { - - @Serializable - private data class Pet( - val id: Int, - val name: String, - val tag: String - ) - - - @Schema(title = "The Schema for a person") - data class Person( - - @field:Schema( - description = "the name of the person", - minLength = 1, - maxLength = 32 - ) - @field:Example("Mr. Example") - val name: String, - - @field:Schema( - description = "the age of the person in years", - nullable = true, - maximum = "99", - minimum = "1", - ) - @field:Example("42") - val age: Int, - - @field:Schema( - name = "city_code", - example = "12345" - ) - val cityCode: Int - - ) - - @Serializable - class WithFieldNames( - val flag: Boolean, - val isSomething: Boolean, - val text: String, - val isSomeText: String, - val getAnotherText: String - ) - - inline fun createSchemaVictools(definitions: Boolean) = - createSchema("\$defs", serializerVictools(definitions)) - - inline fun createSchemaKotlinX(generateDefinitions: Boolean) = - createSchema("definitions", serializerKotlinX(generateDefinitions)) - - inline fun createSchema( - defs: String, - noinline serializer: SchemaEncoder - ): SchemaDefinitions { - return SchemaBuilder(defs, serializer, ObjectMapper(), TypeOverwrites.get()).create(getSchemaType()) - } - - fun serializerVictools(definitions: Boolean): SchemaEncoder { - return { type -> - SchemaGenerator( - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(Swagger2Module()) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES).also { - if (definitions) { - it - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.DEFINITION_FOR_MAIN_SCHEMA) - .without(Option.INLINE_ALL_SCHEMAS) - } else { - it.with(Option.INLINE_ALL_SCHEMAS) - } - } - .also { - it.forTypesInGeneral() - .withTypeAttributeOverride(SchemaTypeAttributeOverride()) - } - .build() - ).generateSchema(type.javaType).toPrettyString() - } - } - - fun serializerKotlinX(generateDefinitions: Boolean): SchemaEncoder { - val kotlinxJson = Json { - prettyPrint = true - encodeDefaults = true - } - return { type -> - kotlinxJson.encodeToSchema( - serializer(type), - generateDefinitions = generateDefinitions, - exposeClassDiscriminator = false - ) - } - } - - } - -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt deleted file mode 100644 index f4040d4..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt +++ /dev/null @@ -1,553 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests.schema - -import com.fasterxml.jackson.databind.ObjectMapper -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import io.github.smiley4.ktorswaggerui.data.PluginConfigData -import io.github.smiley4.ktorswaggerui.dsl.PluginConfigDsl -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.dsl.asSchemaType -import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.builder.route.RouteMeta -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContext -import io.github.smiley4.ktorswaggerui.builder.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.builder.schema.TypeOverwrites -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.custom -import io.github.smiley4.ktorswaggerui.dsl.BodyTypeDescriptor.Companion.multipleOf -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.maps.shouldBeEmpty -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.ktor.http.HttpMethod -import io.swagger.v3.oas.models.media.Schema -import kotlin.reflect.jvm.javaType - -class SchemaContextTest : StringSpec({ - - "route with all schemas" { - val routes = listOf( - route { - request { - queryParameter("queryParam") - pathParameter("pathParam") - headerParameter("headerParam") - body() - } - response { - default { - header("header") - body() - } - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(QueryParamType::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/QueryParamType" - } - schemaContext.getSchema(PathParamType::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/PathParamType" - } - schemaContext.getSchema(HeaderParamType::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/HeaderParamType" - } - schemaContext.getSchema(RequestBodyType::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/RequestBodyType" - } - schemaContext.getSchema(ResponseHeaderType::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/ResponseHeaderType" - } - schemaContext.getSchema(ResponseBodyType::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/ResponseBodyType" - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf( - "QueryParamType", - "PathParamType", - "HeaderParamType", - "RequestBodyType", - "ResponseHeaderType", - "ResponseBodyType", - ) - components.forEach { (_, schema) -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("value") - } - } - } - - "primitive type" { - val routes = listOf( - route { - request { - body() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(Integer::class.asSchemaType()).also { schema -> - schema.type shouldBe "integer" - schema.format shouldBe "int32" - schema.`$ref` shouldBe null - } - schemaContext.getComponentsSection().also { components -> - components.shouldBeEmpty() - } - } - - "primitive array" { - val routes = listOf( - route { - request { - body>() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(getType>()).also { schema -> - schema.type shouldBe "array" - schema.`$ref` shouldBe null - schema.items.also { item -> - item.type shouldBe "string" - } - } - schemaContext.getComponentsSection().also { components -> - components.shouldBeEmpty() - } - } - - "primitive deep array" { - val routes = listOf( - route { - request { - body>>>() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(getType>>>()).also { schema -> - schema.type shouldBe "array" - schema.`$ref` shouldBe null - schema.items.also { item0 -> - item0.type shouldBe "array" - item0.`$ref` shouldBe null - item0.items.also { item1 -> - item1.type shouldBe "array" - item1.`$ref` shouldBe null - item1.items.also { item2 -> - item2.type shouldBe "boolean" - } - } - } - } - schemaContext.getComponentsSection().also { components -> - components.shouldBeEmpty() - } - } - - "object" { - val routes = listOf( - route { - request { - body() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(SimpleDataClass::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/SimpleDataClass" - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass") - components["SimpleDataClass"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") - } - } - } - - "object array" { - val routes = listOf( - route { - request { - body>() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(getType>()).also { schema -> - schema.type shouldBe "array" - schema.`$ref` shouldBe null - schema.items.also { item -> - item.type shouldBe null - item.`$ref` shouldBe "#/components/schemas/SimpleDataClass" - } - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass") - components["SimpleDataClass"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") - } - } - } - - "nested objects" { - val routes = listOf( - route { - request { - body() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(DataWrapper::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/DataWrapper" - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass", "DataWrapper") - components["SimpleDataClass"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") - } - components["DataWrapper"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("data", "enabled") - schema.properties["data"]?.also { nestedSchema -> - nestedSchema.type shouldBe null - nestedSchema.`$ref` shouldBe "#/components/schemas/SimpleDataClass" - } - } - } - } - - "simple enum" { - val routes = listOf( - route { - request { - body() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(SimpleEnum::class.asSchemaType()).also { schema -> - schema.type shouldBe "string" - schema.enum shouldContainExactlyInAnyOrder SimpleEnum.values().map { it.name } - schema.`$ref` shouldBe null - } - schemaContext.getComponentsSection().also { components -> - components.keys.shouldBeEmpty() - } - } - - "maps" { - val routes = listOf( - route { - request { - body() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(DataClassWithMaps::class.asSchemaType()).also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/DataClassWithMaps" - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf( - "DataClassWithMaps", - "Map(String,Long)", - "Map(String,String)" - ) - components["DataClassWithMaps"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("mapStringValues", "mapLongValues") - schema.properties["mapStringValues"]?.also { nestedSchema -> - nestedSchema.type shouldBe null - nestedSchema.`$ref` shouldBe "#/components/schemas/Map(String,String)" - } - schema.properties["mapLongValues"]?.also { nestedSchema -> - nestedSchema.type shouldBe null - nestedSchema.`$ref` shouldBe "#/components/schemas/Map(String,Long)" - } - } - } - } - - "custom schema object" { - val config = PluginConfigDsl().also { - it.customSchemas { - openApi("myCustomSchema") { - Schema().also { schema -> - schema.type = "object" - schema.properties = mapOf( - "custom" to Schema().also { prop -> - prop.type = "string" - } - ) - } - } - } - } - val routes = listOf( - route { - request { - body(custom("myCustomSchema")) - } - } - ) - val schemaContext = schemaContext(routes, config) - schemaContext.getSchema("myCustomSchema").also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf( - "myCustomSchema", - ) - components["myCustomSchema"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("custom") - } - } - } - - "custom schema array" { - val config = PluginConfigDsl().also { - it.customSchemas { - openApi("myCustomSchema") { - Schema().also { schema -> - schema.type = "object" - schema.properties = mapOf( - "custom" to Schema().also { prop -> - prop.type = "string" - } - ) - } - } - } - } - val routes = listOf( - route { - request { - body(multipleOf(custom("myCustomSchema"))) - } - } - ) - val schemaContext = schemaContext(routes, config) - schemaContext.getSchema("myCustomSchema").also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf( - "myCustomSchema", - ) - components["myCustomSchema"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("custom") - } - } - } - - "unwrap inlined array schema" { - val generator = SchemaGenerator( - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.INLINE_ALL_SCHEMAS) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .build() - ) - val config = PluginConfigDsl().also { - it.encoding { - schemaEncoder { type -> - generator.generateSchema(type.javaType).toString() - } - } - } - val routes = listOf( - route { - request { - body>() - } - } - ) - val schemaContext = schemaContext(routes, config) - schemaContext.getSchema(getSchemaType>()).also { schema -> - schema.type shouldBe "array" - schema.`$ref` shouldBe null - schema.items - .also { it shouldNotBe null } - ?.also { items -> - items.`$ref` shouldBe "#/components/schemas/SimpleDataClass" - } - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf( - "SimpleDataClass", - ) - components["SimpleDataClass"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("number", "text") - } - } - } - - "don't include unused custom schema" { - val config = PluginConfigDsl().also { - it.customSchemas { - includeAll = false - openApi("myCustomSchema") { - Schema().also { schema -> - schema.type = "object" - schema.properties = mapOf( - "custom" to Schema().also { prop -> - prop.type = "string" - } - ) - } - } - } - } - val schemaContext = schemaContext(emptyList(), config) - schemaContext.getSchemaOrNull("myCustomSchema") shouldBe null - schemaContext.getComponentsSection().also { components -> - components.keys shouldHaveSize 0 - } - } - - "include unused custom schema" { - val config = PluginConfigDsl().also { - it.customSchemas { - includeAll = true - openApi("myCustomSchema") { - Schema().also { schema -> - schema.type = "object" - schema.properties = mapOf( - "custom" to Schema().also { prop -> - prop.type = "string" - } - ) - } - } - } - } - val schemaContext = schemaContext(emptyList(), config) - schemaContext.getSchema("myCustomSchema").also { schema -> - schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf( - "myCustomSchema", - ) - components["myCustomSchema"]?.also { schema -> - schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("custom") - } - } - } - - "array with wildcard-generic" { - val routes = listOf( - route { - request { - body>() - } - } - ) - val schemaContext = schemaContext(routes) - schemaContext.getSchema(getType>()).also { schema -> - schema.type shouldBe "array" - schema.`$ref` shouldBe null - schema.items.also { item -> - item.`$ref` shouldBe "#/components/schemas/*" - } - } - schemaContext.getComponentsSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("*") - components["*"]?.also { schema -> - schema.type shouldBe "object" - } - } - } - -}) { - - companion object { - - inline fun getType() = getSchemaType() - - private val defaultPluginConfig = PluginConfigDsl() - - private fun schemaContext( - routes: Collection, - pluginConfig: PluginConfigDsl = defaultPluginConfig - ): SchemaContext { - val pluginConfigData = pluginConfig.build(PluginConfigData.DEFAULT) - return SchemaContextBuilder( - config = pluginConfigData, - schemaBuilder = SchemaBuilder( - definitionsField = pluginConfigData.encoding.schemaDefsField, - schemaEncoder = pluginConfigData.encoding.schemaEncoder, - ObjectMapper(), - TypeOverwrites.get() - ) - ).build(routes) - } - - fun route(block: OpenApiRoute.() -> Unit): RouteMeta { - return RouteMeta( - path = "/test", - method = HttpMethod.Get, - documentation = OpenApiRoute().apply(block), - protected = false - ) - } - - private data class QueryParamType(val value: String) - - private data class PathParamType(val value: String) - private data class HeaderParamType(val value: String) - private data class RequestBodyType(val value: String) - private data class ResponseHeaderType(val value: String) - private data class ResponseBodyType(val value: String) - - private data class SimpleDataClass( - val text: String, - val number: Int - ) - - private data class DataWrapper( - val enabled: Boolean, - val data: SimpleDataClass - ) - - private enum class SimpleEnum { - RED, GREEN, BLUE - } - - private data class DataClassWithMaps( - val mapStringValues: Map, - val mapLongValues: Map - ) - - } - -}