Merge pull request #75 from usbharu/feature/post-with-media

Feature/post with media
This commit is contained in:
usbharu 2023-10-11 11:09:18 +09:00 committed by GitHub
commit 03498d05fa
86 changed files with 3055 additions and 218 deletions

View File

@ -1,6 +1,6 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
import kotlin.math.min
import kotlin.math.max
val ktor_version: String by project
val kotlin_version: String by project
@ -29,7 +29,7 @@ version = "0.0.1"
tasks.withType<Test> {
useJUnitPlatform()
val cpus = Runtime.getRuntime().availableProcessors()
maxParallelForks = min(1, cpus - 1)
maxParallelForks = max(1, cpus - 1)
setForkEvery(4)
}
@ -60,8 +60,14 @@ tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask:
configOptions.put("interfaceOnly", "true")
configOptions.put("useSpringBoot3", "true")
additionalProperties.put("useTags", "true")
importMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
typeMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
schemaMappings.put(
"StatusesRequest",
"dev.usbharu.hideout.domain.model.mastodon.StatusesRequest"
)
templateDir.set("$rootDir/templates")
}
repositories {
@ -150,7 +156,7 @@ detekt {
parallel = true
config = files("detekt.yml")
buildUponDefaultConfig = true
basePath = rootDir.absolutePath
basePath = "${rootDir.absolutePath}/src/"
autoCorrect = true
}

View File

@ -4,6 +4,7 @@ build:
Indentation: 0
MagicNumber: 0
InjectDispatcher: 0
EnumEntryNameCase: 0
style:
ClassOrdering:
@ -161,3 +162,7 @@ potential-bugs:
HasPlatformType:
active: false
coroutines:
RedundantSuspendModifier:
active: false

View File

@ -0,0 +1,6 @@
package dev.usbharu.hideout.config
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class JsonOrFormBind

View File

@ -0,0 +1,54 @@
package dev.usbharu.hideout.config
import org.slf4j.LoggerFactory
import org.springframework.core.MethodParameter
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
@Suppress("TooGenericExceptionCaught")
class JsonOrFormModelMethodProcessor(
private val modelAttributeMethodProcessor: ModelAttributeMethodProcessor,
private val requestResponseBodyMethodProcessor: RequestResponseBodyMethodProcessor
) : HandlerMethodArgumentResolver {
private val isJsonRegex = Regex("application/((\\w*)\\+)?json")
override fun supportsParameter(parameter: MethodParameter): Boolean =
parameter.hasParameterAnnotation(JsonOrFormBind::class.java)
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Any? {
val contentType = webRequest.getHeader("Content-Type").orEmpty()
logger.trace("ContentType is {}", contentType)
if (contentType.contains(isJsonRegex)) {
logger.trace("Determine content type as json.")
return requestResponseBodyMethodProcessor.resolveArgument(
parameter,
mavContainer,
webRequest,
binderFactory
)
}
return try {
modelAttributeMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory)
} catch (ignore: Exception) {
try {
requestResponseBodyMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory)
} catch (e: Exception) {
logger.warn("Failed to bind request", e)
}
}
}
companion object {
val logger = LoggerFactory.getLogger(JsonOrFormModelMethodProcessor::class.java)
}
}

View File

@ -0,0 +1,29 @@
package dev.usbharu.hideout.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor
@Configuration
class MvcConfigurer(private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(jsonOrFormModelMethodProcessor)
}
}
@Configuration
class JsonOrFormModelMethodProcessorConfig {
@Bean
fun jsonOrFormModelMethodProcessor(converter: List<HttpMessageConverter<*>>): JsonOrFormModelMethodProcessor {
return JsonOrFormModelMethodProcessor(
ServletModelAttributeMethodProcessor(true),
RequestResponseBodyMethodProcessor(
converter
)
)
}
}

View File

@ -35,7 +35,7 @@ import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.util.*
@EnableWebSecurity(debug = true)
@EnableWebSecurity(debug = false)
@Configuration
class SecurityConfig {

View File

@ -2,7 +2,9 @@ package dev.usbharu.hideout.config
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.filter.CommonsRequestLoggingFilter
import java.net.URL
@Configuration
@ -13,6 +15,17 @@ class SpringConfig {
@Autowired
lateinit var storageConfig: StorageConfig
@Bean
fun requestLoggingFilter(): CommonsRequestLoggingFilter {
val loggingFilter = CommonsRequestLoggingFilter()
loggingFilter.setIncludeHeaders(true)
loggingFilter.setIncludeClientInfo(true)
loggingFilter.setIncludeQueryString(true)
loggingFilter.setIncludePayload(true)
loggingFilter.setMaxPayloadLength(64000)
return loggingFilter
}
}
@ConfigurationProperties("hideout")

View File

@ -2,7 +2,7 @@ package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.StatusApi
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest
import dev.usbharu.hideout.domain.model.mastodon.StatusesRequest
import dev.usbharu.hideout.service.api.mastodon.StatusesApiService
import kotlinx.coroutines.runBlocking
import org.springframework.http.HttpStatus
@ -10,17 +10,20 @@ import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ModelAttribute
@Controller
class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
override fun apiV1StatusesPost(@ModelAttribute statusesRequest: StatusesRequest): ResponseEntity<Status> =
runBlocking {
override fun apiV1StatusesPost(devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest): ResponseEntity<Status> {
return runBlocking {
val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt
ResponseEntity(
statusesApiService.postStatus(statusesRequest, jwt.getClaim<String>("uid").toLong()),
statusesApiService.postStatus(
devUsbharuHideoutDomainModelMastodonStatusesRequest,
jwt.getClaim<String>("uid").toLong()
),
HttpStatus.OK
)
}
}
}

View File

@ -0,0 +1,43 @@
package dev.usbharu.hideout.domain.model.ap
open class Document : Object {
var mediaType: String? = null
var url: String? = null
protected constructor() : super()
constructor(
type: List<String> = emptyList(),
name: String? = null,
mediaType: String,
url: String
) : super(
type = add(type, "Document"),
name = name,
actor = null,
id = null
) {
this.mediaType = mediaType
this.url = url
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Document) return false
if (!super.equals(other)) return false
if (mediaType != other.mediaType) return false
if (url != other.url) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (mediaType?.hashCode() ?: 0)
result = 31 * result + (url?.hashCode() ?: 0)
return result
}
override fun toString(): String = "Document(mediaType=$mediaType, url=$url) ${super.toString()}"
}

View File

@ -2,6 +2,7 @@ package dev.usbharu.hideout.domain.model.ap
open class Note : Object {
var attributedTo: String? = null
var attachment: List<Document> = emptyList()
var content: String? = null
var published: String? = null
var to: List<String> = emptyList()
@ -22,7 +23,8 @@ open class Note : Object {
to: List<String> = emptyList(),
cc: List<String> = emptyList(),
sensitive: Boolean = false,
inReplyTo: String? = null
inReplyTo: String? = null,
attachment: List<Document> = emptyList()
) : super(
type = add(type, "Note"),
name = name,
@ -35,6 +37,7 @@ open class Note : Object {
this.cc = cc
this.sensitive = sensitive
this.inReplyTo = inReplyTo
this.attachment = attachment
}
override fun equals(other: Any?): Boolean {
@ -42,23 +45,34 @@ open class Note : Object {
if (other !is Note) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (attributedTo != other.attributedTo) return false
if (attachment != other.attachment) return false
if (content != other.content) return false
if (published != other.published) return false
return to == other.to
if (to != other.to) return false
if (cc != other.cc) return false
if (sensitive != other.sensitive) return false
if (inReplyTo != other.inReplyTo) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + (attributedTo?.hashCode() ?: 0)
result = 31 * result + attachment.hashCode()
result = 31 * result + (content?.hashCode() ?: 0)
result = 31 * result + (published?.hashCode() ?: 0)
result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode()
result = 31 * result + sensitive.hashCode()
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
return result
}
override fun toString(): String =
"Note(id=$id, attributedTo=$attributedTo, content=$content, published=$published, to=$to) ${super.toString()}"
override fun toString(): String {
return "Note(attributedTo=$attributedTo, attachment=$attachment, " +
"content=$content, published=$published, to=$to, cc=$cc, sensitive=$sensitive," +
" inReplyTo=$inReplyTo) ${super.toString()}"
}
}

View File

@ -84,7 +84,7 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
ExtendedActivityVocabulary.Service -> TODO()
ExtendedActivityVocabulary.Article -> TODO()
ExtendedActivityVocabulary.Audio -> TODO()
ExtendedActivityVocabulary.Document -> TODO()
ExtendedActivityVocabulary.Document -> p.codec.treeToValue(treeNode, Document::class.java)
ExtendedActivityVocabulary.Event -> TODO()
ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java)
ExtendedActivityVocabulary.Page -> TODO()

View File

@ -8,5 +8,6 @@ data class PostCreateDto(
val visibility: Visibility = Visibility.PUBLIC,
val repostId: Long? = null,
val repolyId: Long? = null,
val userId: Long
val userId: Long,
val mediaIds: List<Long> = emptyList()
)

View File

@ -13,7 +13,8 @@ data class Post private constructor(
val repostId: Long? = null,
val replyId: Long? = null,
val sensitive: Boolean = false,
val apId: String = url
val apId: String = url,
val mediaIds: List<Long> = emptyList()
) {
companion object {
@Suppress("FunctionMinLength", "LongParameterList")
@ -28,7 +29,8 @@ data class Post private constructor(
repostId: Long? = null,
replyId: Long? = null,
sensitive: Boolean = false,
apId: String = url
apId: String = url,
mediaIds: List<Long> = emptyList()
): Post {
val characterLimit = Config.configData.characterLimit
@ -67,7 +69,8 @@ data class Post private constructor(
repostId = repostId,
replyId = replyId,
sensitive = sensitive,
apId = apId
apId = apId,
mediaIds = mediaIds
)
}
}

View File

@ -18,6 +18,7 @@ object DeliverPostJob : HideoutJob("DeliverPostJob") {
val post: Prop<DeliverPostJob, String> = string("post")
val actor: Prop<DeliverPostJob, String> = string("actor")
val inbox: Prop<DeliverPostJob, String> = string("inbox")
val media: Prop<DeliverPostJob, String> = string("media")
}
@Component

View File

@ -0,0 +1,77 @@
package dev.usbharu.hideout.domain.model.mastodon
import com.fasterxml.jackson.annotation.JsonProperty
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequestPoll
@Suppress("VariableNaming")
class StatusesRequest {
@JsonProperty("status")
var status: String? = null
@JsonProperty("media_ids")
var media_ids: List<String> = emptyList()
@JsonProperty("poll")
var poll: StatusesRequestPoll? = null
@JsonProperty("in_reply_to_id")
var in_reply_to_id: String? = null
@JsonProperty("sensitive")
var sensitive: Boolean? = null
@JsonProperty("spoiler_text")
var spoiler_text: String? = null
@JsonProperty("visibility")
var visibility: Visibility? = null
@JsonProperty("language")
var language: String? = null
@JsonProperty("scheduled_at")
var scheduled_at: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is StatusesRequest) return false
if (status != other.status) return false
if (media_ids != other.media_ids) return false
if (poll != other.poll) return false
if (in_reply_to_id != other.in_reply_to_id) return false
if (sensitive != other.sensitive) return false
if (spoiler_text != other.spoiler_text) return false
if (visibility != other.visibility) return false
if (language != other.language) return false
if (scheduled_at != other.scheduled_at) return false
return true
}
override fun hashCode(): Int {
var result = status?.hashCode() ?: 0
result = 31 * result + media_ids.hashCode()
result = 31 * result + (poll?.hashCode() ?: 0)
result = 31 * result + (in_reply_to_id?.hashCode() ?: 0)
result = 31 * result + (sensitive?.hashCode() ?: 0)
result = 31 * result + (spoiler_text?.hashCode() ?: 0)
result = 31 * result + (visibility?.hashCode() ?: 0)
result = 31 * result + (language?.hashCode() ?: 0)
result = 31 * result + (scheduled_at?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "StatusesRequest(status=$status, mediaIds=$media_ids, poll=$poll, inReplyToId=$in_reply_to_id, " +
"sensitive=$sensitive, spoilerText=$spoiler_text, visibility=$visibility, language=$language," +
" scheduledAt=$scheduled_at)"
}
@Suppress("EnumNaming")
enum class Visibility {
`public`,
unlisted,
private,
direct;
}
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.Media
interface MediaQueryService {
suspend fun findByPostId(postId: Long): List<Media>
}

View File

@ -0,0 +1,17 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.Media
import dev.usbharu.hideout.repository.PostsMedia
import dev.usbharu.hideout.repository.toMedia
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select
import org.springframework.stereotype.Repository
@Repository
class MediaQueryServiceImpl : MediaQueryService {
override suspend fun findByPostId(postId: Long): List<Media> {
return dev.usbharu.hideout.repository.Media.innerJoin(PostsMedia, onColumn = { id }, otherColumn = { mediaId })
.select { PostsMedia.postId eq postId }
.map { it.toMedia() }
}
}

View File

@ -3,20 +3,29 @@ package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.PostsMedia
import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select
import org.springframework.stereotype.Repository
@Repository
class PostQueryServiceImpl : PostQueryService {
override suspend fun findById(id: Long): Post =
Posts.select { Posts.id eq id }
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId })
.select { Posts.id eq id }
.singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toPost()
override suspend fun findByUrl(url: String): Post = Posts.select { Posts.url eq url }
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }.toPost()
override suspend fun findByUrl(url: String): Post =
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId })
.select { Posts.url eq url }
.toPost()
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }
override suspend fun findByApId(string: String): Post = Posts.select { Posts.apId eq string }
.singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) }.toPost()
override suspend fun findByApId(string: String): Post =
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId })
.select { Posts.apId eq string }
.toPost()
.singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) }
}

View File

@ -0,0 +1,61 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.Account
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Reactions
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toReaction
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.springframework.stereotype.Repository
@Repository
class ReactionQueryServiceImpl : ReactionQueryService {
override suspend fun findByPostId(postId: Long, userId: Long?): List<Reaction> {
return Reactions.select {
Reactions.postId.eq(postId)
}.map { it.toReaction() }
}
@Suppress("FunctionMaxLength")
override suspend fun findByPostIdAndUserIdAndEmojiId(postId: Long, userId: Long, emojiId: Long): Reaction {
return Reactions
.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}
.singleOr {
FailedToGetResourcesException(
"postId: $postId,userId: $userId,emojiId: $emojiId is duplicate or does not exist.",
it
)
}
.toReaction()
}
override suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean {
return Reactions.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}.empty().not()
}
override suspend fun deleteByPostIdAndUserId(postId: Long, userId: Long) {
Reactions.deleteWhere { Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)) }
}
override suspend fun findByPostIdWithUsers(postId: Long, userId: Long?): List<ReactionResponse> {
return Reactions
.leftJoin(Users, onColumn = { Reactions.userId }, otherColumn = { id })
.select { Reactions.postId.eq(postId) }
.groupBy { _: ResultRow -> ReactionResponse("", true, "", emptyList()) }
.map { entry: Map.Entry<ReactionResponse, List<ResultRow>> ->
entry.key.copy(accounts = entry.value.map { Account(it[Users.screenName], "", it[Users.url]) })
}
}
}

View File

@ -1,9 +1,11 @@
package dev.usbharu.hideout.query.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.repository.*
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select
import org.springframework.stereotype.Repository
@ -12,67 +14,21 @@ import java.time.Instant
@Repository
class StatusQueryServiceImpl : StatusQueryService {
@Suppress("LongMethod")
override suspend fun findByPostIds(ids: List<Long>): List<Status> {
val pairs = Posts.innerJoin(Users, onColumn = { userId }, otherColumn = { id })
override suspend fun findByPostIds(ids: List<Long>): List<Status> = findByPostIdsWithMediaAttachments(ids)
@Suppress("unused")
private suspend fun internalFindByPostIds(ids: List<Long>): List<Status> {
val pairs = Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.select { Posts.id inList ids }
.map {
Status(
id = it[Posts.id].toString(),
uri = it[Posts.apId],
createdAt = Instant.ofEpochMilli(it[Posts.createdAt]).toString(),
account = Account(
id = it[Users.id].toString(),
username = it[Users.name],
acct = "${it[Users.name]}@${it[Users.domain]}",
url = it[Users.url],
displayName = it[Users.screenName],
note = it[Users.description],
avatar = it[Users.url] + "/icon.jpg",
avatarStatic = it[Users.url] + "/icon.jpg",
header = it[Users.url] + "/header.jpg",
headerStatic = it[Users.url] + "/header.jpg",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
createdAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(),
lastStatusAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(),
statusesCount = 0,
followersCount = 0,
followingCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false
),
content = it[Posts.text],
visibility = when (it[Posts.visibility]) {
0 -> Status.Visibility.public
1 -> Status.Visibility.unlisted
2 -> Status.Visibility.private
3 -> Status.Visibility.direct
else -> Status.Visibility.public
},
sensitive = it[Posts.sensitive],
spoilerText = it[Posts.overview].orEmpty(),
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = it[Posts.apId],
inReplyToId = it[Posts.replyId].toString(),
inReplyToAccountId = null,
language = null,
text = it[Posts.text],
editedAt = null
) to it[Posts.repostId]
toStatus(it) to it[Posts.repostId]
}
return resolveReplyAndRepost(pairs)
}
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
val statuses = pairs.map { it.first }
return pairs
.map {
@ -90,4 +46,95 @@ class StatusQueryServiceImpl : StatusQueryService {
}
}
}
@Suppress("FunctionMaxLength")
private suspend fun findByPostIdsWithMediaAttachments(ids: List<Long>): List<Status> {
val pairs = Posts
.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId })
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.innerJoin(Media, onColumn = { PostsMedia.mediaId }, otherColumn = { id })
.select { Posts.id inList ids }
.groupBy { it[Posts.id] }
.map { it.value }
.map {
toStatus(it.first()).copy(
mediaAttachments = it.map {
it.toMedia().let {
MediaAttachment(
id = it.id.toString(),
type = when (it.type) {
FileType.Image -> MediaAttachment.Type.image
FileType.Video -> MediaAttachment.Type.video
FileType.Audio -> MediaAttachment.Type.audio
FileType.Unknown -> MediaAttachment.Type.unknown
},
url = it.url,
previewUrl = it.thumbnailUrl,
remoteUrl = it.remoteUrl,
description = "",
blurhash = it.blurHash,
textUrl = it.url
)
}
}
) to it.first()[Posts.repostId]
}
return resolveReplyAndRepost(pairs)
}
}
private fun toStatus(it: ResultRow) = Status(
id = it[Posts.id].toString(),
uri = it[Posts.apId],
createdAt = Instant.ofEpochMilli(it[Posts.createdAt]).toString(),
account = Account(
id = it[Users.id].toString(),
username = it[Users.name],
acct = "${it[Users.name]}@${it[Users.domain]}",
url = it[Users.url],
displayName = it[Users.screenName],
note = it[Users.description],
avatar = it[Users.url] + "/icon.jpg",
avatarStatic = it[Users.url] + "/icon.jpg",
header = it[Users.url] + "/header.jpg",
headerStatic = it[Users.url] + "/header.jpg",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
createdAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(),
lastStatusAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(),
statusesCount = 0,
followersCount = 0,
followingCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false
),
content = it[Posts.text],
visibility = when (it[Posts.visibility]) {
0 -> Status.Visibility.public
1 -> Status.Visibility.unlisted
2 -> Status.Visibility.private
3 -> Status.Visibility.direct
else -> Status.Visibility.public
},
sensitive = it[Posts.sensitive],
spoilerText = it[Posts.overview].orEmpty(),
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = it[Posts.apId],
inReplyToId = it[Posts.replyId].toString(),
inReplyToAccountId = null,
language = null,
text = it[Posts.text],
editedAt = null
)

View File

@ -55,18 +55,18 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me
Media.id eq id
}
}
}
fun ResultRow.toMedia(): EntityMedia {
return EntityMedia(
id = this[Media.id],
name = this[Media.name],
url = this[Media.url],
remoteUrl = this[Media.remoteUrl],
thumbnailUrl = this[Media.thumbnailUrl],
type = FileType.values().first { it.ordinal == this[Media.type] },
blurHash = this[Media.blurhash],
)
}
fun ResultRow.toMedia(): EntityMedia {
return EntityMedia(
id = this[Media.id],
name = this[Media.name],
url = this[Media.url],
remoteUrl = this[Media.remoteUrl],
thumbnailUrl = this[Media.thumbnailUrl],
type = FileType.values().first { it.ordinal == this[Media.type] },
blurHash = this[Media.blurhash],
)
}
object Media : Table("media") {
@ -77,4 +77,5 @@ object Media : Table("media") {
val thumbnailUrl = varchar("thumbnail_url", 255).nullable()
val type = integer("type")
val blurhash = varchar("blurhash", 255).nullable()
override val primaryKey = PrimaryKey(id)
}

View File

@ -29,7 +29,18 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos
it[sensitive] = post.sensitive
it[apId] = post.apId
}
PostsMedia.batchInsert(post.mediaIds) {
this[PostsMedia.postId] = post.id
this[PostsMedia.mediaId] = it
}
} else {
PostsMedia.deleteWhere {
PostsMedia.postId eq post.id
}
PostsMedia.batchInsert(post.mediaIds) {
this[PostsMedia.postId] = post.id
this[PostsMedia.mediaId] = it
}
Posts.update({ Posts.id eq post.id }) {
it[userId] = post.userId
it[overview] = post.overview
@ -46,8 +57,12 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos
return post
}
override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.singleOrNull()?.toPost()
?: throw FailedToGetResourcesException("id: $id was not found.")
override suspend fun findById(id: Long): Post =
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId })
.select { Posts.id eq id }
.toPost()
.singleOrNull()
?: throw FailedToGetResourcesException("id: $id was not found.")
override suspend fun delete(id: Long) {
Posts.deleteWhere { Posts.id eq id }
@ -69,6 +84,12 @@ object Posts : Table() {
override val primaryKey: PrimaryKey = PrimaryKey(id)
}
object PostsMedia : Table() {
val postId = long("post_id").references(Posts.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
override val primaryKey = PrimaryKey(postId, mediaId)
}
fun ResultRow.toPost(): Post {
return Post.of(
id = this[Posts.id],
@ -81,6 +102,12 @@ fun ResultRow.toPost(): Post {
repostId = this[Posts.repostId],
replyId = this[Posts.replyId],
sensitive = this[Posts.sensitive],
apId = this[Posts.apId]
apId = this[Posts.apId],
)
}
fun Query.toPost(): List<Post> {
return this.groupBy { it[Posts.id] }
.map { it.value }
.map { it.first().toPost().copy(mediaIds = it.map { it[PostsMedia.mediaId] }) }
}

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.ap.Create
import dev.usbharu.hideout.domain.model.ap.Document
import dev.usbharu.hideout.domain.model.ap.Note
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
@ -13,6 +14,7 @@ import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.plugins.getAp
import dev.usbharu.hideout.plugins.postAp
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.MediaQueryService
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.PostRepository
@ -46,6 +48,7 @@ class APNoteServiceImpl(
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val postQueryService: PostQueryService,
private val mediaQueryService: MediaQueryService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val applicationConfig: ApplicationConfig,
private val postService: PostService
@ -62,11 +65,13 @@ class APNoteServiceImpl(
val followers = followerQueryService.findFollowersById(post.userId)
val userEntity = userQueryService.findById(post.userId)
val note = objectMapper.writeValueAsString(post)
val mediaList = objectMapper.writeValueAsString(mediaQueryService.findByPostId(post.id))
followers.forEach { followerEntity ->
jobQueueParentService.schedule(DeliverPostJob) {
props[DeliverPostJob.actor] = userEntity.url
props[DeliverPostJob.post] = note
props[DeliverPostJob.inbox] = followerEntity.inbox
props[DeliverPostJob.media] = mediaList
}
}
}
@ -74,13 +79,19 @@ class APNoteServiceImpl(
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor]
val postEntity = objectMapper.readValue<Post>(props[DeliverPostJob.post])
val mediaList =
objectMapper.readValue<List<dev.usbharu.hideout.domain.model.hideout.entity.Media>>(
props[DeliverPostJob.media]
)
val note = Note(
name = "Note",
id = postEntity.url,
attributedTo = actor,
content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf(public, "$actor/follower")
to = listOf(public, "$actor/follower"),
attachment = mediaList.map { Document(mediaType = "image/jpeg", url = it.url) }
)
val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
@ -168,6 +179,7 @@ class APNoteServiceImpl(
postQueryService.findByUrl(it)
}
// TODO: リモートのメディア処理を追加
postService.createRemote(
Post.of(
id = postRepository.generateId(),

View File

@ -1,12 +1,15 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.domain.model.mastodon.StatusesRequest
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.MediaRepository
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.mastodon.AccountService
import dev.usbharu.hideout.service.post.PostService
@ -15,7 +18,10 @@ import java.time.Instant
@Service
interface StatusesApiService {
suspend fun postStatus(statusesRequest: StatusesRequest, userId: Long): Status
suspend fun postStatus(
statusesRequest: dev.usbharu.hideout.domain.model.mastodon.StatusesRequest,
userId: Long
): Status
}
@Service
@ -24,11 +30,16 @@ class StatsesApiServiceImpl(
private val accountService: AccountService,
private val postQueryService: PostQueryService,
private val userQueryService: UserQueryService,
private val mediaRepository: MediaRepository,
private val transaction: Transaction
) :
StatusesApiService {
@Suppress("LongMethod")
override suspend fun postStatus(statusesRequest: StatusesRequest, userId: Long): Status = transaction.transaction {
@Suppress("LongMethod", "CyclomaticComplexMethod")
override suspend fun postStatus(
statusesRequest: dev.usbharu.hideout.domain.model.mastodon.StatusesRequest,
userId: Long
): Status = transaction.transaction {
println("Post status media ids " + statusesRequest.media_ids)
val visibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Visibility.PUBLIC
StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED
@ -40,10 +51,11 @@ class StatsesApiServiceImpl(
val post = postService.createLocal(
PostCreateDto(
text = statusesRequest.status.orEmpty(),
overview = statusesRequest.spoilerText,
overview = statusesRequest.spoiler_text,
visibility = visibility,
repolyId = statusesRequest.inReplyToId?.toLongOrNull(),
userId = userId
repolyId = statusesRequest.in_reply_to_id?.toLongOrNull(),
userId = userId,
mediaIds = statusesRequest.media_ids.map { it.toLong() }
)
)
val account = accountService.findById(userId)
@ -66,6 +78,27 @@ class StatsesApiServiceImpl(
null
}
// TODO: n+1解消
val mediaAttachment = post.mediaIds.map { mediaId ->
mediaRepository.findById(mediaId)
}.map {
MediaAttachment(
id = it.id.toString(),
type = when (it.type) {
FileType.Image -> MediaAttachment.Type.image
FileType.Video -> MediaAttachment.Type.video
FileType.Audio -> MediaAttachment.Type.audio
FileType.Unknown -> MediaAttachment.Type.unknown
},
url = it.url,
previewUrl = it.thumbnailUrl,
remoteUrl = it.remoteUrl,
description = "",
blurhash = it.blurHash,
textUrl = it.url
)
}
Status(
id = post.id.toString(),
uri = post.apId,
@ -75,7 +108,7 @@ class StatsesApiServiceImpl(
visibility = postVisibility,
sensitive = post.sensitive,
spoilerText = post.overview.orEmpty(),
mediaAttachments = emptyList(),
mediaAttachments = mediaAttachment,
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
@ -87,7 +120,7 @@ class StatsesApiServiceImpl(
inReplyToAccountId = replyUser?.toString(),
language = null,
text = post.text,
editedAt = null
editedAt = null,
)
}
}

View File

@ -20,6 +20,7 @@ import javax.imageio.ImageIO
import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia
@Service
@Suppress("TooGenericExceptionCaught")
class MediaServiceImpl(
private val mediaDataStore: MediaDataStore,
private val fileTypeDeterminationService: FileTypeDeterminationService,

View File

@ -29,7 +29,7 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
.key(thumbnailKey)
.build()
val pairList = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) {
awaitAll(
async {
if (dataMediaSave.thumbnailInputStream != null) {
@ -37,23 +37,23 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
thumbnailUploadRequest,
RequestBody.fromBytes(dataMediaSave.thumbnailInputStream)
)
"thumbnail" to s3Client.utilities()
s3Client.utilities()
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(thumbnailKey).build())
} else {
"thumbnail" to null
null
}
},
async {
s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream))
"file" to s3Client.utilities()
s3Client.utilities()
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(dataMediaSave.name).build())
}
)
}.toMap()
}
return SuccessSavedMedia(
dataMediaSave.name,
pairList.getValue("file").toString(),
pairList.getValue("thumbnail").toString()
name = dataMediaSave.name,
url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataMediaSave.name}",
thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey"
)
}

View File

@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
@Suppress("TooGenericExceptionCaught")
class MediaProcessServiceImpl(
private val mediaConverterRoot: MediaConverterRoot,
private val thumbnailGenerateService: ThumbnailGenerateService

View File

@ -44,7 +44,8 @@ class PostServiceImpl(
text = post.text,
createdAt = Instant.now().toEpochMilli(),
visibility = post.visibility,
url = "${user.url}/posts/$id"
url = "${user.url}/posts/$id",
mediaIds = post.mediaIds
)
return internalCreate(createPost, isLocal)
}

View File

@ -13,4 +13,5 @@
<logger name="Exposed" level="INFO"/>
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
<logger name="org.springframework.security" level="DEBUG"/>
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG"/>
</configuration>

View File

@ -10,6 +10,7 @@ import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.MediaQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.*
@ -19,6 +20,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Test
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq
import org.mockito.kotlin.*
import utils.JsonObjectMapper
@ -29,118 +31,131 @@ import kotlin.test.assertEquals
class APNoteServiceImplTest {
@Test
fun `createPost 新しい投稿`() = runTest {
val followers = listOf(
User.of(
2L,
"follower",
"follower.example.com",
"followerUser",
"test follower user",
"https://follower.example.com/inbox",
"https://follower.example.com/outbox",
"https://follower.example.com",
"https://follower.example.com",
publicKey = "",
createdAt = Instant.now()
),
User.of(
3L,
"follower2",
"follower2.example.com",
"follower2User",
"test follower2 user",
"https://follower2.example.com/inbox",
"https://follower2.example.com/outbox",
"https://follower2.example.com",
"https://follower2.example.com",
publicKey = "",
createdAt = Instant.now()
fun `createPost 新しい投稿`() {
val mediaQueryService = mock<MediaQueryService> {
onBlocking { findByPostId(anyLong()) } doReturn emptyList()
}
runTest {
val followers = listOf(
User.of(
2L,
"follower",
"follower.example.com",
"followerUser",
"test follower user",
"https://follower.example.com/inbox",
"https://follower.example.com/outbox",
"https://follower.example.com",
"https://follower.example.com",
publicKey = "",
createdAt = Instant.now()
),
User.of(
3L,
"follower2",
"follower2.example.com",
"follower2User",
"test follower2 user",
"https://follower2.example.com/inbox",
"https://follower2.example.com/outbox",
"https://follower2.example.com",
"https://follower2.example.com",
publicKey = "",
createdAt = Instant.now()
)
)
)
val userQueryService = mock<UserQueryService> {
onBlocking { findById(eq(1L)) } doReturn User.of(
val userQueryService = mock<UserQueryService> {
onBlocking { findById(eq(1L)) } doReturn User.of(
1L,
"test",
"example.com",
"testUser",
"test user",
"a",
"https://example.com/inbox",
"https://example.com/outbox",
"https://example.com",
publicKey = "",
privateKey = "a",
createdAt = Instant.now()
)
}
val followerQueryService = mock<FollowerQueryService> {
onBlocking { findFollowersById(eq(1L)) } doReturn followers
}
val jobQueueParentService = mock<JobQueueParentService>()
val activityPubNoteService =
APNoteServiceImpl(
httpClient = mock(),
jobQueueParentService = jobQueueParentService,
postRepository = mock(),
apUserService = mock(),
userQueryService = userQueryService,
followerQueryService = followerQueryService,
postQueryService = mock(),
objectMapper = objectMapper,
applicationConfig = testApplicationConfig,
postService = mock(),
mediaQueryService = mediaQueryService
)
val postEntity = Post.of(
1L,
"test",
"example.com",
"testUser",
"test user",
"a",
"https://example.com/inbox",
"https://example.com/outbox",
"https://example.com",
publicKey = "",
privateKey = "a",
createdAt = Instant.now()
1L,
null,
"test text",
1L,
Visibility.PUBLIC,
"https://example.com"
)
activityPubNoteService.createNote(postEntity)
verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any())
}
val followerQueryService = mock<FollowerQueryService> {
onBlocking { findFollowersById(eq(1L)) } doReturn followers
}
val jobQueueParentService = mock<JobQueueParentService>()
val activityPubNoteService =
APNoteServiceImpl(
httpClient = mock(),
jobQueueParentService = jobQueueParentService,
}
@Test
fun `createPostJob 新しい投稿のJob`() {
runTest {
val mediaQueryService = mock<MediaQueryService> {
onBlocking { findByPostId(anyLong()) } doReturn emptyList()
}
Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper)
val httpClient = HttpClient(
MockEngine { httpRequestData ->
assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString())
respondOk()
}
)
val activityPubNoteService = APNoteServiceImpl(
httpClient = httpClient,
jobQueueParentService = mock(),
postRepository = mock(),
apUserService = mock(),
userQueryService = userQueryService,
followerQueryService = followerQueryService,
userQueryService = mock(),
followerQueryService = mock(),
postQueryService = mock(),
objectMapper = objectMapper,
applicationConfig = testApplicationConfig,
postService = mock(),
mediaQueryService = mediaQueryService
)
val postEntity = Post.of(
1L,
1L,
null,
"test text",
1L,
Visibility.PUBLIC,
"https://example.com"
)
activityPubNoteService.createNote(postEntity)
verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any())
}
@Test
fun `createPostJob 新しい投稿のJob`() = runTest {
Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper)
val httpClient = HttpClient(
MockEngine { httpRequestData ->
assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString())
respondOk()
}
)
val activityPubNoteService = APNoteServiceImpl(
httpClient = httpClient,
jobQueueParentService = mock(),
postRepository = mock(),
apUserService = mock(),
userQueryService = mock(),
followerQueryService = mock(),
postQueryService = mock(),
objectMapper = objectMapper,
applicationConfig = testApplicationConfig,
postService = mock(),
)
activityPubNoteService.createNoteJob(
JobProps(
data = mapOf<String, Any>(
DeliverPostJob.actor.name to "https://follower.example.com",
DeliverPostJob.post.name to """{
"id": 1,
"userId": 1,
"text": "test text",
"createdAt": 132525324,
"visibility": 0,
"url": "https://example.com"
}""",
DeliverPostJob.inbox.name to "https://follower.example.com/inbox"
),
json = Json
activityPubNoteService.createNoteJob(
JobProps(
data = mapOf<String, Any>(
DeliverPostJob.actor.name to "https://follower.example.com",
DeliverPostJob.post.name to """{
"id": 1,
"userId": 1,
"text": "test text",
"createdAt": 132525324,
"visibility": 0,
"url": "https://example.com"
}""",
DeliverPostJob.inbox.name to "https://follower.example.com/inbox",
DeliverPostJob.media.name to "[]"
),
json = Json
)
)
)
}
}
}

96
templates/api.mustache Normal file
View File

@ -0,0 +1,96 @@
package {{package}}
{{#imports}}import {{import}}
{{/imports}}
{{#swagger2AnnotationLibrary}}
import io.swagger.v3.oas.annotations.*
import io.swagger.v3.oas.annotations.enums.*
import io.swagger.v3.oas.annotations.media.*
import io.swagger.v3.oas.annotations.responses.*
import io.swagger.v3.oas.annotations.security.*
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
import io.swagger.annotations.Api
import io.swagger.annotations.ApiOperation
import io.swagger.annotations.ApiParam
import io.swagger.annotations.ApiResponse
import io.swagger.annotations.ApiResponses
import io.swagger.annotations.Authorization
import io.swagger.annotations.AuthorizationScope
{{/swagger1AnnotationLibrary}}
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
{{#useBeanValidation}}
import org.springframework.validation.annotation.Validated
{{/useBeanValidation}}
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.beans.factory.annotation.Autowired
import dev.usbharu.hideout.config.JsonOrFormBind
{{#useBeanValidation}}
import {{javaxPackage}}.validation.Valid
import {{javaxPackage}}.validation.constraints.DecimalMax
import {{javaxPackage}}.validation.constraints.DecimalMin
import {{javaxPackage}}.validation.constraints.Email
import {{javaxPackage}}.validation.constraints.Max
import {{javaxPackage}}.validation.constraints.Min
import {{javaxPackage}}.validation.constraints.NotNull
import {{javaxPackage}}.validation.constraints.Pattern
import {{javaxPackage}}.validation.constraints.Size
{{/useBeanValidation}}
{{#reactive}}
import kotlinx.coroutines.flow.Flow
{{/reactive}}
import kotlin.collections.List
import kotlin.collections.Map
@RestController{{#beanQualifiers}}("{{package}}.{{classname}}Controller"){{/beanQualifiers}}
{{#useBeanValidation}}
@Validated
{{/useBeanValidation}}
{{#swagger1AnnotationLibrary}}
@Api(value = "{{{baseName}}}", description = "The {{{baseName}}} API")
{{/swagger1AnnotationLibrary}}
{{=<% %>=}}
@RequestMapping("\${api.base-path:<%contextPath%>}")
<%={{ }}=%>
{{#operations}}
class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) val service: {{classname}}Service{{/serviceInterface}}) {
{{#operation}}
{{#swagger2AnnotationLibrary}}
@Operation(
summary = "{{{summary}}}",
operationId = "{{{operationId}}}",
description = """{{{unescapedNotes}}}""",
responses = [{{#responses}}
ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}} ]{{#hasAuthMethods}},
security = [ {{#authMethods}}SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes = [ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} ]{{/isOAuth}}){{^-last}},{{/-last}}{{/authMethods}} ]{{/hasAuthMethods}}
){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiOperation(
value = "{{{summary}}}",
nickname = "{{{operationId}}}",
notes = "{{{notes}}}"{{#returnBaseType}},
response = {{{.}}}::class{{/returnBaseType}}{{#returnContainer}},
responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}},
authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}})
@ApiResponses(
value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}}
@RequestMapping(
method = [RequestMethod.{{httpMethod}}],
value = ["{{#lambda.escapeDoubleQuote}}{{path}}{{/lambda.escapeDoubleQuote}}"]{{#singleContentTypes}}{{#hasProduces}},
produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}},
consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}},
produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}},
consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}}
)
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}): ResponseEntity<{{>returnTypes}}> {
return {{>returnValue}}
}
{{/operation}}
}
{{/operations}}

View File

@ -0,0 +1,25 @@
package {{package}}
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import java.util.Optional
import dev.usbharu.hideout.config.JsonOrFormBind
{{>generatedAnnotation}}
@Controller{{#beanQualifiers}}("{{package}}.{{classname}}Controller"){{/beanQualifiers}}
{{=<% %>=}}
@RequestMapping("\${openapi.<%title%>.base-path:<%>defaultBasePath%>}")
<%={{ }}=%>
{{#operations}}
class {{classname}}Controller(
@org.springframework.beans.factory.annotation.Autowired(required = false) delegate: {{classname}}Delegate?
) : {{classname}} {
private val delegate: {{classname}}Delegate
init {
this.delegate = Optional.ofNullable(delegate).orElse(object : {{classname}}Delegate {})
}
override fun getDelegate(): {{classname}}Delegate = delegate
}
{{/operations}}

View File

@ -0,0 +1,46 @@
package {{package}}
{{#imports}}import {{import}}
{{/imports}}
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.core.io.Resource
{{#reactive}}
import kotlinx.coroutines.flow.Flow
{{/reactive}}
import java.util.Optional
{{#async}}
import java.util.concurrent.CompletableFuture
{{/async}}
{{#operations}}
/**
* A delegate to be called by the {@link {{classname}}Controller}}.
* Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
*/
{{>generatedAnnotation}}
interface {{classname}}Delegate {
fun getRequest(): Optional
<NativeWebRequest> = Optional.empty()
{{#operation}}
/**
* @see {{classname}}#{{operationId}}
*/
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{paramName}}
: {{^isFile}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}
Flow<{{{baseType}}}
>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{/isFile}}{{#isFile}}
Resource?{{/isFile}}{{^-last}},
{{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}}
<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}} {
{{>methodBody}}
}
{{/operation}}
}
{{/operations}}

View File

@ -0,0 +1,112 @@
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package {{package}}
{{#imports}}import {{import}}
{{/imports}}
{{#swagger2AnnotationLibrary}}
import io.swagger.v3.oas.annotations.*
import io.swagger.v3.oas.annotations.enums.*
import io.swagger.v3.oas.annotations.media.*
import io.swagger.v3.oas.annotations.responses.*
import io.swagger.v3.oas.annotations.security.*
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
import io.swagger.annotations.Api
import io.swagger.annotations.ApiOperation
import io.swagger.annotations.ApiParam
import io.swagger.annotations.ApiResponse
import io.swagger.annotations.ApiResponses
import io.swagger.annotations.Authorization
import io.swagger.annotations.AuthorizationScope
{{/swagger1AnnotationLibrary}}
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
{{#useBeanValidation}}
import org.springframework.validation.annotation.Validated
{{/useBeanValidation}}
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.beans.factory.annotation.Autowired
{{#useBeanValidation}}
import {{javaxPackage}}.validation.constraints.DecimalMax
import {{javaxPackage}}.validation.constraints.DecimalMin
import {{javaxPackage}}.validation.constraints.Email
import {{javaxPackage}}.validation.constraints.Max
import {{javaxPackage}}.validation.constraints.Min
import {{javaxPackage}}.validation.constraints.NotNull
import {{javaxPackage}}.validation.constraints.Pattern
import {{javaxPackage}}.validation.constraints.Size
import {{javaxPackage}}.validation.Valid
{{/useBeanValidation}}
{{#reactive}}
import kotlinx.coroutines.flow.Flow
{{/reactive}}
import kotlin.collections.List
import kotlin.collections.Map
import dev.usbharu.hideout.config.JsonOrFormBind
{{#useBeanValidation}}
@Validated
{{/useBeanValidation}}
{{#swagger1AnnotationLibrary}}
@Api(value = "{{{baseName}}}", description = "The {{{baseName}}} API")
{{/swagger1AnnotationLibrary}}
{{^useFeignClient}}
{{=<% %>=}}
@RequestMapping("\${api.base-path:<%contextPath%>}")
<%={{ }}=%>
{{/useFeignClient}}
{{#operations}}
interface {{classname}} {
{{#isDelegate}}
fun getDelegate(): {{classname}}Delegate = object: {{classname}}Delegate {}
{{/isDelegate}}
{{#operation}}
{{#swagger2AnnotationLibrary}}
@Operation(
summary = "{{{summary}}}",
operationId = "{{{operationId}}}",
description = """{{{unescapedNotes}}}""",
responses = [{{#responses}}
ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}}
]{{#hasAuthMethods}},
security = [ {{#authMethods}}SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes = [ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} ]{{/isOAuth}}){{^-last}},{{/-last}}{{/authMethods}} ]{{/hasAuthMethods}}
){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiOperation(
value = "{{{summary}}}",
nickname = "{{{operationId}}}",
notes = "{{{notes}}}"{{#returnBaseType}},
response = {{{.}}}::class{{/returnBaseType}}{{#returnContainer}},
responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}},
authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}})
@ApiResponses(
value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}}, {{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}}
@RequestMapping(
method = [RequestMethod.{{httpMethod}}],
value = ["{{#lambda.escapeDoubleQuote}}{{path}}{{/lambda.escapeDoubleQuote}}"]{{#singleContentTypes}}{{#hasProduces}},
produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}},
consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}},
produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}},
consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}}
)
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}): ResponseEntity<{{>returnTypes}}>{{^skipDefaultInterface}} {
{{^isDelegate}}
return {{>returnValue}}
{{/isDelegate}}
{{#isDelegate}}
return getDelegate().{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}})
{{/isDelegate}}
}{{/skipDefaultInterface}}
{{/operation}}
}
{{/operations}}

View File

@ -0,0 +1,23 @@
package {{apiPackage}}
{{^reactive}}
import org.springframework.web.context.request.NativeWebRequest
import {{javaxPackage}}.servlet.http.HttpServletResponse
import java.io.IOException
{{/reactive}}
object ApiUtil {
{{^reactive}}
fun setExampleResponse(req: NativeWebRequest, contentType: String, example: String) {
try {
val res = req.getNativeResponse(HttpServletResponse::class.java)
res?.characterEncoding = "UTF-8"
res?.addHeader("Content-Type", contentType)
res?.writer?.print(example)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
{{/reactive}}
}

View File

@ -0,0 +1,38 @@
package {{package}}
{{#imports}}import {{import}}
{{/imports}}
import org.junit.jupiter.api.Test
{{#reactive}}
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.test.runBlockingTest
{{/reactive}}
import org.springframework.http.ResponseEntity
class {{classname}}Test {
{{#serviceInterface}}
private val service: {{classname}}Service = {{classname}}ServiceImpl()
{{/serviceInterface}}
private val api: {{classname}}Controller = {{classname}}Controller({{#serviceInterface}}service{{/serviceInterface}})
{{#operations}}
{{#operation}}
/**
* To test {{classname}}Controller.{{operationId}}
*
* @throws ApiException
* if the Api call fails
*/
@Test
fun {{operationId}}Test() {{#reactive}}= runBlockingTest {{/reactive}}{
{{#allParams}}
val {{{paramName}}}: {{>optionalDataType}} = TODO()
{{/allParams}}
val response: ResponseEntity<{{>returnTypes}}> = api.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
// TODO: test validations
}
{{/operation}}
{{/operations}}
}

View File

@ -0,0 +1,4 @@
{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}
@field:Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{!
}}{{^isContainer}}{{^isPrimitiveType}}{{^isNumber}}{{^isUuid}}{{^isDateTime}}
@field:Valid{{/isDateTime}}{{/isUuid}}{{/isNumber}}{{/isPrimitiveType}}{{/isContainer}}

View File

@ -0,0 +1,38 @@
{{!
format: email
}}{{#isEmail}}
@get:Email{{/isEmail}}{{!
pattern set
}}{{#pattern}}
@get:Pattern(regexp="{{{.}}}"){{/pattern}}{{!
minLength && maxLength set
}}{{#minLength}}{{#maxLength}}
@get:Size(min={{minLength}},max={{maxLength}}){{/maxLength}}{{/minLength}}{{!
minLength set, maxLength not
}}{{#minLength}}{{^maxLength}}
@get:Size(min={{minLength}}){{/maxLength}}{{/minLength}}{{!
minLength not set, maxLength set
}}{{^minLength}}{{#maxLength}}
@get:Size(max={{.}}){{/maxLength}}{{/minLength}}{{!
@Size: minItems && maxItems set
}}{{#minItems}}{{#maxItems}}
@get:Size(min={{minItems}},max={{maxItems}}) {{/maxItems}}{{/minItems}}{{!
@Size: minItems set, maxItems not
}}{{#minItems}}{{^maxItems}}
@get:Size(min={{minItems}}){{/maxItems}}{{/minItems}}{{!
@Size: minItems not set && maxItems set
}}{{^minItems}}{{#maxItems}}
@get:Size(max={{.}}){{/maxItems}}{{/minItems}}{{!
check for integer or long / all others=decimal type with @Decimal*
isInteger set
}}{{#isInteger}}{{#minimum}}
@get:Min({{.}}){{/minimum}}{{#maximum}}
@get:Max({{.}}){{/maximum}}{{/isInteger}}{{!
isLong set
}}{{#isLong}}{{#minimum}}
@get:Min({{.}}L){{/minimum}}{{#maximum}}
@get:Max({{.}}L){{/maximum}}{{/isLong}}{{!
Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}}
@get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}}
@get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}}

View File

@ -0,0 +1,22 @@
{{#isEmail}}@Email {{/isEmail}}{{!
pattern set
}}{{#pattern}}@Pattern(regexp="{{{.}}}") {{/pattern}}{{!
minLength && maxLength set
}}{{#minLength}}{{#maxLength}}@Size(min={{minLength}},max={{maxLength}}) {{/maxLength}}{{/minLength}}{{!
minLength set, maxLength not
}}{{#minLength}}{{^maxLength}}@Size(min={{minLength}}) {{/maxLength}}{{/minLength}}{{!
minLength not set, maxLength set
}}{{^minLength}}{{#maxLength}}@Size(max={{.}}) {{/maxLength}}{{/minLength}}{{!
@Size: minItems && maxItems set
}}{{#minItems}}{{#maxItems}}@Size(min={{minItems}},max={{maxItems}}) {{/maxItems}}{{/minItems}}{{!
@Size: minItems set, maxItems not
}}{{#minItems}}{{^maxItems}}@Size(min={{minItems}}) {{/maxItems}}{{/minItems}}{{!
@Size: minItems not set && maxItems set
}}{{^minItems}}{{#maxItems}}@Size(max={{.}}) {{/maxItems}}{{/minItems}}{{!
check for integer or long / all others=decimal type with @Decimal*
isInteger set
}}{{#isInteger}}{{#minimum}}@Min({{.}}){{/minimum}}{{#maximum}} @Max({{.}}) {{/maximum}}{{/isInteger}}{{!
isLong set
}}{{#isLong}}{{#minimum}}@Min({{.}}L){{/minimum}}{{#maximum}} @Max({{.}}L) {{/maximum}}{{/isLong}}{{!
Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}}@DecimalMin("{{.}}"){{/minimum}}{{#maximum}} @DecimalMax("{{.}}") {{/maximum}}{{/isLong}}{{/isInteger}}

View File

@ -0,0 +1 @@
{{! PathParam is always required, no @NotNull necessary }}{{>beanValidationPath}}

View File

@ -0,0 +1 @@
{{#required}}@NotNull {{/required}}{{>beanValidationPath}}

View File

@ -0,0 +1 @@
{{#isBodyParam}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"], defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"]){{/defaultValue}}{{/allowableValues}}{{/isContainer}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}, allowableValues = "{{{.}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @JsonOrFormBind {{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}

View File

@ -0,0 +1,33 @@
/**
* {{{description}}}
{{#vars}}
* @param {{name}} {{{description}}}
{{/vars}}
*/{{#discriminator}}
{{>typeInfoAnnotation}}{{/discriminator}}
{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}}(
{{#requiredVars}}
{{>dataClassReqVar}}{{^-last}},
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>dataClassOptVar}}{{^-last}},
{{/-last}}{{/optionalVars}}
) {{/discriminator}}{{#parent}}: {{{.}}}{{/parent}}{
{{#discriminator}}
{{#requiredVars}}
{{>interfaceReqVar}}
{{/requiredVars}}
{{#optionalVars}}
{{>interfaceOptVar}}
{{/optionalVars}}
{{/discriminator}}
{{#hasEnums}}{{#vars}}{{#isEnum}}
/**
* {{{description}}}
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
*/
enum class {{{nameInCamelCase}}}(val value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}) {
{{#allowableValues}}{{#enumVars}}
@JsonProperty({{{value}}}) {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
}
{{/isEnum}}{{/vars}}{{/hasEnums}}
}

View File

@ -0,0 +1,5 @@
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#deprecated}}
@Deprecated(message = ""){{/deprecated}}
@get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}

View File

@ -0,0 +1,4 @@
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}
@get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}

View File

@ -0,0 +1,8 @@
/**
* {{{description}}}
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
*/
enum class {{classname}}(val value: {{dataType}}) {
{{#allowableValues}}{{#enumVars}}
@JsonProperty({{{value}}}) {{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
}

View File

@ -0,0 +1,29 @@
package {{apiPackage}}
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import {{javaxPackage}}.servlet.http.HttpServletResponse
import {{javaxPackage}}.validation.ConstraintViolationException
// TODO Extend ApiException for custom exception handling, e.g. the below NotFound exception
sealed class ApiException(msg: String, val code: Int) : Exception(msg)
class NotFoundException(msg: String, code: Int = HttpStatus.NOT_FOUND.value()) : ApiException(msg, code)
@ControllerAdvice
class DefaultExceptionHandler {
@ExceptionHandler(value = [ApiException::class])
fun onApiException(ex: ApiException, response: HttpServletResponse): Unit =
response.sendError(ex.code, ex.message)
@ExceptionHandler(value = [NotImplementedError::class])
fun onNotImplemented(ex: NotImplementedError, response: HttpServletResponse): Unit =
response.sendError(HttpStatus.NOT_IMPLEMENTED.value())
@ExceptionHandler(value = [ConstraintViolationException::class])
fun onConstraintViolation(ex: ConstraintViolationException, response: HttpServletResponse): Unit =
response.sendError(HttpStatus.BAD_REQUEST.value(), ex.constraintViolations.joinToString(", ") { it.message })
}

View File

@ -0,0 +1 @@
{{#isFormParam}}{{^isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{paramName}}: {{>optionalDataType}} {{/isFile}}{{#isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "file detail"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "file detail"){{/swagger1AnnotationLibrary}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("file") {{baseName}}: {{>optionalDataType}}{{/isFile}}{{/isFormParam}}

View File

@ -0,0 +1 @@
@{{javaxPackage}}.annotation.Generated(value = ["{{generatorClass}}"]{{^hideGenerationTimestamp}}, date = "{{generatedDate}}"{{/hideGenerationTimestamp}})

View File

@ -0,0 +1 @@
{{#isHeaderParam}}{{#useBeanValidation}}{{>beanValidationPath}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}", `in` = ParameterIn.HEADER{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}) {{paramName}}: {{>optionalDataType}}{{/isHeaderParam}}

View File

@ -0,0 +1,91 @@
package {{basePackage}}
import org.springframework.context.annotation.Bean
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
{{#sourceDocumentationProvider}}
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.io.Resource
import org.springframework.util.StreamUtils
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.bind.annotation.GetMapping
{{/sourceDocumentationProvider}}
{{^sourceDocumentationProvider}}
{{#useSwaggerUI}}
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.bind.annotation.GetMapping
{{/useSwaggerUI}}
{{/sourceDocumentationProvider}}
{{#reactive}}
import org.springframework.web.reactive.function.server.HandlerFunction
import org.springframework.web.reactive.function.server.RequestPredicates.GET
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions.route
import org.springframework.web.reactive.function.server.ServerResponse
import java.net.URI
{{/reactive}}
{{#sourceDocumentationProvider}}
import java.nio.charset.Charset
{{/sourceDocumentationProvider}}
/**
* Home redirection to OpenAPI api documentation
*/
@Controller
class HomeController {
{{#useSwaggerUI}}
{{^springDocDocumentationProvider}}
{{#sourceDocumentationProvider}}
private val apiDocsPath = "/openapi.json"
{{/sourceDocumentationProvider}}
{{#springFoxDocumentationProvider}}
private val apiDocsPath = "/v2/api-docs"
{{/springFoxDocumentationProvider}}
{{/springDocDocumentationProvider}}
{{/useSwaggerUI}}
{{#sourceDocumentationProvider}}
private val yamlMapper = YAMLMapper()
@Value("classpath:/openapi.yaml")
private lateinit var openapi: Resource
@Bean
fun openapiContent(): String {
return openapi.inputStream.use {
StreamUtils.copyToString(it, Charset.defaultCharset())
}
}
@GetMapping(value = ["/openapi.yaml"], produces = ["application/vnd.oai.openapi"])
@ResponseBody
fun openapiYaml(): String = openapiContent()
@GetMapping(value = ["/openapi.json"], produces = ["application/json"])
@ResponseBody
fun openapiJson(): Any = yamlMapper.readValue(openapiContent(), Any::class.java)
{{/sourceDocumentationProvider}}
{{#useSwaggerUI}}
{{^springDocDocumentationProvider}}
@GetMapping(value = ["/swagger-config.yaml"], produces = ["text/plain"])
@ResponseBody
fun swaggerConfig(): String = "url: $apiDocsPath\n"
{{/springDocDocumentationProvider}}
{{#reactive}}
@Bean
fun index(): RouterFunction
<ServerResponse> = route(
GET("/"), HandlerFunction
<ServerResponse> {
ServerResponse.temporaryRedirect(URI.create("swagger-ui.html")).build()
})
{{/reactive}}
{{^reactive}}
@RequestMapping("/")
fun index(): String = "redirect:swagger-ui.html"
{{/reactive}}
{{/useSwaggerUI}}
}

View File

@ -0,0 +1,4 @@
{{#swagger2AnnotationLibrary}}
@get:Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}requiredMode = Schema.RequiredMode.REQUIRED, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@get:ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}
{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? {{^discriminator}}= {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/discriminator}}

View File

@ -0,0 +1,4 @@
{{#swagger2AnnotationLibrary}}
@get:Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}requiredMode = Schema.RequiredMode.REQUIRED, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@get:ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}
{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}

View File

@ -0,0 +1,21 @@
# {{title}}{{^title}}Generated Kotlin Spring Boot App{{/title}}
This Kotlin based [Spring Boot](https://spring.io/projects/spring-boot) application has been generated using the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator).
## Getting Started
This document assumes you have either maven or gradle available, either via the wrapper or otherwise. This does not come with a gradle / maven wrapper checked in.
By default a [`pom.xml`](pom.xml) file will be generated. If you specified `gradleBuildFile=true` when generating this project, a `build.gradle.kts` will also be generated. Note this uses [Gradle Kotlin DSL](https://github.com/gradle/kotlin-dsl).
To build the project using maven, run:
```bash
mvn package && java -jar target/{{artifactId}}-{{artifactVersion}}.jar
```
To build the project using gradle, run:
```bash
gradle build && java -jar build/libs/{{artifactId}}-{{artifactVersion}}.jar
```
If all builds successfully, the server should run on [http://localhost:8080/](http://localhost:{{serverPort}}/)

View File

@ -0,0 +1,10 @@
spring:
application:
name: {{title}}
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
server:
port: {{serverPort}}

View File

@ -0,0 +1,61 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
group = "{{groupId}}"
version = "{{artifactVersion}}"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
maven { url = uri("https://repo.spring.io/milestone") }
}
tasks.withType
<KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}
plugins {
val kotlinVersion = "1.7.10"
id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.springframework.boot") version "3.0.2"
id("io.spring.dependency-management") version "1.0.14.RELEASE"
}
dependencies {
{{#reactive}} val kotlinxCoroutinesVersion = "1.6.1"
{{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}}
implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}}
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.0.0-M5"){{/useSwaggerUI}}{{^useSwaggerUI}}
implementation("org.springdoc:springdoc-openapi-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}
-core:2.0.0-M5"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
implementation("org.webjars:swagger-ui:4.10.3")
implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
{{#useBeanValidation}}
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(module = "junit")
}
{{#reactive}}
testImplementation`("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
{{/reactive}}
}

View File

@ -0,0 +1,68 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.6.7")
}
}
group = "{{groupId}}"
version = "{{artifactVersion}}"
repositories {
mavenCentral()
}
tasks.withType
<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
plugins {
val kotlinVersion = "1.6.21"
id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.springframework.boot") version "2.6.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
}
dependencies {
{{#reactive}} val kotlinxCoroutinesVersion = "1.6.1"
{{/reactive}} compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}}
compile("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}}
compile("org.springframework.boot:spring-boot-starter-webflux")
compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
compile("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
compile("org.springdoc:springdoc-openapi-{{#reactive}}
webflux-{{/reactive}}ui:1.6.8"){{/useSwaggerUI}}{{^useSwaggerUI}}
compile("org.springdoc:springdoc-openapi-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}
-core:1.6.8"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
compile("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
compile("org.webjars:swagger-ui:4.10.3")
compile("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
compile("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
compile("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
compile("com.google.code.findbugs:jsr305:3.0.2")
compile("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
compile("com.fasterxml.jackson.module:jackson-module-kotlin")
{{#useBeanValidation}}
compile("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
compile("jakarta.annotation:jakarta.annotation-api:2.1.0")
testCompile("org.jetbrains.kotlin:kotlin-test-junit5")
testCompile("org.springframework.boot:spring-boot-starter-test") {
exclude(module = "junit")
}
{{#reactive}}
testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
{{/reactive}}
}

View File

@ -0,0 +1 @@
{{contextPath}}

View File

@ -0,0 +1,210 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{{groupId}}</groupId>
<artifactId>{{artifactId}}</artifactId>
<packaging>jar</packaging>
<name>{{artifactId}}</name>
<version>{{artifactVersion}}</version>
<properties>{{#reactive}}
<kotlinx-coroutines.version>1.6.1
</kotlinx-coroutines.version>{{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
<springdoc-openapi.version>2.0.2
</springdoc-openapi.version>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<springfox-swagger2.version>2.9.2
</springfox-swagger2.version>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<swagger-ui.version>4.15.5
</swagger-ui.version>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<swagger-annotations.version>1.6.6
</swagger-annotations.version>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<swagger-annotations.version>2.2.7
</swagger-annotations.version>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<findbugs-jsr305.version>3.0.2</findbugs-jsr305.version>
<jakarta-annotation.version>2.1.0</jakarta-annotation.version>
<kotlin-test-junit5.version>1.7.10</kotlin-test-junit5.version>
<kotlin.version>1.7.10</kotlin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
</parent>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>{{^interfaceOnly}}
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>{{/interfaceOnly}}
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
<jvmTarget>17</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>{{^reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>{{/reactive}}{{#reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>{{/reactive}}
{{#springDocDocumentationProvider}}
<!--SpringDoc dependencies -->{{#useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui
</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{^useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<!--SpringFox dependencies -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<!-- @Nullable annotation -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${findbugs-jsr305.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
{{#useBeanValidation}}
<!-- Bean Validation API support -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>{{/useBeanValidation}}
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>${kotlin-test-junit5.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,195 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{{groupId}}</groupId>
<artifactId>{{artifactId}}</artifactId>
<packaging>jar</packaging>
<name>{{artifactId}}</name>
<version>{{artifactVersion}}</version>
<properties>{{#reactive}}
<kotlinx-coroutines.version>1.6.1
</kotlinx-coroutines.version>{{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
<springdoc-openapi.version>1.6.8
</springdoc-openapi.version>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<springfox-swagger2.version>2.9.2
</springfox-swagger2.version>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<swagger-ui.version>4.10.3
</swagger-ui.version>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<swagger-annotations.version>1.6.6
</swagger-annotations.version>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<swagger-annotations.version>2.2.0
</swagger-annotations.version>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<findbugs-jsr305.version>3.0.2</findbugs-jsr305.version>
<jakarta-annotation.version>2.1.0</jakarta-annotation.version>
<kotlin-test-junit5.version>1.6.21</kotlin-test-junit5.version>
<kotlin.version>1.6.21</kotlin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
</parent>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>{{^interfaceOnly}}
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>{{/interfaceOnly}}
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>{{^reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>{{/reactive}}{{#reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>{{/reactive}}
{{#springDocDocumentationProvider}}
<!--SpringDoc dependencies -->{{#useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{^useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<!--SpringFox dependencies -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<!-- @Nullable annotation -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${findbugs-jsr305.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
{{#useBeanValidation}}
<!-- Bean Validation API support -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>{{/useBeanValidation}}
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>${kotlin-test-junit5.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,15 @@
pluginManagement {
repositories {
maven { url = uri("https://repo.spring.io/snapshot") }
maven { url = uri("https://repo.spring.io/milestone") }
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "org.springframework.boot") {
useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}")
}
}
}
}
rootProject.name = "{{artifactId}}"

View File

@ -0,0 +1,15 @@
package {{basePackage}}
import org.springframework.boot.runApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.ComponentScan
@SpringBootApplication
@ComponentScan(basePackages = ["{{basePackage}}", "{{apiPackage}}", "{{modelPackage}}"])
class Application
fun main(args: Array
<String>) {
runApplication
<Application>(*args)
}

View File

@ -0,0 +1,57 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="webjars/swagger-ui/swagger-ui.css"/>
<link rel="icon" type="image/png" href="webjars/swagger-ui/favicon-32x32.png" sizes="32x32"/>
<link rel="icon" type="image/png" href="webjars/swagger-ui/favicon-16x16.png" sizes="16x16"/>
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="webjars/swagger-ui/swagger-ui-bundle.js" charset="UTF-8"></script>
<script src="webjars/swagger-ui/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
<script>
window.onload = function () {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
configUrl: 'swagger-config.yaml',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>

View File

@ -0,0 +1,83 @@
{{^interfaceOnly}}
# {{artifactId}}
## Requirements
Building the API client library requires [Maven](https://maven.apache.org/) to be installed.
## Installation
To install the API client library to your local Maven repository, simply execute:
```shell
mvn install
```
To deploy it to a remote Maven repository instead, configure the settings of the repository and execute:
```shell
mvn deploy
```
Refer to the [official documentation](https://maven.apache.org/plugins/maven-deploy-plugin/usage.html) for more information.
### Maven users
Add this dependency to your project's POM:
```xml
<dependency>
<groupId>{{{groupId}}}</groupId>
<artifactId>{{{artifactId}}}</artifactId>
<version>{{{artifactVersion}}}</version>
<scope>compile</scope>
</dependency>
```
### Gradle users
Add this dependency to your project's build file:
```groovy
compile "{{{groupId}}}:{{{artifactId}}}:{{{artifactVersion}}}"
```
### Others
At first generate the JAR by executing:
mvn package
Then manually install the following JARs:
* target/{{{artifactId}}}-{{{artifactVersion}}}.jar
* target/lib/*.jar
{{/interfaceOnly}}
{{#interfaceOnly}}
# OpenAPI generated API stub
Spring Framework stub
## Overview
This code was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
By using the [OpenAPI-Spec](https://openapis.org), you can easily generate an API stub.
This is an example of building API stub interfaces in Java using the Spring framework.
The stubs generated can be used in your existing Spring-MVC or Spring-Boot application to create controller endpoints
by adding ```@Controller``` classes that implement the interface. Eg:
```java
@Controller
public class PetController implements PetApi {
// implement all PetApi methods
}
```
You can also use the interface to create [Spring-Cloud Feign clients](http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign-inheritance).Eg:
```java
@FeignClient(name="pet", url="http://petstore.swagger.io/v2")
public interface PetClient extends PetApi {
}
```
{{/interfaceOnly}}

View File

@ -0,0 +1,11 @@
package {{package}}
import org.springframework.cloud.openfeign.FeignClient
import {{configPackage}}.ClientConfiguration
@FeignClient(
name="\${{openbrace}}{{classVarName}}.name:{{classVarName}}{{closebrace}}",
{{#useFeignClientUrl}}url="\${{openbrace}}{{classVarName}}.url:{{basePath}}{{closebrace}}", {{/useFeignClientUrl}}
configuration = [ClientConfiguration::class]
)
interface {{classname}}Client : {{classname}}

View File

@ -0,0 +1,19 @@
package {{configPackage}}
import feign.RequestInterceptor
import feign.RequestTemplate
class ApiKeyRequestInterceptor(
private val location: String,
private val name: String,
private val value: String,
) : RequestInterceptor {
override fun apply(requestTemplate: RequestTemplate) {
if (location == "header") {
requestTemplate.header(name, value)
} else if (location == "query") {
requestTemplate.query(name, value)
}
}
}

View File

@ -0,0 +1,72 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
group = "{{groupId}}"
version = "{{artifactVersion}}"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
maven { url = uri("https://repo.spring.io/milestone") }
}
tasks.withType
<KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}
plugins {
val kotlinVersion = "1.7.10"
id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.springframework.boot") version "3.0.2"
id("io.spring.dependency-management") version "1.0.14.RELEASE"
}
tasks.getByName("bootJar") {
enabled = false
}
tasks.getByName("jar") {
enabled = true
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2021.0.5")
}
}
dependencies {
{{#reactive}} val kotlinxCoroutinesVersion = "1.6.1"
{{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}}
implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}}
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.0.0-M5"){{/useSwaggerUI}}{{^useSwaggerUI}}
implementation("org.springdoc:springdoc-openapi-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}
-core:2.0.0-M5"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
implementation("org.webjars:swagger-ui:4.10.3")
implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign"){{#hasAuthMethods}}
implementation("org.springframework.cloud:spring-cloud-starter-oauth2:2.2.5.RELEASE"){{/hasAuthMethods}}
{{#useBeanValidation}}
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
}

View File

@ -0,0 +1,79 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.6.7")
}
}
group = "{{groupId}}"
version = "{{artifactVersion}}"
repositories {
mavenCentral()
}
tasks.withType
<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
plugins {
val kotlinVersion = "1.6.21"
id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.springframework.boot") version "2.6.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
}
tasks.getByName("bootJar") {
enabled = false
}
tasks.getByName("jar") {
enabled = true
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2021.0.5")
}
}
dependencies {
{{#reactive}} val kotlinxCoroutinesVersion = "1.6.1"
{{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}}
implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}}
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
implementation("org.springdoc:springdoc-openapi-{{#reactive}}
webflux-{{/reactive}}ui:1.6.8"){{/useSwaggerUI}}{{^useSwaggerUI}}
implementation("org.springdoc:springdoc-openapi-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}
-core:1.6.8"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
implementation("org.webjars:swagger-ui:4.10.3")
implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign"){{#hasAuthMethods}}
implementation("org.springframework.cloud:spring-cloud-starter-oauth2:2.2.5.RELEASE"){{/hasAuthMethods}}
{{#useBeanValidation}}
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")
}

View File

@ -0,0 +1,132 @@
package {{configPackage}}
{{#authMethods}}
{{#isBasicBasic}}
import feign.auth.BasicAuthRequestInterceptor
{{/isBasicBasic}}
{{#-first}}
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
{{/-first}}
{{#isOAuth}}
import org.springframework.boot.context.properties.ConfigurationProperties
{{/isOAuth}}
{{/authMethods}}
import org.springframework.boot.context.properties.EnableConfigurationProperties
{{#authMethods}}
{{#-first}}
import org.springframework.context.annotation.Bean
{{/-first}}
{{/authMethods}}
import org.springframework.context.annotation.Configuration
{{#authMethods}}
{{#isOAuth}}
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext
import org.springframework.security.oauth2.client.OAuth2ClientContext
{{#isApplication}}
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails
{{/isApplication}}
{{#isCode}}
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails
{{/isCode}}
{{#isImplicit}}
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitResourceDetails
{{/isImplicit}}
{{#isPassword}}
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails
{{/isPassword}}
{{/isOAuth}}
{{/authMethods}}
@Configuration
@EnableConfigurationProperties
class ClientConfiguration {
{{#authMethods}}
{{#isBasicBasic}}
@Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.username:{{closebrace}}")
private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Username: String
@Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.password:{{closebrace}}")
private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Password: String
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.username")
fun {{{name}}}RequestInterceptor(): BasicAuthRequestInterceptor {
return BasicAuthRequestInterceptor(this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Username, this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Password)
}
{{/isBasicBasic}}
{{#isApiKey}}
@Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.key:{{closebrace}}")
private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Key: String
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.key")
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(): ApiKeyRequestInterceptor {
return ApiKeyRequestInterceptor({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{{keyParamName}}}", this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Key)
}
{{/isApiKey}}
{{#isOAuth}}
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(oAuth2ClientContext: OAuth2ClientContext): OAuth2FeignRequestInterceptor {
return OAuth2FeignRequestInterceptor(oAuth2ClientContext, {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails())
}
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
fun oAuth2ClientContext(): OAuth2ClientContext {
return DefaultOAuth2ClientContext()
}
{{#isCode}}
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
@ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}")
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): AuthorizationCodeResourceDetails {
val details = AuthorizationCodeResourceDetails()
details.accessTokenUri = "{{{tokenUrl}}}"
details.userAuthorizationUri = "{{{authorizationUrl}}}"
return details
}
{{/isCode}}
{{#isPassword}}
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
@ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}")
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ResourceOwnerPasswordResourceDetails {
val details = ResourceOwnerPasswordResourceDetails()
details.accessTokenUri = "{{{tokenUrl}}}"
return details
}
{{/isPassword}}
{{#isApplication}}
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
@ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}")
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ClientCredentialsResourceDetails {
val details = ClientCredentialsResourceDetails()
details.accessTokenUri = "{{{tokenUrl}}}"
return details
}
{{/isApplication}}
{{#isImplicit}}
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
@ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}")
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ImplicitResourceDetails {
val details = ImplicitResourceDetails()
details.userAuthorizationUri= "{{{authorizationUrl}}}"
return details
}
{{/isImplicit}}
{{/isOAuth}}
{{/authMethods}}
}

View File

@ -0,0 +1,233 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{{groupId}}</groupId>
<artifactId>{{artifactId}}</artifactId>
<packaging>jar</packaging>
<name>{{artifactId}}</name>
<version>{{artifactVersion}}</version>
<properties>{{#reactive}}
<kotlinx-coroutines.version>1.6.1
</kotlinx-coroutines.version>{{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
<springdoc-openapi.version>2.0.2
</springdoc-openapi.version>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<springfox-swagger2.version>2.9.2
</springfox-swagger2.version>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<swagger-ui.version>4.15.5
</swagger-ui.version>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<swagger-annotations.version>1.6.6
</swagger-annotations.version>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<swagger-annotations.version>2.2.7
</swagger-annotations.version>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<findbugs-jsr305.version>3.0.2</findbugs-jsr305.version>
<jakarta-annotation.version>2.1.0</jakarta-annotation.version>
<kotlin-test-junit5.version>1.7.10</kotlin-test-junit5.version>
<kotlin.version>1.7.10</kotlin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>2021.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<plugins>{{^interfaceOnly}}
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>{{/interfaceOnly}}
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
<jvmTarget>17</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>{{^reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>{{/reactive}}{{#reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>{{/reactive}}
{{#springDocDocumentationProvider}}
<!--SpringDoc dependencies -->{{#useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-{{#reactive}}
webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui
</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{^useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<!--SpringFox dependencies -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<!-- @Nullable annotation -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${findbugs-jsr305.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
{{#hasAuthMethods}}
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
{{^parentOverridden}}
<version>2.2.5.RELEASE</version>
{{/parentOverridden}}
</dependency>
{{/hasAuthMethods}}
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
{{#useBeanValidation}}
<!-- Bean Validation API support -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>{{/useBeanValidation}}
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>${kotlin-test-junit5.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,218 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{{groupId}}</groupId>
<artifactId>{{artifactId}}</artifactId>
<packaging>jar</packaging>
<name>{{artifactId}}</name>
<version>{{artifactVersion}}</version>
<properties>{{#reactive}}
<kotlinx-coroutines.version>1.6.1
</kotlinx-coroutines.version>{{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
<springdoc-openapi.version>1.6.8
</springdoc-openapi.version>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<springfox-swagger2.version>2.9.2
</springfox-swagger2.version>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<swagger-ui.version>4.10.3
</swagger-ui.version>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<swagger-annotations.version>1.6.6
</swagger-annotations.version>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<swagger-annotations.version>2.2.0
</swagger-annotations.version>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<findbugs-jsr305.version>3.0.2</findbugs-jsr305.version>
<jakarta-annotation.version>2.1.0</jakarta-annotation.version>
<kotlin-test-junit5.version>1.6.21</kotlin-test-junit5.version>
<kotlin.version>1.6.21</kotlin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>2021.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<plugins>{{^interfaceOnly}}
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>{{/interfaceOnly}}
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>{{^reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>{{/reactive}}{{#reactive}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>{{/reactive}}
{{#springDocDocumentationProvider}}
<!--SpringDoc dependencies -->{{#useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{^useSwaggerUI}}
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>{{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}}
<!--SpringFox dependencies -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>{{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}}
<!-- @Nullable annotation -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${findbugs-jsr305.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
{{#hasAuthMethods}}
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
{{^parentOverridden}}
<version>2.2.5.RELEASE</version>
{{/parentOverridden}}
</dependency>
{{/hasAuthMethods}}
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
{{#useBeanValidation}}
<!-- Bean Validation API support -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>{{/useBeanValidation}}
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>${kotlin-test-junit5.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,15 @@
pluginManagement {
repositories {
maven { url = uri("https://repo.spring.io/snapshot") }
maven { url = uri("https://repo.spring.io/milestone") }
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "org.springframework.boot") {
useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}")
}
}
}
}
rootProject.name = "{{artifactId}}"

View File

@ -0,0 +1,28 @@
{{^reactive}}
{{#examples}}
{{#-first}}
{{#async}}
return CompletableFuture.supplyAsync(()-> {
{{/async}}getRequest().ifPresent { request ->
{{#async}} {{/async}} for (mediaType in MediaType.parseMediaTypes(request.getHeader("Accept"))) {
{{/-first}}
{{#async}} {{/async}}{{^async}} {{/async}} if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) {
{{#async}} {{/async}}{{^async}} {{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{example}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}")
{{#async}} {{/async}}{{^async}} {{/async}} break
{{#async}} {{/async}}{{^async}} {{/async}} }
{{#-last}}
{{#async}} {{/async}}{{^async}} {{/async}} }
{{#async}} {{/async}} }
{{#async}} {{/async}} return ResponseEntity({{#returnSuccessCode}}HttpStatus.valueOf({{{statusCode}}}){{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}})
{{#async}}
}, Runnable::run)
{{/async}}
{{/-last}}
{{/examples}}
{{^examples}}
return {{#async}}CompletableFuture.completedFuture({{/async}}ResponseEntity({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}})
{{/examples}}
{{/reactive}}
{{#reactive}}
return ResponseEntity({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}})
{{/reactive}}

28
templates/model.mustache Normal file
View File

@ -0,0 +1,28 @@
package {{package}}
import java.util.Objects
{{#imports}}import {{import}}
{{/imports}}
{{#useBeanValidation}}
import {{javaxPackage}}.validation.constraints.DecimalMax
import {{javaxPackage}}.validation.constraints.DecimalMin
import {{javaxPackage}}.validation.constraints.Email
import {{javaxPackage}}.validation.constraints.Max
import {{javaxPackage}}.validation.constraints.Min
import {{javaxPackage}}.validation.constraints.NotNull
import {{javaxPackage}}.validation.constraints.Pattern
import {{javaxPackage}}.validation.constraints.Size
import {{javaxPackage}}.validation.Valid
{{/useBeanValidation}}
{{#swagger2AnnotationLibrary}}
import io.swagger.v3.oas.annotations.media.Schema
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
import io.swagger.annotations.ApiModelProperty
{{/swagger1AnnotationLibrary}}
{{#models}}
{{#model}}
{{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{>dataClass}}{{/isEnum}}
{{/model}}
{{/models}}

View File

@ -0,0 +1 @@
{{#modelMutable}}var{{/modelMutable}}{{^modelMutable}}val{{/modelMutable}}

View File

@ -0,0 +1 @@
{{{openapi-yaml}}}

View File

@ -0,0 +1 @@
{{#required}}{{{dataType}}}{{/required}}{{^required}}{{#defaultValue}}{{{dataType}}}{{/defaultValue}}{{^defaultValue}}{{{dataType}}}?{{/defaultValue}}{{/required}}

View File

@ -0,0 +1 @@
{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeDoubleQuote}}{{{value}}}{{/lambdaEscapeDoubleQuote}}"{{^-last}}, {{/-last}}{{/enumVars}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeDoubleQuote}}{{{value}}}{{/lambdaEscapeDoubleQuote}}"{{^-last}}, {{/-last}}{{/enumVars}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#enumVars}}{{#lambda.escapeDoubleQuote}}{{{value}}}{{/lambda.escapeDoubleQuote}}{{^-last}}, {{/-last}}{{/enumVars}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @PathVariable("{{baseName}}") {{paramName}}: {{>optionalDataType}}{{/isPathParam}}

View File

@ -0,0 +1 @@
{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/swagger1AnnotationLibrary}}{{#useBeanValidation}} @Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{paramName}}: {{>optionalDataType}}{{/isQueryParam}}

View File

@ -0,0 +1,2 @@
{{#isMap}}Map
<String, {{{returnType}}}>{{/isMap}}{{#isArray}}{{#reactive}}Flow{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}}

View File

@ -0,0 +1 @@
{{#serviceInterface}}ResponseEntity(service.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}), {{#responses}}{{#-first}}HttpStatus.valueOf({{code}}){{/-first}}{{/responses}}){{/serviceInterface}}{{^serviceInterface}}ResponseEntity(HttpStatus.NOT_IMPLEMENTED){{/serviceInterface}}

View File

@ -0,0 +1,36 @@
package {{package}}
{{#imports}}import {{import}}
{{/imports}}
{{#reactive}}
import kotlinx.coroutines.flow.Flow
{{/reactive}}
{{#operations}}
interface {{classname}}Service {
{{#operation}}
/**
* {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
{{#notes}}
* {{.}}
{{/notes}}
*
{{#allParams}}
* @param {{{paramName}}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
{{/allParams}}
* @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
* or {{/-last}}{{/responses}}
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
* @see {{classname}}#{{operationId}}
*/
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}}
{{/operation}}
}
{{/operations}}

View File

@ -0,0 +1,19 @@
package {{package}}
{{#imports}}import {{import}}
{{/imports}}
{{#reactive}}
import kotlinx.coroutines.flow.Flow
{{/reactive}}
import org.springframework.stereotype.Service
@Service
{{#operations}}
class {{classname}}ServiceImpl : {{classname}}Service {
{{#operation}}
override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{paramName}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} {
TODO("Implement me")
}
{{/operation}}
}
{{/operations}}

View File

@ -0,0 +1,53 @@
package {{basePackage}}
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.info.License
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.security.SecurityScheme
@Configuration
class SpringDocConfiguration {
@Bean
fun apiInfo(): OpenAPI {
return OpenAPI()
.info(
Info(){{#appName}}
.title("{{appName}}"){{/appName}}
.description("{{{appDescription}}}"){{#termsOfService}}
.termsOfService("{{termsOfService}}"){{/termsOfService}}{{#openAPI}}{{#info}}{{#contact}}
.contact(
Contact(){{#infoName}}
.name("{{infoName}}"){{/infoName}}{{#infoUrl}}
.url("{{infoUrl}}"){{/infoUrl}}{{#infoEmail}}
.email("{{infoEmail}}"){{/infoEmail}}
){{/contact}}{{#license}}
.license(
License()
{{#licenseInfo}}.name("{{licenseInfo}}")
{{/licenseInfo}}{{#licenseUrl}}.url("{{licenseUrl}}")
{{/licenseUrl}}
){{/license}}{{/info}}{{/openAPI}}
.version("{{appVersion}}")
){{#hasAuthMethods}}
.components(
Components(){{#authMethods}}
.addSecuritySchemes("{{name}}", SecurityScheme(){{#isBasic}}
.type(SecurityScheme.Type.HTTP)
.scheme("{{scheme}}"){{#bearerFormat}}
.bearerFormat("{{bearerFormat}}"){{/bearerFormat}}{{/isBasic}}{{#isApiKey}}
.type(SecurityScheme.Type.APIKEY){{#isKeyInHeader}}
.`in`(SecurityScheme.In.HEADER){{/isKeyInHeader}}{{#isKeyInQuery}}
.`in`(SecurityScheme.In.QUERY){{/isKeyInQuery}}{{#isKeyInCookie}}
.`in`(SecurityScheme.In.COOKIE){{/isKeyInCookie}}
.name("{{keyParamName}}"){{/isApiKey}}{{#isOAuth}}
.type(SecurityScheme.Type.OAUTH2){{/isOAuth}}
){{/authMethods}}
){{/hasAuthMethods}}
}
}

View File

@ -0,0 +1,66 @@
package {{basePackage}}
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.util.UriComponentsBuilder
import springfox.documentation.builders.ApiInfoBuilder
import springfox.documentation.builders.RequestHandlerSelectors
import springfox.documentation.service.ApiInfo
import springfox.documentation.service.Contact
import springfox.documentation.spi.DocumentationType
import springfox.documentation.spring.web.paths.Paths
import springfox.documentation.spring.web.paths.RelativePathProvider
import springfox.documentation.spring.web.plugins.Docket
import springfox.documentation.swagger2.annotations.EnableSwagger2
import {{javaxPackage}}.servlet.ServletContext
{{>generatedAnnotation}}
@Configuration
@EnableSwagger2
class SpringFoxConfiguration {
fun apiInfo(): ApiInfo {
return ApiInfoBuilder()
.title("{{appName}}")
.description("{{{appDescription}}}")
.license("{{licenseInfo}}")
.licenseUrl("{{licenseUrl}}")
.termsOfServiceUrl("{{infoUrl}}")
.version("{{appVersion}}")
.contact(Contact("", "", "{{infoEmail}}"))
.build()
}
@Bean
{{=<% %>=}}
fun customImplementation(servletContext: ServletContext, @Value("\${openapi.<%title%>.base-path:<%>defaultBasePath%>}") basePath: String): Docket {
<%={{ }}=%>
return Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("{{apiPackage}}"))
.build()
.pathProvider(BasePathAwareRelativePathProvider(servletContext, basePath))
.directModelSubstitute(java.time.LocalDate::class.java, java.sql.Date::class.java)
.directModelSubstitute(java.time.OffsetDateTime::class.java, java.util.Date::class.java)
.apiInfo(apiInfo())
}
class BasePathAwareRelativePathProvider(servletContext: ServletContext, private val basePath: String) :
RelativePathProvider(servletContext) {
override fun applicationPath(): String {
return Paths.removeAdjacentForwardSlashes(
UriComponentsBuilder.fromPath(super.applicationPath()).path(basePath).build().toString()
)
}
override fun getOperationPath(operationPath: String): String {
val uriComponentsBuilder = UriComponentsBuilder.fromPath("/")
return Paths.removeAdjacentForwardSlashes(
uriComponentsBuilder.path(operationPath.replaceFirst("^$basePath", "")).build().toString()
)
}
}
}

View File

@ -0,0 +1,8 @@
{{#jackson}}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true)
@JsonSubTypes(
{{#discriminator.mappedModels}}
JsonSubTypes.Type(value = {{modelName}}::class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"){{^-last}},{{/-last}}
{{/discriminator.mappedModels}}
){{/jackson}}