Merge pull request 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
build.gradle.ktsdetekt.yml
src
templates

View File

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

View File

@ -4,6 +4,7 @@ build:
Indentation: 0 Indentation: 0
MagicNumber: 0 MagicNumber: 0
InjectDispatcher: 0 InjectDispatcher: 0
EnumEntryNameCase: 0
style: style:
ClassOrdering: ClassOrdering:
@ -161,3 +162,7 @@ potential-bugs:
HasPlatformType: HasPlatformType:
active: false 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.security.interfaces.RSAPublicKey
import java.util.* import java.util.*
@EnableWebSecurity(debug = true) @EnableWebSecurity(debug = false)
@Configuration @Configuration
class SecurityConfig { class SecurityConfig {

View File

@ -2,7 +2,9 @@ package dev.usbharu.hideout.config
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.web.filter.CommonsRequestLoggingFilter
import java.net.URL import java.net.URL
@Configuration @Configuration
@ -13,6 +15,17 @@ class SpringConfig {
@Autowired @Autowired
lateinit var storageConfig: StorageConfig 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") @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.controller.mastodon.generated.StatusApi
import dev.usbharu.hideout.domain.mastodon.model.generated.Status 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 dev.usbharu.hideout.service.api.mastodon.StatusesApiService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.springframework.http.HttpStatus 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.core.context.SecurityContextHolder
import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ModelAttribute
@Controller @Controller
class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi { class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
override fun apiV1StatusesPost(@ModelAttribute statusesRequest: StatusesRequest): ResponseEntity<Status> = override fun apiV1StatusesPost(devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest): ResponseEntity<Status> {
runBlocking { return runBlocking {
val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt
ResponseEntity( ResponseEntity(
statusesApiService.postStatus(statusesRequest, jwt.getClaim<String>("uid").toLong()), statusesApiService.postStatus(
devUsbharuHideoutDomainModelMastodonStatusesRequest,
jwt.getClaim<String>("uid").toLong()
),
HttpStatus.OK 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 { open class Note : Object {
var attributedTo: String? = null var attributedTo: String? = null
var attachment: List<Document> = emptyList()
var content: String? = null var content: String? = null
var published: String? = null var published: String? = null
var to: List<String> = emptyList() var to: List<String> = emptyList()
@ -22,7 +23,8 @@ open class Note : Object {
to: List<String> = emptyList(), to: List<String> = emptyList(),
cc: List<String> = emptyList(), cc: List<String> = emptyList(),
sensitive: Boolean = false, sensitive: Boolean = false,
inReplyTo: String? = null inReplyTo: String? = null,
attachment: List<Document> = emptyList()
) : super( ) : super(
type = add(type, "Note"), type = add(type, "Note"),
name = name, name = name,
@ -35,6 +37,7 @@ open class Note : Object {
this.cc = cc this.cc = cc
this.sensitive = sensitive this.sensitive = sensitive
this.inReplyTo = inReplyTo this.inReplyTo = inReplyTo
this.attachment = attachment
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -42,23 +45,34 @@ open class Note : Object {
if (other !is Note) return false if (other !is Note) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
if (id != other.id) return false
if (attributedTo != other.attributedTo) return false if (attributedTo != other.attributedTo) return false
if (attachment != other.attachment) return false
if (content != other.content) return false if (content != other.content) return false
if (published != other.published) 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 { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + (attributedTo?.hashCode() ?: 0) result = 31 * result + (attributedTo?.hashCode() ?: 0)
result = 31 * result + attachment.hashCode()
result = 31 * result + (content?.hashCode() ?: 0) result = 31 * result + (content?.hashCode() ?: 0)
result = 31 * result + (published?.hashCode() ?: 0) result = 31 * result + (published?.hashCode() ?: 0)
result = 31 * result + to.hashCode() result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode()
result = 31 * result + sensitive.hashCode()
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
return result return result
} }
override fun toString(): String = override fun toString(): String {
"Note(id=$id, attributedTo=$attributedTo, content=$content, published=$published, to=$to) ${super.toString()}" 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.Service -> TODO()
ExtendedActivityVocabulary.Article -> TODO() ExtendedActivityVocabulary.Article -> TODO()
ExtendedActivityVocabulary.Audio -> TODO() ExtendedActivityVocabulary.Audio -> TODO()
ExtendedActivityVocabulary.Document -> TODO() ExtendedActivityVocabulary.Document -> p.codec.treeToValue(treeNode, Document::class.java)
ExtendedActivityVocabulary.Event -> TODO() ExtendedActivityVocabulary.Event -> TODO()
ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java) ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java)
ExtendedActivityVocabulary.Page -> TODO() ExtendedActivityVocabulary.Page -> TODO()

View File

@ -8,5 +8,6 @@ data class PostCreateDto(
val visibility: Visibility = Visibility.PUBLIC, val visibility: Visibility = Visibility.PUBLIC,
val repostId: Long? = null, val repostId: Long? = null,
val repolyId: 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 repostId: Long? = null,
val replyId: Long? = null, val replyId: Long? = null,
val sensitive: Boolean = false, val sensitive: Boolean = false,
val apId: String = url val apId: String = url,
val mediaIds: List<Long> = emptyList()
) { ) {
companion object { companion object {
@Suppress("FunctionMinLength", "LongParameterList") @Suppress("FunctionMinLength", "LongParameterList")
@ -28,7 +29,8 @@ data class Post private constructor(
repostId: Long? = null, repostId: Long? = null,
replyId: Long? = null, replyId: Long? = null,
sensitive: Boolean = false, sensitive: Boolean = false,
apId: String = url apId: String = url,
mediaIds: List<Long> = emptyList()
): Post { ): Post {
val characterLimit = Config.configData.characterLimit val characterLimit = Config.configData.characterLimit
@ -67,7 +69,8 @@ data class Post private constructor(
repostId = repostId, repostId = repostId,
replyId = replyId, replyId = replyId,
sensitive = sensitive, 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 post: Prop<DeliverPostJob, String> = string("post")
val actor: Prop<DeliverPostJob, String> = string("actor") val actor: Prop<DeliverPostJob, String> = string("actor")
val inbox: Prop<DeliverPostJob, String> = string("inbox") val inbox: Prop<DeliverPostJob, String> = string("inbox")
val media: Prop<DeliverPostJob, String> = string("media")
} }
@Component @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.domain.model.hideout.entity.Post
import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Posts import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.PostsMedia
import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.util.singleOr import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
class PostQueryServiceImpl : PostQueryService { class PostQueryServiceImpl : PostQueryService {
override suspend fun findById(id: Long): Post = 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() .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 } override suspend fun findByUrl(url: String): Post =
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }.toPost() 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 } override suspend fun findByApId(string: String): Post =
.singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) }.toPost() 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 package dev.usbharu.hideout.query.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Account 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.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.repository.Posts import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.repository.Users import dev.usbharu.hideout.repository.*
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@ -12,67 +14,21 @@ import java.time.Instant
@Repository @Repository
class StatusQueryServiceImpl : StatusQueryService { class StatusQueryServiceImpl : StatusQueryService {
@Suppress("LongMethod") @Suppress("LongMethod")
override suspend fun findByPostIds(ids: List<Long>): List<Status> { override suspend fun findByPostIds(ids: List<Long>): List<Status> = findByPostIdsWithMediaAttachments(ids)
val pairs = Posts.innerJoin(Users, onColumn = { userId }, otherColumn = { id })
@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 } .select { Posts.id inList ids }
.map { .map {
Status( toStatus(it) to it[Posts.repostId]
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]
} }
return resolveReplyAndRepost(pairs)
}
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
val statuses = pairs.map { it.first } val statuses = pairs.map { it.first }
return pairs return pairs
.map { .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 Media.id eq id
} }
} }
}
fun ResultRow.toMedia(): EntityMedia { fun ResultRow.toMedia(): EntityMedia {
return EntityMedia( return EntityMedia(
id = this[Media.id], id = this[Media.id],
name = this[Media.name], name = this[Media.name],
url = this[Media.url], url = this[Media.url],
remoteUrl = this[Media.remoteUrl], remoteUrl = this[Media.remoteUrl],
thumbnailUrl = this[Media.thumbnailUrl], thumbnailUrl = this[Media.thumbnailUrl],
type = FileType.values().first { it.ordinal == this[Media.type] }, type = FileType.values().first { it.ordinal == this[Media.type] },
blurHash = this[Media.blurhash], blurHash = this[Media.blurhash],
) )
}
} }
object Media : Table("media") { object Media : Table("media") {
@ -77,4 +77,5 @@ object Media : Table("media") {
val thumbnailUrl = varchar("thumbnail_url", 255).nullable() val thumbnailUrl = varchar("thumbnail_url", 255).nullable()
val type = integer("type") val type = integer("type")
val blurhash = varchar("blurhash", 255).nullable() 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[sensitive] = post.sensitive
it[apId] = post.apId it[apId] = post.apId
} }
PostsMedia.batchInsert(post.mediaIds) {
this[PostsMedia.postId] = post.id
this[PostsMedia.mediaId] = it
}
} else { } 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 }) { Posts.update({ Posts.id eq post.id }) {
it[userId] = post.userId it[userId] = post.userId
it[overview] = post.overview it[overview] = post.overview
@ -46,8 +57,12 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos
return post return post
} }
override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.singleOrNull()?.toPost() override suspend fun findById(id: Long): Post =
?: throw FailedToGetResourcesException("id: $id was not found.") 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) { override suspend fun delete(id: Long) {
Posts.deleteWhere { Posts.id eq id } Posts.deleteWhere { Posts.id eq id }
@ -69,6 +84,12 @@ object Posts : Table() {
override val primaryKey: PrimaryKey = PrimaryKey(id) 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 { fun ResultRow.toPost(): Post {
return Post.of( return Post.of(
id = this[Posts.id], id = this[Posts.id],
@ -81,6 +102,12 @@ fun ResultRow.toPost(): Post {
repostId = this[Posts.repostId], repostId = this[Posts.repostId],
replyId = this[Posts.replyId], replyId = this[Posts.replyId],
sensitive = this[Posts.sensitive], 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 com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.ApplicationConfig import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.ap.Create 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.ap.Note
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility 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.getAp
import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.plugins.postAp
import dev.usbharu.hideout.query.FollowerQueryService import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.MediaQueryService
import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.PostRepository import dev.usbharu.hideout.repository.PostRepository
@ -46,6 +48,7 @@ class APNoteServiceImpl(
private val userQueryService: UserQueryService, private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService, private val followerQueryService: FollowerQueryService,
private val postQueryService: PostQueryService, private val postQueryService: PostQueryService,
private val mediaQueryService: MediaQueryService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper, @Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val applicationConfig: ApplicationConfig, private val applicationConfig: ApplicationConfig,
private val postService: PostService private val postService: PostService
@ -62,11 +65,13 @@ class APNoteServiceImpl(
val followers = followerQueryService.findFollowersById(post.userId) val followers = followerQueryService.findFollowersById(post.userId)
val userEntity = userQueryService.findById(post.userId) val userEntity = userQueryService.findById(post.userId)
val note = objectMapper.writeValueAsString(post) val note = objectMapper.writeValueAsString(post)
val mediaList = objectMapper.writeValueAsString(mediaQueryService.findByPostId(post.id))
followers.forEach { followerEntity -> followers.forEach { followerEntity ->
jobQueueParentService.schedule(DeliverPostJob) { jobQueueParentService.schedule(DeliverPostJob) {
props[DeliverPostJob.actor] = userEntity.url props[DeliverPostJob.actor] = userEntity.url
props[DeliverPostJob.post] = note props[DeliverPostJob.post] = note
props[DeliverPostJob.inbox] = followerEntity.inbox props[DeliverPostJob.inbox] = followerEntity.inbox
props[DeliverPostJob.media] = mediaList
} }
} }
} }
@ -74,13 +79,19 @@ class APNoteServiceImpl(
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) { override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor] val actor = props[DeliverPostJob.actor]
val postEntity = objectMapper.readValue<Post>(props[DeliverPostJob.post]) 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( val note = Note(
name = "Note", name = "Note",
id = postEntity.url, id = postEntity.url,
attributedTo = actor, attributedTo = actor,
content = postEntity.text, content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(), 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] val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
@ -168,6 +179,7 @@ class APNoteServiceImpl(
postQueryService.findByUrl(it) postQueryService.findByUrl(it)
} }
// TODO: リモートのメディア処理を追加
postService.createRemote( postService.createRemote(
Post.of( Post.of(
id = postRepository.generateId(), id = postRepository.generateId(),

View File

@ -1,12 +1,15 @@
package dev.usbharu.hideout.service.api.mastodon 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.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.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility 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.exception.FailedToGetResourcesException
import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.MediaRepository
import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.mastodon.AccountService import dev.usbharu.hideout.service.mastodon.AccountService
import dev.usbharu.hideout.service.post.PostService import dev.usbharu.hideout.service.post.PostService
@ -15,7 +18,10 @@ import java.time.Instant
@Service @Service
interface StatusesApiService { 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 @Service
@ -24,11 +30,16 @@ class StatsesApiServiceImpl(
private val accountService: AccountService, private val accountService: AccountService,
private val postQueryService: PostQueryService, private val postQueryService: PostQueryService,
private val userQueryService: UserQueryService, private val userQueryService: UserQueryService,
private val mediaRepository: MediaRepository,
private val transaction: Transaction private val transaction: Transaction
) : ) :
StatusesApiService { StatusesApiService {
@Suppress("LongMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
override suspend fun postStatus(statusesRequest: StatusesRequest, userId: Long): Status = transaction.transaction { 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) { val visibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Visibility.PUBLIC StatusesRequest.Visibility.public -> Visibility.PUBLIC
StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED
@ -40,10 +51,11 @@ class StatsesApiServiceImpl(
val post = postService.createLocal( val post = postService.createLocal(
PostCreateDto( PostCreateDto(
text = statusesRequest.status.orEmpty(), text = statusesRequest.status.orEmpty(),
overview = statusesRequest.spoilerText, overview = statusesRequest.spoiler_text,
visibility = visibility, visibility = visibility,
repolyId = statusesRequest.inReplyToId?.toLongOrNull(), repolyId = statusesRequest.in_reply_to_id?.toLongOrNull(),
userId = userId userId = userId,
mediaIds = statusesRequest.media_ids.map { it.toLong() }
) )
) )
val account = accountService.findById(userId) val account = accountService.findById(userId)
@ -66,6 +78,27 @@ class StatsesApiServiceImpl(
null 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( Status(
id = post.id.toString(), id = post.id.toString(),
uri = post.apId, uri = post.apId,
@ -75,7 +108,7 @@ class StatsesApiServiceImpl(
visibility = postVisibility, visibility = postVisibility,
sensitive = post.sensitive, sensitive = post.sensitive,
spoilerText = post.overview.orEmpty(), spoilerText = post.overview.orEmpty(),
mediaAttachments = emptyList(), mediaAttachments = mediaAttachment,
mentions = emptyList(), mentions = emptyList(),
tags = emptyList(), tags = emptyList(),
emojis = emptyList(), emojis = emptyList(),
@ -87,7 +120,7 @@ class StatsesApiServiceImpl(
inReplyToAccountId = replyUser?.toString(), inReplyToAccountId = replyUser?.toString(),
language = null, language = null,
text = post.text, 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 import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia
@Service @Service
@Suppress("TooGenericExceptionCaught")
class MediaServiceImpl( class MediaServiceImpl(
private val mediaDataStore: MediaDataStore, private val mediaDataStore: MediaDataStore,
private val fileTypeDeterminationService: FileTypeDeterminationService, private val fileTypeDeterminationService: FileTypeDeterminationService,

View File

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

View File

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

View File

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

View File

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

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}}